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 +}