Skip to content

Commit 5a6d517

Browse files
committed
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. git-svn-id: https://svn.apache.org/repos/asf/apr/apr/trunk@1926155 13f79535-47bb-0310-9956-ffa450edef68
1 parent 8485009 commit 5a6d517

File tree

3 files changed

+262
-0
lines changed

3 files changed

+262
-0
lines changed

include/apr_strings.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,54 @@ APR_DECLARE_NONSTD(char *) apr_psprintf(apr_pool_t *p, const char *fmt, ...)
192192
*/
193193
APR_DECLARE(apr_status_t) apr_memzero_explicit(void *buffer, apr_size_t size);
194194

195+
/**
196+
* Check whether two buffers of equal size have the same content, using a
197+
* constant time algorithm (branch-less with regard to the content of the
198+
* buffers and an execution time solely dependent on the number of bytes
199+
* compared, not the bytes themselves).
200+
*
201+
* @param buf1 first buffer to compare
202+
* @param buf2 second buffer to compare
203+
* @param n number of bytes to compare
204+
* @return 1 if equal, 0 otherwise
205+
*/
206+
APR_DECLARE(int) apr_memeq_timingsafe(const void *buf1, const void *buf2,
207+
apr_size_t n);
208+
209+
/**
210+
* Check whether two NUL-terminated strings have the same content, using a
211+
* constant time algorithm (branch-less with regard to the content of the
212+
* secret string and an execution time solely dependent on the length of
213+
* the non-secret string). The secret string of the two should be set in
214+
* the first parameter \c sec1 to avoid leaking its length.
215+
*
216+
* @param sec1 first string to compare (the secret one)
217+
* @param str2 second string to compare
218+
* @return 1 if equal, 0 otherwise
219+
* @remark The function will compare as much characters as there are in
220+
* \c str2, so the length of \c str2 might leak through side channel,
221+
* while the length of \c sec1 does not.
222+
*/
223+
APR_DECLARE(int) apr_streq_timingsafe(const char *sec1, const char *str2);
224+
225+
/**
226+
* Check whether two NUL-terminated strings have the same content, up to \c n
227+
* characters, using a constant time algorithm (branch-less with regard to the
228+
* content of the secret string and an execution time solely dependent on the
229+
* length of the non-secret string or \c n). The secret string of the two
230+
* should be set in the first parameter \c sec1 to avoid leaking its length.
231+
*
232+
* @param sec1 secret string to compare
233+
* @param str2 string to compare with
234+
* @param n max number of characters to compare
235+
* @return 1 if equal, 0 otherwise
236+
* @remark The function will compare as much characters as there are in
237+
* \c str2 if it's less than \c n, so the length of \c str2 might
238+
* leak through side channel, while the length of \c sec1 does not.
239+
*/
240+
APR_DECLARE(int) apr_strneq_timingsafe(const char *sec1, const char *str2,
241+
apr_size_t n);
242+
195243
/**
196244
* Copy up to dst_size characters from src to dst; does not copy
197245
* past a NUL terminator in src, but always terminates dst with a NUL

strings/apr_strings.c

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
#ifdef HAVE_STDDEF_H
5959
#include <stddef.h> /* NULL */
6060
#endif
61+
#ifdef HAVE_LIMITS_H
62+
#include <limits.h> /* INT_MAX */
63+
#endif
6164

6265
#ifdef HAVE_STDLIB_H
6366
#include <stdlib.h> /* strtol and strtoll */
@@ -244,6 +247,121 @@ APR_DECLARE(apr_status_t) apr_memzero_explicit(void *buffer, apr_size_t size)
244247
return APR_SUCCESS;
245248
}
246249

