Skip to content

feat(rng): one press unlocks a bulk RNG health test on an uninitialized device - #338

Open
BitHighlander wants to merge 2 commits into
developfrom
feat/rng-audit-unlock-preinit
Open

feat(rng): one press unlocks a bulk RNG health test on an uninitialized device#338
BitHighlander wants to merge 2 commits into
developfrom
feat/rng-audit-unlock-preinit

Conversation

@BitHighlander

@BitHighlander BitHighlander commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Scope, up front: this enables RNG health testing, not entropy measurement. No output analysis can bound the entropy of an RNG's internal state — a strong PRNG seeded with 40 bits emits a stream that passes every test below by construction. See "What this does and does not prove".

Why

ENTROPY_FREE_BUDGET (64 KB/boot, added in 2f9269f64) is too small for the health test it exists to enable — because the positive control cannot run at that size.

A zero-collision result proves nothing on its own: a detector that never fires also returns zero. So the scan must also run at a width where collisions are expected and their count checked against theory. 32-bit collisions over N blocks expect N²/2³³:

sample expected 32-bit collisions positive control
64 KB 0.03 cannot run
1 MB 8 weak
8 MB 512 tight (±4.4%)

8 MB is the first size at which the result carries information. Under the old cap, reaching it cost 128 manual replugs.

The cap was also asymmetric in the wrong direction: it never stopped a patient remote attacker — host malware simply waits for the replugs that happen anyway and accumulates 64 KB at a time — while it fully priced out the honest auditor. The original comment concedes the failure mode ("so nobody ever checked").

What

Gate the bulk path on a single explicit press rather than a byte count, and only before initialization:

  • One confirm per boot unlocks unmetered draws. A remote host cannot forge a press — the property the byte cap was only approximating.
  • Uninitialized only. No seed exists, so there is no key material to correlate against. Keeping bulk export off initialized devices also denies a covert exfiltration channel: compromised firmware could otherwise encode a seed into bytes that still look random.

Initialized devices are unchanged: 64 KB, then a confirm on every draw.

Hardware validation

Flashed to an uninitialized device. 8.00 MB in 345 s (23.7 KB/s, 8192 calls, mean 42 ms/chunk), with exactly one stall — chunk 65, 17.4 s, the press — at exactly the 64 KB boundary.

 8-byte birthday  N=1048576  expected 2.98e-08  observed 0     PASS
 6-byte birthday  N=1398101  expected 0.00347   observed 0     PASS
 4-byte birthday  N=2097152  expected 512       observed 490   PASS  (z=-0.97, positive control)
 adjacent equal 32-bit words                    0              PASS
 duplicate 1 KB chunks                          0              PASS
 longest identical-byte run                     4              PASS
 byte chi-square   278.2 (df=255, 4s 165-345)                  PASS
 Shannon entropy   7.999976 bits/byte (empirical, of sample)   PASS
 monobit           z=0.665                                     PASS
 lag-1 autocorr    -0.000289 (bound 0.00138)                   PASS

sha256(sample) 32178de71a1083bb79d9c1fa891b081066632d7403bd99bfdfb8f6dae9cb484c

What this does and does not prove

Does: rules out stuck or badly biased output, repeated buffers, transport caching, gross correlation, and — via the 32-bit control — a collision detector that silently does nothing.

Does not: bound the entropy of the RNG's internal state. Specifically:

  • "Detects ≤39 bits" would be wrong. A 64-bit scan over N=2²⁰ expects one collision at a 39-bit support, but P(0 collisions) is then e⁻¹ = 37%. Zero collisions excludes only ≤37.4 bits at 95% confidence (≤36.8 at 99%). The table reads "1 expected collision at k bits" for this reason.
  • A low-entropy state behind a strong expander is invisible here, by construction.
  • 7.999976 bits/byte is empirical Shannon entropy of the sample, not cryptographic min-entropy. NIST SP 800-90B assesses the raw noise source and wants continuous RCT/APT health tests — separate future work, tracked below.

