upgrade msg signing
This commit is contained in:
parent
a8202972b3
commit
a0949ecb87
@ -5,12 +5,19 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
# Shared Improvements - Both Mk4 and Q
|
||||
|
||||
- New Feature: JSON message signing. Use JSON object to pass data to sign in form `{"msg":"<required msg>","subpath":"<optional sp>","addr_fmt": "<optional af>"}`
|
||||
- New Feature: Sign message from note text, or password note
|
||||
- New Feature: Sign message with key resulting from positive ownership check. Press (0) + enter/scan message text
|
||||
- New Feature: Sign message with key selected from Address Explorer Custom Path menu. Press (2) + enter/scan message text
|
||||
- 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: Add ability to switch between BIP-32 xpub, and obsolete
|
||||
SLIP-132 format in `Export XPUB`
|
||||
- Enhancement: Use the fact that master seed cannot be used as ephemeral seed, to show message
|
||||
about successful master seed verification.
|
||||
- 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).
|
||||
- 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.
|
||||
@ -26,12 +33,6 @@ 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.
|
||||
|
||||
@ -40,5 +41,8 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
## 1.3.1Q - 2024-??-??
|
||||
|
||||
- New Feature: Verify Signed RFC messages via BBQr
|
||||
- New Feature: Sign message from QR scan (format has to be JSON)
|
||||
- Enhancement: Sign scanned Simple Text by pressing (0). Next screens query information about key to use.
|
||||
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||
|
||||
|
||||
@ -4,12 +4,12 @@
|
||||
#
|
||||
# Every function here is called directly by a menu item. They should all be async.
|
||||
#
|
||||
import ckcc, pyb, version, uasyncio, sys, uos
|
||||
import ckcc, pyb, version, uasyncio, sys, uos, chains
|
||||
from uhashlib import sha256
|
||||
from uasyncio import sleep_ms
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from utils import imported, problem_file_line, get_filesize, encode_seed_qr
|
||||
from utils import xfp2str, B2A, addr_fmt_label, txid_from_fname
|
||||
from utils import xfp2str, B2A, txid_from_fname
|
||||
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
|
||||
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X
|
||||
from export import make_json_wallet, make_summary_file, make_descriptor_wallet_export
|
||||
@ -1106,9 +1106,9 @@ async def electrum_skeleton(*a):
|
||||
return
|
||||
|
||||
rv = [
|
||||
MenuItem(addr_fmt_label(af), f=electrum_skeleton_step2,
|
||||
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
|
||||
arg=(af, account_num))
|
||||
for af in [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH]
|
||||
for af in chains.SINGLESIG_AF
|
||||
]
|
||||
the_ux.push(MenuSystem(rv))
|
||||
|
||||
@ -1122,7 +1122,7 @@ def ss_descriptor_export_story(addition="", background="", acct=True):
|
||||
async def ss_descriptor_skeleton(_0, _1, item):
|
||||
# Export of descriptor data (wallet)
|
||||
int_ext, addition, f_pattern = None, "", "descriptor.txt"
|
||||
allowed_af = [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH]
|
||||
allowed_af = chains.SINGLESIG_AF
|
||||
if item.arg:
|
||||
int_ext, allowed_af, ll, f_pattern = item.arg
|
||||
addition = " for " + ll
|
||||
@ -1149,7 +1149,7 @@ async def ss_descriptor_skeleton(_0, _1, item):
|
||||
fname_pattern=f_pattern)
|
||||
else:
|
||||
rv = [
|
||||
MenuItem(addr_fmt_label(af), f=descriptor_skeleton_step2,
|
||||
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
|
||||
arg=(af, account_num, int_ext, f_pattern))
|
||||
for af in allowed_af
|
||||
]
|
||||
@ -1890,10 +1890,11 @@ async def sign_message_on_sd(*a):
|
||||
# min 1 line max 3 lines
|
||||
return 1 <= len(lines) <= 3
|
||||
|
||||
fn = await file_picker(suffix='txt', min_size=2, max_size=500, taster=is_signable,
|
||||
none_msg=('Must be one line of text, optionally '
|
||||
fn = await file_picker(suffix=['txt', "json"], min_size=2, max_size=500, taster=is_signable,
|
||||
none_msg=('Must be txt file with one msg line, optionally '
|
||||
'followed by a subkey derivation path on a second line '
|
||||
'and/or address format on third line.'))
|
||||
'and/or address format on third line. JSON msg signing '
|
||||
'format also supported'))
|
||||
|
||||
if not fn:
|
||||
return
|
||||
|
||||
@ -8,14 +8,14 @@ import chains, stash, version
|
||||
from ux import ux_show_story, the_ux, ux_enter_bip32_index
|
||||
from ux import export_prompt_builder, import_export_prompt_decode
|
||||
from menu import MenuSystem, MenuItem
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH
|
||||
from multisig import MultisigWallet
|
||||
from uasyncio import sleep_ms
|
||||
from uhashlib import sha256
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from glob import settings
|
||||
from auth import write_sig_file
|
||||
from utils import addr_fmt_label, censor_address
|
||||
from utils import censor_address
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_HOME, KEY_LEFT, KEY_RIGHT
|
||||
from charcodes import KEY_CANCEL
|
||||
|
||||
@ -112,9 +112,8 @@ class PickAddrFmtMenu(MenuSystem):
|
||||
def __init__(self, path, parent):
|
||||
self.parent = parent
|
||||
items = [
|
||||
MenuItem(addr_fmt_label(AF_CLASSIC), f=self.done, arg=(path, AF_CLASSIC)),
|
||||
MenuItem(addr_fmt_label(AF_P2WPKH), f=self.done, arg=(path, AF_P2WPKH)),
|
||||
MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=self.done, arg=(path, AF_P2WPKH_P2SH)),
|
||||
MenuItem(chains.addr_fmt_label(af), f=self.done, arg=(path, af))
|
||||
for af in chains.SINGLESIG_AF
|
||||
]
|
||||
super().__init__(items)
|
||||
if path.startswith("m/84h"):
|
||||
@ -198,7 +197,7 @@ class AddressListMenu(MenuSystem):
|
||||
indent = ' ↳ ' if version.has_qwerty else '↳'
|
||||
for i, (address, path, addr_fmt) in enumerate(choices):
|
||||
axi = address[-4:] # last 4 address characters
|
||||
items.append(MenuItem(addr_fmt_label(addr_fmt), f=self.pick_single,
|
||||
items.append(MenuItem(chains.addr_fmt_label(addr_fmt), f=self.pick_single,
|
||||
arg=(path, addr_fmt, axi)))
|
||||
items.append(MenuItem(indent+address, f=self.pick_single,
|
||||
arg=(path, addr_fmt, axi)))
|
||||
@ -338,6 +337,9 @@ Press (3) if you really understand and accept these risks.
|
||||
msg += '\n\n'
|
||||
if n:
|
||||
msg += "Press RIGHT to see next group, LEFT to go back. X to quit."
|
||||
else:
|
||||
escape += "0"
|
||||
msg += " Press (0) to sign message with this key."
|
||||
|
||||
return msg, addrs, escape
|
||||
|
||||
@ -383,8 +385,15 @@ Press (3) if you really understand and accept these risks.
|
||||
|
||||
continue
|
||||
|
||||
elif choice == '0' and allow_change:
|
||||
change = 1
|
||||
elif choice == '0':
|
||||
if allow_change:
|
||||
change = 1
|
||||
else:
|
||||
# only custom path sets allow_change to False
|
||||
# msg sign
|
||||
from auth import sign_with_own_address
|
||||
await sign_with_own_address(path, addr_fmt)
|
||||
|
||||
elif n is None:
|
||||
# makes no sense to do any of below, showing just single address
|
||||
continue
|
||||
|
||||
278
shared/auth.py
278
shared/auth.py
@ -13,12 +13,12 @@ from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P
|
||||
from public_constants import STXN_FLAGS_MASK, STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
|
||||
from sffile import SFFile
|
||||
from ux import ux_aborted, ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys
|
||||
from ux import show_qr_code, OK, X
|
||||
from ux import show_qr_code, OK, X, ux_input_text, ux_enter_bip32_index
|
||||
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, parse_msg_sign_request
|
||||
from utils import B2A, to_ascii_printable
|
||||
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
|
||||
from files import CardSlot
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from exceptions import HSMDenied
|
||||
from version import MAX_TXN_LEN
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT
|
||||
@ -282,14 +282,13 @@ def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_
|
||||
dis.progress_bar_show(i / 6)
|
||||
return sig_nice
|
||||
|
||||
def validate_text_for_signing(text):
|
||||
def validate_text_for_signing(text, only_printable=True):
|
||||
# Check for some UX/UI traps in the message itself.
|
||||
# - messages must be short and ascii only. Our charset is limited
|
||||
# - too many spaces, leading/trailing can be an issue
|
||||
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
||||
|
||||
MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
||||
|
||||
result = to_ascii_printable(text)
|
||||
result = to_ascii_printable(text, only_printable=only_printable)
|
||||
|
||||
length = len(result)
|
||||
assert length >= 2, "msg too short (min. 2)"
|
||||
@ -302,17 +301,55 @@ def validate_text_for_signing(text):
|
||||
# looks ok
|
||||
return result
|
||||
|
||||
def parse_msg_sign_request(data):
|
||||
subpath = ""
|
||||
addr_fmt = "p2pkh"
|
||||
is_json = False
|
||||
try:
|
||||
data_dict = ujson.loads(data.strip())
|
||||
text = data_dict.get("msg", None)
|
||||
if text is None:
|
||||
raise AssertionError("MSG required")
|
||||
subpath = data_dict.get("subpath", subpath)
|
||||
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
|
||||
is_json = True
|
||||
except ValueError:
|
||||
lines = data.split("\n")
|
||||
assert len(lines) >= 1, "min 1 line"
|
||||
assert len(lines) <= 3, "max 3 lines"
|
||||
|
||||
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 = chains.STD_DERIVATIONS[addr_fmt]
|
||||
subpath = subpath.format(
|
||||
coin_type=chains.current_chain().b44_cointype,
|
||||
account=0, change=0, idx=0
|
||||
)
|
||||
|
||||
return text, subpath, addr_fmt, is_json
|
||||
|
||||
|
||||
class ApproveMessageSign(UserAuthorizedAction):
|
||||
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
|
||||
msg_sign_request=None):
|
||||
msg_sign_request=None, only_printable=True):
|
||||
super().__init__()
|
||||
|
||||
is_json = False
|
||||
if msg_sign_request:
|
||||
text, subpath, addr_fmt = parse_msg_sign_request(msg_sign_request)
|
||||
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
|
||||
|
||||
self.text = validate_text_for_signing(text)
|
||||
self.text = validate_text_for_signing(
|
||||
text, only_printable=not is_json and only_printable
|
||||
)
|
||||
self.subpath = cleanup_deriv_path(subpath)
|
||||
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
|
||||
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
|
||||
self.approved_cb = approved_cb
|
||||
|
||||
from glob import dis
|
||||
@ -363,10 +400,158 @@ def sign_msg(text, subpath, addr_fmt):
|
||||
abort_and_goto(UserAuthorizedAction.active_request)
|
||||
|
||||
|
||||
async def msg_sign_ux_get_subpath(addr_fmt):
|
||||
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
||||
chain_n = chains.current_chain().b44_cointype
|
||||
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||
ch = await ux_show_story(title="Change?",
|
||||
msg="Press (0) to use internal/change address,"
|
||||
" %s to use external/receive address." % OK, escape="0")
|
||||
change = 1 if ch == '0' else 0
|
||||
idx = await ux_enter_bip32_index('Index Number:') or 0
|
||||
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
|
||||
|
||||
|
||||
async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
|
||||
from menu import MenuSystem, MenuItem
|
||||
from ux import the_ux
|
||||
|
||||
async def done(_1, _2, item):
|
||||
from auth import approve_msg_sign, msg_sign_ux_get_subpath
|
||||
|
||||
text, af = item.arg
|
||||
subpath = await msg_sign_ux_get_subpath(af)
|
||||
|
||||
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
||||
kill_menu=kill_menu, only_printable=False)
|
||||
|
||||
# pick address format
|
||||
rv = [
|
||||
MenuItem(chains.addr_fmt_label(af), f=done, arg=(txt, af))
|
||||
for af in chains.SINGLESIG_AF
|
||||
]
|
||||
the_ux.push(MenuSystem(rv))
|
||||
|
||||
|
||||
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
||||
msg_sign_request=None, kill_menu=False,
|
||||
only_printable=True):
|
||||
UserAuthorizedAction.cleanup()
|
||||
UserAuthorizedAction.check_busy(ApproveMessageSign)
|
||||
try:
|
||||
UserAuthorizedAction.active_request = ApproveMessageSign(
|
||||
text, subpath, addr_fmt,
|
||||
approved_cb=approved_cb,
|
||||
msg_sign_request=msg_sign_request,
|
||||
only_printable=only_printable,
|
||||
)
|
||||
if kill_menu:
|
||||
abort_and_goto(UserAuthorizedAction.active_request)
|
||||
else:
|
||||
# do not kill the menu stack! just append
|
||||
from ux import the_ux
|
||||
the_ux.push(UserAuthorizedAction.active_request)
|
||||
except (AssertionError, ValueError) as exc:
|
||||
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
|
||||
return
|
||||
|
||||
|
||||
async def msg_signing_done(signature, address, text):
|
||||
from ux import import_export_prompt
|
||||
|
||||
ch = await import_export_prompt("Signed Msg", is_import=False,
|
||||
no_qr=not version.has_qwerty)
|
||||
if ch == KEY_CANCEL:
|
||||
return
|
||||
|
||||
if isinstance(ch, dict):
|
||||
await sd_sign_msg_done(signature, address, text, "msg_sign", **ch)
|
||||
elif version.has_qr and ch == KEY_QR:
|
||||
from ux_q1 import qr_msg_sign_done
|
||||
await qr_msg_sign_done(signature, address, text)
|
||||
elif ch in KEY_NFC+"3":
|
||||
from glob import NFC
|
||||
if NFC:
|
||||
await NFC.msg_sign_done(signature, address, text)
|
||||
|
||||
|
||||
async def sign_with_own_address(subpath, addr_fmt):
|
||||
# used for cases where we already have the key picked, but need the message:
|
||||
# * address_explorer custom path
|
||||
# * positive ownership test
|
||||
from glob import dis
|
||||
|
||||
to_sign = await ux_input_text("", scan_ok=True, prompt="Enter MSG") # max len is 100 only here
|
||||
if not to_sign: return
|
||||
|
||||
await approve_msg_sign(to_sign, subpath, addr_fmt, approved_cb=msg_signing_done, kill_menu=True)
|
||||
|
||||
|
||||
async def sd_sign_msg_done(signature, address, text, base=None, orig_path=None,
|
||||
slot_b=None, force_vdisk=False):
|
||||
from glob import dis
|
||||
dis.fullscreen('Generating...')
|
||||
|
||||
out_fn = None
|
||||
sig = b2a_base64(signature).decode('ascii').strip()
|
||||
|
||||
while 1:
|
||||
# try to put back into same spot
|
||||
# add -signed to end.
|
||||
target_fname = base + '-signed.txt'
|
||||
lst = [orig_path]
|
||||
if orig_path:
|
||||
lst.append(None)
|
||||
|
||||
for path in lst:
|
||||
try:
|
||||
with CardSlot(readonly=True, slot_b=slot_b, force_vdisk=force_vdisk) as card:
|
||||
out_full, out_fn = card.pick_filename(target_fname, path)
|
||||
out_path = path
|
||||
if out_full: break
|
||||
except CardMissingError:
|
||||
prob = 'Missing card.\n\n'
|
||||
out_fn = None
|
||||
|
||||
if not out_fn:
|
||||
# need them to insert a card
|
||||
prob = ''
|
||||
else:
|
||||
# attempt write-out
|
||||
try:
|
||||
dis.fullscreen("Saving...")
|
||||
with CardSlot(slot_b=slot_b, force_vdisk=force_vdisk) as card:
|
||||
with card.open(out_full, 'wt') as fd:
|
||||
# save in full RFC style
|
||||
# gen length is 6
|
||||
gen = rfc_signature_template_gen(addr=address, msg=text, sig=sig)
|
||||
for i, part in enumerate(gen):
|
||||
fd.write(part)
|
||||
dis.progress_bar_show(i / 6)
|
||||
|
||||
# success and done!
|
||||
break
|
||||
|
||||
except OSError as exc:
|
||||
prob = 'Failed to write!\n\n%s\n\n' % exc
|
||||
sys.print_exception(exc)
|
||||
# fall through to try again
|
||||
|
||||
# prompt them to input another card?
|
||||
ch = await ux_show_story(prob + "Please insert an SDCard to receive signed message, "
|
||||
"and press %s." % OK, title="Need Card")
|
||||
if ch == 'x':
|
||||
await ux_aborted()
|
||||
return
|
||||
|
||||
# done.
|
||||
msg = "Created new file:\n\n%s" % out_fn
|
||||
await ux_show_story(msg, title='File Signed')
|
||||
|
||||
|
||||
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
|
||||
|
||||
async def done(signature, address, text):
|
||||
@ -376,59 +561,8 @@ async def sign_txt_file(filename):
|
||||
orig_path, basename = filename.rsplit('/', 1)
|
||||
orig_path += '/'
|
||||
base = basename.rsplit('.', 1)[0]
|
||||
out_fn = None
|
||||
|
||||
sig = b2a_base64(signature).decode('ascii').strip()
|
||||
|
||||
while 1:
|
||||
# try to put back into same spot
|
||||
# add -signed to end.
|
||||
target_fname = base+'-signed.txt'
|
||||
|
||||
for path in [orig_path, None]:
|
||||
try:
|
||||
with CardSlot(readonly=True) as card:
|
||||
out_full, out_fn = card.pick_filename(target_fname, path)
|
||||
out_path = path
|
||||
if out_full: break
|
||||
except CardMissingError:
|
||||
prob = 'Missing card.\n\n'
|
||||
out_fn = None
|
||||
|
||||
if not out_fn:
|
||||
# need them to insert a card
|
||||
prob = ''
|
||||
else:
|
||||
# attempt write-out
|
||||
try:
|
||||
dis.fullscreen("Saving...")
|
||||
with CardSlot() as card:
|
||||
with card.open(out_full, 'wt') as fd:
|
||||
# save in full RFC style
|
||||
# gen length is 6
|
||||
gen = rfc_signature_template_gen(addr=address, msg=text, sig=sig)
|
||||
for i, part in enumerate(gen):
|
||||
fd.write(part)
|
||||
dis.progress_bar_show(i / 6)
|
||||
|
||||
# success and done!
|
||||
break
|
||||
|
||||
except OSError as exc:
|
||||
prob = 'Failed to write!\n\n%s\n\n' % exc
|
||||
sys.print_exception(exc)
|
||||
# fall through to try again
|
||||
|
||||
# prompt them to input another card?
|
||||
ch = await ux_show_story(prob+"Please insert an SDCard to receive signed message, "
|
||||
"and press %s." % OK, title="Need Card")
|
||||
if ch == 'x':
|
||||
await ux_aborted()
|
||||
return
|
||||
|
||||
# done.
|
||||
msg = "Created new file:\n\n%s" % out_fn
|
||||
await ux_show_story(msg, title='File Signed')
|
||||
await sd_sign_msg_done(signature, address, text, base, orig_path)
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
UserAuthorizedAction.check_busy()
|
||||
@ -438,16 +572,8 @@ async def sign_txt_file(filename):
|
||||
with card.open(filename, 'rt') as fd:
|
||||
res = fd.read()
|
||||
|
||||
try:
|
||||
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:
|
||||
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
|
||||
return
|
||||
await approve_msg_sign(None, None, None, approved_cb=done,
|
||||
msg_sign_request=res)
|
||||
|
||||
def verify_signature(msg, addr, sig_str):
|
||||
warnings = ""
|
||||
@ -564,7 +690,6 @@ async def verify_armored_signed_msg(contents, digest_check=True):
|
||||
await ux_show_story('\n\n'.join(m for m in [err_msg, story, warn_msg] if m), title=title)
|
||||
|
||||
async def verify_txt_sig_file(filename):
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
# copy message into memory
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
@ -1074,7 +1199,6 @@ def psbt_encoding_taster(taste, psbt_len):
|
||||
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None):
|
||||
# sign a PSBT file found on a MicroSD card
|
||||
# - or from VirtualDisk (mk4)
|
||||
from files import CardSlot, CardMissingError
|
||||
from glob import dis
|
||||
from ux import the_ux
|
||||
|
||||
|
||||
@ -12,6 +12,9 @@ from serializations import hash160, ser_compact_size, disassemble
|
||||
from ucollections import namedtuple
|
||||
from opcodes import OP_RETURN, OP_1, OP_16
|
||||
|
||||
|
||||
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
|
||||
|
||||
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
|
||||
Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
|
||||
@ -401,6 +404,47 @@ CommonDerivations = [
|
||||
AF_P2WPKH ), # generates bc1 bech32 addresses
|
||||
]
|
||||
|
||||
STD_DERIVATIONS = {
|
||||
"p2pkh": CommonDerivations[0][1],
|
||||
"p2sh-p2wpkh": CommonDerivations[1][1],
|
||||
"p2wpkh-p2sh": CommonDerivations[1][1],
|
||||
"p2wpkh": CommonDerivations[2][1],
|
||||
}
|
||||
|
||||
def parse_addr_fmt_str(addr_fmt):
|
||||
# accepts strings and also integers if already parsed
|
||||
try:
|
||||
if isinstance(addr_fmt, int):
|
||||
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
||||
return addr_fmt
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
addr_fmt = addr_fmt.lower()
|
||||
if addr_fmt in ("p2sh-p2wpkh", "p2wpkh-p2sh"):
|
||||
return AF_P2WPKH_P2SH
|
||||
elif addr_fmt == "p2pkh":
|
||||
return AF_CLASSIC
|
||||
elif addr_fmt == "p2wpkh":
|
||||
return AF_P2WPKH
|
||||
else:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ValueError("Invalid address format: '%s'\n\n"
|
||||
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
|
||||
|
||||
|
||||
def af_to_bip44_purpose(addr_fmt):
|
||||
# single signature only
|
||||
return {AF_CLASSIC: 44,
|
||||
AF_P2WPKH_P2SH: 49,
|
||||
AF_P2WPKH: 84}[addr_fmt]
|
||||
|
||||
|
||||
def addr_fmt_label(addr_fmt):
|
||||
return {AF_CLASSIC: "Classic P2PKH",
|
||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
|
||||
|
||||
def verify_recover_pubkey(sig, digest):
|
||||
# verifies a message digest against a signature and recovers
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#
|
||||
# included in Q builds only, not Mk4 --> manifest_q1.py
|
||||
#
|
||||
import ngu, bip39, ure, stash
|
||||
import ngu, bip39, ure, stash, json
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from exceptions import QRDecodeExplained
|
||||
from bbqr import TYPE_LABELS
|
||||
@ -131,7 +131,11 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
pass
|
||||
|
||||
elif ty == 'J':
|
||||
return 'json', (got,)
|
||||
what = "json"
|
||||
if "msg" in got:
|
||||
what = "smsg"
|
||||
|
||||
return what, (got,)
|
||||
else:
|
||||
msg = TYPE_LABELS.get(ty, 'Unknown FileType')
|
||||
raise QRDecodeExplained("Sorry, %s not useful." % msg)
|
||||
@ -159,6 +163,12 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
if expect_secret:
|
||||
raise QRDecodeExplained("Not a secret?")
|
||||
|
||||
try:
|
||||
dct = json.loads(got)
|
||||
if "msg" in dct:
|
||||
return "smsg", (got,)
|
||||
except: pass
|
||||
|
||||
# try to recognize various bitcoin-related text strings...
|
||||
return decode_short_text(got)
|
||||
|
||||
@ -178,6 +188,9 @@ def decode_short_text(got):
|
||||
|
||||
# might be a PSBT?
|
||||
if len(got) > 100:
|
||||
if got.lstrip().startswith("-----BEGIN BITCOIN SIGNED MESSAGE-----"):
|
||||
return "vmsg", (got,)
|
||||
|
||||
from auth import psbt_encoding_taster
|
||||
try:
|
||||
decoder, _, psbt_len = psbt_encoding_taster(got[0:10].encode(), len(got))
|
||||
|
||||
@ -405,14 +405,7 @@ def generate_electrum_wallet(addr_type, account_num):
|
||||
xfp = settings.get('xfp')
|
||||
|
||||
# Must get the derivation path, and the SLIP32 version bytes right!
|
||||
if addr_type == AF_CLASSIC:
|
||||
mode = 44
|
||||
elif addr_type == AF_P2WPKH:
|
||||
mode = 84
|
||||
elif addr_type == AF_P2WPKH_P2SH:
|
||||
mode = 49
|
||||
else:
|
||||
raise ValueError(addr_type)
|
||||
mode = chains.af_to_bip44_purpose(addr_type)
|
||||
|
||||
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
||||
|
||||
@ -508,14 +501,7 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
|
||||
xfp = settings.get('xfp')
|
||||
dis.progress_bar_show(0.1)
|
||||
if mode is None:
|
||||
if addr_type == AF_CLASSIC:
|
||||
mode = 44
|
||||
elif addr_type == AF_P2WPKH:
|
||||
mode = 84
|
||||
elif addr_type == AF_P2WPKH_P2SH:
|
||||
mode = 49
|
||||
else:
|
||||
raise ValueError(addr_type)
|
||||
mode = chains.af_to_bip44_purpose(addr_type)
|
||||
|
||||
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
# - has GPIO signal "??" which is multipurpose on its own pin
|
||||
# - this chip chosen because it can disable RF interaction
|
||||
#
|
||||
import utime, ngu, ndef, stash
|
||||
import utime, ngu, ndef, stash, chains
|
||||
from uasyncio import sleep_ms
|
||||
import uasyncio as asyncio
|
||||
from ustruct import pack, unpack
|
||||
@ -15,7 +15,7 @@ from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import b2a_base64, a2b_base64
|
||||
|
||||
from ux import ux_show_story, ux_wait_keydown, OK, X
|
||||
from utils import B2A, problem_file_line, parse_addr_fmt_str, txid_from_fname
|
||||
from utils import B2A, problem_file_line, txid_from_fname
|
||||
from public_constants import AF_CLASSIC
|
||||
from charcodes import KEY_ENTER, KEY_CANCEL
|
||||
|
||||
@ -727,7 +727,7 @@ class NFCHandler:
|
||||
else:
|
||||
subpath, addr_fmt_str = winner
|
||||
try:
|
||||
addr_fmt = parse_addr_fmt_str(addr_fmt_str)
|
||||
addr_fmt = chains.parse_addr_fmt_str(addr_fmt_str)
|
||||
except AssertionError as e:
|
||||
await ux_show_story(str(e))
|
||||
return
|
||||
@ -738,32 +738,21 @@ class NFCHandler:
|
||||
await the_ux.interact() # need this otherwise NFC animation takes over
|
||||
|
||||
async def start_msg_sign(self):
|
||||
from auth import UserAuthorizedAction, ApproveMessageSign
|
||||
from ux import the_ux
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
from auth import approve_msg_sign
|
||||
|
||||
def f(m):
|
||||
m = m.decode()
|
||||
split_msg = m.split("\n")
|
||||
if 1 <= len(split_msg) <= 3:
|
||||
return split_msg
|
||||
return m
|
||||
|
||||
winner = await self._nfc_reader(f, 'Unable to find correctly formated message to sign.')
|
||||
|
||||
if not winner:
|
||||
return
|
||||
|
||||
UserAuthorizedAction.check_busy(ApproveMessageSign)
|
||||
try:
|
||||
UserAuthorizedAction.active_request = ApproveMessageSign(
|
||||
None, None, None, approved_cb=self.msg_sign_done,
|
||||
msg_sign_request=winner
|
||||
)
|
||||
the_ux.push(UserAuthorizedAction.active_request)
|
||||
except AssertionError as exc:
|
||||
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
|
||||
return
|
||||
await approve_msg_sign(None, None, None, approved_cb=self.msg_sign_done,
|
||||
msg_sign_request=winner)
|
||||
|
||||
|
||||
async def msg_sign_done(self, signature, address, text):
|
||||
from auth import rfc_signature_template_gen
|
||||
|
||||
@ -298,6 +298,15 @@ class NoteContentBase:
|
||||
# single export
|
||||
await start_export([self])
|
||||
|
||||
async def sign_txt_msg(self, a, b, item):
|
||||
from auth import ux_sign_msg, msg_signing_done
|
||||
txt = item.arg
|
||||
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
|
||||
|
||||
def sign_misc_menu_item(self):
|
||||
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc)
|
||||
|
||||
|
||||
class PasswordContent(NoteContentBase):
|
||||
# "Passwords" have a few more fields and are more structured
|
||||
flds = ['title', 'user', 'password', 'site', 'misc' ]
|
||||
@ -317,6 +326,7 @@ class PasswordContent(NoteContentBase):
|
||||
MenuItem('Edit Metadata', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Change Password', f=self.change_pw),
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='password'),
|
||||
]
|
||||
@ -446,6 +456,7 @@ class NoteContent(NoteContentBase):
|
||||
MenuItem('Edit', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Export', f=self.export),
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
||||
]
|
||||
|
||||
@ -247,7 +247,7 @@ class OwnershipCache:
|
||||
if af == addr_fmt and acct_num:
|
||||
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
|
||||
possibles.append(w)
|
||||
except ValueError: pass # if not single sig address format
|
||||
except (KeyError, ValueError): pass # if not single sig address format
|
||||
|
||||
if not possibles:
|
||||
# can only happen w/ scripts; for single-signer we have things to check
|
||||
@ -307,26 +307,43 @@ class OwnershipCache:
|
||||
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
||||
from ux import ux_show_story, show_qr_code
|
||||
from charcodes import KEY_QR
|
||||
from multisig import MultisigWallet
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M
|
||||
|
||||
try:
|
||||
wallet, subpath = OWNERSHIP.search(addr)
|
||||
is_ms = isinstance(wallet, MultisigWallet)
|
||||
sp = wallet.render_path(*subpath)
|
||||
|
||||
msg = addr
|
||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
||||
msg += '\nDerivation path:\n ' + wallet.render_path(*subpath)
|
||||
if version.has_qwerty:
|
||||
esc = KEY_QR
|
||||
msg += '\nDerivation path:\n ' + sp
|
||||
if is_ms:
|
||||
esc = ""
|
||||
else:
|
||||
msg += '\n\nPress (1) for QR'
|
||||
esc = '1'
|
||||
esc = "0"
|
||||
msg += "\n\nPress (0) to sign message with this key."
|
||||
|
||||
if version.has_qwerty:
|
||||
esc += KEY_QR
|
||||
else:
|
||||
msg += ' (1) for address QR'
|
||||
esc += '1'
|
||||
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, title="Verified Address",
|
||||
escape=esc, hint_icons=KEY_QR)
|
||||
if ch != esc: break
|
||||
await show_qr_code(addr, is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
|
||||
msg=addr)
|
||||
escape=esc, hint_icons=KEY_QR)
|
||||
if ch in ("1"+KEY_QR):
|
||||
await show_qr_code(
|
||||
addr,
|
||||
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
|
||||
msg=addr
|
||||
)
|
||||
elif not is_ms and (ch == "0"): # only singlesig
|
||||
from auth import sign_with_own_address
|
||||
await sign_with_own_address(sp, wallet.addr_fmt)
|
||||
else:
|
||||
break
|
||||
|
||||
except UnknownAddressExplained as exc:
|
||||
await ux_show_story(addr + '\n\n' + str(exc), title="Unknown Address")
|
||||
|
||||
@ -7,17 +7,9 @@ from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import a2b_base64, b2a_base64
|
||||
from uhashlib import sha256
|
||||
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
|
||||
@ -212,16 +204,17 @@ def is_printable(s):
|
||||
return False
|
||||
return True
|
||||
|
||||
def to_ascii_printable(s, strip=False):
|
||||
def to_ascii_printable(s, strip=False, only_printable=True):
|
||||
try:
|
||||
s = str(s, 'ascii')
|
||||
if strip:
|
||||
s = s.strip()
|
||||
assert is_ascii(s)
|
||||
assert is_printable(s)
|
||||
if only_printable:
|
||||
assert is_printable(s)
|
||||
return s
|
||||
except:
|
||||
raise AssertionError('must be ascii printable')
|
||||
raise AssertionError("must be ascii" + (" printable" if only_printable else ""))
|
||||
|
||||
|
||||
def problem_file_line(exc):
|
||||
@ -271,7 +264,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
|
||||
|
||||
# regex for valid chars, m at start, maybe /*h or /* at end sometimes
|
||||
mat = ure.match(r"(m|m/|)[0-9/h]*" + ('' if not allow_star else r"(\*h|\*|)"), s)
|
||||
assert mat.group(0) == s, "invalid characters"
|
||||
assert mat.group(0) == s, "invalid characters in path"
|
||||
|
||||
parts = s.split('/')
|
||||
|
||||
@ -512,24 +505,6 @@ def word_wrap(ln, w):
|
||||
|
||||
yield left
|
||||
|
||||
def parse_addr_fmt_str(addr_fmt):
|
||||
# accepts strings and also integers if already parsed
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
|
||||
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
||||
return addr_fmt
|
||||
|
||||
addr_fmt = addr_fmt.lower()
|
||||
if addr_fmt in ("p2sh-p2wpkh", "p2wpkh-p2sh"):
|
||||
return AF_P2WPKH_P2SH
|
||||
elif addr_fmt == "p2pkh":
|
||||
return AF_CLASSIC
|
||||
elif addr_fmt == "p2wpkh":
|
||||
return AF_P2WPKH
|
||||
else:
|
||||
raise ValueError("Invalid address format: '%s'\n\n"
|
||||
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
|
||||
|
||||
def parse_extended_key(ln, private=False):
|
||||
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
|
||||
# - can handle any garbage line
|
||||
@ -566,14 +541,6 @@ def chunk_writer(fd, body):
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
|
||||
def addr_fmt_label(addr_fmt):
|
||||
return {
|
||||
AF_CLASSIC: "Classic P2PKH",
|
||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||
AF_P2WPKH: "Segwit P2WPKH"
|
||||
}[addr_fmt]
|
||||
|
||||
|
||||
def pad_raw_secret(raw_sec_str):
|
||||
# Chip can hold 72-bytes as a secret
|
||||
# every secret has 0th byte as marker
|
||||
@ -708,28 +675,4 @@ 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
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
# ux_q1.py - UX/UI interactions that are Q1 specific and use big screen, keyboard.
|
||||
#
|
||||
import utime, gc, ngu, sys
|
||||
import utime, gc, ngu, sys, chains
|
||||
import uasyncio as asyncio
|
||||
from uasyncio import sleep_ms
|
||||
from charcodes import *
|
||||
@ -12,7 +12,10 @@ import bip39
|
||||
from decoders import decode_qr_result
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import b2a_base64
|
||||
|
||||
from utils import problem_file_line
|
||||
from public_constants import MSG_SIGNING_MAX_LENGTH
|
||||
from glob import numpad # may be None depending on import order, careful
|
||||
|
||||
class PressRelease:
|
||||
@ -951,6 +954,20 @@ class QRScannerInteraction:
|
||||
await ux_visualize_wif(wif_str, key_pair, compressed, testnet)
|
||||
return
|
||||
|
||||
if what == "vmsg":
|
||||
data, = vals
|
||||
from auth import verify_armored_signed_msg
|
||||
await verify_armored_signed_msg(data)
|
||||
return
|
||||
|
||||
if what == "smsg":
|
||||
data, = vals
|
||||
from auth import approve_msg_sign, msg_signing_done
|
||||
await approve_msg_sign(None, None, None,
|
||||
msg_sign_request=data, kill_menu=True,
|
||||
approved_cb=msg_signing_done)
|
||||
return
|
||||
|
||||
if what == 'text' or what == 'xpub':
|
||||
# we couldn't really decode it.
|
||||
txt, = vals
|
||||
@ -1107,15 +1124,49 @@ async def ux_visualize_wif(wif_str, kp, compressed, testnet):
|
||||
msg += "public key sec:\n" + b2a_hex(kp.pubkey().to_bytes(not compressed)).decode() + "\n\n"
|
||||
await ux_show_story(msg, title="WIF")
|
||||
|
||||
async def ux_visualize_textqr(txt, maxlen=200):
|
||||
async def qr_msg_sign_done(signature, address, text):
|
||||
from ux import ux_show_story
|
||||
from auth import rfc_signature_template_gen
|
||||
from export import export_by_qr
|
||||
|
||||
sig = b2a_base64(signature).decode('ascii').strip()
|
||||
while True:
|
||||
ch = await ux_show_story("Press ENTER to export signature QR only, "
|
||||
"(0) to export full RFC template, "
|
||||
"CANCEL if done.", escape="0")
|
||||
if ch == "x": break
|
||||
if ch == "y":
|
||||
await export_by_qr(sig, "Signature", "U")
|
||||
if ch == "0":
|
||||
armored_str = "".join(rfc_signature_template_gen(addr=address, msg=text,
|
||||
sig=sig))
|
||||
await show_bbqr_codes("U", armored_str, "Armored MSG")
|
||||
|
||||
async def qr_sign_msg(txt):
|
||||
from auth import ux_sign_msg
|
||||
await ux_sign_msg(txt, approved_cb=qr_msg_sign_done, kill_menu=True)
|
||||
|
||||
async def ux_visualize_textqr(txt, maxlen=MSG_SIGNING_MAX_LENGTH):
|
||||
# Show simple text. Don't crash on huge things, but be
|
||||
# able to show a full xpub.
|
||||
from ux import ux_show_story
|
||||
if len(txt) > maxlen:
|
||||
|
||||
txt_len = len(txt)
|
||||
escape = "0"
|
||||
if txt_len > maxlen:
|
||||
escape = None
|
||||
txt = txt[0:maxlen] + '...'
|
||||
|
||||
await ux_show_story("%s\n\nAbove is text that was scanned. "
|
||||
"We can't do any more with it." % txt, title="Simple Text")
|
||||
msg = "%s\n\nAbove is text that was scanned. " % txt
|
||||
if escape:
|
||||
msg += " Press (0) to sign the text. "
|
||||
else:
|
||||
msg += "We can't do any more with it."
|
||||
|
||||
ch = await ux_show_story(title="Simple Text", msg=msg, escape=escape)
|
||||
if escape and (ch == "0"):
|
||||
await qr_sign_msg(txt)
|
||||
|
||||
|
||||
async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
||||
# Compress, encode and split data, then show it animated...
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
#
|
||||
import chains
|
||||
from descriptor import Descriptor
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
from stash import SensitiveValues
|
||||
|
||||
MAX_BIP32_IDX = (2 ** 31) - 1
|
||||
@ -41,17 +40,9 @@ class MasterSingleSigWallet(WalletABC):
|
||||
# - path is optional, and then we use standard path for addr_fmt
|
||||
# - path can be overriden when we come here via address explorer
|
||||
|
||||
if addr_fmt == AF_P2WPKH:
|
||||
n = 'Segwit P2WPKH'
|
||||
prefix = path or 'm/84h/{coin_type}h/{account}h'
|
||||
elif addr_fmt == AF_CLASSIC:
|
||||
n = 'Classic P2PKH'
|
||||
prefix = path or 'm/44h/{coin_type}h/{account}h'
|
||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||
n = 'P2WPKH-in-P2SH'
|
||||
prefix = path or 'm/49h/{coin_type}h/{account}h'
|
||||
else:
|
||||
raise ValueError(addr_fmt)
|
||||
n = chains.addr_fmt_label(addr_fmt)
|
||||
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
||||
prefix = path or 'm/%dh/{coin_type}h/{account}h' % purpose
|
||||
|
||||
if chain_name:
|
||||
self.chain = chains.get_chain(chain_name)
|
||||
|
||||
@ -2258,6 +2258,7 @@ from test_drv_entro import derive_bip85_secret, activate_bip85_ephemeral
|
||||
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
|
||||
from test_ephemeral import ephemeral_seed_disabled_ui, restore_main_seed, confirm_tmp_seed
|
||||
from test_ephemeral import verify_ephemeral_secret_ui, get_identity_story, get_seed_value_ux, seed_vault_enable
|
||||
from test_msg import verify_msg_sign_story, sign_msg_from_text, msg_sign_export, sign_msg_from_address
|
||||
from test_multisig import import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn
|
||||
from test_multisig import make_ms_address, clear_ms, make_myself_wallet, import_multisig
|
||||
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
|
||||
|
||||
@ -41,6 +41,7 @@ addr_fmt_names = {
|
||||
AF_P2WSH: 'p2wsh',
|
||||
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
|
||||
AF_P2WSH_P2SH: 'p2wsh-p2sh',
|
||||
AF_P2TR: "p2tr",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -22,11 +22,12 @@ RFC_SIGNATURE_TEMPLATE = '''\
|
||||
|
||||
|
||||
def parse_signed_message(msg):
|
||||
msplit = msg.strip().split("\n")
|
||||
assert msplit[0] == "-----BEGIN BITCOIN SIGNED MESSAGE-----"
|
||||
assert msplit[2] == "-----BEGIN BITCOIN SIGNATURE-----"
|
||||
assert msplit[5] == "-----END BITCOIN SIGNATURE-----"
|
||||
return msplit[1], msplit[3], msplit[4]
|
||||
msplit = msg.strip().rsplit("\n", 4)
|
||||
assert msplit[0].startswith("-----BEGIN BITCOIN SIGNED MESSAGE-----\n")
|
||||
msg = msplit[0].replace("-----BEGIN BITCOIN SIGNED MESSAGE-----\n", "")
|
||||
assert msplit[1] == "-----BEGIN BITCOIN SIGNATURE-----"
|
||||
assert msplit[4] == "-----END BITCOIN SIGNATURE-----"
|
||||
return msg, msplit[2], msplit[3]
|
||||
|
||||
|
||||
def sig_hdr_base(addr_fmt):
|
||||
|
||||
@ -383,7 +383,8 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
|
||||
need_keypress, cap_menu, parse_display_screen, validate_address,
|
||||
cap_screen_qr, qr_quality_check, nfc_read_text, get_setting,
|
||||
press_select, press_cancel, is_q1, press_nfc, cap_story,
|
||||
generate_addresses_file, settings_set, set_addr_exp_start_idx):
|
||||
generate_addresses_file, settings_set, set_addr_exp_start_idx,
|
||||
sign_msg_from_address):
|
||||
|
||||
path, start_idx = path_sidx
|
||||
settings_set('aei', True if start_idx else False)
|
||||
@ -443,8 +444,8 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
|
||||
|
||||
time.sleep(.5) # .2 not enuf
|
||||
m = cap_menu()
|
||||
assert m[0] == 'Classic P2PKH'
|
||||
assert m[1] == 'Segwit P2WPKH'
|
||||
assert m[1] == 'Classic P2PKH'
|
||||
assert m[0] == 'Segwit P2WPKH'
|
||||
assert m[2] == 'P2SH-Segwit'
|
||||
|
||||
fmts = {
|
||||
@ -505,6 +506,16 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
|
||||
f_path, f_addr = next(addr_gen)
|
||||
assert f_path == path
|
||||
assert f_addr == addr
|
||||
press_select() # file written
|
||||
|
||||
# msg sign
|
||||
time.sleep(.1)
|
||||
title, body = cap_story()
|
||||
assert "Press (0) to sign message with this key" in body
|
||||
need_keypress('0')
|
||||
msg = "COLDCARD the rock solid HWW"
|
||||
sign_msg_from_address(msg, addr, path, which_fmt, "sd", True)
|
||||
press_cancel()
|
||||
else:
|
||||
n = 10
|
||||
if (start_idx + n) > MAX_BIP32_IDX:
|
||||
|
||||
@ -373,4 +373,26 @@ def test_psbt_static(file, goto_home, cap_story, scan_a_qr, press_select,
|
||||
assert res["complete"] is True
|
||||
assert rb.hex() == res["hex"]
|
||||
|
||||
|
||||
def test_verify_signed_msg(goto_home, need_keypress, scan_a_qr, cap_story):
|
||||
goto_home()
|
||||
need_keypress(KEY_QR)
|
||||
|
||||
data = """\n\n\n \t \n-----BEGIN BITCOIN SIGNED MESSAGE-----
|
||||
5b9e372262952ed399dcdd4f5f08458a6d2811f120cddcb4267099f68f60207c addresses.csv
|
||||
-----BEGIN BITCOIN SIGNATURE-----
|
||||
tb1qupyd58ndsh7lut0et0vtrq432jvu9jtdyws9n9
|
||||
KDOloGMDU3fv+Y3NRSe17SoO4uSKo9IUU2+baJ/pqaHZBuvmW6j5nnv/N4M5BCVawiUig/qzExZpFsA7ZKzlUmU=
|
||||
-----END BITCOIN SIGNATURE-----\n\n\n\n"""
|
||||
|
||||
actual_vers, parts = split_qrs(data, 'U', max_version=20)
|
||||
|
||||
for p in parts:
|
||||
scan_a_qr(p)
|
||||
time.sleep(4.0 / len(parts)) # just so we can watch
|
||||
|
||||
title, story = cap_story()
|
||||
assert "Good signature by address" in story
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -179,4 +179,25 @@ def test_wif(data, try_decode):
|
||||
assert compressed == tcompressed
|
||||
assert testnet == ttestnet
|
||||
|
||||
@pytest.mark.parametrize('data', [
|
||||
'{"msg": "coinkite"}',
|
||||
'{"msg": "coink\n\n\tite", "subpath": "m/99h"}',
|
||||
'{"msg": "coinkite", "subpath": "m/96420h", "addr_fmt": "p2wpkh"}',
|
||||
])
|
||||
def test_json_msg_sign(data, try_decode):
|
||||
ft, vals = try_decode(data)
|
||||
assert ft == "smsg"
|
||||
assert vals[0] == data
|
||||
|
||||
|
||||
@pytest.mark.parametrize('data', [
|
||||
"-----BEGIN BITCOIN SIGNED MESSAGE-----\ncoinkite\n-----BEGIN BITCOIN SIGNATURE-----\nmtHSVByP9EYZmB26jASDdPVm19gvpecb5R\nH3c6imctVKRRYC1zOBAitdb/PuoQ9j0xaR6qKXH5dQECZH5OuvvE7aoL6j/WOaR/CFq/+SvIZPAzIhvQYBizBUc=\n-----END BITCOIN SIGNATURE-----",
|
||||
"\n\n-----BEGIN BITCOIN SIGNED MESSAGE-----\ncoinkite\n-----BEGIN BITCOIN SIGNATURE-----\nmtHSVByP9EYZmB26jASDdPVm19gvpecb5R\nH3c6imctVKRRYC1zOBAitdb/PuoQ9j0xaR6qKXH5dQECZH5OuvvE7aoL6j/WOaR/CFq/+SvIZPAzIhvQYBizBUc=\n-----END BITCOIN SIGNATURE-----",
|
||||
"\n\n\t-----BEGIN BITCOIN SIGNED MESSAGE-----\ncoinkite\n-----BEGIN BITCOIN SIGNATURE-----\nmtHSVByP9EYZmB26jASDdPVm19gvpecb5R\nH3c6imctVKRRYC1zOBAitdb/PuoQ9j0xaR6qKXH5dQECZH5OuvvE7aoL6j/WOaR/CFq/+SvIZPAzIhvQYBizBUc=\n-----END BITCOIN SIGNATURE-----",
|
||||
])
|
||||
def test_json_msg_verify(data, try_decode):
|
||||
ft, vals = try_decode(data)
|
||||
assert ft == "vmsg"
|
||||
assert vals[0] == data
|
||||
|
||||
# EOF
|
||||
|
||||
@ -2,13 +2,14 @@
|
||||
#
|
||||
# Message signing.
|
||||
#
|
||||
import pytest, time, os, itertools, hashlib
|
||||
import pytest, time, os, itertools, hashlib, json
|
||||
from bip32 import BIP32Node
|
||||
from msg import verify_message, RFC_SIGNATURE_TEMPLATE, sign_message, parse_signed_message
|
||||
from base64 import b64encode, b64decode
|
||||
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, CCUserRefused
|
||||
from ckcc_protocol.constants import *
|
||||
from constants import addr_fmt_names, msg_sign_unmap_addr_fmt
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
|
||||
|
||||
def default_derivation_by_af(addr_fmt, testnet=True):
|
||||
@ -68,6 +69,200 @@ def test_sign_msg_refused(dev, press_cancel):
|
||||
done = dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def verify_msg_sign_story():
|
||||
def doit(story, msg, subpath=None, addr_fmt=None, testnet=True, addr=None):
|
||||
assert story.startswith('Ok to sign this?')
|
||||
assert msg in story
|
||||
assert 'Using the key associated' in story
|
||||
|
||||
if addr:
|
||||
assert addr in story
|
||||
|
||||
if not subpath:
|
||||
assert 'm =>' not in story
|
||||
subpath = default_derivation_by_af(addr_fmt or AF_CLASSIC, testnet)
|
||||
else:
|
||||
subpath = subpath.lower().replace("'", "h")
|
||||
|
||||
assert ('%s =>' % subpath) in story
|
||||
return subpath
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def msg_sign_export(cap_story, press_nfc, nfc_read_text, press_select, press_cancel,
|
||||
readback_bbqr, cap_screen_qr, need_keypress, microsd_path,
|
||||
virtdisk_path, is_q1, OK):
|
||||
def doit(way, qr_only=False):
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
|
||||
if way == "sd":
|
||||
if "Press (1) to save Signed Msg" in story:
|
||||
need_keypress("1")
|
||||
|
||||
elif way == "nfc":
|
||||
if f"press {KEY_NFC if is_q1 else '(3)'} to share via NFC" not in story:
|
||||
pytest.xfail("NFC disabled")
|
||||
else:
|
||||
press_nfc()
|
||||
time.sleep(0.2)
|
||||
signed_msg = nfc_read_text()
|
||||
time.sleep(0.3)
|
||||
press_cancel()
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert f"Press {OK} to share again" in story
|
||||
press_cancel()
|
||||
|
||||
elif way == "qr":
|
||||
if not is_q1:
|
||||
pytest.xfail("QR disabled")
|
||||
|
||||
if not qr_only:
|
||||
need_keypress(KEY_QR)
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "Press ENTER to export signature QR only" in story
|
||||
assert "(0) to export full RFC template" in story
|
||||
press_select()
|
||||
time.sleep(.1)
|
||||
sig_only = cap_screen_qr().decode('ascii')
|
||||
press_select()
|
||||
time.sleep(.1)
|
||||
need_keypress("0")
|
||||
time.sleep(.1)
|
||||
file_type, signed_msg = readback_bbqr()
|
||||
signed_msg = signed_msg.decode()
|
||||
assert file_type == "U"
|
||||
assert sig_only in signed_msg
|
||||
press_select()
|
||||
press_cancel()
|
||||
|
||||
else:
|
||||
# virtual disk
|
||||
if "press (2) to save to Virtual Disk" not in story:
|
||||
pytest.xfail("Vdisk disabled")
|
||||
else:
|
||||
need_keypress("2")
|
||||
|
||||
if way in ("sd", "vdisk"):
|
||||
path_f = microsd_path if way == "sd" else virtdisk_path
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
fname = story.split("\n\n")[-1]
|
||||
with open(path_f(fname), "r") as f:
|
||||
signed_msg = f.read()
|
||||
|
||||
return signed_msg
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sign_msg_from_text(pick_menu_item, enter_number, press_select,
|
||||
cap_story, need_keypress, settings_set, is_q1,
|
||||
addr_vs_path, bitcoind, msg_sign_export,
|
||||
verify_msg_sign_story, OK):
|
||||
# used when signing note/passwords misc content
|
||||
# used after simple text QR scan
|
||||
# expects to start at menu which offers different single sig address formats
|
||||
|
||||
def doit(msg, addr_fmt, acct, change, idx, way, chain="XTN", qr_only=False):
|
||||
settings_set("chain", chain)
|
||||
path = "m"
|
||||
# pick address format from menu
|
||||
if addr_fmt == AF_CLASSIC:
|
||||
path += "/44h"
|
||||
af_label = "Classic P2PKH"
|
||||
elif addr_fmt == AF_P2WPKH:
|
||||
path += "/84h"
|
||||
af_label = "Segwit P2WPKH"
|
||||
else:
|
||||
path += "/49h"
|
||||
af_label = "P2SH-Segwit"
|
||||
|
||||
pick_menu_item(af_label)
|
||||
|
||||
# chain - no user input - depends on current active settings
|
||||
if chain == "BTC":
|
||||
path += "/0h"
|
||||
else:
|
||||
path += "/1h"
|
||||
|
||||
# pick account
|
||||
if acct is None:
|
||||
path += "/0h"
|
||||
press_select()
|
||||
else:
|
||||
path += ("/%dh" % acct)
|
||||
enter_number(acct)
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert title == "Change?"
|
||||
assert "Press (0) to use internal/change address" in story
|
||||
assert f"{OK} to use external/receive address" in story
|
||||
if change:
|
||||
path += "/1"
|
||||
need_keypress("0")
|
||||
else:
|
||||
path += "/0"
|
||||
press_select()
|
||||
|
||||
# index num
|
||||
if idx is None:
|
||||
path += "/0"
|
||||
press_select()
|
||||
else:
|
||||
path += ("/%d" % idx)
|
||||
enter_number(idx)
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
path = verify_msg_sign_story(story, msg, path, addr_fmt, testnet=True if chain == "XTN" else False)
|
||||
press_select()
|
||||
|
||||
signed_msg = msg_sign_export(way, qr_only)
|
||||
|
||||
ret_msg, addr, sig = parse_signed_message(signed_msg)
|
||||
addr_vs_path(addr, path, addr_fmt, testnet=True if chain == "XTN" else False)
|
||||
assert verify_message(addr, sig, ret_msg) is True
|
||||
if addr_fmt == AF_CLASSIC and chain == "XTN":
|
||||
res = bitcoind.rpc.verifymessage(addr, sig, ret_msg)
|
||||
assert res is True
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sign_msg_from_address(need_keypress, scan_a_qr, press_select, enter_complex, cap_story,
|
||||
addr_vs_path, verify_msg_sign_story, msg_sign_export):
|
||||
def doit(msg, addr, subpath, addr_fmt, way=None, testnet=True):
|
||||
if way == 'qr':
|
||||
# scan text via QR
|
||||
need_keypress(KEY_QR)
|
||||
scan_a_qr(msg)
|
||||
time.sleep(1)
|
||||
press_select()
|
||||
else:
|
||||
enter_complex(msg, b39pass=False)
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
verify_msg_sign_story(story, msg, subpath, addr_fmt, testnet, addr)
|
||||
press_select()
|
||||
time.sleep(.1)
|
||||
signed_msg = msg_sign_export(way)
|
||||
ret_msg, addr, sig = parse_signed_message(signed_msg)
|
||||
addr_vs_path(addr, subpath, addr_fmt, testnet=testnet)
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path,expect', [
|
||||
('1/1hard/2', 'invalid characters'),
|
||||
('m/m/m/1/1hard/2', 'invalid characters'),
|
||||
@ -92,24 +287,36 @@ def test_bad_paths(dev, path, expect):
|
||||
|
||||
@pytest.fixture
|
||||
def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
press_select, microsd_path):
|
||||
press_select, microsd_path, verify_msg_sign_story):
|
||||
|
||||
# sign a file on the microSD card
|
||||
|
||||
def doit(msg, subpath="", addr_fmt=None, expect_fail=False, testnet=True):
|
||||
fname = 't-msgsign.txt'
|
||||
def doit(msg, subpath="", addr_fmt=None, expect_fail=False, testnet=True,
|
||||
use_json=False):
|
||||
|
||||
suffix = "json" if use_json else "txt"
|
||||
fname = f't-msgsign.{suffix}'
|
||||
result_fname = 't-msgsign-signed.txt'
|
||||
|
||||
# cleanup
|
||||
try: os.unlink(microsd_path(result_fname))
|
||||
except OSError: pass
|
||||
|
||||
|
||||
with open_microsd(fname, 'wt') as sd:
|
||||
sd.write(msg + '\n')
|
||||
if subpath or addr_fmt:
|
||||
sd.write((subpath or "") + '\n')
|
||||
if use_json:
|
||||
res = {"msg": msg}
|
||||
if subpath:
|
||||
res["subpath"] = subpath
|
||||
if addr_fmt is not None:
|
||||
sd.write(addr_fmt_names[addr_fmt])
|
||||
res["addr_fmt"] = addr_fmt_names[addr_fmt]
|
||||
sd.write(json.dumps(res))
|
||||
else:
|
||||
sd.write(msg + '\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')
|
||||
@ -129,19 +336,8 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
assert not story.startswith('Ok to sign this?')
|
||||
return story
|
||||
|
||||
assert story.startswith('Ok to sign this?')
|
||||
|
||||
assert msg in story
|
||||
assert 'Using the key associated' in story
|
||||
if not subpath:
|
||||
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
|
||||
|
||||
press_select()
|
||||
verify_msg_sign_story(story, msg, subpath, addr_fmt, testnet)
|
||||
press_select() # confirm msg sign
|
||||
|
||||
# wait for it to finish
|
||||
for r in range(10):
|
||||
@ -151,27 +347,23 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
else:
|
||||
assert False, 'timed out'
|
||||
|
||||
lines = [i.strip() for i in open_microsd(result_fname, 'rt').readlines()]
|
||||
with open_microsd(result_fname, 'rt') as f:
|
||||
res = f.read()
|
||||
|
||||
assert lines[0] == '-----BEGIN BITCOIN SIGNED MESSAGE-----'
|
||||
assert lines[1:-4] == [msg]
|
||||
assert lines[-4] == '-----BEGIN BITCOIN SIGNATURE-----'
|
||||
addr = lines[-3]
|
||||
sig = lines[-2]
|
||||
assert lines[-1] == '-----END BITCOIN SIGNATURE-----'
|
||||
|
||||
return sig, addr
|
||||
ret_msg, addr, sig = parse_signed_message(res)
|
||||
assert ret_msg == msg
|
||||
return sig, addr, msg
|
||||
|
||||
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("use_json", [True, False])
|
||||
@pytest.mark.parametrize('msg', [ 'ab', 'abc def eght', "x"*140, 'a'*240])
|
||||
@pytest.mark.parametrize('path', [
|
||||
"m/84'/0'/22'",
|
||||
None,
|
||||
'm',
|
||||
"m/1/2",
|
||||
"m/1'/100'",
|
||||
'm/23h/22h',
|
||||
])
|
||||
@pytest.mark.parametrize('addr_fmt', [
|
||||
@ -182,11 +374,14 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
])
|
||||
@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):
|
||||
addr_fmt, testnet, settings_set, bitcoind,
|
||||
use_json):
|
||||
|
||||
settings_set("chain", "XTN" if testnet else "BTC")
|
||||
# cases we expect to work
|
||||
sig, addr = sign_on_microsd(msg, path, addr_fmt, testnet=testnet)
|
||||
sig, addr, ret_msg = sign_on_microsd(msg, path, addr_fmt, testnet=testnet,
|
||||
use_json=use_json)
|
||||
assert msg == ret_msg
|
||||
|
||||
raw = b64decode(sig)
|
||||
assert 40 <= len(raw) <= 65
|
||||
@ -201,13 +396,29 @@ def test_sign_msg_microsd_good(sign_on_microsd, msg, path, addr_vs_path,
|
||||
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)
|
||||
res = bitcoind.rpc.verifymessage(addr, sig, ret_msg)
|
||||
assert res is True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sign_using_nfc(goto_home, pick_menu_item, nfc_write_text, cap_story):
|
||||
def doit(body, expect_fail=True):
|
||||
def sign_using_nfc(goto_home, pick_menu_item, nfc_write_text, cap_story, press_select,
|
||||
nfc_read_text, addr_vs_path, press_cancel, OK, verify_msg_sign_story):
|
||||
def doit(msg, subpath=None, addr_fmt=None, expect_fail=False, use_json=False,
|
||||
testnet=True):
|
||||
if use_json:
|
||||
res = {"msg": msg}
|
||||
if subpath:
|
||||
res["subpath"] = subpath
|
||||
if addr_fmt is not None:
|
||||
res["addr_fmt"] = addr_fmt_names[addr_fmt]
|
||||
body = json.dumps(res)
|
||||
else:
|
||||
body = msg + "\n"
|
||||
if subpath or addr_fmt:
|
||||
body += ((subpath or "") + '\n')
|
||||
if addr_fmt is not None:
|
||||
body += addr_fmt_names[addr_fmt]
|
||||
|
||||
goto_home()
|
||||
pick_menu_item('Advanced/Tools')
|
||||
pick_menu_item('NFC Tools')
|
||||
@ -216,49 +427,115 @@ def sign_using_nfc(goto_home, pick_menu_item, nfc_write_text, cap_story):
|
||||
time.sleep(0.5)
|
||||
if expect_fail:
|
||||
return cap_story()
|
||||
raise NotImplementedError
|
||||
|
||||
if not addr_fmt:
|
||||
addr_fmt = AF_CLASSIC
|
||||
|
||||
if not subpath:
|
||||
subpath = default_derivation_by_af(addr_fmt, testnet=testnet)
|
||||
|
||||
_, story = cap_story()
|
||||
subpath = verify_msg_sign_story(story, msg, subpath, addr_fmt, testnet)
|
||||
press_select()
|
||||
signed_msg = nfc_read_text()
|
||||
if "BITCOIN SIGNED MESSAGE" not in signed_msg:
|
||||
# missed it? again
|
||||
signed_msg = nfc_read_text()
|
||||
press_select() # exit NFC animation
|
||||
pmsg, addr, sig = parse_signed_message(signed_msg)
|
||||
assert pmsg == msg
|
||||
addr_vs_path(addr, subpath, addr_fmt, testnet=testnet)
|
||||
assert verify_message(addr, sig, msg) is True
|
||||
time.sleep(0.5)
|
||||
_, story = cap_story()
|
||||
assert f"Press {OK} to share again" in story
|
||||
press_select()
|
||||
signed_msg_again = nfc_read_text()
|
||||
assert signed_msg == signed_msg_again
|
||||
press_cancel() # exit NFC animation
|
||||
press_cancel() # do not want to share again
|
||||
|
||||
return sig, addr, msg
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.mark.parametrize('msg,concern,no_file', [
|
||||
('', 'too short', 0), # zero length not supported
|
||||
('a'*1000, 'too long', 1), # too big, won't even be offered as a file
|
||||
('a'*300, 'too long', 0), # too big
|
||||
('a'*241, 'too long', 0), # too big
|
||||
('hello%20sworld'%'', 'many spaces', 0), # spaces
|
||||
('hello%10sworld'%'', 'many spaces', 0), # spaces
|
||||
('hello%5sworld'%'', 'many spaces', 0), # spaces
|
||||
('test\ttest', "must be ascii printable", 0),
|
||||
('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'):
|
||||
|
||||
@pytest.mark.bitcoind
|
||||
@pytest.mark.parametrize("way", ["nfc", "sd"])
|
||||
@pytest.mark.parametrize("msg", ['test\ttest', "\n\n\tmsg\n\n\tsigning"])
|
||||
def test_sign_msg_with_ascii_non_printable_chars(msg, way, sign_on_microsd, addr_vs_path,
|
||||
settings_set, bitcoind, sign_using_nfc):
|
||||
# only works with the JSON format
|
||||
settings_set("chain", "XTN")
|
||||
if way == "sd":
|
||||
sig, addr, ret_msg = sign_on_microsd(msg, "", None, use_json=True)
|
||||
else:
|
||||
sig, addr, ret_msg = sign_using_nfc(msg, "", None, use_json=True)
|
||||
|
||||
assert ret_msg == msg
|
||||
raw = b64decode(sig)
|
||||
assert 40 <= len(raw) <= 65
|
||||
|
||||
addr_fmt = AF_CLASSIC
|
||||
path = default_derivation_by_af(addr_fmt, testnet=True)
|
||||
|
||||
# check expected addr was used
|
||||
addr_vs_path(addr, path, addr_fmt)
|
||||
assert verify_message(addr, sig, msg) is True
|
||||
res = bitcoind.rpc.verifymessage(addr, sig, msg)
|
||||
assert res is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize('msg,subpath,addr_fmt,concern,no_file,no_json', [
|
||||
('', "m", AF_CLASSIC, 'too short', 0, 0), # zero length not supported
|
||||
('a'*1000, "m/1", AF_P2WPKH,'too long', 1, 0), # too big, won't even be offered as a file
|
||||
('a'*241, "m/400", AF_P2WPKH_P2SH, 'too long', 0, 0), # too big
|
||||
('hello%20sworld'%'', "m", AF_CLASSIC, 'many spaces', 0, 0), # spaces
|
||||
('hello%10sworld'%'', "m/1h/3h", AF_P2WPKH_P2SH, 'many spaces', 0, 0), # spaces
|
||||
('hello%5sworld'%'', "m", AF_CLASSIC, 'many spaces', 0, 0), # spaces
|
||||
("coinkite", "m", AF_P2WSH, "Invalid address format", 0, 0), # invalid address format
|
||||
("coinkite", "m", AF_P2WSH_P2SH, "Invalid address format", 0, 0), # invalid address format
|
||||
("coinkite", " m", AF_P2TR, "Invalid address format", 0, 0), # invalid address format
|
||||
("coinkite", "m/0/0/0/0/0/0/0/0/0/0/0/0/0", AF_CLASSIC, "too deep", 0, 0), # invalid path
|
||||
("coinkite", "m/0/0/0/0/0/q/0/0/0", AF_P2WPKH, "invalid characters in path", 0, 0), # invalid path
|
||||
("coinkite ", "m", AF_CLASSIC, "trailing space(s)", 0, 0), # invalid msg - trailing space
|
||||
(" coinkite", "m", AF_P2WPKH_P2SH, "leading space(s)", 0, 0), # invalid msg - leading space
|
||||
('testêtest', "m", AF_P2WPKH, "must be ascii", 0, 0),
|
||||
# below works only with the JSON format
|
||||
('test\ttest', "m", AF_CLASSIC, "must be ascii printable", 0, 1),
|
||||
])
|
||||
@pytest.mark.parametrize("use_json", [True, False])
|
||||
@pytest.mark.parametrize('transport', ['sd', 'usb', 'nfc'])
|
||||
def test_sign_msg_fails(dev, sign_on_microsd, msg, subpath, addr_fmt, concern,
|
||||
no_file, no_json, transport, sign_using_nfc, use_json):
|
||||
if use_json and no_json:
|
||||
# special cases with ascii non printable characters - can be present in json
|
||||
raise pytest.skip("json can contain ASCII non-printable in msg")
|
||||
if transport == 'usb':
|
||||
with pytest.raises(CCProtoError) as ee:
|
||||
try:
|
||||
encoded_msg = msg.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
encoded_msg = msg.encode()
|
||||
dev.send_recv(CCProtocolPacker.sign_message(encoded_msg, path), timeout=None)
|
||||
dev.send_recv(CCProtocolPacker.sign_message(encoded_msg, subpath, addr_fmt), timeout=None)
|
||||
story = ee.value.args[0]
|
||||
elif transport == 'sd':
|
||||
try:
|
||||
story = sign_on_microsd(msg, path, expect_fail=True)
|
||||
story = sign_on_microsd(msg, subpath, addr_fmt, expect_fail=True, use_json=use_json)
|
||||
assert story.startswith('Problem: ')
|
||||
except AssertionError as e:
|
||||
if no_file:
|
||||
assert ("No suitable files found" in str(e)) or story == 'NO-FILE'
|
||||
return
|
||||
elif transport == 'nfc':
|
||||
title, story = sign_using_nfc(msg+"\n"+path, expect_fail=True)
|
||||
title, story = sign_using_nfc(msg, subpath, addr_fmt, expect_fail=True, use_json=use_json)
|
||||
assert title == 'ERROR' or "Problem" in story
|
||||
else:
|
||||
raise ValueError(transport)
|
||||
|
||||
assert concern in story
|
||||
|
||||
|
||||
@pytest.mark.parametrize('msg,num_iter,expect', [
|
||||
('Test2', 1, 'IHra0jSywF1TjIJ5uf7IDECae438cr4o3VmG6Ri7hYlDL+pUEXyUfwLwpiAfUQVqQFLgs6OaX0KsoydpuwRI71o='),
|
||||
('Test', 2, 'IDgMx1ljPhLHlKUOwnO/jBIgK+K8n8mvDUDROzTgU8gOaPDMs+eYXJpNXXINUx5WpeV605p5uO6B3TzBVcvs478='),
|
||||
@ -303,36 +580,15 @@ def test_low_R_cases(msg, num_iter, expect, dev, set_seed_words, use_mainnet,
|
||||
|
||||
assert sig == expect
|
||||
|
||||
@pytest.mark.parametrize("body", [
|
||||
"coinkite\nm\np2wsh", # invalid address format
|
||||
"coinkite\nm\np2sh-p2wsh", # invalid address format
|
||||
"coinkite\nm\np2tr", # invalid address format
|
||||
"coinkite\nm/0/0/0/0/0/0/0/0/0/0/0/0/0\np2pkh", # invalid path
|
||||
"coinkite\nm/0/0/0/0/0/q/0/0/0\np2pkh", # invalid path
|
||||
"coinkite yes!\nm\np2pkh", # invalid msg - too many spaces
|
||||
"c\nm\np2pkh", # invalid msg - too short
|
||||
"coinkite \nm\np2pkh", # invalid msg - trailing space
|
||||
" coinkite\nm\np2pkh", # invalid msg - leading space
|
||||
])
|
||||
def test_nfc_msg_signing_invalid(body, goto_home, pick_menu_item, nfc_write_text, cap_story):
|
||||
goto_home()
|
||||
pick_menu_item('Advanced/Tools')
|
||||
pick_menu_item('NFC Tools')
|
||||
pick_menu_item('Sign Message')
|
||||
nfc_write_text(body)
|
||||
time.sleep(0.5)
|
||||
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,
|
||||
testnet, settings_set, bitcoind):
|
||||
@pytest.mark.parametrize("use_json", [True, False])
|
||||
@pytest.mark.parametrize("msg", ["Coldcard Signing Device!", 200 * "a"])
|
||||
@pytest.mark.parametrize("path", ["", "m/84'/0'/0'/300/0", "m/0/0/0/0/1/1/1"])
|
||||
@pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, None, AF_P2WPKH, AF_P2WPKH_P2SH])
|
||||
def test_nfc_msg_signing(msg, path, addr_fmt, testnet, settings_set, bitcoind, use_json,
|
||||
sign_using_nfc, goto_home):
|
||||
settings_set("chain", "XTN" if testnet else "BTC")
|
||||
|
||||
for _ in range(5):
|
||||
@ -343,45 +599,10 @@ def test_nfc_msg_signing(msg, path, str_addr_fmt, nfc_write_text, nfc_read_text,
|
||||
except:
|
||||
time.sleep(0.5)
|
||||
|
||||
pick_menu_item('Advanced/Tools')
|
||||
pick_menu_item('NFC Tools')
|
||||
pick_menu_item('Sign Message')
|
||||
if str_addr_fmt != "":
|
||||
addr_fmt = msg_sign_unmap_addr_fmt[str_addr_fmt]
|
||||
body = "\n".join([msg, path, str_addr_fmt])
|
||||
else:
|
||||
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()
|
||||
assert "Ok to sign this?" in story
|
||||
assert msg in story
|
||||
assert path.replace("'", "h") in story
|
||||
press_select()
|
||||
signed_msg = nfc_read_text()
|
||||
if "BITCOIN SIGNED MESSAGE" not in signed_msg:
|
||||
# missed it? again
|
||||
signed_msg = 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, testnet=testnet)
|
||||
assert verify_message(addr, sig, msg) is True
|
||||
time.sleep(0.5)
|
||||
_, story = cap_story()
|
||||
assert f"Press {OK} to share again" in story
|
||||
press_select()
|
||||
signed_msg_again = nfc_read_text()
|
||||
assert signed_msg == signed_msg_again
|
||||
press_cancel() # exit NFC animation
|
||||
press_cancel() # do not want to share again
|
||||
addr, sig, ret_msg = sign_using_nfc(msg, path, addr_fmt, testnet=testnet, use_json=use_json)
|
||||
assert msg == ret_msg
|
||||
if addr_fmt == AF_CLASSIC and testnet:
|
||||
res = bitcoind.rpc.verifymessage(addr, sig, msg)
|
||||
res = bitcoind.rpc.verifymessage(sig, addr, ret_msg)
|
||||
assert res is True
|
||||
|
||||
@pytest.fixture
|
||||
@ -417,7 +638,8 @@ def test_verify_signature_file(way, addr_fmt, path, msg, sign_on_microsd, goto_h
|
||||
cap_story, bitcoind, microsd_path, nfc_write_text,
|
||||
verify_armored_signature, chain, settings_set):
|
||||
settings_set("chain", chain)
|
||||
sig, addr = sign_on_microsd(msg, path, msg_sign_unmap_addr_fmt[addr_fmt])
|
||||
sig, addr, ret_msg = sign_on_microsd(msg, path, msg_sign_unmap_addr_fmt[addr_fmt])
|
||||
assert ret_msg == msg
|
||||
fname = 't-msgsign-signed.txt'
|
||||
should = RFC_SIGNATURE_TEMPLATE.format(addr=addr, sig=sig, msg=msg)
|
||||
with open(microsd_path(fname), "r") as f:
|
||||
@ -705,4 +927,77 @@ def test_verify_signature_file_truncated(way, microsd_path, cap_story, verify_ar
|
||||
assert "Armor text MUST be surrounded by exactly five (5) dashes" in story
|
||||
assert "auth.py" in story
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msg", ["this is the message to sign", "this is meessage to sign\n with newline", "a"*200])
|
||||
@pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, AF_P2WPKH])
|
||||
@pytest.mark.parametrize("acct", [None, 5555])
|
||||
def test_sign_scanned_text(msg, addr_fmt, acct, goto_home, need_keypress, scan_a_qr,
|
||||
sign_msg_from_text, cap_story, skip_if_useless_way):
|
||||
skip_if_useless_way("qr")
|
||||
goto_home()
|
||||
need_keypress(KEY_QR)
|
||||
scan_a_qr(msg)
|
||||
time.sleep(1)
|
||||
title, story = cap_story()
|
||||
assert title == "Simple Text"
|
||||
assert "Press (0) to sign the text" in story
|
||||
need_keypress("0")
|
||||
sign_msg_from_text(msg, addr_fmt, acct, False, 999, "qr", "XTN", True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", [
|
||||
{"msg": "msg to be signed via QR"},
|
||||
{"msg": "msg with some\n\t\n control characters", "addr_fmt": "p2sh-p2wpkh"},
|
||||
{"msg": 100*"CC", "addr_fmt": "p2wpkh", "subpath": "m/900h/0"},
|
||||
])
|
||||
@pytest.mark.parametrize("way", ["sd", "nfc", "qr"])
|
||||
def test_sign_scanned_json(data, way, goto_home, need_keypress, scan_a_qr,
|
||||
cap_story, msg_sign_export, press_select,
|
||||
addr_vs_path, bitcoind, skip_if_useless_way,
|
||||
verify_msg_sign_story):
|
||||
skip_if_useless_way(way)
|
||||
goto_home()
|
||||
af = data.get("addr_fmt", None)
|
||||
if not af:
|
||||
addr_fmt = AF_CLASSIC
|
||||
else:
|
||||
addr_fmt = msg_sign_unmap_addr_fmt[af]
|
||||
|
||||
need_keypress(KEY_QR)
|
||||
scan_a_qr(json.dumps(data))
|
||||
time.sleep(1)
|
||||
title, story = cap_story()
|
||||
|
||||
subpath = verify_msg_sign_story(story, data["msg"], data.get("subpath", None), addr_fmt)
|
||||
press_select()
|
||||
|
||||
signed_msg = msg_sign_export(way)
|
||||
ret_msg, addr, sig = parse_signed_message(signed_msg)
|
||||
assert ret_msg == data["msg"]
|
||||
# check expected addr was used
|
||||
addr_vs_path(addr, subpath, addr_fmt)
|
||||
assert verify_message(addr, sig, ret_msg) is True
|
||||
if addr_fmt == AF_CLASSIC:
|
||||
res = bitcoind.rpc.verifymessage(addr, sig, ret_msg)
|
||||
assert res is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msg", [(50*"a")+"\n\n"+(100*"b"), "Balance replenish 564565456254"])
|
||||
def test_verify_scanned_signed_msg(msg, scan_a_qr, need_keypress, goto_home, cap_story,
|
||||
skip_if_useless_way):
|
||||
skip_if_useless_way("qr")
|
||||
wallet = BIP32Node.from_master_secret(os.urandom(32))
|
||||
addr = wallet.address()
|
||||
sk = bytes(wallet.node.private_key)
|
||||
sig = sign_message(sk, msg.encode())
|
||||
armored = RFC_SIGNATURE_TEMPLATE.format(addr=addr, sig=sig, msg=msg)
|
||||
|
||||
goto_home()
|
||||
need_keypress(KEY_QR)
|
||||
scan_a_qr(armored)
|
||||
time.sleep(1)
|
||||
title, story = cap_story()
|
||||
assert title == "CORRECT"
|
||||
assert "Good signature by address" in story
|
||||
|
||||
# EOF
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import pytest, time, json, random, os, pdb
|
||||
from helpers import prandom
|
||||
from charcodes import *
|
||||
|
||||
from constants import AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2WPKH
|
||||
from test_bbqr import readback_bbqr
|
||||
from bbqr import split_qrs
|
||||
|
||||
@ -43,10 +43,10 @@ def goto_notes(cap_story, cap_menu, press_select, goto_home, pick_menu_item):
|
||||
@pytest.fixture
|
||||
def need_some_notes(settings_get, settings_set):
|
||||
# create a note or use what's there, provide as obj
|
||||
def doit():
|
||||
def doit(title='Title Here', body='Body'):
|
||||
notes = settings_get('notes', [])
|
||||
if not notes:
|
||||
settings_set('notes', [dict(misc='Body', title='Title Here')])
|
||||
settings_set('notes', [dict(misc=body, title=title)])
|
||||
return notes
|
||||
return doit
|
||||
|
||||
@ -384,7 +384,7 @@ def test_huge_notes(size, encoding, goto_notes, enter_text, cap_menu, need_keypr
|
||||
|
||||
time.sleep(.5) # decompression time in some cases
|
||||
m = cap_menu()
|
||||
assert m[-1] == 'Export'
|
||||
assert m[-2] == 'Export'
|
||||
|
||||
notes = settings_get('notes')
|
||||
assert len(notes) == 1
|
||||
@ -624,4 +624,41 @@ def test_tmp_notes_separation(goto_notes, pick_menu_item, generate_ephemeral_wor
|
||||
assert 'pwd-tmp' not in mm
|
||||
assert 'note-tmp2' not in mm
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msg", ["COLDCARD rocks!", "cc\nCC"])
|
||||
@pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH])
|
||||
@pytest.mark.parametrize("acct", [None, 0, 9999])
|
||||
@pytest.mark.parametrize("way", ["sd", "qr", "nfc", "vdisk"])
|
||||
def test_sign_note_body(msg, addr_fmt, acct, need_some_notes,
|
||||
pick_menu_item, sign_msg_from_text, way,
|
||||
goto_notes, settings_set):
|
||||
settings_set("notes", [])
|
||||
title = "aaa"
|
||||
need_some_notes(title, msg)
|
||||
goto_notes()
|
||||
pick_menu_item(f"1: {title}")
|
||||
pick_menu_item("Sign Note Text")
|
||||
sign_msg_from_text(msg, addr_fmt, acct, False, 0, way)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chain", ["BTC", "XTN"])
|
||||
@pytest.mark.parametrize("change", [True, False])
|
||||
@pytest.mark.parametrize("idx", [None, 0, 9999])
|
||||
def test_sign_password_free_form(chain, change, idx, need_some_passwords, settings_set,
|
||||
goto_notes, pick_menu_item, sign_msg_from_text):
|
||||
settings_set('notes', []) # clear
|
||||
title = "A"
|
||||
msg = 'More Notes AAAA'
|
||||
settings_set('notes', [
|
||||
{'misc': msg,
|
||||
'password': 'fds65fd5f1sd51s',
|
||||
'site': 'https://a.com',
|
||||
'title': title,
|
||||
'user': 'AAA'}
|
||||
])
|
||||
goto_notes()
|
||||
pick_menu_item(f"1: {title}")
|
||||
pick_menu_item("Sign Note Text")
|
||||
sign_msg_from_text(msg, AF_P2WPKH, None, change, idx, "qr", chain)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -92,8 +92,7 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
|
||||
menu_item = expect_name = 'Classic P2PKH'
|
||||
path = "m/44h/{ct}h/{acc}h"
|
||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||
expect_name = 'P2WPKH-in-P2SH'
|
||||
menu_item = 'P2SH-Segwit'
|
||||
menu_item = expect_name = 'P2SH-Segwit'
|
||||
path = "m/49h/{ct}h/{acc}h"
|
||||
clear_ms()
|
||||
elif addr_fmt == AF_P2WPKH:
|
||||
@ -154,25 +153,39 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
|
||||
@pytest.mark.parametrize('valid', [ True, False] )
|
||||
@pytest.mark.parametrize('testnet', [ True, False] )
|
||||
@pytest.mark.parametrize('method', [ 'qr', 'nfc'] )
|
||||
def test_ux(valid, testnet, method,
|
||||
@pytest.mark.parametrize('multisig', [ True, False] )
|
||||
def test_ux(valid, testnet, method,
|
||||
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
|
||||
press_cancel, press_select, settings_set, is_q1, nfc_write, need_keypress,
|
||||
cap_screen, cap_story, load_shared_mod, scan_a_qr
|
||||
cap_screen, cap_story, load_shared_mod, scan_a_qr, skip_if_useless_way,
|
||||
sign_msg_from_address, multisig, import_ms_wallet, clear_ms,
|
||||
):
|
||||
|
||||
skip_if_useless_way(method)
|
||||
addr_fmt = AF_CLASSIC
|
||||
|
||||
if valid:
|
||||
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
|
||||
path = "m/44h/{ct}h/{acc}h/0/3".format(acc=0, ct=(1 if testnet else 0))
|
||||
sk = mk.subkey_for_path(path)
|
||||
addr = sk.address(netcode="XTN" if testnet else "BTC")
|
||||
if multisig:
|
||||
from test_multisig import make_ms_address, HARD
|
||||
M, N = 2, 3
|
||||
|
||||
expect_name = f'own_ux_test'
|
||||
clear_ms()
|
||||
keys = import_ms_wallet(M, N, AF_P2WSH, name=expect_name, accept=1)
|
||||
|
||||
# iffy: no cosigner index in this wallet, so indicated that w/ path_mapper
|
||||
addr, scriptPubKey, script, details = make_ms_address(
|
||||
M, keys, is_change=0, idx=50, addr_fmt=AF_P2WSH,
|
||||
testnet=int(testnet), path_mapper=lambda cosigner: [HARD(45), 0, 50]
|
||||
)
|
||||
else:
|
||||
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
|
||||
path = "m/44h/{ct}h/{acc}h/0/3".format(acc=0, ct=(1 if testnet else 0))
|
||||
sk = mk.subkey_for_path(path)
|
||||
addr = sk.address(netcode="XTN" if testnet else "BTC")
|
||||
else:
|
||||
addr = fake_address(addr_fmt, testnet)
|
||||
addr = fake_address(addr_fmt, testnet)
|
||||
|
||||
if method == 'qr':
|
||||
if not is_q1:
|
||||
raise pytest.skip('no QR on Mk4')
|
||||
goto_home()
|
||||
pick_menu_item('Scan Any QR Code')
|
||||
scan_a_qr(addr)
|
||||
@ -214,7 +227,17 @@ def test_ux(valid, testnet, method,
|
||||
assert title == 'Verified Address'
|
||||
assert 'Found in wallet' in story
|
||||
assert 'Derivation path' in story
|
||||
assert 'P2PKH' in story
|
||||
|
||||
if multisig:
|
||||
assert expect_name in story
|
||||
assert "Press (0) to sign message with this key" not in story
|
||||
else:
|
||||
assert 'P2PKH' in story
|
||||
assert "Press (0) to sign message with this key" in story
|
||||
need_keypress('0')
|
||||
msg = "coinkite CC the most solid HWW"
|
||||
sign_msg_from_address(msg, addr, path, addr_fmt, method, testnet)
|
||||
|
||||
else:
|
||||
assert title == 'Unknown Address'
|
||||
assert 'Searched ' in story
|
||||
@ -280,9 +303,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
|
||||
assert title == 'Verified Address'
|
||||
assert 'Found in wallet' in story
|
||||
assert 'Derivation path' in story
|
||||
if af == "P2SH-Segwit":
|
||||
assert "P2WPKH-in-P2SH" in story
|
||||
elif af == "Segwit P2WPKH":
|
||||
if af == "Segwit P2WPKH":
|
||||
assert " P2WPKH " in story
|
||||
else:
|
||||
assert af in story
|
||||
|
||||
Loading…
Reference in New Issue
Block a user