Skip to content
06

The bug

A preprocessor guard checked whether the RNG macro existed, not whether it was enabled. The build passed and used a predictable fallback.

The faulty guard

// board config: macro defined, as zero
#define MICROPY_HW_ENABLE_RNG (0)

// faulty guard
#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif

// correct check
#if !MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif

The guard passed, so rng_get() resolved to the Yasmarang software fallback rather than the hardware RNG. Both carry the same signature, so nothing at the call site distinguished them. Its seed inputs were not secret.

Seed search space

Bit figures follow Block's derivation, which assumes the device UID is known.

Intended — BIP-39 minimum128 bits

12-word floor; COLDCARD's default 24-word seed is 256 bits

Mk4 / Q / Mk5 — UID known, timers unknown73.3 bits

Upper bound over unknown timer states

Mk3 — UID known, timers unknown40.7 bits

Upper bound over unknown timer states

Mk4 / Q / Mk5 — reseed succeeded32 bits

Only 4 bytes reach reseed()

Mk3 — only SysTick unknown16.3 bits

UID and RTC known; about 80,000 reachable SysTick states

Mk3 — UID and timers both known0 bits

The same inputs reproduce the same seed

  1. 1Enumerate states
  2. 2Derive seeds
  3. 3Derive addresses
  4. 4Check balances