Skip to content
Predictable Rng Fallback And 32 Bit Reseed In Coldcard Firmware

Predictable Rng Fallback And 32 Bit Reseed In Coldcard Firmware

engineering.block.xyz August 8, 2026

Following reports from COLDCARD users, and working alongside other security researchers, Block’s Bitcoin Engineering and Security teams root-caused vulnerabilities that allow for theft of Bitcoin from COLDCARD users. This analysis is based on our current understanding of the situation. We have not done full empirical testing to confirm exploitability. We are publishing this advisory early, because active exploitation is under way. This analysis is the opinion of Block, and based on our internal research; we recommend looking at Coinkite's definitive report once available.

This report does not pertain to Block products; no Block products or customers are affected by this vulnerability.

COLDCARD firmware contains an RNG integration error that causes ngu.random to use MicroPython's deterministic Yasmarang fallback instead of the STM32 hardware RNG.

The production board configuration defines MICROPY_HW_ENABLE_RNG as zero because COLDCARD provides a separate hardware-RNG wrapper . Libngu incorrectly checks whether that macro is defined rather than whether it is enabled . The build therefore succeeds, and libngu binds to MicroPython's rng_get() . With the macro set to zero, that function is a Yasmarang software generator initialized from the MCU UID and timer registers .

The consequences differ by device:

Wallet generation hashes the resulting 32 bytes, but deterministic hashing cannot increase the number of possible seeds.

Exposure depends on the firmware used when a secret was generated, not the device's manufacturing date. Upgrading does not retroactively weaken or repair an existing seed.

An attacker who can determine or sufficiently constrain the device UID, timer state and RNG-call history can reproduce the fallback stream offline.

A wallet xpub, address or generated public key provides a candidate-validation oracle. Successful recovery of a wallet seed or private key permits theft of all associated funds.

For Mk2/Mk3 v4, there is no cryptographically generated secret input to enumerate. For current devices, once the fallback state and call history are fixed, the remaining secure-element-derived space is at most 2^32 , averaging approximately 2^31 candidate trials.

This does not mean every remote attacker can immediately recover every seed. Practical cost depends on available UID information, boot timing, prior RNG calls and derivation cost. No end-to-end brute-force benchmark is claimed here.

If you exported a seed generated in a vulnerable coldcard, moving it to another wallet, then that same insecure seed is still affected.

The relevant board headers contain:

This is the normal compiled board configuration, not an environment variable or unusual build override.

COLDCARD does provide a separate hardware-RNG implementation. Its random_buffer() reads the STM32 RNG peripheral and fails on timeout or repeated samples. Python exposes that implementation as ckcc.rng_bytes .

Libngu selects its STM32 entropy function with:

#ifndef verifies only that the macro exists. It does not reject a macro whose value is zero.

The board-local implementation exports random32() and random_buffer() , not a global rng_get() . Consequently, libngu's reference resolves to MicroPython's implementation.

MicroPython selects between its hardware and software implementations using the macro's value:

Because the value is zero, rng_get() initializes Yasmarang with:

These inputs may differ between boots, but they are not a cryptographic entropy source. The UID is fixed device metadata, while SysTick and RTC are observable or constrainable timing state.

Once those values and the number of prior calls are known, the stream is deterministic.

Libngu maintains a second Yasmarang state initialized with public constants:

Each output word is calculated as:

XOR does not create entropy. If both inputs are reproducible, their XOR is reproducible.

The health check rejects adjacent repeated rng_get() outputs. A deterministic PRNG normally produces different adjacent values, so it passes.

Firmware v3.2.2 generated wallet entropy using:

That path reached ckcc.rng_bytes and the board-local STM32 hardware RNG.

Commit b18723dd , dated March 1, 2021, changed generation to:

shared/random.py mapped this call to ngu.random.bytes . The change first appeared in released firmware v4.0.0 on March 17, 2021.

Mk2/Mk3 v4 do not execute the later Mk4 reseeding code.

Mk4 introduced secure-element reseeding in commit 01cb43f7 :

SE1 returns an authenticated 32-byte value incorporating secure-element unpredictability. SE2 reads eight authenticated bytes from a ROM-options page rather than executing a live random command.

Regardless of the original inputs' quality, only four digest bytes reach reseed() .

The complete reseed implementation is:

For fixed fallback state F and call history T :

Internal state evolution may spread those 32 bits across later output, but it cannot create additional entropy.

Current wallet generation performs:

The distinct-byte assertion detects only trivial failures. Yasmarang easily passes it.

SHA256d may make the output statistically uniform, but it cannot increase the input family:

The BIP39 checksum similarly adds no entropy.

MicroPython initializes its fallback Yasmarang generator once, on the first call to rng_get() :

After initialization, no new entropy is collected. Every subsequent output is a deterministic state transition. See the pinned MicroPython implementation .

The UID is intended to distinguish chips, not to serve as a cryptographic secret. It is fixed for the lifetime of the MCU, readable from memory and partly transformed into COLDCARD's USB serial number.

The STM32 UID is a factory-set manufacturing identifier and is not a cryptographic secret.

Only the first 32-bit UID word participates in the fallback. Even if the complete 96-bit UID is globally unique, that does not make its low word uniformly random or secret. Different devices may also the same low word.

If the low word is known, it adds no space. If it is completely unknown, the expression:

still has at most 2^32 possible results—not 2^(32 + SysTick bits) —because both values collapse into one 32-bit pad .

Testing an affected device under controlled boot conditions may allow an attacker to profile the SysTick, RTC, and RNG-call distributions. Because these values are determined by common hardware and firmware behavior, measurements from an attacker-owned device could prioritize or substantially narrow the candidate states for another device running the same configuration. The degree to which those measurements generalize across units and user workflows requires hardware validation.

