Commit 4a98c4ade0 for openssl.org
commit 4a98c4ade08bf7e1abd3fb1958782dbf8bf4680a
Author: David Foster <david@dafoster.net>
Date: Sun Aug 9 09:13:16 2026 -0400
Test the constant-time validation helpers
Suggested by @bbbrumley in review of #31617.
References #15076 (add constant time coverage everywhere).
Assisted-by: Claude:claude-sonnet-5
Reviewed-by: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Reviewed-by: Shane Lontis <shane.lontis@oracle.com>
Merge-date: Tue Sep 1 14:06:52 2026
Merged-from: https://github.com/openssl/openssl/pull/32255
diff --git a/.github/workflows/ct-validation-daily.yml b/.github/workflows/ct-validation-daily.yml
index 499882e850..6151f8ef5d 100644
--- a/.github/workflows/ct-validation-daily.yml
+++ b/.github/workflows/ct-validation-daily.yml
@@ -141,7 +141,12 @@ jobs:
# - memcmp: test_crypto_memcmp
# - ML-KEM: test_internal_ml_kem
# - ML-DSA: test_internal_ml_dsa
+ #
+ # Also covered:
+ # - test_ct_validation_helpers: The Valgrind-based constant-time
+ # validation helpers from constant_time.h, like CONSTTIME_SECRET and
+ # CONSTTIME_DECLASSIFY
run: |
- make TESTS="test_internal_ml_kem test_internal_ml_dsa test_crypto_memcmp" \
+ make TESTS="test_internal_ml_kem test_internal_ml_dsa test_crypto_memcmp test_ct_validation_helpers" \
OSSL_VALGRIND_CT=yes \
test
diff --git a/test/build.info b/test/build.info
index 3725de408d..618026945d 100644
--- a/test/build.info
+++ b/test/build.info
@@ -53,7 +53,8 @@ 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 crypto_memcmp_test safe_math_test verify_extra_test clienthellotest \
+ constant_time_test crypto_memcmp_test ct_validation_helpers_test \
+ safe_math_test verify_extra_test clienthellotest \
packettest asynctest secmemtest srptest memleaktest stack_test \
ct_test threadstest d2i_test \
ssl_test_ctx_test ssl_test x509aux cipherlist_test asynciotest \
@@ -381,6 +382,10 @@ IF[{- !$disabled{tests} -}]
INCLUDE[crypto_memcmp_test]=../include ../apps/include
DEPEND[crypto_memcmp_test]=../libcrypto libtestutil.a
+ SOURCE[ct_validation_helpers_test]=ct_validation_helpers_test.c
+ INCLUDE[ct_validation_helpers_test]=../include ../apps/include
+ DEPEND[ct_validation_helpers_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/ct_validation_helpers_test.c b/test/ct_validation_helpers_test.c
new file mode 100644
index 0000000000..68f3465a95
--- /dev/null
+++ b/test/ct_validation_helpers_test.c
@@ -0,0 +1,203 @@
+/*
+ * 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
+ */
+
+/*
+ * Tests for the constant-time validation helpers in constant_time.h:
+ * - CONSTTIME_SECRET
+ * - CONSTTIME_DECLASSIFY
+ * - constant_time_declassify_u32()
+ *
+ * Most of the modes below check whether Valgrind flags code that is
+ * deliberately NOT constant-time. The accompanying recipe asserts that the
+ * harness flags it. Each such mode is a separate process because Valgrind's
+ * verdict is delivered as a process exit code via Valgrind's --error-exitcode.
+ *
+ * The "identity" mode is different: It ensures constant_time_declassify_u32()
+ * still exists and functions correctly when used OUTSIDE enable-ct-validation.
+ * Thus this mode uses the ordinary test framework pass/fail signal
+ * and does not require Valgrind.
+ */
+
+#include <string.h>
+
+#include <openssl/crypto.h>
+
+#include "internal/constant_time.h"
+#include "internal/nelem.h"
+#include "testutil.h"
+
+#define SECRET_LEN 32
+
+/*
+ * Volatile sink for results computed by tests below.
+ *
+ * Must be volatile so that the compiler cannot delete the offending code as
+ * dead, which would silently cause tests to check nothing and pass falsely.
+ */
+static volatile unsigned int sink;
+
+static const unsigned char lut[16] = {
+ 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3 /* arbitrary values */
+};
+
+static void fill_secret(unsigned char *secret)
+{
+ size_t i;
+
+ for (i = 0; i < SECRET_LEN; i++)
+ secret[i] = (unsigned char)(i * 7 + 1);
+}
+
+/*
+ * "branch" mode: Ensure Valgrind flags a branch (a loop iteration count) on a
+ * secret marked with CONSTTIME_SECRET.
+ *
+ * Valgrind should report:
+ * "Conditional jump or move depends on uninitialised value(s)".
+ */
+static int test_secret_dependent_branch(void)
+{
+ unsigned char secret[SECRET_LEN];
+ unsigned int i, n;
+
+ fill_secret(secret);
+ CONSTTIME_SECRET(secret, sizeof(secret));
+
+ n = secret[0] & 0x0f;
+ for (i = 0; i < n; i++)
+ sink = i;
+
+ CONSTTIME_DECLASSIFY(secret, sizeof(secret));
+ return 1;
+}
+
+/*
+ * "index" mode: Ensure Valgrind flags a lookup table index derived from a
+ * secret marked with CONSTTIME_SECRET.
+ *
+ * Valgrind should report:
+ * "Use of uninitialised value".
+ */
+static int test_secret_dependent_index(void)
+{
+ unsigned char secret[SECRET_LEN];
+
+ fill_secret(secret);
+ CONSTTIME_SECRET(secret, sizeof(secret));
+
+ sink = lut[secret[0] & 0x0f];
+
+ CONSTTIME_DECLASSIFY(secret, sizeof(secret));
+ return 1;
+}
+
+/*
+ * "control" mode: Ensure Valgrind does NOT flag a branch on a secret that has
+ * been declassified by CONSTTIME_DECLASSIFY.
+ */
+static int test_constant_time_control(void)
+{
+ unsigned char secret[SECRET_LEN];
+ unsigned int acc = 0;
+ size_t i;
+
+ fill_secret(secret);
+ CONSTTIME_SECRET(secret, sizeof(secret));
+
+ for (i = 0; i < sizeof(secret); i++)
+ acc |= secret[i];
+
+ CONSTTIME_DECLASSIFY(&acc, sizeof(acc));
+ CONSTTIME_DECLASSIFY(secret, sizeof(secret));
+
+ if (acc == 0)
+ sink = 1;
+ else
+ sink = 2;
+
+ return TEST_uint_eq(sink, 2);
+}
+
+/*
+ * "mask" mode: Ensure Valgrind does NOT flag a branch on the result of
+ * constant_time_declassify_u32().
+ *
+ * Uses the same calling pattern as constant_time_declassify_u32's current
+ * unique caller:
+ * - A constant_time_ge()/constant_time_lt() mask (0 or all-ones) computed from
+ * secret data is declassified and immediately branched on. The boolean
+ * outcome of a rejection-sampling check is safe to leak even though the data
+ * behind it is not.
+ */
+static int test_declassify_mask(void)
+{
+ unsigned char secret[SECRET_LEN];
+ unsigned int mask;
+
+ fill_secret(secret);
+ CONSTTIME_SECRET(secret, sizeof(secret));
+
+ mask = constant_time_ge(secret[0], 0x80);
+
+ if (constant_time_declassify_u32(mask))
+ sink = 1;
+ else
+ sink = 2;
+
+ CONSTTIME_DECLASSIFY(secret, sizeof(secret));
+ return 1;
+}
+
+/*
+ * "identity" mode: Ensure constant_time_declassify_u32() returns its input
+ * unmodified, independent of whether Valgrind or enable-ct-validation are
+ * enabled.
+ */
+static int test_declassify_u32_identity(void)
+{
+ static const uint32_t values[] = { 0, 1, 0xdeadbeef, 0xffffffff };
+ size_t i;
+ int ret = 1;
+
+ for (i = 0; i < OSSL_NELEM(values); i++)
+ ret &= TEST_uint_eq(constant_time_declassify_u32(values[i]), values[i]);
+ return ret;
+}
+
+OPT_TEST_DECLARE_USAGE("branch|index|control|mask|identity\n")
+
+int setup_tests(void)
+{
+ const char *mode;
+
+ if (!test_skip_common_options()) {
+ TEST_error("Error parsing test options\n");
+ return 0;
+ }
+
+ if (!TEST_ptr(mode = test_get_argument(0)))
+ return 0;
+
+ if (strcmp(mode, "branch") == 0)
+ ADD_TEST(test_secret_dependent_branch);
+ else if (strcmp(mode, "index") == 0)
+ ADD_TEST(test_secret_dependent_index);
+ else if (strcmp(mode, "control") == 0)
+ ADD_TEST(test_constant_time_control);
+ else if (strcmp(mode, "mask") == 0)
+ ADD_TEST(test_declassify_mask);
+ else if (strcmp(mode, "identity") == 0)
+ ADD_TEST(test_declassify_u32_identity);
+ else {
+ TEST_error("Unknown mode '%s'\n", mode);
+ return 0;
+ }
+
+ return 1;
+}
diff --git a/test/recipes/90-test_ct_validation_helpers.t b/test/recipes/90-test_ct_validation_helpers.t
new file mode 100644
index 0000000000..bd795c34ac
--- /dev/null
+++ b/test/recipes/90-test_ct_validation_helpers.t
@@ -0,0 +1,104 @@
+#! /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
+
+# Tests for the constant-time validation helpers in constant_time.h:
+# - CONSTTIME_SECRET
+# - CONSTTIME_DECLASSIFY
+# - constant_time_declassify_u32()
+#
+# Most of the modes below check whether Valgrind flags code that is
+# deliberately NOT constant-time. The accompanying recipe asserts that the
+# harness flags it. Each such mode is a separate process because Valgrind's
+# verdict is delivered as a process exit code via Valgrind's --error-exitcode.
+#
+# The "identity" mode is different: It ensures constant_time_declassify_u32()
+# still exists and functions correctly when used OUTSIDE enable-ct-validation.
+# Thus this mode uses the ordinary test framework pass/fail signal
+# and does not require Valgrind.
+
+use strict;
+use warnings;
+
+use OpenSSL::Test qw(:DEFAULT result_file);
+use OpenSSL::Test::Utils;
+
+setup("test_ct_validation_helpers");
+
+my $NUM_VALGRIND_TESTS = 6;
+my $NUM_ALWAYS_TESTS = 1;
+
+plan tests => $NUM_VALGRIND_TESTS + $NUM_ALWAYS_TESTS;
+
+SKIP: {
+ # Ensure test binary is wrapped in Valgrind
+ skip "This test requires a build with enable-ct-validation", $NUM_VALGRIND_TESTS
+ if disabled("ct-validation");
+ skip "This test requires the test suite to be run with OSSL_VALGRIND_CT=yes", $NUM_VALGRIND_TESTS
+ unless defined $ENV{OSSL_VALGRIND_CT};
+
+ # Run one mode of ct_validation_helpers_test, capturing Valgrind's report
+ # (which it writes to stderr). Returns the run's success flag and report text.
+ my $ct_run = sub {
+ my ($mode) = @_;
+ my $errfile = result_file("ct_validation_helpers_$mode.txt");
+ my $ok = run(test(["ct_validation_helpers_test", $mode], stderr => $errfile));
+ my $report = "";
+
+ if (open(my $fh, '<', $errfile)) {
+ local $/ = undef;
+ $report = <$fh>;
+ close($fh);
+ }
+ return ($ok, $report);
+ };
+
+ my ($ok, $report);
+
+ # "branch" mode: Ensure Valgrind flags a branch (a loop iteration count) on a
+ # secret marked with CONSTTIME_SECRET.
+ #
+ # Valgrind should report:
+ # "Conditional jump or move depends on uninitialised value(s)".
+ ($ok, $report) = $ct_run->("branch");
+ ok(!$ok, "secret-dependent branch is rejected");
+ like($report, qr/uninitiali[sz]ed/i,
+ "secret-dependent branch is reported as an uninitialised-value error");
+
+ # "index" mode: Ensure Valgrind flags a lookup table index derived from a
+ # secret marked with CONSTTIME_SECRET.
+ #
+ # Valgrind should report:
+ # "Use of uninitialised value".
+ ($ok, $report) = $ct_run->("index");
+ ok(!$ok, "secret-dependent table index is rejected");
+ like($report, qr/uninitiali[sz]ed/i,
+ "secret-dependent table index is reported as an uninitialised-value error");
+
+ # "control" mode: Ensure Valgrind does NOT flag a branch on a secret that has
+ # been declassified by CONSTTIME_DECLASSIFY.
+ ($ok, $report) = $ct_run->("control");
+ ok($ok, "constant-time code with a declassified output is accepted");
+
+ # "mask" mode: Ensure Valgrind does NOT flag a branch on the result of
+ # constant_time_declassify_u32().
+ #
+ # Uses the same calling pattern as constant_time_declassify_u32's current
+ # unique caller:
+ # - A constant_time_ge()/constant_time_lt() mask (0 or all-ones) computed from
+ # secret data is declassified and immediately branched on. The boolean
+ # outcome of a rejection-sampling check is safe to leak even though the data
+ # behind it is not.
+ ($ok, $report) = $ct_run->("mask");
+ ok($ok, "a declassified comparison mask can be branched on");
+}
+
+# "identity" mode: Ensure constant_time_declassify_u32() returns its input
+# unmodified, independent of whether Valgrind or enable-ct-validation are
+# enabled.
+ok(run(test(["ct_validation_helpers_test", "identity"])),
+ "constant_time_declassify_u32() is an identity function");