Follow-ups (not in this PR)

  • Continuous RCT/APT per SP 800-90B on unconditioned noise. The STM32 SEIS/CECS seed-and-clock error bits give partial coverage today (reset_rng() on fault).
  • Report device model, firmware build hash, tool version and sample digest alongside results.
  • Force a fresh reseed / power cycle between audit and ResetDevice so wallet generation never continues the audited stream.
  • Consider Trezor-style entropy-check at setup (commit/reveal over discarded rounds), which verifies the seed-generation path itself rather than a stream.

Cost

One static bool and two branches. Device build green across all variants.

ENTROPY_FREE_BUDGET (64 KB/boot) is too small for the audit it exists to
enable. A 64-bit birthday scan over N blocks detects an effective keyspace of
~2*log2(N)-1 bits; 64 KB gives N=8192, so ~25 bits. The failure this mechanism
was built in response to -- a build-config slip yielding ~40-bit seeds that
passed every standard statistical test -- needs N=2^20, i.e. 8 MB, for ~39
bits. Measured on rc26 hardware, 64 KB pulls clean in 2.3s and passes every
test we have (birthday, chi-square, Shannon, adjacent-word), which is exactly
what a ~40-bit RNG would also do. Reaching 8 MB under the byte cap costs 128
manual replugs.

The cap was also asymmetric in the wrong direction. It never stopped a patient
remote attacker -- host malware just waits for the replugs that happen anyway
and accumulates 64 KB at a time -- while it fully priced out the honest
auditor, who needs one contiguous run.

So gate the bulk path on a single explicit press rather than a byte count, and
only before initialization:

  - one confirm per boot unlocks unmetered draws. A remote host cannot forge a
    press, which is the property the byte cap was only approximating.
  - uninitialized only. No seed exists, so there is no key material to
    correlate against. The 32 bytes that do become a seed are drawn later, in
    reset.c, from noise that has not happened yet, and are SHA-256'd with
    host-supplied entropy before use.

Initialized devices are unchanged: 64 KB, then a confirm every draw. The unlock
re-locks the moment ResetDevice completes, because storage_isInitialized() is
re-read on every call -- no replug needed. A wiped device can be audited again,
which is intended: it holds no seed, and re-auditing before re-seeding is the
supported flow.

Cost is one static bool and two branches. Device build green, all variants.
Review caught two overclaims in the previous comment, both worth fixing in the
source rather than only in the PR thread.

1. "detects <= N bits" is wrong. A 64-bit scan over N=2^20 expects ONE collision
   at a 39-bit support, but P(0 collisions) is then e^-1 = 37%. Zero collisions
   excludes only <=37.4 bits at 95% confidence (<=36.8 at 99%). Reworded to
   "1 expected collision at k bits", which is what the arithmetic actually says.

2. More fundamentally, no output analysis can bound the entropy of an RNG's
   internal state. A strong PRNG seeded with 40 bits emits a stream that passes
   every one of these tests by construction -- which is precisely the shape of
   the failure that motivated this work, so citing it as the thing we now catch
   was backwards. Empirical Shannon entropy of a sample is likewise not
   cryptographic min-entropy; NIST SP 800-90B assesses the raw noise source and
   wants continuous RCT/APT health tests, which is separate future work.

The justification for 8 MB is unchanged but restated correctly: the size is set
by the POSITIVE control, not a detection threshold. A zero-collision result is
meaningless alone -- a no-op detector also returns zero -- so the scan must run
at a width where collisions are expected and counted against theory. 32-bit
collisions expect N^2/2^33: 0.03 at 64 KB (control cannot run), 8 at 1 MB, 512
at 8 MB. 8 MB is the first size at which the result carries information.

No functional change.
@BitHighlander BitHighlander changed the title feat(rng): one press unlocks a bulk RNG audit on an uninitialized device feat(rng): one press unlocks a bulk RNG health test on an uninitialized device Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant