Skip to content

Commit baaa368

Browse files
authored
Merge pull request #9668 from kaleb-himes/PQ-FS-2026-Part1
PQ FS 2026 part1
2 parents 2c83711 + 20fc2de commit baaa368

File tree

8 files changed

+825
-18
lines changed

8 files changed

+825
-18
lines changed

.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 < SEED_BLOCK_SZ) {
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)