psbt: detect keys and signatures by fingerprint
What changed, and why it matters
This commit adds two helper methods to the PSBT (Partially Signed Bitcoin Transaction) handling code that let the library check whether a specific hardware wallet's fingerprint appears in a transaction input, and whether that fingerprint has already provided a signature. The change is purely additive and includes tests. There is no direct evidence in the commit that this fixes an active security vulnerability; it appears to be a defensive or feature-oriented improvement to support better transaction signing workflows.
Review where these new helper methods will be called in subsequent commits or pull requests; ensure that fingerprint comparisons are constant-time if used in security-critical paths, and that the methods cannot be confused by duplicate or unvalidated fingerprint entries in a PSBT.
Security signals we found
Adds fingerprint-based key/signature detection in PSBT input parsing
Includes unit tests covering legacy BIP32, Taproot key path, and Taproot script path cases
No caller or usage of new methods shown in the diff
No mention of vulnerability, CVE, security bug, or researcher attribution in commit message
Evidence from the diff
The patch introduces PSBTInput.has_fingerprint(fingerprint) and PSBTInput.has_signature(fingerprint). These iterate over BIP32 derivation paths (both legacy and Taproot) to detect the presence of a given master key fingerprint and whether a corresponding partial signature exists. The methods are used nowhere else in the diff, and the commit message frames it as detection logic rather than a bug fix. No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Changed components
hwilib/psbt.pytest/test_psbt.pyInspect captured patch +74 / −0
### hwilib/psbt.py
@@ -164,6 +164,40 @@ def set_null(self) -> None:
self.musig2_partial_sigs.clear()
self.unknown.clear()
+ def has_fingerprint(self, fingerprint: bytes) -> bool:
+ """
+ Return whether this input contains a key with the specified fingerprint.
+ """
+ return any(
+ origin.fingerprint == fingerprint
+ for origin in self.hd_keypaths.values()
+ ) or any(
+ origin.fingerprint == fingerprint
+ for _, origin in self.tap_bip32_paths.values()
+ )
+
+ def has_signature(self, fingerprint: bytes) -> bool:
+ """
+ Return whether a key with the specified fingerprint has a signature.
+ """
+ for pubkey, origin in self.hd_keypaths.items():
+ if (
+ origin.fingerprint == fingerprint
+ and pubkey in self.partial_sigs
+ ):
+ return True
+ for pubkey, (leaf_hashes, origin) in self.tap_bip32_paths.items():
+ if origin.fingerprint != fingerprint:
+ continue
+ if not leaf_hashes and self.tap_key_sig:
+ return True
+ if any(
+ (pubkey, leaf_hash) in self.tap_script_sigs
+ for leaf_hash in leaf_hashes
+ ):
+ return True
+ return False
+
def deserialize(self, f: Readable) -> None:
"""
Deserialize a serialized PSBT input.
### test/test_psbt.py
@@ -65,5 +65,45 @@ def test_convert_to_v0(self):
self.assertEqual(psbt.tx.vin[0].nSequence, 0xffffffff)
+ def test_has_fingerprint(self):
+ cases = [
+ ("BIP32", 7, {"19542eb0": True, "00000001": False}),
+ ("Taproot BIP32", 22, {"7c461e5d": True, "00000001": False}),
+ ]
+ for name, vector, fingerprints in cases:
+ with self.subTest(name=name):
+ psbt = PSBT()
+ psbt.deserialize(self.data["valid"][vector])
+ for fingerprint, expected in fingerprints.items():
+ self.assertEqual(
+ psbt.inputs[0].has_fingerprint(bytes.fromhex(fingerprint)),
+ expected,
+ )
+
+ def test_has_signature(self):
+ cases = [
+ ("partial signature", 4, {"b4a6ba67": True}),
+ (
+ "partial signature for another fingerprint",
+ 7,
+ {"19542eb0": True, "e81a5744": False},
+ ),
+ ("Taproot key path", 9, {"772b2da7": True}),
+ (
+ "Taproot script path",
+ 22,
+ {"2680dd6e": True, "580b0887": False},
+ ),
+ ]
+ for name, vector, fingerprints in cases:
+ with self.subTest(name=name):
+ psbt = PSBT()
+ psbt.deserialize(self.data["valid"][vector])
+ for fingerprint, expected in fingerprints.items():
+ self.assertEqual(
+ psbt.inputs[0].has_signature(bytes.fromhex(fingerprint)),
+ expected,
+ )
+
if __name__ == "__main__":
unittest.main()Why this scored 24/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.