fix(signing): reject BIP86 change output with a non-taproot script type - #333
Merged
Conversation
isCrossAccountSegwitChangeForbidden() enforced purpose/script-type agreement for BIP44, BIP49 and BIP84 change paths, but purpose 86' was never added when taproot support landed. A change output at m/86'/.../1/i declaring PAYTOADDRESS therefore skipped both mixed-mode guards and fell through to the generic path check in check_change_bip32_path(), which accepted it: same length, same account prefix, change chain, in-range index. The output was consequently treated as change, so its confirmation screen was suppressed, while transaction.c serialized it as P2PKH. A malicious host could route the entire change amount into a script that no BIP86 wallet scans for. The funds stay under the device's seed, but they are invisible to normal recovery until the path is derived by hand, and nothing was shown on the display. The reverse direction (44'/49'/84' claiming PAYTOTAPROOT) was already covered by the existing arms; this closes the one-sided hole. Rejecting here does not fail the signing session -- the output simply stops being treated as change and gets confirmed on screen like any other recipient. unittests/firmware/signing.cpp pins both directions. Verified failing without the new arm (make xunit -> Error 2) and passing with it.
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.
Problem
isCrossAccountSegwitChangeForbidden()inlib/firmware/signing.cenforces that a change output's declared script type agrees with the BIP32 purpose it is derived under. It has arms for44',49'and84'— purpose86'was never added when taproot support landed.So a change output at
m/86'/0'/0'/1/0declaringPAYTOADDRESS:isCrossAccountSegwitChangeForbidden()— no86'arm →falseisCrossAccountSegwitChangeAllowed()—86'is not in{44,49,84}→falsecheck_change_bip32_path()— same path length, same account prefix, change chain1, in-range index →trueis_change_output_script_type()acceptsPAYTOADDRESS, so the output is treated as change: its confirmation screen is suppressed, whiletransaction.cserializes it as P2PKH.Impact
A malicious host can route the entire change amount into a P2PKH script derived under a BIP86 path, with nothing shown on the display. The funds remain under the device's seed, but no standard wallet scans P2PKH under
m/86', so they are effectively unrecoverable without hand-deriving that path. Not theft — silent, non-discoverable change.Reachable in practice on
release/7.15.0-rc23, which has the taproot input side (#330).develophas the same latent hole in the predicate but noSPENDTAPROOTyet, so this lands ahead of the merge-up rather than behind it.Fix
One arm, mirroring the three that already exist:
Rejecting here does not fail the signing session — the output simply stops being treated as change and gets confirmed on screen like any other recipient. The reverse direction (
44'/49'/84'claimingPAYTOTAPROOT) was already covered by the existing arms; this closes the one-sided hole.isCrossAccountSegwitChangeForbiddenlosesstaticand gains a prototype insigning.hso the predicate can be unit-tested directly — it is pure, touching neithercoinnor the signing statics.Deliberately not changed
isCrossAccountSegwitChangeAllowed()still omits86'. Mixing an84'input with86'change currently fails the account-prefixmemcmpand gets confirmed on screen — conservative and correct. Widening that set is a feature, not part of this fix.Verification
unittests/firmware/signing.cpppins both directions of the rule.391 tests from 33 test cases ran. [ PASSED ] 391 tests.make xunitaborts withError 2. The test is not vacuous.Also re-run on
release/7.15.0-rc23with the same patch:392 tests ... [ PASSED ] 392, no regressions.Gate 3: no OLED change to capture in the fixed direction — the fix adds a standard output-confirmation screen for a case that previously rendered nothing. Happy to capture the before/after frames if you want it on the record.