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


Reply via email to