Willy, I know it's late in the cycle, but I don't expect a new converter to break anything unrelated.
I also made sure to add a very exhaustive reg-test verifying the correct behavior of the new converter to make sure that it does not need any last minute fixes. This time I also made sure to specify a proper value for REQUIRE_VERSION to avoid you needing to fix up my reg-tests, using new features within reg-tests is just too tempting :-/ Best regards Tim Düsterhus Apply with `git am --scissors` to automatically cut the commit message. -- >8 -- memcmp compares two binary strings in constant time. --- doc/configuration.txt | 21 +++++ reg-tests/converter/memcmp.vtc | 137 +++++++++++++++++++++++++++++++++ src/sample.c | 57 +++++++++++++- 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 reg-tests/converter/memcmp.vtc diff --git a/doc/configuration.txt b/doc/configuration.txt index 15cbe889c..eacd52fd2 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -15007,6 +15007,24 @@ map_<match_type>_<output_type>(<map_file>[,<default_value>]) | `---------------------------- key `------------------------------------ leading spaces ignored +memcmp(<var>) + Compares the contents of <var> with the input value. Both values are treated + as a binary string. Returns a boolean indicating whether both binary strings + match. + + If both binary strings have the same length then the comparison will be + performed in constant time. + + Please note that this converter is only available when haproxy has been + compiled with USE_OPENSSL. + + Example : + + http-request set-var(txn.token) hdr(token) + # Check whether the token sent by the client matches the secret token + # value, without leaking the contents using a timing attack. + acl token_given str(my_secret_token),memcmp(txn.token) + mod(<value>) Divides the input value of type signed integer by <value>, and returns the remainder as an signed integer. If <value> is null, then zero is returned. @@ -15190,6 +15208,9 @@ strcmp(<var>) than 0 otherwise (right string greater than left string or the right string is shorter). + See also the memcmp converter if you need to compare two binary strings in + constant time. + Example : http-request set-var(txn.host) hdr(host) diff --git a/reg-tests/converter/memcmp.vtc b/reg-tests/converter/memcmp.vtc new file mode 100644 index 000000000..9ec252e87 --- /dev/null +++ b/reg-tests/converter/memcmp.vtc @@ -0,0 +1,137 @@ +varnishtest "memcmp converter Test" + +#REQUIRE_VERSION=2.2 +#REQUIRE_OPTION=OPENSSL + +feature ignore_unknown_macro + +server s1 { + rxreq + txresp +} -repeat 4 -start + +server s2 { + rxreq + txresp +} -repeat 7 -start + +haproxy h1 -conf { + defaults + mode http + timeout connect 1s + timeout client 1s + timeout server 1s + + frontend fe + # This frontend matches two base64 encoded values and does not need to + # handle null bytes. + + bind "fd@${fe}" + + #### requests + http-request set-var(txn.hash) req.hdr(hash) + http-request set-var(txn.raw) req.hdr(raw) + + acl is_match var(txn.raw),sha1,base64,memcmp(txn.hash) + + http-response set-header Match true if is_match + http-response set-header Match false if !is_match + + default_backend be + + frontend fe2 + # This frontend matches two binary values, needing to handle null + # bytes. + bind "fd@${fe2}" + + #### requests + http-request set-var(txn.hash) req.hdr(hash),b64dec + http-request set-var(txn.raw) req.hdr(raw) + + acl is_match var(txn.raw),sha1,memcmp(txn.hash) + + http-response set-header Match true if is_match + http-response set-header Match false if !is_match + + default_backend be2 + + backend be + server s1 ${s1_addr}:${s1_port} + + backend be2 + server s2 ${s2_addr}:${s2_port} +} -start + +client c1 -connect ${h1_fe_sock} { + txreq -url "/" \ + -hdr "Raw: 1" \ + -hdr "Hash: NWoZK3kTsExUV00Ywo1G5jlUKKs=" + rxresp + expect resp.status == 200 + expect resp.http.match == "true" + txreq -url "/" \ + -hdr "Raw: 2" \ + -hdr "Hash: 2kuSN7rMzfGcB2DKt67EqDWQELA=" + rxresp + expect resp.status == 200 + expect resp.http.match == "true" + txreq -url "/" \ + -hdr "Raw: 2" \ + -hdr "Hash: 2kuSN7rMzfGcB2DKt67EqDWQELX=" + rxresp + expect resp.status == 200 + expect resp.http.match == "false" + txreq -url "/" \ + -hdr "Raw: 3" \ + -hdr "Hash: 2kuSN7rMzfGcB2DKt67EqDWQELA=" + rxresp + expect resp.status == 200 + expect resp.http.match == "false" +} -run + +client c2 -connect ${h1_fe2_sock} { + txreq -url "/" \ + -hdr "Raw: 1" \ + -hdr "Hash: NWoZK3kTsExUV00Ywo1G5jlUKKs=" + rxresp + expect resp.status == 200 + expect resp.http.match == "true" + txreq -url "/" \ + -hdr "Raw: 2" \ + -hdr "Hash: 2kuSN7rMzfGcB2DKt67EqDWQELA=" + rxresp + expect resp.status == 200 + expect resp.http.match == "true" + txreq -url "/" \ + -hdr "Raw: 2" \ + -hdr "Hash: 2kuSN7rMzfGcB2DKt67EqDWQELX=" + rxresp + expect resp.status == 200 + expect resp.http.match == "false" + txreq -url "/" \ + -hdr "Raw: 3" \ + -hdr "Hash: 2kuSN7rMzfGcB2DKt67EqDWQELA=" + rxresp + expect resp.status == 200 + expect resp.http.match == "false" + + # Test for values with leading nullbytes. + txreq -url "/" \ + -hdr "Raw: 6132845" \ + -hdr "Hash: AAAAVaeL9nNcSok1j6sd40EEw8s=" + rxresp + expect resp.status == 200 + expect resp.http.match == "true" + txreq -url "/" \ + -hdr "Raw: 49177200" \ + -hdr "Hash: AAAA9GLglTNv2JoMv2n/w9Xadhc=" + rxresp + expect resp.status == 200 + expect resp.http.match == "true" + txreq -url "/" \ + -hdr "Raw: 6132845" \ + -hdr "Hash: AAAA9GLglTNv2JoMv2n/w9Xadhc=" + rxresp + expect resp.status == 200 + expect resp.http.match == "false" +} -run diff --git a/src/sample.c b/src/sample.c index b223547f0..253b1bfa6 100644 --- a/src/sample.c +++ b/src/sample.c @@ -3065,7 +3065,7 @@ static int smp_check_concat(struct arg *args, struct sample_conv *conv, return 1; } -/* compares string with a variable containing a string. Return value +/* Compares string with a variable containing a string. Return value * is compatible with strcmp(3)'s return value. */ static int sample_conv_strcmp(const struct arg *arg_p, struct sample *smp, void *private) @@ -3099,6 +3099,41 @@ static int sample_conv_strcmp(const struct arg *arg_p, struct sample *smp, void return 1; } +#ifdef USE_OPENSSL +/* Compares bytestring with a variable containing a bytestring. Return value + * is `true` if both bytestrings are bytewise identical and `false` otherwise. + * + * Comparison will be performed in constant time if both bytestrings are of + * the same length. If the lengths differ execution time will not be constant. + */ +static int sample_conv_memcmp(const struct arg *arg_p, struct sample *smp, void *private) +{ + struct sample tmp; + int result; + + smp_set_owner(&tmp, smp->px, smp->sess, smp->strm, smp->opt); + if (arg_p[0].type != ARGT_VAR) + return 0; + if (!vars_get_by_desc(&arg_p[0].data.var, &tmp)) + return 0; + if (!sample_casts[tmp.data.type][SMP_T_BIN](&tmp)) + return 0; + + if (smp->data.u.str.data != tmp.data.u.str.data) { + smp->data.u.sint = 0; + smp->data.type = SMP_T_BOOL; + return 1; + } + + /* The following comparison is performed in constant time. */ + result = CRYPTO_memcmp(smp->data.u.str.area, tmp.data.u.str.area, smp->data.u.str.data); + + smp->data.u.sint = result == 0; + smp->data.type = SMP_T_BOOL; + return 1; +} +#endif + #define GRPC_MSG_COMPRESS_FLAG_SZ 1 /* 1 byte */ #define GRPC_MSG_LENGTH_SZ 4 /* 4 bytes */ #define GRPC_MSG_HEADER_SZ (GRPC_MSG_COMPRESS_FLAG_SZ + GRPC_MSG_LENGTH_SZ) @@ -3186,6 +3221,23 @@ static int smp_check_strcmp(struct arg *args, struct sample_conv *conv, return 0; } +#ifdef USE_OPENSSL +/* This function checks the "memcmp" converter's arguments and extracts the + * variable name and its scope. + */ +static int smp_check_memcmp(struct arg *args, struct sample_conv *conv, + const char *file, int line, char **err) +{ + /* Try to decode a variable. */ + if (vars_check_arg(&args[0], NULL)) + return 1; + + memprintf(err, "failed to register variable name '%s'", + args[0].data.str.area); + return 0; +} +#endif + /**/ static int sample_conv_htonl(const struct arg *arg_p, struct sample *smp, void *private) { @@ -3727,6 +3779,9 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, { #endif { "concat", sample_conv_concat, ARG3(1,STR,STR,STR), smp_check_concat, SMP_T_STR, SMP_T_STR }, { "strcmp", sample_conv_strcmp, ARG1(1,STR), smp_check_strcmp, SMP_T_STR, SMP_T_SINT }, +#ifdef USE_OPENSSL + { "memcmp", sample_conv_memcmp, ARG1(1,STR), smp_check_memcmp, SMP_T_BIN, SMP_T_BOOL }, +#endif /* gRPC converters. */ { "ungrpc", sample_conv_ungrpc, ARG2(1,PBUF_FNUM,STR), sample_conv_protobuf_check, SMP_T_BIN, SMP_T_BIN }, -- 2.27.0