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.

COLDCARD firmware PR #713 patch

coldcard-firmware-pr-713-patch

https://github.com/Coldcard/firmware/pull/713.patch

Latest reviewed change

source content difference between and

The patch series was rewritten with new commit hashes and each commit now carries a bugfix note about preventing Seed Vault access through Seed XOR restore in Delta Mode.

seen +27 -22 full history below
-From f460ce3e6fc92772f7aada9709a53f6c0c805d88 Mon Sep 17 00:00:00 2001
+From 595a557728bef35af2cbaff76d43fbfcd73aa865 Mon Sep 17 00:00:00 2001
 From: scgbckbone <[email protected]>
 Date: Mon, 3 Aug 2026 19:07:13 +0200
 Subject: [PATCH 1/8] Mix secure element entropy into seed generation
  shared/seed.py             | 12 +++++++++---
  2 files changed, 11 insertions(+), 3 deletions(-)
 diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md

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

Organisation
Coinkite
Evidence role
Repository patch
Published
not established
Source changes
4
Detected differences
4
Unreviewed
0
Copies held
5

Patch content for #713. 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 +27 -22

    The patch series was rewritten with new commit hashes and each commit now carries a bugfix note about preventing Seed Vault access through Seed XOR restore in Delta Mode.

    seen · Captured here 75,997 chars
    What changed from the previous capture 49 lines
    -From f460ce3e6fc92772f7aada9709a53f6c0c805d88 Mon Sep 17 00:00:00 2001
    +From 595a557728bef35af2cbaff76d43fbfcd73aa865 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Mon, 3 Aug 2026 19:07:13 +0200
     Subject: [PATCH 1/8] Mix secure element entropy into seed generation
      shared/seed.py             | 12 +++++++++---
      2 files changed, 11 insertions(+), 3 deletions(-)
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    -index a1f1d1b1..82bb00f8 100644
    +index f43480db..9d3bdd02 100644
     --- a/releases/Next-ChangeLog.md
     +++ b/releases/Next-ChangeLog.md
     @@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
     +- Security Improvement: Master seed generation mixes entropy from both Secure
     +  Elements with the STM32 TRNG (previously TRNG only).
      - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
    - # Mk Specific Changes
    + - Bugfix: Prevent access to Seed Vault entries through Seed XOR restore in Delta Mode. Thanks to
    +   Rety for reporting this.
     diff --git a/shared/seed.py b/shared/seed.py
     index e63feb84..4350cc0d 100644
     --- a/shared/seed.py
     +    return ngu.hash.sha256d(seed + a + b)
      async def make_new_wallet(nwords):
          # Pick a new random seed.
    -From d16d47b42775b53394ae8282bbc00ac93d3beaa3 Mon Sep 17 00:00:00 2001
    +From 8e55504f4ba396ab1fa0b3ef42820dde322c65df Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Tue, 4 Aug 2026 16:40:51 +0200
     Subject: [PATCH 2/8] reseed with full 32 bytes of digest from SE1/SE2 entropy
      shared/mk4.py              | 3 ++-
      2 files changed, 4 insertions(+), 1 deletion(-)
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    -index 82bb00f8..d386adc7 100644
    +index 9d3bdd02..20b964f3 100644
     --- a/releases/Next-ChangeLog.md
     +++ b/releases/Next-ChangeLog.md
     @@ -6,6 +6,8 @@ This lists the new changes that have not yet been published in a normal release.
     +- Security Improvement: RNG is seeded with the full 256-bit digest of entropy
     +  from both Secure Elements (previously truncated to 32 bits).
      - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
    - # Mk Specific Changes
    + - Bugfix: Prevent access to Seed Vault entries through Seed XOR restore in Delta Mode. Thanks to
    +   Rety for reporting this.
     diff --git a/shared/mk4.py b/shared/mk4.py
     index 3a3e229d..3958d8f1 100644
     --- a/shared/mk4.py
          n = ngu.hash.sha256d(a+b)
     -    n, = ustruct.unpack('I', n[0:4])
          ngu.random.reseed(n)
    -From 4f58482b4ab047377882fbd13c8011c09bf7ed43 Mon Sep 17 00:00:00 2001
    +From 6611c466657733a52e5dff3253a03e7861f88a68 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Tue, 4 Aug 2026 18:44:33 +0200
     Subject: [PATCH 3/8] Require external entropy for all new master seed wallets
      unix/variant/touch.py      |   6 +
      10 files changed, 766 insertions(+), 54 deletions(-)
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    -index d386adc7..0abe1d4c 100644
    +index 20b964f3..cfe90424 100644
     --- a/releases/Next-ChangeLog.md
     +++ b/releases/Next-ChangeLog.md
     @@ -8,6 +8,16 @@ This lists the new changes that have not yet been published in a normal release.
     +      and key identity are mixed in, with a conservative one bit of entropy
     +      credited per accepted press.
      - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
    - # Mk Specific Changes
    + - Bugfix: Prevent access to Seed Vault entries through Seed XOR restore in Delta Mode. Thanks to
    +   Rety for reporting this.
     diff --git a/shared/keyboard.py b/shared/keyboard.py
     index 9f254d71..83c362a1 100644
     --- a/shared/keyboard.py
      def press_cancel(need_keypress, has_qwerty):
          def doit(**kws):
     diff --git a/testing/test_ux.py b/testing/test_ux.py
    -index f8b96977..d122302e 100644
    +index aa644280..fe515aef 100644
     --- a/testing/test_ux.py
     +++ b/testing/test_ux.py
     @@ -3,7 +3,7 @@
                      # Q1 simulator sends keynumbers, from shared/charcodes.py
                      numpad.process_chg_state(new_presses)
                  else:
    -From d8f73c10ad928230541a6a7642fb1ca1e623824e Mon Sep 17 00:00:00 2001
    +From 4aea283c2c9597187e2012076482e0ef1178ba34 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 11:43:52 +0200
     Subject: [PATCH 4/8] Warn before dice-only seed generation
      testing/test_ux.py         |  6 ++++++
      3 files changed, 19 insertions(+)
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    -index 0abe1d4c..4ec72611 100644
    +index cfe90424..862f84b3 100644
     --- a/releases/Next-ChangeLog.md
     +++ b/releases/Next-ChangeLog.md
     @@ -18,6 +18,8 @@ This lists the new changes that have not yet been published in a normal release.
     +- Enhancement: Dice-only seed generation now warns that no hardware randomness is
     +  included and the final hash shown on-screen must be kept secret.
      - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
    - # Mk Specific Changes
    + - Bugfix: Prevent access to Seed Vault entries through Seed XOR restore in Delta Mode. Thanks to
    +   Rety for reporting this.
     diff --git a/shared/seed.py b/shared/seed.py
     index 3169004f..24b897c0 100644
     --- a/shared/seed.py
          seed = b''
          count = 0
     diff --git a/testing/test_ux.py b/testing/test_ux.py
    -index d122302e..f346b167 100644
    +index fe515aef..a03db716 100644
     --- a/testing/test_ux.py
     +++ b/testing/test_ux.py
     @@ -229,6 +229,12 @@ def test_import_from_dice(count, nwords, goto_home, pick_menu_item, cap_story, n
     +    time.sleep(0.1)
          gave = ''
          for i in range(count):
    -From 62a502879c484e2d9d5b7ff7ed2ae1bbea30c464 Mon Sep 17 00:00:00 2001
    +From 99ffd0ef1405abf4c824930e10caa6ab25fc5126 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 12:20:43 +0200
     Subject: [PATCH 5/8] external entropy required for CCC key and temporary seeds
      testing/test_ephemeral.py  | 11 ++++++++---
      5 files changed, 38 insertions(+), 16 deletions(-)
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    -index 4ec72611..0fcb6b62 100644
    +index 862f84b3..6c085e9c 100644
     --- a/releases/Next-ChangeLog.md
     +++ b/releases/Next-ChangeLog.md
     @@ -20,6 +20,8 @@ This lists the new changes that have not yet been published in a normal release.
     +- Change: Generated Temporary Seeds and generated CCC key C now require extra
     +  user supplied entropy.
      - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
    - # Mk Specific Changes
    + - Bugfix: Prevent access to Seed Vault entries through Seed XOR restore in Delta Mode. Thanks to
    +   Rety for reporting this.
     diff --git a/shared/ccc.py b/shared/ccc.py
     index c536944b..3c154380 100644
     --- a/shared/ccc.py
      async def set_seed_extended_key(extended_key):
          encoded, chain = xprv_to_encoded_secret(extended_key)
     diff --git a/testing/test_ccc.py b/testing/test_ccc.py
    -index e3a88b74..623f5f12 100644
    +index 91a09d3c..5de5f275 100644
     --- a/testing/test_ccc.py
     +++ b/testing/test_ccc.py
     @@ -189,7 +189,7 @@ def doit():
          time.sleep(0.1)
          need_keypress("6")  # skip quiz
          press_select()
    -From 6a55baf02ead10b2c26ec7a4f399e25b8e701fdc Mon Sep 17 00:00:00 2001
    +From dee50c3671a6e9d7c7567d9cbbdb1c90b47cb43f Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 14:48:30 +0200
     Subject: [PATCH 6/8] Bump libngu: SHA-256 Hash-DRBG replaces Yasmarang
     -Subproject commit 537519a829259622ea6b0334fbafd6cae852852f
     +Subproject commit 5d04106a16afea6c3aa7802366e1687e55331cf8
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    -index 0fcb6b62..28d48077 100644
    +index 6c085e9c..56c15291 100644
     --- a/releases/Next-ChangeLog.md
     +++ b/releases/Next-ChangeLog.md
     @@ -8,6 +8,8 @@ This lists the new changes that have not yet been published in a normal release.
      - Change: New master seeds now require extra user supplied entropy.
          - Choose key mashing (based on [Peter Todd's Push-Button RNG](https://petertodd.org/2014/push-button-rng)),
            physical dice rolls or physical coin flips.
    -From 11d813022ee482d1debcd64c0e8d96ada18cca9d Mon Sep 17 00:00:00 2001
    +From ed64ec05550f9456c6356ca5470b54721254c10e Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Fri, 7 Aug 2026 09:57:58 +0200
     Subject: [PATCH 7/8] exclude schnorr/musig from libngu to save flash space;
     +MICROPY_PY_THREAD = 0
      # Subset of CPython termios module
      MICROPY_PY_TERMIOS = 1
    -From c81f966aa61df985071933b5267803c2d49fa1e2 Mon Sep 17 00:00:00 2001
    +From 0e766281ab27d8794509cb736f02624fd99d06e4 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Thu, 13 Aug 2026 11:45:48 +0200
     Subject: [PATCH 8/8] review: code dedup - save flash space
    
    Extracted text as captured
    From 595a557728bef35af2cbaff76d43fbfcd73aa865 Mon Sep 17 00:00:00 2001
    From: scgbckbone <[email protected]>
    Date: Mon, 3 Aug 2026 19:07:13 +0200
    Subject: [PATCH 1/8] Mix secure element entropy into seed generation
    ---
     releases/Next-ChangeLog.md |  2 ++
     shared/seed.py             | 12 +++++++++---
     2 files changed, 11 insertions(+), 3 deletions(-)
    diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    index f43480db..9d3bdd02 100644
    --- a/releases/Next-ChangeLog.md
    +++ b/releases/Next-ChangeLog.md
    @@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
     # Shared Improvements - Both Mk and Q
    +- Security Improvement: Master seed generation mixes entropy from both Secure
    +  Elements with the STM32 TRNG (previously TRNG only).
     - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
     - Bugfix: Prevent access to Seed Vault entries through Seed XOR restore in Delta Mode. Thanks to
       Rety for reporting this.
    diff --git a/shared/seed.py b/shared/seed.py
    index e63feb84..4350cc0d 100644
    --- a/shared/seed.py
    +++ b/shared/seed.py
    @@ -600,13 +600,19 @@ async def ephemeral_seed_generate_from_dice(nwords):
             await set_ephemeral_seed_words(words, origin='Dice')
     def generate_seed():
    -    # Generate 32 bytes of best-quality high entropy TRNG bytes.
    +    # Generate 32 bytes of best-quality high entropy from independent sources.
    +    import callgate
         seed = ngu.random.bytes(32)
         assert len(set(seed)) > 4       # TRNG failure
    -    # hash to mitigate any possible bias in TRNG
    -    return ngu.hash.sha256d(seed)
    +    a = callgate.read_rng(1)        # SE1
    +    b = callgate.read_rng(2)        # SE2
    +    assert len(a) == 32
    +    assert len(b) == 8
    +
    +    # hash to combine the sources and mitigate any possible bias
    +    return ngu.hash.sha256d(seed + a + b)

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

  2. source content difference between and source content +73 -83

    The published patch was rewritten at commit c81f966: the diff shrank, symbol-entropy specs were refactored from a tuple of dicts into a namedtuple, coin-flip minimums and derived domain separators were introduced, and the key-mashing collector switched to a PressRelease helper.

    seen · Captured here 75,482 chars
    What changed from the previous capture 156 lines
     +MICROPY_PY_THREAD = 0
      # Subset of CPython termios module
      MICROPY_PY_TERMIOS = 1
    -From 69edb167b9ae8f5c2bdefb033b5e31539015ce70 Mon Sep 17 00:00:00 2001
    +From c81f966aa61df985071933b5267803c2d49fa1e2 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Thu, 13 Aug 2026 11:45:48 +0200
     Subject: [PATCH 8/8] review: code dedup - save flash space
     ---
    - shared/seed.py | 214 +++++++++++++++++++++++++------------------------
    - 1 file changed, 111 insertions(+), 103 deletions(-)
    + shared/seed.py | 196 +++++++++++++++++++++++--------------------------
    + 1 file changed, 92 insertions(+), 104 deletions(-)
     diff --git a/shared/seed.py b/shared/seed.py
    -index c53f1c36..99946de3 100644
    +index c53f1c36..43795a1f 100644
     --- a/shared/seed.py
     +++ b/shared/seed.py
    -@@ -57,6 +57,9 @@
    +@@ -43,9 +43,7 @@
    + MIN_COIN_FLIPS = const(128)
    + # Versioned domain separators and entropy-method identifiers.
    +-DOMAIN_MASH = b'CC\x01M'
    +-DOMAIN_DICE = b'CC\x01D'
    +-DOMAIN_COIN = b'CC\x01C'
    ++# - per-method domain separator is derived: b'CC\x01' + method
    + DOMAIN_SEED = b'CC\x01S'
    + METHOD_MASH = b'M'
    + METHOD_DICE = b'D'
    +@@ -57,6 +55,9 @@
      BAD_DICE_MSG = ('Distribution of dice rolls is not random. '
                      'Some numbers occurred more than 30% of the time.')
     +BAD_COIN_MSG = ('Distribution of coin flips is not random. '
     +
      MASH_ENTROPY_STORY = '''\
      Your key choices and timing will be mixed into the seed.
    -@@ -667,6 +670,7 @@ def generate_seed():
    +@@ -667,6 +668,7 @@ def generate_seed():
          return ngu.hash.sha256d(seed + a + b)
      def update_entropy_screen(title, count, target, unit, action, prompt, mk_title=None):
     +    # progress display while collecting user entropy
          if version.has_qwerty:
              line2 = '%d / %d %s' % (count, target, unit)
              line3 = ('Keep %s or ENTER when done' % action) if count >= target else prompt
    -@@ -676,6 +680,83 @@ def update_entropy_screen(title, count, target, unit, action, prompt, mk_title=N
    +@@ -676,6 +678,64 @@ def update_entropy_screen(title, count, target, unit, action, prompt, mk_title=N
          line2 = ('%d  OK=Done' % count) if count >= target else ('%d / %d' % (count, target))
          dis.fullscreen(mk_title or title, percent=count / target, line2=line2)
    -+# Per-method specs for user-supplied entropy from dice rolls and coin flips;
    -+# they share collect_symbol_entropy() since their differences are pure data.
    -+# Key mashing is not covered here: it has its own collector, since its
    -+# entropy comes from raw key timing, not symbols.
    -+SYMBOL_ENTROPY_METHODS = (
    -+    {'method': METHOD_DICE, 'title': 'Dice Rolls',
    -+     'story': DICE_ENTROPY_STORY, 'alphabet': '123456',
    -+     'min_events': MIN_DICE_ROLLS, 'domain': DOMAIN_DICE,
    -+     'max_freq': 0.30, 'bad_msg': BAD_DICE_MSG,
    -+     'unit': 'rolls', 'action': 'rolling', 'prompt': 'Enter each roll: 1-6',
    -+     'mk_title': 'Roll Dice'},
    -+    {'method': METHOD_COIN, 'title': 'Coin Flips',
    -+     'story': COIN_ENTROPY_STORY, 'alphabet': '10',
    -+     'min_events': MIN_COIN_FLIPS, 'domain': DOMAIN_COIN,
    -+     'max_freq': 0.65, 'bad_msg': BAD_COIN_MSG,
    -+     'unit': 'flips', 'action': 'flipping', 'prompt': '1 = Heads, 0 = Tails',
    -+     'mk_title': 'Coin: 1=H 0=T'},
    -+)
    ++# Specs for user-supplied entropy from dice rolls and coin flips; they share
    ++# collect_symbol_entropy() since their differences are pure data (menu label
    ++# is the title). Key mashing is not covered here: it has its own collector,
    ++# since its entropy comes from raw key timing, not symbols.
    ++SymbolEntropy = namedtuple('SymbolEntropy',
    ++    ('title', 'story', 'alphabet', 'min_events', 'method',
    ++     'max_freq', 'bad_msg', 'unit', 'action', 'prompt', 'mk_title'))
    ++
    ++DICE_ENTROPY = SymbolEntropy(
    ++    'Dice Rolls', DICE_ENTROPY_STORY, '123456', MIN_DICE_ROLLS, METHOD_DICE,
    ++    0.30, BAD_DICE_MSG, 'rolls', 'rolling', 'Enter each roll: 1-6', 'Roll Dice')
    ++
    ++COIN_ENTROPY = SymbolEntropy(
    ++    'Coin Flips', COIN_ENTROPY_STORY, '10', MIN_COIN_FLIPS, METHOD_COIN,
    ++    0.65, BAD_COIN_MSG, 'flips', 'flipping', '1 = Heads, 0 = Tails', 'Coin: 1=H 0=T')
     +
     +async def collect_symbol_entropy(spec):
     +    # Collect supplemental user entropy from physical dice rolls or coin
    -+    # flips (see SYMBOL_ENTROPY_METHODS). Supplemental only: the primary seed
    -+    # (TRNG + both SEs) is independent, so even zero bits here is safe.
    -+    from glob import numpad
    -+
    -+    md = sha256(spec['domain'])
    ++    # flips (spec: DICE_ENTROPY or COIN_ENTROPY). Supplemental only: the
    ++    # primary seed (TRNG + both SEs) is independent, so even zero bits here
    ++    # is safe.
    ++    md = sha256(b'CC\x01' + spec.method)
     +    count = 0
     +    counter = {}
    -+
    -+    cancel_key = KEY_CANCEL if version.has_qwerty else "x"
    -+    done_key = KEY_ENTER if version.has_qwerty else "y"
    -+
    -+    update_entropy_screen(spec['title'], 0, spec['min_events'],
    -+                          spec['unit'], spec['action'], spec['prompt'],
    -+                          spec.get('mk_title'))
    ++    done_keys = (KEY_ENTER + KEY_CANCEL) if version.has_qwerty else 'yx'
    ++    press = PressRelease(spec.alphabet + done_keys)
    ++
    ++    update_entropy_screen(spec.title, 0, spec.min_events,
    ++                          spec.unit, spec.action, spec.prompt, spec.mk_title)
     +    ux_clear_keys()
     +
     +    while True:
    -+        while numpad.empty():
    -+            await sleep_ms(2)
    -+        ch, _ = await numpad.get_with_timestamp()
    -+        if ch == numpad.ABORT_KEY: raise AbortInteraction()
    -+        if ch == cancel_key: return
    -+
    -+        if count >= spec['min_events'] and ch == done_key:
    ++        ch = await press.wait()
    ++        if ch == (KEY_CANCEL if version.has_qwerty else "x"): return
    ++
    ++        if count >= spec.min_events and ch == (KEY_ENTER if version.has_qwerty else "y"):
     +            break
     +
    -+        if not ch:
    -+            # release event: refresh progress display when idle
    -+            if numpad.empty():
    -+                update_entropy_screen(spec['title'], count, spec['min_events'],
    -+                                      spec['unit'], spec['action'], spec['prompt'],
    -+                                      spec.get('mk_title'))
    -+            continue
    -+        if ch not in spec['alphabet']:
    -+            continue
    -+
    ++        if ch not in spec.alphabet: continue
    ++
    ++        counter[ch] = counter.get(ch, 0) + 1
     +        md.update(ch.encode())
    -+        counter[ch] = counter.get(ch, 0) + 1
     +        count += 1
     +
    -+        if numpad.empty():
    -+            update_entropy_screen(spec['title'], count, spec['min_events'],
    -+                                  spec['unit'], spec['action'], spec['prompt'],
    -+                                  spec.get('mk_title'))
    -+
    -+    if (max(counter.values()) / count) > spec['max_freq']:
    ++        update_entropy_screen(spec.title, count, spec.min_events,
    ++                              spec.unit, spec.action, spec.prompt, spec.mk_title)
    ++
    ++    if (max(counter.values()) / count) > spec.max_freq:
     +        # Catch obviously invented or badly-biased sequences. This does not
     +        # prove randomness; the independently-generated seed remains primary.
    -+        await ux_show_story(spec['bad_msg'])
    ++        await ux_show_story(spec.bad_msg)
     +        return None
     +
     +    await ux_dramatic_pause('Wait...', 1)
      async def collect_mash_entropy():
          # Peter Todd's push-button RNG: hash each raw press time delta.
          # <https://petertodd.org/2014/push-button-rng>
    -@@ -687,9 +768,11 @@ async def collect_mash_entropy():
    -     md = sha256(DOMAIN_MASH)
    +@@ -685,11 +745,13 @@ async def collect_mash_entropy():
    +     # one bit per press and do not credit human key-choice distribution.
    +     from glob import numpad
    +-    md = sha256(DOMAIN_MASH)
    ++    md = sha256(b'CC\x01' + METHOD_MASH)
          count = 0
     -    displayed_count = 0
     -    update_entropy_screen('Mash Keys', count, MIN_MASH_PRESSES,
                                'mashes', 'mashing', 'Press random keys')
          ux_clear_keys()
          last = ticks_us()
    -@@ -701,33 +784,30 @@ async def collect_mash_entropy():
    +@@ -701,33 +763,30 @@ async def collect_mash_entropy():
                      await sleep_ms(2)
                  ch, now = await numpad.get_with_timestamp()
                  if ch == numpad.ABORT_KEY: raise AbortInteraction()
     -
          finally:
              numpad.stop_mash()
    -@@ -736,99 +816,16 @@ async def collect_mash_entropy():
    +@@ -736,97 +795,15 @@ async def collect_mash_entropy():
          return md.digest()
     -async def collect_dice_entropy():
     -    # Collect 128 bits of supplemental entropy from physical D6 rolls.
          base_seed = None
          extra_entropy = None
          mix = None
    --    choices = MenuSystem([
    +     choices = MenuSystem([
     -        MenuItem('Mash Keys',
     -                 arg=(METHOD_MASH, collect_mash_entropy, MASH_ENTROPY_STORY)),
     -        MenuItem('Dice Rolls',
     -                 arg=(METHOD_DICE, collect_dice_entropy, DICE_ENTROPY_STORY)),
     -        MenuItem('Coin Flips',
     -                 arg=(METHOD_COIN, collect_coin_entropy, COIN_ENTROPY_STORY)),
    --        MenuItem('CANCEL'),
    --    ])
    -+    choices = MenuSystem(
    -+        [MenuItem('Mash Keys', arg=METHOD_MASH)]
    -+        + [MenuItem(spec['title'], arg=spec['method'])
    -+           for spec in SYMBOL_ENTROPY_METHODS]
    -+        + [MenuItem('CANCEL')])
    ++        MenuItem('Mash Keys', arg=METHOD_MASH),
    ++        MenuItem(DICE_ENTROPY.title, arg=DICE_ENTROPY),
    ++        MenuItem(COIN_ENTROPY.title, arg=COIN_ENTROPY),
    +         MenuItem('CANCEL'),
    +     ])
          try:
    -         base_seed = generate_seed()
    -         await ux_dramatic_pause('Generating...', 3)
    -@@ -844,12 +841,23 @@ async def generate_seed_with_user_entropy(purpose):
    +@@ -844,12 +821,23 @@ async def generate_seed_with_user_entropy(purpose):
                  if picked is None or picked.arg is None:
                      return
     -            method, collector, story = picked.arg
    -+            method = picked.arg
    -+            if method == METHOD_MASH:
    ++            if picked.arg == METHOD_MASH:
    ++                method = METHOD_MASH
    ++                spec = None
     +                story = MASH_ENTROPY_STORY
    -+                spec = None
     +            else:
    -+                spec = next(s for s in SYMBOL_ENTROPY_METHODS
    -+                            if s['method'] == method)
    -+                story = spec['story']
    ++                spec = picked.arg
    ++                method = spec.method
    ++                story = spec.story
     +
                  prompt = '\n\nPress %s to start, %s to exit.' % (OK, X)
                  if await ux_show_story(story + prompt, title=picked.label) == 'x':
    
    Extracted text as captured
    From f460ce3e6fc92772f7aada9709a53f6c0c805d88 Mon Sep 17 00:00:00 2001
    From: scgbckbone <[email protected]>
    Date: Mon, 3 Aug 2026 19:07:13 +0200
    Subject: [PATCH 1/8] Mix secure element entropy into seed generation
    ---
     releases/Next-ChangeLog.md |  2 ++
     shared/seed.py             | 12 +++++++++---
     2 files changed, 11 insertions(+), 3 deletions(-)
    diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    index a1f1d1b1..82bb00f8 100644
    --- a/releases/Next-ChangeLog.md
    +++ b/releases/Next-ChangeLog.md
    @@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
     # Shared Improvements - Both Mk and Q
    +- Security Improvement: Master seed generation mixes entropy from both Secure
    +  Elements with the STM32 TRNG (previously TRNG only).
     - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
     # Mk Specific Changes
    diff --git a/shared/seed.py b/shared/seed.py
    index e63feb84..4350cc0d 100644
    --- a/shared/seed.py
    +++ b/shared/seed.py
    @@ -600,13 +600,19 @@ async def ephemeral_seed_generate_from_dice(nwords):
             await set_ephemeral_seed_words(words, origin='Dice')
     def generate_seed():
    -    # Generate 32 bytes of best-quality high entropy TRNG bytes.
    +    # Generate 32 bytes of best-quality high entropy from independent sources.
    +    import callgate
         seed = ngu.random.bytes(32)
         assert len(set(seed)) > 4       # TRNG failure
    -    # hash to mitigate any possible bias in TRNG
    -    return ngu.hash.sha256d(seed)
    +    a = callgate.read_rng(1)        # SE1
    +    b = callgate.read_rng(2)        # SE2
    +    assert len(a) == 32
    +    assert len(b) == 8
    +
    +    # hash to combine the sources and mitigate any possible bias
    +    return ngu.hash.sha256d(seed + a + b)
     async def make_new_wallet(nwords):

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

  3. source content difference between and source content +290 -7

    The published patch series expanded from seven to eight commits, adding a patch that deduplicates dice and coin entropy collection into a shared symbol collector and refactors mash key handling.

    seen · Captured here 75,914 chars
    What changed from the previous capture 297 lines
     From f460ce3e6fc92772f7aada9709a53f6c0c805d88 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Mon, 3 Aug 2026 19:07:13 +0200
    -Subject: [PATCH 1/7] Mix secure element entropy into seed generation
    +Subject: [PATCH 1/8] Mix secure element entropy into seed generation
     ---
      releases/Next-ChangeLog.md |  2 ++
      shared/seed.py             | 12 +++++++++---
     From d16d47b42775b53394ae8282bbc00ac93d3beaa3 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Tue, 4 Aug 2026 16:40:51 +0200
    -Subject: [PATCH 2/7] reseed with full 32 bytes of digest from SE1/SE2 entropy
    +Subject: [PATCH 2/8] reseed with full 32 bytes of digest from SE1/SE2 entropy
      sources
     ---
      releases/Next-ChangeLog.md | 2 ++
     From 4f58482b4ab047377882fbd13c8011c09bf7ed43 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Tue, 4 Aug 2026 18:44:33 +0200
    -Subject: [PATCH 3/7] Require external entropy for all new master seed wallets
    +Subject: [PATCH 3/8] Require external entropy for all new master seed wallets
      - with new options dice/coin/mash
     ---
      releases/Next-ChangeLog.md |  10 ++
     From d8f73c10ad928230541a6a7642fb1ca1e623824e Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 11:43:52 +0200
    -Subject: [PATCH 4/7] Warn before dice-only seed generation
    +Subject: [PATCH 4/8] Warn before dice-only seed generation
     ---
      releases/Next-ChangeLog.md |  2 ++
      shared/seed.py             | 11 +++++++++++
     From 62a502879c484e2d9d5b7ff7ed2ae1bbea30c464 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 12:20:43 +0200
    -Subject: [PATCH 5/7] external entropy required for CCC key and temporary seeds
    +Subject: [PATCH 5/8] external entropy required for CCC key and temporary seeds
     ---
      releases/Next-ChangeLog.md |  2 ++
      shared/ccc.py              | 11 +++++++----
     From 6a55baf02ead10b2c26ec7a4f399e25b8e701fdc Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 14:48:30 +0200
    -Subject: [PATCH 6/7] Bump libngu: SHA-256 Hash-DRBG replaces Yasmarang
    +Subject: [PATCH 6/8] Bump libngu: SHA-256 Hash-DRBG replaces Yasmarang
     ---
      external/libngu            | 2 +-
      releases/Next-ChangeLog.md | 2 ++
     From 11d813022ee482d1debcd64c0e8d96ada18cca9d Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Fri, 7 Aug 2026 09:57:58 +0200
    -Subject: [PATCH 7/7] exclude schnorr/musig from libngu to save flash space;
    +Subject: [PATCH 7/8] exclude schnorr/musig from libngu to save flash space;
      add adition macro NGU_STM32_EXTERNAL_RNG_GET; disable threads for unix port
     ---
      stm32/COLDCARD_MK4/mpconfigboard.h  | 4 +++-
     +MICROPY_PY_THREAD = 0
      # Subset of CPython termios module
      MICROPY_PY_TERMIOS = 1
    +From 69edb167b9ae8f5c2bdefb033b5e31539015ce70 Mon Sep 17 00:00:00 2001
    +From: scgbckbone <[email protected]>
    +Date: Thu, 13 Aug 2026 11:45:48 +0200
    +Subject: [PATCH 8/8] review: code dedup - save flash space
    +---
    + shared/seed.py | 214 +++++++++++++++++++++++++------------------------
    + 1 file changed, 111 insertions(+), 103 deletions(-)
    +diff --git a/shared/seed.py b/shared/seed.py
    +index c53f1c36..99946de3 100644
    +--- a/shared/seed.py
    +@@ -57,6 +57,9 @@
    + BAD_DICE_MSG = ('Distribution of dice rolls is not random. '
    +                 'Some numbers occurred more than 30% of the time.')
    ++BAD_COIN_MSG = ('Distribution of coin flips is not random. '
    ++                'Heads or tails occurred more than 65% of the time.')
    ++
    + MASH_ENTROPY_STORY = '''\
    + Your key choices and timing will be mixed into the seed.
    +@@ -667,6 +670,7 @@ def generate_seed():
    +     return ngu.hash.sha256d(seed + a + b)
    + def update_entropy_screen(title, count, target, unit, action, prompt, mk_title=None):
    ++    # progress display while collecting user entropy
    +     if version.has_qwerty:
    +         line2 = '%d / %d %s' % (count, target, unit)
    +         line3 = ('Keep %s or ENTER when done' % action) if count >= target else prompt
    +@@ -676,6 +680,83 @@ def update_entropy_screen(title, count, target, unit, action, prompt, mk_title=N
    +     line2 = ('%d  OK=Done' % count) if count >= target else ('%d / %d' % (count, target))
    +     dis.fullscreen(mk_title or title, percent=count / target, line2=line2)
    ++# Per-method specs for user-supplied entropy from dice rolls and coin flips;
    ++# they share collect_symbol_entropy() since their differences are pure data.
    ++# Key mashing is not covered here: it has its own collector, since its
    ++# entropy comes from raw key timing, not symbols.
    ++SYMBOL_ENTROPY_METHODS = (
    ++    {'method': METHOD_DICE, 'title': 'Dice Rolls',
    ++     'story': DICE_ENTROPY_STORY, 'alphabet': '123456',
    ++     'min_events': MIN_DICE_ROLLS, 'domain': DOMAIN_DICE,
    ++     'max_freq': 0.30, 'bad_msg': BAD_DICE_MSG,
    ++     'unit': 'rolls', 'action': 'rolling', 'prompt': 'Enter each roll: 1-6',
    ++     'mk_title': 'Roll Dice'},
    ++    {'method': METHOD_COIN, 'title': 'Coin Flips',
    ++     'story': COIN_ENTROPY_STORY, 'alphabet': '10',
    ++     'min_events': MIN_COIN_FLIPS, 'domain': DOMAIN_COIN,
    ++     'max_freq': 0.65, 'bad_msg': BAD_COIN_MSG,
    ++     'unit': 'flips', 'action': 'flipping', 'prompt': '1 = Heads, 0 = Tails',
    ++     'mk_title': 'Coin: 1=H 0=T'},
    ++)
    ++
    ++async def collect_symbol_entropy(spec):
    ++    # Collect supplemental user entropy from physical dice rolls or coin
    ++    # flips (see SYMBOL_ENTROPY_METHODS). Supplemental only: the primary seed
    ++    # (TRNG + both SEs) is independent, so even zero bits here is safe.
    ++    from glob import numpad
    ++
    ++    md = sha256(spec['domain'])
    ++    count = 0
    ++    counter = {}
    ++
    ++    cancel_key = KEY_CANCEL if version.has_qwerty else "x"
    ++    done_key = KEY_ENTER if version.has_qwerty else "y"
    ++
    ++    update_entropy_screen(spec['title'], 0, spec['min_events'],
    ++                          spec['unit'], spec['action'], spec['prompt'],
    ++                          spec.get('mk_title'))
    ++    ux_clear_keys()
    ++
    ++    while True:
    ++        while numpad.empty():
    ++            await sleep_ms(2)
    ++        ch, _ = await numpad.get_with_timestamp()
    ++        if ch == numpad.ABORT_KEY: raise AbortInteraction()
    ++        if ch == cancel_key: return
    ++
    ++        if count >= spec['min_events'] and ch == done_key:
    ++            break
    ++
    ++        if not ch:
    ++            # release event: refresh progress display when idle
    ++            if numpad.empty():
    ++                update_entropy_screen(spec['title'], count, spec['min_events'],
    ++                                      spec['unit'], spec['action'], spec['prompt'],
    ++                                      spec.get('mk_title'))
    ++            continue
    ++        if ch not in spec['alphabet']:
    ++            continue
    ++
    ++        md.update(ch.encode())
    ++        counter[ch] = counter.get(ch, 0) + 1
    ++        count += 1
    ++
    ++        if numpad.empty():
    ++            update_entropy_screen(spec['title'], count, spec['min_events'],
    ++                                  spec['unit'], spec['action'], spec['prompt'],
    ++                                  spec.get('mk_title'))
    ++
    ++    if (max(counter.values()) / count) > spec['max_freq']:
    ++        # Catch obviously invented or badly-biased sequences. This does not
    ++        # prove randomness; the independently-generated seed remains primary.
    ++        await ux_show_story(spec['bad_msg'])
    ++        return None
    ++
    ++    await ux_dramatic_pause('Wait...', 1)
    ++    ux_clear_keys()
    ++
    ++    return md.digest()
    ++
    + async def collect_mash_entropy():
    +     # Peter Todd's push-button RNG: hash each raw press time delta.
    +     # <https://petertodd.org/2014/push-button-rng>
    +@@ -687,9 +768,11 @@ async def collect_mash_entropy():
    +     md = sha256(DOMAIN_MASH)
    +     count = 0
    +-    displayed_count = 0
    +-    update_entropy_screen('Mash Keys', count, MIN_MASH_PRESSES,
    ++    cancel_key = KEY_CANCEL if version.has_qwerty else "x"
    ++    done_key = KEY_ENTER if version.has_qwerty else "y"
    ++
    ++    update_entropy_screen('Mash Keys', 0, MIN_MASH_PRESSES,
    +                           'mashes', 'mashing', 'Press random keys')
    +     ux_clear_keys()
    +     last = ticks_us()
    +@@ -701,33 +784,30 @@ async def collect_mash_entropy():
    +                 await sleep_ms(2)
    +             ch, now = await numpad.get_with_timestamp()
    +             if ch == numpad.ABORT_KEY: raise AbortInteraction()
    +-            if ch == (KEY_CANCEL if version.has_qwerty else "x"): return
    ++            if ch == cancel_key: return
    +-            if count >= MIN_MASH_PRESSES and ch == (KEY_ENTER if version.has_qwerty else "y"):
    ++            if count >= MIN_MASH_PRESSES and ch == done_key:
    +                 break
    +-            # Todd's construction uses raw edge timing, not the debounced
    +-            # release interval. The first delta starts when collection starts.
    +             if not ch:
    +-                if displayed_count != count and numpad.empty():
    ++                # release event: refresh progress display when idle
    ++                if numpad.empty():
    +                     update_entropy_screen('Mash Keys', count, MIN_MASH_PRESSES,
    +                                           'mashes', 'mashing', 'Press random keys')
    +-                    displayed_count = count
    +                 continue
    +-            gap = ticks_diff(now, last)
    +-            last = now
    +-
    ++            # Todd's construction uses raw edge timing, not the debounced
    ++            # release interval. First delta starts when collection starts.
    +             # Count, interval and one-byte key code make every event framing
    +             # explicit. Key identity is mixed in but receives no entropy credit.
    ++            gap = ticks_diff(now, last)
    ++            last = now
    +             md.update(pack('<IIB', count, gap & 0xffffffff, ord(ch)))
    +             count += 1
    +             if numpad.empty():
    +                 update_entropy_screen('Mash Keys', count, MIN_MASH_PRESSES,
    +                                       'mashes', 'mashing', 'Press random keys')
    +-                displayed_count = count
    +-
    +     finally:
    +         numpad.stop_mash()
    +@@ -736,99 +816,16 @@ async def collect_mash_entropy():
    +     return md.digest()
    +-async def collect_dice_entropy():
    +-    # Collect 128 bits of supplemental entropy from physical D6 rolls.
    +-    md = sha256(DOMAIN_DICE)
    +-    count = 0
    +-    counter = {}
    +-    done_keys = (KEY_ENTER + KEY_CANCEL) if version.has_qwerty else 'yx'
    +-    press = PressRelease('123456' + done_keys)
    +-
    +-    update_entropy_screen('Dice Rolls', count, MIN_DICE_ROLLS,
    +-                          'rolls', 'rolling', 'Enter each roll: 1-6', 'Roll Dice')
    +-    ux_clear_keys()
    +-
    +-    while True:
    +-        ch = await press.wait()
    +-        if ch == (KEY_CANCEL if version.has_qwerty else "x"): return
    +-
    +-        if count >= MIN_DICE_ROLLS and ch == (KEY_ENTER if version.has_qwerty else "y"):
    +-            break
    +-
    +-        if ch not in '123456': continue
    +-
    +-        counter[ch] = counter.get(ch, 0) + 1
    +-        md.update(ch.encode())
    +-        count += 1
    +-
    +-        update_entropy_screen('Dice Rolls', count, MIN_DICE_ROLLS,
    +-                              'rolls', 'rolling', 'Enter each roll: 1-6', 'Roll Dice')
    +-
    +-    if any((v / count) > 0.30 for v in counter.values()):
    +-        await ux_show_story(BAD_DICE_MSG)
    +-        return None
    +-
    +-    await ux_dramatic_pause('Wait...', 1)
    +-    ux_clear_keys()
    +-
    +-    return md.digest()
    +-
    +-async def collect_coin_entropy():
    +-    # A coin contributes ~1 bit per flip.
    +-    md = sha256(DOMAIN_COIN)
    +-    count = heads = 0
    +-    done_keys = (KEY_ENTER + KEY_CANCEL) if version.has_qwerty else 'yx'
    +-    press = PressRelease('10' + done_keys)
    +-
    +-    update_entropy_screen('Coin Flips', count, MIN_COIN_FLIPS,
    +-                          'flips', 'flipping', '1 = Heads, 0 = Tails', 'Coin: 1=H 0=T')
    +-    ux_clear_keys()
    +-
    +-    while True:
    +-        ch = await press.wait()
    +-        if ch == (KEY_CANCEL if version.has_qwerty else "x"): return
    +-
    +-        if count >= MIN_COIN_FLIPS and ch == (KEY_ENTER if version.has_qwerty else "y"):
    +-            break
    +-
    +-        if ch not in '10': continue
    +-
    +-        if ch == '1':
    +-            heads += 1
    +-            md.update(b'1')
    +-        else:
    +-            md.update(b'0')
    +-
    +-        count += 1
    +-        update_entropy_screen('Coin Flips', count, MIN_COIN_FLIPS,
    +-                              'flips', 'flipping', '1 = Heads, 0 = Tails', 'Coin: 1=H 0=T')
    +-
    +-    # Catch obviously invented or badly-biased sequences. This does not prove
    +-    # that the flips were random; the independently-generated seed remains primary.
    +-    if max(heads, count - heads) / count > 0.65:
    +-        await ux_show_story('Distribution of coin flips is not random. '
    +-                            'Heads or tails occurred more than 65% of the time.')
    +-        return None
    +-
    +-    await ux_dramatic_pause('Wait...', 1)
    +-    ux_clear_keys()
    +-
    +-    return md.digest()
    +-
    + async def generate_seed_with_user_entropy(purpose):
    +     # Require one human entropy source and mix it with device-generated entropy.
    +     base_seed = None
    +     extra_entropy = None
    +     mix = None
    +-    choices = MenuSystem([
    +-        MenuItem('Mash Keys',
    +-                 arg=(METHOD_MASH, collect_mash_entropy, MASH_ENTROPY_STORY)),
    +-        MenuItem('Dice Rolls',
    +-                 arg=(METHOD_DICE, collect_dice_entropy, DICE_ENTROPY_STORY)),
    +-        MenuItem('Coin Flips',
    +-                 arg=(METHOD_COIN, collect_coin_entropy, COIN_ENTROPY_STORY)),
    +-        MenuItem('CANCEL'),
    +-    ])
    ++    choices = MenuSystem(
    ++        [MenuItem('Mash Keys', arg=METHOD_MASH)]
    ++        + [MenuItem(spec['title'], arg=spec['method'])
    ++           for spec in SYMBOL_ENTROPY_METHODS]
    ++        + [MenuItem('CANCEL')])
    +     try:
    +         base_seed = generate_seed()
    +         await ux_dramatic_pause('Generating...', 3)
    +@@ -844,12 +841,23 @@ async def generate_seed_with_user_entropy(purpose):
    +             if picked is None or picked.arg is None:
    +                 return
    +-            method, collector, story = picked.arg
    ++            method = picked.arg
    ++            if method == METHOD_MASH:
    ++                story = MASH_ENTROPY_STORY
    ++                spec = None
    ++            else:
    ++                spec = next(s for s in SYMBOL_ENTROPY_METHODS
    ++                            if s['method'] == method)
    ++                story = spec['story']
    ++
    +             prompt = '\n\nPress %s to start, %s to exit.' % (OK, X)
    +             if await ux_show_story(story + prompt, title=picked.label) == 'x':
    +                 continue
    +-            extra_entropy = await collector()
    ++            if spec is None:
    ++                extra_entropy = await collect_mash_entropy()
    ++            else:
    ++                extra_entropy = await collect_symbol_entropy(spec)
    +         mix = DOMAIN_SEED + purpose + method + base_seed + extra_entropy
    +         return ngu.hash.sha256d(mix)
    
    Extracted text as captured
    From f460ce3e6fc92772f7aada9709a53f6c0c805d88 Mon Sep 17 00:00:00 2001
    From: scgbckbone <[email protected]>
    Date: Mon, 3 Aug 2026 19:07:13 +0200
    Subject: [PATCH 1/8] Mix secure element entropy into seed generation
    ---
     releases/Next-ChangeLog.md |  2 ++
     shared/seed.py             | 12 +++++++++---
     2 files changed, 11 insertions(+), 3 deletions(-)
    diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    index a1f1d1b1..82bb00f8 100644
    --- a/releases/Next-ChangeLog.md
    +++ b/releases/Next-ChangeLog.md
    @@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
     # Shared Improvements - Both Mk and Q
    +- Security Improvement: Master seed generation mixes entropy from both Secure
    +  Elements with the STM32 TRNG (previously TRNG only).
     - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
     # Mk Specific Changes
    diff --git a/shared/seed.py b/shared/seed.py
    index e63feb84..4350cc0d 100644
    --- a/shared/seed.py
    +++ b/shared/seed.py
    @@ -600,13 +600,19 @@ async def ephemeral_seed_generate_from_dice(nwords):
             await set_ephemeral_seed_words(words, origin='Dice')
     def generate_seed():
    -    # Generate 32 bytes of best-quality high entropy TRNG bytes.
    +    # Generate 32 bytes of best-quality high entropy from independent sources.
    +    import callgate
         seed = ngu.random.bytes(32)
         assert len(set(seed)) > 4       # TRNG failure
    -    # hash to mitigate any possible bias in TRNG
    -    return ngu.hash.sha256d(seed)
    +    a = callgate.read_rng(1)        # SE1
    +    b = callgate.read_rng(2)        # SE2
    +    assert len(a) == 32
    +    assert len(b) == 8
    +
    +    # hash to combine the sources and mitigate any possible bias
    +    return ngu.hash.sha256d(seed + a + b)
     async def make_new_wallet(nwords):

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

  4. source content difference between and source content +68 -53

    The published patch was force-pushed, replacing the earlier seven-commit series with a five-commit series that revises the mash-entropy implementation, adds a testing fixture, and disables MicroPython threads for the unix port.

    seen · Captured here 64,015 chars
    What changed from the previous capture 121 lines
          n = ngu.hash.sha256d(a+b)
     -    n, = ustruct.unpack('I', n[0:4])
          ngu.random.reseed(n)
    -From 9cf5e44ecf3b2178a0c67b5f54d6957408d04702 Mon Sep 17 00:00:00 2001
    +From 4f58482b4ab047377882fbd13c8011c09bf7ed43 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Tue, 4 Aug 2026 18:44:33 +0200
     Subject: [PATCH 3/7] Require external entropy for all new master seed wallets
      shared/mempad.py           |  80 +++++++--
      shared/mk4.py              |   2 +-
      shared/numpad.py           |  42 ++++-
    - shared/seed.py             | 268 ++++++++++++++++++++++++---
    + shared/seed.py             | 266 ++++++++++++++++++++++++---
    + testing/conftest.py        |  15 ++
      testing/test_ux.py         | 360 ++++++++++++++++++++++++++++++++++++-
      unix/variant/touch.py      |   6 +
    - 9 files changed, 753 insertions(+), 54 deletions(-)
    + 10 files changed, 766 insertions(+), 54 deletions(-)
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
     index d386adc7..0abe1d4c 100644
     --- a/releases/Next-ChangeLog.md
     +        self._changes.put_nowait((key, now))
              self.last_event_time = utime.ticks_ms()
     diff --git a/shared/seed.py b/shared/seed.py
    -index 4350cc0d..b80e39bb 100644
    +index 4350cc0d..3169004f 100644
     --- a/shared/seed.py
     +++ b/shared/seed.py
     @@ -17,9 +17,9 @@
                              if not ok:
                                  redraw = True
                                  continue
    -@@ -614,10 +652,201 @@ def generate_seed():
    +@@ -614,10 +652,199 @@ def generate_seed():
          # hash to combine the sources and mitigate any possible bias
          return ngu.hash.sha256d(seed + a + b)
     +def update_entropy_screen(title, count, target, unit, action, prompt, mk_title=None):
     +    try:
     +        numpad.start_mash()
     +        while True:
    ++            while numpad.empty():
    ++                await sleep_ms(2)
     +            ch, now = await numpad.get_with_timestamp()
     +            if ch == numpad.ABORT_KEY: raise AbortInteraction()
     +            if ch == (KEY_CANCEL if version.has_qwerty else "x"): return
     +
     +async def collect_dice_entropy():
     +    # Collect 128 bits of supplemental entropy from physical D6 rolls.
    -+    from glob import numpad
    -+
     +    md = sha256(DOMAIN_DICE)
     +    count = 0
     +    counter = {}
    ++    done_keys = (KEY_ENTER + KEY_CANCEL) if version.has_qwerty else 'yx'
    ++    press = PressRelease('123456' + done_keys)
     +
     +    update_entropy_screen('Dice Rolls', count, MIN_DICE_ROLLS,
     +                          'rolls', 'rolling', 'Enter each roll: 1-6', 'Roll Dice')
     +    ux_clear_keys()
     +
     +    while True:
    -+        ch, _ = await numpad.get_with_timestamp()
    -+        if ch == numpad.ABORT_KEY: raise AbortInteraction()
    ++        ch = await press.wait()
     +        if ch == (KEY_CANCEL if version.has_qwerty else "x"): return
    -+        if not ch: continue
     +
     +        if count >= MIN_DICE_ROLLS and ch == (KEY_ENTER if version.has_qwerty else "y"):
     +            break
     +
     +async def collect_coin_entropy():
     +    # A coin contributes ~1 bit per flip.
    -+    from glob import numpad
    -+
     +    md = sha256(DOMAIN_COIN)
     +    count = heads = 0
    ++    done_keys = (KEY_ENTER + KEY_CANCEL) if version.has_qwerty else 'yx'
    ++    press = PressRelease('10' + done_keys)
     +
     +    update_entropy_screen('Coin Flips', count, MIN_COIN_FLIPS,
     +                          'flips', 'flipping', '1 = Heads, 0 = Tails', 'Coin: 1=H 0=T')
     +    ux_clear_keys()
     +
     +    while True:
    -+        ch, _ = await numpad.get_with_timestamp()
    -+        if ch == numpad.ABORT_KEY: raise AbortInteraction()
    ++        ch = await press.wait()
     +        if ch == (KEY_CANCEL if version.has_qwerty else "x"): return
    -+        if not ch: continue
     +
     +        if count >= MIN_COIN_FLIPS and ch == (KEY_ENTER if version.has_qwerty else "y"):
     +            break
          words = await approve_word_list(seed, nwords)
          if words:
              await commit_new_words(words)
    -@@ -665,14 +894,14 @@ async def approve_word_list(seed, nwords, ephemeral=False):
    +@@ -665,14 +892,14 @@ async def approve_word_list(seed, nwords, ephemeral=False):
          words = bip39.b2a_words(seed).split(' ')
          assert len(words) == nwords
     -    extra_msg = 'Press (4) to add some dice rolls into the mix. '
              if ch == 'x':
                  # user abort, but confirm it!
                  if await ux_confirm("Throw away those words and stop this process?"):
    -@@ -680,15 +909,6 @@ async def approve_word_list(seed, nwords, ephemeral=False):
    +@@ -680,15 +907,6 @@ async def approve_word_list(seed, nwords, ephemeral=False):
                  else:
                      continue
     -        if ch == '4':
              if ch == '6':
                  # wants to skip the quiz (undocumented)
                  if await ux_confirm("Skipping the quiz means you might have "
    +diff --git a/testing/conftest.py b/testing/conftest.py
    +index 1f06595e..79e0d929 100644
    +--- a/testing/conftest.py
    +@@ -698,6 +698,21 @@ def press_select(dev, has_qwerty):
    +     f = functools.partial(_press_select, dev, has_qwerty)
    +     return f
    [email protected]
    ++def enter_mash_entropy(pick_menu_item, press_select, need_keypress):
    ++    def doit():
    ++        pick_menu_item('Mash Keys')
    ++        time.sleep(.1)
    ++        press_select()
    ++        time.sleep(.1)
    ++        for i in range(128):
    ++            need_keypress(str(i % 10))
    ++
    ++        time.sleep(.2)
    ++        press_select()  # done
    ++
    ++    return doit
    ++
    + @pytest.fixture
    + def press_cancel(need_keypress, has_qwerty):
    +     def doit(**kws):
     diff --git a/testing/test_ux.py b/testing/test_ux.py
     index f8b96977..d122302e 100644
     --- a/testing/test_ux.py
                      # Q1 simulator sends keynumbers, from shared/charcodes.py
                      numpad.process_chg_state(new_presses)
                  else:
    -From 28fcd74f6e65d329d4ff3775658027153360ed5c Mon Sep 17 00:00:00 2001
    +From d8f73c10ad928230541a6a7642fb1ca1e623824e Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 11:43:52 +0200
     Subject: [PATCH 4/7] Warn before dice-only seed generation
      - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
      # Mk Specific Changes
     diff --git a/shared/seed.py b/shared/seed.py
    -index b80e39bb..19063881 100644
    +index 3169004f..24b897c0 100644
     --- a/shared/seed.py
     +++ b/shared/seed.py
     @@ -75,6 +75,13 @@
     +    time.sleep(0.1)
          gave = ''
          for i in range(count):
    -From 41aace3deb79a1080a6404f0829fd5dd44cc8d78 Mon Sep 17 00:00:00 2001
    +From 62a502879c484e2d9d5b7ff7ed2ae1bbea30c464 Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 12:20:43 +0200
     Subject: [PATCH 5/7] external entropy required for CCC key and temporary seeds
      releases/Next-ChangeLog.md |  2 ++
      shared/ccc.py              | 11 +++++++----
      shared/seed.py             | 27 +++++++++++++++++++--------
    - testing/conftest.py        | 11 +++++++++++
      testing/test_ccc.py        |  3 ++-
      testing/test_ephemeral.py  | 11 ++++++++---
    - 6 files changed, 49 insertions(+), 16 deletions(-)
    + 5 files changed, 38 insertions(+), 16 deletions(-)
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
     index 4ec72611..0fcb6b62 100644
     --- a/releases/Next-ChangeLog.md
          else:
              return None
     diff --git a/shared/seed.py b/shared/seed.py
    -index 19063881..a8f29106 100644
    +index 24b897c0..c53f1c36 100644
     --- a/shared/seed.py
     +++ b/shared/seed.py
     @@ -50,6 +50,9 @@
     +PURPOSE_CCC = b'C'
      BAD_DICE_MSG = ('Distribution of dice rolls is not random. '
                      'Some numbers occurred more than 30% of the time.')
    -@@ -814,9 +817,8 @@ async def collect_coin_entropy():
    +@@ -812,9 +815,8 @@ async def collect_coin_entropy():
          return md.digest()
     -async def make_new_wallet(nwords):
     -    # Generate the primary seed first, then require one human entropy source.
          base_seed = None
          extra_entropy = None
          mix = None
    -@@ -831,6 +833,7 @@ async def make_new_wallet(nwords):
    +@@ -829,6 +831,7 @@ async def make_new_wallet(nwords):
          ])
          try:
              base_seed = generate_seed()
     +        await ux_dramatic_pause('Generating...', 3)
              while extra_entropy is None:
                  the_ux.push(choices)
    -@@ -850,14 +853,20 @@ async def make_new_wallet(nwords):
    +@@ -848,14 +851,20 @@ async def make_new_wallet(nwords):
                  extra_entropy = await collector()
     -        mix = DOMAIN_SEED + method + base_seed + extra_entropy
     -        seed = ngu.hash.sha256d(mix)
          words = await approve_word_list(seed, nwords)
          if words:
              await commit_new_words(words)
    -@@ -875,12 +884,14 @@ async def import_done_cb(words):
    +@@ -873,12 +882,14 @@ async def import_done_cb(words):
              return WordNestMenu(nwords, done_cb=import_done_cb)
      async def ephemeral_seed_generate(nwords):
     -    await ux_dramatic_pause('Generating...', 3)
     +        await set_ephemeral_seed_words(words, origin="Generated Words")
      async def set_seed_extended_key(extended_key):
          encoded, chain = xprv_to_encoded_secret(extended_key)
    -diff --git a/testing/conftest.py b/testing/conftest.py
    -index 1f06595e..f5adfe83 100644
    -+++ b/testing/conftest.py
    -@@ -698,6 +698,17 @@ def press_select(dev, has_qwerty):
    -     f = functools.partial(_press_select, dev, has_qwerty)
    -     return f
    [email protected]
    -+def enter_mash_entropy(pick_menu_item, press_select, need_keypress):
    -+    def doit():
    -+        pick_menu_item('Mash Keys')
    -+        press_select()
    -+        for i in range(128):
    -+            need_keypress(str(i % 10))
    -+        press_select()
    -+
    -+    return doit
    -+
    - @pytest.fixture
    - def press_cancel(need_keypress, has_qwerty):
    -     def doit(**kws):
     diff --git a/testing/test_ccc.py b/testing/test_ccc.py
     index e3a88b74..623f5f12 100644
     --- a/testing/test_ccc.py
          time.sleep(0.1)
          need_keypress("6")  # skip quiz
          press_select()
    -From b0996f626906f2882466f30f648af00c21f416f9 Mon Sep 17 00:00:00 2001
    +From 6a55baf02ead10b2c26ec7a4f399e25b8e701fdc Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Wed, 5 Aug 2026 14:48:30 +0200
     Subject: [PATCH 6/7] Bump libngu: SHA-256 Hash-DRBG replaces Yasmarang
      releases/Next-ChangeLog.md | 2 ++
      2 files changed, 3 insertions(+), 1 deletion(-)
     diff --git a/external/libngu b/external/libngu
    -index 537519a8..e18669ba 160000
    +index 537519a8..5d04106a 160000
     --- a/external/libngu
     +++ b/external/libngu
     @@ -1 +1 @@
     -Subproject commit 537519a829259622ea6b0334fbafd6cae852852f
    -+Subproject commit e18669baf9e331f86cacb033ecee6858abbea2dd
    ++Subproject commit 5d04106a16afea6c3aa7802366e1687e55331cf8
     diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
     index 0fcb6b62..28d48077 100644
     --- a/releases/Next-ChangeLog.md
      - Change: New master seeds now require extra user supplied entropy.
          - Choose key mashing (based on [Peter Todd's Push-Button RNG](https://petertodd.org/2014/push-button-rng)),
            physical dice rolls or physical coin flips.
    -From e9ca0db525cae27ca888c8fc658d195888ffeb7d Mon Sep 17 00:00:00 2001
    +From 11d813022ee482d1debcd64c0e8d96ada18cca9d Mon Sep 17 00:00:00 2001
     From: scgbckbone <[email protected]>
     Date: Fri, 7 Aug 2026 09:57:58 +0200
     Subject: [PATCH 7/7] exclude schnorr/musig from libngu to save flash space;
    - add adition macro NGU_STM32_EXTERNAL_RNG_GET
    + add adition macro NGU_STM32_EXTERNAL_RNG_GET; disable threads for unix port
     ---
      stm32/COLDCARD_MK4/mpconfigboard.h  | 4 +++-
      stm32/COLDCARD_MK4/mpconfigboard.mk | 5 ++++-
      stm32/COLDCARD_Q1/mpconfigboard.h   | 5 ++++-
      stm32/COLDCARD_Q1/mpconfigboard.mk  | 5 ++++-
    - 4 files changed, 15 insertions(+), 4 deletions(-)
    + unix/variant/mpconfigvariant.mk     | 4 ++--
    + 5 files changed, 17 insertions(+), 6 deletions(-)
     diff --git a/stm32/COLDCARD_MK4/mpconfigboard.h b/stm32/COLDCARD_MK4/mpconfigboard.h
     index b3ccb7c7..0484ef94 100644
     --- a/stm32/COLDCARD_MK4/mpconfigboard.h
     +NGU_INCL_MUSIG   = 0
      USER_C_MODULES = boards/$(BOARD)/c-modules
      # the bulk of the COLDCARD-specific code
    +diff --git a/unix/variant/mpconfigvariant.mk b/unix/variant/mpconfigvariant.mk
    +index 615858a4..19e547ff 100644
    +--- a/unix/variant/mpconfigvariant.mk
    +@@ -17,8 +17,8 @@ MICROPY_USE_READLINE = 1
    + # btree module using Berkeley DB 1.xx
    + MICROPY_PY_BTREE = 1
    +-# _thread module using pthreads
    +-MICROPY_PY_THREAD = 1
    ++# ngu.random holds mutable DRBG and source-check state.
    ++MICROPY_PY_THREAD = 0
    + # Subset of CPython termios module
    + MICROPY_PY_TERMIOS = 1
    
    Extracted text as captured
    From f460ce3e6fc92772f7aada9709a53f6c0c805d88 Mon Sep 17 00:00:00 2001
    From: scgbckbone <[email protected]>
    Date: Mon, 3 Aug 2026 19:07:13 +0200
    Subject: [PATCH 1/7] Mix secure element entropy into seed generation
    ---
     releases/Next-ChangeLog.md |  2 ++
     shared/seed.py             | 12 +++++++++---
     2 files changed, 11 insertions(+), 3 deletions(-)
    diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    index a1f1d1b1..82bb00f8 100644
    --- a/releases/Next-ChangeLog.md
    +++ b/releases/Next-ChangeLog.md
    @@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
     # Shared Improvements - Both Mk and Q
    +- Security Improvement: Master seed generation mixes entropy from both Secure
    +  Elements with the STM32 TRNG (previously TRNG only).
     - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
     # Mk Specific Changes
    diff --git a/shared/seed.py b/shared/seed.py
    index e63feb84..4350cc0d 100644
    --- a/shared/seed.py
    +++ b/shared/seed.py
    @@ -600,13 +600,19 @@ async def ephemeral_seed_generate_from_dice(nwords):
             await set_ephemeral_seed_words(words, origin='Dice')
     def generate_seed():
    -    # Generate 32 bytes of best-quality high entropy TRNG bytes.
    +    # Generate 32 bytes of best-quality high entropy from independent sources.
    +    import callgate
         seed = ngu.random.bytes(32)
         assert len(set(seed)) > 4       # TRNG failure
    -    # hash to mitigate any possible bias in TRNG
    -    return ngu.hash.sha256d(seed)
    +    a = callgate.read_rng(1)        # SE1
    +    b = callgate.read_rng(2)        # SE2
    +    assert len(a) == 32
    +    assert len(b) == 8
    +
    +    # hash to combine the sources and mitigate any possible bias
    +    return ngu.hash.sha256d(seed + a + b)
     async def make_new_wallet(nwords):

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

  5. Earliest copy held
    seen · Captured here 63,344 chars
    Extracted text as captured
    From f460ce3e6fc92772f7aada9709a53f6c0c805d88 Mon Sep 17 00:00:00 2001
    From: scgbckbone <[email protected]>
    Date: Mon, 3 Aug 2026 19:07:13 +0200
    Subject: [PATCH 1/7] Mix secure element entropy into seed generation
    ---
     releases/Next-ChangeLog.md |  2 ++
     shared/seed.py             | 12 +++++++++---
     2 files changed, 11 insertions(+), 3 deletions(-)
    diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
    index a1f1d1b1..82bb00f8 100644
    --- a/releases/Next-ChangeLog.md
    +++ b/releases/Next-ChangeLog.md
    @@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
     # Shared Improvements - Both Mk and Q
    +- Security Improvement: Master seed generation mixes entropy from both Secure
    +  Elements with the STM32 TRNG (previously TRNG only).
     - Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
     # Mk Specific Changes
    diff --git a/shared/seed.py b/shared/seed.py
    index e63feb84..4350cc0d 100644
    --- a/shared/seed.py
    +++ b/shared/seed.py
    @@ -600,13 +600,19 @@ async def ephemeral_seed_generate_from_dice(nwords):
             await set_ephemeral_seed_words(words, origin='Dice')
     def generate_seed():
    -    # Generate 32 bytes of best-quality high entropy TRNG bytes.
    +    # Generate 32 bytes of best-quality high entropy from independent sources.
    +    import callgate
         seed = ngu.random.bytes(32)
         assert len(set(seed)) > 4       # TRNG failure
    -    # hash to mitigate any possible bias in TRNG
    -    return ngu.hash.sha256d(seed)
    +    a = callgate.read_rng(1)        # SE1
    +    b = callgate.read_rng(2)        # SE2
    +    assert len(a) == 32
    +    assert len(b) == 8
    +
    +    # hash to combine the sources and mitigate any possible bias
    +    return ngu.hash.sha256d(seed + a + b)
     async def make_new_wallet(nwords):

    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.