Skip to content

Commit 489c044

Browse files
committed
Address failure rates from FIPS CRNGT test by implementing alternate RCT/ADP tests
Update ret code to match docs and update docs Replace magic numbers with appropriate define Define MAX_ENTROPY_BITS when MEMUSE not enabled Fix type cast windows detection Older FIPS modules still need the old check CodeSpell you're wrong, that is what I want to name my variable Turn the hostap into a manual dispatch until it gets fixed Upon closer review we can not skip the test when memuse enabled Fix whitespace stuff found by multitest More syntax things
1 parent d18b4b2 commit 489c044

8 files changed

Lines changed: 836 additions & 16 deletions

File tree

.github/workflows/codespell.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
check_filenames: true
2424
check_hidden: true
2525
# Add comma separated list of words that occur multiple times that should be ignored (sorted alphabetically, case sensitive)
26-
ignore_words_list: adin,aNULL,brunch,carryIn,chainG,ciph,cLen,cliKs,dout,haveA,inCreated,inOut,inout,larg,LEAPYEAR,Merget,optionA,parm,parms,repid,rIn,userA,ser,siz,te,Te,HSI,
26+
ignore_words_list: adin,aNULL,brunch,carryIn,chainG,ciph,cLen,cliKs,dout,haveA,inCreated,inOut,inout,larg,LEAPYEAR,Merget,optionA,parm,parms,repid,rIn,userA,ser,siz,te,Te,HSI,failT,
2727
# The exclude_file contains lines of code that should be ignored. This is useful for individual lines which have non-words that can safely be ignored.
2828
exclude_file: '.codespellexcludelines'
2929
# To skip files entirely from being processed, add it to the following list:

.github/workflows/hostap-vm.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ name: hostap and wpa-supplicant Tests
22

33
# START OF COMMON SECTION
44
on:
5-
push:
6-
branches: [ 'master', 'main', 'release/**' ]
7-
pull_request:
8-
branches: [ '*' ]
5+
workflow_dispatch: # Allows people to run it manually if they want but
6+
# disables it from running automatically when broken
7+
# To restore this to an auto test delete the above workflow_dispatch line and
8+
# comments and uncomment the below lines for push and pull_request
9+
# push:
10+
# branches: [ 'master', 'main', 'release/**' ]
11+
# pull_request:
12+
# branches: [ '*' ]
913

1014
concurrency:
1115
group: ${{ github.workflow }}-${{ github.ref }}

doc/dox_comments/header_files/random.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ int wc_RNG_DRBG_Reseed(WC_RNG* rng, const byte* seed, word32 seedSz);
476476
477477
\return 0 If valid
478478
\return BAD_FUNC_ARG If seed is NULL
479-
\return RNG_FAILURE_E Validation failed
479+
\return ENTROPY_RT_E || ENTROPY_APT_E Validation failed
480480
481481
\param seed Seed to test
482482
\param seedSz Seed size

tests/api/test_random.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,12 @@ int test_wc_RNG_TestSeed(void)
345345
/* Bad seed as it repeats. */
346346
XMEMSET(seed, 0xa5, sizeof(seed));
347347
/* Return value is DRBG_CONT_FAILURE which is not public. */
348+
/* Moving forward with the RCT test check LT instead of GT */
349+
#if !defined(HAVE_FIPS) || ( defined(HAVE_FIPS) && FIPS_VERSION3_GE(7,0,0) )
350+
ExpectIntLT(wc_RNG_TestSeed(seed, sizeof(seed)), 0);
351+
#else
348352
ExpectIntGT(wc_RNG_TestSeed(seed, sizeof(seed)), 0);
353+
#endif
349354

350355
/* Good seed. */
351356
for (i = 0; i < (byte)sizeof(seed); i++)

wolfcrypt/src/random.c

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -746,22 +746,138 @@ static int Hash_DRBG_Uninstantiate(DRBG_internal* drbg)
746746
}
747747

