From aeed5847f2810c771846decceef21f34f02f0fe9 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sat, 19 Sep 2026 16:16:44 -0600 Subject: [PATCH 01/15] random: keep SP 800-90B RCT/APT verdicts through reseed --- wolfcrypt/src/random.c | 75 +++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 93e7802b977..9986376495a 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -3553,6 +3553,30 @@ int wc_InitRngNonce_ex2(WC_RNG* rng, const byte* nonce, word32 nonceSz, heap, devId, NULL, flags); } +#if defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) +/* Map a failed generate or reseed to the return code and rng->status. + * A failed SP 800-90A health test returns DRBG_CONT_FIPS_E. */ +static int RngGenerateFailure(WC_RNG* rng, int ret) +{ + if (ret == WC_NO_ERR_TRACE(DRBG_CONT_FAILURE)) { + rng->status = DRBG_CONT_FAILED; + return DRBG_CONT_FIPS_E; + } + + rng->status = DRBG_FAILED; + +#if FIPS_VERSION3_GE(7,0,0) + /* SP 800-90B RCT and APT failures keep their own code. */ + if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || + (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { + return ret; + } +#endif + + return RNG_FAILURE_E; +} +#endif + #if defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) #if defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) @@ -3577,8 +3601,7 @@ static WARN_UNUSED_RESULT WC_MAYBE_UNUSED int rng_pid_change_check(WC_RNG* rng) #if defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) ret = PollAndReSeed(rng, NULL, 0); if (ret != DRBG_SUCCESS) { - rng->status = DRBG_FAILED; - ret = RNG_FAILURE_E; + ret = RngGenerateFailure(rng, ret); } #endif @@ -4757,6 +4780,21 @@ int wc_RNG_DRBG_StirRBGC(WC_RNG* rng, WC_RNG* root, #if defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) +/* A failed seed source reports DRBG_FAILURE, except an SP 800-90B RCT or APT + * failure, which keeps its own code. */ +static int ReseedSourceFailure(int ret) +{ +#if FIPS_VERSION3_GE(7,0,0) + if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || + (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { + return ret; + } +#else + (void)ret; +#endif + return DRBG_FAILURE; +} + static WARN_UNUSED_RESULT int PollAndReSeed(WC_RNG* rng, const byte* additional, word32 additionalSz) { @@ -4806,6 +4844,7 @@ static WARN_UNUSED_RESULT int PollAndReSeed(WC_RNG* rng, const byte* additional, "ERROR: wc_GenerateSeed() in PollAndReSeed() failed with " "err %d", ret); #endif + ret = ReseedSourceFailure(ret); } #endif } @@ -5861,11 +5900,13 @@ int wc_RNG_GenerateBlock(WC_RNG* rng, byte* output, word32 sz) ((rng->RBGCStratum > 0) && (banked_stratum == 0))) { ret = wc_RNG_DRBG_NextSeedNow_local(rng); - if ((ret == WC_NO_ERR_TRACE(DRBG_CONT_FIPS_E)) || - (ret == WC_NO_ERR_TRACE(RNG_FAILURE_E))) - { + /* Key the bail on the instance state, not on a list of codes: + * this leg can now also see ENTROPY_RT_E / ENTROPY_APT_E, and + * falling through would let a later generate overwrite ret and + * report success for a call that already condemned the DRBG. */ + if (rng->status != DRBG_OK) { RngAutoLockExit(rng); - return ret; + return (ret != 0) ? ret : RNG_FAILURE_E; } } } @@ -5890,10 +5931,11 @@ int wc_RNG_GenerateBlock(WC_RNG* rng, byte* output, word32 sz) #ifdef WC_RNG_HAVE_LOCK if (WOLFSSL_ATOMIC_LOAD(rng->lock) & WC_RNG_LOCK_ENTROPY_INVALIDATED) { - if (PollAndReSeed(rng, NULL, 0) != DRBG_SUCCESS) { - rng->status = DRBG_FAILED; + int reseed_ret = PollAndReSeed(rng, NULL, 0); + if (reseed_ret != DRBG_SUCCESS) { + reseed_ret = RngGenerateFailure(rng, reseed_ret); RngAutoLockExit(rng); - return RNG_FAILURE_E; + return reseed_ret; } } #endif @@ -5960,12 +6002,15 @@ int wc_RNG_GenerateBlock(WC_RNG* rng, byte* output, word32 sz) rng->status = DRBG_CONT_FAILED; } else { - ret = RNG_FAILURE_E; - /* Note, Hash_DRBG_Generate() always leaves the DRBG in a - * self-consistent state, success or failure, and can fail for retryable - * causes (e.g. failed memory allocation), so we only update rng->status - * above. - */ + /* A mandatory reseed above can leave a seed health verdict in ret, and + * that arm has already set rng->status, so keep the SP 800-90B code + * rather than flattening it. Everything else is unchanged: + * Hash_DRBG_Generate() stays self-consistent and can fail for + * retryable causes such as an allocation, so it is not condemned. */ + if ((ret != WC_NO_ERR_TRACE(ENTROPY_RT_E)) && + (ret != WC_NO_ERR_TRACE(ENTROPY_APT_E))) { + ret = RNG_FAILURE_E; + } } RngAutoLockExit(rng); #else From 00a68582adce67a07efdec48b53a57165ab76ead Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Fri, 11 Sep 2026 14:52:23 -0600 Subject: [PATCH 02/15] tests/api: stuck seed source at reseed reports ENTROPY_RT_E --- tests/api/test_random.c | 59 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_random.h | 2 ++ 2 files changed, 61 insertions(+) diff --git a/tests/api/test_random.c b/tests/api/test_random.c index be08fd1d6a0..6fdd8041368 100644 --- a/tests/api/test_random.c +++ b/tests/api/test_random.c @@ -1092,6 +1092,65 @@ int test_wc_RNG_SeedCb(void) return EXPECT_RESULT(); } +#if defined(WC_RNG_SEED_CB) && defined(HAVE_HASHDRBG) && \ + !defined(CUSTOM_RAND_GENERATE_BLOCK) && !defined(HAVE_SELFTEST) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) && \ + !defined(WC_RNG_SEED_APT_CUTOFF) && !defined(WC_RNG_SEED_APT_WINDOW) && \ + !defined(WC_RNG_SEED_RCT_CUTOFF) +/* Stuck noise source: every seed byte is zero. */ +static int test_random_seedCb_stuck(OS_Seed* os, byte* seed, word32 sz) +{ + (void)os; + XMEMSET(seed, 0, sz); + return 0; +} +#endif + +/* A stuck seed source at reseed reports the SP 800-90B RCT code. + * rng.pid = 0 never matches the process, so the next generate reseeds. */ +int test_wc_RNG_ReseedVerdict(void) +{ + EXPECT_DECLS; +#if defined(WC_RNG_SEED_CB) && defined(HAVE_HASHDRBG) && \ + !defined(CUSTOM_RAND_GENERATE_BLOCK) && !defined(HAVE_SELFTEST) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) && \ + !defined(WC_RNG_SEED_APT_CUTOFF) && !defined(WC_RNG_SEED_APT_WINDOW) && \ + !defined(WC_RNG_SEED_RCT_CUTOFF) + WC_RNG rng; + byte out[32]; + + /* Do not inherit whatever source the previous test left installed: under + * HAVE_FIPS seedCb starts NULL and wc_InitRng() would fail. */ + DoExpectIntEQ(wc_SetSeed_Cb(WC_GENERATE_SEED_DEFAULT), 0); + XMEMSET(&rng, 0, sizeof(WC_RNG)); + ExpectIntEQ(wc_InitRng(&rng), 0); + + /* Control: a forced reseed from a working source succeeds. */ + rng.pid = 0; + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), 0); + + ExpectIntEQ(wc_SetSeed_Cb(test_random_seedCb_stuck), 0); + rng.pid = 0; +#if FIPS_VERSION3_GE(7,0,0) + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), + WC_NO_ERR_TRACE(ENTROPY_RT_E)); +#else + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), + WC_NO_ERR_TRACE(RNG_FAILURE_E)); +#endif + /* The failed instance stays failed. */ + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), + WC_NO_ERR_TRACE(RNG_FAILURE_E)); + + /* Do-form: restore even after a failed check, or later tests inherit it. */ + DoExpectIntEQ(wc_SetSeed_Cb(WC_GENERATE_SEED_DEFAULT), 0); + DoExpectIntEQ(wc_FreeRng(&rng), 0); +#endif + return EXPECT_RESULT(); +} + /* CUSTOM_RAND_GENERATE_BLOCK: an external RNG function bypasses Hash_DRBG * generation entirely in wc_RNG_GenerateBlock() (and _InitRng() itself is * skipped, since it is guarded by diff --git a/tests/api/test_random.h b/tests/api/test_random.h index 1a6d43229ec..84d7a6a289c 100644 --- a/tests/api/test_random.h +++ b/tests/api/test_random.h @@ -40,6 +40,7 @@ int test_wc_RNG_HealthTest_SHA512(void); int test_wc_RNG_HealthTest_SHA256_Ext(void); int test_wc_RNG_HealthTest_SHA512_Ext(void); int test_wc_RNG_SeedCb(void); +int test_wc_RNG_ReseedVerdict(void); int test_wc_RNG_CustomRandBlock(void); int test_wc_RNG_DrbgDisable(void); int test_wc_DrbgDecisionCoverage(void); @@ -63,6 +64,7 @@ int test_wc_Entropy_Get(void); TEST_DECL_GROUP("random", test_wc_RNG_HealthTest_SHA256_Ext), \ TEST_DECL_GROUP("random", test_wc_RNG_HealthTest_SHA512_Ext), \ TEST_DECL_GROUP("random", test_wc_RNG_SeedCb), \ + TEST_DECL_GROUP("random", test_wc_RNG_ReseedVerdict), \ TEST_DECL_GROUP("random", test_wc_RNG_CustomRandBlock), \ TEST_DECL_GROUP("random", test_wc_RNG_DrbgDisable), \ TEST_DECL_GROUP("random", test_wc_DrbgDecisionCoverage), \ From b084a5096a66386a5d0d90645c4a516b7e189502 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sat, 19 Sep 2026 17:14:03 -0600 Subject: [PATCH 03/15] random: seed APT uses the 90B reference window and its own cutoff --- doc/dox_comments/header_files/random.h | 9 +- wolfcrypt/src/random.c | 185 +++++++++++++------------ 2 files changed, 105 insertions(+), 89 deletions(-) diff --git a/doc/dox_comments/header_files/random.h b/doc/dox_comments/header_files/random.h index ba57ec27273..069775efac4 100644 --- a/doc/dox_comments/header_files/random.h +++ b/doc/dox_comments/header_files/random.h @@ -113,6 +113,8 @@ int wc_FreeNetRandom(void); MAX_REQUEST_LEN \return DRBG_CONT_FIPS_E wc_RNG_GenerateBlock: Hash_gen returned DRBG_CONT_FAILURE + \return ENTROPY_RT_E or ENTROPY_APT_E wc_InitRng: the SP 800-90B seed + health test rejected the entropy gathered to instantiate \return RNG_FAILURE_E wc_RNG_GenerateBlock: Default error. rng’s status originally not ok, or set to DRBG_FAILED \return BAD_MUTEX_E the lock that lets threads share this rng could not @@ -157,6 +159,8 @@ int wc_InitRng(WC_RNG* rng); \return 0 on success \return BAD_FUNC_ARG an input is null or sz exceeds MAX_REQUEST_LEN \return DRBG_CONT_FIPS_E Hash_gen returned DRBG_CONT_FAILURE + \return ENTROPY_RT_E or ENTROPY_APT_E the SP 800-90B seed health test + rejected the entropy gathered for a reseed \return RNG_FAILURE_E Default error. rng’s status originally not ok, or set to DRBG_FAILED \return BAD_MUTEX_E the rng's lock could not be taken @@ -544,7 +548,6 @@ int wc_RNG_DRBG_Reseed(WC_RNG* rng, const byte* seed, word32 seedSz); \return BAD_FUNC_ARG If seed is NULL \return ENTROPY_RT_E || ENTROPY_APT_E Validation failed \return ENTROPY_APT_E The adaptive proportion test failed. - \return MEMORY_E Allocation failed. \param seed Seed to test \param seedSz Seed size @@ -1303,7 +1306,7 @@ int wc_RNG_DRBG_GetNextSeedRBGCStratum(const WC_RNG* rng); \return 0 Bytes were banked (bank may or may not yet be complete). \return ALREADY_E The bank is ready or being consumed. - \return NOT_READY_E The health test could not run; simply retry. + \return NOT_READY_E The bank could not be completed; simply retry. \return BAD_FUNC_ARG rng is null or n is 0. \return MISSING_RNG_E rng has no DRBG (RDRAND et al.). @@ -1334,7 +1337,7 @@ int wc_RNG_DRBG_NextSeedGenerate(WC_RNG* rng, word32 n); \return 0 Bytes were banked. \return ALREADY_E The bank is ready or being consumed. - \return NOT_READY_E The health test could not run; simply retry. + \return NOT_READY_E The bank could not be completed; simply retry. \return BAD_FUNC_ARG rng or root is null, or n is 0. \return MISSING_RNG_E rng has no DRBG (RDRAND et al.). \return SEQ_OVERFLOW_E root's stratum is at the representable maximum. diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 9986376495a..69cf79c7b3e 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2593,18 +2593,11 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) return wc_RNG_DRBG_Stir_Nonce(rng, seed, seedSz, NULL, 0); } -/* FIPS 140-3 IG 10.3.A / SP800-90B Health Tests for Seed Data - * - * These tests replace the older FIPS 140-2 Continuous Random Number Generator - * Test (CRNGT) with more mathematically robust statistical tests per - * ISO 19790 / SP800-90B requirements. - * - * When HAVE_ENTROPY_MEMUSE is defined, the wolfentropy.c jitter-based TRNG - * performs another set of these health tests, but those are on the noise not - * the conditioned output so we still need to retest here even in that case - * to evaluate the conditioned output for the same behavior. These tests ensure - * the seed data meets basic entropy requirements regardless of the source. - */ +/* SP800-90B 4.4 health tests run over conditioned seed material, which makes + * them the developer-defined additional tests 4.3 Req 1c allows: 4.2 puts the + * noise source's own tests in that source, not here. A seed shorter than the + * 512 byte window is one window of its own length, with the cutoff for that + * length. */ /* SP800-90B 4.4.1 - Repetition Count Test * Detects if the noise source becomes "stuck" producing repeated output. @@ -2612,7 +2605,10 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) * C = 1 + ceil(-log2(alpha) / H) * For alpha = 2^-30 (false positive probability) and H = 1 (min entropy): * C = 1 + ceil(30 / 1) = 31 - */ + * + * H = 1 bit per byte is a deliberate floor, recorded in the Security Policy: + * the seed source is a build choice and the module is not told the assessed + * rate of the one in use, so it assumes the weakest. */ #ifndef WC_RNG_SEED_RCT_CUTOFF #define WC_RNG_SEED_RCT_CUTOFF 31 #endif @@ -2629,10 +2625,71 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) #ifndef WC_RNG_SEED_APT_WINDOW #define WC_RNG_SEED_APT_WINDOW 512 #endif +#if (WC_RNG_SEED_APT_WINDOW > 512) || (WC_RNG_SEED_APT_WINDOW < 1) + #error WC_RNG_SEED_APT_WINDOW must be 1 to 512 +#endif #ifndef WC_RNG_SEED_APT_CUTOFF + /* No caller-supplied cutoff: take one per window size from the table. + * A caller-supplied cutoff also turns off the all-values test below. */ + #define WC_RNG_SEED_APT_CUTOFF_PER_WINDOW + /* The published W = 512 value, kept for readers and for tests. */ #define WC_RNG_SEED_APT_CUTOFF 325 #endif +#ifdef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW +/* C for every window size, not only 512: a seed shorter than the window is one + * window of its own length, and 325 can never be reached in 132 or 196 bytes. + * 1 + CRITBINOM(W, 2^-H, 1-alpha), H = 1, alpha = 2^-30, W = 1..512. */ +static const word16 aptCutoffTable[512] = { + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 30, 31, 32, 33, 34, 35, 35, + 36, 37, 38, 38, 39, 40, 41, 41, 42, 43, 44, 44, + 45, 46, 47, 47, 48, 49, 49, 50, 51, 52, 52, 53, + 54, 54, 55, 56, 57, 57, 58, 59, 59, 60, 61, 61, + 62, 63, 63, 64, 65, 65, 66, 67, 68, 68, 69, 70, + 70, 71, 72, 72, 73, 74, 74, 75, 76, 76, 77, 77, + 78, 79, 79, 80, 81, 81, 82, 83, 83, 84, 85, 85, + 86, 87, 87, 88, 89, 89, 90, 91, 91, 92, 92, 93, + 94, 94, 95, 96, 96, 97, 98, 98, 99, 99, 100, 101, + 101, 102, 103, 103, 104, 105, 105, 106, 106, 107, 108, 108, + 109, 110, 110, 111, 111, 112, 113, 113, 114, 115, 115, 116, + 116, 117, 118, 118, 119, 120, 120, 121, 121, 122, 123, 123, + 124, 124, 125, 126, 126, 127, 128, 128, 129, 129, 130, 131, + 131, 132, 132, 133, 134, 134, 135, 136, 136, 137, 137, 138, + 139, 139, 140, 140, 141, 142, 142, 143, 143, 144, 145, 145, + 146, 146, 147, 148, 148, 149, 150, 150, 151, 151, 152, 153, + 153, 154, 154, 155, 156, 156, 157, 157, 158, 159, 159, 160, + 160, 161, 162, 162, 163, 163, 164, 165, 165, 166, 166, 167, + 168, 168, 169, 169, 170, 171, 171, 172, 172, 173, 174, 174, + 175, 175, 176, 177, 177, 178, 178, 179, 179, 180, 181, 181, + 182, 182, 183, 184, 184, 185, 185, 186, 187, 187, 188, 188, + 189, 190, 190, 191, 191, 192, 193, 193, 194, 194, 195, 195, + 196, 197, 197, 198, 198, 199, 200, 200, 201, 201, 202, 203, + 203, 204, 204, 205, 205, 206, 207, 207, 208, 208, 209, 210, + 210, 211, 211, 212, 212, 213, 214, 214, 215, 215, 216, 217, + 217, 218, 218, 219, 220, 220, 221, 221, 222, 222, 223, 224, + 224, 225, 225, 226, 227, 227, 228, 228, 229, 229, 230, 231, + 231, 232, 232, 233, 233, 234, 235, 235, 236, 236, 237, 238, + 238, 239, 239, 240, 240, 241, 242, 242, 243, 243, 244, 244, + 245, 246, 246, 247, 247, 248, 249, 249, 250, 250, 251, 251, + 252, 253, 253, 254, 254, 255, 255, 256, 257, 257, 258, 258, + 259, 259, 260, 261, 261, 262, 262, 263, 264, 264, 265, 265, + 266, 266, 267, 268, 268, 269, 269, 270, 270, 271, 272, 272, + 273, 273, 274, 274, 275, 276, 276, 277, 277, 278, 278, 279, + 280, 280, 281, 281, 282, 282, 283, 284, 284, 285, 285, 286, + 286, 287, 288, 288, 289, 289, 290, 290, 291, 292, 292, 293, + 293, 294, 294, 295, 296, 296, 297, 297, 298, 298, 299, 300, + 300, 301, 301, 302, 302, 303, 304, 304, 305, 305, 306, 306, + 307, 308, 308, 309, 309, 310, 310, 311, 312, 312, 313, 313, + 314, 314, 315, 316, 316, 317, 317, 318, 318, 319, 319, 320, + 321, 321, 322, 322, 323, 323, 324, 325 +}; + #define WC_RNG_SEED_APT_CUTOFF_FOR(w) ((word32)aptCutoffTable[(w) - 1]) +#else + #define WC_RNG_SEED_APT_CUTOFF_FOR(w) ((word32)WC_RNG_SEED_APT_CUTOFF) +#endif + int wc_RNG_TestSeed(const byte* seed, word32 seedSz) { int ret = 0; @@ -2669,64 +2726,37 @@ int wc_RNG_TestSeed(const byte* seed, word32 seedSz) } } - /* SP800-90B 4.4.2 - Adaptive Proportion Test (APT) - * Check that no single byte value appears too frequently within - * a sliding window. This detects bias in the entropy source. - * - * For seeds smaller than the window size, we test the entire seed. - * For larger seeds, we use a sliding window approach. - * - * Constant-time implementation: always process full seed and check - * all counts to prevent timing side-channels. - */ + /* SP800-90B 4.4.2 Adaptive Proportion Test: the first byte of each + * non-overlapping window is the reference value, and a window fails when + * matches reach the cutoff for that window size. Bias toward any other + * value is caught by the all-values test below. */ { - #if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_SMALL_STACK_CACHE) - word16* byteCounts = NULL; - #else - word16 byteCounts[MAX_ENTROPY_BITS]; - #endif - word32 windowSize = min(seedSz, (word32)WC_RNG_SEED_APT_WINDOW); - word32 windowStart = 0; - word32 newIdx; - - #if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_SMALL_STACK_CACHE) - byteCounts = (word16*)XMALLOC(MAX_ENTROPY_BITS * sizeof(word16), NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (byteCounts == NULL) - return MEMORY_E; - #endif - XMEMSET(byteCounts, 0, MAX_ENTROPY_BITS * sizeof(word16)); - - /* Indices are WC_OCTET-masked: byteCounts has 256 entries, but a - * byte cell can exceed 255 where CHAR_BIT != 8, so an unmasked seed - * value would index out of bounds. */ - for (i = 0; i < windowSize; i++) { - byteCounts[WC_OCTET(seed[i])]++; - } - - /* Check first window - scan all 256 counts */ - for (i = 0; i < MAX_ENTROPY_BITS; i++) { - aptFailed |= (byteCounts[i] >= WC_RNG_SEED_APT_CUTOFF); - } - - /* Slide window through remaining seed data */ - while ((windowStart + windowSize) < seedSz) { - /* Remove byte leaving the window */ - byteCounts[WC_OCTET(seed[windowStart])]--; - windowStart++; - - /* Add byte entering the window */ - newIdx = windowStart + windowSize - 1; - byteCounts[WC_OCTET(seed[newIdx])]++; + word32 start; + word32 window = min(seedSz, (word32)WC_RNG_SEED_APT_WINDOW); + word32 cutoff = WC_RNG_SEED_APT_CUTOFF_FOR(window); + + /* A cutoff above the window can never be reached (IG D.K Res 16); that + * is every window under 30 bytes at alpha 2^-30, and such a seed gets + * no APT and rests on the RCT above. */ + if (cutoff <= window) { + /* Constant time: every window is scanned in full, no early exit. */ + for (start = 0; start < seedSz; start += window) { + word32 matches = 1; + byte refByte; + + /* A trailing piece shorter than the window slides back to a + * full window instead of forming a short one. */ + if ((seedSz - start) < window) + start = seedSz - window; + + refByte = seed[start]; + for (i = 1; i < window; i++) { + matches += (word32)(seed[start + i] == refByte); + } - /* Accumulate failure flag for new byte's count */ - aptFailed |= (byteCounts[WC_OCTET(seed[newIdx])] >= - WC_RNG_SEED_APT_CUTOFF); + aptFailed |= (matches >= cutoff); + } } - - #if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_SMALL_STACK_CACHE) - XFREE(byteCounts, NULL, DYNAMIC_TYPE_TMP_BUFFER); - #endif } /* Set return code based on accumulated failure flags */ @@ -3565,13 +3595,11 @@ static int RngGenerateFailure(WC_RNG* rng, int ret) rng->status = DRBG_FAILED; -#if FIPS_VERSION3_GE(7,0,0) /* SP 800-90B RCT and APT failures keep their own code. */ if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { return ret; } -#endif return RNG_FAILURE_E; } @@ -4781,17 +4809,13 @@ int wc_RNG_DRBG_StirRBGC(WC_RNG* rng, WC_RNG* root, #if defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) /* A failed seed source reports DRBG_FAILURE, except an SP 800-90B RCT or APT - * failure, which keeps its own code. */ + * verdict, which keeps its own code so the caller can tell the two apart. */ static int ReseedSourceFailure(int ret) { -#if FIPS_VERSION3_GE(7,0,0) if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { return ret; } -#else - (void)ret; -#endif return DRBG_FAILURE; } @@ -5307,17 +5331,6 @@ static WARN_UNUSED_RESULT int wc_RNG_DRBG_NextSeedGenerate_local( #endif return 0; } - else if (ret == WC_NO_ERR_TRACE(MEMORY_E)) { - /* wc_RNG_TestSeed() did nothing with the data -- not - * dispositive. Release complete-but-unpublished for a - * later retry; a purge-discard's BUSY_E percolates (the - * retry cause is then the purge, not the test). */ - ret = NextSeedProducerRelease(lenp, seed, nextSeedSz, - (WC_ATOMIC_INT_ARG)nextSeedSz); - if (ret != 0) - return ret; - return NOT_READY_E; - } else if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { From 2a0d6bbdfc6ace3b584374d26f3ed52605797081 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sat, 19 Sep 2026 17:14:03 -0600 Subject: [PATCH 04/15] tests/api: cover the seed APT cutoff, windows and remainder --- tests/api/test_random.c | 169 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 4 deletions(-) diff --git a/tests/api/test_random.c b/tests/api/test_random.c index 6fdd8041368..0a1f1977dfd 100644 --- a/tests/api/test_random.c +++ b/tests/api/test_random.c @@ -413,6 +413,38 @@ int test_wc_RNG_DRBG_Reseed(void) return EXPECT_RESULT(); } +#if defined(HAVE_HASHDRBG) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + !defined(HAVE_SELFTEST) && !defined(WC_RNG_SEED_APT_CUTOFF) && \ + !defined(WC_RNG_SEED_APT_WINDOW) && !defined(WC_RNG_SEED_RCT_CUTOFF) +/* Build one APT window whose reference value occurs m+1 times, keeping runs + * under the RCT cutoff so only the APT can fire. Returns -1 if m does not + * fit in the window. */ +static int test_random_apt_window(byte* s, word32 start, word32 win, byte ref, + word32 m) +{ + word32 i, placed = 0, run = 1; + byte filler = 0; + + for (i = 0; i < win; i++) { + if (++filler == ref) + filler++; + s[start + i] = filler; + } + s[start] = ref; + for (i = 1; (i < win) && (placed < m); i++) { + if (run >= 29) { + run = 0; + continue; + } + s[start + i] = ref; + placed++; + run++; + } + return (placed == m) ? 0 : -1; +} +#endif + int test_wc_RNG_TestSeed(void) { EXPECT_DECLS; @@ -449,6 +481,90 @@ int test_wc_RNG_TestSeed(void) seed[i] = i; ExpectIntEQ(wc_RNG_TestSeed(seed, sizeof(seed)), 0); #endif + +#if defined(HAVE_HASHDRBG) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + !defined(HAVE_SELFTEST) && !defined(WC_RNG_SEED_APT_CUTOFF) && \ + !defined(WC_RNG_SEED_APT_WINDOW) && !defined(WC_RNG_SEED_RCT_CUTOFF) + /* SP 800-90B 4.4.2 cutoffs are per window size, 1 + CRITBINOM(W, 2^-H, + * 1-alpha) with H = 1 and alpha = 2^-30: 101 at W = 132 (the FIPS reseed + * seed), 140 at W = 196 (instantiate) and 325 at W = 512. */ + { + byte* buf = (byte*)XMALLOC(1024, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + ExpectNotNull(buf); + if (buf != NULL) { + /* one below the cutoff passes, the cutoff itself fails */ + ExpectIntEQ(test_random_apt_window(buf, 0, 132, 0x11, 101 - 2), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 132), 0); + ExpectIntEQ(test_random_apt_window(buf, 0, 132, 0x11, 101 - 1), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 132), + WC_NO_ERR_TRACE(ENTROPY_APT_E)); + + ExpectIntEQ(test_random_apt_window(buf, 0, 196, 0x22, 140 - 2), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 196), 0); + ExpectIntEQ(test_random_apt_window(buf, 0, 196, 0x22, 140 - 1), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 196), + WC_NO_ERR_TRACE(ENTROPY_APT_E)); + + ExpectIntEQ(test_random_apt_window(buf, 0, 512, 0x33, 325 - 2), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 512), 0); + ExpectIntEQ(test_random_apt_window(buf, 0, 512, 0x33, 325 - 1), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 512), + WC_NO_ERR_TRACE(ENTROPY_APT_E)); + + /* windows do not overlap: bias in the second window is caught */ + ExpectIntEQ(test_random_apt_window(buf, 0, 512, 0x44, 0), 0); + ExpectIntEQ(test_random_apt_window(buf, 512, 512, 0x55, 325 - 1), + 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 1024), + WC_NO_ERR_TRACE(ENTROPY_APT_E)); + + /* the all-values test (SP 800-90B 4.3 Req 1c) catches bias toward + * a value that is not the window's first byte; its cutoff is + * 1 + CRITBINOM(W, 2^-H, 1-alpha/256), which is 105 at W = 132 */ + { + word32 j, placed = 0, run = 0; + byte filler = 0; + + for (j = 0; j < 132; j++) { + if ((placed < 105) && (run < 28) && (j > 0)) { + buf[j] = 0x99; + placed++; + run++; + } + else { + if (++filler == 0x99) + filler++; + buf[j] = filler; + run = 0; + } + } + buf[0] = 0x01; + ExpectIntEQ(placed, 105); + ExpectIntEQ(wc_RNG_TestSeed(buf, 132), + WC_NO_ERR_TRACE(ENTROPY_APT_E)); + buf[1] = 0x02; /* one below the cutoff */ + ExpectIntEQ(wc_RNG_TestSeed(buf, 132), 0); + } + + /* a seed that does not divide into whole windows slides its last + * window back to full length, so the trailing bytes are covered by + * a window whose cutoff is reachable (IG D.K Res 16) */ + ExpectIntEQ(test_random_apt_window(buf, 0, 512, 0x44, 0), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 513), 0); + ExpectIntEQ(test_random_apt_window(buf, 88, 512, 0x66, 325 - 1), 0); + ExpectIntEQ(wc_RNG_TestSeed(buf, 600), + WC_NO_ERR_TRACE(ENTROPY_APT_E)); + + /* under 30 bytes no cutoff is reachable, so the APT is not applied + * and a short all-same seed is left to the RCT */ + XMEMSET(buf, 0x5a, 20); + ExpectIntEQ(wc_RNG_TestSeed(buf, 20), 0); + } + XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif return EXPECT_RESULT(); } @@ -1105,6 +1221,36 @@ static int test_random_seedCb_stuck(OS_Seed* os, byte* seed, word32 sz) XMEMSET(seed, 0, sz); return 0; } + +/* Source stuck on one value, with a different byte every 28 so no run can + * reach the RCT cutoff: only the APT can object, at any seed size. */ +static int test_random_seedCb_biased(OS_Seed* os, byte* seed, word32 sz) +{ + word32 i; + byte filler = 0; + + (void)os; + for (i = 0; i < sz; i++) { + if ((i > 0) && ((i % 28) == 0)) { + if (++filler == 0x99) + filler++; + seed[i] = filler; + } + else { + seed[i] = 0x99; + } + } + return 0; +} + +/* Source that fails with something that is not a 90B verdict. */ +static int test_random_seedCb_broken(OS_Seed* os, byte* seed, word32 sz) +{ + (void)os; + (void)seed; + (void)sz; + return WC_NO_ERR_TRACE(BAD_FUNC_ARG); +} #endif /* A stuck seed source at reseed reports the SP 800-90B RCT code. @@ -1133,14 +1279,29 @@ int test_wc_RNG_ReseedVerdict(void) ExpectIntEQ(wc_SetSeed_Cb(test_random_seedCb_stuck), 0); rng.pid = 0; -#if FIPS_VERSION3_GE(7,0,0) ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), WC_NO_ERR_TRACE(ENTROPY_RT_E)); -#else + /* The failed instance stays failed. */ ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), WC_NO_ERR_TRACE(RNG_FAILURE_E)); -#endif - /* The failed instance stays failed. */ + + /* A biased source reports the APT verdict out of the generate path. */ + DoExpectIntEQ(wc_FreeRng(&rng), 0); + XMEMSET(&rng, 0, sizeof(WC_RNG)); + DoExpectIntEQ(wc_SetSeed_Cb(WC_GENERATE_SEED_DEFAULT), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_SetSeed_Cb(test_random_seedCb_biased), 0); + rng.pid = 0; + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), + WC_NO_ERR_TRACE(ENTROPY_APT_E)); + + /* A source error that is not a 90B verdict still reports RNG_FAILURE_E. */ + DoExpectIntEQ(wc_FreeRng(&rng), 0); + XMEMSET(&rng, 0, sizeof(WC_RNG)); + DoExpectIntEQ(wc_SetSeed_Cb(WC_GENERATE_SEED_DEFAULT), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_SetSeed_Cb(test_random_seedCb_broken), 0); + rng.pid = 0; ExpectIntEQ(wc_RNG_GenerateBlock(&rng, out, sizeof(out)), WC_NO_ERR_TRACE(RNG_FAILURE_E)); From 617ad9b1c2035df4f3ded26bad5869492b0fdb90 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sat, 19 Sep 2026 21:41:28 -0600 Subject: [PATCH 05/15] linuxkm: recover the DRBG on a seed health-test alarm too --- linuxkm/lkcapi_sha_glue.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/linuxkm/lkcapi_sha_glue.c b/linuxkm/lkcapi_sha_glue.c index 7fa98f6d32e..5207e07a8dc 100644 --- a/linuxkm/lkcapi_sha_glue.c +++ b/linuxkm/lkcapi_sha_glue.c @@ -3956,7 +3956,12 @@ static int wc_linuxkm_drbg_generate(struct wc_rng_bank *ctx, if (ret == 0) continue; - if (unlikely(ret == WC_NO_ERR_TRACE(RNG_FAILURE_E))) { + /* A seed health-test alarm now arrives as its own SP 800-90B code; + * it is the same recoverable condition as RNG_FAILURE_E. */ + if (unlikely((ret == WC_NO_ERR_TRACE(RNG_FAILURE_E)) || + (ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || + (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E)))) + { if (slen > 0) break; @@ -4035,13 +4040,13 @@ static int wc_linuxkm_drbg_generate(struct wc_rng_bank *ctx, if (ret == 0) { pr_warn_ratelimited("WARNING: reinitialized DRBG #%d after " - "RNG_FAILURE_E from wc_RNG_GenerateBlock().\n", + "a seed health or RNG failure from wc_RNG_GenerateBlock().\n", wc_rng_bank_get_inst_id(drbg)); continue; } else { pr_err_ratelimited("ERROR: reinitialization of DRBG #%d after " - "RNG_FAILURE_E failed with ret %d.\n", + "a seed health or RNG failure failed with ret %d.\n", wc_rng_bank_get_inst_id(drbg), ret); break; } From 0cdb65ee9d0e546f2db72f02321c6560ff4327e4 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sun, 20 Sep 2026 04:42:20 -0600 Subject: [PATCH 06/15] random: add the all-values seed check as a 4.3 Req 1c extra test --- wolfcrypt/src/random.c | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 69cf79c7b3e..14a5d9c2783 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2686,6 +2686,57 @@ static const word16 aptCutoffTable[512] = { 321, 321, 322, 322, 323, 323, 324, 325 }; #define WC_RNG_SEED_APT_CUTOFF_FOR(w) ((word32)aptCutoffTable[(w) - 1]) + +/* Cutoff for the all-values test below, at alpha/256 so that scanning the + * whole alphabet keeps the same 2^-30 budget per window. + * 1 + CRITBINOM(W, 2^-H, 1-alpha/256), H = 1, alpha = 2^-30, W = 1..512. */ +static const word16 aptAllCutoffTable[512] = { + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 38, 38, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, + 47, 48, 49, 50, 50, 51, 52, 53, 53, 54, 55, 56, + 56, 57, 58, 59, 59, 60, 61, 62, 62, 63, 64, 64, + 65, 66, 67, 67, 68, 69, 69, 70, 71, 71, 72, 73, + 74, 74, 75, 76, 76, 77, 78, 78, 79, 80, 80, 81, + 82, 83, 83, 84, 85, 85, 86, 87, 87, 88, 89, 89, + 90, 91, 91, 92, 93, 93, 94, 95, 95, 96, 97, 97, + 98, 99, 99, 100, 101, 101, 102, 103, 103, 104, 105, 105, + 106, 106, 107, 108, 108, 109, 110, 110, 111, 112, 112, 113, + 114, 114, 115, 116, 116, 117, 117, 118, 119, 119, 120, 121, + 121, 122, 123, 123, 124, 125, 125, 126, 126, 127, 128, 128, + 129, 130, 130, 131, 132, 132, 133, 133, 134, 135, 135, 136, + 137, 137, 138, 138, 139, 140, 140, 141, 142, 142, 143, 143, + 144, 145, 145, 146, 147, 147, 148, 148, 149, 150, 150, 151, + 152, 152, 153, 153, 154, 155, 155, 156, 157, 157, 158, 158, + 159, 160, 160, 161, 162, 162, 163, 163, 164, 165, 165, 166, + 166, 167, 168, 168, 169, 170, 170, 171, 171, 172, 173, 173, + 174, 174, 175, 176, 176, 177, 177, 178, 179, 179, 180, 181, + 181, 182, 182, 183, 184, 184, 185, 185, 186, 187, 187, 188, + 188, 189, 190, 190, 191, 191, 192, 193, 193, 194, 195, 195, + 196, 196, 197, 198, 198, 199, 199, 200, 201, 201, 202, 202, + 203, 204, 204, 205, 205, 206, 207, 207, 208, 208, 209, 210, + 210, 211, 211, 212, 213, 213, 214, 214, 215, 216, 216, 217, + 217, 218, 219, 219, 220, 220, 221, 222, 222, 223, 223, 224, + 225, 225, 226, 226, 227, 228, 228, 229, 229, 230, 230, 231, + 232, 232, 233, 233, 234, 235, 235, 236, 236, 237, 238, 238, + 239, 239, 240, 241, 241, 242, 242, 243, 244, 244, 245, 245, + 246, 246, 247, 248, 248, 249, 249, 250, 251, 251, 252, 252, + 253, 254, 254, 255, 255, 256, 257, 257, 258, 258, 259, 259, + 260, 261, 261, 262, 262, 263, 264, 264, 265, 265, 266, 267, + 267, 268, 268, 269, 269, 270, 271, 271, 272, 272, 273, 274, + 274, 275, 275, 276, 276, 277, 278, 278, 279, 279, 280, 281, + 281, 282, 282, 283, 283, 284, 285, 285, 286, 286, 287, 288, + 288, 289, 289, 290, 290, 291, 292, 292, 293, 293, 294, 295, + 295, 296, 296, 297, 297, 298, 299, 299, 300, 300, 301, 302, + 302, 303, 303, 304, 304, 305, 306, 306, 307, 307, 308, 309, + 309, 310, 310, 311, 311, 312, 313, 313, 314, 314, 315, 315, + 316, 317, 317, 318, 318, 319, 320, 320, 321, 321, 322, 322, + 323, 324, 324, 325, 325, 326, 326, 327, 328, 328, 329, 329, + 330, 330, 331, 332, 332, 333, 333, 334 +}; + #define WC_RNG_SEED_APT_ALL_CUTOFF_FOR(w) \ + ((word32)aptAllCutoffTable[(w) - 1]) #else #define WC_RNG_SEED_APT_CUTOFF_FOR(w) ((word32)WC_RNG_SEED_APT_CUTOFF) #endif @@ -2759,6 +2810,46 @@ int wc_RNG_TestSeed(const byte* seed, word32 seedSz) } } +#ifdef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW + /* Additional developer-defined test (SP800-90B 4.3 Req 1c): 4.4.2 watches + * only the window's first byte, this watches every value. Its cutoff uses + * alpha/256 for the alphabet and always lands above half the window. */ + { + word32 start; + word32 window = min(seedSz, (word32)WC_RNG_SEED_APT_WINDOW); + word32 cutoff = WC_RNG_SEED_APT_ALL_CUTOFF_FOR(window); + + if ((cutoff <= window) && (cutoff > (window / 2))) { + for (start = 0; start < seedSz; start += window) { + byte cand = 0; + word32 votes = 0; + word32 count = 0; + + if ((seedSz - start) < window) + start = seedSz - window; + + /* A count above half the window makes that value the window's + * majority, which this vote finds without a histogram. */ + for (i = 0; i < window; i++) { + word32 take = (word32)(votes == 0); + word32 same; + + cand = (byte)((take * seed[start + i]) + + ((1 - take) * cand)); + same = (word32)(seed[start + i] == cand); + votes = (same * (votes + 1)) + ((1 - same) * (votes - 1)); + } + + for (i = 0; i < window; i++) { + count += (word32)(seed[start + i] == cand); + } + + aptFailed |= (count >= cutoff); + } + } + } +#endif + /* Set return code based on accumulated failure flags */ if (rctFailed) { ret = ENTROPY_RT_E; From 8c7e00ce615ed06be8328298a18b2efde60143e9 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sun, 20 Sep 2026 11:07:12 -0600 Subject: [PATCH 07/15] random: hold the seed APT cutoffs as byte deltas from half the window --- wolfcrypt/src/random.c | 167 ++++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 93 deletions(-) diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 14a5d9c2783..e07c5f76278 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2639,104 +2639,85 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) #ifdef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW /* C for every window size, not only 512: a seed shorter than the window is one * window of its own length, and 325 can never be reached in 132 or 196 bytes. - * 1 + CRITBINOM(W, 2^-H, 1-alpha), H = 1, alpha = 2^-30, W = 1..512. */ -static const word16 aptCutoffTable[512] = { - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - 26, 27, 28, 29, 30, 30, 31, 32, 33, 34, 35, 35, - 36, 37, 38, 38, 39, 40, 41, 41, 42, 43, 44, 44, - 45, 46, 47, 47, 48, 49, 49, 50, 51, 52, 52, 53, - 54, 54, 55, 56, 57, 57, 58, 59, 59, 60, 61, 61, - 62, 63, 63, 64, 65, 65, 66, 67, 68, 68, 69, 70, - 70, 71, 72, 72, 73, 74, 74, 75, 76, 76, 77, 77, - 78, 79, 79, 80, 81, 81, 82, 83, 83, 84, 85, 85, - 86, 87, 87, 88, 89, 89, 90, 91, 91, 92, 92, 93, - 94, 94, 95, 96, 96, 97, 98, 98, 99, 99, 100, 101, - 101, 102, 103, 103, 104, 105, 105, 106, 106, 107, 108, 108, - 109, 110, 110, 111, 111, 112, 113, 113, 114, 115, 115, 116, - 116, 117, 118, 118, 119, 120, 120, 121, 121, 122, 123, 123, - 124, 124, 125, 126, 126, 127, 128, 128, 129, 129, 130, 131, - 131, 132, 132, 133, 134, 134, 135, 136, 136, 137, 137, 138, - 139, 139, 140, 140, 141, 142, 142, 143, 143, 144, 145, 145, - 146, 146, 147, 148, 148, 149, 150, 150, 151, 151, 152, 153, - 153, 154, 154, 155, 156, 156, 157, 157, 158, 159, 159, 160, - 160, 161, 162, 162, 163, 163, 164, 165, 165, 166, 166, 167, - 168, 168, 169, 169, 170, 171, 171, 172, 172, 173, 174, 174, - 175, 175, 176, 177, 177, 178, 178, 179, 179, 180, 181, 181, - 182, 182, 183, 184, 184, 185, 185, 186, 187, 187, 188, 188, - 189, 190, 190, 191, 191, 192, 193, 193, 194, 194, 195, 195, - 196, 197, 197, 198, 198, 199, 200, 200, 201, 201, 202, 203, - 203, 204, 204, 205, 205, 206, 207, 207, 208, 208, 209, 210, - 210, 211, 211, 212, 212, 213, 214, 214, 215, 215, 216, 217, - 217, 218, 218, 219, 220, 220, 221, 221, 222, 222, 223, 224, - 224, 225, 225, 226, 227, 227, 228, 228, 229, 229, 230, 231, - 231, 232, 232, 233, 233, 234, 235, 235, 236, 236, 237, 238, - 238, 239, 239, 240, 240, 241, 242, 242, 243, 243, 244, 244, - 245, 246, 246, 247, 247, 248, 249, 249, 250, 250, 251, 251, - 252, 253, 253, 254, 254, 255, 255, 256, 257, 257, 258, 258, - 259, 259, 260, 261, 261, 262, 262, 263, 264, 264, 265, 265, - 266, 266, 267, 268, 268, 269, 269, 270, 270, 271, 272, 272, - 273, 273, 274, 274, 275, 276, 276, 277, 277, 278, 278, 279, - 280, 280, 281, 281, 282, 282, 283, 284, 284, 285, 285, 286, - 286, 287, 288, 288, 289, 289, 290, 290, 291, 292, 292, 293, - 293, 294, 294, 295, 296, 296, 297, 297, 298, 298, 299, 300, - 300, 301, 301, 302, 302, 303, 304, 304, 305, 305, 306, 306, - 307, 308, 308, 309, 309, 310, 310, 311, 312, 312, 313, 313, - 314, 314, 315, 316, 316, 317, 317, 318, 318, 319, 319, 320, - 321, 321, 322, 322, 323, 323, 324, 325 + * 1 + CRITBINOM(W, 2^-H, 1 - alpha), H = 1, alpha = 2^-30, W = 1..512, held as + * the exact distance above W / 2 so a byte covers every window. */ +static const byte aptCutoffDelta[512] = { + 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, + 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 15, 16, 16, + 17, 17, 18, 17, 18, 18, 19, 18, 19, 19, 20, 19, 20, 20, 21, 20, + 21, 21, 22, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 23, 24, 24, + 25, 24, 25, 25, 25, 25, 26, 25, 26, 26, 26, 26, 27, 26, 27, 27, + 28, 27, 28, 28, 28, 28, 29, 28, 29, 29, 29, 29, 30, 29, 30, 29, + 30, 30, 30, 30, 31, 30, 31, 31, 31, 31, 32, 31, 32, 32, 32, 32, + 33, 32, 33, 33, 33, 33, 33, 33, 34, 33, 34, 34, 34, 34, 35, 34, + 35, 34, 35, 35, 35, 35, 36, 35, 36, 36, 36, 36, 36, 36, 37, 36, + 37, 37, 37, 37, 37, 37, 38, 37, 38, 38, 38, 38, 38, 38, 39, 38, + 39, 39, 39, 39, 39, 39, 40, 39, 40, 39, 40, 40, 40, 40, 41, 40, + 41, 40, 41, 41, 41, 41, 41, 41, 42, 41, 42, 42, 42, 42, 42, 42, + 43, 42, 43, 42, 43, 43, 43, 43, 43, 43, 44, 43, 44, 43, 44, 44, + 44, 44, 45, 44, 45, 44, 45, 45, 45, 45, 45, 45, 46, 45, 46, 45, + 46, 46, 46, 46, 46, 46, 47, 46, 47, 46, 47, 47, 47, 47, 47, 47, + 48, 47, 48, 47, 48, 48, 48, 48, 48, 48, 49, 48, 49, 48, 49, 49, + 49, 49, 49, 49, 49, 49, 50, 49, 50, 49, 50, 50, 50, 50, 50, 50, + 51, 50, 51, 50, 51, 51, 51, 51, 51, 51, 52, 51, 52, 51, 52, 51, + 52, 52, 52, 52, 52, 52, 53, 52, 53, 52, 53, 53, 53, 53, 53, 53, + 53, 53, 54, 53, 54, 53, 54, 54, 54, 54, 54, 54, 54, 54, 55, 54, + 55, 54, 55, 55, 55, 55, 55, 55, 56, 55, 56, 55, 56, 55, 56, 56, + 56, 56, 56, 56, 57, 56, 57, 56, 57, 56, 57, 57, 57, 57, 57, 57, + 57, 57, 58, 57, 58, 57, 58, 58, 58, 58, 58, 58, 58, 58, 59, 58, + 59, 58, 59, 58, 59, 59, 59, 59, 59, 59, 60, 59, 60, 59, 60, 59, + 60, 60, 60, 60, 60, 60, 60, 60, 61, 60, 61, 60, 61, 60, 61, 61, + 61, 61, 61, 61, 62, 61, 62, 61, 62, 61, 62, 62, 62, 62, 62, 62, + 62, 62, 63, 62, 63, 62, 63, 62, 63, 63, 63, 63, 63, 63, 63, 63, + 64, 63, 64, 63, 64, 63, 64, 64, 64, 64, 64, 64, 64, 64, 65, 64, + 65, 64, 65, 64, 65, 65, 65, 65, 65, 65, 65, 65, 66, 65, 66, 65, + 66, 65, 66, 66, 66, 66, 66, 66, 66, 66, 67, 66, 67, 66, 67, 66, + 67, 67, 67, 67, 67, 67, 67, 67, 68, 67, 68, 67, 68, 67, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68, 69, 68, 69, 68, 69, 68, 69, 69 }; - #define WC_RNG_SEED_APT_CUTOFF_FOR(w) ((word32)aptCutoffTable[(w) - 1]) + #define WC_RNG_SEED_APT_CUTOFF_FOR(w) \ + (((word32)(w) / 2) + (word32)aptCutoffDelta[(w) - 1]) /* Cutoff for the all-values test below, at alpha/256 so that scanning the - * whole alphabet keeps the same 2^-30 budget per window. - * 1 + CRITBINOM(W, 2^-H, 1-alpha/256), H = 1, alpha = 2^-30, W = 1..512. */ -static const word16 aptAllCutoffTable[512] = { - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, - 38, 38, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, - 47, 48, 49, 50, 50, 51, 52, 53, 53, 54, 55, 56, - 56, 57, 58, 59, 59, 60, 61, 62, 62, 63, 64, 64, - 65, 66, 67, 67, 68, 69, 69, 70, 71, 71, 72, 73, - 74, 74, 75, 76, 76, 77, 78, 78, 79, 80, 80, 81, - 82, 83, 83, 84, 85, 85, 86, 87, 87, 88, 89, 89, - 90, 91, 91, 92, 93, 93, 94, 95, 95, 96, 97, 97, - 98, 99, 99, 100, 101, 101, 102, 103, 103, 104, 105, 105, - 106, 106, 107, 108, 108, 109, 110, 110, 111, 112, 112, 113, - 114, 114, 115, 116, 116, 117, 117, 118, 119, 119, 120, 121, - 121, 122, 123, 123, 124, 125, 125, 126, 126, 127, 128, 128, - 129, 130, 130, 131, 132, 132, 133, 133, 134, 135, 135, 136, - 137, 137, 138, 138, 139, 140, 140, 141, 142, 142, 143, 143, - 144, 145, 145, 146, 147, 147, 148, 148, 149, 150, 150, 151, - 152, 152, 153, 153, 154, 155, 155, 156, 157, 157, 158, 158, - 159, 160, 160, 161, 162, 162, 163, 163, 164, 165, 165, 166, - 166, 167, 168, 168, 169, 170, 170, 171, 171, 172, 173, 173, - 174, 174, 175, 176, 176, 177, 177, 178, 179, 179, 180, 181, - 181, 182, 182, 183, 184, 184, 185, 185, 186, 187, 187, 188, - 188, 189, 190, 190, 191, 191, 192, 193, 193, 194, 195, 195, - 196, 196, 197, 198, 198, 199, 199, 200, 201, 201, 202, 202, - 203, 204, 204, 205, 205, 206, 207, 207, 208, 208, 209, 210, - 210, 211, 211, 212, 213, 213, 214, 214, 215, 216, 216, 217, - 217, 218, 219, 219, 220, 220, 221, 222, 222, 223, 223, 224, - 225, 225, 226, 226, 227, 228, 228, 229, 229, 230, 230, 231, - 232, 232, 233, 233, 234, 235, 235, 236, 236, 237, 238, 238, - 239, 239, 240, 241, 241, 242, 242, 243, 244, 244, 245, 245, - 246, 246, 247, 248, 248, 249, 249, 250, 251, 251, 252, 252, - 253, 254, 254, 255, 255, 256, 257, 257, 258, 258, 259, 259, - 260, 261, 261, 262, 262, 263, 264, 264, 265, 265, 266, 267, - 267, 268, 268, 269, 269, 270, 271, 271, 272, 272, 273, 274, - 274, 275, 275, 276, 276, 277, 278, 278, 279, 279, 280, 281, - 281, 282, 282, 283, 283, 284, 285, 285, 286, 286, 287, 288, - 288, 289, 289, 290, 290, 291, 292, 292, 293, 293, 294, 295, - 295, 296, 296, 297, 297, 298, 299, 299, 300, 300, 301, 302, - 302, 303, 303, 304, 304, 305, 306, 306, 307, 307, 308, 309, - 309, 310, 310, 311, 311, 312, 313, 313, 314, 314, 315, 315, - 316, 317, 317, 318, 318, 319, 320, 320, 321, 321, 322, 322, - 323, 324, 324, 325, 325, 326, 326, 327, 328, 328, 329, 329, - 330, 330, 331, 332, 332, 333, 333, 334 + * whole alphabet keeps the same 2^-30 budget per window. Same W / 2 delta + * form as the table above. + * 1 + CRITBINOM(W, 2^-H, 1 - alpha/256), H = 1, alpha = 2^-30, W = 1..512. */ +static const byte aptAllCutoffDelta[512] = { + 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, + 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, + 18, 18, 19, 19, 20, 19, 20, 20, 21, 21, 22, 21, 22, 22, 23, 23, + 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, + 27, 27, 28, 28, 28, 28, 29, 28, 29, 29, 30, 29, 30, 30, 30, 30, + 31, 30, 31, 31, 32, 31, 32, 32, 32, 32, 33, 32, 33, 33, 33, 33, + 34, 34, 34, 34, 35, 34, 35, 35, 35, 35, 36, 35, 36, 36, 36, 36, + 37, 36, 37, 37, 37, 37, 38, 37, 38, 38, 38, 38, 39, 38, 39, 39, + 39, 39, 40, 39, 40, 39, 40, 40, 40, 40, 41, 40, 41, 41, 41, 41, + 42, 41, 42, 42, 42, 42, 42, 42, 43, 42, 43, 43, 43, 43, 44, 43, + 44, 44, 44, 44, 44, 44, 45, 44, 45, 45, 45, 45, 46, 45, 46, 45, + 46, 46, 46, 46, 47, 46, 47, 46, 47, 47, 47, 47, 48, 47, 48, 47, + 48, 48, 48, 48, 49, 48, 49, 48, 49, 49, 49, 49, 50, 49, 50, 49, + 50, 50, 50, 50, 51, 50, 51, 50, 51, 51, 51, 51, 52, 51, 52, 51, + 52, 52, 52, 52, 52, 52, 53, 52, 53, 53, 53, 53, 53, 53, 54, 53, + 54, 53, 54, 54, 54, 54, 54, 54, 55, 54, 55, 55, 55, 55, 55, 55, + 56, 55, 56, 55, 56, 56, 56, 56, 56, 56, 57, 56, 57, 56, 57, 57, + 57, 57, 58, 57, 58, 57, 58, 58, 58, 58, 58, 58, 59, 58, 59, 58, + 59, 59, 59, 59, 59, 59, 60, 59, 60, 59, 60, 60, 60, 60, 60, 60, + 61, 60, 61, 60, 61, 61, 61, 61, 61, 61, 62, 61, 62, 61, 62, 62, + 62, 62, 62, 62, 63, 62, 63, 62, 63, 63, 63, 63, 63, 63, 63, 63, + 64, 63, 64, 63, 64, 64, 64, 64, 64, 64, 65, 64, 65, 64, 65, 65, + 65, 65, 65, 65, 66, 65, 66, 65, 66, 65, 66, 66, 66, 66, 66, 66, + 67, 66, 67, 66, 67, 67, 67, 67, 67, 67, 68, 67, 68, 67, 68, 67, + 68, 68, 68, 68, 68, 68, 69, 68, 69, 68, 69, 69, 69, 69, 69, 69, + 69, 69, 70, 69, 70, 69, 70, 70, 70, 70, 70, 70, 70, 70, 71, 70, + 71, 70, 71, 71, 71, 71, 71, 71, 71, 71, 72, 71, 72, 71, 72, 72, + 72, 72, 72, 72, 72, 72, 73, 72, 73, 72, 73, 73, 73, 73, 73, 73, + 73, 73, 74, 73, 74, 73, 74, 74, 74, 74, 74, 74, 74, 74, 75, 74, + 75, 74, 75, 75, 75, 75, 75, 75, 75, 75, 76, 75, 76, 75, 76, 75, + 76, 76, 76, 76, 76, 76, 77, 76, 77, 76, 77, 76, 77, 77, 77, 77, + 77, 77, 77, 77, 78, 77, 78, 77, 78, 77, 78, 78, 78, 78, 78, 78 }; #define WC_RNG_SEED_APT_ALL_CUTOFF_FOR(w) \ - ((word32)aptAllCutoffTable[(w) - 1]) + (((word32)(w) / 2) + (word32)aptAllCutoffDelta[(w) - 1]) #else #define WC_RNG_SEED_APT_CUTOFF_FOR(w) ((word32)WC_RNG_SEED_APT_CUTOFF) #endif From 95ba963aef80a7e69cc6d7404877584c90473b8b Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sun, 20 Sep 2026 14:32:54 -0600 Subject: [PATCH 08/15] random: preserve 90B verdicts at instantiate and drop stale returns --- doc/dox_comments/header_files/random.h | 6 ++-- linuxkm/lkcapi_sha_glue.c | 6 +--- wolfcrypt/src/random.c | 48 +++++++++++++++----------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/doc/dox_comments/header_files/random.h b/doc/dox_comments/header_files/random.h index 069775efac4..5c5d540459e 100644 --- a/doc/dox_comments/header_files/random.h +++ b/doc/dox_comments/header_files/random.h @@ -1306,7 +1306,8 @@ int wc_RNG_DRBG_GetNextSeedRBGCStratum(const WC_RNG* rng); \return 0 Bytes were banked (bank may or may not yet be complete). \return ALREADY_E The bank is ready or being consumed. - \return NOT_READY_E The bank could not be completed; simply retry. + \return ENTROPY_RT_E or ENTROPY_APT_E The SP 800-90B seed health test + rejected the banked material, which is burned. \return BAD_FUNC_ARG rng is null or n is 0. \return MISSING_RNG_E rng has no DRBG (RDRAND et al.). @@ -1337,7 +1338,8 @@ int wc_RNG_DRBG_NextSeedGenerate(WC_RNG* rng, word32 n); \return 0 Bytes were banked. \return ALREADY_E The bank is ready or being consumed. - \return NOT_READY_E The bank could not be completed; simply retry. + \return ENTROPY_RT_E or ENTROPY_APT_E The SP 800-90B seed health test + rejected the banked material, which is burned. \return BAD_FUNC_ARG rng or root is null, or n is 0. \return MISSING_RNG_E rng has no DRBG (RDRAND et al.). \return SEQ_OVERFLOW_E root's stratum is at the representable maximum. diff --git a/linuxkm/lkcapi_sha_glue.c b/linuxkm/lkcapi_sha_glue.c index 5207e07a8dc..f0e43238238 100644 --- a/linuxkm/lkcapi_sha_glue.c +++ b/linuxkm/lkcapi_sha_glue.c @@ -2747,10 +2747,6 @@ static void wc_linuxkm_vmgenid_poll_teardown( * draining nextSeeds as fast as it can. Per-turn classification of * wc_rng_bank_next_seed_generate() returns: * 0 gathered/published -- progress; - * NOT_READY_E transient (incl. a burned bank, which is refill-eligible - * now, and an environmental TestSeed miss with the aperture - * preserved) -- progress, so a forced burn can never induce - * a nap; * ALREADY_E ready or consuming -- no work on this instance; * BUSY_E instance-op gate held by a reinit -- no progress here, * but the gate holder is making it; @@ -3219,7 +3215,7 @@ static int wc_linuxkm_entropy_daemon(void *arg) ret = wc_rng_bank_next_seed_generate( bank, i, WC_LINUXKM_ENTROPY_DAEMON_GRANULE); - if ((ret == 0) || (ret == WC_NO_ERR_TRACE(NOT_READY_E))) { + if (ret == 0) { progress = 1; } else if ((ret == WC_NO_ERR_TRACE(ALREADY_E)) || diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index e07c5f76278..0d957514d3a 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2625,16 +2625,20 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) #ifndef WC_RNG_SEED_APT_WINDOW #define WC_RNG_SEED_APT_WINDOW 512 #endif -#if (WC_RNG_SEED_APT_WINDOW > 512) || (WC_RNG_SEED_APT_WINDOW < 1) - #error WC_RNG_SEED_APT_WINDOW must be 1 to 512 +#if WC_RNG_SEED_APT_WINDOW < 1 + #error WC_RNG_SEED_APT_WINDOW must be at least 1 #endif #ifndef WC_RNG_SEED_APT_CUTOFF - /* No caller-supplied cutoff: take one per window size from the table. - * A caller-supplied cutoff also turns off the all-values test below. */ + /* No caller-supplied cutoff: take one per window size from the table. */ #define WC_RNG_SEED_APT_CUTOFF_PER_WINDOW /* The published W = 512 value, kept for readers and for tests. */ #define WC_RNG_SEED_APT_CUTOFF 325 #endif +/* Only the tables cap the window; a caller-supplied cutoff has no such + * limit, so this bound belongs to the table build alone. */ +#if defined(WC_RNG_SEED_APT_CUTOFF_PER_WINDOW) && (WC_RNG_SEED_APT_WINDOW > 512) + #error WC_RNG_SEED_APT_WINDOW must be 1 to 512 unless WC_RNG_SEED_APT_CUTOFF is set +#endif #ifdef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW /* C for every window size, not only 512: a seed shorter than the window is one @@ -2719,7 +2723,10 @@ static const byte aptAllCutoffDelta[512] = { #define WC_RNG_SEED_APT_ALL_CUTOFF_FOR(w) \ (((word32)(w) / 2) + (word32)aptAllCutoffDelta[(w) - 1]) #else + /* A caller-supplied cutoff is a threshold, so it serves both tests; the + * all-values scan still runs only where its majority precondition holds. */ #define WC_RNG_SEED_APT_CUTOFF_FOR(w) ((word32)WC_RNG_SEED_APT_CUTOFF) + #define WC_RNG_SEED_APT_ALL_CUTOFF_FOR(w) ((word32)WC_RNG_SEED_APT_CUTOFF) #endif int wc_RNG_TestSeed(const byte* seed, word32 seedSz) @@ -2791,7 +2798,6 @@ int wc_RNG_TestSeed(const byte* seed, word32 seedSz) } } -#ifdef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW /* Additional developer-defined test (SP800-90B 4.3 Req 1c): 4.4.2 watches * only the window's first byte, this watches every value. Its cutoff uses * alpha/256 for the alphabet and always lands above half the window. */ @@ -2829,7 +2835,6 @@ int wc_RNG_TestSeed(const byte* seed, word32 seedSz) } } } -#endif /* Set return code based on accumulated failure flags */ if (rctFailed) { @@ -2955,6 +2960,15 @@ int wc_Sha512Drbg_IsDisabled(void) * _LOCK_INITIALLY (born held at both layers). wc_FreeRng() releases (if the * latch is held) and frees the mutex. */ +static int ReseedSourceFailure(int ret) +{ + if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || + (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { + return ret; + } + return DRBG_FAILURE; +} + static WARN_UNUSED_RESULT int _InitRng(WC_RNG* rng, const byte* nonce, word32 nonceSz, const byte *perso, word32 persoSz, @@ -3345,7 +3359,8 @@ static WARN_UNUSED_RESULT int _InitRng(WC_RNG* rng, "ERROR: seedCb in _InitRng() failed with err = %d", ret); #endif - ret = DRBG_FAILURE; + /* mapped once, by the shared arm every seed path falls + * into below */ } } #else @@ -3366,7 +3381,9 @@ static WARN_UNUSED_RESULT int _InitRng(WC_RNG* rng, "ERROR: seed acquisition in _InitRng() failed with err %d", ret); #endif - ret = DRBG_FAILURE; + /* A verdict from the source survives here as it does on the + * reseed path, so instantiate and reseed classify it alike. */ + ret = ReseedSourceFailure(ret); rng->status = DRBG_FAILED; } @@ -4882,15 +4899,6 @@ int wc_RNG_DRBG_StirRBGC(WC_RNG* rng, WC_RNG* root, /* A failed seed source reports DRBG_FAILURE, except an SP 800-90B RCT or APT * verdict, which keeps its own code so the caller can tell the two apart. */ -static int ReseedSourceFailure(int ret) -{ - if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || - (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { - return ret; - } - return DRBG_FAILURE; -} - static WARN_UNUSED_RESULT int PollAndReSeed(WC_RNG* rng, const byte* additional, word32 additionalSz) { @@ -5071,9 +5079,9 @@ int wc_RNG_DRBG_Reseed_Now(WC_RNG* rng, const byte* nonce, word32 nonceSz) /* Banked-next-seed services. _NextSeedGenerate() banks up to n more * bytes from the module's seed source (clamped to the space remaining; * ALREADY_E when the bank is ready or being consumed), health-testing - * and publishing the bank when it completes (NOT_READY_E when the health - * test could not run and the call should simply be retried); a - * scheduling daemon may call it without owning the instance. + * and publishing the bank when it completes, burning it on a failed + * health test (ENTROPY_RT_E / ENTROPY_APT_E); a scheduling daemon may + * call it without owning the instance. * _NextSeedCurrent() reports the raw aperture value (racy snapshot). * _NextSeedNow() claims a ready bank and performs a source-free * credited reseed with it -- safe in atomic context -- or returns From b517d91771b20b68be8225875881286b4d0e6517 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sun, 20 Sep 2026 14:38:32 -0600 Subject: [PATCH 09/15] tests/api: sweep every window the seed APT cutoff tables cover --- tests/api/test_random.c | 115 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/api/test_random.c b/tests/api/test_random.c index 0a1f1977dfd..bd21dd9b4a3 100644 --- a/tests/api/test_random.c +++ b/tests/api/test_random.c @@ -443,6 +443,62 @@ static int test_random_apt_window(byte* s, word32 start, word32 win, byte ref, } return (placed == m) ? 0 : -1; } + +/* Build one window whose majority value is NOT its reference sample, so the + * 4.4.2 test stays quiet and only the all-values test can fire. */ +static int test_random_allvals_window(byte* s, word32 win, byte ref, byte val, + word32 m) +{ + word32 i, placed = 0, run = 0; + byte filler = 0; + + for (i = 0; i < win; i++) { + if (++filler == val) + filler++; + s[i] = filler; + } + s[0] = ref; + for (i = 1; (i < win) && (placed < m); i++) { + if (run >= 29) { + run = 0; + continue; + } + s[i] = val; + placed++; + run++; + } + return (placed == m) ? 0 : -1; +} + +/* Smallest match count that trips the APT at this window, discovered through + * the public API; 0 when the window cannot reach its cutoff. */ +static word32 test_random_apt_cutoff(byte* buf, word32 win, int allValues) +{ + word32 lo = 2, hi = win, best = 0; + + while (lo <= hi) { + word32 mid = lo + ((hi - lo) / 2); + int built; + + if (allValues) + built = test_random_allvals_window(buf, win, 0x01, 0x99, mid); + else + built = test_random_apt_window(buf, 0, win, 0x11, mid - 1); + + if ((built != 0) || + (wc_RNG_TestSeed(buf, win) != WC_NO_ERR_TRACE(ENTROPY_APT_E))) { + if (built != 0) + hi = mid - 1; + else + lo = mid + 1; + } + else { + best = mid; + hi = mid - 1; + } + } + return best; +} #endif int test_wc_RNG_TestSeed(void) @@ -561,6 +617,65 @@ int test_wc_RNG_TestSeed(void) * and a short all-same seed is left to the RCT */ XMEMSET(buf, 0x5a, 20); ExpectIntEQ(wc_RNG_TestSeed(buf, 20), 0); + + /* Sweep the windows a seed can actually present. seedSz is at + * least SEED_BLOCK_SZ, and no cutoff is reachable below W = 30 + * (38 for the all-values table), so smaller windows are visited + * but only the reachable ones are asserted. That still covers far + * more of the 1024 constants than the few sizes one build uses. */ + { + word32 w; + word32 prev = 0, prevAll = 0; + word32 firstFire = 0, firstFireAll = 0; + int bad = 0, badAll = 0; + + for (w = 4; w <= 512; w++) { + word32 c = test_random_apt_cutoff(buf, w, 0); + word32 a2 = test_random_apt_cutoff(buf, w, 1); + + if (c != 0) { + if (firstFire == 0) + firstFire = w; + /* reachable, and a strict majority of the window */ + if ((c > w) || (c <= (w / 2))) + bad = (bad != 0) ? bad : (int)w; + /* C(W) rises by 0 or 1 as the window grows */ + if ((prev != 0) && ((c < prev) || ((c - prev) > 1))) + bad = (bad != 0) ? bad : (int)w; + prev = c; + } + if (a2 != 0) { + if (firstFireAll == 0) + firstFireAll = w; + if ((a2 > w) || (a2 <= (w / 2))) + badAll = (badAll != 0) ? badAll : (int)w; + if ((prevAll != 0) && + ((a2 < prevAll) || ((a2 - prevAll) > 1))) + badAll = (badAll != 0) ? badAll : (int)w; + /* alpha/256 is stricter, so it never sits lower */ + if ((c != 0) && (a2 < c)) + badAll = (badAll != 0) ? badAll : (int)w; + prevAll = a2; + } + } + /* the failing window, not just "something broke" */ + ExpectIntEQ(bad, 0); + ExpectIntEQ(badAll, 0); + /* Nothing fires below the window where alpha 2^-30 first + * makes a cutoff reachable. This is one-sided on purpose: + * the densest window these helpers can build still keeps + * runs under the RCT cutoff, so the smallest window they can + * actually trip sits above the table's own floor. */ + ExpectIntGE((int)firstFire, 30); + ExpectIntGE((int)firstFireAll, 38); + /* and the anchors the derivation is quoted against */ + ExpectIntEQ((int)test_random_apt_cutoff(buf, 132, 0), 101); + ExpectIntEQ((int)test_random_apt_cutoff(buf, 196, 0), 140); + ExpectIntEQ((int)test_random_apt_cutoff(buf, 512, 0), 325); + ExpectIntEQ((int)test_random_apt_cutoff(buf, 132, 1), 105); + ExpectIntEQ((int)test_random_apt_cutoff(buf, 196, 1), 146); + ExpectIntEQ((int)test_random_apt_cutoff(buf, 512, 1), 334); + } } XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } From 04b8bc4a584702f051177ccb84535ac854166207 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sun, 20 Sep 2026 19:36:20 -0600 Subject: [PATCH 10/15] random: build the seed verdict helper only where DRBG_FAILURE exists --- wolfcrypt/src/random.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 0d957514d3a..4d944fdc0d6 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2942,6 +2942,19 @@ int wc_Sha512Drbg_IsDisabled(void) #endif /* HAVE_HASHDRBG */ /* End NIST DRBG Code */ +/* Same condition as both callers: DRBG_FAILURE is a Hash_DRBG internal and + * does not exist without it. */ +#if defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) +static int ReseedSourceFailure(int ret) +{ + if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || + (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { + return ret; + } + return DRBG_FAILURE; +} +#endif + /* Semantics of "flags": * * Security attributes are fixed at instantiation and caller-declared -- the @@ -2960,15 +2973,6 @@ int wc_Sha512Drbg_IsDisabled(void) * _LOCK_INITIALLY (born held at both layers). wc_FreeRng() releases (if the * latch is held) and frees the mutex. */ -static int ReseedSourceFailure(int ret) -{ - if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || - (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E))) { - return ret; - } - return DRBG_FAILURE; -} - static WARN_UNUSED_RESULT int _InitRng(WC_RNG* rng, const byte* nonce, word32 nonceSz, const byte *perso, word32 persoSz, From 5c41bfc17020baf6b11fb9537d7f5a138db2734b Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Sun, 20 Sep 2026 20:55:12 -0600 Subject: [PATCH 11/15] random: guard caller-set seed cutoffs and correct the APT comments --- wolfcrypt/src/random.c | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 4d944fdc0d6..279b89bc751 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2612,6 +2612,10 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) #ifndef WC_RNG_SEED_RCT_CUTOFF #define WC_RNG_SEED_RCT_CUTOFF 31 #endif +/* The run starts at 1, so a cutoff below 2 fails every seed. */ +#if WC_RNG_SEED_RCT_CUTOFF < 2 + #error WC_RNG_SEED_RCT_CUTOFF must be at least 2 +#endif /* SP800-90B 4.4.2 - Adaptive Proportion Test * Monitors if a particular sample value appears too frequently within a @@ -2639,6 +2643,18 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) #if defined(WC_RNG_SEED_APT_CUTOFF_PER_WINDOW) && (WC_RNG_SEED_APT_WINDOW > 512) #error WC_RNG_SEED_APT_WINDOW must be 1 to 512 unless WC_RNG_SEED_APT_CUTOFF is set #endif +/* A caller-supplied cutoff is compared against the window unchanged, so one + * above it can never be reached and leaves the seed on the RCT alone. The + * window is min(seedSz, WC_RNG_SEED_APT_WINDOW), so this catches only the + * statically visible case; a cutoff above the seed size still goes unseen. */ +#ifndef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW + #if WC_RNG_SEED_APT_CUTOFF < 2 + #error WC_RNG_SEED_APT_CUTOFF must be at least 2 + #endif + #if WC_RNG_SEED_APT_CUTOFF > WC_RNG_SEED_APT_WINDOW + #error WC_RNG_SEED_APT_CUTOFF exceeds the window and can never fire + #endif +#endif #ifdef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW /* C for every window size, not only 512: a seed shorter than the window is one @@ -2765,10 +2781,13 @@ int wc_RNG_TestSeed(const byte* seed, word32 seedSz) } } - /* SP800-90B 4.4.2 Adaptive Proportion Test: the first byte of each - * non-overlapping window is the reference value, and a window fails when - * matches reach the cutoff for that window size. Bias toward any other - * value is caught by the all-values test below. */ + /* SP800-90B 4.4.2 Adaptive Proportion Test: the first byte of each window + * is the reference value, and a window fails when matches reach the cutoff + * for that window size. Windows do not overlap except the last, which + * slides back to full length and so re-judges the bytes it covers; that + * only adds windows, so it can raise the false alarm rate but never mask a + * failure. A frequent value that is not the reference is left to the + * all-values test below. */ { word32 start; word32 window = min(seedSz, (word32)WC_RNG_SEED_APT_WINDOW); @@ -2799,8 +2818,10 @@ int wc_RNG_TestSeed(const byte* seed, word32 seedSz) } /* Additional developer-defined test (SP800-90B 4.3 Req 1c): 4.4.2 watches - * only the window's first byte, this watches every value. Its cutoff uses - * alpha/256 for the alphabet and always lands above half the window. */ + * only the window's first byte, this watches whichever value is the + * window's majority, wherever it falls. Its cutoff uses alpha/256 for the + * alphabet, so it catches a near-majority (334 of 512) and not bias in + * general, and it needs a 38 byte window before it can fire at all. */ { word32 start; word32 window = min(seedSz, (word32)WC_RNG_SEED_APT_WINDOW); From 4d46a11e23f6cb861ff1ac9a431434986a52a16f Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 24 Sep 2026 11:14:13 -0600 Subject: [PATCH 12/15] random: condemn on a seed verdict and bound a caller-set APT cutoff --- doc/dox_comments/header_files/random.h | 4 ++++ wolfcrypt/src/random.c | 23 +++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/doc/dox_comments/header_files/random.h b/doc/dox_comments/header_files/random.h index 5c5d540459e..97cfe5a2132 100644 --- a/doc/dox_comments/header_files/random.h +++ b/doc/dox_comments/header_files/random.h @@ -1393,6 +1393,8 @@ int wc_RNG_DRBG_NextSeedCurrent(WC_RNG* rng, WC_ATOMIC_INT_ARG* n); \return NOT_READY_E No bank is ready. \return BAD_FUNC_ARG rng is null. \return MISSING_RNG_E rng has no DRBG (RDRAND et al.). + \return ENTROPY_RT_E or ENTROPY_APT_E A seed health test failed on + the reseed this performs; the instance is condemned. \param rng The RNG object to reseed. @@ -1422,6 +1424,8 @@ int wc_RNG_DRBG_NextSeedNow(WC_RNG* rng); \return NOT_READY_E No bank is ready. \return BAD_FUNC_ARG rng is null, or nonce is null with nonceSz nonzero. \return MISSING_RNG_E rng has no DRBG (RDRAND et al.). + \return ENTROPY_RT_E or ENTROPY_APT_E A seed health test failed on + the reseed this performs; the instance is condemned. \return DRBG_CONT_FIPS_E The continuous test failed; the DRBG is out of service. \return RNG_FAILURE_E The DRBG is out of service. diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 279b89bc751..6438d3fd383 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2648,11 +2648,19 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) * window is min(seedSz, WC_RNG_SEED_APT_WINDOW), so this catches only the * statically visible case; a cutoff above the seed size still goes unseen. */ #ifndef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW + /* The smallest window the module ever judges is its own reseed seed, and + * that size is a constant here, so bound the cutoff by it rather than by + * the 512 cap: a value between the two compiles but can never fire. */ + #if (SEED_SZ + SEED_BLOCK_SZ) < WC_RNG_SEED_APT_WINDOW + #define WC_RNG_SEED_APT_MIN_WINDOW (SEED_SZ + SEED_BLOCK_SZ) + #else + #define WC_RNG_SEED_APT_MIN_WINDOW WC_RNG_SEED_APT_WINDOW + #endif #if WC_RNG_SEED_APT_CUTOFF < 2 #error WC_RNG_SEED_APT_CUTOFF must be at least 2 #endif - #if WC_RNG_SEED_APT_CUTOFF > WC_RNG_SEED_APT_WINDOW - #error WC_RNG_SEED_APT_CUTOFF exceeds the window and can never fire + #if WC_RNG_SEED_APT_CUTOFF > WC_RNG_SEED_APT_MIN_WINDOW + #error WC_RNG_SEED_APT_CUTOFF exceeds the seed size and can never fire #endif #endif @@ -3700,7 +3708,7 @@ int wc_InitRngNonce_ex2(WC_RNG* rng, const byte* nonce, word32 nonceSz, #if defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) /* Map a failed generate or reseed to the return code and rng->status. * A failed SP 800-90A health test returns DRBG_CONT_FIPS_E. */ -static int RngGenerateFailure(WC_RNG* rng, int ret) +static WC_MAYBE_UNUSED int RngGenerateFailure(WC_RNG* rng, int ret) { if (ret == WC_NO_ERR_TRACE(DRBG_CONT_FAILURE)) { rng->status = DRBG_CONT_FAILED; @@ -5080,9 +5088,12 @@ int wc_RNG_DRBG_Reseed_Now(WC_RNG* rng, const byte* nonce, word32 nonceSz) else { wc_drbg_reseed_ctr_t ctr = WC_RESEED_INTERVAL; (void)wc_RNG_DRBG_GetReseedCtr(rng, &ctr); - if (ctr >= WC_RESEED_INTERVAL) { - /* The instance is out of generate runway -- condemn now, matching - * wc_RNG_GenerateBlock()'s behavior for mandatory reseeds. */ + /* A seed verdict says the module's own source is bad, not that it is + * briefly busy, so it condemns whatever runway is left, as + * wc_RNG_GenerateBlock() and rng_pid_change_check() already do. */ + if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || + (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E)) || + (ctr >= WC_RESEED_INTERVAL)) { rng->status = DRBG_FAILED; } if (ret > 0) { From f03294aa35065e13e8921df5b929b317879fc5d3 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Fri, 25 Sep 2026 13:41:41 -0600 Subject: [PATCH 13/15] linuxkm: reinstantiate up to three times before failing a generate --- linuxkm/lkcapi_sha_glue.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/linuxkm/lkcapi_sha_glue.c b/linuxkm/lkcapi_sha_glue.c index f0e43238238..f06fc70db80 100644 --- a/linuxkm/lkcapi_sha_glue.c +++ b/linuxkm/lkcapi_sha_glue.c @@ -3725,6 +3725,12 @@ WC_MAYBE_UNUSED static int linuxkm_InitRng_DefaultRef(WC_RNG* rng) { #define WC_LINUXKM_DRBG_SMALL_LIMIT 8 #endif +/* Reinstantiations attempted when a generate keeps failing. Each one gathers + * a fresh seed and health tests it, so this never re-judges rejected data. */ +#ifndef WC_LINUXKM_DRBG_REINIT_TRIES + #define WC_LINUXKM_DRBG_REINIT_TRIES 3 +#endif + #ifdef WC_RNG_HAVE_POOL wc_static_assert(WC_LINUXKM_DRBG_SMALL_LIMIT <= WC_LINUXKM_RNG_POOL_SIZE); #endif @@ -3961,9 +3967,9 @@ static int wc_linuxkm_drbg_generate(struct wc_rng_bank *ctx, if (slen > 0) break; - if (retried) + if (retried >= WC_LINUXKM_DRBG_REINIT_TRIES) break; - retried = 1; + ++retried; if (! can_wait) break; From c997468781828e078aa425ee77ebd15cdc981984 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Wed, 30 Sep 2026 13:09:51 -0600 Subject: [PATCH 14/15] random: drop an unsound cutoff bound and stop masking source errors --- doc/dox_comments/header_files/random.h | 2 ++ doc/dox_comments/header_files/rng_bank.h | 6 ++++-- linuxkm/lkcapi_sha_glue.c | 4 ++++ wolfcrypt/src/random.c | 21 ++++++--------------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/doc/dox_comments/header_files/random.h b/doc/dox_comments/header_files/random.h index 97cfe5a2132..5761b95e6bb 100644 --- a/doc/dox_comments/header_files/random.h +++ b/doc/dox_comments/header_files/random.h @@ -1012,6 +1012,8 @@ int wc_RNG_DRBG_ScheduleReseed(WC_RNG* rng); \return DRBG_CONT_FIPS_E The continuous test failed; the DRBG is out of service. \return RNG_FAILURE_E The DRBG is out of service or reseeding failed. + \return ENTROPY_RT_E or ENTROPY_APT_E The SP 800-90B seed health test + rejected the gathered seed; the instance is condemned. \param rng The RNG object to reseed. \param nonce Optional additional input. diff --git a/doc/dox_comments/header_files/rng_bank.h b/doc/dox_comments/header_files/rng_bank.h index 18f4f999bba..bc2f1311d27 100644 --- a/doc/dox_comments/header_files/rng_bank.h +++ b/doc/dox_comments/header_files/rng_bank.h @@ -402,7 +402,8 @@ int wc_rng_bank_get_inst_id(struct wc_rng_bank_inst *rng_inst); \return 0 Bytes were banked. \return ALREADY_E The instance's bank is ready or being consumed. - \return NOT_READY_E The health test could not run; simply retry. + \return ENTROPY_RT_E or ENTROPY_APT_E The SP 800-90B seed health test + rejected the banked material, which is burned. \return BAD_FUNC_ARG bank is null, inst_offset is out of range, or n is 0. @@ -437,7 +438,8 @@ int wc_rng_bank_next_seed_generate(struct wc_rng_bank *bank, int inst_offset, \return 0 Bytes were banked. \return ALREADY_E The instance's bank is ready or being consumed. - \return NOT_READY_E The health test could not run; simply retry. + \return ENTROPY_RT_E or ENTROPY_APT_E The SP 800-90B seed health test + rejected the banked material, which is burned. \return BAD_FUNC_ARG bank or root is null, inst_offset is out of range, or n is 0. diff --git a/linuxkm/lkcapi_sha_glue.c b/linuxkm/lkcapi_sha_glue.c index f06fc70db80..4acead3bf00 100644 --- a/linuxkm/lkcapi_sha_glue.c +++ b/linuxkm/lkcapi_sha_glue.c @@ -3730,6 +3730,10 @@ WC_MAYBE_UNUSED static int linuxkm_InitRng_DefaultRef(WC_RNG* rng) { #ifndef WC_LINUXKM_DRBG_REINIT_TRIES #define WC_LINUXKM_DRBG_REINIT_TRIES 3 #endif +/* Zero would silently turn off recovery rather than bounding it. */ +#if WC_LINUXKM_DRBG_REINIT_TRIES < 1 + #error WC_LINUXKM_DRBG_REINIT_TRIES must be at least 1 +#endif #ifdef WC_RNG_HAVE_POOL wc_static_assert(WC_LINUXKM_DRBG_SMALL_LIMIT <= WC_LINUXKM_RNG_POOL_SIZE); diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 6438d3fd383..af1f2f8fee0 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -2643,24 +2643,16 @@ int wc_RNG_DRBG_Stir(WC_RNG* rng, const byte* seed, word32 seedSz) #if defined(WC_RNG_SEED_APT_CUTOFF_PER_WINDOW) && (WC_RNG_SEED_APT_WINDOW > 512) #error WC_RNG_SEED_APT_WINDOW must be 1 to 512 unless WC_RNG_SEED_APT_CUTOFF is set #endif -/* A caller-supplied cutoff is compared against the window unchanged, so one - * above it can never be reached and leaves the seed on the RCT alone. The - * window is min(seedSz, WC_RNG_SEED_APT_WINDOW), so this catches only the - * statically visible case; a cutoff above the seed size still goes unseen. */ +/* A caller-supplied cutoff above the window can never be reached at any seed + * size, so it is refused here. A cutoff the window allows but a given seed + * does not is not a build-time property: the window is min(seedSz, WINDOW) and + * seedSz is the caller's, so that case is left to the runtime skip below. */ #ifndef WC_RNG_SEED_APT_CUTOFF_PER_WINDOW - /* The smallest window the module ever judges is its own reseed seed, and - * that size is a constant here, so bound the cutoff by it rather than by - * the 512 cap: a value between the two compiles but can never fire. */ - #if (SEED_SZ + SEED_BLOCK_SZ) < WC_RNG_SEED_APT_WINDOW - #define WC_RNG_SEED_APT_MIN_WINDOW (SEED_SZ + SEED_BLOCK_SZ) - #else - #define WC_RNG_SEED_APT_MIN_WINDOW WC_RNG_SEED_APT_WINDOW - #endif #if WC_RNG_SEED_APT_CUTOFF < 2 #error WC_RNG_SEED_APT_CUTOFF must be at least 2 #endif - #if WC_RNG_SEED_APT_CUTOFF > WC_RNG_SEED_APT_MIN_WINDOW - #error WC_RNG_SEED_APT_CUTOFF exceeds the seed size and can never fire + #if WC_RNG_SEED_APT_CUTOFF > WC_RNG_SEED_APT_WINDOW + #error WC_RNG_SEED_APT_CUTOFF exceeds the window and can never fire #endif #endif @@ -4981,7 +4973,6 @@ static WARN_UNUSED_RESULT int PollAndReSeed(WC_RNG* rng, const byte* additional, "ERROR: wc_GenerateSeed() in PollAndReSeed() failed with " "err %d", ret); #endif - ret = ReseedSourceFailure(ret); } #endif } From e65f05e0afe3d0838d8307187e1bea739ed053c2 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Fri, 2 Oct 2026 11:04:36 -0600 Subject: [PATCH 15/15] random: a rejected seed fails the reseed, not the instantiation --- doc/dox_comments/header_files/random.h | 3 ++- wolfcrypt/src/random.c | 11 +++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/dox_comments/header_files/random.h b/doc/dox_comments/header_files/random.h index 5761b95e6bb..4d40c1c001f 100644 --- a/doc/dox_comments/header_files/random.h +++ b/doc/dox_comments/header_files/random.h @@ -1013,7 +1013,8 @@ int wc_RNG_DRBG_ScheduleReseed(WC_RNG* rng); service. \return RNG_FAILURE_E The DRBG is out of service or reseeding failed. \return ENTROPY_RT_E or ENTROPY_APT_E The SP 800-90B seed health test - rejected the gathered seed; the instance is condemned. + rejected the gathered seed; the reseed did not happen and the instance + remains usable until its reseed interval is reached. \param rng The RNG object to reseed. \param nonce Optional additional input. diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index af1f2f8fee0..c68394545f2 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -5079,12 +5079,11 @@ int wc_RNG_DRBG_Reseed_Now(WC_RNG* rng, const byte* nonce, word32 nonceSz) else { wc_drbg_reseed_ctr_t ctr = WC_RESEED_INTERVAL; (void)wc_RNG_DRBG_GetReseedCtr(rng, &ctr); - /* A seed verdict says the module's own source is bad, not that it is - * briefly busy, so it condemns whatever runway is left, as - * wc_RNG_GenerateBlock() and rng_pid_change_check() already do. */ - if ((ret == WC_NO_ERR_TRACE(ENTROPY_RT_E)) || - (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E)) || - (ctr >= WC_RESEED_INTERVAL)) { + /* A rejected seed never entered the state, so the instantiation is + * still valid and only loses the reseed (SP 800-90A 9.2 returns the + * status; 11.4.1 treats unavailable entropy as a normal-operation + * error). Out of runway is different: that one condemns. */ + if (ctr >= WC_RESEED_INTERVAL) { rng->status = DRBG_FAILED; } if (ret > 0) {