coldcard: extract transaction signing helpers
What changed, and why it matters
This commit simply reorganizes the Coldcard hardware wallet signing code in HWI by moving existing logic into two new helper methods. There is no change to what the code actually does; it is a pure refactoring (code cleanup) with no security-relevant behavior change visible in the diff.
No security action required; treat as routine refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff extracts the single-pass PSBT signing flow into _sign_tx_once() and the multi-pass counting logic into _sign_without_policy_names(). The original sign_tx() method now calls _sign_without_policy_names() instead of inlining the same steps. Byte-for-byte, the algorithm, checksum verification, upload loop, signing command, simulator keypress, and result download remain identical. No new inputs are accepted, no validation is removed, and no cryptographic operations are altered.
Changed components
hwilib/devices/coldcard.pyInspect captured patch +76 / −64
### hwilib/devices/coldcard.py
@@ -155,6 +155,81 @@ 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:
+ # Get psbt in hex and then make binary
+ fd = io.BytesIO(base64.b64decode(psbt.serialize()))
+
+ # learn size (portable way)
+ sz = fd.seek(0, 2)
+ fd.seek(0)
+
+ left = sz
+ chk = sha256()
+ for pos in range(0, sz, MAX_BLK_LEN):
+ here = fd.read(min(MAX_BLK_LEN, left))
+ if not here:
+ break
+ left -= len(here)
+ result = self.device.send_recv(CCProtocolPacker.upload(pos, sz, here))
+ assert result == pos
+ chk.update(here)
+
+ # do a verify
+ expect = chk.digest()
+ result = self.device.send_recv(CCProtocolPacker.sha256())
+ assert len(result) == 32
+ if result != expect:
+ 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)
+ assert ok is None
+ if self.device.is_simulator:
+ self.device.send_recv(CCProtocolPacker.sim_keypress(b'y'))
+
+ print("Waiting for OK on the Coldcard...", file=sys.stderr)
+
+ while 1:
+ time.sleep(0.250)
+ done = self.device.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
+ if done is None:
+ continue
+ break
+
+ if len(done) != 2:
+ raise DeviceFailureError('Failed: %r' % done)
+
+ result_len, result_sha = done
+
+ result = self.device.download_file(result_len, result_sha, file_number=1)
+
+ psbt = PSBT()
+ psbt.deserialize(base64.b64encode(result).decode())
+ return psbt
+
+ def _sign_without_policy_names(
+ self,
+ psbt: PSBT,
+ master_fp: bytes,
+ ) -> PSBT:
+ # For multisigs, we may need to do multiple passes if we appear in an
+ # input multiple times.
+ passes = 1
+ if not self.is_edge:
+ for psbt_in in psbt.inputs:
+ our_keys = 0
+ for key in psbt_in.hd_keypaths.keys():
+ keypath = psbt_in.hd_keypaths[key]
+ if keypath.fingerprint == master_fp and key not in psbt_in.partial_sigs:
+ our_keys += 1
+ if our_keys > passes:
+ passes = our_keys
+
+ for _ in range(passes):
+ psbt = self._sign_tx_once(psbt)
+
+ return psbt
+
@coldcard_exception
def sign_tx(
self,
@@ -176,73 +251,10 @@ def sign_tx(
xpub = self.device.send_recv(CCProtocolPacker.get_xpub('m/0\''), timeout=None)
master_fp = get_xpub_fingerprint(xpub)
- # For multisigs, we may need to do multiple passes if we appear in an input multiple times
- passes = 1
- if not self.is_edge:
- for psbt_in in psbt.inputs:
- our_keys = 0
- for key in psbt_in.hd_keypaths.keys():
- keypath = psbt_in.hd_keypaths[key]
- if keypath.fingerprint == master_fp and key not in psbt_in.partial_sigs:
- our_keys += 1
- if our_keys > passes:
- passes = our_keys
-
if psbt.version == 2 and not self._supports_psbt_v2():
psbt.convert_to_v0()
- for _ in range(passes):
- # Get psbt in hex and then make binary
- fd = io.BytesIO(base64.b64decode(psbt.serialize()))
-
- # learn size (portable way)
- sz = fd.seek(0, 2)
- fd.seek(0)
-
- left = sz
- chk = sha256()
- for pos in range(0, sz, MAX_BLK_LEN):
- here = fd.read(min(MAX_BLK_LEN, left))
- if not here:
- break
- left -= len(here)
- result = self.device.send_recv(CCProtocolPacker.upload(pos, sz, here))
- assert result == pos
- chk.update(here)
-
- # do a verify
- expect = chk.digest()
- result = self.device.send_recv(CCProtocolPacker.sha256())
- assert len(result) == 32
- if result != expect:
- 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)
- assert ok is None
- if self.device.is_simulator:
- self.device.send_recv(CCProtocolPacker.sim_keypress(b'y'))
-
- print("Waiting for OK on the Coldcard...", file=sys.stderr)
-
- while 1:
- time.sleep(0.250)
- done = self.device.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
- if done is None:
- continue
- break
-
- if len(done) != 2:
- raise DeviceFailureError('Failed: %r' % done)
-
- result_len, result_sha = done
-
- result = self.device.download_file(result_len, result_sha, file_number=1)
-
- psbt = PSBT()
- psbt.deserialize(base64.b64encode(result).decode())
-
- return psbt
+ return self._sign_without_policy_names(psbt, master_fp)
@coldcard_exception
def sign_message(self, message: Union[str, bytes], keypath: str) -> str: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.