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");