Commit 32acc57828 for openssl.org

commit 32acc578282cd6e08bff4dbde211a7aa9c0be1c2
Author: Daniel Kubec <kubec@openssl.foundation>
Date:   Wed Oct 21 20:17:50 2026 +0000

    x509: draw the duplicate-extension hash key with a RAND fallback

    scan_ext_flags() keys the hash table for unknown or dynamically registered
    OIDs with a SipHash key from RAND_bytes_ex(). In a library context without
    a usable RAND, e.g. when only the null provider is loaded, drawing the key
    failed and every certificate carrying such an extension was marked
    EXFLAG_INVALID.

    The key only has to be unpredictable, not secret. Add ossl_rand_hash_salt(),
    which fills a salt for hashing untrusted input and tries in turn:

     1. RAND_bytes_ex() on the library context,
     2. the operating system entropy sources, read directly with
        ossl_rand_get_entropy() and freed with ossl_rand_cleanup_entropy(),
        unless the context has a strict seed source configuration,
     3. a last-resort salt condensed with SipHash from the time, the time
        stamp counter, the process and thread ids, a call counter and a few
        addresses.

    Errors raised by a source that failed are discarded when a later one
    succeeds. The helper is only built into libcrypto; inside the FIPS
    provider RAND is always backed by its own DRBG.

    Assisted-by: Claude:claude-fable-5-1
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Richard Levitte <levitte@openssl.foundation>
    Merge-date: Thu Nov  5 08:07:42 2026
    Merged-from: https://github.com/openssl/openssl/pull/33311

diff --git a/crypto/rand/build.info b/crypto/rand/build.info
index e4436521e7..5d0958f200 100644
--- a/crypto/rand/build.info
+++ b/crypto/rand/build.info
@@ -1,7 +1,8 @@
 LIBS=../../libcrypto

 $COMMON=rand_lib.c
-$CRYPTO=randfile.c rand_err.c rand_deprecated.c prov_seed.c rand_uniform.c
+$CRYPTO=randfile.c rand_err.c rand_deprecated.c prov_seed.c rand_uniform.c \
+        rand_hash_salt.c

 IF[{- !$disabled{'egd'} -}]
   $CRYPTO=$CRYPTO rand_egd.c
