]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
Add ap_*_timingsafe() constant-time comparison functions:
authorJoe Orton <jorton@apache.org>
Mon, 27 Apr 2026 12:54:48 +0000 (12:54 +0000)
committerJoe Orton <jorton@apache.org>
Mon, 27 Apr 2026 12:54:48 +0000 (12:54 +0000)
* include/httpd.h: Declare ap_memeq_timingsafe(),
  ap_streq_timingsafe(), ap_strneq_timingsafe().

* server/util.c: Implement, wrapping apr_*_timingsafe() if
  APR >= 1.8, with a fallback to copied-in versions.

* modules/aaa/mod_auth_digest.c: Replace apr_crypto_equals()
  with ap_memeq_timingsafe(). Remove apr_crypto.h include.

* modules/session/mod_session_crypto.c: Replace local
  ap_crypto_equals() with ap_memeq_timingsafe(). Remove
  the local implementation and macro alias.

Github: closes #638

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1933389 13f79535-47bb-0310-9956-ffa450edef68

include/ap_mmn.h
include/httpd.h
modules/aaa/mod_auth_digest.c
modules/session/mod_session_crypto.c
server/util.c

index 09770fa1a574b60dbbfd6e22c4f7be2d1ce3f1ed..ccd504d8e82f06bc8ecf9b97d5c7f972a53e8df5 100644 (file)
  * 20211221.28 (2.5.1-dev) Add dav_get_base_path() to mod_dav
  * 20211221.29 (2.5.1-dev) Add ap_set_time_process_request() to scoreboard.h
  * 20211221.30 (2.5.1-dev) Add ap_stat_check() to httpd.h
+ * 20211221.31 (2.5.1-dev) Add ap_*_timingsafe() to httpd.h
  */
 
 #define MODULE_MAGIC_COOKIE 0x41503235UL /* "AP25" */
 #ifndef MODULE_MAGIC_NUMBER_MAJOR
 #define MODULE_MAGIC_NUMBER_MAJOR 20211221
 #endif
-#define MODULE_MAGIC_NUMBER_MINOR 30             /* 0...n */
+#define MODULE_MAGIC_NUMBER_MINOR 31             /* 0...n */
 
 /**
  * Determine if the server's current MODULE_MAGIC_NUMBER is at least a
index b12b7e4e21bed5775a9bb8965eb46b25f8cfeef7..03376db99587676ed603842922d3b0a782a07b8a 100644 (file)
@@ -2261,6 +2261,54 @@ AP_DECLARE(int) ap_ind(const char *str, char c);        /* Sigh... */
  */
 AP_DECLARE(int) ap_rind(const char *str, char c);
 
+/**
+ * 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
+ */
+AP_DECLARE(int) ap_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.
+ */
+AP_DECLARE(int) ap_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.
+ */
+AP_DECLARE(int) ap_strneq_timingsafe(const char *sec1, const char *str2,
+                                     apr_size_t n);
+
 /**
  * Given a string, replace any bare &quot; with \\&quot; .
  * @param p The pool to allocate memory from
index ac02e6b96731e9254b56cd4f40efd44183768da9..fb4d26306be064dc8f33bbfb7300fc83082a4892 100644 (file)
@@ -73,7 +73,6 @@
 #include "apr_shm.h"
 #include "apr_rmm.h"
 #include "ap_provider.h"
-#include "apr_crypto.h" /* for apr_crypto_equals */
 
 #include "mod_auth.h"
 
