Commit 1a824261ef for openssl.org
commit 1a824261ef16231ea036b7935613ea4b219e3a42
Author: David Foster <david@dafoster.net>
Date: Thu Jun 4 22:02:44 2026 -0400
Add constant-time validation for CRYPTO_memcmp
Add test/crypto_memcmp_test.c which provides functional coverage for
CRYPTO_memcmp under regular builds and constant-time coverage under
enable-ct-validation builds.
The added constant-time coverage checks:
- there are no data dependent branches or memory accesses,
on x86_64 and aarch64 architectures
The added constant-time coverage does NOT check:
- there are no data-dependent variable-time instructions, such as
instructions NOT on the x86 Data Operand Independent Timing list
or NOT on the ARM Data-Independent Timing list
- any architectures beyond x86_64 and aarch64
New CONSTTIME_SECRET annotations live only in the test rather than in
the generic C version of CRYPTO_memcmp so that both the C and
assembler versions of CRYPTO_memcmp are constant-time covered.
CRYPTO_memcmp directly backs CPython's secrets.compare_digest() and
hmac.compare_digest(), so a timing leak in it is high impact, yet it had
essentially no direct test coverage.
References #15076.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-by: Milan Broz <mbroz@openssl.org>
Reviewed-by: Bob Beck <beck@openssl.org>
MergeDate: Thu Jun 11 16:11:58 2026
(Merged from https://github.com/openssl/openssl/pull/31398)
diff --git a/.github/workflows/ct-validation-daily.yml b/.github/workflows/ct-validation-daily.yml
index 5ae384e5a5..18d8911fcb 100644
--- a/.github/workflows/ct-validation-daily.yml
+++ b/.github/workflows/ct-validation-daily.yml
@@ -6,7 +6,8 @@
# https://www.openssl.org/source/license.html
name: Constant-time validation (daily)
-# Verifies that ML-KEM and ML-DSA signing do not branch on secret data.
+# Verifies that several algorithms needing constant-time execution do not
+# branch on secret data.
#
# The library is built with enable-ct-validation, which defines
# OPENSSL_CONSTANT_TIME_VALIDATION and causes secret regions to be marked
@@ -49,11 +50,26 @@ jobs:
strategy:
fail-fast: false
matrix:
+ # Constant-timeness is a property of the generated machine code, which
+ # the compiler derives differently per architecture. Therefore we verify
+ # both the assembly and C implementations on every architecture we can
+ # run Valgrind on.
include:
+ # Default builds use assembler implementations (when available)
- name: linux-x86_64
runs-on: ubuntu-latest
+ config_extra: ""
- name: linux-aarch64
runs-on: ubuntu-24.04-arm
+ config_extra: ""
+
+ # no-asm builds always use C implementations
+ - name: linux-x86_64-no-asm
+ runs-on: ubuntu-latest
+ config_extra: no-asm
+ - name: linux-aarch64-no-asm
+ runs-on: ubuntu-24.04-arm
+ config_extra: no-asm
name: CT validation (${{ matrix.name }})
runs-on: ${{ matrix.runs-on }}
@@ -72,18 +88,23 @@ jobs:
- name: Configure with CT validation enabled
run: |
- ./Configure enable-ct-validation
+ ./Configure enable-ct-validation ${{ matrix.config_extra }}
./configdata.pm --dump
- name: Build
run: make -j$(nproc)
- - name: Run ML-KEM and ML-DSA CT validation under Valgrind
+ - name: Run CT validation under Valgrind
# OSSL_VALGRIND_CT=yes causes OpenSSL::Test::test() to wrap each
# test binary with valgrind --track-origins=yes --error-exitcode=1.
# util/wrap.pl -> util/shlib_wrap.sh sets LD_LIBRARY_PATH first, so
# the shared libraries are found correctly.
+ #
+ # Algorithms covered:
+ # - memcmp: test_crypto_memcmp
+ # - ML-KEM: test_internal_ml_kem
+ # - ML-DSA: test_internal_ml_dsa
run: |
- make TESTS="test_internal_ml_kem test_internal_ml_dsa" \
+ make TESTS="test_internal_ml_kem test_internal_ml_dsa test_crypto_memcmp" \
OSSL_VALGRIND_CT=yes \
test
diff --git a/crypto/cpuid.c b/crypto/cpuid.c
index d659135919..89d841bfd0 100644
--- a/crypto/cpuid.c
+++ b/crypto/cpuid.c
@@ -193,6 +193,10 @@ void OPENSSL_cpuid_setup(void)
* not volatile, but compilers do this in practice anyway.
*
* There are also assembler versions of this function.
+ *
+ * This C version and the per-architecture assembler versions are all verified
+ * to be constant-time under enable-ct-validation for Valgrind-supported
+ * architectures, currently x86_64 and aarch64.
*/
#undef CRYPTO_memcmp
int CRYPTO_memcmp(const void *in_a, const void *in_b, size_t len)
diff --git a/test/build.info b/test/build.info
index 6084d0b16b..7e3bf669c7 100644
--- a/test/build.info
+++ b/test/build.info
@@ -49,7 +49,7 @@ IF[{- !$disabled{tests} -}]
v3nametest v3ext byteorder_test punycode_test evp_byname_test \
crltest danetest bad_dtls_test lhash_test sparse_array_test \
conf_include_test params_api_test params_conversion_test \
- constant_time_test safe_math_test verify_extra_test clienthellotest \
+ constant_time_test crypto_memcmp_test safe_math_test verify_extra_test clienthellotest \
packettest asynctest secmemtest srptest memleaktest stack_test \
dtlsv1listentest ct_test threadstest d2i_test \
ssl_test_ctx_test ssl_test x509aux cipherlist_test asynciotest \
@@ -364,6 +364,10 @@ IF[{- !$disabled{tests} -}]
INCLUDE[constant_time_test]=../include ../apps/include
DEPEND[constant_time_test]=../libcrypto libtestutil.a
+ SOURCE[crypto_memcmp_test]=crypto_memcmp_test.c
+ INCLUDE[crypto_memcmp_test]=../include ../apps/include
+ DEPEND[crypto_memcmp_test]=../libcrypto libtestutil.a
+
SOURCE[safe_math_test]=safe_math_test.c
INCLUDE[safe_math_test]=../include ../apps/include
DEPEND[safe_math_test]=../libcrypto libtestutil.a
diff --git a/test/crypto_memcmp_test.c b/test/crypto_memcmp_test.c
new file mode 100644
index 0000000000..479c54cf76
--- /dev/null
+++ b/test/crypto_memcmp_test.c
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+/*
+ * Functional and constant-time tests for CRYPTO_memcmp().
+ *
+ * CRYPTO_memcmp() must compare its two operands without any control-flow
+ * branch or memory access that depends on the operand *contents* (only the
+ * length is public).
+ *
+ * When built with enable-ct-validation (OPENSSL_CONSTANT_TIME_VALIDATION),
+ * CONSTTIME_SECRET marks the operands as "undefined" for Valgrind's memcheck;
+ * any branch or memory index derived from them then makes Valgrind exit
+ * non-zero. Because the taint is injected here at the call site, this test
+ * verifies whichever CRYPTO_memcmp implementation is actually linked -- the
+ * per-arch assembler version where one exists (x86_64, aarch64, ...), or the
+ * C fallback in crypto/cpuid.c otherwise. Outside a CT build the macros are
+ * no-ops and this is an ordinary functional test.
+ *
+ * The accumulated comparison result is the function's intended *public*
+ * output, so it is declassified before being asserted on; the operand buffers
+ * are declassified too so the (stack) memory does not stay tainted.
+ */
+
+#include <openssl/crypto.h>
+
+#include "internal/nelem.h"
+#include "internal/constant_time.h"
+#include "testutil.h"
+
+#define MAX_LEN 64
+
+/*
+ * len == 16 exercises the dedicated fast path in several assembler versions
+ * (e.g. crypto/x86_64cpuid.pl); the other lengths exercise the byte loop and
+ * the empty-input early return.
+ */
+static const struct {
+ /* byte length of buffers to compare; must be <= MAX_LEN */
+ size_t len;
+ /* index at which the second buffer differs, or -1 for equal buffers */
+ int diff_pos;
+} memcmp_cases[] = {
+ /* empty: always equal */
+ { 0, -1 },
+ { 1, -1 },
+ { 1, 0 },
+
+ /* asm fast path (length = 16) */
+ { 16, -1 },
+ { 16, 0 },
+ { 16, 8 },
+ { 16, 15 },
+
+ /* byte loop */
+ { 64, -1 },
+ { 64, 0 },
+ { 64, 31 },
+ { 64, 63 },
+};
+
+static int test_crypto_memcmp(int idx)
+{
+ size_t i;
+ size_t len = memcmp_cases[idx].len;
+ int diff_pos = memcmp_cases[idx].diff_pos;
+ /* nonzero result iff buffers differ */
+ int expected = diff_pos >= 0;
+ unsigned char a[MAX_LEN], b[MAX_LEN];
+ int result;
+
+ for (i = 0; i < len; i++)
+ a[i] = b[i] = (unsigned char)(i * 7 + 1);
+ if (diff_pos >= 0)
+ b[diff_pos] ^= 0xff;
+
+ CONSTTIME_SECRET(a, len);
+ CONSTTIME_SECRET(b, len);
+
+ result = CRYPTO_memcmp(a, b, len);
+
+ CONSTTIME_DECLASSIFY(&result, sizeof(result));
+ CONSTTIME_DECLASSIFY(a, len);
+ CONSTTIME_DECLASSIFY(b, len);
+
+ if (!TEST_int_eq(result != 0, expected))
+ return 0;
+ else
+ return 1;
+}
+
+int setup_tests(void)
+{
+ ADD_ALL_TESTS(test_crypto_memcmp, OSSL_NELEM(memcmp_cases));
+ return 1;
+}
diff --git a/test/recipes/90-test_crypto_memcmp.t b/test/recipes/90-test_crypto_memcmp.t
new file mode 100644
index 0000000000..6ddd279458
--- /dev/null
+++ b/test/recipes/90-test_crypto_memcmp.t
@@ -0,0 +1,12 @@
+#! /usr/bin/env perl
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+
+use OpenSSL::Test::Simple;
+
+simple_test("test_crypto_memcmp", "crypto_memcmp_test");