Derive the PSBT approval screen from the PSBT, not the requesting page - #38
Open
Jossec101 wants to merge 1 commit into
Open
Derive the PSBT approval screen from the PSBT, not the requesting page#38Jossec101 wants to merge 1 commit into
Jossec101 wants to merge 1 commit into
Conversation
The approval screen showed "Operation Type" and "Amount" scraped by the content script from the DOM of the page asking for the signature, while the PSBT's own destinations and amounts were never displayed. Two transactions paying different recipients rendered identically, so the screen offered no verification value. - PSBTDetails now carries every output (destination + amount) and the network, and both approval screens render only those PSBT-derived rows via PSBTDetails::display_fields. - content.ts no longer reads #request-type or #channel-amount; the page can supply nothing but the PSBT itself. - An undecodable PSBT yields ApprovalScreen::Refuse and no Sign button, replacing unwrap_or_default() which rendered a blank screen while leaving Sign enabled and signing the raw string anyway. - An uncomputable fee and an overflowing total render as "unknown" rather than 0. - The fee is computed locally with checked arithmetic instead of bdk's PsbtUtils::fee_amount, which panics on crafted PSBTs (outputs above inputs, out-of-range non_witness_utxo vout); in wasm that panic traps mid-render and would bypass the refusal screen. - The network the addresses were rendered for is stated on screen, since scripts carry no network and the setting defaults to mainnet. Regression tests cover recipient substitution, multi-output PSBTs, non-standard scripts, unknown fees, the crafted-amount panic cases, and that the content script hands over the PSBT and nothing else. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
ApprovePSBTdisplayed four fields. OnlyTx IdandFeecame from the PSBT —Operation TypeandAmountwere scraped by the content script from#request-typeand#channel-amounton the page requesting the signature:Meanwhile
PSBTDetailscarried onlytx_idandfee, so no destination address, per-output amount, or total was ever derived from the PSBT and shown. The two fields a user actually reads to decide were authored by the party asking for the signature, and a transaction paying a substituted recipient rendered byte-for-byte identically to an honest one — the only difference being an opaque txid the extension never cross-references.Two further display failures understated risk:
PSBTDetails::from_str(&psbt).unwrap_or_default()rendered an empty txid andFee: 0when the PSBT failed to parse for display, while the Sign button stayed enabled and signed the raw string anyway; andfee_amount().unwrap_or_default()showedFee: 0 SATSwhenever the fee was uncomputable.The fix
Everything on the approval screen now derives from the PSBT.
PSBTDetailscarriesoutputs: Vec<PSBTOutput>(destination + amount),fee: Option<u64>, and thenetwork.display_fields()produces the exact rows the UI draws, so the component and the tests cannot drift apart.content.tsno longer reads#request-typeor#channel-amount.findPSBTreturns{psbt}and nothing else, andOperationRequestDatalost both fields — the page has no channel onto the approval screen.ApprovalScreen::for_psbtreturnsRefuse { reason }for an undecodable PSBT;ApprovePSBTrenders the reason with only a Go back button, andApprovePastedPSBTkeeps Sign disabled. The screen decision lives insignerso it is unit-testable at the layer where the original defect lived.unknown, never0.Two things worth flagging beyond the original finding, both found while reviewing this change:
bdk::psbt::PsbtUtils::fee_amount, which is not panic-free: it does.expect("input amount must be greater than output amount")and indexesnon_witness_utxo.output[vout]unchecked, both driven entirely by attacker-supplied PSBT fields. In wasm a panic traps mid-render, which would have taken out the refusal screen this PR adds. Reproduced both cases; they are covered byt7.SettingsStorage::get_network()falls back toNetwork::Bitcoinwhen the setting is unset, so a regtest PSBT would renderbc1q…addresses matching nothing NodeGuard shows. Stating the network makes the mismatch visible instead of silently training users to click through it. The underlying mainnet default is a separate issue and is untouched here.Verification
signer/tests/ngc01_blind_confirmation.rs— 7 tests, run withcargo test --test ngc01_blind_confirmation -- --nocapture:t1t2t3t4witness_utxoshowsunknown, not0t5t6t7unknowninstead of panicking the parseextension/tests/ngc01_content_script_poc.mjscompiles and executes the realcontent.tsagainst a hostile DOM and assertsfindPSBTreturns the PSBT and nothing else. Run withnode extension/tests/ngc01_content_script_poc.mjs.Also passing: full
cargo testinsigner(36 tests),cargo clippy --target wasm32-unknown-unknownclean forextension,tsc --noEmitclean.Notes for review
t2's output shows Total and Fee are identical between the honest and substituted PSBT — that is expected and is the point: only the destination distinguishes them, which is precisely what used to be invisible.extension/dist/andextension/pkg/are stale build outputs still containing the pre-fix content script. Runjust build-extensionbeforejust sign-extension.SIGHASHscope and the auto-submit behaviour inpastePSBTare separate concerns and are unchanged.🤖 Generated with Claude Code