]> dgit.raspbian.org Git - dovecot.git/commitdiff
[PATCH 1/2] lib: Add str_equals_timing_safe()
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Mon, 27 Apr 2026 22:50:56 +0000 (01:50 +0300)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
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

src/lib/strfuncs.c
src/lib/strfuncs.h
src/lib/test-strfuncs.c

index 93e14abfa04d96bda94c082de0f5e5ab9fa23087..75f9be89fe355d1f602b16cd4c7a043bf7785212 100644 (file)
@@ -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 <stdio.h>
 #include <limits.h>
@@ -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)
 {
index b59c8b53f816c8e24b7ad36d4166d432a1fc6793..2834db08801875302c1e07aee531bfb91635fda7 100644 (file)
@@ -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;
index d9f5bd82af31b1e625a802b1d8970f4eafb5989b..31842d90953e21cccb2a147091508b5fb2b74d2d 100644 (file)
@@ -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();