ERA TRNG Evaluation - Technical Report

Share
ERA TRNG Evaluation - Technical Report

Evaluating Two Hardware True-Random Sources in an Air-Gapped Wallet

A NIST SP 800-90B / SP 800-22 / AIS-31 / PractRand assessment of the STM32H753 RNG and ATECC608C secure element, with cold-boot restart forensics

Motivated by the July 2026 Coldcard incident. The ERA hardware wallet has two independent hardware entropy sources: the STM32H753 on-chip RNG and the Microchip ATECC608C secure element. We evaluated both with the standard statistical batteries, and then with the class of test the batteries cannot replace: cold-boot restart correlation analysis. Restart correlation is the flaw class behind the Coldcard incident, and behind a decade of wallet failures whose output looked statistically random while the keys were predictable.

Hardware-wallet vendors usually answer the RNG question by naming a certified chip. A certificate describes the chip vendor's silicon under the chip vendor's test conditions. It says nothing about the unit in your hand: how the chip is integrated, whether the firmware actually calls it, or what it produces in the first seconds after power-on. Every incident in §1 happened in exactly those gaps. We therefore tested our own hardware, through the same firmware paths the product uses, and we publish the data, the pipeline, and the limits of the method along with the passes.

Every test also runs against synthetic broken generators, each modelling the failure class that test targets: a stuck source, a biased source, a deliberately weak classic LCG, a 32-bit-seeded PRNG, a boot-correlated source, an error-run source, and a small-boot-state source - plus Yasmarang, the exact PRNG that replaced the TRNG in the Coldcard incident: run as published through every output battery, and restart-tested in both candidate boot seedings (§8.5). These negative controls show what failure looks like under each test, and they demonstrate that the harness detects it. The real-device passes mean something because the same harness visibly catches the broken generators.

Reproducibility. All datasets, tool versions, SHA-256 hashes, and exact commands are recorded in §3 and the appendix. Synthetic controls use fixed seeds. This report renders every figure and table from analysis/results/summary.json plus the raw capture files; the summary itself is regenerated end-to-end by analysis/run_analysis.py (SP 800-90B, PractRand, forensics) and analysis/run_sp22.py (official NIST STS). The full pipeline, datasets, and manifest accompany the published repository.

Results at a glance

Six results, each stated with the scope it actually covers. The sections that follow are the evidence.

# Result Scope Where
1 Both sources pass every standard battery applied: SP 800-90B sequential, SP 800-22 (official STS), AIS-31 Procedure A T0–T5, PractRand output-stream properties only; necessary but not sufficient §5–§7
2 Both sources pass the formal SP 800-90B restart validation on the non-IID track across 1000 verified cold boots independence across power-cycles on one device §8
3 For the ATECC608C, 1000 wake-ups show no detectable correlation – the observable consequence of reseed-on-power-up, on our board our firmware path, within §8's detection limits §8
4 Cold-boot state spaces up to ≈2¹&sup7; are excluded at 95% confidence; position-aligned early-output weakness (bias tied to byte position after wake) is excluded at any state size detection power decays above 2¹&sup7; (38% at 2²&sup0;); reaching 2³² takes a 32-day cycling campaign, not a different method §8
4b Not tested here: whether different units are independent of each other cannot be asked on one device, since everything unit-specific is constant across our boots; Part 2 §8, §10
5 No output-only test in this evaluation distinguishes real hardware from a 32-bit-seeded PRNG – or from Yasmarang, the actual Coldcard fallback, a generator with exactly one possible output stream a property of the test methods themselves, shown with our own hardware, our controls, and the incident's own artifact §4–§7, §9
6 The measured entropy figures carry ±0.22 bits/byte of estimator noise at the standard 1 MB sample no ranking between healthy generators is meaningful at this sample size §5

Result 5 is the central one. Output-only testing, however thorough, cannot establish where bytes came from; a certificate for the chip cannot establish it either. Detecting that class of failure requires tests that compare independent generation events, which is what §8 does across power-cycles and what Part 2 will do across devices. Result 6 is why every entropy figure in this report is quoted with a range. Where the evidence runs out (a single device, conditioned outputs, 2³²-class boot states), §10 says so.

1. Why statistical tests are not enough

On 30 July 2026, funds began draining from Coldcard hardware wallets: 594.5 BTC in the first 25 minutes, and more than US$114M across some 7,300 addresses by 3 August, still climbing as further waves were identified. The root cause was not a weak noise source. A firmware build error (a preprocessor guard that tested whether a macro was defined rather than whether it was non-zero) silently substituted a deterministic software PRNG, MicroPython's "Yasmarang", for the STM32 hardware TRNG in the seed-generation path. Wallet seeds became a pure function of a small startup state: chip UID, a timer, and RTC registers.

How small a key space that left is itself disputed, which is worth pausing on. Coinkite's own technical backgrounder estimates the effective search space at about 2⁴⁰ for Mk2/Mk3 and about 2⁷² for Mk4/Q/Mk5, against the 2¹²⁸ a 12-word seed is supposed to carry. An independent analysis by Block, Predictable RNG Fallback and 32-Bit Reseed in COLDCARD Firmware, puts the newer models far lower - at most 2³² - on the grounds that although the secure elements supply 40 bytes, only four bytes of the reseed digest reach the PRNG state, replacing a single 32-bit word. Coinkite cites that analysis and attributes its own higher figure to secure-element entropy mixed in "as a backup to a backup", but does not reconcile the arithmetic.

We take no position on which is right. We note only that a vendor and an independent analyst, looking at the same shipped firmware, differ by forty bits on the one quantity that decides whether the seeds are enumerable - and that neither figure could be read off the output, because the output looked perfect either way.

The output stream, however, was statistically perfect. A well-behaved small-state PRNG passes NIST SP 800-22, dieharder, and PractRand comfortably, because entropy is a property of the process that produced the bytes, not of the bytes themselves. NIST is explicit about this: SP 800-22's introduction cautions that "statistical testing cannot serve as a substitute for cryptanalysis", and SP 800-90B builds its entropy-source requirements around process analysis, restart testing, and tailored health tests rather than generic randomness batteries alone (SP 800-90B §3.1.4, §4.1).

The pattern recurs. The table lists the best-known cases, and the last column records which part of this evaluation detects each failure class:

