BIP-322 msg verification
This commit is contained in:
parent
d5d7c4bb68
commit
8342e289ea
@ -13,8 +13,8 @@ from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, AF_P2SH
|
||||
from sffile import SFFile
|
||||
from menu import MenuSystem, MenuItem
|
||||
from serializations import ser_uint256, SIGHASH_ALL
|
||||
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys, ux_confirm
|
||||
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction, the_ux, ux_enter_number
|
||||
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys, ux_confirm, the_ux
|
||||
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction, ux_input_text, ux_enter_number
|
||||
from usb import CCBusyError
|
||||
from utils import (HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A,
|
||||
show_single_address, keypath_to_str, seconds2human_readable)
|
||||
@ -291,6 +291,56 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
self.chain = chains.current_chain()
|
||||
self.miniscript_wallet = miniscript_wallet
|
||||
|
||||
async def por322_msg_verify(self):
|
||||
# https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c
|
||||
from glob import NFC
|
||||
from ux import import_export_prompt
|
||||
from actions import file_picker
|
||||
ch = await import_export_prompt("message", is_import=True, force_prompt=True,
|
||||
intro="Import msg that hashes to 'to_spend' msg hash.",
|
||||
key0="to input message manually", title="BIP-322 MSG",
|
||||
no_qr=not version.has_qwerty)
|
||||
|
||||
# TODO move elswhere
|
||||
bip322_tag_hash = b'te\x84\xa1\x87/\xa1\x00AUN\xff\xa08\xd6\x12IB\xddy\xb4\xe5\x8aL\xda\x18N\x13\xdb\xe6,I'
|
||||
|
||||
if ch == KEY_CANCEL:
|
||||
return
|
||||
elif ch == "0":
|
||||
msg = await ux_input_text("")
|
||||
elif ch == KEY_NFC:
|
||||
msg = await NFC.read_bip322_msg()
|
||||
elif ch == KEY_QR:
|
||||
from ux_q1 import QRScannerInteraction
|
||||
msg = await QRScannerInteraction().scan_text('Scan MSG from a QR code')
|
||||
else:
|
||||
choices = await file_picker(suffix='.txt', ux=False)
|
||||
target = "%s.txt" % b2a_hex(self.psbt.por322_msg_hash).decode()
|
||||
|
||||
for fname, dir, _ in choices:
|
||||
if target == fname:
|
||||
fn = dir + "/" + fname
|
||||
break
|
||||
else:
|
||||
fn = await file_picker(choices=choices)
|
||||
|
||||
if not fn: return
|
||||
|
||||
with CardSlot(readonly=True, **ch) as card:
|
||||
with open(fn, 'rt') as fd:
|
||||
msg = fd.read()
|
||||
|
||||
# TODO needs newer libngu with sha256t
|
||||
assert msg, "need msg"
|
||||
msg_hash = ngu.hash.sha256s(bip322_tag_hash+bip322_tag_hash+msg)
|
||||
assert msg_hash == self.psbt.por322_msg_hash, "hash verification failed"
|
||||
ch = await ux_show_story(
|
||||
msg+"\n\nPress %s to approve message, otherwise %s to exit." % (OK, X),
|
||||
title="MSG:"
|
||||
)
|
||||
return True if ch == "y" else False
|
||||
|
||||
|
||||
def render_output(self, o):
|
||||
# Pretty-print a transactions output.
|
||||
# - expects CTxOut object
|
||||
@ -430,6 +480,16 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
msg.write('(%d warnings below)\n\n' % wl)
|
||||
|
||||
if self.psbt.por322:
|
||||
|
||||
try:
|
||||
if not await self.por322_msg_verify():
|
||||
self.refused = True
|
||||
await ux_dramatic_pause("Refused.", 1)
|
||||
self.done()
|
||||
return
|
||||
except Exception as exc:
|
||||
return await self.failure("Msg verification failed.", exc)
|
||||
|
||||
msg.write("Proof of Reserves\n\n")
|
||||
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
|
||||
msg.write("Message Hash:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_hash).decode())
|
||||
|
||||
@ -736,6 +736,10 @@ class NFCHandler:
|
||||
f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None
|
||||
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
|
||||
|
||||
async def read_bip322_msg(self):
|
||||
f = lambda x: x.decode()
|
||||
return await self._nfc_reader(f, 'Unable to find BIP-322 message.')
|
||||
|
||||
async def _nfc_reader(self, func, fail_msg):
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
15
shared/ux.py
15
shared/ux.py
@ -357,12 +357,12 @@ async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
|
||||
|
||||
return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel)
|
||||
|
||||
def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
|
||||
def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False, key0=None, key6=None):
|
||||
from glob import NFC, VD
|
||||
|
||||
prompt, escape = None, KEY_CANCEL+"x"
|
||||
|
||||
if (NFC or VD) or num_sd_slots>1:
|
||||
if (NFC or VD) or (num_sd_slots > 1) or key0 or key6:
|
||||
if slot_b_only and (num_sd_slots>1):
|
||||
prompt = "Press (B) to import %s from lower slot SD Card" % title
|
||||
escape += "b"
|
||||
@ -388,6 +388,14 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
|
||||
prompt += ", " + KEY_QR + " to scan QR code"
|
||||
escape += KEY_QR
|
||||
|
||||
if key6:
|
||||
prompt += ', (6) ' + key6
|
||||
escape += '6'
|
||||
|
||||
if key0:
|
||||
prompt += ', (0) ' + key0
|
||||
escape += '0'
|
||||
|
||||
prompt += "."
|
||||
|
||||
return prompt, escape
|
||||
@ -492,7 +500,8 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
||||
from glob import NFC
|
||||
|
||||
if is_import:
|
||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
|
||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only,
|
||||
key0=key0, key6=key6)
|
||||
else:
|
||||
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, key6=key6, key0=key0,
|
||||
force_prompt=force_prompt, offer_kt=offer_kt)
|
||||
|
||||
@ -18,7 +18,20 @@ def bip322_msg_hash(msg):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bip322_txn(dev, pytestconfig):
|
||||
def create_msg_file(sim_root_dir, garbage_collector):
|
||||
|
||||
def doit(msg, msg_hash):
|
||||
# carelessly overwrites
|
||||
fpath = f"{sim_root_dir}/MicroSD/{msg_hash.hex()}.txt"
|
||||
with open(fpath, "w") as f:
|
||||
f.write(msg.decode())
|
||||
garbage_collector.append(fpath)
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bip322_txn(dev, pytestconfig, create_msg_file):
|
||||
|
||||
def doit(inputs, msg=b"POR", addr_fmt="p2wpkh", input_amount=1E8, to_sign_lock_time=0,
|
||||
sighash=None, psbt_hacker=None, witness_utxo=[], to_sign_nVersion=0):
|
||||
@ -88,7 +101,9 @@ def bip322_txn(dev, pytestconfig):
|
||||
to_spend = CTransaction()
|
||||
to_spend.nVersion = 0
|
||||
out_point = COutPoint(hash=0, n=0xffffffff)
|
||||
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + bip322_msg_hash(msg))]
|
||||
msg_hash = bip322_msg_hash(msg)
|
||||
create_msg_file(msg, msg_hash)
|
||||
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
|
||||
to_spend.vout = [CTxOut(0, scr)] # always zero val
|
||||
msg_challenge = scr
|
||||
else:
|
||||
@ -144,7 +159,7 @@ def bip322_txn(dev, pytestconfig):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bip322_ms_txn(pytestconfig):
|
||||
def bip322_ms_txn(pytestconfig, create_msg_file):
|
||||
from test_multisig import make_ms_address
|
||||
|
||||
def doit(num_ins, M, keys, msg=b"POR", inp_af=AF_P2WSH, input_amount=1E8, path_mapper=None,
|
||||
@ -188,7 +203,9 @@ def bip322_ms_txn(pytestconfig):
|
||||
to_spend = CTransaction()
|
||||
to_spend.nVersion = 0
|
||||
out_point = COutPoint(hash=0, n=0xffffffff)
|
||||
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + bip322_msg_hash(msg))]
|
||||
msg_hash = bip322_msg_hash(msg)
|
||||
create_msg_file(msg, msg_hash)
|
||||
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
|
||||
to_spend.vout.append(CTxOut(0, scriptPubKey))
|
||||
msg_challenge = scriptPubKey
|
||||
else:
|
||||
|
||||
@ -2981,6 +2981,6 @@ from test_seed_xor import restore_seed_xor
|
||||
from test_sign import txid_from_export_prompt
|
||||
from test_ux import pass_word_quiz, word_menu_entry, enable_hw_ux
|
||||
from txn import fake_txn
|
||||
from bip322 import bip322_txn, bip322_ms_txn
|
||||
from bip322 import bip322_txn, bip322_ms_txn, create_msg_file
|
||||
|
||||
# EOF
|
||||
|
||||
@ -2,13 +2,83 @@
|
||||
#
|
||||
# BIP-322 message signing & Proof of Reserves
|
||||
#
|
||||
import pytest
|
||||
import pytest, time
|
||||
from io import BytesIO
|
||||
from decimal import Decimal
|
||||
from constants import SIGHASH_MAP, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH
|
||||
from bip322 import bip322_txn, bip322_ms_txn, bip322_msg_hash
|
||||
from ctransaction import CTransaction, CTxIn, COutPoint
|
||||
from helpers import str_to_path
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
from bbqr import split_qrs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def verify_msg_bip322_por(cap_story, need_keypress, press_select, press_cancel, cap_menu,
|
||||
nfc_write_text, is_q1, press_nfc, scan_a_qr, split_scan_bbqr,
|
||||
enter_complex, pick_menu_item):
|
||||
def doit(msg, refuse=False, way="sd", fname=None):
|
||||
title, story = cap_story()
|
||||
assert title == "BIP-322 MSG"
|
||||
# file was already created with bip322_txn fixture above
|
||||
if "qr" in way and not is_q1:
|
||||
raise pytest.xfail("Mk4 no QR")
|
||||
|
||||
if way == "input":
|
||||
enter_complex(msg, b39pass=False)
|
||||
elif way == "qr":
|
||||
assert f"{KEY_QR} to scan QR code" in story
|
||||
need_keypress(KEY_QR)
|
||||
scan_a_qr(msg)
|
||||
time.sleep(1)
|
||||
|
||||
elif way == "bbqr":
|
||||
assert f"{KEY_QR} to scan QR code" in story
|
||||
need_keypress(KEY_QR)
|
||||
|
||||
# def split_qrs(raw, type_code, encoding=None,
|
||||
# min_split=1, max_split=1295, min_version=5, max_version=40
|
||||
actual_vers, parts = split_qrs(msg, "U", max_version=20)
|
||||
|
||||
for p in parts:
|
||||
scan_a_qr(p)
|
||||
time.sleep(2.0 / len(parts)) # just so we can watch
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
elif way == "nfc":
|
||||
if f"press {KEY_NFC if is_q1 else '(3)'} to import via NFC" not in story:
|
||||
pytest.xfail("NFC disabled")
|
||||
else:
|
||||
press_nfc()
|
||||
time.sleep(0.2)
|
||||
nfc_write_text(msg)
|
||||
time.sleep(0.3)
|
||||
else:
|
||||
assert way in ["sd", "vdisk"]
|
||||
if way == "vdisk":
|
||||
if "(2) to import from Virtual Disk" not in story:
|
||||
pytest.xfail("Vdisk disabled")
|
||||
else:
|
||||
need_keypress("2")
|
||||
else:
|
||||
need_keypress("1")
|
||||
|
||||
if fname:
|
||||
pick_menu_item(fname)
|
||||
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert msg in story
|
||||
if refuse:
|
||||
press_cancel()
|
||||
time.sleep(.1)
|
||||
assert "Ready To Sign" in cap_menu()
|
||||
else:
|
||||
press_select()
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msg", [b"POR", b"This is the signed message"])
|
||||
@ -20,11 +90,16 @@ from helpers import str_to_path
|
||||
[["p2wpkh", None, None]] + ([["p2wpkh", None, 1000000]] * 20),
|
||||
[["p2pkh", None, None]] + ([["p2wpkh", None, 1000000]] * 5) + ([["p2sh-p2wpkh", None, 10000000]] * 5),
|
||||
])
|
||||
def test_bip322_por(msg, ins, bip322_txn, start_sign, end_sign, cap_story):
|
||||
def test_bip322_por(msg, ins, bip322_txn, start_sign, end_sign, cap_story, need_keypress,
|
||||
press_select, verify_msg_bip322_por):
|
||||
num_ins = len(ins)
|
||||
amt = sum([i[2] or 0 for i in ins])
|
||||
psbt, msg_challenge = bip322_txn(ins, msg=msg)
|
||||
start_sign(psbt, finalize=True)
|
||||
|
||||
verify_msg_bip322_por(msg.decode(), way="sd")
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert title == "OK TO SIGN?"
|
||||
assert "Proof of Reserves" in story
|
||||
@ -46,7 +121,8 @@ def test_bip322_por(msg, ins, bip322_txn, start_sign, end_sign, cap_story):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sighash", [sh for sh in SIGHASH_MAP if sh != 'ALL'])
|
||||
def test_bip322_por_invalid_sighash(sighash, bip322_txn, start_sign, cap_story, settings_set, end_sign):
|
||||
def test_bip322_por_invalid_sighash(sighash, bip322_txn, start_sign, cap_story, settings_set,
|
||||
end_sign, verify_msg_bip322_por):
|
||||
settings_set("sighshchk", 0) # disable checks
|
||||
# all POR txns must have only SIGHASH_ALL
|
||||
psbt, _ = bip322_txn([["p2sh-p2wpkh", None, None], ["p2wpkh", None, 100000], ["p2pkh", None, 1000000]],
|
||||
@ -57,6 +133,10 @@ def test_bip322_por_invalid_sighash(sighash, bip322_txn, start_sign, cap_story,
|
||||
assert title == "Failure"
|
||||
return
|
||||
|
||||
verify_msg_bip322_por("POR", way="sd")
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "warning" in story
|
||||
with pytest.raises(Exception):
|
||||
end_sign(accept=True, finalize=True)
|
||||
@ -84,11 +164,14 @@ def test_bip322_0th_input_witness_utxo(ins, bip322_txn, start_sign, cap_story):
|
||||
[["p2wpkh", None, None], ["p2sh-p2wpkh", None, 10000000], ["p2pkh", None, 10000000]],
|
||||
[["p2pkh", None, None], ["p2wpkh", None, 10000000], ["p2sh-p2wpkh", None, 10000000]],
|
||||
])
|
||||
def test_bip322_Xth_input_witness_utxo(ins, bip322_txn, start_sign, cap_story, end_sign):
|
||||
def test_bip322_Xth_input_witness_utxo(ins, bip322_txn, start_sign, cap_story, end_sign,
|
||||
verify_msg_bip322_por):
|
||||
# allowed - 0th input needs to have full pre-segwit utxo, all other can be just witness_utxo
|
||||
msg = b"hellow world"
|
||||
psbt, msg_challenge = bip322_txn(ins, witness_utxo=[1, 2], msg=msg)
|
||||
start_sign(psbt, finalize=True)
|
||||
verify_msg_bip322_por(msg.decode(), way="sd")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert title == "OK TO SIGN?"
|
||||
assert bip322_msg_hash(msg).hex() in story
|
||||
@ -104,7 +187,8 @@ def test_bip322_Xth_input_witness_utxo(ins, bip322_txn, start_sign, cap_story, e
|
||||
[["p2wpkh", None, None], ["p2pkh", None, 10000000], ["p2pkh", None, 10000000]],
|
||||
[["p2sh-p2wpkh", None, None], ["p2sh-p2wpkh", None, 10000000], ["p2sh-p2wpkh", None, 10000000]],
|
||||
])
|
||||
def test__bip322_incomplete_psbt_bip32_paths(ins, bip322_txn, start_sign, cap_story):
|
||||
def test_bip322_incomplete_psbt_bip32_paths(ins, bip322_txn, start_sign, cap_story,
|
||||
verify_msg_bip322_por):
|
||||
|
||||
def hack(psbt_in):
|
||||
for i, inp in enumerate(psbt_in.inputs):
|
||||
@ -118,6 +202,9 @@ def test__bip322_incomplete_psbt_bip32_paths(ins, bip322_txn, start_sign, cap_st
|
||||
assert title == "Failure"
|
||||
assert 'PSBT does not contain any key path information.' in story
|
||||
else:
|
||||
verify_msg_bip322_por("POR", way="sd")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "warning" in story
|
||||
assert "Limited Signing" in story
|
||||
assert "because we do not know the key: 0" in story
|
||||
@ -334,7 +421,7 @@ def test_bip322_invalid_to_spend_out_nVal(ins, bip322_txn, start_sign, cap_story
|
||||
@pytest.mark.parametrize("signed", [True, False])
|
||||
@pytest.mark.parametrize("num_ins", [1, 7])
|
||||
def test_ms_bip322_por(addr_fmt, M_N, signed, bip322_ms_txn, start_sign, end_sign, cap_story,
|
||||
import_ms_wallet, clear_ms, num_ins):
|
||||
import_ms_wallet, clear_ms, num_ins, verify_msg_bip322_por):
|
||||
clear_ms()
|
||||
|
||||
M, N = M_N
|
||||
@ -358,6 +445,8 @@ def test_ms_bip322_por(addr_fmt, M_N, signed, bip322_ms_txn, start_sign, end_sig
|
||||
psbt, msg_challenge = bip322_ms_txn(num_ins, M, keys, path_mapper=path_mapper, inp_af=addr_fmt,
|
||||
with_sigs=signed, input_amount=inp_amount)
|
||||
start_sign(psbt, finalize=signed)
|
||||
verify_msg_bip322_por("POR", way="sd")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert title == "OK TO SIGN?"
|
||||
assert "Proof of Reserves" in story
|
||||
@ -411,4 +500,27 @@ def test_bip322_invalid_ms_psbt(addr_fmt, bip322_ms_txn, start_sign, cap_story,
|
||||
assert title == "Failure"
|
||||
assert "Missing redeem/witness script" in story
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msg", [b"COLDCARD\n\nTHE\n\nBEST\n\nSIGNER", b"X" * 512])
|
||||
@pytest.mark.parametrize("ins", [
|
||||
[["p2sh-p2wpkh", None, None]],
|
||||
[["p2pkh", None, None]] + ([["p2wpkh", None, 1000000]] * 5) + ([["p2sh-p2wpkh", None, 10000000]] * 5),
|
||||
])
|
||||
@pytest.mark.parametrize("way", ["sd", "qr", "nfc", "vdisk", "bbqr"])
|
||||
def test_bip322_msg_import(msg, ins, way, bip322_txn, start_sign, end_sign, cap_story, need_keypress,
|
||||
press_select, verify_msg_bip322_por):
|
||||
|
||||
if b"\n" in msg and way == "qr":
|
||||
raise pytest.skip("QR code with newlines not supported")
|
||||
|
||||
psbt, msg_challenge = bip322_txn(ins, msg=msg)
|
||||
start_sign(psbt, finalize=True)
|
||||
|
||||
verify_msg_bip322_por(msg.decode(), way=way)
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert title == "OK TO SIGN?"
|
||||
assert "Proof of Reserves" in story
|
||||
|
||||
# EOF
|
||||
Loading…
Reference in New Issue
Block a user