coldcard: support BIP388 policy signing
What changed, and why it matters
This commit adds support for signing Bitcoin transactions with named wallet policies (BIP388) on newer Coldcard hardware wallets. It is a feature addition, not a fix for a known security flaw. The change removes an error that previously blocked this feature and sends each registered policy name to the device in separate signing requests. There is no indication in the commit that this addresses a security vulnerability.
No security action required. Treat as a normal feature/enhancement commit. Reviewers may optionally verify that the miniscript_name length assertion and ASCII encoding prevent malformed protocol messages, and that the multiple signing-pass logic does not produce partially signed PSBTs in an unexpected order.
Security signals we found
Feature addition for BIP388 policy signing
Removal of UnavailableActionError guard for registered_descriptors
New miniscript_name parameter length-bounded to 1-32 ASCII bytes
Multiple signing passes per policy plus fallback unnamed pass
No mention of vulnerability, CVE, bug, or security fix in commit message
Evidence from the diff
The patch extends the Coldcard device driver in HWI to support BIP388 registered descriptor policy signing. It modifies CCProtocolPacker.sign_transaction() to optionally append an ASCII miniscript/policy name (1-32 bytes) to the ‘stxn’ command, and adds _sign_with_policy_names() which iterates over registered descriptors, signing once per policy name, then performs a final unnamed signing pass for any remaining inputs. The sign_tx() method now branches on self.is_edge and registered_descriptors instead of unconditionally raising UnavailableActionError. Older Coldcards continue to infer stored multisig policies from the PSBT.
Changed components
hwilib/devices/ckcc/protocol.pyhwilib/devices/coldcard.pyColdcard hardware wallet integrationBIP388 registered descriptor signing pathInspect captured patch +54 / −7
### hwilib/devices/ckcc/protocol.py
@@ -91,11 +91,17 @@ def sha256():
return b'sha2'
@staticmethod
- def sign_transaction(length, file_sha, finalize=False, flags=0x0):
+ def sign_transaction(length, file_sha, finalize=False, flags=0x0,
+ miniscript_name=None):
# must have already uploaded binary, and give expected sha256
assert len(file_sha) == 32
flags |= (STXN_FINALIZE if finalize else 0x00)
- return pack('<4sII32s', b'stxn', length, int(flags), file_sha)
+ rv = pack('<4sII32s', b'stxn', length, int(flags), file_sha)
+ if miniscript_name:
+ name = miniscript_name.encode('ascii')
+ assert 1 <= len(name) <= 32, "name len"
+ rv += pack('B', len(name)) + name
+ return rv
@staticmethod
def sign_message(raw_msg, subpath='m', addr_fmt=AF_CLASSIC):
### hwilib/devices/coldcard.py
@@ -155,7 +155,11 @@ def get_master_fingerprint(self) -> bytes:
# quick method to get fingerprint of wallet
return struct.pack('<I', self.device.master_fingerprint)
- def _sign_tx_once(self, psbt: PSBT) -> PSBT:
+ def _sign_tx_once(
+ self,
+ psbt: PSBT,
+ miniscript_name: Optional[str] = None,
+ ) -> PSBT:
# Get psbt in hex and then make binary
fd = io.BytesIO(base64.b64decode(psbt.serialize()))
@@ -182,7 +186,14 @@ def _sign_tx_once(self, psbt: PSBT) -> PSBT:
raise DeviceFailureError("Wrong checksum:\nexpect: %s\n got: %s" % (b2a_hex(expect).decode('ascii'), b2a_hex(result).decode('ascii')))
# start the signing process
- ok = self.device.send_recv(CCProtocolPacker.sign_transaction(sz, expect), timeout=None)
+ ok = self.device.send_recv(
+ CCProtocolPacker.sign_transaction(
+ sz,
+ expect,
+ miniscript_name=miniscript_name,
+ ),
+ timeout=None,
+ )
assert ok is None
if self.device.is_simulator:
self.device.send_recv(CCProtocolPacker.sim_keypress(b'y'))
@@ -230,6 +241,29 @@ def _sign_without_policy_names(
return psbt
+ def _sign_with_policy_names(
+ self,
+ psbt: PSBT,
+ registered_descriptors: Set[RegisteredDescriptor],
+ master_fp: bytes,
+ ) -> PSBT:
+ for registration in sorted(
+ registered_descriptors,
+ key=lambda registration: registration.serialize(),
+ ):
+ psbt = self._sign_tx_once(psbt, registration.name)
+
+ # Named policy requests only cover matching inputs. Use an unnamed
+ # request to infer a policy for any remaining unsigned device inputs.
+ if any(
+ psbt_in.has_fingerprint(master_fp)
+ and not psbt_in.has_signature(master_fp)
+ for psbt_in in psbt.inputs
+ ):
+ psbt = self._sign_tx_once(psbt)
+
+ return psbt
+
@coldcard_exception
def sign_tx(
self,
@@ -243,8 +277,6 @@ def sign_tx(
- Multisigs need to be registered on the device before a transaction spending that multisig will be signed by the device.
- Multisigs must use BIP 67. This can be accomplished in Bitcoin Core using the `sortedmulti()` descriptor, available in Bitcoin Core 0.20.
"""
- if registered_descriptors:
- raise UnavailableActionError("The Coldcard does not support BIP388 policy signing")
self.device.check_mitm()
# Get this devices master key fingerprint
@@ -254,7 +286,16 @@ def sign_tx(
if psbt.version == 2 and not self._supports_psbt_v2():
psbt.convert_to_v0()
- return self._sign_without_policy_names(psbt, master_fp)
+ if self.is_edge and registered_descriptors:
+ psbt = self._sign_with_policy_names(
+ psbt,
+ registered_descriptors,
+ master_fp,
+ )
+ else:
+ psbt = self._sign_without_policy_names(psbt, master_fp)
+
+ return psbt
@coldcard_exception
def sign_message(self, message: Union[str, bytes], keypath: str) -> str:Why this scored 19/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.