Commit 5f9b4da9dd for openssl.org

commit 5f9b4da9ddf835aa7c532202d8d84eab02a07067
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Tue Jul 14 00:27:25 2026 +0200

    rand: add seed_strict option enforcing the configured seed source

    When a provider requests seeding material via the get_user_entropy or
    get_user_nonce core callbacks before the primary DRBG was created, the
    request silently falls back to the operating system entropy sources,
    because the seed source EVP_RAND_CTX only comes into existence as a
    side effect of creating the primary DRBG.  Whether a configured seed
    source is used therefore depends on operation order: for example with
    the FIPS provider, genrsa happened to use the configured source while
    ecparam -genkey did not.

    This fallback is acceptable for most setups, but not when the seed
    source selection is meant to be a guarantee, for example for compliance
    reasons.  Add a seed_strict option to the [random] configuration
    section which makes the callbacks instantiate the seed source on
    demand and turns an unusable seed source into an error rather than a
    silent fallback.  The option is off by default with two exceptions:
    selecting the JITTER seed source, at runtime or at build time via
    -DOPENSSL_DEFAULT_SEED_SRC, seeds strictly unless the option disables
    it, and enable-fips-jitter builds always seed strictly.

    The boolean value parsing is shared with the provider section by
    factoring the existing parser out into ossl_conf_parse_bool().  A build
    time default property query can now be set with
    -DOPENSSL_DEFAULT_SEED_PROPQ.

    Add a regression test covering the on-demand creation and the no
    fallback behaviour on the strict provider seeding path, a test for the
    default non-strict fallback behaviour including the JITTER strictness
    default, and [random] seed configuration coverage in the rand config
    test recipe.

    Fixes #25941

    Assisted-by: Claude:claude-fable-5
    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Tue Sep  1 12:07:39 2026
    Merged-from: https://github.com/openssl/openssl/pull/31939

diff --git a/CHANGES.md b/CHANGES.md
index b3daa8cf18..2e8ce6daa5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -32,6 +32,25 @@ OpenSSL 4.1

 ### Changes between 4.0 and 4.1 [xx XXX xxxx]

+ * Added a `seed_strict` option to the `random` configuration section
+   which makes the configured random seed source strictly enforced when
+   a provider (such as the FIPS provider) requests entropy or a nonce.
+
+   When a provider requests seeding material before the primary DRBG has
+   been created, the request falls back to the operating system entropy
+   sources, because the seed source only comes into existence as a side
+   effect of creating the primary DRBG.  Whether a configured seed source
+   is used therefore depends on operation order.  With `seed_strict`
+   enabled, the seed source is instead instantiated on demand and an
+   error is reported if it cannot be used.  The option is off by default
+   with two exceptions: the `JITTER` seed source seeds strictly unless
+   the option disables it and `enable-fips-jitter` builds always seed
+   strictly.  Additionally, the property query used to fetch the default
+   seed source can now be set at build time with
+   `-DOPENSSL_DEFAULT_SEED_PROPQ`.
+
+   *Jakub Zelenka*
+
  * Fixed a bug where a TLS 1.3 session ticket could retain a stale ALPN
    protocol from an earlier connection after a resumption negotiated a
    different protocol (or none), on both the server and the client,
diff --git a/Configure b/Configure
index 0af6e7c3c2..1d9c890a4a 100755
--- a/Configure
+++ b/Configure
@@ -1418,7 +1418,8 @@ application.

 Instead of manually seeding, a different random generator can be set
 at runtime in openssl.cnf or configured at build time with
--DOPENSSL_DEFAULT_SEED_SRC.
+-DOPENSSL_DEFAULT_SEED_SRC.  The property query used to fetch it can
+be set with -DOPENSSL_DEFAULT_SEED_PROPQ if needed.

 Please read the 'Note on random number generation' section in the
 INSTALL.md instructions for more details.
diff --git a/crypto/conf/conf_mod.c b/crypto/conf/conf_mod.c
index 9070f2967e..f20bc6ab46 100644
--- a/crypto/conf/conf_mod.c
+++ b/crypto/conf/conf_mod.c
@@ -723,3 +723,35 @@ int CONF_parse_list(const char *list_, int sep, int nospc,
         lstart = p + 1;
     }
 }
