Merge branch 'master' of github.com:Coldcard/firmware

This commit is contained in:
Peter D. Gray 2025-01-17 09:05:12 -05:00
commit e9d17e5efb
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
10 changed files with 110 additions and 64 deletions

View File

@ -7,14 +7,18 @@ This lists the new changes that have not yet been published in a normal release.
- Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Enhancement: Ability to switch between BIP-32 XPUB and SLIP-132 garbage in `Export XPUB`
- Enhancement: Use the fact that master seed cannot be used as ephemeral and add UX message
for successful master seed verification.
- Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence.
On Q, result is blank screen, on Mk4, result is three-dots screen.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode
- Bugfix: Bless Firmware causes hanging progress bar
- Bugfix: Prevent yikes in ownership search
- Bugfix: Factory-disabled NFC was not recognized correctly.
- Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault
- Change: Do not include sighash in PSBT input data, if sighash value is SIGHASH_ALL
- Bugfix: Factory-disabled NFC was not recognized correctly.
- Change: Testnet3 -> Testnet4 (all parameters are the same)
# Mk4 Specific Changes

View File

@ -1009,6 +1009,7 @@ async def export_xpub(label, _2, item):
chain = chains.current_chain()
acct = 0
slip132 = False # non-slip is default from Oct 2024
# decode menu code => standard derivation
mode = item.arg
@ -1024,24 +1025,44 @@ async def export_xpub(label, _2, item):
else:
remap = {44:0, 49:1, 84:2}[mode]
_, path, addr_fmt = chains.CommonDerivations[remap]
path = path.format(account='{acct}', coin_type=chain.b44_cointype, change=0, idx=0)[:-4]
# always show SLIP-132 style, because defacto
show_slip132 = (addr_fmt != AF_CLASSIC)
path = path.format(account=acct, coin_type=chain.b44_cointype,
change=0, idx=0)[:-4]
while 1:
msg = '''Show QR of the XPUB for path:\n\n%s\n\n''' % path
msg = 'Show QR of the XPUB for path:\n\n%s\n\n' % path
esc = ""
if path != "m":
esc += "1"
msg += "Press (1) to select account other than %s. " % (acct or "zero")
if addr_fmt != AF_CLASSIC:
esc += "2"
slp_af = addr_fmt
if slip132:
slp_af = AF_CLASSIC
if '{acct}' in path:
msg += "Press (1) to select account other than zero. "
slp = chain.slip132[slp_af].hint + "pub"
msg += " Press (2) to show %s %s." % (
slp, "(BIP-32)" if slip132 else "(SLIP-132)"
)
if glob.NFC:
msg += "Press %s to share via NFC. " % (KEY_NFC if version.has_qwerty else "(3)")
if version.has_qwerty:
esc += KEY_NFC
key_hint = KEY_NFC
else:
esc += "3"
key_hint = "(3)"
msg += " Press %s to share via NFC. " % key_hint
ch = await ux_show_story(msg, escape='13')
ch = await ux_show_story(msg, escape=esc)
if ch == 'x': return
if ch == "2":
slip132 = not slip132
continue
if ch == '1':
acct = await ux_enter_bip32_index('Account Number:') or 0
path = path.format(acct=acct)
pth_split = path.split("/")
pth_split[-1] = ("%dh" % acct)
path = "/".join(pth_split)
continue
# assume zero account if not picked
@ -1053,7 +1074,7 @@ async def export_xpub(label, _2, item):
# render xpub/ypub/zpub
with stash.SensitiveValues() as sv:
node = sv.derive_path(path) if path != 'm' else sv.node
xpub = chain.serialize_public(node, addr_fmt)
xpub = chain.serialize_public(node, addr_fmt if slip132 else AF_CLASSIC)
from ownership import OWNERSHIP
OWNERSHIP.note_wallet_used(addr_fmt, acct)
@ -1063,8 +1084,6 @@ async def export_xpub(label, _2, item):
else:
await show_qr_code(xpub, False)
break
def electrum_export_story(background=False):
# saves memory being in a function

View File

@ -179,8 +179,7 @@ class AddressListMenu(MenuSystem):
# Create list of choices (address_index_0, path, addr_fmt)
choices = []
for name, path, addr_fmt in chains.CommonDerivations:
if '{coin_type}' in path:
path = path.replace('{coin_type}', str(chain.b44_cointype))
path = path.replace('{coin_type}', str(chain.b44_cointype))
if self.account_num != 0 and '{account}' not in path:
# skip derivations that are not affected by account number

View File

