diff --git a/src/script/sign.cpp b/src/script/sign.cpp index c4d59de46d5..efee5a35fe0 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -318,14 +318,15 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda } // Get the BIP32 derivation tweaks CExtPubKey extpub = CreateMuSig2SyntheticXpub(agg_pub); - for (const int i : agg_info.path) { + for (const uint32_t i : agg_info.path) { + if (i >> 31) return false; // Hardened derivation is not possible from a public key auto& [t, xonly] = tweaks.emplace_back(); xonly = false; if (!extpub.Derive(extpub, i, &t)) { return false; } } - Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey); + if (XOnlyPubKey(extpub.pubkey) != script_pubkey) continue; plain_pub = extpub.pubkey; } diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 445a44f3ef1..d8c60fe61a3 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -13,6 +13,7 @@ from test_framework.blocktools import ( MAX_STANDARD_TX_WEIGHT, ) from test_framework.descriptors import descsum_create +from test_framework.extendedkey import hardened from test_framework.key import H_POINT from test_framework.messages import ( COutPoint, @@ -40,6 +41,8 @@ from test_framework.psbt import ( PSBT_IN_MUSIG2_PUB_NONCE, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_PROPRIETARY, + PSBT_IN_TAP_BIP32_DERIVATION, + PSBT_IN_TAP_INTERNAL_KEY, PSBT_IN_WITNESS_UTXO, PSBT_IN_FINAL_SCRIPTWITNESS, PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS, @@ -47,8 +50,8 @@ from test_framework.psbt import ( PSBT_OUT_TAP_TREE, PSBT_OUT_SCRIPT, ) -from test_framework.script import CScript, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY -from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE +from test_framework.script import CScript, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY, hash160 +from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE, output_key_to_p2tr_script from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_not_equal, @@ -302,6 +305,25 @@ class PSBTTest(BitcoinTestFramework): assert "participant_pubkeys" in out_participant_pks assert_equal(out_participant_pks["participant_pubkeys"], [out_pubkey1.hex(), out_pubkey2.hex()]) + def test_musig2_untrusted_derivation(self): + self.log.info("Test MuSig2 aggregate derivation from untrusted PSBT fields") + node = self.nodes[0] + + script_pubkey = bytes.fromhex(H_POINT) + _, aggregate_pubkey = generate_keypair() + _, participant_pubkey = generate_keypair() + + # Both have a matching aggregate fingerprint but cannot derive the script pubkey: 0 derives a different key, hardened(0) cannot be derived at all + for index in [0, hardened(0)]: + psbt = self.create_psbt(inputs={ + PSBT_IN_WITNESS_UTXO: CTxOut(nValue=1, scriptPubKey=output_key_to_p2tr_script(script_pubkey)).serialize(), + bytes([PSBT_IN_TAP_BIP32_DERIVATION]) + script_pubkey: ser_compact_size(0) + hash160(aggregate_pubkey)[:4] + index.to_bytes(4, "little"), + PSBT_IN_TAP_INTERNAL_KEY: script_pubkey, + bytes([PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS]) + aggregate_pubkey: [participant_pubkey], + }).to_base64() + assert_equal(node.analyzepsbt(psbt)["inputs"][0]["is_final"], False) + assert_equal(node.finalizepsbt(psbt)["complete"], False) + def test_combinepsbt_preserves_proprietary_fields(self): self.log.info("Test that combining PSBTs preserves proprietary fields") @@ -1500,6 +1522,7 @@ class PSBTTest(BitcoinTestFramework): self.test_psbt_roundtrip() self.test_psbt_version() self.test_psbt_with_invalid_signature() + self.test_musig2_untrusted_derivation() if __name__ == '__main__': PSBTTest(__file__).main()