testing: block_h bumped for SSSP too, when CCC overrides SSSP block
What changed, and why it matters
This commit fixes a bookkeeping bug in the COLDCARD firmware's transaction-signing logic. When two optional security features—SSSP (a spending velocity limit) and CCC (a policy that can override SSSP)—are both active, the block-height tracker for SSSP was not being updated in one specific case where CCC allowed a transaction that SSSP would have blocked. The fix ensures both trackers stay in sync, so SSSP's velocity limit remains accurate after an override. There is no direct evidence in the commit that this is exploitable as a security vulnerability; it reads as a test-driven correctness fix.
Treat as a normal correctness/test fix. Review whether the stale block_h value could have caused any policy bypass in prior firmware versions, but the commit itself does not present evidence of an exploitable security flaw. No urgent vendor disclosure action is required based solely on this commit.
Security signals we found
State-synchronization fix between two policy enforcement features
Regression test added for override interaction
No mention of CVE, advisory, or security disclosure in commit or diff
Change is small, comment-only in production code plus tests
Evidence from the diff
In shared/auth.py, inside ApproveTransaction.sign_psbt(), the code already called SSSPFeature.update_last_signed(self.psbt) whenever SSSP was enabled. The comment is updated to clarify that this update must happen even when SSSP itself would block the transaction but CCC overrides that block. A new regression test in testing/test_sssp.py (test_ccc_with_sssp_block_h) reproduces the scenario: SSSP velocity is set to 48 blocks, CCC is set to unlimited, a first PSBT is signed at the current block height, then a second PSBT with a lock-time only 1 block later is signed. Because CCC overrides SSSP’s velocity block, the signing succeeds, and the test now asserts that both ccc[‘pol’][‘block_h’] and sssp[‘pol’][‘block_h’] are updated to the chosen lock time. The other changes are test infrastructure imports and whitespace cleanup.
Changed components
shared/auth.py: ApproveTransaction.sign_psbt()testing/test_sssp.pytesting/conftest.pytesting/test_hobble.pyInspect captured patch +60 / −4
diff --git a/shared/auth.py b/shared/auth.py
index 3bc1e5b..a445801 100644
--- a/shared/auth.py
+++ b/shared/auth.py
@@ -622,7 +622,7 @@ class ApproveTransaction(UserAuthorizedAction):
CCCFeature.sign_psbt(self.psbt)
if SSSPFeature.is_enabled():
- # capture new min-height for velocity limit
+ # update SSSP block_h even if SSSP blocks and overridden by CCC
SSSPFeature.update_last_signed(self.psbt)
except FraudulentChangeOutput as exc:
diff --git a/testing/conftest.py b/testing/conftest.py
index 501422f..ad876f3 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -3023,7 +3023,7 @@ def import_wif_to_store(goto_home, pick_menu_item, cap_story, press_select, cap_
from test_backup import backup_system
from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll, try_sign_bbqr, split_scan_bbqr
from test_bip39pw import set_bip39_pw
-from test_ccc import get_last_violation
+from test_ccc import get_last_violation, setup_ccc, goto_ccc_menu, ccc_ms_setup, bitcoind_create_watch_only_wallet
from test_drv_entro import derive_bip85_secret, activate_bip85_ephemeral
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
from test_ephemeral import ephemeral_seed_disabled_ui, restore_main_seed, confirm_tmp_seed
diff --git a/testing/test_hobble.py b/testing/test_hobble.py
index b18e499..cdcfd93 100644
--- a/testing/test_hobble.py
+++ b/testing/test_hobble.py
@@ -20,7 +20,7 @@ import pytest, time, os, pdb
from bip32 import BIP32Node
from constants import simulator_fixed_words, simulator_fixed_xprv
from test_ephemeral import SEEDVAULT_TEST_DATA, WORDLISTS
-from test_ephemeral import confirm_tmp_seed, verify_ephemeral_secret_ui
+from test_ephemeral import confirm_tmp_seed, verify_ephemeral_secret_ui
from test_ux import word_menu_entry
from charcodes import KEY_QR
@@ -495,5 +495,5 @@ def test_empty_notes_bug(set_hobble, goto_notes, cap_menu, pick_menu_item, is_q1
m = cap_menu()
assert len(m) == 1
assert m[0] == "(none saved yet)"
-
+
# EOF
diff --git a/testing/test_sssp.py b/testing/test_sssp.py
index 9023ace..c00c33a 100644
--- a/testing/test_sssp.py
+++ b/testing/test_sssp.py
@@ -767,4 +767,60 @@ def test_sssp_notes_enable(only_q1, setup_sssp):
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)
+
+
+@pytest.mark.bitcoind
+def test_ccc_with_sssp_block_h(setup_ccc, ccc_ms_setup, setup_sssp, bitcoind, policy_sign,
+ settings_get, settings_set, bitcoind_create_watch_only_wallet,
+ pick_menu_item, press_select, cap_story):
+ settings_set("ccc", None)
+ settings_set("sssp", None)
+ settings_set("multisig", [])
+ settings_set("chain", "XRT")
+
+ setup_ccc(mag=10, vel='Unlimited')
+ _, target_mi = ccc_ms_setup()
+
+ bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
+
+ setup_sssp(pin="11-11", mag=10, vel='48 blocks (8h)')
+
+ pick_menu_item("Test Drive")
+ time.sleep(.1)
+ _, story = cap_story()
+ assert "COLDCARD operation will look like with Spending Policy" in story
+ press_select()
+
+ multi_addr = bitcoind_wo.getnewaddress()
+ bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=49)
+ bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
+
+ cur_h = bitcoind.supply_wallet.getblockchaininfo()["blocks"]
+ psbt1 = bitcoind_wo.walletcreatefundedpsbt(
+ [], [{bitcoind.supply_wallet.getnewaddress(): 1}], cur_h
+ )["psbt"]
+ policy_sign(bitcoind_wo, psbt1)
+
+ assert cur_h == settings_get("sssp")["pol"]["block_h"]
+ assert cur_h == settings_get("ccc")["pol"]["block_h"]
+ baseline_block_h = cur_h
+
+ bitcoind.supply_wallet.generatetoaddress(5, bitcoind.supply_wallet.getnewaddress())
+
+ # second signing -> SSSP velocity BLOCKS but CCC overrides and signing allowed
+ chosen_lock_time = baseline_block_h + 1
+ psbt2 = bitcoind_wo.walletcreatefundedpsbt(
+ [], [{bitcoind.supply_wallet.getnewaddress(): 1}], chosen_lock_time
+ )["psbt"]
+ policy_sign(bitcoind_wo, psbt2)
+
+ assert chosen_lock_time == settings_get("ccc")["pol"]["block_h"]
+ # SSSP block_h is updated too
+ assert chosen_lock_time == settings_get("sssp")["pol"]["block_h"]
+
+ pick_menu_item("EXIT TEST DRIVE")
+ settings_set("ccc", None)
+ settings_set("sssp", None)
+
+
# EOF
Why this scored 29/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.