Allow activation of calculated Codex32 secrets
What changed, and why it matters
This commit adds a new feature to COLDCARD firmware that lets users activate a Bitcoin wallet directly from a Codex32 checksum calculation screen, instead of having to export and re-import the secret. It is a user-experience improvement for legitimate seed setup, not a fix for an attack. The change only affects how a user chooses to turn a valid secret into their wallet, and it still requires the user to confirm the action. There is no direct evidence in the commit that this resolves a security vulnerability.
Review as a normal feature change. Verify that the confirmation prompt and ephemeral flag handling prevent unintended master-seed replacement, and that the new tests cover both temporary-seed and master-seed activation paths. No security patch deployment urgency is indicated by the commit itself.
Security signals we found
Feature addition enabling direct activation of calculated secrets
User confirmation still required before setting master seed
Activation restricted to secret shares (index 'S') only
No cryptographic changes; only UI/workflow changes
No mention of vulnerability, CVE, or security bug in commit message or diff
Evidence from the diff
The patch extends the Codex32 checksum calculator so that when the computed share has index ‘S’ (the secret share), pressing ‘0’ can immediately import it as either the master seed or a temporary seed. The function signatures in shared/actions.py and shared/seed.py are updated to pass the ephemeral flag through the menu, and the UI prompt now offers activation for secret shares. Tests are added to verify the new activation paths and to confirm that non-secret shares do not offer activation.
Changed components
shared/actions.pyshared/seed.pydocs/codex32.mdtesting/test_codex32.pyInspect captured patch +78 / −11
### docs/codex32.md
@@ -182,7 +182,11 @@ The result shows the checksum and completed string, preserving all payload and
padding bits. Entering a complete share shows its existing checksum and leaves
the string unchanged. You can display the completed string as a QR code, send it
via NFC, or save it to MicroSD or Virtual Disk, depending on your device and
-enabled features. It does not import or activate a wallet.
+enabled features. For a secret with index `S`,
+press **0** to activate it without exporting and re-importing it. From the direct
+`Codex32` menu this creates the Master Seed. From `Temporary Seed > Codex32` it
+always creates a Temporary Seed, even when no Master Seed is configured.
+Calculating the checksum alone does not activate a wallet.
Calculating a checksum cannot detect existing transcription mistakes: it computes
a checksum for exactly the header and payload you entered.
### shared/actions.py
@@ -1486,7 +1486,7 @@ def contains_xprv(fname):
await import_extended_key_as_secret(extended_key, ephemeral, origin='Imported XPRV')
# not reached; will do reset.
-async def codex32_calculate_checksum(*a):
+async def codex32_calculate_checksum(_1, _2, item):
from codex32 import Share
value = ""
@@ -1508,7 +1508,7 @@ async def codex32_calculate_checksum(*a):
continue
intro = 'Checksum:\n\n%s\n\nCodex32:\n\n' % share.checksum().upper()
- await show_shamir_share(share.to_string(), share.uid, intro=intro)
+ await show_shamir_share(share.to_string(), share.uid, intro=intro, ephemeral=item.arg)
break
async def import_codex32_as_secret(value, ephemeral, origin=None):
@@ -2692,19 +2692,29 @@ async def edit_custom(menu, picked, xx_self):
async def shamir_share_story(menu, label, item):
await show_shamir_share(*item.arg)
-async def show_shamir_share(value, uid, intro=None):
+async def show_shamir_share(value, uid, intro=None, ephemeral=None):
+ # ephemeral=None: no activation offered; True/False: offer activation as
+ # temporary or master seed for secret shares (index S) only.
from glob import NFC, dis
from seed import render_codex32
- index = value[8]
+ index = value[8].upper()
+ key0 = None
+ if index == 'S' and ephemeral is not None:
+ key0 = 'to use as temporary seed' if ephemeral else 'to use as master seed'
name = "Share '%s'" % index
intro = (intro or '') + render_codex32(value)
while True:
choice = await import_export_prompt(name, title=name, intro=intro,
- sensitive=True)
+ sensitive=True, key0=key0)
if choice == KEY_CANCEL: return
- if choice == KEY_QR:
+ if choice == '0' and key0:
+ if ephemeral or await ux_confirm('This share becomes the master seed of this'
+ ' device.', title='Master Seed'):
+ return await import_codex32_as_secret(value, ephemeral=ephemeral)
+
+ elif choice == KEY_QR:
await show_qr_code(value, is_alnum=True, msg=name, is_secret=True)
elif choice == KEY_NFC:
### shared/seed.py
@@ -1641,7 +1641,7 @@ async def make_codex32_menu(menu, label, item):
MenuItem('Shamir Recover', f=codex32_shamir_recover, arg=ephemeral),
MenuItem('Derive Shares', f=codex32_derive_shares, predicate=not_hobbled_mode),
MenuItem('Calculate Checksum' if version.has_qwerty else 'Calc Checksum',
- f=codex32_calculate_checksum),
+ f=codex32_calculate_checksum, arg=ephemeral),
])
async def start_b39_pw(menu, label, item):
### testing/test_codex32.py
@@ -247,19 +247,22 @@ def doit(active=None, words=None):
return doit
-@pytest.mark.parametrize('text', [SHARES[0], CW_SHARE_A, SHARES[3]])
+@pytest.mark.parametrize('text', [SHARES[0], CW_SHARE_A, CW_SHARES[0], SHARES[3]])
def test_calculate_checksum_manual(text, goto_codex32_menu, pick_menu_item, cap_story,
cap_screen, press_select, enter_bech32, press_cancel,
cap_menu, sim_exec, is_q1, enable_nfc, enable_hw_ux,
load_export, need_keypress, microsd_path, virtdisk_path,
- garbage_collector, is_headless, cap_screen_qr, goto_home):
+ garbage_collector, is_headless, cap_screen_qr, goto_home,
+ settings_set, active_secret, confirm_tmp_seed, reset_seed_words):
goto_home()
+ settings_set('seedvault', False)
enable_nfc()
enable_hw_ux('vdisk')
goto_codex32_menu(tmp=True)
snapshot = ('RV.write(repr((bytes(pa.fetch(bypass_tmp=True)), pa.tmp_value, '
'settings.nvram_key, settings.current)))')
before = sim_exec(snapshot)
+ master = sim_exec('RV.write(repr(bytes(pa.fetch(bypass_tmp=True))))')
pick_menu_item('Calculate Checksum' if is_q1 else 'Calc Checksum')
assert 'cannot detect existing transcription mistakes' in cap_story()[1]
assert '(0) to enter manually' in cap_story()[1]
@@ -286,6 +289,10 @@ def test_calculate_checksum_manual(text, goto_codex32_menu, pick_menu_item, cap_
assert parse_rendered_codex32(story.split('Codex32:', 1)[1]) == text.upper()
assert 'to show QR code' in story
assert 'to share via NFC' in story
+ is_secret = text[8].lower() == SECRET
+ assert ('(0)' in story) == is_secret
+ assert ('(0) to use as temporary seed' in story) == is_secret
+ assert '(0) to use as master seed' not in story
for way, path_f in [('sd', microsd_path), ('vdisk', virtdisk_path)]:
if way == 'sd':
need_keypress('1')
@@ -304,6 +311,14 @@ def test_calculate_checksum_manual(text, goto_codex32_menu, pick_menu_item, cap_
press_cancel()
time.sleep(.2)
assert sim_exec(snapshot) == before
+ if is_secret:
+ need_keypress('0')
+ node = bip32_node_from_codex32_share(Share.parse(text))
+ confirm_tmp_seed(expect_xfp=node.fingerprint().hex().upper())
+ assert active_secret() == native_encoding(text).hex()
+ assert sim_exec('RV.write(repr(bytes(pa.fetch(bypass_tmp=True))))') == master
+ reset_seed_words()
+ return
press_cancel()
time.sleep(.2)
assert ('Calculate Checksum' if is_q1 else 'Calc Checksum') in cap_menu()
@@ -334,6 +349,40 @@ def test_calculate_checksum_full_share(text, goto_codex32_menu, pick_menu_item,
assert ('Calculate Checksum' if is_q1 else 'Calc Checksum') in cap_menu()
+@pytest.mark.parametrize('tmp', [False, True])
+def test_calculate_checksum_seedless_activation(tmp, unit_test, goto_codex32_menu,
+ pick_menu_item, press_select,
+ enter_bech32, cap_story, need_keypress,
+ confirm_tmp_seed, expect_ftux, is_q1,
+ active_secret, sim_exec, reset_seed_words):
+ unit_test('devtest/clear_seed.py')
+ try:
+ goto_codex32_menu(tmp=tmp)
+ pick_menu_item('Calculate Checksum' if is_q1 else 'Calc Checksum')
+ need_keypress('0')
+ time.sleep(.2)
+ enter_bech32(SHARES[0][:-13])
+ time.sleep(.2)
+ story = cap_story()[1]
+ assert ('(0) to use as temporary seed' in story) == tmp
+ assert ('(0) to use as master seed' in story) == (not tmp)
+ need_keypress('0')
+ if tmp:
+ confirm_tmp_seed()
+ else:
+ title, story = cap_story()
+ assert title == 'Master Seed'
+ assert 'becomes the master seed' in story
+ press_select()
+ expect_ftux()
+ assert active_secret() == native_encoding(SHARES[0]).hex()
+ assert sim_exec('RV.write(str(pa.is_secret_blank()))') == str(tmp)
+ assert sim_exec('RV.write(str(pa.tmp_value is not None))') == str(tmp)
+ finally:
+ reset_seed_words()
+
+
+
@pytest.mark.parametrize('case', ['lower', 'upper', 'mixed'])
@pytest.mark.parametrize('from_editor', [False, True])
def test_calculate_checksum_scan_case(case, from_editor, is_q1, goto_codex32_menu, pick_menu_item,
@@ -553,6 +602,7 @@ def doit(way, num_shares, threshold, hrp=None, sec_length=None):
pick_menu_item(label)
title, story = cap_story()
assert title == label
+ assert '(0)' not in story
value = parse_rendered_codex32(story)
share = Share.parse(value)
assert share.threshold == threshold
@@ -1540,7 +1590,9 @@ def test_derive_codex32_shares(hrp, size, state, threshold, reset_seed_words, un
secret = generate_share(shares, SECRET).to_string()
for index in output_indices[:2]:
pick_menu_item("Share '%s'" % index)
- value = parse_rendered_codex32(cap_story()[1])
+ story = cap_story()[1]
+ assert '(0)' not in story
+ value = parse_rendered_codex32(story)
if index == output_indices[0]:
assert value == expected
assert value == generate_share(shares, index.lower()).to_string()
@@ -1566,6 +1618,7 @@ def test_derive_codex32_shares(hrp, size, state, threshold, reset_seed_words, un
assert 'Exit and discard collected shares?' in story
press_cancel() # keep the session, including its original inputs
pick_menu_item("Share '%s'" % output_indices[0])
+ assert '(0)' not in cap_story()[1]
assert parse_rendered_codex32(cap_story()[1]) == expected
press_cancel()
press_cancel()Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.