748748

749+
/* FIPS 140-3 IG 10.3.A / SP800-90B Health Tests for Seed Data
750+
*
751+
* These tests replace the older FIPS 140-2 Continuous Random Number Generator
752+
* Test (CRNGT) with more mathematically robust statistical tests per
753+
* ISO 19790 / SP800-90B requirements.
754+
*
755+
* When HAVE_ENTROPY_MEMUSE is defined, the wolfentropy.c jitter-based TRNG
756+
* already performs these health tests, so we can skip them here to avoid
757+
* duplicate testing overhead. However, when customers use custom seed
758+
* callbacks (e.g., for hardware TRNGs with ESV certs), these tests ensure
759+
* the seed data meets entropy requirements when we can not be 100% positive
760+
* that the entropy source is implementing these tests since we don't control
761+
* it's implementation.
762+
*/
763+
764+
/* SP800-90B 4.4.1 - Repetition Count Test
765+
* Detects if the noise source becomes "stuck" producing repeated output.
766+
*
767+
* C = 1 + ceil(-log2(alpha) / H)
768+
* For alpha = 2^-30 (false positive probability) and H = 1 (min entropy):
769+
* C = 1 + ceil(30 / 1) = 31
770+
*/
771+
#ifndef WC_RNG_SEED_RCT_CUTOFF
772+
#define WC_RNG_SEED_RCT_CUTOFF 31
773+
#endif
774+
775+
/* SP800-90B 4.4.2 - Adaptive Proportion Test
776+
* Monitors if a particular sample value appears too frequently within a
777+
* window of samples, indicating loss of entropy.
778+
*
779+
* Window size W = 512 for non-binary alphabet (byte values 0-255)
780+
* C = 1 + CRITBINOM(W, 2^(-H), 1-alpha)
781+
* For alpha = 2^-30 and H = 1, W = 512:
782+
* C = 1 + CRITBINOM(512, 0.5, 1-2^-30) = 325
783+
*/
784+
#ifndef WC_RNG_SEED_APT_WINDOW
785+
#define WC_RNG_SEED_APT_WINDOW 512
786+
#endif
787+
#ifndef WC_RNG_SEED_APT_CUTOFF
788+
#define WC_RNG_SEED_APT_CUTOFF 325
789+
#endif
790+
749791
int wc_RNG_TestSeed(const byte* seed, word32 seedSz)
750792
{
751793
int ret = 0;
752794

753-
/* Check the seed for duplicate words. */
754-
word32 seedIdx = 0;
755-
word32 scratchSz = min(SEED_BLOCK_SZ, seedSz - SEED_BLOCK_SZ);
795+
/* It would be desirable to skip a second round of RCT/ADP testing when
796+
* HAVE_ENTROPY_MEMUSE is enabled and already doing these tests on the
797+
* entropy, however it is run on the "unconditioned noise" so once it
798+
* arrives here in TestSeed it's now been conditioned and should in fact
799+
* be checked again before being consumed by the DRBG */
800+
word32 i;
801+
int rctFailed = 0;
802+
int aptFailed = 0;
756803

757-
while (seedIdx < seedSz - SEED_BLOCK_SZ) {
758-
if (ConstantCompare(seed + seedIdx,
759-
seed + seedIdx + scratchSz,
760-
(int)scratchSz) == 0) {
761-
ret = DRBG_CONT_FAILURE;
804+
if (seed == NULL || seedSz == 0) {
805+
return BAD_FUNC_ARG;
806+
}
807+
808+
/* SP800-90B 4.4.1 - Repetition Count Test (RCT)
809+
* Check for consecutive identical bytes that would indicate a stuck
810+
* entropy source. Fail if we see WC_RNG_SEED_RCT_CUTOFF or more
811+
* consecutive identical values.
812+
*
813+
* Constant-time implementation: always process full seed, accumulate
814+
* failure status without early exit to prevent timing side-channels.
815+
*/
816+
{
817+
int repCount = 1;
818+
byte prevByte = seed[0];
819+
820+
for (i = 1; i < seedSz; i++) {
821+
/* Constant-time: always evaluate both branches effects */
822+
int match = (seed[i] == prevByte);
823+
/* If match, increment count, if not, reset to 1 */
824+
repCount = (match * (repCount + 1)) + (!match * 1);
825+
/* Update prevByte only when not matching (new value) */
826+
prevByte = (byte) ((match * prevByte) + (!match * seed[i]));
827+
/* Accumulate failure flag - once set, stays set */
828+
rctFailed |= (repCount >= WC_RNG_SEED_RCT_CUTOFF);
829+
}
830+
}
831+
832+
/* SP800-90B 4.4.2 - Adaptive Proportion Test (APT)
833+
* Check that no single byte value appears too frequently within
834+
* a sliding window. This detects bias in the entropy source.
835+
*
836+
* For seeds smaller than the window size, we test the entire seed.
837+
* For larger seeds, we use a sliding window approach.
838+
*
839+
* Constant-time implementation: always process full seed and check
840+
* all counts to prevent timing side-channels.
841+
*/
842+
{
843+
word16 byteCounts[MAX_ENTROPY_BITS];
844+
word32 windowSize = min(seedSz, (word32)WC_RNG_SEED_APT_WINDOW);
845+
word32 windowStart = 0;
846+
word32 newIdx;
847+
848+
XMEMSET(byteCounts, 0, sizeof(byteCounts));
849+
850+
/* Initialize counts for first window */
851+
for (i = 0; i < windowSize; i++) {
852+
byteCounts[seed[i]]++;
853+
}
854+
855+
/* Check first window - scan all 256 counts */
856+
for (i = 0; i < MAX_ENTROPY_BITS; i++) {
857+
aptFailed |= (byteCounts[i] >= WC_RNG_SEED_APT_CUTOFF);
858+
}
859+
860+
/* Slide window through remaining seed data */
861+
while ((windowStart + windowSize) < seedSz) {
862+
/* Remove byte leaving the window */
863+
byteCounts[seed[windowStart]]--;
864+
windowStart++;
865+
866+
/* Add byte entering the window */
867+
newIdx = windowStart + windowSize - 1;
868+
byteCounts[seed[newIdx]]++;
869+
870+
/* Accumulate failure flag for new byte's count */
871+
aptFailed |= (byteCounts[seed[newIdx]] >= WC_RNG_SEED_APT_CUTOFF);
762872
}
763-
seedIdx += SEED_BLOCK_SZ;
764-
scratchSz = min(SEED_BLOCK_SZ, (seedSz - seedIdx));
873+
}
874+
875+
/* Set return code based on accumulated failure flags */
876+
if (rctFailed) {
877+
ret = ENTROPY_RT_E;
878+
}
879+
else if (aptFailed) {
880+
ret = ENTROPY_APT_E;
765881
}
766882

767883
return ret;

wolfcrypt/test/README.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,185 @@ For building wolfCrypt test project in Visual Studio open the `test.sln`. For ne
5858
If you see an error about `rc.exe` then you'll need to update the "Target Platform Version". You can do this by right-clicking on the test project -> General -> "Target Platform Version" and changing to 8.1 (needs to match the wolfssl library project).
5959

6060
This solution includes the wolfSSL library project at `<wolfssl-root>wolfssl.vcxproj` and will compile the library, then the test project.
61+
62+
--------
63+
64+
Jan 2026 - Reviewing the older FIPS compliant CRNGT test specified in FIPS 140-2
65+
ss 4.9.2 vs the newer replacement tests RCT/ADP that are allowed to replace the
66+
CRNGT under the new FIPS 140-3 / ISO 19790 standard.
67+
68+
================================================================================
69+
DRBG Continuous Health Test Statistical Analysis & Diagnostic Report
70+
================================================================================
71+
72+
OVERVIEW
73+
--------
74+
This document describes the statistical false positive behavior of the DRBG
75+
continuous health test in wc_RNG_TestSeed() and provides diagnostic tools to
76+
distinguish between:
77+
1. Statistical false positives (expected behavior)
78+
2. Entropy source depletion (under heavy concurrent load)
79+
3. Actual stuck entropy source (hardware failure)
80+
81+
82+
BACKGROUND: THE ISSUE
83+
---------------------
84+
The DRBG was experiencing high volumes of (DRBG_CONT_FIPS_E) on wc_InitRng()
85+
calls.
86+
87+
Example error:
88+
ERROR: wc_InitRng failed at iteration 330788 with code -209
89+
90+
This raises the question: Is this a bug in wc_RNG_TestSeed() or expected
91+
statistical behavior?
92+
93+
94+
STATISTICAL ANALYSIS
95+
--------------------
96+
97+
The wc_RNG_TestSeed() Function Behavior:
98+
- Compares ALL consecutive SEED_BLOCK_SZ chunks in the seed buffer
99+
- With FIPS mode (typical configuration):
100+
SEED_SZ = 256 * 4 / 8 = 128 bytes (1024-bits)
101+
SEED_BLOCK_SZ = 4 bytes (default) (32-bits)
102+
seedSz passed to test = 132 bytes (SEED_SZ + SEED_BLOCK_SZ)
103+
Number of comparisons = ~32 consecutive block pairs
104+
105+
False Positive Probability Calculation:
106+
- Probability one 4-byte block equals another random 4-byte block: 1/2^32
107+
- With 32 comparisons per seed: 32/2^32 ≈ 1 in 134 million per wc_InitRng()
108+
109+
Test Configuration (Default):
110+
- 40 threads × 100M iterations = 4 BILLION total wc_InitRng() calls
111+
- Expected false positives: 4,000,000,000 × (32/2^32) ≈ 30 failures
112+
113+
Conclusion:
114+
Seeing failures around 1 in 30-140 million is EXPECTED STATISTICAL BEHAVIOR.
115+
Under heavy concurrent load (40 threads), entropy source
116+
depletion can also cause legitimate failures.
117+
118+
119+
TESTING IT
120+
--------------------
121+
122+
Non-FIPS:
123+
124+
./configure CFLAGS="-DWC_RNG_SEED_DEBUG -DREALLY_LONG_DRBG_CONTINUOUS_TEST"
125+
make
126+
./wolfcrypt/test/testwolfcrypt
127+
128+
FIPS:
129+
130+
./configure --enable-fips=<flavor> CFLAGS="-DWC_RNG_SEED_DEBUG -DREALLY_LONG_DRBG_CONTINUOUS_TEST"
131+
make
132+
./fips-hash.sh
133+
make
134+
./wolfcrypt/test/testwolfcrypt
135+
136+
137+
OUTPUTS EXPECTED
138+
--------------------
139+
140+
Non-FIPS:
141+
142+
Math: Multi-Precision: Wolf(SP) word-size=64 bits=4096 sp_int.c
143+
------------------------------------------------------------------------------
144+
wolfSSL version 5.8.4
145+
------------------------------------------------------------------------------
146+
macro test passed!
147+
error test passed!
148+
MEMORY test passed!
149+
base64 test passed!
150+
asn test passed!
151+
MD5 test passed!
152+
SHA test passed!
153+
SHA-224 test passed!
154+
SHA-256 test passed!
155+
SHA-384 test passed!
156+
SHA-512 test passed!
157+
SHA-512/224 test passed!
158+
SHA-512/256 test passed!
159+
SHA-3 test passed!
160+
RNG Entropy Source: getrandom() syscall
161+
===============================================
162+
DRBG Continuous Test Validation Suite
163+
===============================================
164+
FIPS Build: NO
165+
166+
--- Test 1: Basic RNG Functionality ---
167+
Generated 32 random bytes successfully
168+
[PASS] Basic RNG Functionality
169+
170+
--- Test 2: Multiple RNG Instances ---
171+
Successfully operated 100 RNG instances concurrently
172+
[PASS] Multiple RNG Instances
173+
174+
--- Test 3: FIPS Status Check ---
175+
SKIPPED: FIPS not enabled
176+
[PASS] FIPS Status Check
177+
178+
--- Test 4: RNG ReInit Test (multi-threaded) ---
179+
Configuration: 40 threads × 100000000 iterations = 4000000000 total
180+
Test Profile: Default (Aggressive multi-threaded)
181+
Expected statistical false positive rate: ~29.80 failures
182+
Duplicate block at offset 4:
183+
Block 1: E6 E9 D1 7B
184+
Block 2: E6 E9 D1 7B
185+
Full seed buffer (52 bytes):
186+
DA 93 B7 88 E6 E9 D1 7B E6 E9 D1 7B A5 4C C9 E9
187+
13 EE D8 4C B3 C1 71 DE 32 37 17 F2 E7 A4 29 7D
188+
9B 02 B0 0C EC 8D AC F5 DA B1 71 05 84 C0 61 75
189+
59 6D 87 B5
190+
ERROR: wc_InitRng failed at iteration 778551 with code -209
191+
ERROR: wc_RNG_GenerateBlock failed at iteration 778551 with code -199
192+
...
193+
(18 other failures truncated here for brevity)
194+
...
195+
Duplicate block at offset 16:
196+
Block 1: C1 19 37 B1
197+
Block 2: C1 19 37 B1
198+
Full seed buffer (52 bytes):
199+
62 66 5B D2 F5 54 47 9B 59 DD 0A 55 4B 52 8C 39
200+
C1 19 37 B1 C1 19 37 B1 3F 62 CB 2E FE 56 65 4D
201+
4F 0C A7 7D 1C 09 48 51 30 1B CA 00 56 9F 29 A7
202+
E3 93 EF 8E
203+
ERROR: wc_InitRng failed at iteration 90467867 with code -209
204+
ERROR: wc_RNG_GenerateBlock failed at iteration 90467867 with code -199
205+
Thread 0 Succeeded
206+
...
207+
38 other thread results truncated here for brevity (all threads succeeded
208+
even though they experienced 1 or 2 failures in several of the threads)
209+
...
210+
Thread 39 Succeeded
211+
Reinitialized RNG 4000000000 times across 40 threads
212+
Experienced 0 thread failures and 40 thread successes
213+
20/4000000000 API calls failed <--- This is the bread and the butter of the
214+
test, we unfortunately expect to see
215+
~29.80 failures, prior to the newer FIPS
216+
140-3 RCT and ADP tests the CRNGT was
217+
required. Now the CRNGT is replaceable
218+
by the more mathematically robust
219+
RCT/ADP.
220+
[PASS] RNG Reinitialization
221+
222+
223+
224+
TESTING RESULTS with the CRNGT test:
225+
--------------------
226+
227+
Old implementation non-FIPS:
228+
Run 1 - 6 failures in 4 billion runs (100M per thread, 40 threads)
229+
Run 2 - 11 failures in 4 billion (100M per thread, 40 threads)
230+
Run 3 - 13 failures in 4 billion (100M per thread, 40 threads)
231+
232+
Old implementation with FIPS:
233+
(keeping in mind just a single failure means catastrophic
234+
failure for the entire module until power cycled):
235+
Run 1 - 3990118689 failures in 4 billion API calls (yikes)
236+
237+
TESTING RESULTS with the RCT/ADP tests in place of the CRNGT test:
238+
239+
New implementation non-FIPS: 4 billion successes
240+
New implementation FIPS: 4 billion successes
241+
242+

0 commit comments

Comments
 (0)