What changed, and why it matters
This commit only adds and updates automated test code for the COLDCARD firmware's 'hobble' restricted-access mode and related features (secure notes, NFC, key teleport). It does not change any firmware source code that runs on the device, so it cannot by itself introduce a security vulnerability or fix one in shipped code.
No security action required for this commit. Treat as normal test-suite maintenance. If reviewing a related security issue, examine the separate firmware commits that implement the hobble-mode logic being tested here.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff touches four files under testing/: conftest.py gains an nfc_is_enabled fixture; test_hobble.py is heavily expanded with menu-content assertions and hobble-mode behavior tests for notes and key teleport; test_notes.py’s need_some_notes fixture gains an is_q1 assertion; test_teleport.py adds hobbled-mode parameterization and a new test_hobble_limited case. No runtime firmware code is modified.
Changed components
testing/conftest.pytesting/test_hobble.pytesting/test_notes.pytesting/test_teleport.pyInspect captured patch +216 / −26
diff --git a/testing/conftest.py b/testing/conftest.py
index aaecc2e..1fd1d7d 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -1827,6 +1827,16 @@ def nfc_block4rf(sim_eval):
return doit
+@pytest.fixture
+def nfc_is_enabled(sim_eval):
+ # NFC is disabled by default in real product, and simulator w/o args
+ # - but some tests don't need to fail if it's off
+ # - or maybe your test can use some other method when it's off
+ # - use this to see if disabled at present and choose the right path
+ def doit():
+ return eval(sim_eval('bool(glob.NFC)'))
+ return doit
+
@pytest.fixture
def load_shared_mod():
# load indicated file.py as a module
diff --git a/testing/test_hobble.py b/testing/test_hobble.py
index 1dd5eb5..4042a5c 100644
--- a/testing/test_hobble.py
+++ b/testing/test_hobble.py
@@ -12,27 +12,16 @@ from charcodes import KEY_QR, KEY_NFC
from base64 import b32encode
from constants import *
from test_ephemeral import SEEDVAULT_TEST_DATA
-from test_backup import make_big_notes
+from test_notes import need_some_notes
'''TODO
When hobbled...
-- check adv menu is minimal
-- load a secure note/pw; check readonly once hobbled
- - cannot export
- - cannot edit
- - can view / use for kbd emulation
-- check notes not offered if none defined
-- check readonly features on notes when note pre-defined before entering hobbled mode
-- notes hidden if the exist but access disabled in policy
-
-- key teleport
- - check KT only offered if MS wallet setup
- - scan a KT and have it rejected if not PSBT type: so R and E types
- - MS psbt KT should still work in hobbled mode: test_teleport.py::test_teleport_ms_sign
-
-- verify no settings menu
+* check adv menu is minimal
+* notes hidden if the exist but access disabled in policy
+* verify no settings menu
+
- temp seeds are read only: no create, no rename, etc.
- seed vault can be accessed tho
@@ -48,8 +37,166 @@ When hobbled...
- verify whitelist of QR types is correct when in hobbled mode
- no private key material, no teleport starting, unless "okeys" is set
-- update menu tree w/ hobble mode view
+- TODO: update menu tree w/ hobble mode view
- verify that PSBT can be "signed" when SP enabled and delta-mode pin is active. seed not wiped.
'''
+
+# NOTE:
+# - these are unit tests of the effect of the hobble mode, not how it is enabled/disabled
+# - test_teleport.py::test_teleport_ms_sign
+# - verifies: MS psbt KT should still work in hobbled mode
+# - test_teleport.py::test_hobble_limited
+# - verifies: scan a KT and have it rejected if not PSBT type: so R and E types
+
+@pytest.fixture
+def set_hobble(sim_exec, settings_set, settings_remove, goto_home):
+ def doit(mode, enabled={}): # okeys, words, notes
+ assert mode in { True, False, 2 }
+
+ if mode:
+ v = dict(en=True, pol={})
+ for w in enabled:
+ v[w] = True
+ settings_set('sssp', v)
+ print(f'sssp = {v!r}')
+ else:
+ settings_remove('sssp')
+
+ sim_exec(f'''
+from pincodes import pa; from actions import goto_top_menu
+pa.hobbled_mode = {mode!r}
+goto_top_menu()
+''')
+ goto_home() # required, not sure why
+
+ yield doit
+
+ doit(False)
+
+@pytest.mark.parametrize('en_okeys', [ True, False] )
+@pytest.mark.parametrize('en_notes', [ True, False] )
+@pytest.mark.parametrize('en_nfc', [ True, False] )
+@pytest.mark.parametrize('en_multisig', [ True, False] )
+def test_menu_contents(set_hobble, pick_menu_item, cap_menu, en_okeys, en_notes, settings_set, need_some_notes, is_q1, is_mark4, en_nfc, sim_exec, en_multisig, vdisk_disabled):
+
+ # just enough to pass/fail the menu predicates!
+ settings_set('seedvault', True)
+
+ #settings_set('nfc', en_nfc)
+ sim_exec(f'import glob; glob.NFC = {(True if en_nfc else None)!r};')
+
+ settings_set('multisig', en_multisig)
+
+ if is_q1:
+ need_some_notes()
+
+ # main menu basics
+ expect = {'Ready To Sign', 'Address Explorer', 'Advanced/Tools' }
+
+ if is_q1:
+ expect.add('Scan Any QR Code')
+ else:
+ expect.add('Secure Logout')
+
+ en = set()
+ if en_okeys:
+ en.add('okeys')
+ expect.add('Seed Vault')
+ expect.add('Passphrase')
+
+ if en_notes:
+ en.add('notes')
+ if is_q1:
+ expect.add('Secure Notes & Passwords')
+
+ # enables hobble and goes to top menu
+ set_hobble(True, en)
+
+ m = cap_menu()
+ assert set(m) == expect, 'Main menu wrong'
+
+ # advanced menu
+ pick_menu_item("Advanced/Tools")
+
+ adv_expect = { 'File Management',
+ 'Export Wallet',
+ 'View Identity',
+ 'Paper Wallets',
+ 'Destroy Seed' }
+
+ if is_q1 and en_multisig:
+ adv_expect.add('Teleport Multisig PSBT')
+
+ if en_nfc:
+ adv_expect.add('NFC Tools')
+
+ m = cap_menu()
+ assert set(m) == adv_expect, "Adv menu wrong"
+
+ # file management
+ pick_menu_item("File Management")
+
+ fm_expect = { 'Sign Text File',
+ 'Batch Sign PSBT',
+ 'List Files',
+ 'Export Wallet',
+ 'Verify Sig File',
+ 'Format SD Card' }
+
+ if not vdisk_disabled:
+ fm_expect.add('Format RAM Disk')
+
+ if en_nfc:
+ fm_expect.add('NFC File Share')
+ if is_q1:
+ fm_expect.add('BBQr File Share')
+ fm_expect.add('QR File Share')
+
+ m = cap_menu()
+ assert set(m) == fm_expect, "File Mgmt menu wrong"
+
+
+def test_h_notes(only_q1, set_hobble, pick_menu_item, cap_menu, settings_set, need_some_notes, is_q1, sim_exec, settings_remove):
+ '''
+ * load a secure note/pw; check readonly once hobbled
+ * cannot export
+ * cannot edit
+ * can view / use for kbd emulation
+ * check notes not offered if none defined
+ * check readonly features on notes when note pre-defined before entering hobbled mode
+ '''
+ need_some_notes()
+ set_hobble(True, {'notes'})
+
+ pick_menu_item('Secure Notes & Passwords')
+
+ m = cap_menu()
+ assert m == [ '1: Title Here' ]
+ pick_menu_item(m[0])
+
+ m = cap_menu()
+ assert m == [ '"Title Here"', 'View Note', 'Sign Note Text' ]
+
+ # clear notes, should not be offered
+ settings_remove('notes')
+ settings_remove('secnap')
+ set_hobble(True, {'notes'})
+
+ m = cap_menu()
+ assert 'Secure Notes & Passwords' not in m
+
+def test_kt_limits(only_q1, set_hobble, pick_menu_item, cap_menu, settings_set, need_some_notes, is_q1, sim_exec, settings_remove):
+ '''
+ - key teleport
+ * check KT only offered if MS wallet setup
+ '''
+ settings_remove('multisig')
+ set_hobble(True)
+ pick_menu_item("Advanced/Tools")
+
+ assert 'Teleport Multisig PSBT' not in cap_menu()
+ # converse already tested in test_menu_contents
+
+# EOF
diff --git a/testing/test_notes.py b/testing/test_notes.py
index af33cfb..b544132 100644
--- a/testing/test_notes.py
+++ b/testing/test_notes.py
@@ -40,9 +40,10 @@ def goto_notes(cap_story, cap_menu, press_select, goto_home, pick_menu_item):
return doit
@pytest.fixture
-def need_some_notes(settings_get, settings_set):
+def need_some_notes(is_q1, settings_get, settings_set):
# create a note or use what's there, provide as obj
def doit(title='Title Here', body='Body'):
+ assert is_q1
notes = settings_get('notes', [])
if not notes:
settings_set('notes', [dict(misc=body, title=title)])
diff --git a/testing/test_teleport.py b/testing/test_teleport.py
index a986a1e..b48d40e 100644
--- a/testing/test_teleport.py
+++ b/testing/test_teleport.py
@@ -12,6 +12,7 @@ from base64 import b32encode
from constants import *
from test_ephemeral import SEEDVAULT_TEST_DATA
from test_backup import make_big_notes
+from test_hobble import set_hobble
# All tests in this file are exclusively meant for Q
#
@@ -132,7 +133,7 @@ def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr,
if 'Teleport Password' in scr: break
time.sleep(.2)
else:
- assert False, "Teleport Password not in screen"
+ raise RuntimeError("Teleport Password not in screen")
if expect_xfp:
assert xfp2str(expect_xfp) in scr
@@ -417,19 +418,22 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite
@pytest.mark.unfinalized
@pytest.mark.parametrize('num_ins', [ 15 ])
-@pytest.mark.parametrize('M', [2, 4])
+@pytest.mark.parametrize('M', [4])
@pytest.mark.parametrize('segwit', [True])
@pytest.mark.parametrize('incl_xpubs', [ False ])
+@pytest.mark.parametrize('hobbled', [ False, True ])
def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, dev, clear_ms,
fake_ms_txn, try_sign, incl_xpubs, bitcoind, cap_story, need_keypress,
cap_menu, pick_menu_item, grab_payload, rx_complete, press_select,
ndef_parse_txn_psbt, press_nfc, nfc_read, settings_get, settings_set,
- txid_from_export_prompt, sim_root_dir):
+ txid_from_export_prompt, sim_root_dir,
+ set_hobble, hobbled, readback_bbqr, nfc_is_enabled):
# IMPORTANT: won't work if you start simulator with --ms flag. Use no args
all_out_styles = list(unmap_addr_fmt.keys())
num_outs = len(all_out_styles)
+ set_hobble(hobbled)
clear_ms()
use_regtest()
@@ -526,12 +530,19 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d
txid = txid_from_export_prompt()
press_select() # exit QR
- # share signed txn via low-level NFC
- press_nfc()
- time.sleep(.1)
- contents = nfc_read()
+ if nfc_is_enabled():
+ # share signed txn via low-level NFC
+ press_nfc()
+ time.sleep(.1)
+ contents = nfc_read()
- got_psbt, got_txn, _ = ndef_parse_txn_psbt(contents, txid, expect_finalized=True)
+ got_psbt, got_txn, _ = ndef_parse_txn_psbt(contents, txid, expect_finalized=True)
+ else:
+ # NFC disabled. use other means .. bbqr
+ need_keypress(KEY_QR)
+ tcode, contents = readback_bbqr()
+ got_txn = (tcode == 'T')
+ got_psbt = (tcode == 'P')
assert not got_psbt
assert got_txn
@@ -725,4 +736,25 @@ def test_send_backup(testcase, rx_start, tx_start, cap_menu, enter_complex, pick
settings_set('notes', [])
+def test_hobble_limited(set_hobble, scan_a_qr, cap_menu, cap_screen, pick_menu_item, grab_payload, rx_complete, cap_story, press_cancel, press_select, settings_get, settings_set, restore_backup_unpacked, main_do_over, set_encoded_secret, reset_seed_words, make_big_notes):
+ # verify: in hobbled mode, KT is blocked for everything except multisig cases
+
+ set_hobble(True)
+
+ from bbqr.split import split_qrs
+ from bbqr import split_qrs, join_qrs
+
+ _, parts = split_qrs(b's'*33, 'R')
+ rx_complete(parts[0], '12345678', expect_fail=True)
+ last = cap_screen().split('\n')[-1]
+ assert last == 'KT Blocked'
+
+ _, parts = split_qrs(b's'*33, 'S')
+ rx_complete(parts[0], 'abcdefgh', expect_fail=True)
+ last = cap_screen().split('\n')[-1]
+ assert last == 'KT Blocked'
+
+
+
+
# EOF
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.