default non-root derivation paths for sd/nfc msg signing
This commit is contained in:
parent
85ff2dcf45
commit
a8202972b3
@ -26,6 +26,12 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
# Mk4 Specific Changes
|
||||
|
||||
## 5.4.1 - 2024-??-??
|
||||
- Change: If derivation path is omitted during message signing, default is used
|
||||
based on address format (`m/44h/0h/0h/0/0` for p2pkh, and `m/84h/0h/0h/0/0` for p2wpkh).
|
||||
Default is no longer root (m).
|
||||
|
||||
|
||||
## 5.4.? - 2024-??-??
|
||||
|
||||
- Enhancement: Export single sig descriptor with simple QR.
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ from ux import ux_aborted, ux_show_story, abort_and_goto, ux_dramatic_pause, ux_
|
||||
from ux import show_qr_code, OK, X
|
||||
from usb import CCBusyError
|
||||
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path
|
||||
from utils import B2A, parse_addr_fmt_str, to_ascii_printable
|
||||
from utils import B2A, parse_addr_fmt_str, to_ascii_printable, parse_msg_sign_request
|
||||
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
|
||||
from files import CardSlot
|
||||
from exceptions import HSMDenied
|
||||
@ -303,8 +303,13 @@ def validate_text_for_signing(text):
|
||||
return result
|
||||
|
||||
class ApproveMessageSign(UserAuthorizedAction):
|
||||
def __init__(self, text, subpath, addr_fmt, approved_cb=None):
|
||||
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
|
||||
msg_sign_request=None):
|
||||
super().__init__()
|
||||
|
||||
if msg_sign_request:
|
||||
text, subpath, addr_fmt = parse_msg_sign_request(msg_sign_request)
|
||||
|
||||
self.text = validate_text_for_signing(text)
|
||||
self.subpath = cleanup_deriv_path(subpath)
|
||||
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
|
||||
@ -362,25 +367,8 @@ async def sign_txt_file(filename):
|
||||
# sign a one-line text file found on a MicroSD card
|
||||
# - not yet clear how to do address types other than 'classic'
|
||||
from files import CardSlot, CardMissingError
|
||||
|
||||
from ux import the_ux
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
|
||||
# copy message into memory
|
||||
with CardSlot() as card:
|
||||
with card.open(filename, 'rt') as fd:
|
||||
text = fd.readline().strip()
|
||||
subpath = fd.readline().strip()
|
||||
addr_fmt = fd.readline().strip()
|
||||
|
||||
if not subpath:
|
||||
# default: top of wallet.
|
||||
subpath = 'm'
|
||||
|
||||
if not addr_fmt:
|
||||
addr_fmt = AF_CLASSIC
|
||||
|
||||
async def done(signature, address, text):
|
||||
# complete. write out result
|
||||
from glob import dis
|
||||
@ -442,9 +430,19 @@ async def sign_txt_file(filename):
|
||||
msg = "Created new file:\n\n%s" % out_fn
|
||||
await ux_show_story(msg, title='File Signed')
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
UserAuthorizedAction.check_busy()
|
||||
|
||||
# copy message into memory
|
||||
with CardSlot() as card:
|
||||
with card.open(filename, 'rt') as fd:
|
||||
res = fd.read()
|
||||
|
||||
try:
|
||||
UserAuthorizedAction.active_request = ApproveMessageSign(text, subpath, addr_fmt, approved_cb=done)
|
||||
UserAuthorizedAction.active_request = ApproveMessageSign(
|
||||
None, None, None, approved_cb=done,
|
||||
msg_sign_request=res
|
||||
)
|
||||
# do not kill the menu stack!
|
||||
the_ux.push(UserAuthorizedAction.active_request)
|
||||
except AssertionError as exc:
|
||||
@ -1039,7 +1037,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
msg.write("\n")
|
||||
|
||||
# if we didn't already show all outputs, then give user a chance to
|
||||
# if we didn't already show all outputs, then give user a chance to
|
||||
# view them individually
|
||||
return needs_txn_explorer
|
||||
|
||||
@ -1396,7 +1394,7 @@ class ShowAddressBase(UserAuthorizedAction):
|
||||
|
||||
else:
|
||||
# finish the Wait...
|
||||
dis.progress_bar_show(1)
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
if self.restore_menu:
|
||||
self.pop_menu()
|
||||
|
||||
@ -667,7 +667,7 @@ class NFCHandler:
|
||||
if len(m) < 70:
|
||||
return
|
||||
m = m.decode()
|
||||
|
||||
|
||||
# multi( catches both multi( and sortedmulti(
|
||||
if 'pub' in m or "multi(" in m:
|
||||
return m
|
||||
@ -742,7 +742,7 @@ class NFCHandler:
|
||||
from ux import the_ux
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
|
||||
|
||||
def f(m):
|
||||
m = m.decode()
|
||||
split_msg = m.split("\n")
|
||||
@ -754,21 +754,11 @@ class NFCHandler:
|
||||
if not winner:
|
||||
return
|
||||
|
||||
if len(winner) == 1:
|
||||
text = winner[0]
|
||||
subpath = "m"
|
||||
addr_fmt = AF_CLASSIC
|
||||
elif len(winner) == 2:
|
||||
text, subpath = winner
|
||||
addr_fmt = AF_CLASSIC # maybe default to native segwit?
|
||||
else:
|
||||
# len(winner) == 3
|
||||
text, subpath, addr_fmt = winner
|
||||
|
||||
UserAuthorizedAction.check_busy(ApproveMessageSign)
|
||||
try:
|
||||
UserAuthorizedAction.active_request = ApproveMessageSign(
|
||||
text, subpath, addr_fmt, approved_cb=self.msg_sign_done
|
||||
None, None, None, approved_cb=self.msg_sign_done,
|
||||
msg_sign_request=winner
|
||||
)
|
||||
the_ux.push(UserAuthorizedAction.active_request)
|
||||
except AssertionError as exc:
|
||||
@ -784,7 +774,7 @@ class NFCHandler:
|
||||
|
||||
async def verify_sig_nfc(self):
|
||||
from auth import verify_armored_signed_msg
|
||||
|
||||
|
||||
f = lambda x: x.decode().strip() if b"SIGNED MESSAGE" in x else None
|
||||
winner = await self._nfc_reader(f, 'Unable to find signed message.')
|
||||
|
||||
@ -794,7 +784,7 @@ class NFCHandler:
|
||||
async def verify_address_nfc(self):
|
||||
# Get an address or complete bip-21 url even and search it... slow.
|
||||
from utils import decode_bip21_text
|
||||
|
||||
|
||||
def f(m):
|
||||
m = m.decode()
|
||||
what, vals = decode_bip21_text(m)
|
||||
@ -814,7 +804,7 @@ class NFCHandler:
|
||||
async def read_tapsigner_b64_backup(self):
|
||||
f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None
|
||||
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
|
||||
|
||||
|
||||
async def _nfc_reader(self, func, fail_msg):
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
@ -11,6 +11,13 @@ from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
|
||||
B2A = lambda x: str(b2a_hex(x), 'ascii')
|
||||
|
||||
STD_DERIVATIONS = {
|
||||
"p2pkh": "m/44h/{chain}h/0h/0/0",
|
||||
"p2sh-p2wpkh": "m/49h/{chain}h/0h/0/0",
|
||||
"p2wpkh-p2sh": "m/49h/{chain}h/0h/0/0",
|
||||
"p2wpkh": "m/84h/{chain}h/0h/0/0",
|
||||
}
|
||||
|
||||
try:
|
||||
from font_iosevka import FontIosevka
|
||||
DOUBLE_WIDE = FontIosevka.DOUBLE_WIDE
|
||||
@ -416,7 +423,7 @@ def check_firmware_hdr(hdr, binary_size):
|
||||
ok = (hw_compat & MK_4_OK)
|
||||
elif hw_label == 'q1':
|
||||
ok = (hw_compat & MK_Q1_OK)
|
||||
|
||||
|
||||
if not ok:
|
||||
return "That firmware doesn't support this version of Coldcard hardware (%s)."%hw_label
|
||||
|
||||
@ -701,4 +708,28 @@ def decode_bip21_text(got):
|
||||
def encode_seed_qr(words):
|
||||
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
|
||||
|
||||
def parse_msg_sign_request(data):
|
||||
lines = data.split("\n")
|
||||
assert len(lines) >= 1, "min 1 line"
|
||||
assert len(lines) <= 3, "max 3 lines"
|
||||
|
||||
subpath = ""
|
||||
addr_fmt = "p2pkh"
|
||||
if len(lines) == 1:
|
||||
text = lines[0]
|
||||
elif len(lines) == 2:
|
||||
text, subpath = lines
|
||||
else:
|
||||
text, subpath, addr_fmt = lines
|
||||
if not addr_fmt:
|
||||
addr_fmt = "p2pkh"
|
||||
|
||||
if not subpath:
|
||||
subpath = STD_DERIVATIONS[addr_fmt]
|
||||
subpath = subpath.format(
|
||||
chain=chains.current_chain().b44_cointype
|
||||
)
|
||||
|
||||
return text, subpath, addr_fmt
|
||||
|
||||
# EOF
|
||||
|
||||
@ -11,6 +11,20 @@ from ckcc_protocol.constants import *
|
||||
from constants import addr_fmt_names, msg_sign_unmap_addr_fmt
|
||||
|
||||
|
||||
def default_derivation_by_af(addr_fmt, testnet=True):
|
||||
b44ct = "1" if testnet else "0"
|
||||
if addr_fmt == AF_CLASSIC:
|
||||
path = "m/44h/{chain}h/0h/0/0"
|
||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||
path = "m/49h/{chain}h/0h/0/0"
|
||||
elif addr_fmt == AF_P2WPKH:
|
||||
path = "m/84h/{chain}h/0h/0/0"
|
||||
else:
|
||||
assert False, "unsupported address format"
|
||||
|
||||
return path.format(chain=b44ct)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('msg', [ 'aZ', 'hello', 'abc def eght', "x"*140, 'a'*240])
|
||||
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'", 'm/23h/22h'])
|
||||
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH ])
|
||||
@ -82,7 +96,7 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
|
||||
# sign a file on the microSD card
|
||||
|
||||
def doit(msg, subpath=None, addr_fmt=None, expect_fail=False):
|
||||
def doit(msg, subpath="", addr_fmt=None, expect_fail=False, testnet=True):
|
||||
fname = 't-msgsign.txt'
|
||||
result_fname = 't-msgsign-signed.txt'
|
||||
|
||||
@ -92,10 +106,10 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
|
||||
with open_microsd(fname, 'wt') as sd:
|
||||
sd.write(msg + '\n')
|
||||
if subpath is not None:
|
||||
sd.write(subpath + '\n')
|
||||
if addr_fmt is not None:
|
||||
sd.write(addr_fmt_names[addr_fmt] + '\n')
|
||||
if subpath or addr_fmt:
|
||||
sd.write((subpath or "") + '\n')
|
||||
if addr_fmt is not None:
|
||||
sd.write(addr_fmt_names[addr_fmt])
|
||||
|
||||
goto_home()
|
||||
pick_menu_item('Advanced/Tools')
|
||||
@ -120,7 +134,9 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
assert msg in story
|
||||
assert 'Using the key associated' in story
|
||||
if not subpath:
|
||||
assert 'm =>' in story
|
||||
assert 'm =>' not in story
|
||||
pth = default_derivation_by_af(addr_fmt or AF_CLASSIC, testnet)
|
||||
assert pth in story
|
||||
else:
|
||||
x_subpath = subpath.lower().replace("'", "h")
|
||||
assert ('%s =>' % x_subpath) in story
|
||||
@ -148,6 +164,7 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.mark.bitcoind # only for testnet and p2pkh
|
||||
@pytest.mark.parametrize('msg', [ 'ab', 'hello', 'abc def eght', "x"*140, 'a'*240])
|
||||
@pytest.mark.parametrize('path', [
|
||||
"m/84'/0'/22'",
|
||||
@ -163,24 +180,29 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
AF_CLASSIC,
|
||||
AF_P2WPKH_P2SH,
|
||||
])
|
||||
def test_sign_msg_microsd_good(sign_on_microsd, msg, path, addr_vs_path, addr_fmt):
|
||||
|
||||
if (path is None) and (addr_fmt is not None):
|
||||
# must give path if addr fmt is to be specified
|
||||
return
|
||||
@pytest.mark.parametrize("testnet", [True, False])
|
||||
def test_sign_msg_microsd_good(sign_on_microsd, msg, path, addr_vs_path,
|
||||
addr_fmt, testnet, settings_set, bitcoind):
|
||||
|
||||
settings_set("chain", "XTN" if testnet else "BTC")
|
||||
# cases we expect to work
|
||||
sig, addr = sign_on_microsd(msg, path, addr_fmt)
|
||||
sig, addr = sign_on_microsd(msg, path, addr_fmt, testnet=testnet)
|
||||
|
||||
raw = b64decode(sig)
|
||||
assert 40 <= len(raw) <= 65
|
||||
|
||||
if path is None:
|
||||
path = 'm'
|
||||
if addr_fmt is None:
|
||||
addr_fmt = AF_CLASSIC
|
||||
|
||||
if not path:
|
||||
path = default_derivation_by_af(addr_fmt, testnet=testnet)
|
||||
|
||||
# check expected addr was used
|
||||
addr_vs_path(addr, path, addr_fmt)
|
||||
addr_vs_path(addr, path, addr_fmt, testnet=testnet)
|
||||
assert verify_message(addr, sig, msg) is True
|
||||
if addr_fmt == AF_CLASSIC and testnet:
|
||||
res = bitcoind.rpc.verifymessage(addr, sig, msg)
|
||||
assert res is True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -210,7 +232,8 @@ def sign_using_nfc(goto_home, pick_menu_item, nfc_write_text, cap_story):
|
||||
('testêtest', "must be ascii printable", 0),
|
||||
])
|
||||
@pytest.mark.parametrize('transport', ['sd', 'usb', 'nfc'])
|
||||
def test_sign_msg_fails(dev, sign_on_microsd, msg, concern, no_file, transport, sign_using_nfc, path='m/12/34'):
|
||||
def test_sign_msg_fails(dev, sign_on_microsd, msg, concern, no_file,
|
||||
transport, sign_using_nfc, path='m/12/34'):
|
||||
|
||||
if transport == 'usb':
|
||||
with pytest.raises(CCProtoError) as ee:
|
||||
@ -229,7 +252,7 @@ def test_sign_msg_fails(dev, sign_on_microsd, msg, concern, no_file, transport,
|
||||
assert ("No suitable files found" in str(e)) or story == 'NO-FILE'
|
||||
return
|
||||
elif transport == 'nfc':
|
||||
title, story = sign_using_nfc(msg, expect_fail=True)
|
||||
title, story = sign_using_nfc(msg+"\n"+path, expect_fail=True)
|
||||
assert title == 'ERROR' or "Problem" in story
|
||||
else:
|
||||
raise ValueError(transport)
|
||||
@ -301,11 +324,16 @@ def test_nfc_msg_signing_invalid(body, goto_home, pick_menu_item, nfc_write_text
|
||||
title, story = cap_story()
|
||||
assert title == 'ERROR' or "Problem" in story
|
||||
|
||||
|
||||
@pytest.mark.bitcoind # only for testnet and p2pkh
|
||||
@pytest.mark.parametrize("testnet", [True, False])
|
||||
@pytest.mark.parametrize("msg", ["coinkite", "Coldcard Signing Device!", 200 * "a"])
|
||||
@pytest.mark.parametrize("path", ["", "m/84'/0'/0'/300/0", "m/800h/0h", "m/0/0/0/0/1/1/1"])
|
||||
@pytest.mark.parametrize("str_addr_fmt", ["p2pkh", "", "p2wpkh", "p2wpkh-p2sh", "p2sh-p2wpkh"])
|
||||
def test_nfc_msg_signing(msg, path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_menu_item,
|
||||
goto_home, cap_story, press_select, press_cancel, addr_vs_path, OK):
|
||||
goto_home, cap_story, press_select, press_cancel, addr_vs_path, OK,
|
||||
testnet, settings_set, bitcoind):
|
||||
settings_set("chain", "XTN" if testnet else "BTC")
|
||||
|
||||
for _ in range(5):
|
||||
# need to wait for ApproveMessageSign to be popped from ux stack
|
||||
@ -325,6 +353,9 @@ def test_nfc_msg_signing(msg, path, str_addr_fmt, nfc_write_text, nfc_read_text,
|
||||
addr_fmt = AF_CLASSIC
|
||||
body = "\n".join([msg, path])
|
||||
|
||||
if not path:
|
||||
path = default_derivation_by_af(addr_fmt, testnet=testnet)
|
||||
|
||||
nfc_write_text(body)
|
||||
time.sleep(0.5)
|
||||
_, story = cap_story()
|
||||
@ -339,7 +370,7 @@ def test_nfc_msg_signing(msg, path, str_addr_fmt, nfc_write_text, nfc_read_text,
|
||||
press_select() # exit NFC animation
|
||||
pmsg, addr, sig = parse_signed_message(signed_msg)
|
||||
assert pmsg == msg
|
||||
addr_vs_path(addr, path, addr_fmt)
|
||||
addr_vs_path(addr, path, addr_fmt, testnet=testnet)
|
||||
assert verify_message(addr, sig, msg) is True
|
||||
time.sleep(0.5)
|
||||
_, story = cap_story()
|
||||
@ -349,9 +380,12 @@ def test_nfc_msg_signing(msg, path, str_addr_fmt, nfc_write_text, nfc_read_text,
|
||||
assert signed_msg == signed_msg_again
|
||||
press_cancel() # exit NFC animation
|
||||
press_cancel() # do not want to share again
|
||||
if addr_fmt == AF_CLASSIC and testnet:
|
||||
res = bitcoind.rpc.verifymessage(addr, sig, msg)
|
||||
assert res is True
|
||||
|
||||
@pytest.fixture
|
||||
def verify_armored_signature(pick_menu_item, nfc_write_text, press_select,
|
||||
def verify_armored_signature(pick_menu_item, nfc_write_text,
|
||||
cap_story, goto_home):
|
||||
def doit(way, fname=None, signed_msg=None):
|
||||
goto_home()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user