SysTick is a counter driven by the processor clock and reloaded every millisecond:

This is a maximum enumeration count, not guaranteed entropy. Knowledge of when the first RNG call occurs can reduce it significantly.

RTC->TR and RTC->SSR represent time and subsecond position. They are correlated with each other and with SysTick, so treating them as independent random variables substantially overstates their strength.

For legacy Mk2/Mk3 startup, the selected RTC oscillator is disabled, strongly suggesting zero or static RTC values on a normal cold boot. Current devices configure an RTC source but label it unused, while MicroPython RTC initialization remains disabled. Exact runtime behavior should be measured on hardware.

These values can create operational uncertainty for an attacker, but none is a cryptographic entropy source. Their important characteristics are:

Therefore, the software fallback may produce output that looks statistically random while remaining reproducible from a relatively small set of device and timing states.

For a fixed UID, timer state and RNG-call history:

Under a normal cold-boot model with a known UID but unknown SysTick:

If the effective 32-bit UID_low32 XOR SysTick value is entirely unknown:

Unknown call history multiplies these counts by the number of plausible execution traces.

For a successful reseed with known fallback state and call history:

A deliberately loose known-UID ceiling can be obtained by treating every timer field as independent:

Including the reseed gives a raw ceiling near 2^73.27 .

This is not 73-bit cryptographic security. The timer fields are correlated, may occupy much smaller ranges, and can potentially be observed or reconstructed.

For Mk2/Mk3 v4, assuming the UID and RNG-call history are known, counting every possible SysTick and RTC state gives a broad upper bound of 2^40.7 candidates. If the RTC is stable during cold boot, the estimate narrows to roughly 2^16.3 , based on SysTick alone.

Current devices also contain a code path that could continue without reseeding after a catchable initialization error. The reviewed source suggests ordinary secure-element failures halt instead, so the no-reseed case is best treated as conditional pending hardware validation.

† Conditional source path; ordinary production secure-element failure appears to halt rather than continue.

Early boot wraps mk4.init0() and q1.init0() in a broad exception handler that continues after catchable errors. A catchable exception before reseeding could therefore leave libngu at its public initial state.

However, current production SE1/SE2 communication and authentication failures normally enter non-returning bootloader failure paths. It is therefore not established that an ordinary production hardware failure silently proceeds without reseeding.

The broad exception remains a dangerous fail-open structure, but it should be treated separately from the proven 32-bit successful-reseed weakness.

The same ngu.random construction is used for:

Callers using ckcc.rng_bytes instead reach the separate STM32 hardware-RNG implementation.

This does not mean every feature has the same severity. It means they ultimately consume the same my_random_bytes() stream, so their security inherits its entropy limits.

A COLDCARD “paper wallet” is a standalone Bitcoin private key and address, separate from the device’s main BIP39 wallet.

Its generation path is:

Unlike normal seed generation, the RNG output is used directly as the secp256k1 private key—there is no BIP39 or BIP32 step. See shared/paper.py:91 and external/libngu/ngu/k1.c:428 .

The public Bitcoin address provides an oracle: an attacker can generate candidate private keys, derive each address, and stop when it matches. Recovering that key compromises funds held by that paper wallet, but not necessarily the COLDCARD’s main wallet.

The paper-wallet “Use Dice” option supplies the private key independently and bypasses this RNG path.

When “random split” is selected, each mask comes from ngu.random.bytes() and is then hashed.

For a two-part split:

If an attacker obtains B , they can enumerate candidate masks, derive candidate original seeds, and validate them against an address or xpub. If they possess only A , that alone still does not reveal the original seed. Multi-part cases similarly depend on which shares are exposed.

The default deterministic split mode does not use ngu.random and is separate from this bug.

Cloning generates ephemeral ECDH private keys on both devices. Their public keys are stored on the MicroSD card, including in the clone filename.

A candidate private key can be checked directly against its public key. Once recovered, it can derive the ECDH session key and potentially decrypt the cloned backup.

USB encryption similarly creates a random device keypair. A captured transcript contains the necessary public keys to test candidates. This threatens that session’s confidentiality; it does not directly disclose the wallet signing key.

Ordinary Key Teleport uses ngu.random to generate temporary ECDH private keys and a five-byte secondary password that protect the transferred secret. If an attacker captures the exchange, the exposed public-key material and encrypted-payload checksums can help them test candidate RNG states. Recovering either temporary private key reveals the shared encryption key; recovering the sender’s RNG state may also reveal the secondary password. The resulting impact is potential decryption of the teleported seed, private key, or other secret.

The multisig-PSBT variant is less directly affected because its ECDH keys are derived from the existing wallet; ngu.random mainly selects a child index. Predicting that index does not reveal the private key when the underlying wallet seed is secure. A wallet originally generated by the affected RNG remains separately vulnerable.

Web2FA uses the RNG for three different values:

Predictable TOTP material could weaken the second factor, while a recoverable ECDH key could expose captured request contents. These require separate protocol-specific analysis.

The password generator uses generate_seed() for its 12-word, 24-word, and dense-password options, and ngu.random.uniform() for its shorter mixed-format option.

Calling generate_seed() twice for the dense option does not double the entropy: both outputs are sequential results from the same small-state generator.

Manually entered and BIP85-derived passwords are not directly affected, assuming the underlying wallet seed was generated safely.

Even if a COLDCARD is used in a multisig arrangement, if the arrangement is composed of exclusively vulnerable devices, then the impact of the vulnerability remains. A quorum of secure devices is necessary to protect against this issue.

Extracted Entities

Attack Types (1)

Platforms (2)