Repository navigation
fips: run pending CASTs at provider init - #508
MarkAtwood wants to merge 1 commit into
Conversation
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 wolfSSL#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.
There was a problem hiding this comment.
🟡 Changes recommended
The regression test does not compile on supported pthread platforms without POSIX barriers and mishandles barrier initialization failure.
1 open finding
What changed in this PR
Runs pending FIPS CASTs during provider initialization to prevent first-use races while preserving lazy DH/ECC primitive-Z tests.
Changes:
- Pre-runs pending CASTs once per process.
- Serializes lazy DH and hybrid ML-KEM ECDH CASTs.
- Adds a concurrent regression test.
| File | Description |
|---|---|
src/wp_internal.c |
Implements pending CAST execution. |
src/wp_wolfprov.c |
Runs CASTs during provider initialization. |
src/wp_dh_kmgmt.c |
Serializes lazy DH CAST initialization. |
src/wp_mlx_kem.c |
Serializes hybrid ECDH CAST initialization. |
include/wolfprovider/internal.h |
Declares the pending-CAST API. |
test/standalone/include.am |
Adds the regression test to make check. |
test/standalone/tests/fips_cast_init/test_fips_cast_init.c |
Tests CAST states and concurrent first use. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
|
|
||
| typedef struct race_ctx { | ||
| pthread_barrier_t barrier; |
|
Reproducers for the first-use CAST race, as asked on Slack.
Build: The KDF rows (
|
|
Closing per discussion with @ColtonWilley: he is handling CAST startup in wolfProvider, and the failure here was only shown by a synthetic stress test, not on a real application path. The reproducers stay in the comment above for reference. |

With a FIPS build, threads that make the first use of an algorithm at the same time can fail. Eight threads started together, each running its first
EVP_KDF_derive()on TLS1-PRF, TLS13-KDF or SSHKDF, or its first HMAC, in a fresh process with wolfProvider loaded:The failing calls return 0. On FIPS Ready that call is lost. On FIPS v5 it is worse: the thread that finds the CAST in progress puts the module in degraded mode, and every later crypto call in the process fails with
FIPS_NOT_ALLOWED_E(-197). In our wolfProvider 1.2.2 package testing (1000 launches, 8 threads), 109 to 208 per 1000 processes lost all crypto on v5 that way through the TLS 1.2, TLS 1.3 and SSH KDFs.wolfCrypt FIPS runs each CAST on the first use of its algorithm.
wp_init_cast()serializes the CAST groups it runs, but not the KDF CASTs wolfCrypt runs inside the KDFs, nor the HMAC CASTs they depend on.wp_init_cast()also callswc_RunCast_fips()on CASTs that already passed, which puts them back in progress while other threads may be using them.The fix runs, once per process at provider init, every CAST still in
FIPS_CAST_STATE_INITexcept DH and ECC primitive-Z. Those are the slow ones and stay lazy as #351 intends (decode_oid_precheckdepends on that). Every path to a wolfCrypt call gated on them now goes throughwp_init_cast()first:wp_dh_new()for every DH key, and the ECC part of the ML-KEM hybrids, which now takes the ECDH group CAST likewp_ecdh_init().wp_init_cast()skips CASTs that already passed. A failed CAST is logged and init continues; its algorithms fail when used. Non-FIPS builds are unchanged. Cost is once per process: about +5 to +9 ms foropenssl list -providersin our 1.2.2 package measurements (v5 and FIPS Ready).New standalone test
test/fips_cast_init.test, inmake check, modelsdecode_oid_precheck. It checks that no CAST except DH and ECC primitive-Z is left to run after the provider loads, and that DH and ECC primitive-Z are still cold. It also races 8 threads on the first use of TLS1-PRF, TLS13-KDF, SSHKDF and HMAC, 8 fresh processes each. A module built without a CAST (ECC CDH here) leaves itPROCESSINGwhen run, so onlyINITandFAILUREcount as errors.Tested on master cd8836f with
scripts/build-wolfprovider.sh(OpenSSL 3.5.4, wolfSSL 5.9.4):--fips-check=ready): unit.test 224/224, decode_oid_precheck and fips_cast_init pass. With only thesrc/andinclude/changes reverted, fips_cast_init failed 20 of 20 runs. Every run had 25 CASTs stillINITafter load. In the race, processes with a failed thread were TLS1-PRF 136/160, TLS13-KDF 147/160, SSHKDF 121/160 and HMAC 80/160. With the fix: 0.--replace-default --enable-replace-default-testing: 247/247; fips_cast_init skips.FIPS v5 was not built here. The v5 numbers above are from our wolfProvider 1.2.2 package testing with the same change, where the failures dropped to 0 of 1000.