+
+/*
+ * Parse a boolean configuration value: 1, yes, true or on (in lower or
+ * uppercase) enables, 0, no, false or off disables.  Returns 0 when the
+ * value is missing or not recognised, without raising an error.
+ */
+int ossl_conf_parse_bool(const char *value, int *result)
+{
+    if (value == NULL)
+        return 0;
+    if (strcmp(value, "1") == 0
+        || strcmp(value, "yes") == 0
+        || strcmp(value, "YES") == 0
+        || strcmp(value, "true") == 0
+        || strcmp(value, "TRUE") == 0
+        || strcmp(value, "on") == 0
+        || strcmp(value, "ON") == 0) {
+        *result = 1;
+        return 1;
+    }
+    if (strcmp(value, "0") == 0
+        || strcmp(value, "no") == 0
+        || strcmp(value, "NO") == 0
+        || strcmp(value, "false") == 0
+        || strcmp(value, "FALSE") == 0
+        || strcmp(value, "off") == 0
+        || strcmp(value, "OFF") == 0) {
+        *result = 0;
+        return 1;
+    }
+    return 0;
+}
diff --git a/crypto/provider_conf.c b/crypto/provider_conf.c
index f2e76ac402..3b62622939 100644
--- a/crypto/provider_conf.c
+++ b/crypto/provider_conf.c
@@ -15,6 +15,7 @@
 #include <openssl/provider.h>
 #include "internal/provider.h"
 #include "internal/cryptlib.h"
+#include "internal/conf.h"
 #include "provider_local.h"
 #include "crypto/context.h"

