Coldcard Incident

COLDCARD Incident

Incident Summary#

On July 30, 2026, Coinkite disclosed a vulnerability after unauthorized wallet drains had been observed since July 29. COLDCARD had been vulnerable in seed generation on affected firmware since the March 2021 libNgU migration, including Mk2, Mk3, Mk4, Mk5, and Q.1

The initial major sweep stole 1,082.65 BTC from 1,196 addresses in 41 minutes.2 Galaxy Research later reported a total of 1,367.05 BTC (~$88.6M at the time) across 4,585 addresses.3 A single wallet can control multiple Bitcoin addresses, so this does not mean 4,585 wallets were affected. The final victim scope has not been confirmed, and the investigation remains ongoing.

Fixed firmware has been released, but existing affected seeds remain compromised and must be replaced.1

Normal Flow#

Scope: The COLDCARD-generated New Wallet path on affected Mk2/Mk3 v4 firmware. Mk4/Q/Mk5, dice-added entropy, and imported seeds are out of scope.

Coinkite: Build Configuration#

COLDCARD was built with MICROPY_HW_ENABLE_RNG = 0. Its custom hardware RNG wrapper exposed ckcc.rng_bytes(), random_buffer(), and random32(), but did not provide the global rng_get() required by libNgU. libNgU checked only whether MICROPY_HW_ENABLE_RNG was defined—not whether its value was enabled—and declared extern rng_get(). During symbol resolution, that reference bound to MicroPython's global rng_get(), making ngu.random use the software Yasmarang fallback rather than the STM32 hardware TRNG.

User: Wallet Generation Flow#

Notation: Rectangle = entity / data, rounded rectangle = function, diamond = conditional branch.

Unable to render diagram.

※ The 1 branch is not compiled because the build sets MICROPY_HW_ENABLE_RNG = 0.

  • UID_low32: Lower 32 bits of the STM32 unique ID. Fixed per device.
  • SysTick→VAL: Current value of the CPU periodic counter. Depends on execution timing.
  • RTC→TR: Real-time clock time register.
  • RTC→SSR: Real-time clock subsecond register.
  • rng_get() call history: Number and order of rng_get() calls before wallet generation.

Attack Flow#

Unable to render diagram.

Attacker Search Space#

The red-outlined entities in the graph represent unknown inputs for the attacker, which they must enumerate or guess:

  • UID_low32
  • SysTick→VAL
  • RTC→TR
  • RTC→SSR
  • rng_get() call history
  • BIP-39 passphrase, where applicable However, these inputs are not independent entropy sources. The UID is fixed device metadata; SysTick has a bounded state space; RTC time and subsecond values are correlated with each other and with execution timing; and RNG-call history is constrained by the executed code path. A BIP-39 passphrase is optional: if none is configured, the empty string "" is used. Moreover, users may tend to choose a short, common, patterned, or reused passphrase that adds only a guessable search space because the device is a cold wallet (they might think it is inherently safe!).

Consequently, the effective attacker search space can be much smaller than the nominal BIP-39 entropy space. For Mk2/Mk3 v4, Block estimates a broad upper bound of < 2^40.7 candidates when UID and RNG-call history are known. Coinkite independently estimates the effective search space at approximately 40 bits under its current attack assumptions.

Block explicitly notes that exploitability has not been fully benchmarked.

Root Causes#

  1. Software PRNG Used Instead of Hardware RNG: There were cases where a general-purpose software PRNG was needed for MCUs (microcontroller units) that did not have a hardware RNG.4 COLDCARD was originally designed so that this software PRNG would never be used for wallet seed generation, but after the libNgU integration in 2021, the rng_get() symbol was mistakenly bound to that backend.
  2. Non-Cryptographic PRNG: The fallback PRNG was not designed to provide cryptographically secure entropy. MicroPython itself explicitly commented that Yasmarang was "not really ideal".5

Defensive Opportunities#

  1. Validate MICROPY_HW_ENABLE_RNG Check the macro value, not just whether it is defined. If a hardware RNG is required, MICROPY_HW_ENABLE_RNG == 0 should trigger a build error and be enforced in CI.
  2. Guarantee rng_get() Linkage Verify at build time that rng_get() resolves to the board-specific implementation. Separate cryptographic and general-purpose RNG symbols.
  3. Validate RNG Source and Health Confirm that seed generation actually depends on the hardware RNG, and fail on RNG faults. Use hardware health tests in addition to source-path verification.
  4. Remove Fallback PRNGs from Cryptographic Paths Exclude non-cryptographic PRNG backends from production wallet builds. Test the complete seed-to-entropy call path in CI and audits.
  5. Define a Security Requirement / Entropy Contract Explicitly define the required entropy source and security level for wallet seed generation, including that no non-cryptographic fallback is allowed.6
  6. Encourage Strong BIP-39 Passphrases Clearly communicate the additional protection provided by a strong, unique passphrase and the limitations of an empty or weak passphrase.

Sources#

Footnotes#

  1. [https://blog.coinkite.com/entropy-technical-backgrounder/] 2

  2. [https://www.theblock.co/post/410332/bitcoin-losses-linked-coldcard-vulnerability-70-million-galaxy-research]

  3. [https://x.com/glxyresearch/status/2083623500183421043]

  4. [https://github.com/micropython/micropython/pull/3379]

  5. [https://github.com/Coldcard/micropython/blob/4107246f8a080807b62c3b4838e71e812ea68b6f/ports/stm32/rng.c#L67]

  6. [https://wizardsardine.com/blog/coldcard-rng-vulnerability/]