What changed, and why it matters
This commit changes how SeedSigner handles Bitcoin transaction files (PSBTs) that contain placeholder '00000000' fingerprints. Normally, a fingerprint identifies which wallet seed a transaction belongs to. Some wallet software exports incomplete data (xpub only, no derivation path), leaving zeros in the fingerprint field. Previously, SeedSigner could not recognize these transactions as belonging to the user's seed, showing a '?' and potentially refusing to sign. The new code tries to reconstruct the correct fingerprint by deriving public keys from the seed and comparing them. This is a usability fix, not a vulnerability patch, but it touches security-critical matching logic.
Review the fallback logic carefully during normal QA. Ensure that `self.root` is always the correct seed root before `fill_zero_fingerprints()` runs, because the function trusts `self.root` to rewrite fingerprints. Consider whether an attacker can craft a PSBT with a zero fingerprint and a derivation path that accidentally matches a different seed's derived key; the comparison is against `pub.sec()`, so this would only succeed if the public key genuinely belongs to the seed, which is the intended behavior. No urgent security patch is indicated.
Security signals we found
Change to fingerprint matching logic for PSBT ownership verification
New fallback derivation-and-public-key comparison when fingerprint is zero
Mutation of PSBT derivation metadata before parsing/signing
Taproot and legacy BIP32 derivation paths both affected
No input sanitization changes or network/serialization hardening
Evidence from the diff
The patch adds fill_zero_fingerprints() and _fill_zero_fingerprint_scope() to PSBTParser. Before parsing, it iterates all inputs/outputs, finds BIP32 and Taproot derivations whose fingerprint is b'\x00\x00\x00\x00', derives the expected public key using self.root.derive(derivation.derivation), and if it matches the PSBT’s public key, replaces the zero fingerprint with the seed’s actual fingerprint. has_matching_input_fingerprint() is also updated to accept zero-fingerprint derivations if the derived public key matches the seed. The change is defensive: it makes SeedSigner more tolerant of PSBTs produced by wallets that omit the master fingerprint, but it does not relax cryptographic validation.
Changed components
src/seedsigner/models/psbt_parser.pyPSBT parsing and seed-matching logicBIP32 and Taproot derivation fingerprint handlingInspect captured patch +85 / −5
diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py
index 7aa9953..aae88b6 100644
--- a/src/seedsigner/models/psbt_parser.py
+++ b/src/seedsigner/models/psbt_parser.py
@@ -3,9 +3,9 @@ from binascii import hexlify
from embit import psbt, script, ec, bip32
from embit.descriptor import Descriptor
from embit.networks import NETWORKS
-from embit.psbt import PSBT
+from embit.psbt import PSBT, DerivationPath, InputScope, OutputScope
from io import BytesIO
-from typing import List
+from typing import List, Union
from seedsigner.models.seed import Seed
from seedsigner.models.settings import SettingsConstants
@@ -79,6 +79,9 @@ class PSBTParser():
self._set_root()
+ # Try to fix zero fingerprints before parsing
+ self.fill_zero_fingerprints()
+
rt = self._parse_inputs()
if rt == False:
return False
@@ -372,14 +375,36 @@ class PSBTParser():
the current seed.
"""
seed_fingerprint = seed.get_fingerprint(network)
+
+ def check_fingerprint_match(pub, derivation_path):
+ """Check fingerprint match with zero fingerprint fallback"""
+
+ # If exact fingerprint match
+ if hexlify(derivation_path.fingerprint).decode() == seed_fingerprint:
+ return True
+
+ # Zero fingerprint fallback
+ if derivation_path.fingerprint == b"\x00\x00\x00\x00":
+ root = bip32.HDKey.from_seed(seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
+ try:
+ derived_key = root.derive(derivation_path.derivation)
+ return derived_key.key.sec() == pub.sec() # Public keys match
+ except Exception:
+ pass
+ return False
+
+ # Check all derivations in all inputs
for input in psbt.inputs:
+ # Check regular BIP32 derivations
for pub, derivation_path in input.bip32_derivations.items():
- if seed_fingerprint == hexlify(derivation_path.fingerprint).decode():
+ if check_fingerprint_match(pub, derivation_path):
return True
-
+
+ # Check Taproot derivations
for pub, (leaf_hashes, derivation_path) in input.taproot_bip32_derivations.items():
- if seed_fingerprint == hexlify(derivation_path.fingerprint).decode():
+ if check_fingerprint_match(pub, derivation_path):
return True
+
return False
@@ -390,3 +415,58 @@ class PSBTParser():
is_owner = descriptor.owns(output)
# print(f"{self.psbt.tx.vout[i].script_pubkey.address()} | {output.value} | {is_owner}")
return is_owner
+
+
+ def fill_zero_fingerprints(self):
+ """Fix for zeros in fingerprint that happen when user imports the wallet
+ with XPUB only (without derivation path)
+ """
+ if not self.root:
+ return 0
+
+ filled = 0
+
+ for inp in self.psbt.inputs:
+ filled += self._fill_zero_fingerprint_scope(inp)
+
+ for out in self.psbt.outputs:
+ filled += self._fill_zero_fingerprint_scope(out)
+
+ if filled > 0:
+ logger.info(f"Filled {filled} zero fingerprints with correct fingerprint")
+
+ return filled
+
+
+ def _fill_zero_fingerprint_scope(self, scope: Union[InputScope, OutputScope]):
+ """Helper function to fill zero fingerprints in a scope (input/output)"""
+ filled = 0
+ correct_fingerprint = self.root.child(0).fingerprint
+
+ # Helper function to check and fix fingerprint
+ def fix_fingerprint(pub, derivation):
+ if derivation.fingerprint != b"\x00\x00\x00\x00":
+ return False
+ try:
+ derived_key = self.root.derive(derivation.derivation)
+ if derived_key.key.sec() == pub.sec():
+ return DerivationPath(correct_fingerprint, derivation.derivation)
+ except Exception:
+ pass
+ return False
+
+ # Handle regular BIP32 derivations
+ for pub, derivation in list(scope.bip32_derivations.items()):
+ new_derivation = fix_fingerprint(pub, derivation)
+ if new_derivation:
+ scope.bip32_derivations[pub] = new_derivation
+ filled += 1
+
+ # Handle Taproot derivations
+ for pub, (leaf_hashes, derivation) in list(scope.taproot_bip32_derivations.items()):
+ new_derivation = fix_fingerprint(pub, derivation)
+ if new_derivation:
+ scope.taproot_bip32_derivations[pub] = (leaf_hashes, new_derivation)
+ filled += 1
+
+ return filled
Why this scored 32/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.