diff --git a/crypto/rand/rand_hash_salt.c b/crypto/rand/rand_hash_salt.c
new file mode 100644
index 0000000000..163cee052d
--- /dev/null
+++ b/crypto/rand/rand_hash_salt.c
@@ -0,0 +1,139 @@
+/*
+ * 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
+ */
+
+#include <string.h>
+#include <openssl/err.h>
+#include "crypto/rand.h"
+#include "crypto/siphash.h"
+#include "internal/cryptlib.h"
+#include "internal/time.h"
+#include "internal/tsan_assist.h"
+
+/*
+ * Last resort: condense the current time, the time stamp counter (where the
+ * CPU has one), the process (fork) and thread ids, a call counter and a
+ * couple of addresses (ASLR) into |num| bytes with SipHash-2-4. Within a
+ * process the counter makes the result differ on every call, and the process
+ * id and time keep forked children apart.
+ *
+ * This is not entropy in the cryptographic sense. With a time stamp counter
+ * or address space randomisation it is not practically guessable by a
+ * remote peer, which is all a hash-flooding defense needs. On a platform
+ * with neither, it only rests on the clock, the process and thread ids and
+ * the call count, which a peer may be able to narrow down; nothing better is
+ * left once RAND and the operating system sources have failed.
+ */
+static int hash_salt_fallback(unsigned char *buf, size_t num)
+{
+    static TSAN_QUALIFIER unsigned int counter;
+    static const unsigned char zero_key[SIPHASH_KEY_SIZE] = { 0 };
+    struct {
+        OSSL_TIME now;
+        uint32_t tsc;
+        unsigned int counter;
+        int pid;
+        CRYPTO_THREAD_ID tid;
+        const void *stack;
+        const void *data;
+    } salt;
+    unsigned char block[SIPHASH_MAX_DIGEST_SIZE];
+    size_t off, n;
+    uint64_t i;
+    SIPHASH sip;
+    int ret = 0;
+
+    memset(&salt, 0, sizeof(salt));
+    salt.now = ossl_time_now();
+    salt.tsc = OPENSSL_rdtsc();
+    salt.counter = tsan_counter(&counter);
+    salt.pid = openssl_get_fork_id();
+    salt.tid = CRYPTO_THREAD_get_current_id();
+    salt.stack = &salt;
+    salt.data = &zero_key;
+
+    for (i = 0, off = 0; off < num; i++, off += n) {
+        memset(&sip, 0, sizeof(sip));
+        if (!SipHash_set_hash_size(&sip, SIPHASH_MAX_DIGEST_SIZE)
+            || !SipHash_Init(&sip, zero_key, 0, 0))
+            goto err;
+        SipHash_Update(&sip, (unsigned char *)&i, sizeof(i));
+        SipHash_Update(&sip, (unsigned char *)&salt, sizeof(salt));
+        if (!SipHash_Final(&sip, block, sizeof(block)))
+            goto err;
+        n = num - off < sizeof(block) ? num - off : sizeof(block);
+        memcpy(buf + off, block, n);
+    }
+    ret = 1;
+err:
+    OPENSSL_cleanse(block, sizeof(block));
+    return ret;
+}
+
+/*
+ * Fill |buf| with a |num| byte salt for hashing untrusted input, e.g. the
+ * SipHash key of a hash table indexed by attacker controlled data, so that
+ * colliding inputs cannot be precomputed (hash flooding). The salt only has
+ * to be unpredictable, not secret: never use this for cryptographic keys,
+ * IVs, nonces or anything else that must come from a CSPRNG.
+ *
+ * Such tables must keep working in library contexts that have no usable
+ * RAND, e.g. when only the null provider is loaded, so this tries in turn:
+ *
+ *  1. RAND_bytes_ex() on |ctx|,
+ *  2. the operating system entropy sources, unless |ctx| has a strict seed
+ *     source configuration, which they must not stand in for,
+ *  3. time, time stamp counter, process and thread ids and addresses, see
+ *     hash_salt_fallback(). This is used with strict seeding as well: it
+ *     makes a hash salt, not seed material.
+ *
+ * Errors raised by a source that failed are discarded when a later one
+ * succeeds. Returns 1 on success and 0 if every source failed.
+ */
+int ossl_rand_hash_salt(OSSL_LIB_CTX *ctx, unsigned char *buf, size_t num)
+{
+    unsigned char *seed = NULL;
+    size_t seed_len;
+    int ok;
+
+    if (num == 0)
+        return 1;
+
+    ERR_set_mark();
+    if (RAND_bytes_ex(ctx, buf, num, 0) > 0)
+        goto done;
+
+    /*
+     * Read the operating system sources directly rather than through
+     * ossl_rand_get_user_entropy(), which would consult, and with strict
+     * seeding create, the seed source of |ctx| from within the caller (e.g.
+     * certificate parsing), and whose cleanup cannot reliably tell which of
+     * the two allocated a buffer. ossl_rand_get_entropy() and
+     * ossl_rand_cleanup_entropy() always pair up.
+     */
+    if (num <= RAND_POOL_MAX_LENGTH && !ossl_rand_seed_source_strict(ctx)) {
+        seed_len = ossl_rand_get_entropy(ctx, &seed, (int)(8 * num), num, num);
+        ok = seed_len == num;
+        if (ok)
+            memcpy(buf, seed, num);
+        ossl_rand_cleanup_entropy(ctx, seed, seed_len);
+        if (ok)
+            goto done;
+    }
+
+    if (hash_salt_fallback(buf, num))
+        goto done;
+
+    ERR_clear_last_mark();
+    ERR_raise(ERR_LIB_RAND, RAND_R_ERROR_RETRIEVING_ENTROPY);
+    return 0;
+
+done:
+    ERR_pop_to_mark();
+    return 1;
+}
diff --git a/crypto/x509/v3_purp.c b/crypto/x509/v3_purp.c
index de63ff588a..f1ed8d6bae 100644
--- a/crypto/x509/v3_purp.c
+++ b/crypto/x509/v3_purp.c
@@ -17,7 +17,7 @@
 #include "x509_local.h"
 #include "crypto/objects/obj_dat.h"
 #include "crypto/siphash.h"