250+
/* A volatile variable which is always zero but allows to block the compiler
251+
* from optimizing or eliding code using it. Volatile forces the compiler to
252+
* emit a memory load for which no value can be assumed, so for instance an
253+
* add/sub/xor/or with "optblocker" is a noop that will hide the result to
254+
* the optimizer.
255+
*/
256+
static volatile const apr_uint32_t optblocker;
257+
258+
/* Return whether x is not zero, with no branching controlled by x.
259+
*
260+
* Taken from the cryptoint library (public domain) by D. J. Bernstein,
261+
* which provides timing attacks safe integer operations/primitives.
262+
* Code:
263+
* https://lib.mceliece.org/libmceliece-20250507/cryptoint/crypto_uint32.h
264+
* Paper:
265+
* https://cr.yp.to/papers/cryptoint-20250424.pdf
266+
*/
267+
#if __has_attribute(always_inline)
268+
__attribute__((always_inline))
269+
#endif
270+
static APR_INLINE int test_nonzero_timingsafe(apr_uint32_t x)
271+
{
272+
x |= -x; /* sets the most significant bit unless x == 0 */
273+
274+
/* shift bit 31 (MSB) to bit 0 */
275+
x >>= 32-6; /* keep 6 bits */
276+
x += optblocker; /* lose the optimizer */
277+
x >>= 5; /* keep the (original) MSB only */
278+
279+
/* x is now 0 or 1 */
280+
return x & INT_MAX;
281+
}
282+
283+
APR_DECLARE(int) apr_memeq_timingsafe(const void *buf1, const void *buf2,
284+
apr_size_t n)
285+
{
286+
apr_uint32_t diff = 0;
287+
volatile apr_size_t count = n; /* prevent loop unrolling */
288+
apr_size_t i = 0;
289+
290+
for (; i < count; ++i) {
291+
const unsigned char c1 = ((volatile const unsigned char *)buf1)[i];
292+
const unsigned char c2 = ((volatile const unsigned char *)buf2)[i];
293+
294+
diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */
295+
}
296+
297+
/* (diff == 0) <=> (diff != 0) ^ 1 */
298+
return test_nonzero_timingsafe(diff) ^ 1;
299+
}
300+
301+
APR_DECLARE(int) apr_streq_timingsafe(const char *sec1, const char *str2)
302+
{
303+
apr_uint32_t diff = 0;
304+
apr_size_t i1 = 0, i2 = 0;
305+
306+
for (;; ++i2) {
307+
const unsigned char c1 = ((volatile const unsigned char *)sec1)[i1];
308+
const unsigned char c2 = ((volatile const unsigned char *)str2)[i2];
309+
310+
diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */
311+
312+
/* Not a shortest/longest match because an attacker would usually know
313+
* one of the strings and could then determine the length of the other.
314+
* So assume only sec1 and its length are secret and stop the loop at
315+
* the end of str2. If sec1 is shorter than str2 the loop will continue
316+
* by comparing the rest of str2 with the trailing NUL byte of sec1.
317+
* In any case since the diff above is computed up to and including a
318+
* NUL byte, only the same content and length will raise match.
319+
*/
320+
if (!c2) {
321+
break;
322+
}
323+
324+
/* Don't go above sec1's NUL byte */
325+
i1 += test_nonzero_timingsafe(c1);
326+
}
327+
328+
/* (diff == 0) <=> (diff != 0) ^ 1 */
329+
return test_nonzero_timingsafe(diff) ^ 1;
330+
}
331+
332+
APR_DECLARE(int) apr_strneq_timingsafe(const char *sec1, const char *str2,
333+
apr_size_t n)
334+
{
335+
apr_uint32_t diff = 0;
336+
volatile apr_size_t count = n; /* prevent loop unrolling */
337+
apr_size_t i1 = 0, i2 = 0;
338+
339+
for (; i2 < count; ++i2) {
340+
const unsigned char c1 = ((volatile const unsigned char *)sec1)[i1];
341+
const unsigned char c2 = ((volatile const unsigned char *)str2)[i2];
342+
343+
diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */
344+
345+
/* Not a shortest/longest match because an attacker would usually know
346+
* one of the strings and could then determine the length of the other.
347+
* So assume only sec1 and its length are secret and stop the loop at
348+
* the end of str2. If sec1 is shorter than str2 the loop will continue
349+
* by comparing the rest of str2 with the trailing NUL byte of sec1.
350+
* In any case since the diff above is computed up to and including a
351+
* NUL byte, only the same content and length will raise match.
352+
*/
353+
if (!c2) {
354+
break;
355+
}
356+
357+
/* Don't go above sec1's NUL byte */
358+
i1 += test_nonzero_timingsafe(c1);
359+
}
360+
361+
/* (diff == 0) <=> (diff != 0) ^ 1 */
362+
return test_nonzero_timingsafe(diff) ^ 1;
363+
}
364+
247365
#if (!APR_HAVE_MEMCHR)
248366
void *memchr(const void *s, int c, size_t n)
249367
{

test/teststr.c

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,101 @@ static void pstrcat(abts_case *tc, void *data)
408408
"abcdefghij12345");
409409
}
410410