@ -29,8 +29,6 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
class ChainsBase:
curve = 'secp256k1'
menu_name = None # use 'name' if this isn't defined
core_name = None # name of chain's "core" p2p software
# b44_cointype comes from
# <https://github.com/satoshilabs/slips/blob/master/slip-0044.md>
@ -290,8 +288,7 @@ class ChainsBase:
class BitcoinMain(ChainsBase):
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
ctype = 'BTC'
name = 'Bitcoin'
core_name = 'Bitcoin Core'
name = 'Bitcoin Mainnet'
slip132 = {
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
@ -310,9 +307,9 @@ class BitcoinMain(ChainsBase):
b44_cointype = 0
class BitcoinTestnet(BitcoinMain):
# testnet4 (was testnet3 up until 2025 but all parameters are the same)
ctype = 'XTN'
name = 'Bitcoin Testnet'
menu_name = 'Testnet: BTC'
name = 'Bitcoin Testnet 4'
slip132 = {
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
@ -334,7 +331,6 @@ class BitcoinTestnet(BitcoinMain):
class BitcoinRegtest(BitcoinMain):
ctype = 'XRT'
name = 'Bitcoin Regtest'
menu_name = 'Regtest: BTC'
slip132 = {
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),

View File

@ -73,14 +73,7 @@ be needed for different systems.
sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp))
for name, path, addr_fmt in chains.CommonDerivations:
if '{coin_type}' in path:
path = path.replace('{coin_type}', str(chain.b44_cointype))
if '{' in name:
name = name.format(core_name=chain.core_name)
show_slip132 = ('Core' not in name)
path = path.replace('{coin_type}', str(chain.b44_cointype))
yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path))
yield ('''First %d receive addresses (account=0, change=0):\n\n''' % num_rx)
@ -103,7 +96,7 @@ be needed for different systems.
node = sv.derive_path(hard_sub, register=False)
yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node)))
if show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
if addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
yield ("%s => %s ##SLIP-132##\n" % (
hard_sub, chain.serialize_public(node, addr_fmt)))

View File

