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;
}