Blast radius
A sound mnemonic does not make the device sound. Beyond seed generation, a set of COLDCARD features drew their own secrets from the affected generator, so dice-only and imported seeds are outside only the first of the two failure families below.
Firmware and cryptographic detail.
Two failure families, and only one of them is about your seed
Most incident guidance sorts users by how their mnemonic was produced, because that is what decides whether the wallet keys themselves are enumerable. That question is real and it is the urgent one. It is not the whole of the defect. A second family of features asks the same generator for its own bytes and never consults the mnemonic at all, so the answer to "where did my seed come from" does not reach them.
flowchart TB S["How was the mnemonic produced?"] --> D["Dice-only, imported,<br/>or made before the regression"] S --> A["Generated on affected firmware"] D --> DS["The mnemonic itself is<br/>outside this defect"] A --> AS["The mnemonic and everything<br/>derived from it inherit the defect"] DS --> F["Separately: device features that call<br/>the affected generator without<br/>consulting the mnemonic at all"] AS --> F classDef n-act stroke-width:1.5px classDef n-safe stroke-width:1.5px class A,AS,F n-act class D,DS n-safe
Dice-only, imported and pre-regression mnemonics sit outside the left branch and inside the shared one. The feature list is set out in the register below.
Anyone who used the listed features on affected firmware, including people who rolled 50 or more fair dice, people who imported a mnemonic generated on another device, and people whose wallet keys were never at issue. The exposures differ sharply in consequence: a paper wallet's private key is the generator's output outright, while a randomised PIN keypad only matters to someone watching the screen. The register grades each one separately rather than presenting the list as uniform risk.
ngu.random.bytes() and others using ckcc.rng_bytes(). The affected symbol-resolution path sits behind the first surface. The second reaches the STM32 hardware RNG directly. This distinction identifies which features require further analysis; it does not by itself establish the full security consequence for every feature.
flowchart LR BR["ngu.random.bytes()"] --> M["libngu my_random_bytes"] M --> Y["MicroPython Yasmarang<br/>UID word, SysTick, RTC"] M --> N["libngu Yasmarang<br/>constants or at-most-32-bit reseed"] OK["ckcc.rng_bytes()"] --> T["STM32 hardware TRNG<br/>analogue transistor noise"] Y --> BL["Call sites requiring review:<br/>seeds, paper keys, passwords,<br/>session material and nonces"] N --> BL T --> GL["Backup password, 7z salt,<br/>HSM user secrets,<br/>SD overwrite blocks"] Y -. "the hotfix rebinds this<br/>to the same hardware" .-> T classDef n-act stroke-width:1.5px classDef n-safe stroke-width:1.5px class BR,M,Y,N,BL n-act class OK,T,GL n-safe
Both call surfaces were intended to incorporate the STM32 hardware source.
Before the fix, only the direct ckcc.rng_bytes() surface did so.
The hotfix supplies the hardware-backed symbol used by libngu, which is why
the table distinguishes call paths.
| Secret or material | Source | RNG path before the fix | Scope established here |
|---|---|---|---|
| Master wallet seed | seed.py:605 | Affected API | Confirmed use of the affected generation path. |
| Ephemeral and temporary seeds | seed.py:605 | Affected API | Calls the same generation function; downstream use must be assessed by feature. |
| Paper-wallet private keys | paper.py:94 | Affected API | The device-generated path uses the affected API. The separate pure-dice path at paper.py:191-202 supplies its own private key and does not. |
| Seed XOR split masks | xor_seed.py:78 | Affected API | Mask generation uses the affected API, and only in the optional random-split mode selected at xor_seed.py:49-52. The default deterministic mode does not call it. The information leaked by a particular share set requires separate analysis. |
| Key Teleport ECDH material and password | teleport.py:79,204,229 | Affected API | Random ECDH-related material reaches this path. The receiver password is five bytes, and the source comment at teleport.py:226 states the intended width as 40 bits. Protocol impact has not been independently demonstrated here. |
| Key Teleport co-signer derivation index | multisig.py:1190 | Affected API | The index used to derive a co-signer's receive key is drawn as ngu.random.uniform(1<<28), so its intended width is 28 bits before any generator weakness is considered. |
| Clone and USB session material | backups.py:752,862usb.py:715 | Affected API | Session-related values reach this path. Exploitability depends on transcript access and protocol use. |
| Web2FA secret and nonces | web2fa.py:25,110,145,148 | Affected API | Secret or nonce generation reaches this path. The account-level consequence depends on what an attacker can observe. |
| HSM local confirmation code | hsm.py:857 | Affected API | The confirmation-code call site uses the affected API. |
| HSM policy-approval character | hsm_ux.py:58 | Affected API | The character a user must press to enable a new HSM policy is chosen from a five-item set through the affected API. This is a presence prompt rather than key material. |
| MicroSD 2FA enrolment nonce | pwsave.py:318 | Affected API | The eight-byte nonce written when enrolling an authorised card uses the affected API. What that means at the account level remains untested in this archive. |
| Secure Notes and Passwords random generators | notes.py:58-92 | Affected API | The 12-word, 24-word and dense-password choices call generate_seed(). The mixed-format choice uses ngu.random through random.randbelow() and shuffle(), and its own source comment at notes.py:83 budgets it at 49 bits before this defect is considered. The separate BIP85 choice is classified below. |
| Co-signing policy key C | ccc.py:899 | Affected API | The 12 words the device retains to enforce a spending policy are produced by the same generate_seed() function as a wallet seed. The import paths at ccc.py:870-895 supply words from elsewhere instead. |
| Randomised PIN keypad layout | login.py:32-34 | Affected API | The shuffle uses ngu.random through shared/random.py. This is shoulder-surfing and wear-pattern hardening, not key material, and any consequence requires physical observation. |
| BIP85-derived entropy, mnemonics and passwords | drv_entro.py:57-112 | Deterministic derivation | BIP85 derives child entropy from the active BIP32 root rather than calling the affected RNG again. If that root is recoverable, the same application and index paths can be rederived. An active BIP39 passphrase remains part of the root from which the child is derived. |
| Trick-PIN duress wallets | trick_pins.py:14,80 | Deterministic derivation | Duress wallet secrets come from bip85_derive, and the device's own prompt at trick_pins.py:597 describes indices 1 to 3. They follow the root rather than adding independent randomness. |
| MicroSD 2FA file encryption key | pwsave.py:39-47 | Deterministic derivation | The key is derived from the wallet secret and a card-identity salt, so it follows the seed's provenance. This is separate from the enrolment nonce listed above. |
| MuSig2 session randomness | libngu k1.c:836 | Affected API | The session-randomness call site uses the affected API. Nonce reuse or predictability can be security-critical, but no incident-specific exploit is established here. |
| secp256k1 context blinding | libngu k1.c:110-112 | Affected API | This is side-channel hardening rather than wallet-key generation. Residual protection is not assessed here. |
| Backup file password, 12 words | backups.py:337 | Intact path | Uses ckcc.rng_bytes(), the hardware TRNG. The password path is separate from the provenance of the mnemonic inside the backup. |
| 7z archive salt and IV | compat7z.py:23 | Intact | Hardware TRNG. |
| HSM per-user secrets | users.py:182 | Intact | Hardware TRNG, ten bytes. This is the one HSM secret on the intact path, which is why the HSM rows above are split rather than treated as one finding. |
| SD card overwrite blocks | files.py:65,124 | Intact | Hardware TRNG. |
Restoring the backup restores the same mnemonic. If that mnemonic used affected firmware, installing the hotfix or retaining a correctly generated backup password does not change its generation history. A migration plan should account for both the live wallet and retained backup copies.
Adjudicating the Wizardsardine feature list
On 1 August 2026 Wizardsardine published a long-form post-mortem whose "broken regardless of your seed" section is the most complete public enumeration of this second family captured here. Its author, Kevin Loaec, separately stressed on X that imported and dice-generated seeds are still exposed through those features.
Wizardsardine sells Liana, a wallet product that competes with COLDCARD's software surface, and the post-mortem's first section is directed at Liana users and closes by recommending the company's institutional offering. The author also describes the post as written quickly under stress by three people in parallel and invites corrections. Both facts are disclosed for the same reason this archive discloses Foundation Devices' competing interest around the OPENDIME test: a vendor's analysis of a rival's product is evidence of what the vendor asserts, and is checked before it is repeated.
Each asserted exposure is set against what this archive can itself establish from the pinned firmware and libngu clones. The result is that the mechanism Wizardsardine describes holds at every call site checked, that several of its specific numbers are quotable from the source comments, that one claim needs narrowing, and that its list is not exhaustive.
| Asserted exposure | What this archive establishes | Basis assigned |
|---|---|---|
| Paper wallets. The private key is the generator's output and bears no relation to the seed, except where the dice option was used. | Both halves confirmed. paper.py:94 constructs ngu.secp256k1.keypair() with no argument, and the libngu constructor at k1.c:485-487 fills the secret key from my_random_bytes() in exactly that case. The dice path at paper.py:191-202 passes its own private key in, so it never reaches that branch. | Verified |
| Clone to another COLDCARD. The ephemeral SD-card key exchange is recomputable, so a clone-file holder can decrypt the transfer and recover the seed in clear. | The call sites are confirmed: backups.py:752 and backups.py:862 both create argument-free keypairs, and the second derives the archive encryption key by ecdh_multiply. The onward step, that possession of the clone file is sufficient to reconstruct the session key and recover the mnemonic, is a protocol consequence this archive has not demonstrated. | Verified Reported |
| Encrypted USB session. Same ephemeral key construction, described by the author as not significant. | Confirmed at usb.py:715. The archive agrees the consequence is bounded by transcript access and does not extend it further. | Verified |
| Co-signing key C. The 12 words the device keeps to enforce a spending policy come from the broken generator. | Confirmed. ccc.py:899 calls the same generate_seed() used for a wallet mnemonic. A user who instead imported key C words at ccc.py:870-895 is outside this call site. | Verified |
| Secure Notes password generator, two modes. The word mode shares the seed-generation function; the strict-requirements mode states a 49-bit budget in a code comment. | Both confirmed, including the number. notes.py:66-78 builds the word and dense choices on generate_seed(), and the comment at notes.py:83 reads entropy: 11+11 + (3.8*3) + 16 = 49 bits. That budget is the design intent before this defect, not a measurement of the delivered strength. | Verified |
| Key Teleport, both mechanisms. The seed-transfer password is 40 bits by design; the co-signer exchange relies on a 28-bit derivation index. | Both numbers are quotable from source. teleport.py:226-229 carries the comment "pick an 40 bit password" above ngu.random.bytes(5), and multisig.py:1190 draws the co-signer index as ngu.random.uniform(1<<28). Whether either width is reachable in a live protocol run is not established here. | Verified |
| HSM mode. The second factor's shared secret and the local confirmation codes come from the same generator, so the second factor no longer blocks an attacker and physical presence becomes predictable. | Confirmed for three call sites and narrowed at a fourth. The Web2FA shared secret (web2fa.py:110), the local confirmation code (hsm.py:857) and the policy-approval character (hsm_ux.py:58) all use the affected API. But HSM per-user secrets at users.py:182 use ckcc.rng_bytes(), the intact hardware path. "HSM secrets came from the same generator" is therefore true of the second factor and the confirmation prompts, and not true of the per-user credentials. | Verified Narrowed |
| Randomised PIN keypad. The layout meant to defeat shoulder surfing and screen wear can be reconstructed. | The call site is confirmed at login.py:32-34, which shuffles through shared/random.py and therefore ngu.random.uniform. The author places this low on the priority list because it needs physical observation, and this archive agrees rather than testing reconstruction. | Verified |
| Side-channel masking. The cryptography library's countermeasure is predictable and therefore useless. | The call site is confirmed: ctx_randomize() at libngu k1.c:110-112 fills its 32 blinding bytes from my_random_bytes(). "Therefore useless" is a security conclusion about residual protection that this archive has not assessed, and the same call site is graded as hardening rather than key material in the register above. | Verified Reported |
| Seed XOR, random split mode only. The optional mode calls the broken generator, and the screen presenting it says the split uses the TRNG. | Confirmed including the irony. The prompt string at xor_seed.py:49 reads "If you would prefer a random split using the TRNG, press (2)", and the branch it selects at xor_seed.py:77-79 is the one calling ngu.random.bytes. The default deterministic mode does not. The author's own scoping, that this affects only unaffected seeds split in that mode, matches the code. | Verified |
| Seed-derived material. BIP-85 children, trick-PIN duress wallets and microSD 2FA all fall with the seed. | Confirmed as derivation facts. BIP-85 is set out in the section below. Duress wallets call bip85_derive at trick_pins.py:14,80. The microSD 2FA file key is derived from the wallet secret at pwsave.py:39-47. These follow the seed's provenance, so a sound mnemonic leaves them sound. | Verified |
| Not asserted in the post-mortem. | The list omits the MuSig2 session-randomness call site at libngu k1.c:836 and the microSD 2FA enrolment nonce at pwsave.py:318, both of which this archive classifies on the affected surface. The enumeration should be read as substantially correct rather than complete. | Verified |
BIP85 descendants follow the active root
BIP85
deterministically derives application entropy from a BIP32 master root.
COLDCARD's implementation in
shared/drv_entro.py
derives the selected BIP85 path from the active wallet secret and then
applies the specified HMAC-SHA512 transform. It does not generate an
independent random root for the child wallet.
The consequence is conditional. If an affected active root can be recovered, its BIP85 children can be reproduced when their application and index paths are known or searched. If the active wallet included an undisclosed BIP39 passphrase, that passphrase remains an independent input to the root. A migration inventory should therefore include BIP85-derived wallets rather than treating their different mnemonic words as independent provenance. A separate passphrase later applied to a BIP85-derived mnemonic adds another input whose effect depends on its construction and attacker knowledge; it does not change the mnemonic's deterministic provenance.
Benma and Kevin Loaec each published incident-specific warnings about BIP85-derived wallets.
Where this sits in the record
The commit history behind the affected call surface, including an independent walkthrough by Dustin Dettmer and what this archive could and could not confirm of it, is set out on the developer-response page. The wider set of published analyses, with the commercial interests attached to each, is on the response page. For deciding what to do about a particular device or wallet, use the risk guide rather than this register.