upgrade msg signing

This commit is contained in:
scgbckbone 2024-09-20 12:16:03 +02:00 committed by doc-hex
parent a8202972b3
commit a0949ecb87
22 changed files with 968 additions and 375 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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'),
]

View File

@ -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")

View File

@ -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

View File

@ -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...

View File

@ -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)

View File

@ -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

View File

@ -41,6 +41,7 @@ addr_fmt_names = {
AF_P2WSH: 'p2wsh',
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
AF_P2WSH_P2SH: 'p2wsh-p2sh',
AF_P2TR: "p2tr",
}

View File

@ -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):

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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