From: Timo Sirainen Date: Mon, 27 Apr 2026 22:50:56 +0000 (+0300) Subject: [PATCH 1/2] lib: Add str_equals_timing_safe() X-Git-Tag: archive/raspbian/1%2.4.1+dfsg1-6+rpi1+deb13u7^2~4 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=dd8d1955150cc3c9810aab35e8074d3bf5ce6c12;p=dovecot.git [PATCH 1/2] lib: Add str_equals_timing_safe() Constant-time string comparison that avoids the length leak in str_equals_timing_almost_safe(). Compares HMAC-SHA256 digests of the inputs (keyed with hash_iv) rather than the strings themselves, so neither the contents nor the length of either input affects timing in a way an attacker can exploit. Gbp-Pq: Name 0001-lib-Add-str_equals_timing_safe.patch --- diff --git a/src/lib/strfuncs.c b/src/lib/strfuncs.c index 93e14ab..75f9be8 100644 --- a/src/lib/strfuncs.c +++ b/src/lib/strfuncs.c @@ -7,6 +7,9 @@ #include "printf-format-fix.h" #include "strfuncs.h" #include "array.h" +#include "hash.h" +#include "hmac.h" +#include "sha2.h" #include #include @@ -627,6 +630,32 @@ bool str_equals_timing_almost_safe(const char *s1, const char *s2) return ret == 0; } +bool str_equals_hash_timing_safe(const char *s1, const char *s2) +{ + struct hmac_context ctx; + unsigned char digest1[SHA256_RESULTLEN]; + unsigned char digest2[SHA256_RESULTLEN]; + + /* Compare HMAC-SHA256 digests of the inputs rather than the inputs + themselves. The digest length is constant, so the subsequent + mem_equals_timing_safe() leaks no length information. The HMAC + times depend on the input lengths, but each side's length is either + a deployment constant (for the secret) or already known to the + attacker (for their own input), so neither leaks a useful signal. + hash_iv keys the HMAC to prevent precomputation. */ + hmac_init(&ctx, (const unsigned char *)&hash_iv, sizeof(hash_iv), + &hash_method_sha256); + hmac_update(&ctx, s1, strlen(s1)); + hmac_final(&ctx, digest1); + + hmac_init(&ctx, (const unsigned char *)&hash_iv, sizeof(hash_iv), + &hash_method_sha256); + hmac_update(&ctx, s2, strlen(s2)); + hmac_final(&ctx, digest2); + + return mem_equals_timing_safe(digest1, digest2, sizeof(digest1)); +} + size_t str_match(const char *p1, const char *p2) { diff --git a/src/lib/strfuncs.h b/src/lib/strfuncs.h index b59c8b5..2834db0 100644 --- a/src/lib/strfuncs.h +++ b/src/lib/strfuncs.h @@ -93,6 +93,11 @@ bool mem_equals_timing_safe(const void *p1, const void *p2, size_t size); the string lengths are the same. If not, the length of the secret string may be leaked, but otherwise the contents won't be. */ bool str_equals_timing_almost_safe(const char *s1, const char *s2); +/* Returns TRUE if the two strings are equal. Safe against timing attacks: + neither the contents nor the length of either string is leaked. + Implemented by HMAC-SHA256ing both inputs under a random per-process key + and comparing the fixed-length digests. */ +bool str_equals_hash_timing_safe(const char *s1, const char *s2); size_t str_match(const char *p1, const char *p2) ATTR_PURE; size_t str_match_icase(const char *p1, const char *p2) ATTR_PURE; diff --git a/src/lib/test-strfuncs.c b/src/lib/test-strfuncs.c index d9f5bd8..31842d9 100644 --- a/src/lib/test-strfuncs.c +++ b/src/lib/test-strfuncs.c @@ -478,6 +478,32 @@ static void test_str_equals_timing_almost_safe(void) test_end(); } +static void test_str_equals_hash_timing_safe(void) +{ + const struct { + const char *a, *b; + } tests[] = { + { "", "" }, + { "a", "a" }, + { "b", "a" }, + { "ab", "ab" }, + { "ab", "ba" }, + { "ab", "bc" }, + { "a", "" }, + { "a", "ab" }, + { "a", "abc" }, + { "ab", "abc" }, + }; + test_begin("str_equals_hash_timing_safe()"); + for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) { + test_assert((strcmp(tests[i].a, tests[i].b) == 0) == + str_equals_hash_timing_safe(tests[i].a, tests[i].b)); + test_assert((strcmp(tests[i].a, tests[i].b) == 0) == + str_equals_hash_timing_safe(tests[i].b, tests[i].a)); + } + test_end(); +} + static void test_dec2str_buf(void) { const uintmax_t test_input[] = { @@ -773,6 +799,7 @@ void test_strfuncs(void) test_p_array_const_string_join(); test_mem_equals_timing_safe(); test_str_equals_timing_almost_safe(); + test_str_equals_hash_timing_safe(); test_dec2str_buf(); test_str_match(); test_str_match_icase();