When the Math Fails: Inside the COLDCARD Entropy Vulnerability
A technical deep dive into the RNG bug that presumably weakened Bitcoin self-custody — and why self-custody remains non-negotiable
Contents
What Happened
On July 30, 2026, Coinkite published a security advisory warning that a bug in COLDCARD firmware had weakened the entropy of seed phrases generated on affected devices. Block’s Bitcoin Engineering and Security team simultaneously published a technical deep dive root-causing the vulnerability.
Block’s report includes a sobering statement: “active exploitation is under way.” Users have lost Bitcoin. This is not theoretical.
The root cause: a subtle preprocessor directive bug caused the wallet seed generation path to use a non-cryptographic software PRNG called Yasmarang instead of the STM32 hardware True Random Number Generator (TRNG) that COLDCARD was designed to use.
The Analogy
Imagine your house key is not cut from a random blank, but generated from two pieces of information: the serial number printed on your door lock (visible to anyone who walks up to your door) and the exact millisecond you first turned the key in that lock.
In the COLDCARD bug, the “serial number on the door” is the chip’s UID — a factory-set identifier burned into every STM32 microcontroller at manufacture. It’s fixed for the life of the chip, readable from memory, and partly transformed into COLDCARD’s USB serial number. The “millisecond you first turned the key” is the SysTick counter — a hardware timer that counts down from a fixed value every clock cycle and wraps around every millisecond. At the moment Yasmarang initializes, it grabs the current SysTick value and XORs it with the UID.
If someone knows both those values — or can narrow them down to a small enough range — they can fabricate an identical key. They don’t need to break any encryption. They just need to replay the same arithmetic the device performed. That’s what happened here, but for Bitcoin private keys.
Yasmarang: When a Toy PRNG Replaces a Hardware TRNG
The source code shows that MicroPython includes a software fallback PRNG for MCUs without a hardware RNG. It’s called Yasmarang, and it was never intended for cryptographic use.
The Algorithm
Yasmarang’s internal state is just four variables: pad (32-bit), n (32-bit), d (32-bit), and dat (8-bit). It’s initialized once:
pad = *(uint32_t*)MP_HAL_UNIQUE_ID_ADDRESS ^ SysTick->VAL; // chip UID XOR timer n = RTC->TR; // RTC time register d = RTC->SSR; // RTC subsecond register dat = 0; // Every subsequent call: pad += dat + d * n; pad = (pad << 3) + (pad >> 29); // 3-bit rotation n = pad | 2; d ^= (pad << 31) + (pad >> 1); dat ^= (char)pad ^ (d >> 8) ^ 1; return pad ^ (d << 5) ^ (pad >> 18) ^ (dat << 1); // 32-bit output
Why This Is Catastrophic
1. Tiny, non-cryptographic state. The initial state comes from the chip’s UID (a factory identifier, not a secret), the SysTick counter (~80,000 possible values on Mk2/Mk3), and the RTC registers (correlated with boot time, potentially static on cold boot). That’s not entropy — it’s device metadata and timing.
2. No reseeding. After initialization, no new entropy is ever mixed in. Every output is a deterministic function of the initial state. Know the state → reproduce the entire stream.
3. Non-cryptographic operations. Shifts, XORs, and additions are not a cipher. A CSPRNG like HMAC-DRBG would resist prediction even with partial state knowledge. Yasmarang offers no such guarantee.
4. SHA256d at the end doesn’t help. Current wallet generation hashes the 32-byte RNG output with SHA256d. But deterministic hashing cannot increase the number of possible inputs. If there are only 232 possible RNG outputs, there are at most 232 possible seeds, regardless of hashing.
| Property | CSPRNG (e.g. HMAC-DRBG) | Yasmarang |
|---|---|---|
| Internal state | 256+ bits | ~104 bits (non-random) |
| Entropy source | Hardware TRNG, periodic reseed | UID + timer, once |
| Prediction resistance | Computationally infeasible | Trivial if state is known |
| Reseeding | Periodic from TRNG | Never (or 32 bits on Mk4) |
| Standard | NIST SP 800-90A | None |
The #ifndef vs #if Bug
The regression entered in March 2021, when COLDCARD migrated its elliptic-curve operations to Bitcoin Core’s libsecp256k1. Wallet seed generation moved from ckcc.rng_bytes() (which used the hardware TRNG) to ngu.random.bytes() (which resolved to Yasmarang).
The Preprocessor Bug
Libngu’s code checked for hardware RNG availability with:
#ifndef MICROPY_HW_ENABLE_RNG #error "get a HW TRNG plz" #endif
#ifndef tests whether the macro is defined — not whether its value is nonzero. The COLDCARD board config defined it as:
#define MICROPY_HW_ENABLE_RNG (0)
The macro is defined — its value is 0. #ifndef sees it as “defined” and does not trigger the #error. The build succeeds. But because the value is zero, MicroPython compiles its Yasmarang fallback instead of the hardware RNG.
The Correct Check
#if !MICROPY_HW_ENABLE_RNG #error "get a HW TRNG plz" #endif
#if evaluates the value. If it’s 0, !0 is true, and the #error fires. The build fails. The developer notices.
Why This Pattern Is Dangerous Beyond COLDCARD
This is not a Coinkite-specific mistake. It’s a pattern that affects any C/C++ codebase that mixes #ifdef checks with numeric macro definitions:
| Macro definition | #ifdef / #ifndef |
#if |
Detects value 0? |
|---|---|---|---|
#define FOO |
defined | #if FOO → 0 (false) |
N/A |
#define FOO 1 |
defined | #if FOO → 1 (true) |
Yes |
#define FOO 0 |
defined | #if FOO → 0 (false) |
#ifndef fails here |
In embedded C, it’s common to define features as 0 or 1 rather than undefined/defined. If a library checks with #ifdef while the board config defines as 0, the guard silently passes.
Lessons for Code Review
- Never mix
#ifdefwith numeric values. If you define as0/1, always use#if. If you want existence checks, use#ifdef. - Establish a project-wide convention. Either everything is boolean (
#define FOO 0/1, check with#if FOO) or existential (#define FOOor nothing, check with#ifdef FOO). - Add compile-time symbol verification. The fix in COLDCARD’s hotfail now explicitly excludes MicroPython’s fallback object and adds a build-time check that fails unless the board-specific object defines
rng_get()and the fallback defines no symbols. - Verify reachability end-to-end, not just presence. The TRNG code was present in the binary — it just wasn’t being called from the seed generation path. Existing review confirmed the code was there but never verified which
rng_get()implementation the call path actually reached.
This bug belongs to a broader category that has caused some of the worst vulnerabilities in history:
- Heartbleed (OpenSSL, 2014): the bounds check existed in a branch that wasn’t taken
- goto fail (Apple, 2014): a duplicated
gotoskipped certificate validation - COLDCARD (2026): the TRNG was in the binary but symbol resolution picked the fallback
The common lesson: presence of safe code in the binary does not prove the runtime uses it. Verification must be end-to-end reachability, not just presence.
Search Space: How Many Candidates?
Block’s analysis provides detailed search-space estimates for each affected device:
Mk2/Mk3 v4.0.0–4.1.9
These devices have no cryptographic reseed. If the attacker knows the device UID, timer state, and RNG call history:
- Known timers: 20 candidates — deterministic. The seed can be reproduced exactly.
- Unknown SysTick (known UID): ~80,000 values ≈ 216.3
- Completely unknown
UID_low32 XOR SysTick: at most 232 - Broad upper bound (all timers independent): ~240.7
How Could an Attacker Obtain the UID, Timer State, and Call History?
None of these values are cryptographic secrets. They can be obtained or constrained through several realistic channels:
- Device UID (32-bit low word): The STM32 UID is a factory identifier, not a secret. It’s readable from the chip’s memory and is partly transformed into the COLDCARD’s USB serial number. If the attacker ever had physical access to the device — even briefly — or if the victim posted a photo of the device showing its serial number, or if the serial was logged by a compromised USB host, the low 32 bits of the UID may be recoverable. Even without the exact value, the attacker may be able to narrow it to a small range based on the device’s manufacturing batch.
- SysTick value: SysTick is a periodic down-counter that wraps every millisecond. On Mk2/Mk3 it has ~80,000 possible values. But the attacker doesn’t need to try all 80,000 — they can profile the boot sequence on their own identical device. Firmware boot takes a roughly predictable number of clock cycles before the first RNG call occurs. By measuring this on an attacker-owned Coldcard of the same model, they can narrow the SysTick window to a much smaller range. If the victim’s RTC was static or zero during cold boot (which Block’s analysis suggests is likely on Mk2/Mk3), SysTick becomes the primary variable, and 80,000 candidates is a trivial search.
- RNG call history: The number of times
rng_get()was called before wallet seed generation determines the position in the Yasmarang stream. In practice, the boot sequence is deterministic — the same firmware performs the same initialization steps in the same order. The attacker can reproduce this on their own device and determine the exact call count before seed generation. If the victim performed any additional RNG-consuming operations (generating a paper wallet, dice rolls that still called the PRNG, etc.), the attacker may need to enumerate a few plausible offsets, but this multiplies the search space by a small constant, not an exponential factor.
The key insight: none of these values are secrets. They are device metadata and timing information. The attacker doesn’t need to break any encryption — they just need to replay the same arithmetic the device performed at boot time.
Mk4/Q/Mk5
These devices add a secure-element reseed at boot, but only 32 bits of entropy reach the PRNG state:
- Known fallback state + call history: at most 232 securely distinguished output streams
- Average enumeration: ~231 candidate trials
- Loose upper bound (all timers independent + reseed): ~273.3
How a Public Address Becomes a Validation Oracle
The attack doesn’t require access to the victim’s device or private keys. It only requires one piece of public information: any Bitcoin address known to belong to the victim’s wallet.
Here’s why: every Bitcoin address is derived from a public key, which is derived from the seed through BIP-32 derivation paths. Given a candidate seed, the attacker can deterministically derive the same sequence of addresses the wallet would produce. If any derived address matches a known address of the victim, the candidate seed is correct.
Where do these known addresses come from?
- On-chain transactions: Every Bitcoin transaction is public. If the victim has ever received or sent Bitcoin, their addresses are permanently visible on the blockchain. An attacker monitoring the blockchain can collect addresses associated with a specific xpub.
- Exchange deposit addresses: If the victim sent Bitcoin from their COLDCARD to an exchange, the sending address is on-chain and traceable.
Once the attacker has a single known address, the attack is pure computation: generate candidate seed → derive BIP-32 path → derive address → compare. Repeat 232 times for Mk4/Q/Mk5, or as few as 80,000 times for Mk2/Mk3 with known timers. The oracle never touches the victim’s device.
Summary Table
| Device / Firmware | Attacker knows timers | Best-case hidden-timer ceiling |
|---|---|---|
| Mk1; Mk2/Mk3 through v3.2.2 | ~2256 | ~2256 |
| Mk2/Mk3 v4.0.0–4.1.9 | 20 | <240.7 |
| Mk4/Q/Mk5 (successful reseed) | ≤232 | <273.3 |
| Mk4/Q/Mk5 (no reseed)* | 20 | <241.3 |
* Conditional source path; ordinary production secure-element failure appears to halt rather than continue.
Affected Devices — Complete Table
| Device | Firmware at seed generation | Status |
|---|---|---|
| Mk1 | All through v3.0.6 | Not affected (outside regression) |
| Mk2 | Through v3.2.2 | Not affected (uses hardware TRNG) |
| Mk2 | v4.0.0–4.1.9 | Affected — no secure reseed |
| Mk3 | Through v3.2.2 | Not affected (uses hardware TRNG) |
| Mk3 | v4.0.0–4.1.9 | Affected — no secure reseed |
| Mk4 | Production v5.0.0 onward (before 5.6.0) | Affected — 32-bit reseed |
| Q | All production (before 1.5.0Q) | Affected — 32-bit reseed |
| Mk5 | All production (before 5.6.0) | Affected — 32-bit reseed |
| TAPSIGNER / OPENDIME / SATSCARD | All | Not affected (different codebase) |
Fixed Firmware Versions
| Model | Fixed firmware |
|---|---|
| Mk3 | 4.2.0 or later |
| Mk4/Mk5 (Standard) | 5.6.0 or later |
| Mk4/Mk5 (Edge) | 6.6.0X or later |
| Q (Standard) | 1.5.0Q or later |
| Q (Edge) | 6.6.0QX or later |
Technical Timeline
- January 28, 2021: The vulnerable libngu STM32 guard exists.
- March 1, 2021: COLDCARD migrates wallet generation to libngu.
- March 17, 2021: Firmware v4.0.0 includes the vulnerable path.
- March 11, 2022: The 32-bit reseed API and Mk4 boot reseeding are added.
- March 14, 2022: First production Mk4 v5.0.0 includes the reseed.
- July 30, 2026: Block and other researchers notice reports of COLDCARD users losing funds. Root cause found.
- July 31, 2026: Fixed firmware available for all affected models.
- August 3, 2026: Community reports that the hotfix introduces a TRNG recovery bug that can brick devices. PR #692 and #693 proposed.
AI as a Double-Edged Sword
Coinkite’s firmware has always been open source. Block’s analysis assumes that someone used AI to review the publicly available firmware code and discovered the bug before Coinkite did.
The most revealing detail: Coinkite themselves used one of the best available AI models to review their code for security issues — and it did not find this bug.
“Both attackers and defenders have the same AI tools, but today it did not help us, and only helped the bad guys.”
— Coinkite
This is “Don’t trust, verify” in its rawest form. Open-source code is not magic — it requires active verification, not passive transparency. The code was public for over five years. The bug was there the entire time. It took an attacker with AI tools to find it.
The Hotfix That Bricks: A New Bug in the Fix
On July 31, 2026, Coinkite released the hotfix firmware that replaced Yasmarang with the hardware TRNG. The fix was correct in direction — rng_get() now resolves to the board’s hardware TRNG instead of the software fallback. But it introduced a new bug that can brick the device permanently.
What Happens
The STM32 hardware TRNG can experience transient “seed errors” — temporary glitches caused by voltage fluctuations, temperature changes, or electrical noise on the chip. These are normal hardware events, not permanent failures. The reference manual (RM0432 §25.3.7) documents the recovery procedure: clear the SEIS error flag, then toggle RNGEN off and on.
The hotfix code does not implement this recovery. When a seed error occurs:
- The
SEISflag latches andDRDYstops asserting RNGENstays set, sorng_init()considers the peripheral “fine” and does nothing- Every subsequent call to
rng_get()times out after 10ms and raisesOSError(EFAULT) - This persists forever — across reboots of the Python layer
Why It Bricks the Device
After the hotfix, rng_get() is called from the keypad scan-order shuffle — the anti-Tempest routine that randomizes which order keys are scanned. This runs from an interrupt callback at up to 60 Hz, before login. Each keypress triggers 3+ TRNG reads.
If any of those reads hits the faulted TRNG state:
OSErrorpropagates todie_with_debug→show_fatal_error+show_logout- The user sees a fatal error screen
- They cannot enter their PIN
- They cannot reach the upgrade menu
- The device is bricked
This matches real user reports of Coldcards getting “stuck on error screen / won’t boot” after installing the hotfix.
The Irony
Before the hotfix, Yasmarang (the software PRNG) could not fail — it always produced output, even though it was weak. After the hotfix, the hardware TRNG can fail transiently, but the code has no recovery path. The fix replaced a generator that never crashed with one that can brick the device on a random hardware glitch.
Current Status
Two community PRs address this:
- PR #692 (Silexperience210): proposes
rng_reset()to clear error flags and re-enable the peripheral, with 3 retry attempts before failing. Also wraps the keypad shuffle intry/exceptso an RNG fault degrades anti-Tempest hygiene instead of bricking the login. Preventive only — cannot recover already-bricked devices. - PR #693 (scgbckbone, Coldcard collaborator): an alternative fix. The collaborator said “I think I have something better here.”
Neither PR has been merged yet. If you have not installed the hotfix, wait for a firmware that includes the TRNG recovery fix. If you have already installed it and your device works, avoid generating new seeds with the device’s TRNG — use the dice-only method described below.
The Dice-Only Workaround
If you need to generate a new seed and your only option is a Coldcard, you can bypass both bugs entirely:
- On your Coldcard, go to:
New Wallet → Advanced → Dice Rolls - Roll a physical six-sided die 99 times minimum (ideally 100+)
- Enter each roll manually into the device
- The Coldcard hashes the dice sequence directly to generate the seed
- This path does not use Yasmarang or the hardware TRNG — the dice are the sole entropy source
With 99+ rolls of a fair die, you get ~256 bits of entropy (≥128 bits with 50+ rolls). This method is officially documented by Coinkite and bypasses both the Yasmarang bug and the hotfix bricking bug. The dice sequence is secret key material — never photograph it, save it digitally, or enter it into a networked computer.
• Have not updated: Generate new seeds with 99+ dice rolls. Wait for a firmware that includes both the Yasmarang fix AND the TRNG recovery fix before updating.
• Updated and device works: Do NOT generate new seeds using the device’s TRNG. If you need a new seed now, use dice-only. Wait for the next stable firmware.
• Updated and device bricked: The current PRs are preventive, not curative. Contact Coinkite support.
• Seed created with 50+ dice rolls: Your seed is SAFE from this vulnerability. No migration needed for this bug.
How to Check if Your Seed Is Affected
Step 1: Identify Your Model and Firmware
On your COLDCARD, navigate to:
Advanced → Upgrade → Current Firmware Info
Note the exact model (Mk2, Mk3, Mk4, Mk5, Q) and firmware version.
Step 2: The Critical Question
The firmware you had when you generated the seed is what matters. Upgrading today does not retroactively repair a seed generated on vulnerable firmware. Compare your device model and the firmware version you had at seed generation against the “Affected Devices — Complete Table” below. If your seed was generated on any model/firmware combination marked as affected, you need to migrate to a new seed.
Step 3: Check the Dice Exception
If you used “Add Dice Rolls” with 50+ independent, private rolls when creating your seed, your seed is not compromised by this bug — the dice alone contribute ≥128 bits of entropy.
Step 4: Check Your Passphrase
A BIP-39 passphrase (not the COLDCARD PIN) adds an independent barrier. If you used a strong, unique passphrase, it provides some protection — but you should still migrate to a new seed.
Step 5: If Affected, Migrate
If your seed is affected and you need to migrate, you have several options:
Option A: Generate a new seed on your Coldcard using dice only
You can create a secure seed even on affected firmware — without updating — using the dice-only method. This bypasses both Yasmarang and the TRNG entirely:
- On your Coldcard, go to:
New Wallet → Advanced → Dice Rolls - Roll a physical six-sided die 99 times minimum (ideally 100+)
- Enter each roll manually into the device
- The Coldcard hashes the dice sequence directly to generate the seed — no RNG involved
- With 99+ rolls you get ~256 bits of entropy (≥128 bits with 50+ rolls)
This method is officially documented by Coinkite. The dice sequence is secret key material — never photograph it, save it digitally, or enter it into a networked computer.
Option B: Move funds to a hardware wallet from another manufacturer
If you have access to a hardware wallet from a different manufacturer that generates entropy properly, you can migrate your funds there:
- Trezor Model T / Safe 5: Uses a hardware TRNG with open-source firmware. You can also add dice entropy during seed generation.
- BitBox02: Uses a hardware TRNG (STM32) with open-source firmware.
- SeedSigner: A DIY air-gapped signer that lets you generate seeds from dice rolls or photos of entropy. Fully open-source.
- Krux: Another DIY air-gapped device that supports dice-based seed generation. Open-source.
Always verify that the device you choose generates entropy from a hardware TRNG, and ideally supports external entropy input (dice). Research the device’s track record and whether its firmware is open-source.
Option C: Temporarily move funds to a hot wallet
If you don’t have access to a second hardware wallet and need to move your funds quickly, you can send them to a hot wallet as a temporary measure:
- Use a reputable mobile or desktop wallet (e.g. Blue Wallet, Electrum, Sparrow Wallet)
- Generate a new seed on the hot wallet — these use the operating system’s CSPRNG, which is cryptographically sound
- Move funds there temporarily until you can acquire a hardware wallet with proper entropy generation
Regardless of which option you choose:
- Record and verify the new backup, wallet fingerprint, and a receive address
- Send a small test transaction and confirm it arrives
- Move the remaining funds
- Keep the old backup until the migration is complete and confirmed
Use a Passphrase — Always
Whether you use a hot wallet or a cold wallet, a BIP-39 passphrase is strongly recommended. A passphrase (not the device PIN) creates a separate wallet layered on top of your seed phrase. Even if an attacker recovers your seed, they cannot access your funds without the passphrase.
- Use a strong, unique passphrase — not a dictionary word, not a quote, not something reused
- Store the passphrase separately from the seed words
- Every passphrase produces a valid wallet — verify the wallet fingerprint before depositing funds
- Losing the passphrase means losing access to that wallet permanently
A passphrase adds an independent layer of security that protects you even if the seed entropy is weak. It’s not a substitute for proper entropy, but it significantly raises the bar for an attacker.
Decision Summary
Seed generated on Mk2/Mk3 v4.0.0–4.1.9? → AFFECTED, migrate Seed generated on Mk4/Mk5/Q before fix? → AFFECTED, migrate Used 50+ dice rolls when creating seed? → SAFE from this vulnerability Seed generated on firmware before v4.0.0? → SAFE Device is TAPSIGNER / OPENDIME / SATSCARD? → NOT AFFECTED MIGRATION OPTIONS (pick one): A) New seed on Coldcard with 99+ dice rolls (works on affected firmware) B) Move to another manufacturer's hardware wallet (Trezor, BitBox02, etc.) C) Temporarily move to a hot wallet (Electrum, Sparrow, Blue Wallet) DO NOT install the July 31 hotfix until a firmware with TRNG recovery fix is released — it can brick your device. ALWAYS use a BIP-39 passphrase, hot or cold wallet.
The Bigger Lesson: Self-Custody Is Still Imperative
Some will use this incident to argue that self-custody is dangerous. It’s exactly the opposite.
Exchanges get hacked, freeze accounts, and collapse — FTX, Celsius, BlockFi are just recent examples. When it’s not your key, it’s not your Bitcoin. This bug was found, publicly documented, and patched — because the code was open source and the community could audit it. Try demanding that level of transparency from a centralized exchange.
Self-custody is imperative. But it requires active responsibility:
- Verify your firmware version
- Consider adding dice entropy when generating seeds
- Maintain verified backups
- Migrate calmly when vulnerabilities are discovered, not with panic
- Use multisig with quorums from independent, secure devices
“Be your own bank” is not a slogan — it’s a commitment to security that no one else can assume for you.
The bug was in the code for over five years. The code was open. Nobody found it until an attacker did. That’s the reality of open-source security: transparency is necessary but not sufficient. It enables verification — it doesn’t replace it.
Self-custody remains the only way to be truly sovereign over your Bitcoin. This incident doesn’t change that. It reinforces it.
References
- Coinkite Security Advisory — Mk3 Seed Generation Warning
- Coinkite Technical Backgrounder — Entropy Issue
- Block Engineering — Predictable RNG Fallback and 32-Bit Reseed in COLDCARD Firmware
- MicroPython fallback RNG source (Coinkite fork)
- Libngu RNG implementation
- Commit that introduced the regression (March 2021)
- LLFOURN: Attack-cost model for affected COLDCARD generations
- PR #692: rng: recover the TRNG after a seed/clock error instead of failing forever
- PR #693: Alternative TRNG recovery fix (scgbckbone)
- Coinkite: Dice-roll seed generation documentation
