Skip to content

Commit 20b2fd2

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 Correct comments based on latest findings
1 parent 62ca344 commit 20b2fd2

8 files changed

Lines changed: 825 additions & 18 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: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -746,28 +746,131 @@ 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+
* performs another set of these health tests, but those are on the noise not
757+
* the conditioned output so we still need to retest here even in that case
758+
* to evaluate the conditioned output for the same behavior. These tests ensure
759+
* the seed data meets basic entropy requirements regardless of the source.
760+
*/
761+
762+
/* SP800-90B 4.4.1 - Repetition Count Test
763+
* Detects if the noise source becomes "stuck" producing repeated output.
764+
*
765+
* C = 1 + ceil(-log2(alpha) / H)
766+
* For alpha = 2^-30 (false positive probability) and H = 1 (min entropy):
767+
* C = 1 + ceil(30 / 1) = 31
768+
*/
769+
#ifndef WC_RNG_SEED_RCT_CUTOFF
770+
#define WC_RNG_SEED_RCT_CUTOFF 31
771+
#endif
772+
773+
/* SP800-90B 4.4.2 - Adaptive Proportion Test
774+
* Monitors if a particular sample value appears too frequently within a
775+
* window of samples, indicating loss of entropy.
776+
*
777+
* Window size W = 512 for non-binary alphabet (byte values 0-255)
778+
* C = 1 + CRITBINOM(W, 2^(-H), 1-alpha)
779+
* For alpha = 2^-30 and H = 1, W = 512:
780+
* C = 1 + CRITBINOM(512, 0.5, 1-2^-30) = 325
781+
*/
782+
#ifndef WC_RNG_SEED_APT_WINDOW
783+
#define WC_RNG_SEED_APT_WINDOW 512
784+
#endif
785+
#ifndef WC_RNG_SEED_APT_CUTOFF
786+
#define WC_RNG_SEED_APT_CUTOFF 325
787+
#endif
788+
749789
int wc_RNG_TestSeed(const byte* seed, word32 seedSz)
750790
{
751791
int ret = 0;
752792

753-
/* Check the seed for duplicate words. */
754-
word32 seedIdx = 0;
755-
word32 scratchSz = 0;
793+
word32 i;
794+
int rctFailed = 0;
795+
int aptFailed = 0;
756796

757-
if (seed == NULL || seedSz < SEED_BLOCK_SZ)
797+
if (seed == NULL || seedSz == 0) {
758798
return BAD_FUNC_ARG;
799+
}
759800

760-
scratchSz = min(SEED_BLOCK_SZ, seedSz - SEED_BLOCK_SZ);
801+
/* SP800-90B 4.4.1 - Repetition Count Test (RCT)
802+
* Check for consecutive identical bytes that would indicate a stuck
803+
* entropy source. Fail if we see WC_RNG_SEED_RCT_CUTOFF or more
804+
* consecutive identical values.
805+
*
806+
* Constant-time implementation: always process full seed, accumulate
807+
* failure status without early exit to prevent timing side-channels.
808+
*/
809+
{
810+
int repCount = 1;
811+
byte prevByte = seed[0];
812+
813+
for (i = 1; i < seedSz; i++) {
814+
/* Constant-time: always evaluate both branches effects */
815+
int match = (seed[i] == prevByte);
816+
/* If match, increment count, if not, reset to 1 */
817+
repCount = (match * (repCount + 1)) + (!match * 1);
818+
/* Update prevByte only when not matching (new value) */
819+
prevByte = (byte) ((match * prevByte) + (!match * seed[i]));
820+
/* Accumulate failure flag - once set, stays set */
821+
rctFailed |= (repCount >= WC_RNG_SEED_RCT_CUTOFF);
822+
}
823+
}
824+
825+
/* SP800-90B 4.4.2 - Adaptive Proportion Test (APT)
826+
* Check that no single byte value appears too frequently within
827+
* a sliding window. This detects bias in the entropy source.
828+
*
829+
* For seeds smaller than the window size, we test the entire seed.
830+
* For larger seeds, we use a sliding window approach.
831+
*
832+
* Constant-time implementation: always process full seed and check
833+
* all counts to prevent timing side-channels.
834+
*/
835+
{
836+
word16 byteCounts[MAX_ENTROPY_BITS];
837+
word32 windowSize = min(seedSz, (word32)WC_RNG_SEED_APT_WINDOW);
838+
word32 windowStart = 0;
839+
word32 newIdx;
761840

762-
while (seedIdx < seedSz - SEED_BLOCK_SZ) {
763-
if (ConstantCompare(seed + seedIdx,
764-
seed + seedIdx + scratchSz,
765-
(int)scratchSz) == 0) {
841+
XMEMSET(byteCounts, 0, sizeof(byteCounts));
766842

767-
ret = DRBG_CONT_FAILURE;
843+
/* Initialize counts for first window */
844+
for (i = 0; i < windowSize; i++) {
845+
byteCounts[seed[i]]++;
846+
}
847+
848+
/* Check first window - scan all 256 counts */
849+
for (i = 0; i < MAX_ENTROPY_BITS; i++) {
850+
aptFailed |= (byteCounts[i] >= WC_RNG_SEED_APT_CUTOFF);
768851
}
769-
seedIdx += SEED_BLOCK_SZ;
770-
scratchSz = min(SEED_BLOCK_SZ, (seedSz - seedIdx));
852+
853+
/* Slide window through remaining seed data */
854+
while ((windowStart + windowSize) < seedSz) {
855+
/* Remove byte leaving the window */
856+
byteCounts[seed[windowStart]]--;
857+
windowStart++;
858+
859+
/* Add byte entering the window */
860+
newIdx = windowStart + windowSize - 1;
861+
byteCounts[seed[newIdx]]++;
862+
863+
/* Accumulate failure flag for new byte's count */
864+
aptFailed |= (byteCounts[seed[newIdx]] >= WC_RNG_SEED_APT_CUTOFF);
865+
}
866+
}
867+
868+
/* Set return code based on accumulated failure flags */
869+
if (rctFailed) {
870+
ret = ENTROPY_RT_E;
871+
}
872+
else if (aptFailed) {
873+
ret = ENTROPY_APT_E;
771874
}
772875

773876
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)