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