Merge branch 'master' of github.com:Coldcard/firmware
This commit is contained in:
commit
e9d17e5efb
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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 = []
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user