Incident Year Mechanism Passes stat tests? Detected by
Debian OpenSSL 2008 PRNG seed reduced to the PID (~15 bits) yes §8 restart collisions (small state)
Mining Your Ps and Qs 2012 boot-time entropy hole → shared/factorable keys yes cross-device comparison (Part 2)
Android
SecureRandom
2013 repeated ECDSA nonce k across signatures yes §8 collision screen across generation events
YubiKey FIPS (YSA-2019-02) 2019 reduced randomness in the first outputs after power-up yes §8 positional bias (works at any state size)
Milk Sad (CVE-2023-39910) 2023 Mersenne Twister seeded with 32 bits of time yes above single-device restart power; population key-collision (Part 2)
Trust Wallet ext. (CVE-2023-31290) 2023 32-bit MT seed → ~4×109 possible mnemonics yes above single-device restart power; population key-collision (Part 2)
Coldcard 2026 software PRNG substituted for the TRNG yes §8 restart forensics; walk-through in §8.5. The substituted PRNG itself runs through this report's full pipeline as control #6 (§4)

Two entries in the last column say Part 2 rather than §8. A single-device campaign has quantified limits, stated in §8, and overstating what it can detect would be the same mistake as citing a certificate and stopping.

The common thread across all seven incidents: each was invisible to tests that examine one output stream, and visible to tests that compare independent generation events (different boots, different devices) or bound the state space. Section 8 performs that comparison across 1000 verified power-cycles. That is what separates a wallet TRNG evaluation from a generic randomness check.

2. What ERA is, and what these numbers can claim

ERA is an air-gapped hardware wallet (STM32H753, FreeRTOS) that communicates only by QR code. It has two independent hardware entropy sources, and seed generation mixes both, plus optional user-motion entropy, through SHA-512/256. A failure of either single source is therefore contained rather than fatal. This evaluation characterises each source independently.

Scope. Both sources expose conditioned output only. That determines what every figure in this report can and cannot say:

STM32H753 RNG ATECC608C secure element
What the interface exposes post-processed output of an analog ring-oscillator noise source (noise → XOR → LFSR conditioner → 32-bit words). No raw-noise tap. output of an SP 800-90A CTR_DRBG (the Random command). Raw noise is not accessible.
What our numbers measure the min-entropy of the conditioned output stream a functional verification that the certified DRBG behaves as specified in our integration
What they do not establish the entropy of the raw noise source the entropy of the raw noise source (cited from ESV #E46, not measured here)
Source-entropy claim ST publishes none for this RNG as configured in the shipping product; this evaluation fills that gap for the output stream 1-bit-per-sample physical noise source validated at 0.5071 min-entropy/bit (ESV #E46), with hardware repetition-count and adaptive-proportion health tests completing a full cycle on power-up/wake before any output is released; DRBG re-instantiated with fresh entropy on every power-up/wake (CAVP #A4280)
What §8 adds cross-boot independence of the output no detectable wake-up correlation across 1,000 cycles – the observable consequence of reseed-on-power-up

Why the conditioned-output scope matters. A deterministic conditioner cannot create entropy. A measurement at the output therefore bounds the entropy present in the output; it does not measure the noise source behind the conditioner. The dangerous case is a conditioner fed by a dead or substituted source, because its output still measures ≈8 bits/byte. That is the Coldcard failure mode, and it is the reason this evaluation adds negative controls (§4) and cross-boot testing (§8) instead of relying on any output-only figure.

The evaluation has four elements:

  1. Statistical batteries (90B sequential, SP 800-22, AIS-31 Procedure A, PractRand): necessary but not sufficient. They would not have caught Coldcard - not a supposition: §4–§7 run them against the incident's own fallback PRNG, and it passes every one.
  2. Cold-boot restart correlation (90B §3.1.4 plus forensics): the wallet-relevant part.
  3. Negative controls: synthetic broken generators - plus the Coldcard fallback itself — run through the identical analysis pipeline.
  4. Production-path capture: every byte analysed here was drawn through the same firmware calls the product uses - the SE via its standard Random-command API, the MCU via the production RNG driver.

3. Data provenance and capture methodology

All data comes from a single physical ERA unit (dev-provisioned, config-locked ATECC608C), streamed over the debug UART and powered from a bench supply via the battery terminals. The device ran an instrumented test build made for this evaluation: the export commands below exist only in that build, not in release firmware — what they read comes through the production driver paths, which is the property the evaluation needs. The capture rig and its exact commands:

  • Bulk MCU capture - rngdump mcu <bytes>: length-prefixed raw binary at 921,600 baud with a CRC32 trailer verified on the host. Any mismatch aborts the capture.
  • SE capture - seDev rng 256 in a scripted loop. This is the same firmware API call the product uses for all secure-element randomness; the chip, not the link, sets the rate (~1.1 KiB/s).
  • Cold-boot campaign - a USB relay (RelayToggleUart) switches the Qi charging pad, which is the wake source, and poweroff shuts the device down. Every cycle is verified: the device must provably die (no shell response after poweroff) and provably reboot (boot banner observed) before its samples are recorded. A device that does not power down on the first request is power-cycled and retried with an escalating settle delay; 77 of the 1000 recorded boots (7.7%) show that retry signature in their cycle time. Cycles that never verified were discarded; there were 6 such events across the campaign (1006 cycles recorded in all, yielding exactly 1000 verified boots; 4.73 h unattended), and none contributed data. A retry pulses the charging pad while the device is still running, so retried boots have a different power history from clean ones. §8.4 tests whether that made any difference; it did not.
  • Restart dumps - after each verified boot, the first 1024 bytes are read from each source (SE first, so that its earliest post-boot draws exercise reseed-on-wake, then the MCU). The shell prompt appears ~2.09 s after power-on (median), and the app-layer command used to reach the SE registers a few seconds after that, so the first sample lands roughly 2.5–8 s after power-on depending on the boot. The samples are therefore the first bytes retrievable through the debug shell, not the first words the peripheral ever produced; a first-draw instrumentation hook is on the Part 2 list. 1000 restarts × 1024 bytes are captured per source, and the formal SP 800-90B matrix uses the first 1000 bytes of each row (1000 × 1000, per the standard).

Dataset inventory and SHA-256 integrity hashes are recorded in MANIFEST.sha256 in the repository. The campaign's timing profile:

4. Negative controls: what failure looks like

Before trusting a pass on the real hardware, we establish what a fail looks like. Each synthetic generator below models a specific real-world failure and runs through the identical analysis pipeline as the device data - the stream generators through the batteries, and the boot-related generators as synthesised 1000 × 1024 restart matrices analysed exactly like the device matrices (the relay rig is physical only for the real device). Their only purpose is calibration. The positive baseline ("Ideal reference") is a deterministic SHA-256 counter-mode stream, chosen from a different generator class than any of the controls.

Control Models Real precedent
Stuck-at total noise-source failure any dead TRNG
Biased P(1)=0.6 unwhitened / skewed analog source classic hardware bias
Classic weak LCG (§7) a 1970s-grade PRNG with visible long-stream structure textbook bad RNG; calibrates PractRand's failure depth
32-bit PRNG uniform output, enumerable state Coldcard, Milk Sad, Trust Wallet
Yasmarang the Coldcard fallback itself – MicroPython's built-in PRNG, run exactly as published Coldcard, July 2026 (§1)
Boot-correlated low entropy in the first outputs after power-on YubiKey FIPS, Ps-and-Qs
Masked error-runs error constants ( 0xFF… ) emitted into an otherwise good stream documented RNG-driver failure mode
32-bit boot state (§8) a fresh 32-bit state per boot, strong PRNG output the Coldcard newer-model case under Block's reading (§1); an enumerable boot state generally

The most important controls are a pair. The 32-bit PRNG is the failure class: output statistically indistinguishable from a real TRNG's, the whole stream determined by 32 bits of state. Yasmarang is the failure instance, the exact algorithm that replaced the TRNG in the July 2026 incident: "yasmarang" (Ilya Levin, 2011) is the PRNG built into MicroPython (extmod/modurandom.c), which is the runtime Coldcard firmware is written in. It runs here precisely as published, in uint32 arithmetic from the fixed default initial state, with no seed input, so it has exactly one possible output stream and carries zero entropy by construction. (Our implementation is verified byte-for-byte against a compile of the reference C.) Both appear in every section that follows; whatever any output-only test says about them, it says about the incident that motivated this report.

5. NIST SP 800-90B - sequential min-entropy

SP 800-90B estimates min-entropy (worst-case unpredictability) with a battery of estimators and takes the minimum. We ran the official SP800-90B_EntropyAssessment tool (non-IID track, initial-entropy estimate) on the first 1,000,000 bytes of each capture and each control: seven estimators on the 8-bit symbols (H_original) and ten on the derived bitstring (H_bitstring, computed on the first 10⁶ bits per the tool's -t option). The assessment is min(H_original, 8 × H_bitstring).

A single 1 MB assessment is what the standard specifies, but it is one draw from a noisy estimator, and this section asks whether small differences between sources mean anything. To measure the estimator's own noise, we ran the same assessment on 12 disjoint 1 MB slices of each stream - the STM32 capture, the ATECC608C capture, the ideal reference, the 32-bit PRNG and Yasmarang. The STM32 estimate ranges 6.55 – 7.23 bits/byte (6.83 ± 0.22) and the ATECC608C 6.64 – 7.15 (6.85 ± 0.21); the ideal reference gives 6.72 ± 0.12, the 32-bit PRNG 6.69 ± 0.17, and Yasmarang - a generator with exactly one possible output stream - 6.56 – 7.23 (6.84 ± 0.20), a range that sits inside the STM32's own. The estimator's run-to-run spread is larger than every difference between these five generators — the widest gap between any two means is 0.16 bits/byte, against a within-stream spread of about ±0.2 - so any ranking among them at this sample size reads noise, not hardware.

Two reporting conventions, both fixed before the analysis ran. First, the headline single-slice figures below use the first megabyte of each stream. Second, we quote the -t result throughout: the binding term is the bitstring Compression estimate, which -t computes from only 10⁶ bits (125 kB) of the megabyte, and running the same slice with -a over all 8×10⁶ bits raises the MCU assessment from 6.92 to 7.25. The -t value is the lower and more conservative of the two.

The columns to watch are the two deterministic ones. The 32-bit PRNG's Shannon entropy is 7.9998 bits/byte and its SP 800-90B assessment 6.56, with a slice-to-slice range of 6.41–6.94 that overlaps the STM32's 6.55–7.23. And Yasmarang - the actual Coldcard fallback, a generator whose true min-entropy is exactly zero because it has one possible output stream — measures 6.68 on the standard sample with a slice range of 6.56–7.23, inside the STM32's. No amount of sequential data reveals either. The two ERA sources score 6.92 (STM32) and 7.03 (ATECC608C) bits/byte on the standard sample. For the ATECC608C this is a property of the certified CTR_DRBG output (per the §2 scope), consistent with the E46/A4280 certificates.

Two entries in these tables look like findings and are not:

  1. H_original is identical for the two real sources (7.333766) because the binding literal estimator, t-Tuple at t = 2, is a pure function of the maximum 2-byte-tuple count, and both datasets happen to share the value 36. The ideal reference has 35 and the 32-bit PRNG 34. Since 34 falls below the tool's threshold of 35 occurrences, the PRNG's t collapses to 1 and its H_original rises to 7.877; on this estimator the deterministic PRNG scores higher than the real hardware. This is quantisation in the estimator, not structure in the data.
  2. The value 6.6384 appears several times in this report: as the ATECC608C's restart H_c, as the 32-bit-boot-state control's H_r, and in individual STM32 slices. It is not a property of any of them. It is where the predictors' local-predictability term lands whenever it binds at N ≈ 10⁶. The tool prints r = 4, which by the standard's convention means a longest run of three consecutive correct predictions (the tool reports longest-run + 1). A predictor guessing at 1/256 has a 5.7% chance of showing such a run in a million ideal samples, so across four predictors and two datasets per source, seeing one is unremarkable. §8 returns to this.

5.1 Does the answer survive a different capture session?

Slice variance measures how much the estimator moves across slices of one stream. It cannot tell you whether the number is a property of the source or of the capture - the rig state, the session, the hour. The only way to separate those is to capture the same source again on a separate occasion and assess it identically.

Three STM32 capture sessions, recorded hours apart, are assessed below on the same 12-slice basis. The comparison that matters is between the two spreads: within a capture (the estimator's own noise) and between captures (any session effect).

capture session MB headline (slice 0) mean over slices min–max
session_1 131.1 6.92 6.83 6.55–7.23
session_2a 131.1 7.13 6.84 6.64–7.18
session_2b 131.1 7.17 6.79 6.59–7.17

Between-session spread of the mean: 0.0525 bits/byte over 3 sessions, against a pooled within-capture SD of 0.2117 - the answer travels.

6. NIST SP 800-22 - statistical test suite

We ran the full two-level SP 800-22 methodology with the official NIST STS 2.1.2 suite, all 15 tests, on five streams: the STM32 capture, the ATECC608C capture, the ideal SHA-256 reference, the 32-bit-seeded PRNG, and Yasmarang - the Coldcard fallback itself. All five use the same 100 sequences × 1,000,000 bits, so the acceptance band (0.9602) and both criteria are identical across the table and the columns compare directly.

The verdict rule. We fixed the rule before running the suite and applied it identically to all five streams. Several tests emit multiple report rows (188 in total, 148 of them from NonOverlappingTemplate; the random-excursion rows apply only to sequences with ≥ 500 cycles, so their sample size and band differ). The band is a 3σ interval on a skewed binomial, which means ideal data is expected to drop a few of the 188 rows marginally below it, about 1.8% per 100-sequence row. A test therefore fails if any row is far below its band (>6σ), if any row's p-value uniformity fails, or if the count of marginal rows is inconsistent with chance (Poisson tail < 10⁻³). The controls calibrate the rule: the ideal reference produces 3 marginal rows against 3.61 expected, and the STM32 capture 6 against 3.66 (Poisson p = 0.1642). Both are ordinary.

The second-level criterion (p-value uniformity) is a chi-square over 10 bins and, per SP 800-22 Rev 1a §4.2.2, needs at least 55 sequences to be meaningful. At 100 sequences it is applicable to every stream here, so both criteria are reported for all five.

The result of the section, visible in every table below, is how little daylight there is between the real hardware and the deterministic generators - with one instructive exception on the Yasmarang column, resolved in §6.1.

stream sequences proportion uniformity marginal rows expected Poisson p
STM32H753 RNG (real) 100 15/15 15/15 6/188 3.66 0.1642
ATECC608C SE (real) 100 15/15 15/15 5/188 3.66 0.3051
Ideal reference 100 15/15 15/15 3/188 3.61 0.699
32-bit PRNG 100 15/15 15/15 2/188 3.56 0.8703
Yasmarang (Coldcard fallback) 100 15/15 14/15 8/188 3.1 0.0142

6.1 Resolving the one flagged column

Yasmarang's first window is the only entry in this section's tables that trips either acceptance criterion: one NonOverlappingTemplate row's uniformity P_T comes out at 6.0e-05, below the 10⁻⁴ acceptance line - 26 of its 100 per-sequence p-values pile into a single bin - and the window shows 8 marginally-below-band rows against 3.1 expected (Poisson p = 0.0142). Under the pre-registered rule that makes the NonOverlappingTemplate test fail uniformity for that window: 14/15.

This is either weak real structure in the stream or multiple-comparisons noise, and the report already has a rule for deciding which, the same one applied to PractRand's marginal grades in §7.1: re-run the full procedure on a disjoint window of the same stream. A defect of the generator is a property of every window; a sampling fluke is not. (The base rate says treat the flag with suspicion: with 188 rows per stream, ideal data lands some row below 10⁻⁴ about 2% of the time, and this section tests five streams.)

The replication window - 12.5 MB starting at byte 2²⁵, sharing no bytes with the first - returns 15/15 on proportion and 15/15 on uniformity, with 2 marginal rows against 3.49 expected (p = 0.8631). Nothing reproduces. The verdict row for Yasmarang therefore reads pass, and the honest summary cuts both ways:

  • This is not SP 800-22 catching the Coldcard fallback. On the rule fixed before the data was seen, Yasmarang passes; the flag is what a false alarm looks like at this scale. Real defects in this report do not whisper: the biased stream fails 777 AIS-31 checks and the weak LCG collects 285 PractRand alarms.
  • But the criterion that even noticed something is the one almost nobody runs. The second-level P_T requires ≥ 55 sequences (§4.2.2) and the full two-level protocol; a vendor running a handful of sequences - or one - never computes it. The strongest statistical wobble the entire canon could raise against the incident's actual generator appeared only under the suite's most demanding configuration, and evaporated on replication. Result 5 of the summary stands: on output alone, this generator is not distinguishable from the hardware.
window proportion uniformity marginal rows expected Poisson p min PT
window 1 (bytes 0–12.5 MB) 15/15 14/15 8/188 3.1 0.0142 6.0e-05
window 2 (bytes 225–225+12.5 MB) 15/15 15/15 2/188 3.49 0.8631 4.6e-03

7. AIS-31 Procedure A and PractRand

We applied AIS-31 Test Procedure A (BSI AIS 20/31, 2011) in full to the internal random numbers: T0 (disjointness over 2¹⁶ consecutive 48-bit words) plus T1–T5, the FIPS 140-1 tests (monobit, poker, runs, long-run) and the autocorrelation test, each applied to 257 samples of 20,000 bits with the FIPS 140-1 bounds the standard specifies. Those bounds put each basic test at a ~10⁻⁶ significance level, which justifies the standard's strict pass rule: no basic test may fail, and a single failure may be repeated once on fresh data. The procedure is implemented in analysis/ais31.py and runs live in the analysis notebook.

We run PractRand, the most sensitive long-stream structure detector available, as a depth ladder: one independent evaluation at every power-of-two prefix from 2¹⁰ to 2²⁶ bytes, on every stream. This measures the depth at which structure first appears. A single run at one length reports only the size of the file it was given. Five of the six streams are analysed over the same 2²⁶ bytes; the ATECC608C's chip-limited capture reaches 2²⁵, which is stated wherever its depth is compared, so no comparison is confounded by one stream having been fed less data without saying so.

The contrast has three parts. The weak classic LCG is clean to 64KB, turns suspicious at 128KB, and fails outright at 256KB: a real, located failure depth. The strong 32-bit-seeded PRNG stays clean through every checkpoint to 2²⁶ bytes, exactly like the real capture. And Yasmarang, the PRNG that actually replaced the TRNG in the Coldcard incident, is clean through every checkpoint to 2²⁶ as well, collecting 2 'unusual' grades against 1.34 expected by chance, fewer than the ideal reference. The most sensitive structure detector in the canon reads 64 MB of output from a generator whose entire future is fixed by four initial constants, and finds nothing. Even PractRand cannot see a small state behind an adequate mixing function. Sensitivity to structure is not sensitivity to state size, and the Coldcard class of flaw lives entirely on the far side of that distinction.

stream basic-test failures (of 1,286 checks) verdict
STM32H753 (real) 0 PASS
ATECC608C (real) 0 PASS
Ideal reference 0 PASS
32-bit PRNG 0 PASS
Yasmarang 0 PASS
Biased P(1)=0.6 777 FAIL
stream ladder run to clean through first FAIL serious grades mild grades
STM32H753 RNG (real) 64MB 64MB 0 2
ATECC608C SE (real) 32MB 32MB 0 1
Ideal reference 64MB 64MB 0 3
32-bit PRNG, strong core 64MB 64MB 0 0
Yasmarang (Coldcard fallback) 64MB 64MB 0 2
Classic LCG (weak) 64MB 64KB 256KB 285 32

7.1 Resolving the marginal grades

A ladder of this length asks PractRand for a verdict several hundred times, so the interesting question is not whether a marginal grade appears but whether one means anything. Two facts settle it, and both are computed rather than asserted.

First, the base rate. Each rung evaluates 80–100 sub-tests and PractRand starts labelling at roughly p < 10⁻³, so a perfect stream is expected to collect one or two marginal labels over a 16-rung ladder - which is about what the real ladders show.

Second, and decisively: a grade caused by the source recurs in any window of the stream; one caused by multiple testing does not. Every rung that earns a grade above PractRand's weakest is therefore automatically re-run at the same depth on a different window of the same capture. The check is part of the pipeline, so the answer is on the record rather than argued after the fact.

8. Cold-boot restart forensics - the test that matters

A generic randomness check omits this section, and it is the one that separates a real TRNG from a Coldcard-style substitution. From each source we collected the first 1024 bytes retrievable after each of 1000 verified power-cycles (§3: every boot proven dead, then proven rebooted; dumps begin ~2.09 s after power-on), forming the NIST SP 800-90B restart matrix. We apply the formal 90B restart test on the non-IID track, matching the sequential assessment as the standard requires, plus three forensic views. Two synthetic restart controls bracket the analysis: a boot-correlated matrix (the YubiKey-class pathology the test must catch) and a 32-bit-boot-state matrix (an enumerable per-boot state — the Coldcard newer-model case if Block's 2³² reading is right, and either way the pathology that marks the limit of a single-device campaign).

Reading the verdicts. The standard's decision rule: the sanity check must pass (no value may occur more often than the binomial cutoff in any row or column), the row/column estimates must satisfy min(H_r, H_c) ≥ H_I/2, and the final assessed entropy is min(H_r, H_c, H_I). Both real sources pass with wide margin. For the STM32, the assessment stays at the sequential value (6.92 bits/byte). The boot-correlated control fails the sanity check outright: X_max = 290 against a cutoff of 29, so the tool awards no entropy at all. The 32-bit-boot-state control passes; the power discussion below explains why.

Why the ATECC608C's assessed value is 6.64. Its column dataset binds (H_c = 6.64 against H_r = 7.87) because two column predictors, MultiMMC and LZ78Y, trip SP 800-90B's local-predictability term. The tool reports r = 4 for them, which by the standard's convention means a longest run of three consecutive correct predictions (it prints longest-run + 1). Every other estimator in this campaign reports r = 3, a run of two.

A run of three is not a rare event. A predictor guessing at 1/256 has a 5.7% chance of producing one somewhere in a million ideal samples, and with four predictors applied to both the row and the column dataset, the chance that at least one trips is roughly 38% per source. When the term trips at this sample size it always lands on the same number, 6.6384. The identical value appears as the 32-bit-boot-state control's H_r (6.6384) and in individual 1 MB slices of the STM32 capture (§5). It is a fixed point of the estimator, not a property of any device.

The assessed value 6.64 bits/byte is therefore what the standard requires us to report. It sits far above the H_I/2 = 3.51 validation floor, and it reflects the estimator's built-in conservatism rather than anything observed about the secure element.

8.4 Are the 1000 boots exchangeable?

A restart matrix is only meaningful if its rows are comparable events. Ours differ by construction in two ways, both visible in the §3 timing data, and both worth testing rather than assuming away:

  • 77 boots (7.7%) followed a retried power-off, in which the charging pad is pulsed while the device is still running. Those boots have a different power history from a clean cycle.
  • The delay from power-on to the first sample varies by several seconds between boots, because the rig polls until the app-layer command that reaches the SE has registered.

If either mattered (incomplete reseed at the earlier draw times, or a noise source perturbed by the pad pulse), the groups would differ. They do not:

The retried-power-off group, the one case where the hardware saw a charger pulse before the cycle, is indistinguishable from the rest at p = 0.9548 (STM32) and 0.2023 (ATECC608C), and the weakest homogeneity result anywhere in the table is p = 0.1678. (The smallest entry in the byte-uniformity column, p = 0.0151, is likewise unremarkable: the chance that the smallest of 8 uniform p-values falls at or below it is ≈11%.) The matrix rows can be treated as exchangeable.

Statistical power - what N = 1000 does and does not prove

Collisions are governed by the birthday bound. With 499,500 boot pairs and zero collisions observed:

Hypothetical boot-state space Expected collisions Probability we would have seen one
216 7.6 99.95%
217 3.8 97.8%
218 1.9 85.1%
220 0.48 37.9%
232 0.0001 0.01%

The campaign therefore excludes boot-state spaces up to ≈2¹⁷ at 95% confidence, and 2¹⁶ with near certainty - below Coinkite's own ~2⁴⁰ estimate for the Coldcard Mk2/Mk3 case, which this campaign would not have caught by collisions alone. Power decays fast above that: at 2²⁰ the detection probability is 38%, so we do not claim to have excluded it. Separately, the per-position analyses exclude position-aligned early-output weakness at any state size - the YubiKey-FIPS and Ps-and-Qs class, where the bias is tied to byte position after wake. Correlation that is not position-aligned (say, between non-corresponding positions of different boots) is outside this instrument's aperture; the pair-comparison screens and Part 2 address that class.

Why we stop at 2¹⁷ and not 2³². The limit is wall clock, not method. Collision detection scales with the number of pairs, which grows as n², so excluding one more bit of state space costs √2 ≈ 41% more boots, and every boot costs a full power-cycle:

Boot-state space excluded (95%) Boots required Continuous unattended cycling
217 – this campaign 1,000 4.73 h ✓
220 2,507 12 h
224 10,026 2.0 days
228 40,104 8.1 days
232 – Coldcard's newer models under Block's reading 160,416 32 days

At the 17.4 s median cycle time, reaching 2³² means 160,416 verified power-cycles over 32 days of uninterrupted bench time on one device. Stripping the rig to its observed floor cycle of 11.3 s (no settle dwell, no retry ladder, no post-boot poll) still takes 21 days, and that rig would be measuring a different thermal and power-sequencing regime from the one the product experiences. The 1,000-boot campaign presented here took 4.73 h; the 2³² version is roughly 160× that. We have queued it as a long-run Part 2 background campaign. It is a scheduling and device-wear decision, not a limit of the method.

More boots is not the interesting direction, though. Our 32-bit-boot-state control passes every single-device restart view here, exactly as the arithmetic predicts. No amount of cycling this device changes the more important blind spot:

A single-device campaign asks whether this unit produces independent output across its own power cycles. It cannot ask whether different units produce independent output from each other, because everything that distinguishes one unit from another is constant across all 1,000 of our boots.

The collision bound never reaches that failure class at any campaign length. If a seed derived from a chip UID, a factory-programmed constant, or shared provisioning state, every per-boot test in this report would still pass on every individual device while keys across the population were related or enumerable. That happened in Mining Your Ps and Qs: individually healthy-looking devices produced keys that shared prime factors across the population. No single-unit experiment can see it.

Part 2 is therefore not the same test with more boots. It changes the hypothesis: put several units side by side and compare their outputs to each other. That detects device-dependent state at any state-space size and needs a handful of devices rather than a 32-day campaign. Extending the birthday bound is a secondary benefit (5 units × 1,000 boots pools to ≈2²², not 2³²); the primary reason for a population is that it is the only way to ask the question at all.

Within those limits, both real sources pass the formal SP 800-90B restart validation on the correct non-IID track (STM32: H_r 7.87, H_c 7.35, assessed 6.92; ATECC608C: H_r 7.87, H_c 6.64, assessed 6.64 bits/byte), with the sanity check clean. For the SE, the pass means 1000 wake-ups show no detectable correlation — the observable consequence of reseed-on-power-up, within this campaign's stated detection limits. What the datasheet promises as a mechanism, we observe as its output-side consequence; the mechanism itself is not directly measurable from outside the chip.

8.5 What this bench does to a Coldcard-class fault

The Coldcard seed was a pure function of a chip UID, a running timer, and RTC registers. Consider how that construction behaves in this campaign. Across 1,000 verified power-cycles of one device the UID contribution is constant, and a timer or RTC value read at a fixed point in a repeatable boot sequence varies over a small range. The state that actually changes from boot to boot is therefore far smaller than the nominal seed material, and that per-boot state is exactly what §8 measures. Strongly correlated seeds produce first-output collisions, shared prefixes and positional bias; our boot-correlated control models this case and fails the restart validation outright (X_max = 290 against a cutoff of 29; Figures 8.1 and 8.2). If the residual per-boot variability is instead large — timer jitter worth more than the ≈2¹⁷ states this campaign excludes - the single-device bound runs out, and the cross-device comparison of Part 2 takes over: the same seed recipe on different units produces related keys across the population, which a Ps-and-Qs-style comparison detects at any state size.

That reasoning is now checked directly instead of argued. We built restart matrices for the fallback formula itself, in the two seedings the incident debate is about, and ran them through the same §8 analysis as the device data.

As published, the formula starts from its fixed built-in constants, which is what the code does if nothing seeds it. Every boot then replays the identical stream, and the analysis convicts it at once: all 999 of the 999 possible first-boot collisions occur at every prefix width, every pair of boots shares its entire 16-byte opening, positional deviation pegs at the theoretical maximum of 0.5, and the SP 800-90B restart sanity check fails with X_max = 1000 against a cutoff of 30. In practice this configuration is caught at the second boot.

Reseeded with 32 bits per boot, following urandom.seed() semantics and Block's reading of the shipped configuration, the same formula sails through the same analysis: zero collisions at every width, prefix agreement on the geometric ideal, positional bias inside sampling noise, and a passing restart validation (H_r 7.35, H_c 7.33). The published seeding is caught instantly. The shipped-per-Block seeding is invisible at N = 1,000, exactly as the birthday arithmetic above predicts. What decided the incident's detectability was never the formula; it was the seeding, and at 2³² the seeding is beyond a single device's reach.

Neither chip's certificate would have flagged any of this, because the chips were never the problem. The integration was, and a bench test of the shipped device is a test of the integration.

Yasmarang seeding 8B first-draw collisions mean shared prefix (bits) max positional dev (first 64B) sanity X_max (cutoff) 90B restart
as published (fixed constants) 999 128.0 0.5 1000 (30) FAIL
32-bit reseed per boot (Block's reading) 0 1.0 0.06 16 (30) PASS

9. Real vs. control - the whole picture on one grid

The master comparison. Two rows carry the lesson. The boot-correlated control fails the restart column: the harness catches the YubiKey and Ps-and-Qs class outright. The 32-bit PRNG family passes every output-only battery and, at single-device scale, even the restart test. No output-only test distinguishes it from real hardware. That is the Coldcard blind spot, §8 quantifies its extent, and it is the reason the evaluation is built as a set: process scoping (§2), restart forensics at N = 1,000 (§8), and the Part 2 cross-device campaign each cover what the others cannot.

generator Shannon/B 90B seq slice range SP 800-22 AIS-31 A PractRand 90B restart
STM32H753 RNG (real) 8.00 6.92 6.55–7.23 15/15 PASS clean → 64MB PASS
ATECC608C SE (real) 8.00 7.03 6.64–7.15 15/15 PASS clean → 32MB PASS
Ideal reference 8.00 6.76 6.44–6.90 15/15 PASS clean → 64MB PASS
Yasmarang (Coldcard fallback) 8.00 6.68 6.56–7.23 15/15 (unif 14/15, §6.1) PASS clean → 64MB
Yasmarang boots, as published FAIL
Yasmarang boots, 32-bit reseed PASS
32-bit PRNG 8.00 6.56 6.41–6.94 15/15 PASS clean → 64MB
32-bit boot state PASS
Classic LCG (weak) FAIL @ 256KB
Boot-correlated FAIL
Biased .6 7.767 4.20 FAIL
Masked error-runs 7.998 0.73
Stuck-at 0.00 0.00

How to read the grid. A dash means the test was not run on that generator: stream fakes that already failed a cheaper column decisively were not fed to the expensive batteries, and the boot-related rows are synthetic restart matrices; of those, only the two Yasmarang seedings have a sequential counterpart (the stream row above them). Left to right, the tests get more expensive and more specific. Shannon entropy flags only the crudest failures (stuck, biased). Sequential 90B additionally catches the error-run stream and quantifies the healthy sources, but scores the 32-bit PRNG - and Yasmarang, the incident's actual generator - like hardware. SP 800-22, AIS-31 and PractRand pass them too (Yasmarang's one second-level near-miss does not survive replication; §6.1). The PractRand column is where the weak classic LCG falls. The restart column is the only one that fails the boot-correlated pathology. Two stream fakes therefore survive every output-only column - the 32-bit PRNG and Yasmarang - and the per-boot small-state rows (the 32-bit boot state control, and Yasmarang under a 32-bit reseed) additionally survive the restart column at single-device scale, with the detection limit quantified in §8 and assigned to Part 2's cross-device analysis. Yasmarang's restart behaviour is a seeding choice, not a property of the formula, so §8.5 runs it both ways: as published, it repeats its one stream identically and is caught at the second boot (the FAIL in its row), while the same formula under a 32-bit per-boot reseed passes at N = 1000. No single column is sufficient; the evaluation is the set.

10. Conclusions, limitations, and Part 2

Findings.

  • Both ERA hardware entropy sources are healthy on every test applied to them. STM32H753 RNG: SP 800-90B sequential min-entropy 6.92 bits/byte on the standard 1 MB sample (6.55–7.23 across 12 disjoint slices - see below); SP 800-22 (official STS 2.1.2, both criteria) 15/15 tests pass; full AIS-31 Procedure A (T0–T5) pass with zero basic-test failures; PractRand clean through every checkpoint of the ladder to 64MB; and the SP 800-90B restart validation passes on the non-IID track (H_r 7.87, H_c 7.35, assessed 6.92 bits/byte).
  • ATECC608C: behaves as its certificates specify (ESV #E46 / CAVP #A4280). Sequential min-entropy 7.03 bits/byte; SP 800-22 15/15 tests pass on both criteria at the same 100 sequences as every other stream; full AIS-31 Procedure A pass; and the restart validation passes with assessed entropy 6.64 bits/byte (a predictor's local-predictability term binds; §8 shows that value is an estimator fixed point rather than an observation about the device). The restart pass means 1000 wake-ups show no detectable correlation - the observable consequence of reseed-on-power-up in our integration, within §8's detection limits.
  • The entropy figures are not device constants. Re-running the same assessment on 12 disjoint 1 MB slices gives 6.83 ± 0.22 bits/byte for the STM32, 6.72 ± 0.12 for an ideal SHA-256 reference, 6.69 ± 0.17 for a deterministic 32-bit-seeded PRNG and 6.84 ± 0.20 for Yasmarang, whose true min-entropy is exactly zero: four overlapping distributions. At this sample size the estimator cannot separate real hardware from any of them.
  • The cold-boot forensics show no correlation across 1000 verified power-cycles on either source: zero first-output collisions at 4/8/16-byte widths; positional bias inside pure sampling noise (max 0.050/0.052 against 0.051 expected, 0.062 at the 95th percentile, and 0.051 for the ideal reference matrix); boot-pair prefix agreement on the geometric ideal; and no detectable difference between boots taken under different rig conditions (§8.4; weakest homogeneity p = 0.1678).
  • The full-stream data-quality screens are clean: across the entire 125 MiB (131 MB) MCU capture, no 0xFF run of even 4 bytes, no 32-bit-aligned error-constant words (0 × 0xFFFFFFFF, 0 × 0x00000000), and a longest identical-byte run of 4, which is the chance expectation.
  • The negative controls validate the harness. Total failure, bias, and error-runs are caught by the batteries; boot-correlation is caught by the restart analysis; the weak classic LCG is caught by PractRand at 256KB. The small-state generators are caught by nothing output-only: the 32-bit PRNG passes Shannon, SP 800-22 (15/15), AIS-31, PractRand to 64MB and sequential 90B - and Yasmarang, the PRNG that actually replaced the TRNG in the Coldcard incident, passes the identical set: clean to 64MB in PractRand, 15/15 on SP 800-22 proportion with its one second-level near-miss resolved by replication (§6.1), and a slice range (6.56–7.23) inside the STM32's own (6.55–7.23). Restarted, the same formula is caught at the second boot in its published seeding (999/999 first-draw collisions; sanity X_max 1000 against a cutoff of 30) and passes at N = 1000 under a 32-bit per-boot reseed. The two seedings bracket exactly what §8 can and cannot see (§8.5). Output-only testing, however thorough, cannot establish where bytes came from.

Limitations.

  • Single device, single configuration: the structural limit. Every test here compares one unit against itself across power-cycles, so it cannot detect device-dependent state. A seed derived from a chip UID, a factory constant, or shared provisioning material would pass every measurement in this report on every individual device while keys across the population were related. Everything that distinguishes one unit from another is constant across all 1,000 of our boots, so no campaign length changes this. It is the Mining Your Ps and Qs failure mode, and it is Part 2's centrepiece.
  • Conditioned outputs only. Both sources expose conditioned output. A deterministic conditioner cannot create entropy, so these figures bound the entropy present in the output and are not measurements of either noise source. The ATECC608C's source claim rests on ESV #E46; no equivalent public assessment exists for the STM32 RNG as configured here.
  • Restart-dump latency. Restart samples are the first bytes retrievable via the debug shell, roughly 2.5–8 s after power-on depending on the boot, not the peripheral's literal first words. A first-draw instrumentation hook is Part 2 work.
  • Restart-test power is quantified, and the ceiling is bench time. N = 1000 excludes boot-state spaces up to ≈2¹⁷ at 95% confidence, and position-aligned early-output weakness at any state size; at 2²⁰ the detection probability is already down to 38%. Because collisions scale with pairs (n²), each additional bit costs 41% more boots: reaching 2³² needs 160,416 verified power-cycles ≈ 32 days of continuous unattended cycling (21 days even at the rig's floor cycle time), against the 4.73 h spent here. It is queued as a long-run Part 2 campaign. The extra cycling would only extend the same hypothesis; the device-dependent-state question above is not reachable this way at all.
  • Estimator variance. Single-sample SP 800-90B figures carry ≈±0.2 bits/byte of run-to-run spread at 1 MB. Every comparison in this report is made against that spread, not across it.
  • PractRand depth is bounded by captured volume: 64MB for the MCU, 32MB for the SE. The SE's ceiling is the chip itself - its Random command is rate-limited to ~1.1 KiB/s, so each additional PractRand rung costs twice the previous rung's hours of bench time.

What this means for a wallet seed. This paragraph is an architectural argument, not a measurement. A 256-bit seed requires 256 bits of input min-entropy, and at the most conservative figure in this report (6.55 bits/byte at the output), about 39 bytes from a single source already carry that much. ERA's seed generation draws on both sources, plus optional user-motion entropy, and mixes them through SHA-512/256. Producing a weak seed would therefore require both independent sources - different silicon, different vendors, different interfaces — to fail at the same time, in a way that also escaped the §4 screens and the §8 restart forensics. The measurements here cannot prove that scenario impossible; no output-side measurement can. What they show is that each source, tested independently through the production firmware path, behaves as a healthy entropy source across 1,000 cold boots.

Part 2 roadmap. The centrepiece is cross-device, because that is the only way to test whether different units are independent of each other: 3–5 units, restart matrices repeated per device, and population-level key-collision analysis in the Mining Your Ps and Qs style. This detects device-dependent state at any state-space size and needs a handful of devices rather than a 32-day cycling run. Alongside it: a long-run single-device campaign pushing the birthday bound past 2²⁴ as background bench time allows; first-draw restart instrumentation (capturing the peripheral's very first post-boot words rather than the first shell-retrievable ones); environmental corners (temperature/voltage) within the ATECC608C's certified −40…105 °C range; and deeper PractRand and TestU01 on both sources.

Bottom line. Both of ERA's independent hardware sources pass every test applied to them: SP 800-90B (sequential and restart, on the correct track), SP 800-22 under both acceptance criteria with the official suite, complete AIS-31 Procedure A, and PractRand through every checkpoint of the available depth. The cold-boot forensics show the cross-boot independence a true entropy source must have. The same evaluation also shows why such results are never the whole story: our small-state controls pass everything output-only, and the estimator's own variance is larger than any difference between real hardware and a deterministic PRNG. The defence in depth is architectural (two independent sources mixed through SHA-512/256, samples drawn through the production API path) and procedural (the restart analysis here, and the cross-device campaign of Part 2).

Two things this bench cannot say, stated once more. Pushing the cold-boot bound from 2¹⁷ to 2³² is available but expensive (160,416 power-cycles, about 32 days of continuous cycling against the 4.73 h spent here), so it is queued rather than claimed. And no single-device campaign of any length can test whether different units are independent of each other, because everything that varies between units is constant across our boots. That question needs a population, and it is the reason Part 2 exists.

Appendix - reproducibility

  • Datasets: data/{mcu_raw_125MB, se_seq_2026-08-06, mcu_restart, se_restart}.bin (+ .meta.json, coldboot_log.jsonl). SHA-256 manifest in §3; restart .bin.1000x1000 files are the exact ea_restart inputs (first 1000 bytes of each 1024-byte boot record).
  • Capture: capture.py - rngdump mcu <bytes> (raw @921600, CRC32-verified), seDev rng 256 loops (production SE API), mcuRnd (production RNG driver), poweroff, RelayToggleUart USB relay switching the Qi pad (charger-connect wake), boot-banner + dead-device verification per cycle.
  • Tools: NIST SP800-90B_EntropyAssessment (ea_non_iid -i -t -v, ea_restart -n; commit 87c104d); official NIST STS 2.1.2 (assess, 100 × 1 Mbit per stream; reports in analysis/results/sts/); PractRand 0.94, driven as a ladder of independent runs at every power-of-two prefix from 2¹⁰ to 2²⁶ bytes; AIS-31 Procedure A (T0–T5, FIPS 140-1 bounds) implemented in analysis/ais31.py.
  • Standards references for the non-obvious choices: restart test on the non-IID track to match the sequential assessment (SP 800-90B §3.1.4); r in the predictor output is the longest run of correct predictions plus one (§6.3); p-value uniformity requires ≥55 sequences (SP 800-22 Rev 1a §4.2.2); AIS-31 Procedure A uses the FIPS 140-1 bounds, not 140-2, because the looser 140-2 bounds produce expected false alarms over 257 repetitions.
  • Pipeline: analysis/{run_analysis,run_sp22,pipeline,controls,viz,ais31}.py; results cached to analysis/results/summary.json (the report renders from the cache; both runners regenerate it end-to-end). Synthetic controls use fixed seeds (controls.SEED, run_analysis.CTRL_SEED); the ideal reference is SHA-256 in counter mode; Yasmarang is seedless as published (a single fixed stream), verified byte-exact against a compile of the reference C from MicroPython's extmod/modurandom.c.
  • Environment: Python 3.14.4, numpy 2.5.1; requirements.txt pins the notebook environment.
  • Standards: NIST SP 800-90B (Jan 2018); NIST SP 800-22 Rev 1a (STS 2.1.2); BSI AIS 20/31 (2011); Microchip ESV #E46; CAVP #A4280.

📁 Data, code, tools and manifest: github.com/ERAWLT/ERA-Entropy-Assessment 📄 PDF rendering: era_trng_evaluation-report.pdf


Dan Mutsvanga, CTO, ERA Wallet

Testing ERA’s Randomness: What a Certificate Can’t Tell You
TL;DR: Nearly every hardware wallet answers “is your randomness good?” by naming a certified chip. A certificate describes silicon in a lab, not the wallet on your desk, and the Coldcard failure happened entirely in the gap between the two. So we put our own shipping device on a

📰 The plain-language companion to this report

ERA Wallet
A direct line to the ERA team. Security updates before they’re public, straight from the people who build it. 🌐 https://era-wallet.com Group @EraWalletchat Support @ERAwlt_support_bot

Read more