@@ -1438,7 +1437,7 @@ static int check_nonce(request_rec *r, digest_header_rec *resp,
     resp->nonce[NONCE_TIME_LEN] = tmp;
     resp->nonce_time = nonce_time.time;
 
-    if (!apr_crypto_equals(hash, resp->nonce+NONCE_TIME_LEN, NONCE_HASH_LEN)) {
+    if (!ap_memeq_timingsafe(hash, resp->nonce+NONCE_TIME_LEN, NONCE_HASH_LEN)) {
         ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01776)
                       "invalid nonce %s received - hash is not %s",
                       resp->nonce, hash);
@@ -1769,7 +1768,7 @@ static int authenticate_digest_user(request_rec *r)
 
     if (resp->message_qop == NULL) {
         /* old (rfc-2069) style digest */
-        if (!apr_crypto_equals(resp->digest, old_digest(r, resp), MD5_DIGEST_LEN)) {
+        if (!ap_memeq_timingsafe(old_digest(r, resp), resp->digest, MD5_DIGEST_LEN)) {
             ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01792)
                           "user %s: password mismatch: %s", r->user,
                           r->uri);
@@ -1804,7 +1803,7 @@ static int authenticate_digest_user(request_rec *r)
             /* we failed to allocate a client struct */
             return HTTP_INTERNAL_SERVER_ERROR;
         }
-        if (!apr_crypto_equals(resp->digest, exp_digest, MD5_DIGEST_LEN)) {
+        if (!ap_memeq_timingsafe(exp_digest, resp->digest, MD5_DIGEST_LEN)) {
             ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01794)
                           "user %s: password mismatch: %s", r->user,
                           r->uri);
index f072d4c6fbccba4e2463dff95dceccfc5f7b5737..67b9e75464f51d1ec10af78d442dfd22dc9e63d9 100644 (file)
@@ -69,8 +69,6 @@ typedef struct {
 #define AP_SIPHASH_KSIZE    APR_SIPHASH_KSIZE
 #define ap_siphash24_auth   apr_siphash24_auth
 
-#define ap_crypto_equals    apr_crypto_equals
-
 #else
 
 #define AP_SIPHASH_DSIZE    8
@@ -165,21 +163,6 @@ static void ap_siphash24_auth(unsigned char out[AP_SIPHASH_DSIZE],
     U64TO8_LE(out, h);
 }
 
-static int ap_crypto_equals(const void *buf1, const void *buf2,
-                            apr_size_t size)
-{
-    const unsigned char *p1 = buf1;
-    const unsigned char *p2 = buf2;
-    unsigned char diff = 0;
-    apr_size_t i;
-
-    for (i = 0; i < size; ++i) {
-        diff |= p1[i] ^ p2[i];
-    }
-
-    return 1 & ((diff - 1) >> 8);
-}
-
 #endif
 
 static void compute_auth(const void *src, apr_size_t len,
@@ -404,7 +387,7 @@ static apr_status_t decrypt_string(request_rec * r, const apr_crypto_t *f,
          * the MAC and comparing it (timing safe) with the one in the payload.
          */
         compute_auth(slider, len, passphrase, passlen, auth);
-        if (!ap_crypto_equals(auth, decoded, AP_SIPHASH_DSIZE)) {
+        if (!ap_memeq_timingsafe(auth, decoded, AP_SIPHASH_DSIZE)) {
             ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(10006)
                     "auth does not match, skipping");
             continue;
index eda0eceaf9c33b39eab48139308b23d7b6d47160..d1d06fc15b4e6abbd1dffb7c11e6d5c7bdff736e 100644 (file)
@@ -3934,3 +3934,141 @@ AP_DECLARE(const char *)ap_dir_fnmatch(ap_dir_match_t *w, const char *path,
 
     return NULL;
 }
+
+
+#if APR_VERSION_AT_LEAST(1,8,0)
+AP_DECLARE(int) ap_memeq_timingsafe(const void *buf1, const void *buf2,
+                                     apr_size_t n)
+{
+    return apr_memeq_timingsafe(buf1, buf2, n);
+}
+
+AP_DECLARE(int) ap_streq_timingsafe(const char *sec1, const char *str2)
+{
+    return apr_streq_timingsafe(sec1, str2);
+}
+
+AP_DECLARE(int) ap_strneq_timingsafe(const char *sec1, const char *str2,
+                                      apr_size_t n)
+{
+    return apr_strneq_timingsafe(sec1, str2, n);
+}
+
+#else /* !APR_VERSION_AT_LEAST(1,8,0) */
+
+/* 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;
+}
+
+AP_DECLARE(int) ap_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;
+}
+
+AP_DECLARE(int) ap_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;
+}
+
+AP_DECLARE(int) ap_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;
+}
+
+#endif /* !APR_VERSION_AT_LEAST(1,8,0) */