COLDCARD vulnerability what happened, and what to do
Informational only, and this site never asks for your recovery words. details

Informational only. This is independent analysis and an evidence-backed explainer, not financial, security or legal advice, and not a substitute for professional advice about your own situation. It is not affiliated with, endorsed by, or speaking for Coinkite, Block, or any other party named here. Published estimates are attributed, and differing scenarios are kept separate with their assumptions. Act on your own judgement. Editorial standards and corrections.

Do not disclose recovery material to a website, form, message or support account. This site never asks for it. Deliberate recovery on independently verified offline equipment is a separate operation. Seed-word safety.

Mk3 bounded proof README at e17d833b

threez-mk3-rng-disclosure-pinned

https://raw.githubusercontent.com/3z/coldcard-mk3-rng-disclosure/e17d833bc02371ef779e66e25a78c755e57039ef/README.md

Organisation
3z
Evidence role
Repository commit
Published
2026-07-31
Source changes
0
Detected differences
0
Unreviewed
0
Copies held
1

Immutable README revision used by the fixed synthetic Mk3 regression vector. The separate main-branch source remains registered for change detection.

Every check is recorded, including checks that found no text change. A detected edit is therefore bounded between two checks. The publisher's exact save time is not observable from this record. Last checked 2 Aug 2026, 00:58 UTC.

  1. Earliest copy held Current
    seen 1 Aug 2026, 06:39 UTC · Captured here text sha256 73f88d222d10b1d6e72f104c 10,793 chars
    Extracted text as captured
    # COLDCARD Mk3 RNG Vulnerability — Analysis & Proof of Concept
    > **Independent analysis of a publicly disclosed, already-fixed vulnerability.**
    > The defect was announced by Coinkite in their
    > [Mk3 seed generation advisory](https://blog.coinkite.com/coldcard-mk3-seed-generation-warning/)
    > and fixed in firmware 4.2.0. This repository contains a binary-verified
    > reverse engineering of the bug, plus a bounded PoC with no
    > wallet-recovery or fund-targeting capability.
    | | |
    |---|---|
    | **Affected** | COLDCARD Mk3, firmware 4.0.1 – 4.1.9 (inclusive) |
    | **Fixed** | 4.2.0, commit `4543629941a83a3e2788ac06a12b208338cb8314` (`v4-legacy`) |
    | **Class** | Weak RNG / insufficient entropy in wallet seed generation |
    | **Analyzed builds** | 4.1.9 (vulnerable, reproducibly rebuilt) and 4.2.0 (fixed control) |
    ## TL;DR
    On the Mk3, the code that generates new wallet seeds called a "random bytes"
    function that — due to a linker-level mistake — resolved to MicroPython's
    **deterministic software PRNG** (Yasmarang) instead of the STM32 hardware RNG.
    The PRNG was seeded once from the device unique-ID word, the SysTick phase,
    and raw RTC registers. Every Mk3 wallet seed generated on affected firmware is
    therefore the output of a deterministic function of a small, partially-knowable
    state — not 256 bits of hardware entropy. Hashing the output with SHA-256 did
    not add entropy, and the firmware's sanity checks (repeated-word check,
    byte-diversity check) cannot distinguish a PRNG from a real RNG.
    ## 1. How seed generation was supposed to work
    When you pick **New Wallet** on a COLDCARD, the firmware (MicroPython) runs
    `shared/seed.py`:
    ```python
    seed = random.bytes(32)          # 32 bytes of "randomness"
    assert len(set(seed)) > 4        # sanity check: >4 distinct byte values
    seed = ngu.hash.sha256s(seed)    # compress to the final 32-byte seed
    ```
    That seed becomes a 24-word BIP39 phrase, from which all wallet keys derive.
    `random.bytes` was an alias for `ngu.random.bytes` (libngu), which fills its
    buffer by repeatedly calling a global C function: **`rng_get()`**.
    Everything hinges on one question: *which `rng_get()` ends up in the binary?*
    ## 2. Two RNGs, one symbol — the linker decides
    The Mk3 hardware (STM32L475) has a true hardware RNG. The board code in
    `stm32/COLDCARD/rng.c` reads it — but as a **`static` function**,
    `rng_get_or_fault()`. Static means: invisible to the linker outside that file.
    Meanwhile, the Mk3 build deliberately set `MICROPY_HW_ENABLE_RNG = 0`. That

    Excerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.

How to check this yourself

Each copy above is identified by the SHA-256 of its extracted text, shown beside it, and the diffs are plain unified diffs. To verify a quotation, compare it against the page itself or against the Internet Archive's copies, which are independent of this project.

Complete captures are held offline rather than mirrored here, so this page shows diffs and excerpts. If a quotation is ever disputed, the full copy can be produced. Ask.