add test for getting C key from Seed Vault

This commit is contained in:
scgbckbone 2025-03-25 14:30:38 +01:00 committed by doc-hex
parent 446bea9926
commit e726637319
5 changed files with 132 additions and 84 deletions

View File

@ -1495,8 +1495,8 @@ async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, abort=False):
psbt_len,
approved_cb=done_signing,
cb_kws={"filename": filename,
"force_vdisk": force_vdisk,
"output_encoder": output_encoder}
"force_vdisk": force_vdisk,
"output_encoder": output_encoder}
)
if abort:
# needed for auto vdisk mode

View File

@ -311,11 +311,7 @@ class CCCConfigMenu(MenuSystem):
# trying to exit from CCCConfigMenu
from seed import in_seed_vault
try:
enc = CCCFeature.get_encoded_secret()
except:
# some test cases?
enc = None
enc = CCCFeature.get_encoded_secret()
if in_seed_vault(enc):
# remind them to clear the seed-vault copy of Key C because it defeats feature
@ -706,7 +702,7 @@ async def gen_or_import():
"12-words or (2) for 24-words import." % OK
if settings.master_get("seedvault", False):
msg += ' (6) for import from Seed Vault'
msg += ' Press (6) to import from Seed Vault.'
ch = await ux_show_story(msg, escape='126', title="CCC Key C")

View File