@@ -275,30 +276,7 @@ static int provider_conf_activate(OSSL_LIB_CTX *libctx, const char *name,
 static int provider_conf_parse_bool_setting(const char *confname,
     const char *confvalue, int *val)
 {
-
-    if (confvalue == NULL) {
-        ERR_raise_data(ERR_LIB_CRYPTO, CRYPTO_R_PROVIDER_SECTION_ERROR,
-            "directive %s set to unrecognized value",
-            confname);
-        return 0;
-    }
-    if ((strcmp(confvalue, "1") == 0)
-        || (strcmp(confvalue, "yes") == 0)
-        || (strcmp(confvalue, "YES") == 0)
-        || (strcmp(confvalue, "true") == 0)
-        || (strcmp(confvalue, "TRUE") == 0)
-        || (strcmp(confvalue, "on") == 0)
-        || (strcmp(confvalue, "ON") == 0)) {
-        *val = 1;
-    } else if ((strcmp(confvalue, "0") == 0)
-        || (strcmp(confvalue, "no") == 0)
-        || (strcmp(confvalue, "NO") == 0)
-        || (strcmp(confvalue, "false") == 0)
-        || (strcmp(confvalue, "FALSE") == 0)
-        || (strcmp(confvalue, "off") == 0)
-        || (strcmp(confvalue, "OFF") == 0)) {
-        *val = 0;
-    } else {
+    if (!ossl_conf_parse_bool(confvalue, val)) {
         ERR_raise_data(ERR_LIB_CRYPTO, CRYPTO_R_PROVIDER_SECTION_ERROR,
             "directive %s set to unrecognized value",
             confname);
diff --git a/crypto/rand/prov_seed.c b/crypto/rand/prov_seed.c
index 8466ded8ab..8b640855e3 100644
--- a/crypto/rand/prov_seed.c
+++ b/crypto/rand/prov_seed.c
@@ -45,13 +45,27 @@ size_t ossl_rand_get_user_entropy(OSSL_LIB_CTX *ctx,
     unsigned char **pout, int entropy,
     size_t min_len, size_t max_len)
 {
-    EVP_RAND_CTX *rng = ossl_rand_get0_seed_noncreating(ctx);
-
-    if (rng != NULL && evp_rand_can_seed(rng))
-        return evp_rand_get_seed(rng, pout, entropy, min_len, max_len,
-            0, NULL, 0);
-    else
-        return ossl_rand_get_entropy(ctx, pout, entropy, min_len, max_len);
+    EVP_RAND_CTX *rng;
+
+    if (ossl_rand_seed_source_strict(ctx)) {
+        /*
+         * With strict seeding the seed source must be used, even when the
+         * request arrives before anything instantiated it: create it now
+         * and fail instead of silently substituting the operating system
+         * entropy sources.
+         */
+        rng = ossl_rand_get0_seed(ctx);
+        if (rng == NULL || !evp_rand_can_seed(rng)) {
+            ERR_raise(ERR_LIB_RAND, RAND_R_ERROR_RETRIEVING_ENTROPY);
+            return 0;
+        }
+    } else {
+        rng = ossl_rand_get0_seed_noncreating(ctx);
+        if (rng == NULL || !evp_rand_can_seed(rng))
+            return ossl_rand_get_entropy(ctx, pout, entropy, min_len, max_len);
+    }
+    return evp_rand_get_seed(rng, pout, entropy, min_len, max_len,
+        0, NULL, 0);
 }

 void ossl_rand_cleanup_entropy(ossl_unused OSSL_LIB_CTX *ctx,
@@ -103,10 +117,21 @@ size_t ossl_rand_get_user_nonce(OSSL_LIB_CTX *ctx,
     const void *salt, size_t salt_len)
 {
     unsigned char *buf;
-    EVP_RAND_CTX *rng = ossl_rand_get0_seed_noncreating(ctx);
-
-    if (rng == NULL)
-        return ossl_rand_get_nonce(ctx, pout, min_len, max_len, salt, salt_len);
+    EVP_RAND_CTX *rng;
+
+    if (ossl_rand_seed_source_strict(ctx)) {
+        /* See ossl_rand_get_user_entropy() */
+        rng = ossl_rand_get0_seed(ctx);
+        if (rng == NULL) {
+            ERR_raise(ERR_LIB_RAND, RAND_R_ERROR_RETRIEVING_NONCE);
+            return 0;
+        }
+    } else {
+        rng = ossl_rand_get0_seed_noncreating(ctx);
+        if (rng == NULL)
+            return ossl_rand_get_nonce(ctx, pout, min_len, max_len,
+                salt, salt_len);
+    }

     if ((buf = OPENSSL_malloc(min_len)) == NULL)
         return 0;
diff --git a/crypto/rand/rand_lib.c b/crypto/rand/rand_lib.c
index a58b0bb9ef..d4a8203851 100644
--- a/crypto/rand/rand_lib.c
+++ b/crypto/rand/rand_lib.c
@@ -68,6 +68,12 @@ typedef struct rand_global_st {
     /* Allow the randomness source to be changed */
     char *seed_name;
     char *seed_propq;
+
+    /*
+     * Whether the seed source may never fall back to the OS entropy:
+     * 1 strict, 0 not strict, -1 unset (strict only for JITTER)
+     */
+    int seed_strict;
 } RAND_GLOBAL;

 static EVP_RAND_CTX *rand_get0_primary(OSSL_LIB_CTX *ctx, RAND_GLOBAL *dgbl);
@@ -85,6 +91,7 @@ static RAND_GLOBAL *rand_get_global(OSSL_LIB_CTX *libctx)
 #include <limits.h>
 #include <openssl/conf.h>
 #include <openssl/trace.h>
+#include "internal/conf.h"
 #include "crypto/rand_pool.h"
 #include "prov/seeding.h"
 #include "internal/e_os.h"
@@ -451,6 +458,8 @@ void *ossl_rand_ctx_new(OSSL_LIB_CTX *libctx)
     if (dgbl == NULL)
         return NULL;

+    dgbl->seed_strict = -1;
+
 #ifndef FIPS_MODULE
     /*
      * We need to ensure that base libcrypto thread handling has been
@@ -522,6 +531,27 @@ static void rand_delete_thread_state(void *arg)
 }

 #if !defined(FIPS_MODULE) || !defined(OPENSSL_NO_FIPS_JITTER)
+/*
+ * Return 1 if the seed source must always be used and never be silently
+ * substituted by the operating system entropy sources: requested via the
+ * seed_strict option of the [random] configuration section, defaulting
+ * to strict for the JITTER seed source or implied by an
+ * enable-fips-jitter build which hard-wires the JITTER seed source.
+ */
+static int rand_seed_source_strict(ossl_unused RAND_GLOBAL *dgbl)
+{
+#ifdef OPENSSL_NO_FIPS_JITTER
+    const char *name;
+
+    if (dgbl->seed_strict >= 0)
+        return dgbl->seed_strict;
+    name = dgbl->seed_name != NULL ? dgbl->seed_name : OPENSSL_SEED_SRC_NAME;
+    return OPENSSL_strcasecmp(name, "JITTER") == 0;
+#else /* !OPENSSL_NO_FIPS_JITTER */
+    return 1;
+#endif /* OPENSSL_NO_FIPS_JITTER */
+}
+
 static EVP_RAND_CTX *rand_new_seed(OSSL_LIB_CTX *libctx)
 {
     EVP_RAND *rand;
@@ -535,10 +565,14 @@ static EVP_RAND_CTX *rand_new_seed(OSSL_LIB_CTX *libctx)
     if (dgbl == NULL)
         return NULL;
     propq = dgbl->seed_propq;
+#ifdef OPENSSL_DEFAULT_SEED_PROPQ
+    if (propq == NULL)
+        propq = OPENSSL_MSTR(OPENSSL_DEFAULT_SEED_PROPQ);
+#endif /* OPENSSL_DEFAULT_SEED_PROPQ */
     if (dgbl->seed_name != NULL) {
         name = dgbl->seed_name;
     } else {
-        fallback = 1;
+        fallback = !rand_seed_source_strict(dgbl);
         name = OPENSSL_SEED_SRC_NAME;
     }
 #else /* !OPENSSL_NO_FIPS_JITTER */
@@ -621,6 +655,22 @@ EVP_RAND_CTX *ossl_rand_get0_seed_noncreating(OSSL_LIB_CTX *ctx)
     CRYPTO_THREAD_unlock(dgbl->lock);
     return ret;
 }
+
+EVP_RAND_CTX *ossl_rand_get0_seed(OSSL_LIB_CTX *ctx)
+{
+    RAND_GLOBAL *dgbl = rand_get_global(ctx);
+
+    if (dgbl == NULL)
+        return NULL;
+    return rand_get0_seed(ctx, dgbl);
+}
+
+int ossl_rand_seed_source_strict(OSSL_LIB_CTX *ctx)
+{
+    RAND_GLOBAL *dgbl = rand_get_global(ctx);
+
+    return dgbl != NULL && rand_seed_source_strict(dgbl);
+}
 #endif /* !FIPS_MODULE */

 static EVP_RAND_CTX *rand_new_drbg(OSSL_LIB_CTX *libctx, EVP_RAND_CTX *parent,
@@ -934,6 +984,16 @@ static int random_set_string(char **p, const char *s)
     return 1;
 }

+static int random_set_bool(int *p, const CONF_VALUE *cval)
+{
+    if (!ossl_conf_parse_bool(cval->value, p)) {
+        ERR_raise_data(ERR_LIB_CRYPTO, CRYPTO_R_RANDOM_SECTION_ERROR,
+            "name=%s, value=%s", cval->name, cval->value);
+        return 0;
+    }
+    return 1;
+}
+
 /*
  * Load the DRBG definitions from a configuration file.
  */
@@ -978,6 +1038,9 @@ static int random_conf_init(CONF_IMODULE *md, const CONF *cnf)
         } else if (OPENSSL_strcasecmp(cval->name, "seed_properties") == 0) {
             if (!random_set_string(&dgbl->seed_propq, cval->value))
                 return 0;
+        } else if (OPENSSL_strcasecmp(cval->name, "seed_strict") == 0) {
+            if (!random_set_bool(&dgbl->seed_strict, cval))
+                return 0;
         } else if (OPENSSL_strcasecmp(cval->name, "random_provider") == 0) {
 #ifndef FIPS_MODULE
             OSSL_PROVIDER *prov = ossl_provider_find(libctx, cval->value, 0);
diff --git a/doc/internal/man3/ossl_rand_get_entropy.pod b/doc/internal/man3/ossl_rand_get_entropy.pod
index be39369f2b..0adb09404c 100644
--- a/doc/internal/man3/ossl_rand_get_entropy.pod
+++ b/doc/internal/man3/ossl_rand_get_entropy.pod
@@ -44,7 +44,15 @@ returned to the caller.
 ossl_rand_get_user_entropy() is the same as ossl_rand_get_entropy()
 except that it retrieves the seeding material from the library context's
 DRBG seed source.  By default this is the operating system but it can
-be changed by calling L<RAND_set_seed_source_type(3)>.
+be changed by calling L<RAND_set_seed_source_type(3)>, via the B<random>
+configuration section or at build time by overriding
+B<OPENSSL_DEFAULT_SEED_SRC>.  With strict seeding, enabled via the
+B<seed_strict> option of the B<random> configuration section, used by
+default for the B<JITTER> seed source or implied by an
+B<enable-fips-jitter> build, the seed source is instantiated on first
+use and an error is returned when it cannot be used.  Otherwise the
+operating system entropy sources are used as a fallback while the seed
+source has not been instantiated yet.

 ossl_rand_cleanup_entropy() cleanses and frees any storage allocated by
 ossl_rand_get_entropy().  The entropy buffer is pointed to by I<buf>
@@ -64,8 +72,9 @@ buffer length returned to the caller.

 ossl_rand_get_user_nonce() is the same as ossl_rand_get_nonce() except
 that it retrieves the seeding material from the library context's DRBG
-seed source.  By default this is the operating system but it can be
-changed by calling L<RAND_set_seed_source_type(3)>.
+seed source.  The seed source is selected and instantiated in the same
+way as for ossl_rand_get_user_entropy(), including the strict seeding
+behaviour.

 ossl_rand_cleanup_nonce() cleanses and frees any storage allocated by
 ossl_rand_get_nonce() or ossl_rand_get_user_nonce().  The nonce buffer
diff --git a/doc/man3/RAND_set_DRBG_type.pod b/doc/man3/RAND_set_DRBG_type.pod
index 4d26029c2f..bf40838064 100644
--- a/doc/man3/RAND_set_DRBG_type.pod
+++ b/doc/man3/RAND_set_DRBG_type.pod
@@ -39,11 +39,19 @@ These functions must be called before the random bit generators are first
 created in the library context.  They will return an error if the call
 is made too late.

+Note that with strict seeding, enabled via the B<seed_strict> option of
+the B<random> configuration section, used by default for the B<JITTER>
+seed source or implied by an B<enable-fips-jitter> build, a provider
+(for example the FIPS provider) requesting entropy or a nonce
+instantiates the seed source before the application's first use of it.
+RAND_set_seed_source_type() fails once this happens.
+
 The default DRBG is "CTR-DRBG" using the "AES-256-CTR" cipher.

 The default seed source can be configured when OpenSSL is compiled by
 setting B<-DOPENSSL_DEFAULT_SEED_SRC=SEED-SRC>. If not set then
-"SEED-SRC" is used.
+"SEED-SRC" is used.  The property query used when fetching the default
+seed source can similarly be set with B<-DOPENSSL_DEFAULT_SEED_PROPQ>.

 =head1 EXAMPLES

diff --git a/doc/man5/config.pod b/doc/man5/config.pod
index 2e455ea78c..3da360b7e7 100644
--- a/doc/man5/config.pod
+++ b/doc/man5/config.pod
@@ -416,6 +416,18 @@ to access the same randomness sources from outside the validated boundary.

 This sets the property query used when fetching the randomness source.

+=item B<seed_strict>
+
+This enables strict seeding.  The randomness source is then instantiated
+on first use, even for a provider entropy or nonce request arriving before
+the DRBG setup, and an unusable randomness source is an error instead of
+a silent fallback to the operating system entropy sources.  Recognised
+values are B<yes>, B<on>, B<true>, B<1> to enable and B<no>, B<off>,
+B<false>, B<0> to disable, in lower or uppercase.  The default is B<no>,
+except for the B<JITTER> randomness source, which seeds strictly unless
+this option disables it.  Builds configured with B<enable-fips-jitter>
+always seed strictly.
+
 =item B<random_provider>

 This sets the provider to use for the L<RAND_bytes(3)> calls instead of the built-in
diff --git a/include/crypto/rand.h b/include/crypto/rand.h
index 357ce885b7..b77f009fde 100644
--- a/include/crypto/rand.h
+++ b/include/crypto/rand.h
@@ -153,6 +153,8 @@ int ossl_pool_add_nonce_data(RAND_POOL *pool);
 EVP_RAND_CTX *ossl_rand_get0_private_noncreating(OSSL_LIB_CTX *ctx);
 #else
 EVP_RAND_CTX *ossl_rand_get0_seed_noncreating(OSSL_LIB_CTX *ctx);
+EVP_RAND_CTX *ossl_rand_get0_seed(OSSL_LIB_CTX *ctx);
+int ossl_rand_seed_source_strict(OSSL_LIB_CTX *ctx);
 #endif

 /* Generate a uniformly distributed random integer in the interval [0, upper) */
diff --git a/include/internal/conf.h b/include/internal/conf.h
index bf0ca15f74..5e27f9d36c 100644
--- a/include/internal/conf.h
+++ b/include/internal/conf.h
@@ -62,5 +62,6 @@ struct conf_imodule_st {
 int ossl_config_int(const OPENSSL_INIT_SETTINGS *);
 void ossl_no_config_int(void);
 void ossl_config_modules_free(void);
+int ossl_conf_parse_bool(const char *value, int *result);

 #endif
diff --git a/test/rand_strict.cnf b/test/rand_strict.cnf
new file mode 100644
index 0000000000..4ac3458354
--- /dev/null
+++ b/test/rand_strict.cnf
@@ -0,0 +1,8 @@
+openssl_conf = openssl_init
+
+[openssl_init]
+random = random_section
+
+[random_section]
+seed = TEST-RAND
+seed_strict = yes
diff --git a/test/rand_test.c b/test/rand_test.c
index afbd746fc3..22e72be51b 100644
--- a/test/rand_test.c
+++ b/test/rand_test.c
@@ -16,6 +16,7 @@
 #include "testutil.h"

 static char *configfile;
+static char *strictconfigfile;

 static int test_rand(void)
 {
@@ -319,6 +320,166 @@ err:
     return res;
 }

+/*
+ * Create a parentless DRBG in a provider: instantiating it requests
+ * seeding material through the core's get_user_entropy and
+ * get_user_nonce callbacks, the same path the FIPS provider uses.
+ */
+static EVP_RAND_CTX *provider_side_drbg(OSSL_LIB_CTX *ctx)
+{
+    EVP_RAND *rand;
+    EVP_RAND_CTX *rctx;
+
+    if (!TEST_ptr(rand = EVP_RAND_fetch(ctx, "CTR-DRBG", NULL)))
+        return NULL;
+    rctx = EVP_RAND_CTX_new(rand, NULL);
+    EVP_RAND_free(rand);
+    return rctx;
+}
+
+static int provider_side_drbg_instantiate(EVP_RAND_CTX *rctx)
+{
+    OSSL_PARAM params[2];
+
+    params[0] = OSSL_PARAM_construct_utf8_string(OSSL_DRBG_PARAM_CIPHER,
+        (char *)"AES-256-CTR", 0);
+    params[1] = OSSL_PARAM_construct_end();
+    return EVP_RAND_instantiate(rctx, 0, 0, NULL, 0, params);
+}
+
+/*
+ * Regression test for #25941: with strict seeding the configured seed
+ * source must be instantiated on demand and used when a provider
+ * requests seeding material before anything else created it, instead of
+ * being silently replaced by the operating system entropy sources.
+ */
+static int test_rand_seed_source_strict(void)
+{
+#ifndef OPENSSL_NO_FIPS_JITTER
+    TEST_info("skipped: enable-fips-jitter forces the JITTER seed source");
+    return 1;
+#else
+    OSSL_LIB_CTX *ctx = NULL;
+    EVP_RAND_CTX *drbg = NULL, *seed;
+    unsigned char entropy[64], buf[16];
+    OSSL_PARAM params[3];
+    int generate = 1, res = 0;
+    size_t i;
+
+    for (i = 0; i < sizeof(entropy); i++)
+        entropy[i] = 0xff & (i + 1);
+
+    /* The config configures TEST-RAND as the seed source and seed_strict */
+    if (!TEST_ptr(ctx = OSSL_LIB_CTX_new())
+        || !TEST_true(OSSL_LIB_CTX_load_config(ctx, strictconfigfile)))
+        goto err;
+
+    /*
+     * The first seeding request must fail: the configured TEST-RAND has
+     * no entropy to hand out yet and falling back to the operating
+     * system sources would defeat the configuration.
+     */
+    if (!TEST_ptr(drbg = provider_side_drbg(ctx)))
+        goto err;
+    ERR_set_mark();
+    if (!TEST_false(provider_side_drbg_instantiate(drbg))) {
+        ERR_clear_last_mark();
+        goto err;
+    }
+    ERR_pop_to_mark();
+    EVP_RAND_CTX_free(drbg);
+    drbg = NULL;
+
+    /* The request must have instantiated the configured seed source */
+    if (!TEST_ptr(seed = ossl_rand_get0_seed_noncreating(ctx))
+        || !TEST_str_eq(EVP_RAND_get0_name(EVP_RAND_CTX_get0_rand(seed)),
+            "TEST-RAND"))
+        goto err;
+
+    /* Provision the seed source and check that it feeds the DRBG */
+    params[0] = OSSL_PARAM_construct_octet_string(OSSL_RAND_PARAM_TEST_ENTROPY,
+        entropy, sizeof(entropy));
+    params[1] = OSSL_PARAM_construct_int(OSSL_RAND_PARAM_GENERATE, &generate);
+    params[2] = OSSL_PARAM_construct_end();
+    if (!TEST_true(EVP_RAND_CTX_set_params(seed, params))
+        || !TEST_ptr(drbg = provider_side_drbg(ctx))
+        || !TEST_true(provider_side_drbg_instantiate(drbg))
+        || !TEST_true(EVP_RAND_generate(drbg, buf, sizeof(buf), 0, 0,
+            NULL, 0)))
+        goto err;
+
+    res = 1;
+err:
+    EVP_RAND_CTX_free(drbg);
+    OSSL_LIB_CTX_free(ctx);
+    return res;
+#endif /* OPENSSL_NO_FIPS_JITTER */
+}
+
+/*
+ * Verify that a provider requesting seeding material keeps the fallback
+ * behaviour without strict seeding: the request falls back to the
+ * operating system sources without instantiating the seed source, even
+ * when one was configured, so a later RAND_set_seed_source_type() call
+ * still succeeds.  In enable-fips-jitter builds seeding is always
+ * strict and the request instantiates the seed source instead.
+ */
+static int test_rand_seed_source_nonstrict(void)
+{
+    OSSL_LIB_CTX *ctx = NULL;
+    EVP_RAND_CTX *drbg = NULL;
+    int ok, res = 0;
+
+    if (!TEST_ptr(ctx = OSSL_LIB_CTX_new()))
+        goto err;
+
+    if (ossl_rand_seed_source_strict(ctx)) {
+        /* enable-fips-jitter build: the JITTER seed source is hard-wired */
+        if (!TEST_ptr(drbg = provider_side_drbg(ctx)))
+            goto err;
+        ERR_set_mark();
+        ok = provider_side_drbg_instantiate(drbg);
+        ERR_pop_to_mark();
+        /* The seed source may be unusable in this configuration */
+        if (ok
+            && (!TEST_ptr(ossl_rand_get0_seed_noncreating(ctx))
+                || !TEST_false(RAND_set_seed_source_type(ctx, "TEST-RAND",
+                    NULL))))
+            goto err;
+    } else {
+#ifdef OPENSSL_RAND_SEED_NONE
+        TEST_info("skipped: no operating system entropy sources");
+#else
+        /* Strict seeding is implied when the JITTER source is selected */
+        if (!TEST_true(RAND_set_seed_source_type(ctx, "JITTER", NULL))
+            || !TEST_true(ossl_rand_seed_source_strict(ctx))
+            || !TEST_true(RAND_set_seed_source_type(ctx, NULL, NULL))
+            || !TEST_false(ossl_rand_seed_source_strict(ctx)))
+            goto err;
+
+        if (!TEST_ptr(drbg = provider_side_drbg(ctx))
+            || !TEST_true(provider_side_drbg_instantiate(drbg))
+            || !TEST_ptr_null(ossl_rand_get0_seed_noncreating(ctx))
+            || !TEST_true(RAND_set_seed_source_type(ctx, "TEST-RAND", NULL)))
+            goto err;
+        EVP_RAND_CTX_free(drbg);
+        drbg = NULL;
+
+        /* A configured but non-strict seed source still falls back */
+        if (!TEST_ptr(drbg = provider_side_drbg(ctx))
+            || !TEST_true(provider_side_drbg_instantiate(drbg))
+            || !TEST_ptr_null(ossl_rand_get0_seed_noncreating(ctx)))
+            goto err;
+#endif /* OPENSSL_RAND_SEED_NONE */
+    }
+
+    res = 1;
+err:
+    EVP_RAND_CTX_free(drbg);
+    OSSL_LIB_CTX_free(ctx);
+    return res;
+}
+
 /* Warm up the DRBG cipher fetch caches outside the mfail injection window */
 static int rand_drbg_fetch_warmup(EVP_RAND *drbg_alg)
 {
@@ -463,6 +624,7 @@ int setup_tests(void)
     }

     if (!TEST_ptr(configfile = test_get_argument(0))
+        || !TEST_ptr(strictconfigfile = test_get_argument(1))
         || !TEST_true(RAND_set_DRBG_type(NULL, "TEST-RAND", "fips=no",
             NULL, NULL))
         || (fips_provider_version_ge(NULL, 3, 0, 8)
@@ -483,6 +645,9 @@ int setup_tests(void)
         || fips_provider_version_ge(NULL, 3, 5, 1))
         ADD_TEST(test_rand_get0_primary);

+    ADD_TEST(test_rand_seed_source_strict);
+    ADD_TEST(test_rand_seed_source_nonstrict);
+
     ADD_MFAIL_ALL_TESTS(test_rand_bytes_mfail, 2);
     ADD_MFAIL_TEST(test_rand_seed_src_mfail);
     ADD_MFAIL_TEST(test_rand_drbg_mfail);
diff --git a/test/recipes/05-test_rand.t b/test/recipes/05-test_rand.t
index d70b65c649..69a1887bb0 100644
--- a/test/recipes/05-test_rand.t
+++ b/test/recipes/05-test_rand.t
@@ -16,12 +16,14 @@ use Cwd qw(abs_path);
 plan tests => 6;
 setup("test_rand");

-ok(run(test(["rand_test", srctop_file("test", "default.cnf")])));
+ok(run(test(["rand_test", srctop_file("test", "default.cnf"),
+             srctop_file("test", "rand_strict.cnf")])));

 SKIP: {
     skip "Skipping FIPS test in this build", 1 if disabled('fips');

-    ok(run(test(["rand_test", srctop_file("test", "fips.cnf")])));
+    ok(run(test(["rand_test", srctop_file("test", "fips.cnf"),
+                 srctop_file("test", "rand_strict.cnf")])));
 }

 ok(run(test(["drbgtest"])));
diff --git a/test/recipes/20-test_rand_config.t b/test/recipes/20-test_rand_config.t
index 1db541ffe0..34bcf38782 100644
--- a/test/recipes/20-test_rand_config.t
+++ b/test/recipes/20-test_rand_config.t
@@ -57,7 +57,33 @@ my @aria_tests = (

 push @rand_tests, @aria_tests unless disabled("aria");

-plan tests => scalar @rand_tests * 2;
+# Configured seed sources must be honoured: an available one is used and
+# an unavailable one is an error rather than a silent fallback to the
+# operating system entropy sources.  Not applicable to enable-fips-jitter
+# builds, which hard-wire the JITTER seed source.
+my $rand_seed_none =
+    grep { $_ eq 'OPENSSL_RAND_SEED_NONE' }
+        @{ config('openssl_feature_defines') // [] };
+my @seed_tests;
+if (disabled("fips-jitter")) {
+    push @seed_tests,
+        { seed => 'SEED-SRC',
+          expected_ok => 1,
+          desc => 'configured SEED-SRC seed source works' }
+        unless $rand_seed_none;
+    push @seed_tests,
+        { seed => 'SEED-SRC',
+          strict => 'yes',
+          expected_ok => 1,
+          desc => 'strictly configured SEED-SRC seed source works' }
+        unless $rand_seed_none;
+    push @seed_tests,
+        { seed => 'NONEXISTENT-SEED-SOURCE',
+          expected_ok => 0,
+          desc => 'unavailable configured seed source fails, no fallback' };
+}
+
+plan tests => scalar @rand_tests * 2 + scalar @seed_tests;

 my $contents =<<'CONFIGEND';
 openssl_conf = openssl_init
@@ -91,6 +117,20 @@ foreach (@rand_tests) {
     ok(run(app(["openssl", "rand", "-writerand", "$result_dir/$tmpfile.bin"])));
 }

+foreach (@seed_tests) {
+    my $tmpfile = 'rand_seed_config.cfg';
+    open(my $cfg, '>', $tmpfile) or die "Could not open file";
+    print $cfg $contents;
+    print $cfg "seed = $_->{seed}\n";
+    print $cfg "seed_strict = $_->{strict}\n" if defined $_->{strict};
+    close $cfg;
+
+    $ENV{OPENSSL_CONF} = $tmpfile;
+
+    my $ok = run(app(["openssl", "rand", "-hex", "16"]));
+    ok(!$ok == !$_->{expected_ok}, $_->{desc});
+}
+
 # Check that the stdout output contains the expected values.
 sub comparelines {
     my @lines = run(app(["openssl", "list", "--random-instances"]),