#36114 wallet: harden external signer psbt processing, revamp mock
| Reviewability | Reviews | Agreement | Size | ||||
|---|---|---|---|---|---|---|---|
| #36114 wallet: harden external signer psbt processing, revamp mock | Ready | 1 | Strong | 66 + 328 tests | |||
|
|
|
|
| |||
Reviewability: ReadyStalePaused Agreement: StrongPositivePositive w/ caveatsNeutralMildDisputedBlockedCrickets Reviews: current code-review ACKs, then (+stale ACKs) and -NACKs; greener = more ACKs. Size: lines added or modified outside tests, then in tests; greener = smaller. | |||||||
Goal
Hardens external signer PSBT handling in the wallet by merging the returned PSBT into the original instead of replacing it, enforcing matching PSBT versions, rejecting finalized inputs, and rejecting signatures that use unsafe sighash types (SIGHASH_NONE or SIGHASH_SINGLE). It also revamps the functional test external signer mock to use an offline descriptor wallet capable of simulating misbehaving or stripped signer responses, and enforces include-what-you-use (IWYU) on external signer code.
Problem: Bitcoin Core previously replaced its PSBT wholesale with whatever an external signer returned, trusting hardware devices or intermediate bridge software not to tamper with outputs, sequence numbers, or sighash flags. A compromised or buggy signer could finalize inputs, divert funds to fees, alter outputs, or use unsafe sighash types that allow third parties to modify outputs. Furthermore, the functional test framework's mock signer was static and could only replay hardcoded PSBTs, preventing automated testing of malicious signer behavior.
Evidence of importance:
- Commit 2b801fc384: 'Previously the PSBT returned by the external signer replaced the original wholesale, trusting the signer not to modify the transaction. Merge it instead. This rejects a response that describes a different transaction.'
- Commit 4110fbd057: 'A signature with SIGHASH_NONE or SIGHASH_SINGLE doesn't commit to all outputs, letting anyone alter them after signing. Reject a PSBT from an external signer that declares such a sighash type or contains signatures made with one.'
- Description: 'This PR absorbs the scenarios from #35358, but is better able to test them thanks to the new mock signer implementation.'
Scope: Most of the diff is in Python functional test harnesses (327 lines added across mock and functional tests); C++ changes in src/external_signer.cpp are about 60 lines.
Risk of a mistake: medium (wallet, tests). A flaw in sighash inspection or PSBT merging could cause valid hardware wallet responses to be rejected, breaking external signer transactions for users.
Changed since the previous assessment: Added IWYU enforcement for src/external_signer.cpp in CI and cleaned up headers.
Reviewability
Ready
Nothing known that would invalidate a review now.
Waiting on (from the thread read):
- nothing: all suggestions and objections addressed, fresh Tested ACK received 2026-10-25
Agreement
Strong
substantive support, no open objection (jeanpablojp, fametrano, fametrano, fametrano)
Thread notes: Sjors added the two test cases suggested by fametrano, who confirmed with a Tested ACK after testing each failure case.
Objections, suggestions, and questions:
| Reviewer | Type | Harm, request, or question | Status | Must be | Author replied | Quote |
|---|---|---|---|---|---|---|
| jeanpablojp (contributor) | objection, safety | Finalized PSBT responses clear input fields under BIP 174, allowing signatures with unsafe sighash types like SIGHASH_NONE to bypass FindUnsafeSighashType undetected. | fixed · fix pushed · author updated the code to reject finalized PSBTs and added a test case | changed | yes | 2026-09-08: 'I made the new mock return a finalized response and `FindUnsafeSighashType` doesn't catch it.' Status from: 2026-09-28: Sjors: 'It now rejects finalized PSBTs, which avoids complicating `FindUnsafeSighashType`.' |
| jeanpablojp (contributor) | question, interface | Whether returning a v0 PSBT should have a separate error message or whether the external signer must return the matching PSBT version. | answered · author replied that a constraint was added requiring matching versions | answered | yes | 2026-09-08: 'Worth a separate message, or does the signer have to return the version it was given?' Status from: 2026-09-28: Sjors: 'I added a constraint that the version must match.' |
| jeanpablojp (contributor) | suggestion, maintenance | Add single-input test transactions for ECDSA and taproot so each FindUnsafeSighashType branch is exercised independently. | fixed · fix pushed · author added a test case | answered | yes | 2026-09-08: 'A single-input transaction of each type would make each branch carry its own case. Worth adding?' Status from: 2026-09-28: Sjors: 'Added a test case.' |
| fametrano (contributor) | suggestion, maintenance | Add tests pinning behavior for a changed v2 input sequence and a corrupted signature | accepted · fix pushed · author added the requested test cases and reviewer confirmed they pass and fail when protections are disabled | answered | yes | 2026-10-24: 'the new tests cover the cases in my tACK except two: a changed v2 input sequence and a corrupted signature. In both the wallet's transaction stays as it was and `walletprocesspsbt` returns an incomple' Status from: 2026-10-25: fametrano: 'The two new cases are the ones I suggested, and each fails when its protection is removed.' |
Support:
- jeanpablojp (contributor) (Concept ACK, 2026-09-08) [read]: supports hardening external signer handling and testing mock changes
- fametrano (contributor) (Tested ACK, 2026-10-21) [tested]: tested head against combiners, confirming external signer rejects unsafe sighashes and finalized inputs
- fametrano (contributor) (Tested ACK, 2026-10-24) [tested]: re-ran probe on rebased head and verified wallet_signer.py passes
- fametrano (contributor) (Tested ACK, 2026-10-25) [both: wallet_signer.py, ProduceSignature, CWallet::FillPSBT]: verified the two new test cases fail when protections are removed and wallet_signer.py passes
Participants:
- dangervslash (none, neutral, 2 statements 2026-08-28 to 2026-08-28): left non-substantive comments
- jeanpablojp (contributor, objection, 3 statements 2026-09-08 to 2026-09-08): Concept ACK, but pointed out finalized PSBT responses bypass FindUnsafeSighashType (fixed)
- fametrano (contributor, support, 3 statements 2026-10-21 to 2026-10-25): Tested ACK; suggested two test cases for changed sequence and corrupted signature, then verified them after author added them
Files
394 lines under test/bench/ci.
- test/functional/wallet_signer.py +185/-55
- test/functional/mocks/signer.py +142/-10
- src/external_signer.cpp +62/-2
- doc/external-signer.md +4/-0
- ci/test/03_test_script.sh +1/-1
- src/external_signer.h +0/-1
Card
The short text the ranking pass compares across PRs.
Bitcoin Core previously trusted external signers completely when signing transactions, replacing the wallet's PSBT wholesale with whatever the signer returned. This PR hardens external signer processing by merging the response into the original PSBT, verifying matching PSBT versions, rejecting finalized inputs, and rejecting signatures with unsafe sighash types like SIGHASH_NONE or SIGHASH_SINGLE. It also revamps the functional test mock signer into a dynamic offline descriptor wallet to test tampered and misbehaving signers, and enforces IWYU for the external signer module.
Data
thread read through 2026-10-25 (16 statements), head 42298793aa
labels: Wallet
Categories
Wallet
P1 · fund safety
Core previously trusted external signer responses wholesale, leaving users vulnerable to output tampering, fee diversion, or unsafe sighash flags by buggy signers or compromised intermediate bridge software. Merging PSBTs instead of replacing them and rejecting unsafe signatures protects against unauthorized output alterations and theft.
Membership: Carries the Wallet label and modifies external signer PSBT handling in src/external_signer.cpp and test/functional/wallet_signer.py.
Factors: security/stability 2, bug 2, performance 0, user value 1, leverage 1