block some USB command in hobble mode
What changed, and why it matters
This commit adds extra guardrails to a COLDCARD hardware wallet feature called 'hobble mode' (a spending-policy / restricted-access state). It now blocks several USB commands—such as creating backups, enrolling multisig wallets, and certain user-management commands—when the device is hobbled. It also makes firmware uploads in hobbled mode follow the same stricter rules used in HSM mode. The change appears to be a hardening patch rather than a fix for an active exploit, because the feature was already intended to restrict these commands but some were not being blocked.
Treat as a security hardening commit. Review whether any other USB commands (for example, those that export seed material, change PINs, or modify policy settings) should also be blocked in hobbled mode. Verify the 'okeys' policy semantics are documented and that the error message does not leak sensitive state. Consider whether the change warrants a changelog/security note for users relying on hobble mode.
Security signals we found
New command blacklist enforced when hobbled_mode is active
Existing HSM-style firmware-upload restrictions extended to hobbled mode
Spending-policy flag 'okeys' gates the 'pass' command
New exception handling returns explicit 'Spending policy in effect' error
Unit tests added to verify command blocking
Evidence from the diff
The patch extends command filtering in shared/usb.py. A new HOBBLED_CMDS frozenset lists USB commands that must be rejected when pa.hobbled_mode is active, raising SpendPolicyViolation (returned to the host as ‘err_Spending policy in effect’). Blocked commands include ‘back’ (backup), ‘enrl’ (enroll multisig), ‘bagi’/’dfu_’, and several HSM-related commands. The ‘pass’ command is allowed only if the ‘okeys’ spending-policy flag is enabled. Additionally, firmware upload checks now apply the same PSBT-only, MAX_TXN_LEN restriction used in HSM mode when the device is hobbled. Tests in testing/test_hobble.py cover the new USB command blocking behavior.
Changed components
shared/usb.pyshared/exceptions.pytesting/test_hobble.pyInspect captured patch +77 / −30
diff --git a/shared/exceptions.py b/shared/exceptions.py
index a9a6f1e..b19175f 100644
--- a/shared/exceptions.py
+++ b/shared/exceptions.py
@@ -19,10 +19,10 @@ class CCBusyError(RuntimeError):
# HSM is blocking your action
class HSMDenied(RuntimeError):
pass
-
class HSMCMDDisabled(RuntimeError):
pass
+
# PSBT / transaction related
class FatalPSBTIssue(RuntimeError):
pass
@@ -51,7 +51,7 @@ class QRDecodeExplained(ValueError):
class UnknownAddressExplained(ValueError):
pass
-# We're not going to co-sign using CCC feature
+# We're not going to co-sign using spending policy features
class SpendPolicyViolation(RuntimeError):
pass
diff --git a/shared/usb.py b/shared/usb.py
index 413592d..e3f3d05 100644
--- a/shared/usb.py
+++ b/shared/usb.py
@@ -11,7 +11,8 @@ from ustruct import pack, unpack_from
from ckcc import watchpoint, is_simulator
from utils import problem_file_line, call_later_ms
from version import supports_hsm, is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN
-from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled
+from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled, SpendPolicyViolation
+from pincodes import pa
# Unofficial, unpermissioned... numbers
COINKITE_VID = 0xd13e
@@ -68,6 +69,21 @@ HSM_DISABLE_CMDS = frozenset({
"hsms",
})
+# spending policy active: blacklist some commands
+# - 'pass' may be allowed if 'okeys' is enabled
+HOBBLED_CMDS = frozenset({
+ 'enrl', # no new multisigs during policy enforcement
+ 'back', # no backups
+ 'bagi', 'dfu_', # just in case
+
+ "user", # same as HSM_DISABLE_CMDS
+ "rmur",
+ "nwur",
+ "gslr",
+ "hsts",
+ "hsms",
+})
+
# singleton instance of USBHandler()
handler = None
@@ -217,6 +233,8 @@ class USBHandler:
except CCBusyError:
# auth UX is doing something else
resp = b'busy'
+ except SpendPolicyViolation:
+ resp = b'err_Spending policy in effect'
except HSMDenied:
resp = b'err_Not allowed in HSM mode'
except HSMCMDDisabled:
@@ -345,7 +363,7 @@ class USBHandler:
except:
raise FramingError('decode')
- if cmd[0].isupper() and is_devmode:
+ if is_devmode and cmd[0].isupper():
# special hacky commands to support testing w/ the simulator
try:
from usb_test_commands import do_usb_command
@@ -358,7 +376,18 @@ class USBHandler:
if cmd not in HSM_WHITELIST:
raise HSMDenied
- if not settings.get('hsmcmd', False):
+ if pa.hobbled_mode:
+ # block some commands when we are hobbled.
+ if cmd in HOBBLED_CMDS:
+ raise SpendPolicyViolation
+
+ if cmd in {'pwok', 'pass'}:
+ from ccc import sssp_spending_policy
+ if not sssp_spending_policy('okeys'):
+ raise SpendPolicyViolation
+
+ elif not settings.get('hsmcmd', False):
+ # block these HSM-related command if not using feature
if cmd in HSM_DISABLE_CMDS:
raise HSMCMDDisabled
@@ -741,7 +770,6 @@ class USBHandler:
from glob import dis, hsm_active
from utils import check_firmware_hdr
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
- from pincodes import pa
# maintain a running SHA256 over what's received
if offset == 0:
@@ -754,8 +782,8 @@ class USBHandler:
assert offset % 256 == 0, 'alignment'
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'
- if hsm_active:
- # additional restrictions in HSM mode
+ if hsm_active or pa.hobbled_mode:
+ # additional restriction in HSM mode or hobbled: must be PSBT
assert offset+len(data) <= total_size <= MAX_TXN_LEN, 'psbt'
if offset == 0:
assert data[0:5] == b'psbt\xff', 'psbt'
@@ -834,7 +862,6 @@ class USBHandler:
def handle_bag_number(self, bag_num):
import version, callgate
from glob import dis, settings
- from pincodes import pa
if bag_num and version.is_factory_mode and not version.has_qr:
# check state first
diff --git a/testing/test_hobble.py b/testing/test_hobble.py
index 137c468..e5b546a 100644
--- a/testing/test_hobble.py
+++ b/testing/test_hobble.py
@@ -12,26 +12,6 @@ from test_ephemeral import SEEDVAULT_TEST_DATA, WORDLISTS
from test_ephemeral import confirm_tmp_seed, verify_ephemeral_secret_ui
from test_ux import word_menu_entry
-'''
-TODO -- When hobbled...
-
-- login sequence
- 1) system has lgto value: should get bypass pin, main pin, delay, then main pin again
- 2) using a trick PIN with delay, after bypass pin should delay
- 3) bypass pin + duress wallet PIN => should work => but not a useful trick combo
-
-- word entry during login
- - q1 vs mk4 style
- - wrong values given, etc
-
-- verify whitelist of QR types is correct when in hobbled mode
- - no private key material, no teleport starting, unless "okeys" is set
-
-- 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 effects of hobble mode, not how it is enabled/disabled:
#
@@ -386,6 +366,46 @@ def test_h_tempseeds(mode, set_hobble, pick_menu_item, cap_menu, settings_set, i
pick_menu_item("Restore Master")
press_select()
-# TODO: test usb commands are blocked
+@pytest.mark.parametrize('en_okeys', [ True, False])
+def test_h_usbcmds(en_okeys, set_hobble, dev):
+ # test various usb commands are blocked during hobble
+
+ from ckcc_protocol.protocol import CCProtoError
+
+ set_hobble(True, {'okeys'} if en_okeys else {})
+
+ block_list = [
+ 'back', 'enrl', 'bagi', 'hsms', 'user', 'nwur', 'rmur',
+ ]
+
+ if not en_okeys:
+ block_list.insert(0, 'pass')
+
+ for cmd in block_list:
+ with pytest.raises(CCProtoError) as ee:
+ got = dev.send_recv(cmd)
+ assert 'Spending policy in effect' in str(ee)
+
+# TODO: verify that PSBT can be "signed" when SP enabled and delta-mode pin is active. seed not wiped.
+
+
+# TODO verify whitelist of QR types is correct when in hobbled mode
+# - no private key material, no teleport starting, unless "okeys" is set
+
+
+# TODO Special Login Sequence
+
+'''
+- login sequence
+ 1) system has lgto value: should get bypass pin, main pin, delay, then main pin again
+ 2) using a trick PIN with delay, after bypass pin should delay
+ 3) bypass pin + duress wallet PIN => should work => but not a useful trick combo
+
+- word entry during login
+ - q1 vs mk4 style
+ - wrong values given, etc
+
+'''
+
# EOF
Why this scored 44/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.