-#include <openssl/rand.h>
+#include "crypto/rand.h"

 static int check_ssl_ca(const X509 *x);
 static int check_purpose_ssl_client(const X509_PURPOSE *xp, const X509 *x,
@@ -504,8 +504,16 @@ static void scan_ext_flags(const X509 *x509, uint32_t *flags)
              * time an attacker-controlled OID is seen, so unkeyed hashing
              * cannot be used to force O(n^2) collisions (hash flooding).
              *
+             * The key only has to be unpredictable, not secret, so it is
+             * drawn with ossl_rand_hash_salt(), which still succeeds in a
+             * library context without a usable RAND (e.g. only the null
+             * provider loaded) instead of making every such certificate
+             * with an unknown extension invalid.
+             *
              * The key and the entry wrappers remain valid for the duration of
-             * the scan; the table never outlives this function.
+             * the scan; the table never outlives this function. Only this
+             * and the later extensions can be inserted, so the wrappers are
+             * sized for count - i entries.
              *
              * If the table cannot be set up or an insert fails, fail closed
              * with EXFLAG_INVALID, as ossl_x509v3_cache_extensions() does for
@@ -517,8 +525,8 @@ static void scan_ext_flags(const X509 *x509, uint32_t *flags)
                 OSSL_LIB_CTX *libctx = NULL;

                 ossl_x509_get0_libctx(x509, &libctx, NULL);
-                if (RAND_bytes_ex(libctx, (unsigned char *)k, sizeof(k), 0) <= 0
-                    || (arr = OPENSSL_malloc_array(count, sizeof(*arr))) == NULL
+                if (!ossl_rand_hash_salt(libctx, (unsigned char *)k, sizeof(k))
+                    || (arr = OPENSSL_malloc_array(count - i, sizeof(*arr))) == NULL
                     || (h = OPENSSL_LH_new(oid_hash, oid_cmp)) == NULL) {
                     *flags |= EXFLAG_INVALID;
                     break;
diff --git a/include/crypto/rand.h b/include/crypto/rand.h
index 183fd5ec57..4ac417ea43 100644
--- a/include/crypto/rand.h
+++ b/include/crypto/rand.h
@@ -167,6 +167,22 @@ uint32_t ossl_rand_uniform_uint32(OSSL_LIB_CTX *ctx, uint32_t upper, int *err);
 uint32_t ossl_rand_range_uint32(OSSL_LIB_CTX *ctx, uint32_t lower, uint32_t upper,
     int *err);

+/*
+ * Random salt for hashing untrusted input, e.g. the SipHash key of a hash
+ * table, with fallbacks for library contexts without a usable RAND.
+ * Not suitable for cryptographic keys, IVs, nonces or other secrets.
+ *
+ * Only built into libcrypto: its fallbacks rely on the libcrypto seeding
+ * code (ossl_rand_get_entropy()), which the FIPS provider does not have,
+ * and inside the FIPS provider RAND is always backed by its own DRBG, so no
+ * fallback is needed there. Hiding the declaration from FIPS_MODULE builds
+ * turns an accidental use into a compile error rather than a link failure
+ * of the FIPS provider.
+ */
+#ifndef FIPS_MODULE
+int ossl_rand_hash_salt(OSSL_LIB_CTX *ctx, unsigned char *buf, size_t num);
+#endif
+
 /*
  * Check if the named provider is the nominated entropy/random provider.
  * If it is, use it.
diff --git a/test/rand_test.c b/test/rand_test.c
index 47ff7286be..f6014acd32 100644
--- a/test/rand_test.c
+++ b/test/rand_test.c
@@ -23,6 +23,7 @@
 #include <openssl/params.h>
 #include <openssl/proverr.h>
 #include "crypto/rand.h"
+#include "internal/nelem.h"
 #include "testutil.h"

 static char *configfile;
@@ -156,6 +157,81 @@ err:
     return res;
 }

+/*
+ * ossl_rand_hash_salt() must hand out a salt from each of its sources and
+ * leave no errors behind:
+ *
+ * idx 0: a fresh library context, served by RAND_bytes_ex().
+ * idx 1: only the null provider is loaded, so RAND is unavailable and the
+ *        salt comes from the operating system entropy sources. With strict
+ *        seeding, which enable-fips-jitter builds force, those are skipped
+ *        and this covers the fallback of idx 2 instead.
+ * idx 2: strict seeding from a TEST-RAND seed source that has no entropy,
+ *        so RAND fails, the operating system sources must not stand in for
+ *        the seed source, and the salt comes from the time, counter, process
+ *        and thread id fallback. Not run in enable-fips-jitter builds, which
+ *        force the JITTER seed source.
+ */
+static int test_rand_hash_salt(int idx)
+{
+    static const size_t lens[] = { 1, 8, 16, 17, 40 };
+    OSSL_LIB_CTX *ctx = NULL;
+    OSSL_PROVIDER *nullprov = NULL;
+    unsigned char a[40], b[40];
+    size_t i;
+    int ok, res = 0;
+
+    if (!TEST_ptr(ctx = OSSL_LIB_CTX_new()))
+        goto err;
+    if (idx == 1 && !TEST_ptr(nullprov = OSSL_PROVIDER_load(ctx, "null")))
+        goto err;
+    if (idx == 2 && !TEST_true(OSSL_LIB_CTX_load_config(ctx, strictconfigfile)))
+        goto err;
+
+    /* Make sure each case gets the source it is about */
+    if (idx > 0) {
+        ERR_set_mark();
+        ok = RAND_bytes_ex(ctx, a, sizeof(a), 0) <= 0;
+#ifndef OPENSSL_RAND_SEED_NONE
+        /* Without strict seeding the operating system sources serve it */
+        if (idx == 1 && !ossl_rand_seed_source_strict(ctx)) {
+            unsigned char *seed = NULL;
+            size_t seed_len = ossl_rand_get_entropy(ctx, &seed, 8 * 16, 16, 16);
+
+            ok = ok && seed_len == 16;
+            ossl_rand_cleanup_entropy(ctx, seed, seed_len);
+        }
+#endif
+        /* Strict seeding keeps the operating system sources out */
+        if (idx == 2)
+            ok = ok && ossl_rand_seed_source_strict(ctx);
+        ERR_pop_to_mark();
+        if (!TEST_true(ok))
+            goto err;
+    }
+
+    /* Only errors raised by ossl_rand_hash_salt() itself count below */
+    ERR_clear_error();
+    if (!TEST_true(ossl_rand_hash_salt(ctx, a, 0)))
+        goto err;
+    for (i = 0; i < OSSL_NELEM(lens); i++)
+        if (!TEST_true(ossl_rand_hash_salt(ctx, a, lens[i]))
+            || !TEST_ulong_eq(ERR_peek_error(), 0))
+            goto err;
+
+    /* Consecutive salts must differ */
+    if (!TEST_true(ossl_rand_hash_salt(ctx, a, sizeof(a)))
+        || !TEST_true(ossl_rand_hash_salt(ctx, b, sizeof(b)))
+        || !TEST_mem_ne(a, sizeof(a), b, sizeof(b)))
+        goto err;
+
+    res = 1;
+err:
+    OSSL_PROVIDER_unload(nullprov);
+    OSSL_LIB_CTX_free(ctx);
+    return res;
+}
+
 /*
  * Check that creating the primary DRBG creates the seed source and
  * stores it in the library context: later users must keep getting the
@@ -1210,6 +1286,11 @@ int setup_tests(void)
     ADD_TEST(test_jitter_fips_indicator);
 #endif
     ADD_TEST(test_rand_uniform);
+#ifdef OPENSSL_NO_FIPS_JITTER
+    ADD_ALL_TESTS(test_rand_hash_salt, 3);
+#else
+    ADD_ALL_TESTS(test_rand_hash_salt, 2);
+#endif
     ADD_TEST(test_rand_primary_seed_stored);

     if (OSSL_PROVIDER_available(NULL, "fips")
diff --git a/test/x509_internal_test.c b/test/x509_internal_test.c
index 316baa8cbc..b2798cd626 100644
--- a/test/x509_internal_test.c
+++ b/test/x509_internal_test.c
@@ -1247,14 +1247,18 @@ err:
 }

 /*
- * scan_ext_flags() must not silently stop checking extensions when the
- * duplicate-extension table cannot be set up. In a library context with no
- * usable RAND the per-scan SipHash key cannot be drawn; an unknown critical
- * extension must then still be reported, either as unsupported-critical or by
- * failing closed with EXFLAG_INVALID. Loading the null provider explicitly
- * stops the default provider from being activated, so RAND is unavailable.
+ * scan_ext_flags() keys its duplicate-extension table with a random SipHash
+ * key. In a library context with no usable RAND, the key must still be drawn
+ * (from the ossl_rand_hash_salt() fallbacks) and the extensions fully
+ * checked, rather than every certificate with an unknown extension being
+ * marked EXFLAG_INVALID. Loading the null provider explicitly stops the
+ * default provider from being activated, so RAND is unavailable.
+ *
+ * idx 0: a single unknown critical extension is reported as unsupported
+ *        critical.
+ * idx 1: a repeated unknown extension is reported as a duplicate.
  */
-static int tests_x509_check_ext_unknown_critical_no_rand(void)
+static int tests_x509_check_ext_unknown_oid_no_rand(int idx)
 {
     OSSL_LIB_CTX *libctx = NULL;
     OSSL_PROVIDER *nullprov = NULL;
@@ -1270,22 +1274,25 @@ static int tests_x509_check_ext_unknown_critical_no_rand(void)
     if (!TEST_ptr(libctx = OSSL_LIB_CTX_new())
         || !TEST_ptr(nullprov = OSSL_PROVIDER_load(libctx, "null")))
         goto err;
-    if (RAND_bytes_ex(libctx, buf, sizeof(buf), 0) > 0) {
-        TEST_info("RAND unexpectedly available in a null-provider context");
-        test = 1;
+    /* Otherwise the fallbacks this test is about would not be exercised */
+    if (!TEST_int_le(RAND_bytes_ex(libctx, buf, sizeof(buf), 0), 0))
         goto err;
-    }
     ERR_clear_error();
     if (!TEST_ptr(x = X509_new_ex(libctx, NULL))
         || !TEST_true(X509_set_version(x, X509_VERSION_3))
         || !TEST_ptr(obj = OBJ_txt2obj("1.3.6.1.4.1.99999.2.1", 1))
         || !TEST_ptr(oct = ASN1_OCTET_STRING_new())
         || !TEST_true(ASN1_OCTET_STRING_set(oct, data, sizeof(data)))
-        || !TEST_ptr(ext = X509_EXTENSION_create_by_OBJ(NULL, obj, 1, oct))
-        || !TEST_true(X509_add_ext(x, ext, -1)))
+        || !TEST_ptr(ext = X509_EXTENSION_create_by_OBJ(NULL, obj, !idx, oct))
+        || !TEST_true(X509_add_ext(x, ext, -1))
+        || (idx == 1 && !TEST_true(X509_add_ext(x, ext, -1))))
         goto err;
     flags = X509_get_extension_flags(x);
-    if (!TEST_true((flags & (EXFLAG_CRITICAL | EXFLAG_INVALID)) != 0))
+    if (!TEST_false(flags & EXFLAG_INVALID))
+        goto err;
+    if (idx == 0 && !TEST_true(flags & EXFLAG_CRITICAL))
+        goto err;
+    if (idx == 1 && !TEST_true(flags & EXFLAG_DUPLICATE))
         goto err;
     test = 1;
 err:
@@ -1493,7 +1500,7 @@ int setup_tests(void)
     ADD_TEST(tests_x509_check_ext_duplicity);
     ADD_TEST(tests_x509_check_ext_duplicity_nid_undef);
     ADD_TEST(tests_x509_check_ext_duplicity_nid_dynamic);
-    ADD_TEST(tests_x509_check_ext_unknown_critical_no_rand);
+    ADD_ALL_TESTS(tests_x509_check_ext_unknown_oid_no_rand, 2);
     ADD_ALL_TESTS(test_x509_attribute_bit_string, 2);

     ADD_TEST(test_X509_ALGOR_set_md_sha1);