wallet: don't abort on crafted MuSig2 PSBT inputs #35154

pull ESultanik wants to merge 1 commits into bitcoin:master from trail-of-forks:security/fix-signmusig2-psbt-assert changing 1 files +6 −1
  1. ESultanik commented at 8:35 PM on April 24, 2026: none

    Summary

    SignMuSig2() in src/script/sign.cpp derives an attacker-supplied aggregate pubkey along an attacker-supplied BIP32 path (both from untrusted PSBT fields PSBT_IN_TAP_BIP32_DERIVATION and PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS) and then Assert()s the result matches the script pubkey. A mismatched derivation or a hardened index aborts bitcoind via finalizepsbt, analyzepsbt, or descriptorprocesspsbt, with no wallet loaded.

    Two crash vectors:

    • A hardened index (bit 31 set) in the path hits assert((nChild >> 31) == 0) in CPubKey::Derive (pubkey.cpp).
    • A non-hardened path that derives to the wrong pubkey hits Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey) at sign.cpp:313.

    Assert() uses std::abort() in all builds (NDEBUG is #error'd in check.h), so this terminates the process.

    Impact

    • Attack surface: authenticated RPC only; not P2P-reachable.
    • No wallet required: finalizepsbt, analyzepsbt, and descriptorprocesspsbt all reach the vulnerable code via DUMMY_SIGNING_PROVIDER. analyzepsbt makes even a read-only inspection a crash trigger.
    • Gate is trivial: the only precondition is a 4-byte fingerprint match between two attacker-supplied PSBT fields.
    • Affected versions: v31.0rc1, v31.0rc2, and master. Not present in any GA release. Introduced in commit 4a273edda0 (PR #29675). This is release-blocking for v31.0 GA.
    • Severity: DoS — instant node abort. No memory corruption, no key or fund compromise.

    Reproduction

    On master (2d5ab09f0d at the time of testing):

    bitcoind -regtest -disablewallet -daemon
    # Variant A — non-hardened path, hits sign.cpp:313 Assert
    bitcoin-cli -regtest finalizepsbt "cHNidP8BAF4CAAAAAaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqAAAAAAAAAAAAAbiCAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5gAAAAAAAEBK6CGAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5ghFnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYCQAGr9RrAAAAAAEXIHm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYIhoCxgR/lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuUhAvkwigGSWMMQSTRPhfidUim1MchFg2+ZsIYB8RO84Db5AAA="
    # Variant B — hardened index m/0', hits pubkey.cpp:343 assert
    bitcoin-cli -regtest finalizepsbt "cHNidP8BAF4CAAAAAaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqAAAAAAAAAAAAAbiCAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5gAAAAAAAEBK6CGAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5ghFnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYCQAGr9RrAAAAgAEXIHm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYIhoCxgR/lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuUhAvkwigGSWMMQSTRPhfidUim1MchFg2+ZsIYB8RO84Db5AAA="
    

    Both PSBTs pass decodepsbt cleanly and would not look suspicious in a MuSig2 / coinjoin workflow. bitcoind aborts on each call; bitcoin-cli reports "EOF reached". analyzepsbt with the same PSBTs also aborts.

    Fix

    Two small changes in SignMuSig2() at src/script/sign.cpp:

    1. Reject hardened derivation indices before calling CPubKey::Derive. Public-key derivation only supports non-hardened steps; a hardened index is attacker-controlled input that can never produce a valid derivation, so skipping the aggregate is correct.
    2. Replace the fatal Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey) with if (...) continue;. A pubkey mismatch means this aggregate is not the one that produced script_pubkey. The correct behavior is to move on to the next candidate, not to abort the process.

    The diff is 6 added / 1 removed lines.

    Credit

    Found by Claude during an automated review conducted by Anthropic; manually validated and patched by Trail of Bits. Reference: ANT-2026-05771.

  2. script: fix reachable Assert() abort in SignMuSig2 via crafted PSBT
    Attacker-controlled PSBT fields (PSBT_IN_TAP_BIP32_DERIVATION path and
    PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS) flow through a trivially-satisfied
    4-byte fingerprint check into CPubKey::Derive() and a fatal Assert().
    
    Two crash vectors exist:
    - A hardened derivation index (bit 31 set) in the path hits
      assert((nChild >> 31) == 0) in CPubKey::Derive (pubkey.cpp).
    - A mismatched (but unhardened) path derives to the wrong pubkey,
      hitting Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey)
      in SignMuSig2 (sign.cpp).
    
    Both are reachable from any PSBT-processing RPC (finalizepsbt,
    analyzepsbt, descriptorprocesspsbt) without a wallet.
    
    Fix: reject hardened indices before entering the derivation loop,
    and replace the fatal Assert with a graceful continue that skips
    the mismatched aggregate pubkey.
    6904dba4f5
  3. DrahtBot commented at 8:35 PM on April 24, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35011 (ci, iwyu: Fix warnings in src/script and treat them as errors by BrandonOdiwuor)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. ESultanik renamed this:
    Don't abort on crafted MuSig2 PSBT inputs
    cli: don't abort on crafted MuSig2 PSBT inputs
    on Apr 24, 2026
  5. DrahtBot added the label Scripts and tools on Apr 24, 2026
  6. ESultanik renamed this:
    cli: don't abort on crafted MuSig2 PSBT inputs
    wallet: don't abort on crafted MuSig2 PSBT inputs
    on Apr 24, 2026
  7. achow101 commented at 8:48 PM on April 24, 2026: member

    This is release-blocking for v31.0 GA.

    The release is already out, and this would not be a blocker for it.

    Stop calling RPC crashes vulnerabilities.

  8. in src/script/sign.cpp:306 in 6904dba4f5
     301 | @@ -301,6 +302,10 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     302 |              if (!std::equal(agg_info.fingerprint, agg_info.fingerprint + sizeof(agg_info.fingerprint), keyid.data())) {
     303 |                  continue;
     304 |              }
     305 | +            // Reject hardened derivation indices (public key derivation only supports unhardened)
     306 | +            if (std::any_of(agg_info.path.begin(), agg_info.path.end(), [](uint32_t idx) { return idx >> 31; })) {
    


    achow101 commented at 8:53 PM on April 24, 2026:

    Instead of looping through the path again, we can do this check below prior to derivation.

  9. Sjors commented at 4:14 PM on April 28, 2026: member

    Stop calling RPC crashes vulnerabilities.

    It would be nice if a PSBT co-signer service doesn't need to restart their node any time a PSBT from an untrusted source does something unexpected. Perhaps we can add fuzz coverage to enforce that CHECK_NONFATAL is hit, rather than Assert.

  10. achow101 commented at 10:22 AM on May 7, 2026: member

    Thanks, but LLM output is not accepted if the author can not properly explain, test or could have written the change themselves.

    The bottleneck in this project has always been review and testing, not writing code. Development here is intentionally conservative and slow, and reviewer attention is the scarcest resource we have. LLMs have made this worse, anyone can now prompt them and post their output as PRs. There is an infinite amount plausible looking "improvements" for LLMs to suggest and work on.

    Unless we fully trust LLMs to both write and review code, humans still have to spend time understanding the proposed changes, which incurs a non-zero cost for every opened PR.

    I understand that contributing to this project can be intimidating, and using LLMs may seem tempting, but it really creates more issues for this project than it solves. The best way to help this project, is to review and test changes. You can use LLMs for this, but you shouldn't solely rely on them, or just post their output.

    Please reconsider whether it's something you genuinely think the project should pursue, independent of what your LLM suggested.

    I'll close this for now.

  11. achow101 closed this on May 7, 2026

  12. ESultanik commented at 2:22 PM on May 7, 2026: none

    Nowhere in the PR, other than the vestigial branch name, did we claim that this is a security vulnerability. As was stated in the PR, the bug was discovered by Anthropic, but was subsequently manually patched and validated by engineers at Trail of Bits. Would you rather us create a GitHub issue to track the bug?

  13. maflcko commented at 5:34 AM on May 9, 2026: member

    I've manually converted the two inputs to the fuzz input format and uploaded this to OSS-Fuzz for fun:

  14. maflcko removed the label Scripts and tools on May 9, 2026
  15. DrahtBot added the label Wallet on May 9, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-05-20 06:52 UTC