@ -997,7 +997,7 @@ def settings_get(sim_exec):
def master_settings_get(sim_exec):
def doit(key):
cmd = f"RV.write(repr(settings.master_get('{key}')))"
cmd = f"RV.write(repr(settings.master_get('{key}', False)))"
resp = sim_exec(cmd)
assert 'Traceback' not in resp, resp
return eval(resp)
@ -2469,6 +2469,29 @@ def garbage_collector():
except: pass
@pytest.fixture
def build_test_seed_vault():
def doit():
from test_ephemeral import SEEDVAULT_TEST_DATA
sv = []
for item in SEEDVAULT_TEST_DATA:
xfp, entropy, mnemonic = item
# build stashed encoded secret
entropy_bytes = bytes.fromhex(entropy)
if mnemonic:
vlen = len(entropy_bytes)
assert vlen in [16, 24, 32]
marker = 0x80 | ((vlen // 8) - 2)
stored_secret = bytes([marker]) + entropy_bytes
else:
stored_secret = entropy_bytes
sv.append((xfp, stored_secret.hex(), f"[{xfp}]", "meta"))
return sv
return doit
# useful fixtures
from test_backup import backup_system
from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll, try_sign_bbqr

View File

@ -547,28 +547,10 @@ def test_seed_vault_backup(settings_set, reset_seed_words, generate_ephemeral_wo
assert xfp_ui in sv_xfp_menu
def test_seed_vault_backup_frozen(reset_seed_words, settings_set, repl):
from test_ephemeral import SEEDVAULT_TEST_DATA
def test_seed_vault_backup_frozen(reset_seed_words, settings_set, repl, build_test_seed_vault):
reset_seed_words()
settings_set("seedvault", 1)
sv = []
for item in SEEDVAULT_TEST_DATA:
xfp, entropy, mnemonic = item
# build stashed encoded secret
entropy_bytes = bytes.fromhex(entropy)
if mnemonic:
vlen = len(entropy_bytes)
assert vlen in [16, 24, 32]
marker = 0x80 | ((vlen // 8) - 2)
stored_secret = bytes([marker]) + entropy_bytes
else:
stored_secret = entropy_bytes
sv.append((xfp, stored_secret.hex(), f"[{xfp}]", "meta"))
sv = build_test_seed_vault()
settings_set("seeds", sv)
bk = repl.exec('import backups; RV.write(backups.render_backup_contents())', raw=1)
assert 'Coldcard backup file' in bk

View File

@ -178,7 +178,8 @@ _skip_quiz = False
@pytest.fixture
def setup_ccc(goto_home, 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):
enter_number, scan_a_qr, cap_screen, settings_get, need_keypress, microsd_path,
master_settings_get):
def doit(c_words=None, mag=None, vel=None, whitelist=None, w2fa=None, first_time=True):
if first_time:
@ -196,6 +197,8 @@ def setup_ccc(goto_home, pick_menu_item, cap_story, press_select, pass_word_quiz
assert f"Press {OK} to generate new 12-word seed phrase"
assert "(1)" in story
assert "(2)" in story
if master_settings_get("seedvault"):
assert "(6) to import from Seed Vault" in story
if c_words is None:
nwords = 12 # always 12 words if generated by us
@ -357,25 +360,24 @@ def setup_ccc(goto_home, pick_menu_item, cap_story, press_select, pass_word_quiz
@pytest.fixture
def enter_enabled_ccc(goto_home, pick_menu_item, cap_story, press_select, is_q1,
word_menu_entry, cap_menu):
def doit(c_words, first_time=False, seed_vault=False):
if not first_time:
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Coldcard Co-Signing")
def doit(c_words, seed_vault=False):
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Coldcard Co-Signing")
time.sleep(.1)
title, story = cap_story()
if seed_vault:
assert "You have a copy of the CCC key C in the Seed Vault" in story
assert "You must delete that key from the vault once setup and debug is finished" in story
assert "or all benefit of this feature is lost!" in story
press_select()
else:
assert title == "CCC Enabled"
assert "policy cannot be viewed, changed" in story
assert "unless you have the seed words for key C" in story
press_select()
time.sleep(.1)
title, story = cap_story()
if seed_vault:
assert "You have a copy of the CCC key C in the Seed Vault" in story
assert "You must delete that key from the vault once setup and debug is finished" in story
assert "or all benefit of this feature is lost!" in story
press_select()
else:
assert title == "CCC Enabled"
assert "policy cannot be viewed, changed" in story
assert "unless you have the seed words for key C" in story
press_select()
time.sleep(.1)
word_menu_entry(c_words)
word_menu_entry(c_words)
return doit
@ -565,7 +567,7 @@ def policy_sign(start_sign, end_sign, cap_story, get_last_violation):
@pytest.mark.bitcoind
@pytest.mark.parametrize("mag_ok", [True, False])
@pytest.mark.parametrize("mag", [1000000, None, 2])
def test_ccc_magnitude(mag_ok, mag, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
def test_ccc_magnitude(mag_ok, mag, setup_ccc, ccc_ms_setup,
bitcoind, settings_set, policy_sign,
bitcoind_create_watch_only_wallet):
@ -585,8 +587,7 @@ def test_ccc_magnitude(mag_ok, mag, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
else:
to_send = ((mag / 100000000)+1) if mag > 1000 else (mag+0.001)
words = setup_ccc(mag=mag, vel="Unlimited")
enter_enabled_ccc(words, first_time=True)
setup_ccc(mag=mag, vel="Unlimited")
_, target_mi = ccc_ms_setup()
bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
@ -604,7 +605,7 @@ def test_ccc_magnitude(mag_ok, mag, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
@pytest.mark.bitcoind
@pytest.mark.parametrize("whitelist_ok", [True, False])
def test_ccc_whitelist(whitelist_ok, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
def test_ccc_whitelist(whitelist_ok, setup_ccc, ccc_ms_setup,
bitcoind, settings_set, policy_sign,
bitcoind_create_watch_only_wallet):
@ -624,8 +625,7 @@ def test_ccc_whitelist(whitelist_ok, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
else:
send_to = bitcoind.supply_wallet.getnewaddress()
words = setup_ccc(whitelist=whitelist, vel="Unlimited")
enter_enabled_ccc(words, first_time=True)
setup_ccc(whitelist=whitelist, vel="Unlimited")
_, target_mi = ccc_ms_setup()
bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
@ -642,9 +642,8 @@ def test_ccc_whitelist(whitelist_ok, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
@pytest.mark.bitcoind
@pytest.mark.parametrize("velocity_mi", ['6 blocks (hour)', '48 blocks (8h)'])
def test_ccc_velocity(velocity_mi, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
bitcoind, settings_set, policy_sign, settings_get,
bitcoind_create_watch_only_wallet):
def test_ccc_velocity(velocity_mi, setup_ccc, ccc_ms_setup, bitcoind, settings_set,
policy_sign, settings_get, bitcoind_create_watch_only_wallet):
settings_set("ccc", None)
settings_set("chain", "XRT")
@ -652,8 +651,7 @@ def test_ccc_velocity(velocity_mi, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
blocks = int(velocity_mi.split()[0])
words = setup_ccc(vel=velocity_mi)
enter_enabled_ccc(words, first_time=True)
setup_ccc(vel=velocity_mi)
_, target_mi = ccc_ms_setup()
assert settings_get("ccc")["pol"]["block_h"] == 0
@ -728,8 +726,7 @@ def test_ccc_velocity(velocity_mi, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
@pytest.mark.bitcoind
def test_ccc_warnings(setup_ccc, enter_enabled_ccc, ccc_ms_setup,
bitcoind, settings_set, policy_sign,
def test_ccc_warnings(setup_ccc, ccc_ms_setup, bitcoind, settings_set, policy_sign,
bitcoind_create_watch_only_wallet, settings_get):
settings_set("ccc", None)
@ -740,8 +737,7 @@ def test_ccc_warnings(setup_ccc, enter_enabled_ccc, ccc_ms_setup,
"2Mxp1Dy2MyR4w36J2VaZhrFugNNFgh6LC1j",
"mjR14oKxYzRg9RAZdpu3hrw8zXfFgGzLKm"]
words = setup_ccc(mag=10000000, vel='6 blocks (hour)', whitelist=whitelist,)
enter_enabled_ccc(words, first_time=True)
setup_ccc(mag=10000000, vel='6 blocks (hour)', whitelist=whitelist,)
_, target_mi = ccc_ms_setup()
bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
@ -800,15 +796,14 @@ def test_maxed_out(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, sim
# C mnemonic is 24 words
c_words = "cluster comic depend absent grain circle demand tag pass clock certain strategy lunar bless pulse useful comfort fatigue glove decorate taste allow adult journey".split()
words = setup_ccc(c_words=c_words, mag=100000000, vel='4032 blocks (4w)', whitelist=None)
enter_enabled_ccc(words, first_time=True)
setup_ccc(c_words=c_words, mag=100000000, vel='4032 blocks (4w)', whitelist=None)
# B mnemonic is 24 words
b_words = "ceiling apology excite illegal accident define boat prosper decrease utility romance try trial dizzy win lawsuit much sustain similar meadow draw oil cousin wagon".split()
_, target_mi = ccc_ms_setup(b_words=b_words)
bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
# create whitelist with own addresses - only conso to first 25 addrs allowed
enter_enabled_ccc(c_words, first_time=False)
enter_enabled_ccc(c_words)
# pick random internal/external descriptor
ms_descriptors = bitcoind_wo.listdescriptors()
@ -863,8 +858,7 @@ def test_load_and_sign_key_C(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_
settings_set("seedvault", int(seed_vault))
settings_set("seeds", [])
words = setup_ccc(c_words=None)
enter_enabled_ccc(words, first_time=True)
setup_ccc(c_words=None)
_, target_mi = ccc_ms_setup()
bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
@ -925,9 +919,10 @@ def test_load_and_sign_key_C(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_
@pytest.mark.parametrize("c_num_words", [None, 12, 24])
@pytest.mark.parametrize("acct", [None, 9999])
def test_ccc_xpub_export(chain, c_num_words, acct, settings_set, load_export, setup_ccc,
enter_enabled_ccc, pick_menu_item, enter_number, press_select,
settings_get, cap_menu):
pick_menu_item, enter_number, press_select, settings_get, cap_menu,
goto_home):
# - "export cc xpubs" path
goto_home()
settings_set("ccc", None)
settings_set("chain", chain)
settings_set("multisig", [])
@ -941,7 +936,6 @@ def test_ccc_xpub_export(chain, c_num_words, acct, settings_set, load_export, se
words = words.split()
setup_ccc(c_words=words)
enter_enabled_ccc(words, first_time=True)
pick_menu_item("Export CCC XPUBs")
if acct is None:
press_select() # default zero
@ -981,14 +975,14 @@ def test_ccc_xpub_export(chain, c_num_words, acct, settings_set, load_export, se
def test_multiple_multisig_wallets(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
bitcoind_create_watch_only_wallet, cap_story, bitcoind,
policy_sign, settings_get, cap_menu, pick_menu_item,
press_select, load_export, offer_ms_import):
press_select, load_export, offer_ms_import, goto_home):
# - 'build 2-of-N' path
goto_home()
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
words = setup_ccc(c_words=None, mag=2, vel='6 blocks (hour)')
enter_enabled_ccc(words, first_time=True)
b_keys_0, mi = ccc_ms_setup(N=5)
assert len(b_keys_0) == 3 # 5 - 2 (C, A) = 3
w0 = bitcoind_create_watch_only_wallet(mi)
@ -1029,7 +1023,7 @@ def test_multiple_multisig_wallets(settings_set, setup_ccc, enter_enabled_ccc, c
init_block_height+1)["psbt"]
policy_sign(w, psbt, violation="velocity")
enter_enabled_ccc(words, first_time=False)
enter_enabled_ccc(words)
_, ami = ccc_ms_setup(N=8)
_, mi = ccc_ms_setup(N=4)
time.sleep(.1)
@ -1062,20 +1056,19 @@ def test_multiple_multisig_wallets(settings_set, setup_ccc, enter_enabled_ccc, c
press_select()
time.sleep(.1)
enter_enabled_ccc(words, first_time=False)
enter_enabled_ccc(words)
m = cap_menu()
assert f"{w_mn} {new_name}" in m
def test_remove_ccc(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, settings_get,
pick_menu_item, cap_story, press_select, need_keypress, policy_sign,
bitcoind_create_watch_only_wallet, bitcoind):
def test_remove_ccc(settings_set, setup_ccc, ccc_ms_setup, settings_get, policy_sign,
pick_menu_item, cap_story, press_select, need_keypress,
bitcoind_create_watch_only_wallet, bitcoind, goto_home):
goto_home()
settings_set("ccc", None)
settings_set("multisig", [])
words = setup_ccc(c_words=None, mag=2, vel='6 blocks (hour)')
enter_enabled_ccc(words, first_time=True)
setup_ccc(c_words=None, mag=2, vel='6 blocks (hour)')
_, mi = ccc_ms_setup(N=3)
w0 = bitcoind_create_watch_only_wallet(mi)
@ -1109,8 +1102,62 @@ def test_remove_ccc(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, se
policy_sign(w0, psbt, ccc_disabled=True)
@pytest.mark.parametrize("has_candidates", [True, False])
def test_c_key_from_seed_vault(has_candidates, setup_ccc, build_test_seed_vault, settings_set,
goto_home, pick_menu_item, press_select, need_keypress, cap_menu,
cap_story, press_cancel, enter_enabled_ccc):
goto_home()
settings_set("ccc", None)
settings_set("multisig", [])
# TODO
# - policy-fail reason submenu; check display
settings_set("seedvault", True)
sv = build_test_seed_vault()
if not has_candidates:
# last item is XPR - not acceptable
sv = sv[-1:]
# EOF
settings_set("seeds", sv)
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Coldcard Co-Signing")
press_select()
time.sleep(.1)
title, story = cap_story()
assert title == "CCC Key C"
assert "(6) to import from Seed Vault" in story
need_keypress("6")
time.sleep(.1)
m = cap_menu()
if not has_candidates:
assert len(m) == 1
assert m[0] == "(none suitable)"
# unpickable
for _ in range(3):
pick_menu_item(m[0])
# nothing happened
m = cap_menu()
assert len(m) == 1
assert m[0] == "(none suitable)"
press_cancel()
return
# build_test_seed_vault has length of 4, but last item is xprv
# xprvs not allowed here - so not displayed in SeedVaultChooserMenu
assert len(m) == 3
m0_xfp = m[0].strip().split(" ", 1)[-1]
pick_menu_item(m[0])
time.sleep(.1)
m = cap_menu()
assert m0_xfp in m[0]
press_cancel()
time.sleep(.1)
title, story = cap_story()
assert title == "REMINDER"
assert "Key C is in your Seed Vault" in story
assert "MUST delete" in story
press_select()
# EOF