COLDCARD RNG incident the public record, collected and explained
Informational only, and this site never asks for your seed words. details

Informational only. This is an open source collection of what others have published about the incident, together with an explanation of it. It is 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. Material is attributed and quoted as published; where sources disagree their scenarios are kept separate with their assumptions rather than reconciled into one answer. Everything is meant to be checked against the linked evidence rather than taken on trust. Act on your own judgement about a particular situation. Editorial standards and corrections.

Do not disclose recovery material to a website, form, message or support account. This site never asks for it, and contributions containing recovery words or private keys are not accepted.

libngu PR #60 patch

libngu-pr-60-patch

https://github.com/switck/libngu/pull/60.patch

Latest reviewed change

source content difference between and

The published patch series gained a second commit adding a ValueError on an empty seed, a regression test and a comment documenting the generator's roughly 72-bit independent state.

seen +61 -1 full history below
 From a51217432308fd9ebc7c3d54b96a5af72b71b2e9 Mon Sep 17 00:00:00 2001
 From: Ballance <[email protected]>
 Date: Fri, 31 Jul 2026 23:31:52 -0400
-Subject: [PATCH] fix: absorb full-width entropy in random.reseed()
+Subject: [PATCH 1/2] fix: absorb full-width entropy in random.reseed()
 MIME-Version: 1.0
 Content-Type: text/plain; charset=UTF-8
 Content-Transfer-Encoding: 8bit

First lines only. The complete diff is in the timeline below.

Organisation
switck
Evidence role
Repository patch
Published
not established
Source changes
1
Detected differences
1
Unreviewed
0
Copies held
2