@ -176,7 +176,7 @@ XpubExportMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
MenuItem("Classic (BIP-44)", f=export_xpub, arg=44),
MenuItem("P2WPKH/P2SH (49)", f=export_xpub, arg=49),
MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49),
MenuItem("Master XPUB", f=export_xpub, arg=0),
MenuItem("Current XFP", f=export_xpub, arg=-1),
]
@ -304,7 +304,7 @@ If you disable sighash flag restrictions, and ignore the \
warnings, funds can be stolen by specially crafted PSBT or MitM.
Keep blocked unless you intend to sign special transactions.'''),
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3', 'Regtest'],
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet4', 'Regtest'],
value_map=['BTC', 'XTN', 'XRT'],
on_change=change_which_chain,
story="Testnet must only be used by developers because \

View File

@ -216,8 +216,7 @@ class OwnershipCache:
addr_fmt = ch.possible_address_fmt(addr)
if not addr_fmt:
# might be valid address over on testnet vs mainnet
nm = ch.name if ch.ctype != 'BTC' else 'Bitcoin Mainnet'
raise UnknownAddressExplained('That address is not valid on ' + nm)
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
possibles = []

View File

@ -473,6 +473,7 @@ class PinAttempt:
def tmp_secret(self, encoded, chain=None, bip39pw=''):
# Use indicated secret and stop using the SE; operate like this until reboot
from glob import settings
from utils import xfp2str
from nvstore import SettingsObject
val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
@ -483,7 +484,9 @@ class PinAttempt:
target_nvram_key = None
if encoded is not None:
# disallow using master seed as temporary
master_err = "Cannot use master seed as temporary."
xfp = xfp2str(settings.master_get("xfp", 0))
master_err = ("Cannot use master seed as temporary. BUT you have just successfully "
"tested recovery of your master seed [%s].") % xfp
target_nvram_key = settings.hash_key(val)
if SettingsObject.master_nvram_key:
assert self.tmp_value

View File

@ -1405,6 +1405,7 @@ def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story,
title, story = cap_story()
assert "FAILED" == title
assert 'Cannot use master seed as temporary.' in story
assert 'tested recovery of your master seed' in story
press_cancel()
# go to ephemeral seed and then try to create new ephemeral seed from master
@ -1433,6 +1434,7 @@ def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story,
title, story = cap_story()
assert "FAILED" == title
assert 'Cannot use master seed as temporary.' in story
assert 'tested recovery of your master seed' in story
press_cancel()
# now import same seed but represented as master extended key

View File

@ -519,15 +519,15 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi
@pytest.mark.qrcode
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
@pytest.mark.parametrize('acct_num', [ None, 0, 99, 8989])
@pytest.mark.parametrize('use_nfc', [False, True])
def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home,
def test_export_xpub(chain, acct_num, dev, cap_menu, pick_menu_item, goto_home,
cap_story, need_keypress, enter_number, cap_screen_qr,
use_mainnet, nfc_read_text, is_q1, press_select, press_cancel,
settings_set, nfc_read_text, is_q1, press_select, press_cancel,
press_nfc, expect_acctnum_captured):
# XPUB's via QR
use_mainnet()
settings_set("chain", chain)
chain_num = 0 if chain == "BTC" else 1
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('Export Wallet')
@ -537,11 +537,11 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
for m in top_items:
is_xfp = False
if '-84' in m:
expect = "m/84h/0h/{acct}h"
expect = f"m/84h/{chain_num}h/{{acct}}h"
elif '-44' in m:
expect = "m/44h/0h/{acct}h"
expect = f"m/44h/{chain_num}h/{{acct}}h"
elif '49' in m:
expect = "m/49h/0h/{acct}h"
expect = f"m/49h/{chain_num}h/{{acct}}h"
elif 'Master' in m:
expect = "m"
elif 'XFP' in m:
@ -551,17 +551,21 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
time.sleep(0.3)
if is_xfp:
got = cap_screen_qr().decode('ascii')
if use_nfc:
press_nfc()
assert got == xfp2str(simulator_fixed_xfp).upper()
press_cancel()
time.sleep(.1)
press_nfc()
time.sleep(.2)
nfc_got = nfc_read_text()
time.sleep(.2)
assert nfc_got == got == xfp2str(simulator_fixed_xfp).upper()
press_cancel() # cancel animation
press_cancel() # cancel QR
continue
time.sleep(0.3)
title, story = cap_story()
assert expect in story
assert expect.format(acct=0) in story
if 'acct' in expect:
if expect != "m":
assert "Press (1) to select account" in story
if acct_num is not None:
need_keypress('1')
@ -571,24 +575,52 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
expect = expect.format(acct=acct_num)
title, story = cap_story()
assert expect in story
assert "Press (1) to select account" not in story
assert "Press (1) to select account" in story
expect = expect.format(acct=0)
if not use_nfc:
press_select()
got_pub = cap_screen_qr().decode('ascii')
else:
if f'Press {KEY_NFC if is_q1 else "(3)"}' not in story:
raise pytest.skip("NFC disabled")
expect = expect.format(acct=0)
press_select()
got_pub = cap_screen_qr().decode('ascii')
if f'Press {KEY_NFC if is_q1 else "(3)"}' in story:
assert 'NFC' in story
press_nfc()
time.sleep(0.2)
got_pub = nfc_read_text()
got_nfc_pub = nfc_read_text()
time.sleep(0.1)
#press_select()
press_cancel() # cancel animation
press_cancel() # cancel QR
assert got_nfc_pub == got_pub
if got_pub[0] not in 'xt':
got_pub,*_ = slip132undo(got_pub)
time.sleep(.1)
_, story = cap_story()
assert got_pub[0] in 'xt'
if "Press (2)" in story:
if chain == "BTC":
assert f"{'z' if expect[:5] == 'm/84h' else 'y'}pub (SLIP-132)" in story
else:
assert f"{'v' if expect[:5] == 'm/84h' else 'u'}pub (SLIP-132)" in story
need_keypress("2")
time.sleep(.1)
_, story = cap_story()
assert ("%spub (BIP-32)" % ("x" if chain == "BTC" else "t")) in story
assert "Press (2)" in story
press_select()
got_slip_pub = cap_screen_qr().decode('ascii')
got_unslip, *_ = slip132undo(got_slip_pub)
assert got_unslip == got_pub
if f'Press {KEY_NFC if is_q1 else "(3)"}' in story:
assert 'NFC' in story
press_nfc()
time.sleep(0.2)
got_nfc_slip_pub = nfc_read_text()
time.sleep(0.1)
press_cancel() # cancel animation
assert got_slip_pub == got_nfc_slip_pub
press_cancel() # cancel QR
expect_acctnum_captured(acct_num)
@ -598,7 +630,6 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
if expect != 'm':
wallet = wallet.subkey_for_path(expect[2:].replace('h', "'"))
assert got.sec() == wallet.sec()
press_cancel()
@pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"])