external_signer: validate fingerprint from enumerate response
What changed, and why it matters
This commit adds a safety check in Bitcoin Core when it talks to an external hardware wallet or signer tool. Previously, the software accepted whatever 'fingerprint' string the external tool returned. Now it requires that fingerprint to be exactly 8 hexadecimal characters. Without this check, a misbehaving or malicious signer could supply an oddly formatted fingerprint that might later confuse wallet logic, cause crashes, or be used to trick the user into pairing with the wrong device. The change is small and defensive: it rejects bad fingerprints early and throws a clear error.
Treat as a low-to-moderate hardening fix. Review whether the fingerprint is used anywhere else without similar validation, and consider whether other fields from the signer response (type, model, path) need comparable validation. No urgent action required unless external signer workflows are exposed to untrusted signer binaries.
Security signals we found
Input validation added to externally supplied JSON field
Missing length and format check on signer fingerprint before use
Functional tests added for malformed fingerprint rejection
Potential for downstream logic confusion or misuse if invalid fingerprint propagated
Evidence from the diff
In src/external_signer.cpp, ExternalSigner::Enumerate now validates the ‘fingerprint’ field parsed from a signer tool’s JSON response. The new check ensures fingerprintStr.size() == 8 and IsHex(fingerprintStr) is true; otherwise a std::runtime_error is thrown. A functional test in test/functional/rpc_signer.py exercises rejection of empty, short, long, and non-hex fingerprints. The patch is input validation only and does not change how valid fingerprints are used.
Changed components
src/external_signer.cppExternalSigner::Enumeratetest/functional/rpc_signer.pyInspect captured patch +9 / −0
diff --git a/src/external_signer.cpp b/src/external_signer.cpp
index da75de77..cb49cedc 100644
--- a/src/external_signer.cpp
+++ b/src/external_signer.cpp
@@ -48,6 +48,9 @@ bool ExternalSigner::Enumerate(const std::string& command, std::vector<ExternalS
throw std::runtime_error(strprintf("'%s' received invalid response, missing signer fingerprint", command));
}
const std::string& fingerprintStr{fingerprint.get_str()};
+ if (fingerprintStr.size() != 8 || !IsHex(fingerprintStr)) {
+ throw std::runtime_error(strprintf("'%s' received invalid fingerprint, must be 8 hex characters", command));
+ }
// Skip duplicate signer
bool duplicate = false;
for (const ExternalSigner& signer : signers) {
diff --git a/test/functional/rpc_signer.py b/test/functional/rpc_signer.py
index 51a01249..6d51182c 100755
--- a/test/functional/rpc_signer.py
+++ b/test/functional/rpc_signer.py
@@ -85,6 +85,12 @@ class RPCSignerTest(BitcoinTestFramework):
]})
self.clear_mock_result(self.nodes[1])
+ # Invalid fingerprints are rejected
+ for fingerprint in ["", "0000001", "000000001", "0000000g", "zzzzzzzz"]:
+ self.set_mock_result(self.nodes[1], '0 [{"type": "trezor", "model": "trezor_t", "fingerprint": "%s"}]' % fingerprint)
+ assert_raises_rpc_error(-1, 'invalid fingerprint', self.nodes[1].enumeratesigners)
+ self.clear_mock_result(self.nodes[1])
+
assert_equal({'fingerprint': '00000001', 'name': 'trezor_t'} in self.nodes[1].enumeratesigners()['signers'], True)
if __name__ == '__main__':
Why this scored 47/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.