MINOR: sample: Add secure_memcmp converter
secure_memcmp compares two binary strings in constant time. It's only available when haproxy is compiled with USE_OPENSSL.
This commit is contained in:
parent
9fc6c97fb3
commit
f38175cf6e
|
@ -15148,6 +15148,24 @@ sdbm([<avalanche>])
|
||||||
32-bit hash is trivial to break. See also "crc32", "djb2", "wt6", "crc32c",
|
32-bit hash is trivial to break. See also "crc32", "djb2", "wt6", "crc32c",
|
||||||
and the "hash-type" directive.
|
and the "hash-type" directive.
|
||||||
|
|
||||||
|
secure_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),secure_memcmp(txn.token)
|
||||||
|
|
||||||
set-var(<var name>)
|
set-var(<var name>)
|
||||||
Sets a variable with the input content and returns the content on the output
|
Sets a variable with the input content and returns the content on the output
|
||||||
as-is. The variable keeps the value and the associated input type. The name of
|
as-is. The variable keeps the value and the associated input type. The name of
|
||||||
|
@ -15190,6 +15208,9 @@ strcmp(<var>)
|
||||||
than 0 otherwise (right string greater than left string or the right string is
|
than 0 otherwise (right string greater than left string or the right string is
|
||||||
shorter).
|
shorter).
|
||||||
|
|
||||||
|
See also the secure_memcmp converter if you need to compare two binary
|
||||||
|
strings in constant time.
|
||||||
|
|
||||||
Example :
|
Example :
|
||||||
|
|
||||||
http-request set-var(txn.host) hdr(host)
|
http-request set-var(txn.host) hdr(host)
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
varnishtest "secure_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,secure_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,secure_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
|
57
src/sample.c
57
src/sample.c
|
@ -3065,7 +3065,7 @@ static int smp_check_concat(struct arg *args, struct sample_conv *conv,
|
||||||
return 1;
|
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.
|
* is compatible with strcmp(3)'s return value.
|
||||||
*/
|
*/
|
||||||
static int sample_conv_strcmp(const struct arg *arg_p, struct sample *smp, void *private)
|
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;
|
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_secure_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_COMPRESS_FLAG_SZ 1 /* 1 byte */
|
||||||
#define GRPC_MSG_LENGTH_SZ 4 /* 4 bytes */
|
#define GRPC_MSG_LENGTH_SZ 4 /* 4 bytes */
|
||||||
#define GRPC_MSG_HEADER_SZ (GRPC_MSG_COMPRESS_FLAG_SZ + GRPC_MSG_LENGTH_SZ)
|
#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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_OPENSSL
|
||||||
|
/* This function checks the "secure_memcmp" converter's arguments and extracts the
|
||||||
|
* variable name and its scope.
|
||||||
|
*/
|
||||||
|
static int smp_check_secure_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)
|
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
|
#endif
|
||||||
{ "concat", sample_conv_concat, ARG3(1,STR,STR,STR), smp_check_concat, SMP_T_STR, SMP_T_STR },
|
{ "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 },
|
{ "strcmp", sample_conv_strcmp, ARG1(1,STR), smp_check_strcmp, SMP_T_STR, SMP_T_SINT },
|
||||||
|
#ifdef USE_OPENSSL
|
||||||
|
{ "secure_memcmp", sample_conv_secure_memcmp, ARG1(1,STR), smp_check_secure_memcmp, SMP_T_BIN, SMP_T_BOOL },
|
||||||
|
#endif
|
||||||
|
|
||||||
/* gRPC converters. */
|
/* gRPC converters. */
|
||||||
{ "ungrpc", sample_conv_ungrpc, ARG2(1,PBUF_FNUM,STR), sample_conv_protobuf_check, SMP_T_BIN, SMP_T_BIN },
|
{ "ungrpc", sample_conv_ungrpc, ARG2(1,PBUF_FNUM,STR), sample_conv_protobuf_check, SMP_T_BIN, SMP_T_BIN },
|
||||||
|
|
Loading…
Reference in New Issue