411+
#define TIMINGSAFE_RANDS_NUM 20u
412+
#define TIMINGSAFE_RANDS_LEN 32u
413+
static void timingsafe(abts_case *tc, void *data)
414+
{
415+
struct {
416+
const char *sec;
417+
const char *str;
418+
int eq_res;
419+
int neq_res;
420+
apr_size_t neq_n;
421+
} sample[] = {
422+
{"", "", 1, 1, 0},
423+
{"", "", 1, 1, 1},
424+
{"a", "a", 1, 1, 1},
425+
{"a", "a", 1, 1, 2},
426+
{"a", "b", 0, 0, 1},
427+
{"a", "aa", 0, 1, 1},
428+
{"a", "aa", 0, 0, 2},
429+
{"a", "aa", 0, 0, 3},
430+
{"aa", "a", 0, 1, 1},
431+
{"aa", "a", 0, 0, 2},
432+
{"aa", "a", 0, 0, 3},
433+
{"aa", "aa", 1, 1, 1},
434+
{"aa", "aa", 1, 1, 2},
435+
{"aa", "aa", 1, 1, 3},
436+
{"ab", "ba", 0, 0, 1},
437+
{"ab", "ba", 0, 0, 2},
438+
{"ab", "ba", 0, 0, 3},
439+
{NULL,}
440+
}, *sp;
441+
struct {
442+
char str[TIMINGSAFE_RANDS_LEN+1];
443+
apr_size_t len;
444+
} rands[TIMINGSAFE_RANDS_NUM];
445+
apr_size_t i, j, k;
446+
int res;
447+
448+
/* test the sample */
449+
for (sp = sample; sp->sec; ++sp) {
450+
res = apr_streq_timingsafe(sp->sec, sp->str);
451+
ABTS_INT_EQUAL(tc, strcmp(sp->sec, sp->str) == 0, res);
452+
ABTS_INT_EQUAL(tc, sp->eq_res, res);
453+
454+
res = apr_strneq_timingsafe(sp->sec, sp->str, sp->neq_n);
455+
ABTS_INT_EQUAL(tc, strncmp(sp->sec, sp->str, sp->neq_n) == 0, res);
456+
ABTS_INT_EQUAL(tc, sp->neq_res, res);
457+
458+
if (strlen(sp->sec) == strlen(sp->str)) {
459+
res = apr_memeq_timingsafe(sp->sec, sp->str, strlen(sp->sec));
460+
ABTS_INT_EQUAL(tc, memcmp(sp->sec, sp->str, strlen(sp->sec)) == 0, res);
461+
ABTS_INT_EQUAL(tc, sp->eq_res, res);
462+
}
463+
}
464+
465+
/* test random strings */
466+
memset(rands, 0, sizeof(rands)); /* zero init/pad the whole */
467+
for (i = 0; i < TIMINGSAFE_RANDS_NUM; ++i) {
468+
unsigned char randlen = 0;
469+
apr_generate_random_bytes((void *)&randlen, sizeof(randlen));
470+
rands[i].len = (unsigned int)randlen % TIMINGSAFE_RANDS_LEN;
471+
apr_generate_random_bytes((void *)rands[i].str, rands[i].len);
472+
}
473+
for (i = 0; i < TIMINGSAFE_RANDS_NUM; ++i) {
474+
for (j = i; j < TIMINGSAFE_RANDS_NUM; ++j) {
475+
for (k = (j == i); k < 2; ++k) { /* both ways for j != i */
476+
apr_size_t i1 = (k) ? j : i,
477+
i2 = (k) ? i : j;
478+
const char *s1 = rands[i1].str,
479+
*s2 = rands[i2].str;
480+
unsigned int n1 = rands[i1].len,
481+
n2 = rands[i2].len;
482+
483+
ABTS_INT_EQUAL(tc, strcmp(s1, s2) == 0,
484+
apr_streq_timingsafe(s1, s2));
485+
486+
ABTS_INT_EQUAL(tc, strncmp(s1, s2, n1) == 0,
487+
apr_strneq_timingsafe(s1, s2, n1));
488+
ABTS_INT_EQUAL(tc, strncmp(s1, s2, n2) == 0,
489+
apr_strneq_timingsafe(s1, s2, n2));
490+
491+
/* including trailing \0 */
492+
ABTS_INT_EQUAL(tc, strncmp(s1, s2, n1 + 1) == 0,
493+
apr_strneq_timingsafe(s1, s2, n1 + 1));
494+
ABTS_INT_EQUAL(tc, strncmp(s1, s2, n2 + 1) == 0,
495+
apr_strneq_timingsafe(s1, s2, n2 + 1));
496+
497+
ABTS_INT_EQUAL(tc, memcmp(s1, s2, n1) == 0,
498+
apr_memeq_timingsafe(s1, s2, n1));
499+
ABTS_INT_EQUAL(tc, memcmp(s1, s2, n2) == 0,
500+
apr_memeq_timingsafe(s1, s2, n2));
501+
}
502+
}
503+
}
504+
}
505+
411506
abts_suite *teststr(abts_suite *suite)
412507
{
413508
suite = ADD_SUITE(suite)
@@ -427,6 +522,7 @@ abts_suite *teststr(abts_suite *suite)
427522
abts_run_test(suite, snprintf_overflow, NULL);
428523
abts_run_test(suite, skip_prefix, NULL);
429524
abts_run_test(suite, pstrcat, NULL);
525+
abts_run_test(suite, timingsafe, NULL);
430526

431527
return suite;
432528
}

0 commit comments

Comments
 (0)