Trezor Seed Phrase Generation — Security Analysis Report
Date: 2026-07-31
Analysis by: Claude Fable 5 (
claude-fable-5) via Claude CodeTrigger: Coldcard Mk3 seed-generation advisory (Coinkite, 2026-07-30/31)
Code audited:
trezor-firmware@66205f1b6b(origin/main, 2026-07-31) andtrezor-suite(local checkout), read-only source analysisScope: Wallet-creation (seed generation) paths only — modern core firmware (Model T / Safe 3 / Safe 5 / T3W1), legacy firmware (Trezor One), the shared crypto library, and the host side (Trezor Suite / @trezor/connect, trezorlib). Signing-nonce generation, storage encryption, and supply-chain integrity are out of scope.
1. Executive summary
Verdict: Trezor’s seed generation, as present in today’s source tree, does not contain the Coldcard class of flaw, and its architecture makes that class of flaw substantially harder to introduce or to hide.
Key reasons, in order of importance:
Two independent entropy parties. A Trezor seed is never derived from device randomness alone. The final secret is
SHA-256(device_entropy[32 bytes] ‖ host_entropy), where the host (Trezor Suite or trezorlib) contributes 32 bytes from the OS CSPRNG. Either side’s RNG failing completely still leaves the full entropy of the other side. Coldcard’s bug was catastrophic precisely because the device was the only entropy source.The device’s honesty is cryptographically auditable. Since firmware 2.8.7 / 1.13.1, a commit-then-reveal “entropy check” protocol lets the host verify — before the seed is kept — that the device really sampled fresh entropy, committed to it before seeing the host’s bytes, mixed both exactly as specified, and derived the served xpubs from the result. Trezor Suite runs this check by default during wallet creation.
The weak PRNG cannot be silently linked into device firmware. Trezor ships an intentionally weak LCG for the emulator (
crypto/rand_insecure.c), but it is fenced byUSE_INSECURE_PRNG, defined only in emulator build configurations, emits a loud compile-time#pragma message, and — critically — would collide at link time with the hardwarerandom_buffersymbol in a device build. Coldcard’s bug was exactly a silent fallback-PRNG link-in caused by an#ifndef-vs-value preprocessor mistake.The hardware RNG fails closed. Both the legacy (STM32F2) and core (STM32F4/U5) TRNG drivers only consume data when the peripheral’s seed-error (SECS) and clock-error (CECS) flags are clear and data-ready (DRDY) is set, and both implement the reference-manual-mandated consecutive-word repeat test. A faulted TRNG causes a visible hang, never silent weak output.
On all current Safe-family models, two independent hardware TRNGs are XORed (STM32 TRNG ⊕ Optiga Trust M and/or Tropic01 secure element), with secure-element failure raising an exception rather than silently degrading.
No vulnerabilities were found in the seed-generation path. Section 6 lists residual hardening observations (none exploitable on their own) and Section 7 gives practical guidance for you as a Trezor owner.
2. Background: what happened to Coldcard
Per Coinkite’s advisory and their technical backgrounder:
A 2021 migration to libsecp256k1 moved seed generation from
ckcc.rng_bytes()(hardware TRNG wrapper) tongu.random.bytes(), which resolved to MicroPython’s software fallback PRNG instead of the hardware RNG.The root cause was a preprocessor guard bug:
MICROPY_HW_ENABLE_RNGwas set to0, but the guard used#ifndef— testing whether the macro was defined, not its value — so the weak fallback PRNG still compiled and linked. In Coinkite’s words, the bulk of randomness came from “a PRNG that I didn’t know was actually in the source code base.”Effective seed entropy: ~40 bits on Mk3 (PRNG seeded from device/timing state; every firmware 4.0.1 → 5.0.3, i.e. since March 2021), ~72 bits on Mk4/Mk5/Q before the fixed releases (secure-element TRNG values happened to be mixed into the PRNG state as a side effect).
Before disclosure, roughly 594 BTC (~$38M) was swept from ~500 single-sig addresses by brute-forcing the reduced search space.
Firmware updates cannot repair existing seeds; affected users must migrate funds. Users who added 50+ dice rolls at creation are considered safe.
Their fix includes a build-time symbol check: the build now fails unless the board object defines
rng_get()and the upstream fallback object defines no symbols.
The generalized failure modes to test Trezor against:
(A) A weak/fallback RNG reachable from the production seed path (build-system or refactor hazard).
(B) An RNG delivering fewer effective bits than requested, or failing silently.
(C) Entropy-combination logic that can drop or weaken a source.
(D) A single point of entropy failure with no external auditability.
3. Architecture: how a Trezor seed is created
The protocol (identical in spirit across legacy and core firmware):
Host Device
| ResetDevice(strength, ...) ----> | samples int_entropy = TRNG(32 bytes)
| <---- EntropyRequest | (+ commitment = HMAC-SHA256(int_entropy, ""))
| EntropyAck(host 32B CSPRNG) ---> | secret = SHA-256(int_entropy ‖ ext_entropy)[:strength/8]
| | mnemonic = BIP-39(secret) (or SLIP-39 shares)
Properties of SHA-256(int ‖ ext) concatenate-then-hash mixing:
The device’s 256-bit contribution is sampled (and, with the entropy check, committed to) before the host’s bytes arrive, so a malicious host cannot adaptively cancel or bias the result — and even a constant/empty host contribution leaves the full 256 bits of device entropy.
Symmetrically, a broken device RNG is masked by honest host entropy — and the entropy-check protocol exists to detect that case rather than merely mask it.
This is not an XOR design; there is no way for one input to cancel the other.
This two-party design is the single biggest structural difference from Coldcard, where the device was the sole entropy source and no external party could audit it.
The entropy-check protocol (commit–reveal audit)
Implemented in core firmware (core/src/apps/management/reset_device/init.py:85-114), legacy firmware (legacy/firmware/reset.c), @trezor/connect (packages/connect/src/api/resetDevice.ts:69-140), and trezorlib (python/src/trezorlib/device.py:455-564):
Device sends
entropy_commitment = HMAC-SHA256(key=int_entropy, msg="")before receiving host entropy.Host sends its 32 bytes; device computes the candidate seed but does not store it; host fetches xpubs (
m/84'/0'/0',m/44'/60'/0') derived from the candidate seed (never displayed).Host runs a random number of rounds (connect: 1–4, drawn via rejection-sampled
crypto.getRandomValues; trezorlib: 2–8). On each non-final round the device reveals the previous round’sint_entropy; the host re-verifies the commitment, recomputesSHA-256(int‖ext)→ BIP-39/SLIP-39 seed → xpubs from scratch (independent implementations: @noble/hashes + bip32 in connect;mnemonic/shamir_mnemonicin trezorlib) and compares.Only entropy from discarded rounds is ever revealed; the kept seed’s entropy stays secret. Because the device can’t predict which round is final, cheating on the kept round risks detection proportional to the round count.
Suite treats any non-transport error during the critical phase as a check failure (anti-bypass, resetDevice.ts:74-79), persists SHA-256 hashes of the verified xpubs per device, and re-compares them on every subsequent account discovery.
What this catches: a device whose RNG is broken the way Coldcard’s was (predictable output) is not directly detected — predictable-but-honestly-mixed entropy still verifies. What it catches is a device that lies about the derivation: ignoring host entropy, reusing pre-planted entropy, adaptively choosing entropy after seeing the host’s, or serving xpubs from a different seed. Combined with the mixing rule, the two mechanisms cover complementary failure modes: honest-device-weak-RNG is masked by host entropy; dishonest-device is detected by the check.
4. Findings — firmware
4.1 Modern core firmware (Model T, Safe 3, Safe 5, T3W1)
Internal entropy: 32 bytes, strong path, fail-closed. reset_device/init.py:88: int_entropy = random.bytes(32, True) — always 256 bits regardless of requested strength. The strong=True flag routes to rng_fill_buffer_strong() and raises RuntimeError on any failure (modtrezorcrypto-random.h:49-69) — no fallback path exists.
Mixing (reset_device/init.py:343-356):
ehash = hashlib.sha256()
ehash.update(int_entropy)
ehash.update(ext_entropy)
entropy = ehash.digest()
secret = entropy[: strength_bits // 8]
Verified byte-for-byte against the host-side re-derivation in both connect and trezorlib.
Hardware RNG layer, bottom-up:
STM32 TRNG driver (shared F4/U5): core/embed/sys/rng/stm32/rng.c:40-64. Data is consumed only when
RNG->SR == DRDYwith SECS and CECS clear; a repeated consecutive 32-bit word is rejected (RM0090 §24.3.1 / FIPS continuous test). A faulted peripheral causes a spin (visible hang), never weak output.rng_init()runs in the reset handler before any consumer.Dual-source XOR on Safe models (core/embed/sec/rng/rng_strong.c:38-72): the buffer is first filled from the MCU TRNG, then XORed block-wise with output from the secure element(s) — Optiga Trust M (T2B1/T3B1/T3T1, T3W1 revC) and/or Tropic01 (T3W1). An SE command failure returns
false→ Python exception; the buffer is never silently left MCU-only when an SE is configured. XOR of independent sources means each is a full fallback for the other.Model T (T2T1) has no secure element; its strong path is the MCU TRNG alone — still status-checked and then SHA-256-conditioned with host entropy in the seed path.
No DRBG sits in the seed path — raw (XOR-mixed) TRNG output feeds
SHA-256(int‖ext), which acts as the conditioning/extraction step for the stored secret.
Emulator PRNG fencing. The emulator uses /dev/urandom, or under USE_INSECURE_PRNG the deterministic LCG in crypto/rand_insecure.c (for reproducible tests). USE_INSECURE_PRNG is defined only in emulator build scripts (core/SConscript.unix:120); device firmware builds never define it, and the file carries a compile-time #pragma message("NOT SUITABLE FOR PRODUCTION USE! ...").
4.2 Legacy firmware (Trezor One)
Internal entropy: always 32 bytes from
random_buffer()regardless of strength (legacy/firmware/reset.c:37, reset.c:99), sampled before EntropyAck and committed first in entropy-check mode.Mixing:
SHA-256(int_entropy[32] ‖ ext_entropy[0..128])→mnemonic_from_data(secret, strength/8)(reset.c:117-124), withmemzerocleanup of secret, entropy, and mnemonic buffers on finish/abort.STM32F2 TRNG driver (legacy/rng.c:26-47): same SECS/CECS/DRDY gate and consecutive-word repeat test as core. The first word after enabling the peripheral is discarded, and the counter is re-primed at the bootloader→firmware boundary (legacy/setup.c:77-83, 122-131) per the reference manual.
Entropy-check protocol supported since 1.13.1; the temporary xpub is deliberately never displayed on-device.
Backup display: every mnemonic word is shown twice, each requiring a physical button press (reset.c:197-223).
Weak-PRNG fencing:
rand_insecure.ois linked andUSE_INSECURE_PRNGdefined only underifeq ($(EMULATOR),1)(legacy/Makefile.include:106-109, legacy/firmware/Makefile:85-88). A hardware build that somehow defined it would fail to link (duplicaterandom_buffersymbol fromlegacy/rng.c) — the configurations are mutually exclusive by construction, not merely by convention. This is precisely the guard whose absence sank Coldcard.
4.3 Entropy → mnemonic conversion
BIP-39 (crypto/bip39.c:56-88,
mnemonic_from_data): verified conforming — accepts only 16/20/24/28/32 bytes; checksum is the firstENT/32bits of SHA-256(entropy); 11-bit big-endian slices index the English wordlist; exactlyENT + ENT/32bits are consumed with no truncation or off-by-one; working buffers are memzeroed. The mnemonic losslessly encodes the input entropy.The historical
mnemonic_generate()footgun (mnemonic straight fromrandom_buffer, bypassing host mixing) no longer exists in the tree — a positive change versus older audits.SLIP-39 (core/src/trezor/crypto/slip39.py): Shamir share randomness uses the strong RNG path (
random.bytes(n, True)); shares are bound to the secret with an HMAC-SHA256 digest share; mnemonics carry an RS1024 checksum. The host-side entropy check independently reimplements the SLIP-39 master-secret cipher (4-round Feistel, PBKDF2-HMAC-SHA256) and verifies it end-to-end.
5. Findings — host side
5.1 Trezor Suite / @trezor/connect
Host entropy:
generateEntropy(32)→randomBytesfrom@noble/hashes, a thin wrapper over the platform WebCrypto CSPRNG (crypto.getRandomValues). It throws if no CSPRNG is available — there is no weak fallback (verifyEntropy.ts:1-16, resetDevice.ts:59-66).Entropy check: on by default (
entropy_checkdefaults totrue), with full independent re-derivation — commitment verification,SHA-256(int‖ext), BIP-39 or SLIP-39 seed, BIP-32 xpub comparison (verifyEntropy.ts:124-174). On firmware without the capability (below 2.8.7 / 1.13.1) it silently falls back to the unverified legacy flow.Continuity: Suite stores SHA-256 hashes of the verified xpubs per device and re-checks them during every account discovery for the standard wallet. A mismatch currently only logs an error (explicit TODO in discoverAccounts.ts:300-314 to make it fatal); check failures at setup are reported to Sentry.
5.2 trezorlib (python-trezor)
Host entropy:
secrets.token_bytes(32)(OS CSPRNG, no weak fallback) (device.py:323-324). The generator is injectable, which is how tests supply deterministic entropy — an intentional test hook, not a production path.Entropy check: same protocol, 2–8 rounds, identical verification math to connect (device.py:455-564).
6. Coldcard failure-mode comparison and residual observations
Coldcard failure modeTrezor status(A) Fallback PRNG silently linked into production (#ifndef guard bug)Not possible as structured. Weak PRNG is emulator-only, gated by a macro defined solely in emulator build configs, announced by a compile-time pragma, and duplicate-symbol link failure makes device+weak-PRNG builds mutually exclusive. Core’s strong path additionally has no software fallback at all — it either gets hardware randomness or raises.(B) RNG under-delivering entropy / failing silentlyFail-closed everywhere. SECS/CECS/DRDY gating + consecutive-word repeat test on all MCU TRNG drivers; partial fills impossible; SE failures raise exceptions. Failure presents as a hang or error, never as weak seed material.(C) Combination logic weakening a sourceNot present. Concatenate-then-hash (never XOR with attacker-influenced data); device entropy fixed and committed before host input; full 256-bit device contribution independent of host behavior. Verified identical on device and both host re-implementations.(D) Single entropy source, no external auditabilityStructurally avoided. Two-party entropy (device TRNG(s) + host CSPRNG) plus a default-on commit–reveal audit protocol with independent host-side re-derivation. Dual hardware TRNGs (MCU ⊕ secure element) on all current Safe models.
Residual observations (hardening notes, none a vulnerability on its own):
Health-test depth. The only software health test on the MCU TRNG is the 32-bit consecutive-repeat check — no SP800-90B startup, repetition-count-over-window, or adaptive-proportion tests. A subtly biased (correlated-but-nonrepeating) oscillator would pass. Mitigations: ST’s internal conditioning, the SE XOR on Safe models, and SHA-256 conditioning in the seed path. Model T, lacking a secure element, leans most heavily on the two-party mixing here.
U5 RNG configuration relies on peripheral reset defaults (
RNG->CR = RNG_CR_RNGENonly); a latched seed error hangs the device rather than being recovered/reported. Safe, but not graceful.No length/quality enforcement on
EntropyAck.entropy— the device accepts even empty host entropy (harmless given the construction, but the documented “32 bytes” is not enforced device-side).Suite’s post-setup xpub continuity check is log-only (mismatch doesn’t yet block the wallet), and it skips passphrase wallets and non-Bitcoin-like accounts. The setup-time check is the enforced one.
Fencing is structural, not runtime-attested. The emulator-PRNG exclusion rests on build-system structure and link-time symbol collision. That’s currently airtight, but Coldcard’s incident argues for an explicit build-time assertion as well (theirs now verifies the
rng_get()symbol source). A refactor oflegacy/Makefile.include/ SConscript emulator blocks should preserve the exclusivity.A fully compromised host plus a silently weak device RNG together defeat both defenses — the check verifies honesty of derivation, not entropy quality. Only an independent verifier (e.g. running trezorlib’s entropy check from a separate machine at setup) reduces this further. This is inherent to any such design.
7. What this means for you as a Trezor owner
No action required based on this analysis: no Coldcard-analogous defect exists in the current tree, and the architecture means a device-side RNG defect alone would not have produced brute-forceable seeds for wallets created via Suite (host entropy is always mixed in).
Wallets created with Trezor Suite on firmware ≥ 2.8.7 (Safe/T) or ≥ 1.13.1 (One) also got the default-on entropy check — the device’s derivation honesty was cryptographically verified at creation.
Wallets created on older firmware still benefited from two-party mixing (that has been the protocol since the beginning); they simply lacked the audit rounds. The mixing alone already rules out the Coldcard scenario for host-initiated setup.
For maximum paranoia on a future wallet: run the setup with the entropy check from an independent host (e.g.
trezorctl/trezorlib on a separate machine), which closes the “compromised host + dishonest device” corner.The Coldcard incident’s real lesson — refactors silently rerouting RNG calls — is mitigated in Trezor’s tree by fail-closed strong-RNG bindings and link-level exclusion of the test PRNG, but it is a class of bug that only continued scrutiny of build systems prevents. Observation 5 above would be a reasonable upstream suggestion.
8. Sources
Press coverage of the ~594 BTC sweep: CryptoSlate, The Crypto Times, Cryptopolitan, KuCoin News
Trezor source: local checkouts of
trezor-firmware(@66205f1b6b, 2026-07-31) andtrezor-suite; all file:line references above point into those trees. Key quoted snippets (_compute_secret_from_entropy,rng_fill_buffer_strong,legacy/rng.c,rand_insecure.c,resetDevice.ts) were verified byte-for-byte against the source during compilation of this report.
Methodology: three parallel source-analysis passes (core firmware + RNG drivers; legacy firmware + shared crypto library; host-side Suite/connect/trezorlib), followed by manual spot-verification of every load-bearing code quote. Static source analysis only — no binaries were disassembled and no hardware RNG output was statistically tested.

