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

[email protected]
Newsgroups gmane.comp.apache.apr.cvs
Message-ID <[email protected]>
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;
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.