771 lines
26 KiB
Python
771 lines
26 KiB
Python
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# tests related to Single Signer Spending Policy feature (SSSP)
|
|
#
|
|
# run simulator without --eff
|
|
#
|
|
#
|
|
import pytest, time, base64, random
|
|
from psbt import BasicPSBT
|
|
from ckcc.protocol import CCProtocolPacker
|
|
|
|
|
|
@pytest.fixture
|
|
def goto_sssp_menu(goto_home, pick_menu_item):
|
|
def doit():
|
|
goto_home()
|
|
pick_menu_item("Advanced/Tools")
|
|
pick_menu_item("Spending Policy")
|
|
pick_menu_item("Single-Signer")
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def setup_sssp(goto_sssp_menu, pick_menu_item, cap_story, press_select, pass_word_quiz, is_q1,
|
|
seed_story_to_words, cap_menu, OK, word_menu_entry, press_cancel, press_delete,
|
|
enter_number, scan_a_qr, cap_screen, settings_get, need_keypress, microsd_path,
|
|
master_settings_get, enter_pin, settings_remove, sim_exec):
|
|
|
|
def doit(pin=None, mag=None, vel=None, whitelist=None, w2fa=None, has_violation=None,
|
|
word_check=None, notes_and_pws=None, rel_keys=None):
|
|
|
|
goto_sssp_menu()
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
|
|
# it is possible that PIN was set beforehand
|
|
if title == "Spending Policy":
|
|
assert "stops you from signing transactions unless conditions are met" in story
|
|
assert "locked into a special mode" in story
|
|
assert "First step is to define a new PIN" in story
|
|
press_select()
|
|
time.sleep(.1)
|
|
scr = cap_screen()
|
|
if "Spending Policy" in scr:
|
|
what = "Enter first part of PIN" if is_q1 else "Enter PIN Prefix"
|
|
assert what in scr
|
|
|
|
enter_pin(pin)
|
|
time.sleep(.1)
|
|
scr = cap_screen()
|
|
what = "Confirm PIN value"if is_q1 else "CONFIRM PIN VALUE"
|
|
assert what in scr
|
|
enter_pin(pin)
|
|
time.sleep(.1)
|
|
|
|
m = cap_menu()
|
|
|
|
assert "Edit Policy..." in m
|
|
if has_violation is not None:
|
|
if has_violation:
|
|
assert "Last Violation" in m
|
|
else:
|
|
assert "last Violation" not in m
|
|
|
|
assert "Word Check" in m
|
|
assert ("Allow Notes" in m) or not is_q1
|
|
assert "Related Keys" in m
|
|
assert "Remove Policy" in m
|
|
assert "Test Drive" in m
|
|
assert "ACTIVATE" in m
|
|
|
|
pick_menu_item("Edit Policy...")
|
|
|
|
whitelist_mi = "Whitelist Addresses" if is_q1 else "Whitelist"
|
|
mag_mi = "Max Magnitude"
|
|
vel_mi = "Limit Velocity"
|
|
mi_2fa = "Web 2FA"
|
|
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
assert mag_mi in m
|
|
assert vel_mi in m
|
|
assert whitelist_mi in m
|
|
assert mi_2fa in m
|
|
|
|
# setting above values here
|
|
if mag:
|
|
pick_menu_item(mag_mi)
|
|
enter_number(mag)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert f"{mag} {'BTC' if int(mag) < 1000 else 'SATS'}" in story
|
|
press_select()
|
|
|
|
time.sleep(.1)
|
|
assert settings_get("sssp")["pol"]["mag"] == mag
|
|
|
|
if vel:
|
|
if not settings_get("sssp")["pol"].get("mag", None):
|
|
pick_menu_item(vel_mi)
|
|
title, story = cap_story()
|
|
assert 'Velocity limit requires' in story
|
|
assert 'starting value' in story
|
|
press_select()
|
|
else:
|
|
pick_menu_item(vel_mi)
|
|
|
|
if vel == "Unlimited":
|
|
target = 0
|
|
else:
|
|
target = int(vel.split()[0])
|
|
|
|
pick_menu_item(vel) # actually a full menu item
|
|
time.sleep(.3)
|
|
assert settings_get("sssp")["pol"]["vel"] == target
|
|
|
|
if whitelist:
|
|
pick_menu_item(whitelist_mi)
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
assert "(none yet)" in m
|
|
assert "Import from File" in m
|
|
if is_q1:
|
|
assert "Scan QR" in m
|
|
pick_menu_item("Scan QR")
|
|
for i, addr in enumerate(whitelist, start=1):
|
|
scan_a_qr(addr)
|
|
|
|
for _ in range(10):
|
|
scr = cap_screen()
|
|
if (f"Got {i} so far" in scr) and ("ENTER to apply" in scr):
|
|
break
|
|
time.sleep(.2)
|
|
else:
|
|
assert False, "updating whitelist failed"
|
|
|
|
press_select()
|
|
else:
|
|
assert "Scan QR" not in m
|
|
fname = "ccc_addrs.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
for a in whitelist:
|
|
f.write(f"{a}\n")
|
|
|
|
pick_menu_item("Import from File")
|
|
time.sleep(.1)
|
|
_, story = cap_story()
|
|
if "Press (1)" in story:
|
|
need_keypress("1")
|
|
pick_menu_item(fname)
|
|
|
|
time.sleep(.1)
|
|
_, story = cap_story()
|
|
if len(whitelist) == 1:
|
|
assert "Added new address to whitelist" in story
|
|
else:
|
|
assert f"Added {len(whitelist)} new addresses to whitelist" in story
|
|
|
|
for addr in whitelist:
|
|
assert addr in story
|
|
|
|
# check menu correct
|
|
press_select()
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
mi_addrs = [a for a in m if '⋯' in a]
|
|
for mia, addr in zip(mi_addrs, reversed(whitelist)):
|
|
_start, _end = mia.split('⋯')
|
|
assert addr.startswith(_start)
|
|
assert addr.endswith(_end)
|
|
|
|
press_cancel()
|
|
|
|
assert settings_get("sssp")["pol"]["addrs"] == whitelist
|
|
|
|
if w2fa:
|
|
pick_menu_item(mi_2fa)
|
|
|
|
press_cancel() # leave Edit Policy... (shared settings with CCC)
|
|
|
|
# now rest of sssp specific settings
|
|
if word_check is not None:
|
|
pick_menu_item("Word Check")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "addition to special PIN" in story
|
|
assert "provide the first and last seed words" in story
|
|
if word_check:
|
|
assert "Enable?" in story
|
|
press_select() # confirm action
|
|
time.sleep(.1)
|
|
assert settings_get("sssp")["words"]
|
|
else:
|
|
assert "Disable?" in story
|
|
pol = settings_get("sssp")
|
|
if "words" in pol:
|
|
assert not pol["words"]
|
|
|
|
if notes_and_pws is not None:
|
|
assert is_q1
|
|
pick_menu_item("Allow Notes")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "Allow (read-only) access to secure notes and passwords?" in story
|
|
if notes_and_pws:
|
|
assert "Enable?" in story
|
|
press_select() # confirm action
|
|
time.sleep(.1)
|
|
assert settings_get("sssp")["notes"]
|
|
else:
|
|
assert "Disable?" in story
|
|
pol = settings_get("sssp")
|
|
if "notes" in pol:
|
|
assert not pol["notes"]
|
|
|
|
if rel_keys is not None:
|
|
pick_menu_item("Related Keys")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "Allow access to BIP-39 passphrase wallets" in story
|
|
assert "and Seed Vault (read-only)" in story
|
|
if rel_keys:
|
|
assert "Enable?" in story
|
|
press_select() # confirm action
|
|
time.sleep(.1)
|
|
assert settings_get("sssp")["okeys"]
|
|
else:
|
|
assert "Disable?" in story
|
|
pol = settings_get("sssp")
|
|
if "okeys" in pol:
|
|
assert not pol["okeys"]
|
|
|
|
yield doit
|
|
|
|
# cleanup code -- all users of this fixture will get this code
|
|
|
|
settings_remove("sssp")
|
|
sim_exec('from pincodes import pa;pa.hobbled_mode = False; from actions import goto_top_menu; goto_top_menu()')
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
def policy_sign(start_sign, end_sign, cap_story, get_last_violation):
|
|
def doit(wallet, psbt, violation=None):
|
|
start_sign(base64.b64decode(psbt))
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
|
|
if violation:
|
|
# assume SSSP cases
|
|
assert title == "Failure"
|
|
assert 'warning' not in story
|
|
assert "Spending Policy violation." in story
|
|
assert violation in get_last_violation()
|
|
return
|
|
|
|
assert 'OK TO SEND?' == title
|
|
assert "warning" not in story
|
|
|
|
signed = end_sign(accept=True)
|
|
po = BasicPSBT().parse(signed)
|
|
|
|
tx_hex = None
|
|
if violation is None:
|
|
assert not get_last_violation()
|
|
assert len(po.inputs[0].part_sigs) or po.inputs[0].taproot_key_sig
|
|
res = wallet.finalizepsbt(base64.b64encode(signed).decode())
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
res = wallet.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
res = wallet.sendrawtransaction(tx_hex)
|
|
assert len(res) == 64 # tx id
|
|
|
|
return signed, tx_hex
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("mag_ok", [True, False])
|
|
@pytest.mark.parametrize("mag", [1000000, 2])
|
|
def test_magnitude(mag_ok, mag, setup_sssp, bitcoind, settings_set, pick_menu_item,
|
|
bitcoind_d_sim_watch, policy_sign, press_select,
|
|
reset_seed_words, settings_path):
|
|
|
|
wo = bitcoind_d_sim_watch
|
|
|
|
settings_set("chain", "XRT")
|
|
|
|
if mag_ok:
|
|
# always try limit/border value
|
|
if mag is None:
|
|
to_send = 1
|
|
else:
|
|
to_send = mag / 100000000 if mag > 1000 else mag
|
|
else:
|
|
if mag is None:
|
|
to_send = 1.1
|
|
else:
|
|
to_send = ((mag / 100000000)+1) if mag > 1000 else (mag+0.001)
|
|
|
|
setup_sssp("11-11", mag=mag)
|
|
|
|
pick_menu_item("ACTIVATE")
|
|
press_select()
|
|
|
|
addr = wo.getnewaddress()
|
|
bitcoind.supply_wallet.sendtoaddress(address=addr, amount=5.0)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
# create funded PSBT
|
|
psbt_resp = wo.walletcreatefundedpsbt(
|
|
[], [{bitcoind.supply_wallet.getnewaddress(): to_send}], 0, {"fee_rate": 2}
|
|
)
|
|
psbt = psbt_resp.get("psbt")
|
|
|
|
policy_sign(wo, psbt, violation=None if mag_ok else "magnitude")
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("whitelist_ok", [True, False])
|
|
def test_whitelist(whitelist_ok, setup_sssp, bitcoind, settings_set, policy_sign,
|
|
bitcoind_d_sim_watch, pick_menu_item, press_select):
|
|
|
|
wo = bitcoind_d_sim_watch
|
|
|
|
settings_set("chain", "XRT")
|
|
|
|
whitelist = [
|
|
"bcrt1qqca9eefwz8tzn7rk6aumhwhapyf5vsrtrddxxp",
|
|
"bcrt1q7nck280nje50gzjja3gyguhp2ds6astu5ndhkj",
|
|
"bcrt1qhexpvdhwuerqq0h24j06g8y5eumjjdr28ng4vv",
|
|
"bcrt1q3ylr55pk7rl0rc06d8th7h25zmcuvvg8wt0yl3",
|
|
]
|
|
|
|
if whitelist_ok:
|
|
send_to = whitelist[0]
|
|
else:
|
|
send_to = bitcoind.supply_wallet.getnewaddress()
|
|
|
|
setup_sssp("11-11", whitelist=whitelist)
|
|
pick_menu_item("ACTIVATE")
|
|
press_select()
|
|
|
|
multi_addr = wo.getnewaddress()
|
|
bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=5.0)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
# create funded PSBT
|
|
psbt_resp = wo.walletcreatefundedpsbt(
|
|
[], [{send_to: 1}], 0, {"fee_rate": 2}
|
|
)
|
|
psbt = psbt_resp.get("psbt")
|
|
policy_sign(wo, psbt, violation=None if whitelist_ok else "whitelist")
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("velocity_mi", ['6 blocks (hour)', '48 blocks (8h)'])
|
|
def test_velocity(velocity_mi, setup_sssp, bitcoind, settings_set, pick_menu_item,
|
|
policy_sign, settings_get, bitcoind_d_sim_watch, press_select):
|
|
|
|
wo = bitcoind_d_sim_watch
|
|
wo.keypoolrefill(20)
|
|
settings_set("chain", "XRT")
|
|
|
|
blocks = int(velocity_mi.split()[0])
|
|
|
|
setup_sssp("11-11", vel=velocity_mi)
|
|
pick_menu_item("ACTIVATE")
|
|
press_select()
|
|
|
|
assert "block_h" not in settings_get("sssp")["pol"]
|
|
|
|
multi_addr = wo.getnewaddress()
|
|
bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
# create funded PSBT, first tx
|
|
init_block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] # block height
|
|
psbt_resp = wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}],
|
|
init_block_height) # nLockTime set to current block height
|
|
psbt = psbt_resp.get("psbt")
|
|
po = BasicPSBT().parse(base64.b64decode(psbt))
|
|
assert po.parsed_txn.nLockTime == init_block_height
|
|
policy_sign(wo, psbt) # success as this is first tx that sets block height from 0
|
|
|
|
assert settings_get("sssp")["pol"]["block_h"] == init_block_height
|
|
|
|
# mine some, BUT not enough to satisfy velocity policy
|
|
# - check velocity is exactly right to block number vs. required gap
|
|
bitcoind.supply_wallet.generatetoaddress(blocks - 1, bitcoind.supply_wallet.getnewaddress())
|
|
block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"]
|
|
psbt_resp = wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}],
|
|
block_height)
|
|
psbt = psbt_resp.get("psbt")
|
|
po = BasicPSBT().parse(base64.b64decode(psbt))
|
|
assert po.parsed_txn.nLockTime == block_height
|
|
policy_sign(wo, psbt, violation="velocity")
|
|
|
|
assert settings_get("sssp")["pol"]["block_h"] == init_block_height # still initial block height as above failed
|
|
|
|
# mine the remaining one block to satisfy velocity policy
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"]
|
|
psbt_resp = wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}],
|
|
block_height)
|
|
psbt = psbt_resp.get("psbt")
|
|
po = BasicPSBT().parse(base64.b64decode(psbt))
|
|
assert po.parsed_txn.nLockTime == block_height
|
|
policy_sign(wo, psbt) # success
|
|
|
|
assert settings_get("sssp")["pol"]["block_h"] == block_height # updated block height
|
|
|
|
# check txn re-sign fails (if velocity in effect)
|
|
policy_sign(wo, psbt, violation="rewound")
|
|
# check decreasing nLockTime
|
|
policy_sign(
|
|
wo,
|
|
wo.walletcreatefundedpsbt(
|
|
[], [{bitcoind.supply_wallet.getnewaddress(): 1}], block_height - 1
|
|
)["psbt"],
|
|
violation="rewound"
|
|
)
|
|
# check nLockTime disabled when velocity enabled - fail
|
|
policy_sign(
|
|
wo,
|
|
wo.walletcreatefundedpsbt(
|
|
[], [{bitcoind.supply_wallet.getnewaddress(): 1}], 0
|
|
)["psbt"],
|
|
violation="no nLockTime"
|
|
)
|
|
# unix timestamp
|
|
policy_sign(
|
|
wo,
|
|
wo.walletcreatefundedpsbt(
|
|
[], [{bitcoind.supply_wallet.getnewaddress(): 1}], 500000000
|
|
)["psbt"],
|
|
violation="nLockTime not height"
|
|
)
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("active", [True, False])
|
|
def test_warnings(setup_sssp, bitcoind, settings_set, policy_sign, pick_menu_item,
|
|
bitcoind_d_sim_watch, settings_get, press_select, active):
|
|
|
|
wo = bitcoind_d_sim_watch
|
|
wo.keypoolrefill(20)
|
|
|
|
settings_set("chain", "XRT")
|
|
|
|
whitelist = ["bcrt1qlk39jrclgnawa42tvhu2n7se987qm96qg8v76e",
|
|
"2Mxp1Dy2MyR4w36J2VaZhrFugNNFgh6LC1j",
|
|
"mjR14oKxYzRg9RAZdpu3hrw8zXfFgGzLKm"]
|
|
|
|
setup_sssp("11-11", mag=10000000, vel='6 blocks (hour)', whitelist=whitelist)
|
|
if active:
|
|
pick_menu_item("ACTIVATE")
|
|
press_select()
|
|
else:
|
|
# demonstration that policy is in effect from configuration
|
|
# user does not need to activate (or test-drive) and policy in effect already
|
|
pass
|
|
|
|
bitcoind.supply_wallet.sendtoaddress(address=wo.getnewaddress(), amount=2)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
# create funded PSBT, first tx
|
|
# whitelist OK, velocity OK, & magnitude OK - but fee high
|
|
init_block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] # block height
|
|
psbt_resp = wo.walletcreatefundedpsbt([], [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}],
|
|
init_block_height, {"fee_rate":48000})
|
|
psbt = psbt_resp.get("psbt")
|
|
po = BasicPSBT().parse(base64.b64decode(psbt))
|
|
assert po.parsed_txn.nLockTime == init_block_height
|
|
policy_sign(wo, psbt, violation="has warnings")
|
|
|
|
# invalidate nLockTime with use of nSequence max values
|
|
utxos = wo.listunspent()
|
|
ins = []
|
|
for i, utxo in enumerate(utxos):
|
|
# block height based RTL
|
|
inp = {
|
|
"txid": utxo["txid"],
|
|
"vout": utxo["vout"],
|
|
"sequence": 0xffffffff,
|
|
}
|
|
ins.append(inp)
|
|
|
|
psbt_resp = wo.walletcreatefundedpsbt(ins, [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}],
|
|
0, {"fee_rate":2, "replaceable": False}) # locktime needs to be zero, otherwise exception from core (contradicting parameters)
|
|
po = BasicPSBT().parse(base64.b64decode(psbt_resp.get("psbt")))
|
|
assert po.parsed_txn.nLockTime == 0
|
|
po.parsed_txn.nLockTime = init_block_height # add locktime
|
|
po.txn = po.parsed_txn.serialize_with_witness()
|
|
# num_warn=2, warn_list=["Bad Locktime"]
|
|
policy_sign(wo, po.as_b64_str(), violation="has warnings")
|
|
|
|
# exotic sighash warning
|
|
settings_set("sighshchk", 1) # needed to only get warning instead of failure
|
|
psbt_resp = wo.walletcreatefundedpsbt([], [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}],
|
|
init_block_height, {"fee_rate":2, "replaceable": True})
|
|
po = BasicPSBT().parse(base64.b64decode(psbt_resp.get("psbt")))
|
|
for idx, i in enumerate(po.inputs):
|
|
i.sighash = 2 # NONE
|
|
|
|
# num_warn=2, warn_list=["sighash NONE"]
|
|
policy_sign(wo, po.as_b64_str(), violation="has warnings")
|
|
|
|
|
|
def test_remove_sssp(setup_sssp, pick_menu_item, press_select, cap_story, cap_menu, settings_get):
|
|
setup_sssp("11-11", mag=10000000, vel='6 blocks (hour)')
|
|
|
|
# check test drive
|
|
pick_menu_item("Test Drive")
|
|
time.sleep(.1)
|
|
_, story = cap_story()
|
|
assert "COLDCARD operation will look like with Spending Policy" in story
|
|
press_select()
|
|
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
assert "EXIT TEST DRIVE" in m
|
|
assert "Settings" not in m
|
|
|
|
pick_menu_item("EXIT TEST DRIVE")
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
assert "Edit Policy..." in m # back in policy settings
|
|
|
|
pick_menu_item("Remove Policy")
|
|
time.sleep(.1)
|
|
_, story = cap_story()
|
|
assert "Bypass PIN will be removed" in story
|
|
assert "spending policy settings forgotten" in story
|
|
press_select()
|
|
|
|
time.sleep(.1)
|
|
assert not settings_get("sssp")
|
|
|
|
tps = settings_get("tp")
|
|
if tps:
|
|
assert "11-11" not in tps
|
|
|
|
assert not settings_get("sssp")
|
|
|
|
|
|
def test_use_main_pin_as_unlock(setup_sssp, cap_story):
|
|
# not allowed
|
|
# simulator PIN
|
|
with pytest.raises(Exception):
|
|
setup_sssp("12-12")
|
|
|
|
_, story = cap_story()
|
|
assert "already in use" in story
|
|
assert "PIN codes must be unique" in story
|
|
|
|
|
|
@pytest.mark.parametrize("hide", [True, False])
|
|
def test_use_trick_pin_as_unlock(hide, setup_sssp, cap_story, new_trick_pin, pick_menu_item,
|
|
press_select, clear_all_tricks):
|
|
clear_all_tricks()
|
|
pin = "11-11"
|
|
new_trick_pin(pin, 'Wipe Seed', 'Wipe the seed and maybe do more')
|
|
pick_menu_item('Wipe & Reboot')
|
|
press_select()
|
|
press_select()
|
|
if hide:
|
|
pick_menu_item(f"↳{pin}")
|
|
pick_menu_item("Hide Trick")
|
|
press_select() # confirm
|
|
|
|
with pytest.raises(Exception):
|
|
setup_sssp(pin)
|
|
|
|
_, story = cap_story()
|
|
assert "already in use" in story
|
|
assert "PIN codes must be unique" in story
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("active_policy", [False, True])
|
|
def test_deltamode_signature(active_policy, setup_sssp, bitcoind, settings_set,
|
|
start_sign, end_sign, pick_menu_item, press_select,
|
|
set_deltamode, bitcoind_d_sim_watch, settings_get):
|
|
|
|
# verify that "deltamode" trick pins will work in SSSP mode
|
|
# - and that resulting signature is bad
|
|
# - device should **not** wipe itself
|
|
|
|
dest = "bcrt1qlk39jrclgnawa42tvhu2n7se987qm96qg8v76e"
|
|
wo = bitcoind_d_sim_watch
|
|
wo.keypoolrefill(20)
|
|
|
|
settings_set("chain", "XRT")
|
|
|
|
if active_policy:
|
|
setup_sssp(f"{random.randint(0,99)}-11", mag=100)
|
|
pick_menu_item("ACTIVATE")
|
|
press_select()
|
|
|
|
bitcoind.supply_wallet.sendtoaddress(address=wo.getnewaddress(), amount=2)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
|
|
# create funded PSBT, first tx
|
|
# - within active policy.
|
|
init_block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] # block height
|
|
psbt_resp = wo.walletcreatefundedpsbt([], [{dest: 0.06}],
|
|
init_block_height, {"fee_rate":2, "replaceable": False})
|
|
psbt = psbt_resp.get("psbt")
|
|
|
|
po = BasicPSBT().parse(base64.b64decode(psbt))
|
|
assert po.parsed_txn.nLockTime == init_block_height
|
|
|
|
start_sign(base64.b64decode(psbt), finalize=True)
|
|
signed = end_sign(accept=True, finalize=True)
|
|
|
|
set_deltamode(True)
|
|
|
|
start_sign(base64.b64decode(psbt), finalize=True)
|
|
signed2 = end_sign(accept=True, finalize=True)
|
|
|
|
# check wrong signature happened
|
|
assert signed != signed2
|
|
probs = wo.testmempoolaccept([signed2.hex()])[0]
|
|
try:
|
|
# old bitcoind
|
|
assert 'Signature must be zero' in probs['reject-reason'], probs
|
|
except AssertionError:
|
|
assert 'mandatory-script-verify-flag-failed' in probs['reject-reason'], probs
|
|
assert not probs['allowed']
|
|
|
|
# check right signature
|
|
no_probs = wo.testmempoolaccept([signed.hex()])[0]
|
|
assert no_probs['allowed']
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
def test_sssp_enforce_tmp_seed(setup_sssp, bitcoind, settings_set, settings_get, press_select,
|
|
pick_menu_item, cap_menu, go_to_passphrase, enter_complex,
|
|
need_keypress, word_menu_entry, fake_txn, start_sign, dev,
|
|
cap_story, get_last_violation, end_sign):
|
|
tmp_words = "style car win bomb plug raccoon predict warm wrap flush usual seminar"
|
|
blocks = 6 # ~1 hour
|
|
settings_set("chain", "XRT")
|
|
setup_sssp("11-11", mag=2, vel='6 blocks (hour)', rel_keys=True)
|
|
assert "block_h" not in settings_get("sssp")["pol"]
|
|
pick_menu_item("ACTIVATE")
|
|
press_select()
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
# check we are in hobbled mode & okeys is respected
|
|
assert "Passphrase" in m
|
|
assert "Settings" not in m
|
|
|
|
# import word-based seed as tmp and check that sssp is enforced
|
|
pick_menu_item("Advanced/Tools")
|
|
pick_menu_item("Temporary Seed")
|
|
need_keypress("4")
|
|
pick_menu_item("Import Words")
|
|
pick_menu_item("12 Words")
|
|
word_menu_entry(tmp_words.split())
|
|
press_select()
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
assert "Passphrase" in m # word based + okeys
|
|
assert "Settings" not in m
|
|
|
|
xpub = dev.send_recv(CCProtocolPacker.get_xpub("m"), timeout=None)
|
|
psbt = fake_txn(2,2, input_amount=200000000, master_xpub=xpub)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
_, story = cap_story()
|
|
assert "Spending Policy violation" in story
|
|
press_select()
|
|
|
|
# recurse deeper, to passphrase wallet, on top of word-based tmp seed
|
|
go_to_passphrase()
|
|
enter_complex("AAA", apply=True)
|
|
|
|
press_select()
|
|
m = cap_menu()
|
|
assert "Passphrase" not in m # xprv based
|
|
assert "Settings" not in m # still in hobbled
|
|
|
|
xpub1 = dev.send_recv(CCProtocolPacker.get_xpub("m"), timeout=None)
|
|
psbt = fake_txn(2, 2, input_amount=200000000, master_xpub=xpub1)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
_, story = cap_story()
|
|
assert "Spending Policy violation" in story
|
|
press_select()
|
|
time.sleep(.1)
|
|
|
|
# try success signing
|
|
psbt = fake_txn(2, 2, input_amount=1000000, master_xpub=xpub1, lock_time=50)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == 'OK TO SEND?'
|
|
assert "Spending Policy violation" not in story
|
|
assert end_sign()
|
|
|
|
# go back to previous temporary seed and verify block_h was updated
|
|
pick_menu_item("Advanced/Tools")
|
|
pick_menu_item("Temporary Seed")
|
|
need_keypress("4")
|
|
pick_menu_item("Import Words")
|
|
pick_menu_item("12 Words")
|
|
word_menu_entry(tmp_words.split())
|
|
press_select()
|
|
|
|
# lock time is still 50 - as in previous case = rewound
|
|
psbt = fake_txn(2, 2, input_amount=1000000, master_xpub=xpub, lock_time=50)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "Spending Policy violation" in story
|
|
assert get_last_violation() == "rewound (50)"
|
|
press_select()
|
|
time.sleep(.1)
|
|
|
|
# bump locktime
|
|
psbt = fake_txn(2, 2, input_amount=1000000, master_xpub=xpub, lock_time=56)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == 'OK TO SEND?'
|
|
assert end_sign()
|
|
time.sleep(.1)
|
|
|
|
pick_menu_item("Restore Master")
|
|
press_select()
|
|
|
|
time.sleep(.1)
|
|
m = cap_menu()
|
|
assert "Passphrase" in m
|
|
assert "Settings" not in m # still in hobbled
|
|
psbt = fake_txn(2, 2, input_amount=200000000, lock_time=150)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
_, story = cap_story()
|
|
assert "Spending Policy violation" in story
|
|
press_select()
|
|
|
|
# lock time is still 56 - as in previous case = rewound
|
|
psbt = fake_txn(2, 2, input_amount=1000000, lock_time=56)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "Spending Policy violation" in story
|
|
assert get_last_violation() == "rewound (56)"
|
|
press_select()
|
|
time.sleep(.1)
|
|
|
|
# bump locktime
|
|
psbt = fake_txn(2, 2, input_amount=1000000, lock_time=70)
|
|
start_sign(psbt)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == 'OK TO SEND?'
|
|
assert end_sign()
|
|
time.sleep(.1)
|
|
|
|
def test_sssp_notes_enable(only_q1, setup_sssp):
|
|
# just test menu item works
|
|
setup_sssp("11-11", mag=2, vel='6 blocks (hour)', notes_and_pws=True)
|
|
|
|
def test_sssp_word_check(setup_sssp):
|
|
# just test menu item works
|
|
setup_sssp("11-11", mag=2, vel='6 blocks (hour)', word_check=True)
|
|
# EOF
|