You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@apr.apache.org by yl...@apache.org on 2025/06/05 14:40:34 UTC

svn commit: r1926156 - in /apr/apr/branches/1.8.x: ./ include/apr_strings.h strings/apr_strings.c test/teststr.c

Author: ylavic
Date: Thu Jun  5 14:40:33 2025
New Revision: 1926156

URL: http://svn.apache.org/viewvc?rev=1926156&view=rev
Log:
apr_strings: Provide timing safe memory and string comparison functions.

* include/apr_strings.h, strings/apr_strings.c:
  Add apr_memeq_timingsafe(), apr_streq_timingsafe() and apr_strneq_timingsafe()
  to compare (for equality) memory and/or NUL-terminated strings, using constant
  time algorithms with no branch depending on secret data.

* test/teststr.c:
  Tests for above functions.


Merges r1926155 trunk
Submitted by: ylavic

Modified:
    apr/apr/branches/1.8.x/   (props changed)
    apr/apr/branches/1.8.x/include/apr_strings.h
    apr/apr/branches/1.8.x/strings/apr_strings.c
    apr/apr/branches/1.8.x/test/teststr.c

Propchange: apr/apr/branches/1.8.x/
------------------------------------------------------------------------------
  Merged /apr/apr/trunk:r1926155

Modified: apr/apr/branches/1.8.x/include/apr_strings.h
URL: http://svn.apache.org/viewvc/apr/apr/branches/1.8.x/include/apr_strings.h?rev=1926156&r1=1926155&r2=1926156&view=diff
==============================================================================
--- apr/apr/branches/1.8.x/include/apr_strings.h (original)
+++ apr/apr/branches/1.8.x/include/apr_strings.h Thu Jun  5 14:40:33 2025
@@ -183,6 +183,54 @@ APR_DECLARE_NONSTD(char *) apr_psprintf(
         __attribute__((format(printf,2,3)));
 
 /**
+ * Check whether two buffers of equal size have the same content, using a
+ * constant time algorithm (branch-less with regard to the content of the
+ * buffers and an execution time solely dependent on the number of bytes
+ * compared, not the bytes themselves).
+ *
+ * @param buf1 first buffer to compare
+ * @param buf2 second buffer to compare
+ * @param n number of bytes to compare
+ * @return 1 if equal, 0 otherwise
+ */
+APR_DECLARE(int) apr_memeq_timingsafe(const void *buf1, const void *buf2,
+                                      apr_size_t n);
+
+/**
+ * Check whether two NUL-terminated strings have the same content, using a
+ * constant time algorithm (branch-less with regard to the content of the
+ * secret string and an execution time solely dependent on the length of
+ * the non-secret string). The secret string of the two should be set in
+ * the first parameter \c sec1 to avoid leaking its length.
+ *
+ * @param sec1 first string to compare (the secret one)
+ * @param str2 second string to compare
+ * @return 1 if equal, 0 otherwise
+ * @remark The function will compare as much characters as there are in
+ *         \c str2, so the length of \c str2 might leak through side channel,
+ *         while the length of \c sec1 does not.
+ */
+APR_DECLARE(int) apr_streq_timingsafe(const char *sec1, const char *str2);
+
+/**
+ * Check whether two NUL-terminated strings have the same content, up to \c n
+ * characters, using a constant time algorithm (branch-less with regard to the
+ * content of the secret string and an execution time solely dependent on the
+ * length of the non-secret string or \c n). The secret string of the two
+ * should be set in the first parameter \c sec1 to avoid leaking its length.
+ *
+ * @param sec1 secret string to compare
+ * @param str2 string to compare with
+ * @param n max number of characters to compare
+ * @return 1 if equal, 0 otherwise
+ * @remark The function will compare as much characters as there are in
+ *         \c str2 if it's less than \c n, so the length of \c str2 might
+ *         leak through side channel, while the length of \c sec1 does not.
+ */
+APR_DECLARE(int) apr_strneq_timingsafe(const char *sec1, const char *str2,
+                                       apr_size_t n);
+
+/**
  * Copy up to dst_size characters from src to dst; does not copy
  * past a NUL terminator in src, but always terminates dst with a NUL
  * regardless.

Modified: apr/apr/branches/1.8.x/strings/apr_strings.c
URL: http://svn.apache.org/viewvc/apr/apr/branches/1.8.x/strings/apr_strings.c?rev=1926156&r1=1926155&r2=1926156&view=diff
==============================================================================
--- apr/apr/branches/1.8.x/strings/apr_strings.c (original)
+++ apr/apr/branches/1.8.x/strings/apr_strings.c Thu Jun  5 14:40:33 2025
@@ -58,6 +58,9 @@
 #ifdef HAVE_STDDEF_H
 #include <stddef.h> /* NULL */
 #endif
+#ifdef HAVE_LIMITS_H
+#include <limits.h> /* INT_MAX */
+#endif
 
 #ifdef HAVE_STDLIB_H
 #include <stdlib.h> /* strtol and strtoll */
@@ -212,6 +215,121 @@ APR_DECLARE(char *) apr_pstrcatv(apr_poo
     return res;
 }
 
+/* A volatile variable which is always zero but allows to block the compiler
+ * from optimizing or eliding code using it. Volatile forces the compiler to
+ * emit a memory load for which no value can be assumed, so for instance an
+ * add/sub/xor/or with "optblocker" is a noop that will hide the result to
+ * the optimizer.
+ */
+static volatile const apr_uint32_t optblocker;
+
+/* Return whether x is not zero, with no branching controlled by x.
+ *
+ * Taken from the cryptoint library (public domain) by D. J. Bernstein,
+ * which provides timing attacks safe integer operations/primitives.
+ * Code:
+ *   https://lib.mceliece.org/libmceliece-20250507/cryptoint/crypto_uint32.h
+ * Paper:
+ *   https://cr.yp.to/papers/cryptoint-20250424.pdf
+ */
+#if __has_attribute(always_inline)
+__attribute__((always_inline))
+#endif
+static APR_INLINE int test_nonzero_timingsafe(apr_uint32_t x)
+{
+    x |= -x; /* sets the most significant bit unless x == 0 */
+
+    /* shift bit 31 (MSB) to bit 0 */
+    x >>= 32-6;      /* keep 6 bits */
+    x += optblocker; /* lose the optimizer */
+    x >>= 5;         /* keep the (original) MSB only */
+
+    /* x is now 0 or 1 */
+    return x & INT_MAX;
+}
+
+APR_DECLARE(int) apr_memeq_timingsafe(const void *buf1, const void *buf2,
+                                      apr_size_t n)
+{
+    apr_uint32_t diff = 0;
+    volatile apr_size_t count = n; /* prevent loop unrolling */
+    apr_size_t i = 0;
+
+    for (; i < count; ++i) {
+        const unsigned char c1 = ((volatile const unsigned char *)buf1)[i];
+        const unsigned char c2 = ((volatile const unsigned char *)buf2)[i];
+
+        diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */
+    }
+
+    /* (diff == 0) <=> (diff != 0) ^ 1 */
+    return test_nonzero_timingsafe(diff) ^ 1;
+}
+
+APR_DECLARE(int) apr_streq_timingsafe(const char *sec1, const char *str2)
+{
+    apr_uint32_t diff = 0;
+    apr_size_t i1 = 0, i2 = 0;
+
+    for (;; ++i2) {
+        const unsigned char c1 = ((volatile const unsigned char *)sec1)[i1];
+        const unsigned char c2 = ((volatile const unsigned char *)str2)[i2];
+
+        diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */
+
+        /* Not a shortest/longest match because an attacker would usually know
+         * one of the strings and could then determine the length of the other.
+         * So assume only sec1 and its length are secret and stop the loop at
+         * the end of str2. If sec1 is shorter than str2 the loop will continue
+         * by comparing the rest of str2 with the trailing NUL byte of sec1.
+         * In any case since the diff above is computed up to and including a
+         * NUL byte, only the same content and length will raise match.
+         */
+        if (!c2) {
+            break;
+        }
+
+        /* Don't go above sec1's NUL byte */
+        i1 += test_nonzero_timingsafe(c1);
+    }
+
+    /* (diff == 0) <=> (diff != 0) ^ 1 */
+    return test_nonzero_timingsafe(diff) ^ 1;
+}
+
+APR_DECLARE(int) apr_strneq_timingsafe(const char *sec1, const char *str2,
+                                       apr_size_t n)
+{
+    apr_uint32_t diff = 0;
+    volatile apr_size_t count = n; /* prevent loop unrolling */
+    apr_size_t i1 = 0, i2 = 0;
+
+    for (; i2 < count; ++i2) {
+        const unsigned char c1 = ((volatile const unsigned char *)sec1)[i1];
+        const unsigned char c2 = ((volatile const unsigned char *)str2)[i2];
+
+        diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */
+
+        /* Not a shortest/longest match because an attacker would usually know
+         * one of the strings and could then determine the length of the other.
+         * So assume only sec1 and its length are secret and stop the loop at
+         * the end of str2. If sec1 is shorter than str2 the loop will continue
+         * by comparing the rest of str2 with the trailing NUL byte of sec1.
+         * In any case since the diff above is computed up to and including a
+         * NUL byte, only the same content and length will raise match.
+         */
+        if (!c2) {
+            break;
+        }
+
+        /* Don't go above sec1's NUL byte */
+        i1 += test_nonzero_timingsafe(c1);
+    }
+
+    /* (diff == 0) <=> (diff != 0) ^ 1 */
+    return test_nonzero_timingsafe(diff) ^ 1;
+}
+
 #if (!APR_HAVE_MEMCHR)
 void *memchr(const void *s, int c, size_t n)
 {

Modified: apr/apr/branches/1.8.x/test/teststr.c
URL: http://svn.apache.org/viewvc/apr/apr/branches/1.8.x/test/teststr.c?rev=1926156&r1=1926155&r2=1926156&view=diff
==============================================================================
--- apr/apr/branches/1.8.x/test/teststr.c (original)
+++ apr/apr/branches/1.8.x/test/teststr.c Thu Jun  5 14:40:33 2025
@@ -408,6 +408,101 @@ static void pstrcat(abts_case *tc, void
                    "abcdefghij12345");
 }
 
+#define TIMINGSAFE_RANDS_NUM 20u
+#define TIMINGSAFE_RANDS_LEN 32u
+static void timingsafe(abts_case *tc, void *data)
+{
+    struct {
+        const char *sec;
+        const char *str;
+        int eq_res;
+        int neq_res;
+        apr_size_t neq_n;
+    } sample[] = {
+        {"",    "",     1,  1,  0},
+        {"",    "",     1,  1,  1},
+        {"a",   "a",    1,  1,  1},
+        {"a",   "a",    1,  1,  2},
+        {"a",   "b",    0,  0,  1},
+        {"a",   "aa",   0,  1,  1},
+        {"a",   "aa",   0,  0,  2},
+        {"a",   "aa",   0,  0,  3},
+        {"aa",  "a",    0,  1,  1},
+        {"aa",  "a",    0,  0,  2},
+        {"aa",  "a",    0,  0,  3},
+        {"aa",  "aa",   1,  1,  1},
+        {"aa",  "aa",   1,  1,  2},
+        {"aa",  "aa",   1,  1,  3},
+        {"ab",  "ba",   0,  0,  1},
+        {"ab",  "ba",   0,  0,  2},
+        {"ab",  "ba",   0,  0,  3},
+        {NULL,}
+    }, *sp;
+    struct {
+        char str[TIMINGSAFE_RANDS_LEN+1];
+        apr_size_t len;
+    } rands[TIMINGSAFE_RANDS_NUM];
+    apr_size_t i, j, k;
+    int res;
+
+    /* test the sample */
+    for (sp = sample; sp->sec; ++sp) {
+        res = apr_streq_timingsafe(sp->sec, sp->str);
+        ABTS_INT_EQUAL(tc, strcmp(sp->sec, sp->str) == 0, res);
+        ABTS_INT_EQUAL(tc, sp->eq_res, res);
+
+        res = apr_strneq_timingsafe(sp->sec, sp->str, sp->neq_n);
+        ABTS_INT_EQUAL(tc, strncmp(sp->sec, sp->str, sp->neq_n) == 0, res);
+        ABTS_INT_EQUAL(tc, sp->neq_res, res);
+
+        if (strlen(sp->sec) == strlen(sp->str)) {
+            res = apr_memeq_timingsafe(sp->sec, sp->str, strlen(sp->sec));
+            ABTS_INT_EQUAL(tc, memcmp(sp->sec, sp->str, strlen(sp->sec)) == 0, res);
+            ABTS_INT_EQUAL(tc, sp->eq_res, res);
+        }
+    }
+
+    /* test random strings */
+    memset(rands, 0, sizeof(rands)); /* zero init/pad the whole */
+    for (i = 0; i < TIMINGSAFE_RANDS_NUM; ++i) {
+        unsigned char randlen = 0;
+        apr_generate_random_bytes((void *)&randlen, sizeof(randlen));
+        rands[i].len = (unsigned int)randlen % TIMINGSAFE_RANDS_LEN;
+        apr_generate_random_bytes((void *)rands[i].str, rands[i].len);
+    }
+    for (i = 0; i < TIMINGSAFE_RANDS_NUM; ++i) {
+        for (j = i; j < TIMINGSAFE_RANDS_NUM; ++j) {
+            for (k = (j == i); k < 2; ++k) { /* both ways for j != i */
+                apr_size_t i1 = (k) ? j : i,
+                           i2 = (k) ? i : j;
+                const char *s1 = rands[i1].str,
+                           *s2 = rands[i2].str;
+                unsigned int n1 = rands[i1].len,
+                             n2 = rands[i2].len;
+
+                ABTS_INT_EQUAL(tc, strcmp(s1, s2) == 0,
+                               apr_streq_timingsafe(s1, s2));
+
+                ABTS_INT_EQUAL(tc, strncmp(s1, s2, n1) == 0,
+                               apr_strneq_timingsafe(s1, s2, n1));
+                ABTS_INT_EQUAL(tc, strncmp(s1, s2, n2) == 0,
+                               apr_strneq_timingsafe(s1, s2, n2));
+                
+                /* including trailing \0 */
+                ABTS_INT_EQUAL(tc, strncmp(s1, s2, n1 + 1) == 0,
+                               apr_strneq_timingsafe(s1, s2, n1 + 1));
+                ABTS_INT_EQUAL(tc, strncmp(s1, s2, n2 + 1) == 0,
+                               apr_strneq_timingsafe(s1, s2, n2 + 1));
+
+                ABTS_INT_EQUAL(tc, memcmp(s1, s2, n1) == 0,
+                               apr_memeq_timingsafe(s1, s2, n1));
+                ABTS_INT_EQUAL(tc, memcmp(s1, s2, n2) == 0,
+                               apr_memeq_timingsafe(s1, s2, n2));
+            }
+        }
+    }
+}
+
 abts_suite *teststr(abts_suite *suite)
 {
     suite = ADD_SUITE(suite)
@@ -427,6 +522,7 @@ abts_suite *teststr(abts_suite *suite)
     abts_run_test(suite, snprintf_overflow, NULL);
     abts_run_test(suite, skip_prefix, NULL);
     abts_run_test(suite, pstrcat, NULL);
+    abts_run_test(suite, timingsafe, NULL);
 
     return suite;
 }