Patch content for the open full-width reseed proposal. Kept separately from the conversation and review-state capture.

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 .

  1. source content difference between and Current source content +61 -1

    The published patch series gained a second commit adding a ValueError on an empty seed, a regression test and a comment documenting the generator's roughly 72-bit independent state.

    seen · Captured here 7,567 chars
    What changed from the previous capture 62 lines
     From a51217432308fd9ebc7c3d54b96a5af72b71b2e9 Mon Sep 17 00:00:00 2001
     From: Ballance <[email protected]>
     Date: Fri, 31 Jul 2026 23:31:52 -0400
    -Subject: [PATCH] fix: absorb full-width entropy in random.reseed()
    +Subject: [PATCH 1/2] fix: absorb full-width entropy in random.reseed()
     MIME-Version: 1.0
     Content-Type: text/plain; charset=UTF-8
     Content-Transfer-Encoding: 8bit
     +    }
          return mp_const_none;
      }
    +From 6766258eae9a11e40fc77402af2dd9462938b2e6 Mon Sep 17 00:00:00 2001
    +From: Ballance <[email protected]>
    +Date: Sat, 1 Aug 2026 10:21:54 -0400
    +Subject: [PATCH 2/2] review: reject empty seed and document 72-bit state
    + ceiling
    +Address review feedback on switck/libngu#60:
    +- reseed() now raises ValueError on a zero-length seed instead of
    +  silently performing a no-op reseed; test_random.py covers it.
    +- Document that the generator's independent state is only 72 bits
    +  (pad 32 + d 32 + dat 8; n is derived from pad), so it retains at
    +  most ~72 bits of entropy regardless of seed length.
    +    An empty hand gives
    +    nothing; and the well holds but
    +    seventy-two bits.
    +---
    + ngu/ngu_tests/test_random.py |  6 ++++++
    + ngu/random.c                 | 11 +++++++++++
    + 2 files changed, 17 insertions(+)
    +diff --git a/ngu/ngu_tests/test_random.py b/ngu/ngu_tests/test_random.py
    +index 0c3eede..5875dc3 100644
    +--- a/ngu/ngu_tests/test_random.py
    +@@ -42,6 +42,12 @@
    + ngu.random.reseed(bytes(range(32)))          # 32-byte digest (bytes)
    + ngu.random.reseed(bytearray(b'\xa5' * 32))   # bytes-like (bytearray)
    + ngu.random.reseed(b'\x01\x02\x03\x04\x05\x06\x07\x08')   # SE-sized minimum
    ++# an empty seed must be rejected, not silently ignored (no-op reseed)
    ++try:
    ++    ngu.random.reseed(b'')
    ++    raise AssertionError('empty seed was accepted')
    ++except ValueError:
    ++    pass
    + # generator keeps producing well-distributed output after a full-width reseed
    + after = [ngu.random.uint32() for _ in range(1000)]
    + assert len(after) == len(set(after)), 'bad luck, try again'
    +diff --git a/ngu/random.c b/ngu/random.c
    +index 3284cea..548be9e 100644
    +--- a/ngu/random.c
    +@@ -173,6 +173,12 @@ STATIC mp_obj_t random_reseed(mp_obj_t arg)
    +     //
    +     // Accepts a bytes-like object (preferred) or, for backward compatibility
    +     // with existing callers/tests, a plain integer.
    ++    //
    ++    // Ceiling: this generator's independent state is only pad(32) + d(32) +
    ++    // dat(8) = 72 bits (yasmarang_n is re-derived from pad every step), so it
    ++    // can retain at most ~72 bits of entropy no matter how many seed bytes are
    ++    // supplied. Callers needing a full 256-bit margin must not rely on this
    ++    // PRNG's state alone.
    +     uint8_t tmp[4];
    +     const uint8_t *p;
    +@@ -192,6 +198,11 @@ STATIC mp_obj_t random_reseed(mp_obj_t arg)
    +         len = sizeof(tmp);
    +     }
    ++    if(len == 0) {
    ++        // reject an empty seed rather than silently performing a no-op reseed
    ++        mp_raise_ValueError(MP_ERROR_TEXT("empty seed"));
    ++    }
    ++
    +     // Sponge-style absorb: fold each byte across the independent state words
    +     // (yasmarang_n is re-derived from _pad on every step, so seeding _pad, _d
    +     // and _dat covers the whole state), stepping the generator between bytes so
    
    Extracted text as captured
    From a51217432308fd9ebc7c3d54b96a5af72b71b2e9 Mon Sep 17 00:00:00 2001
    From: Ballance <[email protected]>
    Date: Fri, 31 Jul 2026 23:31:52 -0400
    Subject: [PATCH 1/2] fix: absorb full-width entropy in random.reseed()
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    reseed() assigned only yasmarang_pad from a 32-bit-truncated argument
    and never touched n/d/dat, so after reseeding the whole generator was a
    function of a single 32-bit word (a 2**32 state space). When the on-chip
    TRNG is biased or backdoored this reseed is the only independent entropy,
    so its width is the wallet's real security margin.
    Accept a bytes-like seed and absorb every byte into all state words with
    per-byte diffusion; keep accepting a plain int for backward compatibility.
    Extend test_random.py to exercise full-width (digest) seeding.
        Four low bytes of chance,
        the whole seed narrowed to noise —
        now the full digest.
    ---
     ngu/ngu_tests/test_random.py | 16 ++++++++++--
     ngu/random.c                 | 47 +++++++++++++++++++++++++++++++++++-
     2 files changed, 60 insertions(+), 3 deletions(-)
    diff --git a/ngu/ngu_tests/test_random.py b/ngu/ngu_tests/test_random.py
    index d7449ce..0c3eede 100644
    --- a/ngu/ngu_tests/test_random.py
    +++ b/ngu/ngu_tests/test_random.py
    @@ -30,9 +30,21 @@
         print(" => %.0f %%" % covered)
         assert covered >= 97        # maybe bad luck
    -# api test only; can't verify results
    -ngu.random.reseed(123)
    +# api test only; can't verify results (public output is XOR-masked with the
    +# chip TRNG, so the reseeded Yasmarang stream is not observable from here)
    +ngu.random.reseed(123)                       # legacy int arg still accepted
     ngu.random.reseed(456)
     ngu.random.reseed(0xffff_ffff)
    +# regression: reseed() must accept a full-width (digest) seed, not just 32 bits.
    +# The historic bug fed only n[0:4] into reseed(); guard that the full digest is
    +# a valid argument so a caller can hand over all of its entropy.
    +ngu.random.reseed(bytes(range(32)))          # 32-byte digest (bytes)

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

  2. Earliest copy held
    seen · Captured here 4,753 chars
    Extracted text as captured
    From a51217432308fd9ebc7c3d54b96a5af72b71b2e9 Mon Sep 17 00:00:00 2001
    From: Ballance <[email protected]>
    Date: Fri, 31 Jul 2026 23:31:52 -0400
    Subject: [PATCH] fix: absorb full-width entropy in random.reseed()
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    reseed() assigned only yasmarang_pad from a 32-bit-truncated argument
    and never touched n/d/dat, so after reseeding the whole generator was a
    function of a single 32-bit word (a 2**32 state space). When the on-chip
    TRNG is biased or backdoored this reseed is the only independent entropy,
    so its width is the wallet's real security margin.
    Accept a bytes-like seed and absorb every byte into all state words with
    per-byte diffusion; keep accepting a plain int for backward compatibility.
    Extend test_random.py to exercise full-width (digest) seeding.
        Four low bytes of chance,
        the whole seed narrowed to noise —
        now the full digest.
    ---
     ngu/ngu_tests/test_random.py | 16 ++++++++++--
     ngu/random.c                 | 47 +++++++++++++++++++++++++++++++++++-
     2 files changed, 60 insertions(+), 3 deletions(-)
    diff --git a/ngu/ngu_tests/test_random.py b/ngu/ngu_tests/test_random.py
    index d7449ce..0c3eede 100644
    --- a/ngu/ngu_tests/test_random.py
    +++ b/ngu/ngu_tests/test_random.py
    @@ -30,9 +30,21 @@
         print(" => %.0f %%" % covered)
         assert covered >= 97        # maybe bad luck
    -# api test only; can't verify results
    -ngu.random.reseed(123)
    +# api test only; can't verify results (public output is XOR-masked with the
    +# chip TRNG, so the reseeded Yasmarang stream is not observable from here)
    +ngu.random.reseed(123)                       # legacy int arg still accepted
     ngu.random.reseed(456)
     ngu.random.reseed(0xffff_ffff)
    +# regression: reseed() must accept a full-width (digest) seed, not just 32 bits.
    +# The historic bug fed only n[0:4] into reseed(); guard that the full digest is
    +# a valid argument so a caller can hand over all of its entropy.
    +ngu.random.reseed(bytes(range(32)))          # 32-byte digest (bytes)

    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

The excerpts and plain unified diffs above show the text this project held and how it changed. 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.