From 3b04f008483410b1511df3a6566891ef0c299e00 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Wed, 7 Oct 2026 11:45:13 -0700 Subject: [PATCH] fips: run pending CASTs at provider init wolfCrypt FIPS runs an algorithm's CAST on first use. Threads making that first use at once race on the CAST state, and the losing calls fail; on FIPS v5 the loser puts the module in degraded mode, so every later call fails with FIPS_NOT_ALLOWED_E. wp_init_cast() serializes the CAST groups it runs, but not the TLS 1.2, TLS 1.3 and SSH KDF CASTs wolfCrypt runs inside the KDFs, nor the HMAC CASTs those depend on. Provider init now runs, once per process, every CAST still in FIPS_CAST_STATE_INIT except the DH and ECC primitive-Z ones. Those stay lazy as #351 intends (decode_oid_precheck relies on it), and every path to a wolfCrypt call gated on them goes through wp_init_cast() first: wp_dh_new() for all DH keys, and the ECC hybrid encap/decap in wp_mlx_kem.c, which now takes the ECDH group CAST like wp_ecdh_init(). CASTs that passed are no longer re-run by wp_init_cast(): wc_RunCast_fips() puts them back in progress, failing other threads. A failed CAST is logged and init goes on; its algorithms fail when used. Add test/fips_cast_init.test to make check. It checks that no CAST but DH and ECC primitive-Z is left to run once the provider is loaded, and races 8 threads on the first use of TLS1-PRF, TLS13-KDF, SSHKDF and HMAC in fresh processes. Non-FIPS builds skip it. --- include/wolfprovider/internal.h | 1 + src/wp_dh_kmgmt.c | 4 + src/wp_internal.c | 101 ++++- src/wp_mlx_kem.c | 14 + src/wp_wolfprov.c | 8 + test/standalone/include.am | 10 + .../fips_cast_init/test_fips_cast_init.c | 390 ++++++++++++++++++ 7 files changed, 516 insertions(+), 12 deletions(-) create mode 100644 test/standalone/tests/fips_cast_init/test_fips_cast_init.c diff --git a/include/wolfprovider/internal.h b/include/wolfprovider/internal.h index 0b10515c..78570e72 100644 --- a/include/wolfprovider/internal.h +++ b/include/wolfprovider/internal.h @@ -202,6 +202,7 @@ void wp_provctx_unlock_rng(WOLFPROV_CTX* provCtx); #define WP_CAST_ALGO_COUNT 7 int wp_init_cast_mutexes(void); +int wp_run_pending_casts(void); int wp_init_cast(int algo); diff --git a/src/wp_dh_kmgmt.c b/src/wp_dh_kmgmt.c index be3bc696..7442932c 100644 --- a/src/wp_dh_kmgmt.c +++ b/src/wp_dh_kmgmt.c @@ -393,6 +393,10 @@ static wp_Dh* wp_dh_new(WOLFPROV_CTX *provCtx) { wp_Dh* dh = NULL; + /* Every wolfCrypt DH call is made on a key created here, and wolfCrypt + * FIPS gates them on the DH primitive-Z CAST: serialize it here. */ + WP_CHECK_FIPS_ALGO_PTR(WP_CAST_ALGO_DH); + if (wolfssl_prov_is_running()) { dh = (wp_Dh*)OPENSSL_zalloc(sizeof(*dh)); } diff --git a/src/wp_internal.c b/src/wp_internal.c index d4bb7e3c..33462b1d 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -121,6 +121,83 @@ int wp_init_cast_mutexes(void) return CRYPTO_THREAD_run_once(&castAlgosOnce, wolfprov_init_cast_mutex); } +static CRYPTO_ONCE castPendingOnce = CRYPTO_ONCE_STATIC_INIT; +/** Number of CASTs that failed when the pending CASTs were run. */ +static int castPendingFailed = 0; + +/** + * Run a CAST self-test unless it has already passed. + * + * wc_RunCast_fips() re-runs a passed CAST, putting it back in progress; a + * thread using the algorithm meanwhile fails, and on FIPS v5 that puts the + * whole module in degraded mode. + * + * @param [in] type CAST identifier (FIPS_CAST_*). + * @return 0 on success. + * @return Other value on failure. + */ +static int wp_run_cast(int type) +{ + if (wc_GetCastStatus_fips(type) == FIPS_CAST_STATE_SUCCESS) { + return 0; + } + return wc_RunCast_fips(type); +} + +/** + * Run every CAST self-test that has not run yet, except the DH and ECC + * primitive-Z ones. + * + * Run once, through wp_run_pending_casts(). + */ +static void wolfprov_run_pending_casts(void) +{ + int i; + int state; + + for (i = 0; i < FIPS_CAST_COUNT; i++) { + /* Left to run lazily (the slowest CASTs, #351), serialized by + * wp_init_cast() on every path that reaches them. */ + if ((i == FIPS_CAST_DH_PRIMITIVE_Z) || + (i == FIPS_CAST_ECC_PRIMITIVE_Z)) { + continue; + } + state = wc_GetCastStatus_fips(i); + if (state == FIPS_CAST_STATE_INIT) { + if (wc_RunCast_fips(i) != 0) { + castPendingFailed++; + } + } + else if (state == FIPS_CAST_STATE_FAILURE) { + castPendingFailed++; + } + } +} + +/** + * Run, once per process, the CAST self-tests that have not run yet. + * + * wolfCrypt runs a CAST on first use of its algorithm. Two threads making + * that first use at once race on the CAST state, and on FIPS v5 the loser + * puts the module in degraded mode, so all crypto of the process fails. + * wp_init_cast() serializes the CASTs it runs, but not the ones wolfCrypt + * runs inside other algorithms (TLS 1.2, TLS 1.3 and SSH KDF, and + * dependencies across algorithm groups). Call before the provider is usable + * so that only the DH and ECC primitive-Z CASTs are left to run lazily, each + * through wp_init_cast() before any wolfCrypt call gated on it. CASTs that + * passed are not re-run. + * + * @return 1 when no CAST failed. + * @return 0 on failure. + */ +int wp_run_pending_casts(void) +{ + if (!CRYPTO_THREAD_run_once(&castPendingOnce, wolfprov_run_pending_casts)) { + return 0; + } + return castPendingFailed == 0; +} + /** * Initialize a CAST self-test for a specific algorithm. * @@ -153,54 +230,54 @@ int wp_init_cast(int algo) switch (algo) { #ifdef WP_HAVE_AES case WP_CAST_ALGO_AES: - if (wc_RunCast_fips(FIPS_CAST_AES_CBC) != 0 || - wc_RunCast_fips(FIPS_CAST_AES_GCM) != 0) { + if (wp_run_cast(FIPS_CAST_AES_CBC) != 0 || + wp_run_cast(FIPS_CAST_AES_GCM) != 0) { ok = 0; } break; #endif #ifdef WP_HAVE_HMAC case WP_CAST_ALGO_HMAC: - if (wc_RunCast_fips(FIPS_CAST_HMAC_SHA1) != 0 || - wc_RunCast_fips(FIPS_CAST_HMAC_SHA2_256) != 0 || - wc_RunCast_fips(FIPS_CAST_HMAC_SHA2_512) != 0 || - wc_RunCast_fips(FIPS_CAST_HMAC_SHA3_256) != 0) { + if (wp_run_cast(FIPS_CAST_HMAC_SHA1) != 0 || + wp_run_cast(FIPS_CAST_HMAC_SHA2_256) != 0 || + wp_run_cast(FIPS_CAST_HMAC_SHA2_512) != 0 || + wp_run_cast(FIPS_CAST_HMAC_SHA3_256) != 0) { ok = 0; } break; #endif #ifdef WP_HAVE_RSA case WP_CAST_ALGO_RSA: - if (wc_RunCast_fips(FIPS_CAST_RSA_SIGN_PKCS1v15) != 0) { + if (wp_run_cast(FIPS_CAST_RSA_SIGN_PKCS1v15) != 0) { ok = 0; } break; #endif #ifdef WP_HAVE_ECDSA case WP_CAST_ALGO_ECDSA: - if (wc_RunCast_fips(FIPS_CAST_ECDSA) != 0) { + if (wp_run_cast(FIPS_CAST_ECDSA) != 0) { ok = 0; } break; #endif #ifdef WP_HAVE_ECDH case WP_CAST_ALGO_ECDH: - if (wc_RunCast_fips(FIPS_CAST_ECC_CDH) != 0 || - wc_RunCast_fips(FIPS_CAST_ECC_PRIMITIVE_Z) != 0) { + if (wp_run_cast(FIPS_CAST_ECC_CDH) != 0 || + wp_run_cast(FIPS_CAST_ECC_PRIMITIVE_Z) != 0) { ok = 0; } break; #endif #ifdef WP_HAVE_DH case WP_CAST_ALGO_DH: - if (wc_RunCast_fips(FIPS_CAST_DH_PRIMITIVE_Z) != 0) { + if (wp_run_cast(FIPS_CAST_DH_PRIMITIVE_Z) != 0) { ok = 0; } break; #endif #ifdef WP_HAVE_RANDOM case WP_CAST_ALGO_DRBG: - if (wc_RunCast_fips(FIPS_CAST_DRBG) != 0) { + if (wp_run_cast(FIPS_CAST_DRBG) != 0) { ok = 0; } break; diff --git a/src/wp_mlx_kem.c b/src/wp_mlx_kem.c index 09d15140..fd50c99c 100644 --- a/src/wp_mlx_kem.c +++ b/src/wp_mlx_kem.c @@ -233,6 +233,13 @@ static int wp_mlx_kem_classical_encap(wp_MlxCtx* ctx, unsigned char* cbuf, ok = 0; } } +#if defined(HAVE_FIPS) && !defined(WP_SINGLE_THREADED) + /* wc_ecc_shared_secret fires the ECC primitive-Z CAST; serialize it + * here. */ + if (ok && wp_init_cast(WP_CAST_ALGO_ECDH) != 1) { + ok = 0; + } +#endif if (ok) { PRIVATE_KEY_UNLOCK(); rc = wc_ecc_shared_secret(&eph, @@ -382,6 +389,13 @@ static int wp_mlx_kem_classical_decap(wp_MlxCtx* ctx, const unsigned char* cbuf, ok = 0; } } +#if defined(HAVE_FIPS) && !defined(WP_SINGLE_THREADED) + /* wc_ecc_shared_secret fires the ECC primitive-Z CAST; serialize it + * here. */ + if (ok && wp_init_cast(WP_CAST_ALGO_ECDH) != 1) { + ok = 0; + } +#endif if (ok) { PRIVATE_KEY_UNLOCK(); rc = wc_ecc_shared_secret(priv, &peer, sbuf, &ssLen); diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index 69446838..9c621431 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -1906,6 +1906,14 @@ int wolfssl_provider_init(const OSSL_CORE_HANDLE* handle, "Failed to initialize FIPS CAST mutexes"); ok = 0; } + /* Threads making the first use of an algorithm at once race on its + * CAST, so run the CASTs not run yet now, before the provider is + * usable (DH and ECC primitive-Z stay lazy, behind wp_init_cast()). + * A failed CAST still fails its algorithms when used. */ + else if (!wp_run_pending_casts()) { + WOLFPROV_ERROR_MSG(WP_LOG_COMP_PROVIDER, + "FIPS CAST failed at provider init"); + } #endif } diff --git a/test/standalone/include.am b/test/standalone/include.am index a8b9349e..be8494e6 100644 --- a/test/standalone/include.am +++ b/test/standalone/include.am @@ -56,6 +56,16 @@ test_decode_oid_precheck_test_SOURCES = \ test/standalone/tests/decode_oid_precheck/test_decode_oid_precheck.c test_decode_oid_precheck_test_LDADD = $(STANDALONE_COMMON_LDADD) +# FIPS CAST at provider init regression test - part of 'make check'. Runs in +# fresh processes so the CASTs are cold (the shared unit.test warms them). +check_PROGRAMS += test/fips_cast_init.test +noinst_PROGRAMS += test/fips_cast_init.test +DISTCLEANFILES += test/.libs/fips_cast_init.test +test_fips_cast_init_test_CPPFLAGS = $(STANDALONE_COMMON_CPPFLAGS) +test_fips_cast_init_test_SOURCES = \ + test/standalone/tests/fips_cast_init/test_fips_cast_init.c +test_fips_cast_init_test_LDADD = $(STANDALONE_COMMON_LDADD) + test_pqc_interop_test_CPPFLAGS = $(STANDALONE_COMMON_CPPFLAGS) test_pqc_interop_test_SOURCES = test/standalone/tests/pqc_interop/test_pqc_interop.c test_pqc_interop_test_LDADD = $(STANDALONE_COMMON_LDADD) diff --git a/test/standalone/tests/fips_cast_init/test_fips_cast_init.c b/test/standalone/tests/fips_cast_init/test_fips_cast_init.c new file mode 100644 index 00000000..67bba300 --- /dev/null +++ b/test/standalone/tests/fips_cast_init/test_fips_cast_init.c @@ -0,0 +1,390 @@ +/* test_fips_cast_init.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +/* + * Regression test for running the FIPS CASTs at provider init. + * + * wolfCrypt FIPS runs an algorithm's CAST on its first use. Threads making + * that first use at once race on the CAST state and the losers fail. The + * provider now runs every CAST not run yet when it is loaded, except the DH + * and ECC primitive-Z ones, which stay lazy behind wp_init_cast(). + * + * Standalone process because the CASTs must be cold, and the shared unit.test + * process warms them. Each race round runs in a fresh child process, as a + * CAST can only be raced once per process. + * Non-FIPS: there are no CASTs, so this skips. + */ + +#include +#include +#include + +#ifdef WOLFPROV_USER_SETTINGS +#include +#endif +#include +#include + +#include +#include +#include +#include +#include + +#include "test_common.h" + +#if defined(HAVE_FIPS) && !defined(WP_SINGLE_THREADED) && !defined(_WIN32) +#include +#include +#include +#include + +/* Threads making their first use of an algorithm at once. */ +#define RACE_THREADS 8 +/* Fresh processes per operation. */ +#define RACE_ROUNDS 8 + +/* First uses raced. Each fires CASTs that wolfCrypt runs inside the call. */ +enum { + RACE_TLS1_PRF, + RACE_TLS13_KDF, + RACE_SSHKDF, + RACE_HMAC, + RACE_OP_COUNT +}; + +static const char* raceOpName[RACE_OP_COUNT] = { + "TLS1-PRF", "TLS13-KDF", "SSHKDF", "HMAC" +}; + +static OSSL_LIB_CTX* wpLibCtx = NULL; +static OSSL_PROVIDER* wpProv = NULL; + +static int load_provider(void) +{ + wpLibCtx = OSSL_LIB_CTX_new(); + if (wpLibCtx == NULL) { + TEST_ERROR("Failed to create library context"); + return TEST_FAILURE; + } + /* Match unit.test: find libwolfprov in .libs. */ + OSSL_PROVIDER_set_default_search_path(wpLibCtx, ".libs"); + wpProv = OSSL_PROVIDER_load(wpLibCtx, "libwolfprov"); + if (wpProv == NULL) { + TEST_ERROR("Failed to load wolfProvider"); + OSSL_LIB_CTX_free(wpLibCtx); + wpLibCtx = NULL; + return TEST_FAILURE; + } + return TEST_SUCCESS; +} + +static void unload_provider(void) +{ + if (wpProv != NULL) { + OSSL_PROVIDER_unload(wpProv); + wpProv = NULL; + } + if (wpLibCtx != NULL) { + OSSL_LIB_CTX_free(wpLibCtx); + wpLibCtx = NULL; + } +} + +static const char* cast_state_str(int s) +{ + switch (s) { + case FIPS_CAST_STATE_INIT: return "INIT"; + case FIPS_CAST_STATE_PROCESSING: return "PROCESSING"; + case FIPS_CAST_STATE_SUCCESS: return "SUCCESS"; + case FIPS_CAST_STATE_FAILURE: return "FAILURE"; + default: return "?"; + } +} + +/* + * TEST 1: once the provider is loaded, no CAST is left to run except the DH + * and ECC primitive-Z ones, which must still be cold. A CAST the module was + * built without is left PROCESSING by wolfCrypt when run, so only INIT and + * FAILURE are errors. + */ +static int test_casts_run_at_init(void) +{ + int err = TEST_SUCCESS; + int i; + int state; + + TEST_INFO("Test 1: CASTs have run once the provider is loaded"); + + for (i = 0; i < FIPS_CAST_COUNT; i++) { + state = wc_GetCastStatus_fips(i); + if ((i == FIPS_CAST_DH_PRIMITIVE_Z) || + (i == FIPS_CAST_ECC_PRIMITIVE_Z)) { + if (state != FIPS_CAST_STATE_INIT) { + TEST_ERROR(" CAST %d is %s, expected INIT (lazy)", i, + cast_state_str(state)); + err = TEST_FAILURE; + } + } + else if ((state == FIPS_CAST_STATE_INIT) || + (state == FIPS_CAST_STATE_FAILURE)) { + TEST_ERROR(" CAST %d is %s after provider load", i, + cast_state_str(state)); + err = TEST_FAILURE; + } + } + return err; +} + +static int race_kdf(EVP_KDF* kdf, int op) +{ + int ok; + EVP_KDF_CTX* kctx; + OSSL_PARAM params[6]; + OSSL_PARAM* p = params; + unsigned char key[32]; + unsigned char seed[32]; + unsigned char out[32]; + int mode = EVP_KDF_HKDF_MODE_EXTRACT_ONLY; + + memset(key, 0x0b, sizeof(key)); + memset(seed, 0x0c, sizeof(seed)); + + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)"SHA256", 0); + if (op == RACE_TLS1_PRF) { + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SECRET, key, + sizeof(key)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SEED, seed, + sizeof(seed)); + } + else if (op == RACE_TLS13_KDF) { + *p++ = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, key, + sizeof(key)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, seed, + sizeof(seed)); + } + else { + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, key, + sizeof(key)); + *p++ = OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SSHKDF_XCGHASH, seed, sizeof(seed)); + *p++ = OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SSHKDF_SESSION_ID, seed, sizeof(seed)); + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_SSHKDF_TYPE, + (char*)"A", 0); + } + *p = OSSL_PARAM_construct_end(); + + kctx = EVP_KDF_CTX_new(kdf); + ok = (kctx != NULL) && + (EVP_KDF_derive(kctx, out, sizeof(out), params) == 1); + EVP_KDF_CTX_free(kctx); + return ok; +} + +static int race_hmac(EVP_MAC* mac) +{ + int ok; + EVP_MAC_CTX* mctx; + OSSL_PARAM params[2]; + unsigned char key[32]; + unsigned char out[32]; + size_t outLen = 0; + + memset(key, 0x0b, sizeof(key)); + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, + (char*)"SHA256", 0); + params[1] = OSSL_PARAM_construct_end(); + + mctx = EVP_MAC_CTX_new(mac); + ok = (mctx != NULL) && + (EVP_MAC_init(mctx, key, sizeof(key), params) == 1) && + (EVP_MAC_update(mctx, key, sizeof(key)) == 1) && + (EVP_MAC_final(mctx, out, &outLen, sizeof(out)) == 1); + EVP_MAC_CTX_free(mctx); + return ok; +} + +typedef struct race_ctx { + pthread_barrier_t barrier; + int op; + EVP_KDF* kdf; + EVP_MAC* mac; +} race_ctx; + +static void* race_thread(void* arg) +{ + race_ctx* rc = (race_ctx*)arg; + int ok; + + pthread_barrier_wait(&rc->barrier); + if (rc->op == RACE_HMAC) { + ok = race_hmac(rc->mac); + } + else { + ok = race_kdf(rc->kdf, rc->op); + } + return ok ? NULL : (void*)1; +} + +/* Child process: load the provider and race first use of op. Returns the + * number of threads that failed, or RACE_THREADS + 1 on setup failure. */ +static int race_child(int op) +{ + race_ctx rc; + pthread_t threads[RACE_THREADS]; + void* res; + int i; + int failed = 0; + + if (load_provider() != TEST_SUCCESS) { + return RACE_THREADS + 1; + } + rc.op = op; + rc.kdf = NULL; + rc.mac = NULL; + /* Fetch up front: only the first use of the algorithm is raced. */ + if (op == RACE_HMAC) { + rc.mac = EVP_MAC_fetch(wpLibCtx, "HMAC", NULL); + } + else { + rc.kdf = EVP_KDF_fetch(wpLibCtx, raceOpName[op], NULL); + } + if ((rc.mac == NULL) && (rc.kdf == NULL)) { + unload_provider(); + return RACE_THREADS + 1; + } + + pthread_barrier_init(&rc.barrier, NULL, RACE_THREADS); + for (i = 0; i < RACE_THREADS; i++) { + if (pthread_create(&threads[i], NULL, race_thread, &rc) != 0) { + /* The barrier can never be reached: give up on this process. */ + _exit(RACE_THREADS + 1); + } + } + for (i = 0; i < RACE_THREADS; i++) { + pthread_join(threads[i], &res); + if (res != NULL) { + failed++; + } + } + pthread_barrier_destroy(&rc.barrier); + + EVP_MAC_free(rc.mac); + EVP_KDF_free(rc.kdf); + unload_provider(); + return failed; +} + +/* + * TEST 2: threads making the first use of a KDF or HMAC at once all succeed. + * Each round is a fresh process so the CASTs start cold. + */ +static int test_first_use_race(void) +{ + int err = TEST_SUCCESS; + int op; + int round; + int status; + int failed; + int badRounds; + pid_t pid; + + TEST_INFO("Test 2: %d threads making first use at once, %d processes " + "per algorithm", RACE_THREADS, RACE_ROUNDS); + + for (op = 0; op < RACE_OP_COUNT; op++) { + badRounds = 0; + for (round = 0; round < RACE_ROUNDS; round++) { + fflush(NULL); + pid = fork(); + if (pid < 0) { + TEST_ERROR(" fork failed"); + return TEST_FAILURE; + } + if (pid == 0) { + _exit(race_child(op)); + } + if ((waitpid(pid, &status, 0) != pid) || !WIFEXITED(status)) { + TEST_ERROR(" %s: child did not exit normally", + raceOpName[op]); + return TEST_FAILURE; + } + failed = WEXITSTATUS(status); + if (failed > RACE_THREADS) { + TEST_ERROR(" %s: child setup failed", raceOpName[op]); + return TEST_FAILURE; + } + if (failed != 0) { + badRounds++; + } + } + TEST_INFO(" %s: %d of %d processes had a failed thread", + raceOpName[op], badRounds, RACE_ROUNDS); + if (badRounds != 0) { + err = TEST_FAILURE; + } + } + return err; +} + +#endif /* HAVE_FIPS && !WP_SINGLE_THREADED && !_WIN32 */ + +int main(void) +{ + TEST_INFO("========================================"); + TEST_INFO("FIPS CAST at provider init test"); + TEST_INFO("========================================"); + +#if !defined(HAVE_FIPS) || defined(WP_SINGLE_THREADED) || defined(_WIN32) + TEST_INFO("SKIPPED - not a multi-threaded POSIX FIPS build"); + return TEST_SUCCESS; +#else + { + int rc = TEST_SUCCESS; + + /* Race first: the children must start from cold CASTs, so this process + * loads no wolfCrypt state before forking. */ + if (test_first_use_race() != TEST_SUCCESS) { + rc = TEST_FAILURE; + } + if (load_provider() != TEST_SUCCESS) { + return TEST_FAILURE; + } + if (test_casts_run_at_init() != TEST_SUCCESS) { + rc = TEST_FAILURE; + } + unload_provider(); + + TEST_INFO("========================================"); + if (rc == TEST_SUCCESS) { + TEST_INFO("All FIPS CAST at provider init tests PASSED"); + } + else { + TEST_ERROR("FIPS CAST at provider init tests FAILED"); + } + TEST_INFO("========================================"); + + return rc; + } +#endif +}