diff --git a/README.md b/README.md index 5decb7ee..e35b4647 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ generic message signing and verification. ## Types of Signatures At the moment this crate supports `P2TR`, `P2WPKH` and `P2SH-P2WPKH` single-sig +addresses, as well as `P2WSH`, `P2SH-P2WSH` and `P2SH` CHECKMULTISIG multisig addresses. Feedback through issues or PRs on the interface design and security is welcome and encouraged. diff --git a/examples/simple_sign_verify_encoded.rs b/examples/simple_sign_verify_encoded.rs index c1c0bec0..e37267cf 100644 --- a/examples/simple_sign_verify_encoded.rs +++ b/examples/simple_sign_verify_encoded.rs @@ -5,7 +5,7 @@ fn main() { let message = "Hello World"; let wif_private_key = "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k"; - let base64_signature = sign_simple_encoded(address, message, wif_private_key).unwrap(); + let base64_signature = sign_simple_encoded(address, message, &[wif_private_key], None).unwrap(); assert!(verify_simple_encoded(address, message, &base64_signature).is_ok()); } diff --git a/src/error.rs b/src/error.rs index 8ac1a5e2..bcb36ef7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,7 +10,7 @@ pub enum Error { }, #[snafu(display("Failed to parse private key"))] PrivateKeyParse { source: bitcoin::key::FromWifError }, - #[snafu(display("Unsuported address `{address}`, only P2TR, P2WPKH and P2SH-P2WPKH allowed"))] + #[snafu(display("Unsupported address `{address}`, only P2TR, P2WPKH, P2SH-P2WPKH, and P2WSH/P2SH multisig allowed"))] UnsupportedAddress { address: String }, #[snafu(display("Decode error for signature `{signature}`"))] SignatureDecode { @@ -66,4 +66,24 @@ pub enum Error { InvalidWitness, #[snafu(display("Public key does not match"))] PublicKeyMismatch, + #[snafu(display("At least one private key is required"))] + NoPrivateKeys, + #[snafu(display("Non-standard sighash type: {source}"))] + SigHashTypeNonStandard { + source: bitcoin::sighash::NonStandardSighashTypeError, + }, + #[snafu(display("Signer's public key not present in multisig script"))] + UnknownSigner, + #[snafu(display("Duplicate private key provided"))] + DuplicateSigner, + #[snafu(display("Expected exactly {required} private keys, got {provided}"))] + SignatureCount { required: usize, provided: usize }, + #[snafu(display("P2WPKH requires a compressed public key"))] + UncompressedPublicKey { + source: bitcoin::key::UncompressedPublicKeyError, + }, + #[snafu(display("Failed to parse witness script"))] + WitnessScriptParse { + source: bitcoin::hex::HexToBytesError, + }, } diff --git a/src/lib.rs b/src/lib.rs index 607205be..1e2fe11b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use { key::{Keypair, TapTweak}, opcodes, psbt::Psbt, - script::PushBytes, + script::{Instruction, PushBytes}, secp256k1::{self, schnorr::Signature, Message, Secp256k1, XOnlyPublicKey}, sighash::{self, SighashCache, TapSighashType}, transaction::Version, @@ -37,6 +37,8 @@ mod tests { // From https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#test-vectors // and https://github.com/ACken2/bip322-js/blob/main/test/Verifier.test.ts // and https://github.com/bitcoin/bitcoin/blob/29b28d07fa958b89e1c7916fda5d8654474cf495/src/test/util_tests.cpp#L2747 + // and https://github.com/bitcoin/bips/blob/master/bip-0322/generated-test-vectors.json + // (with the smp/ful signature variant prefixes stripped) const WIF_PRIVATE_KEY: &str = "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k"; const SEGWIT_ADDRESS: &str = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l"; @@ -47,6 +49,32 @@ mod tests { "KwTbAxmBXjoZM3bzbXixEr9nxLhyYSM4vp2swet58i19bw9sqk5z"; const NESTED_SEGWIT_ADDRESS: &str = "3HSVzEhCFuH9Z3wvoWTexy7BMVVp3PjS6f"; + const P2WSH_2OF2_ADDRESS: &str = "bc1qg8r3cl47rrr75dwvr7jhzdukptegnmq8v0nmjd2jdn4qvlczqkts0rqtav"; + const P2WSH_2OF2_WITNESS_SCRIPT: &str = + "52210244f7cb842a4ce4f352ce4062ae5e0a5d60d6faa0b07b62c2063484aa5297bbce210234eed6190efc47716b953a050b563f8b2b523addea955ae43351dd2a92aa49f452ae"; + const P2WSH_2OF2_PRIVATE_KEY_1: &str = "L14bn1tSDZUKYLLiTConCRHbqzGef8eqB2tU5PBPFBkyPLUyob7V"; + const P2WSH_2OF2_PRIVATE_KEY_2: &str = "KyJnWYygb7P2P8khWyDMW9yFGA3dUe7kpkEHtLbzY6cfvvn9T5CS"; + const P2WSH_2OF2_MESSAGE: &str = "QXYOWYWO7ZGJC4OPNC367HBUQF"; + + const P2SH_P2WSH_2OF2_ADDRESS: &str = "3PGZjFkYBL1m9WBWkWbCW5FEFTaS1Hj4EB"; + const P2SH_P2WSH_2OF2_WITNESS_SCRIPT: &str = + "522103fb824153fc000a213c5456d01780d1f292a0cfbfbc5f6f8f1dc713706c5519d12103db88ce9fb8081e50460beb37539741b0667d6f2439dd1ca283d63182421c10b152ae"; + const P2SH_P2WSH_2OF2_PRIVATE_KEY_1: &str = + "L246N8J5x5ehwjoz97ZfHXBCELxGcK2jqRFinReMBcRnqH1X4zdc"; + const P2SH_P2WSH_2OF2_PRIVATE_KEY_2: &str = + "L1WzdMN476EHhwsDLHJwVHZKrwVLFFsdvNoZFsZVk2Mb5rKst2Et"; + const P2SH_P2WSH_2OF2_MESSAGE: &str = "NQVRV3DJYLKBANM3OPTNBULEU3"; + + const UNCOMPRESSED_WIF_PRIVATE_KEY: &str = "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf"; + + const P2SH_MULTISIG_2OF2_ADDRESS: &str = "3Nye4j1GUFqCEBR3do2KEFZAs9oLe8NZ6X"; + const P2SH_MULTISIG_2OF2_REDEEM_SCRIPT: &str = + "52210384a8dc6ff3efd7fec062eb73397c2079cd3cc300ff57088f9b9f0b734c63a21821021c9c8e9c1d06e3f7de8ad05c01f5080b2b81292eb29c4cdbe14f43838256141652ae"; + const P2SH_MULTISIG_2OF2_PRIVATE_KEY_1: &str = + "L5Teubyzf4mFSMHGCzADK42oRi9xz45qhBrYx2Xs8uCY6WyrymT5"; + const P2SH_MULTISIG_2OF2_PRIVATE_KEY_2: &str = + "L4HsBh1Rb5DWP5Hf82tPw3whgwFyt8hdRTChxZQE4HzWfdbVgiWT"; + #[test] fn message_hashes_are_correct() { assert_eq!( @@ -137,7 +165,7 @@ mod tests { #[test] fn simple_sign_taproot() { assert_eq!( - sign::sign_simple_encoded(TAPROOT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap(), + sign::sign_simple_encoded(TAPROOT_ADDRESS, "Hello World", &[WIF_PRIVATE_KEY], None).unwrap(), "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" ); } @@ -147,7 +175,7 @@ mod tests { assert!(verify::verify_simple_encoded( TAPROOT_ADDRESS, "Hello World", - &sign::sign_simple_encoded(TAPROOT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap() + &sign::sign_simple_encoded(TAPROOT_ADDRESS, "Hello World", &[WIF_PRIVATE_KEY], None).unwrap() ) .is_ok()); } @@ -157,7 +185,7 @@ mod tests { assert!(verify::verify_full_encoded( TAPROOT_ADDRESS, "Hello World", - &sign::sign_full_encoded(TAPROOT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap() + &sign::sign_full_encoded(TAPROOT_ADDRESS, "Hello World", &[WIF_PRIVATE_KEY], None).unwrap() ) .is_ok()); } @@ -168,7 +196,7 @@ mod tests { LEGACY_ADDRESS, "", "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=").unwrap_err().to_string(), - format!("Unsuported address `{LEGACY_ADDRESS}`, only P2TR, P2WPKH and P2SH-P2WPKH allowed") + format!("Unsupported address `{LEGACY_ADDRESS}`, only P2TR, P2WPKH, P2SH-P2WPKH, and P2WSH/P2SH multisig allowed") ) } @@ -249,12 +277,12 @@ mod tests { #[test] fn simple_sign_p2wpkh() { assert_eq!( - sign::sign_simple_encoded(SEGWIT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap(), + sign::sign_simple_encoded(SEGWIT_ADDRESS, "Hello World", &[WIF_PRIVATE_KEY], None).unwrap(), "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy" ); assert_eq!( - sign::sign_simple_encoded(SEGWIT_ADDRESS, "", WIF_PRIVATE_KEY).unwrap(), + sign::sign_simple_encoded(SEGWIT_ADDRESS, "", &[WIF_PRIVATE_KEY], None).unwrap(), "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy" ); } @@ -264,7 +292,7 @@ mod tests { assert!(verify::verify_simple_encoded( SEGWIT_ADDRESS, "Hello World", - &sign::sign_simple_encoded(SEGWIT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap() + &sign::sign_simple_encoded(SEGWIT_ADDRESS, "Hello World", &[WIF_PRIVATE_KEY], None).unwrap() ) .is_ok()); } @@ -274,7 +302,7 @@ mod tests { assert!(verify::verify_full_encoded( SEGWIT_ADDRESS, "Hello World", - &sign::sign_full_encoded(SEGWIT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap() + &sign::sign_full_encoded(SEGWIT_ADDRESS, "Hello World", &[WIF_PRIVATE_KEY], None).unwrap() ) .is_ok()); } @@ -299,7 +327,7 @@ mod tests { #[test] fn simple_sign_p2sh_p2wpkh() { assert_eq!( - sign::sign_simple_encoded(NESTED_SEGWIT_ADDRESS, "Hello World", NESTED_SEGWIT_WIF_PRIVATE_KEY).unwrap(), + sign::sign_simple_encoded(NESTED_SEGWIT_ADDRESS, "Hello World", &[NESTED_SEGWIT_WIF_PRIVATE_KEY], None).unwrap(), "AkgwRQIhAMd2wZSY3x0V9Kr/NClochoTXcgDaGl3OObOR17yx3QQAiBVWxqNSS+CKen7bmJTG6YfJjsggQ4Fa2RHKgBKrdQQ+gEhAxa5UDdQCHSQHfKQv14ybcYm1C9y6b12xAuukWzSnS+w" ); } @@ -312,7 +340,8 @@ mod tests { &sign::sign_simple_encoded( NESTED_SEGWIT_ADDRESS, "Hello World", - NESTED_SEGWIT_WIF_PRIVATE_KEY + &[NESTED_SEGWIT_WIF_PRIVATE_KEY], + None ) .unwrap() ) @@ -327,7 +356,8 @@ mod tests { &sign::sign_full_encoded( NESTED_SEGWIT_ADDRESS, "Hello World", - NESTED_SEGWIT_WIF_PRIVATE_KEY + &[NESTED_SEGWIT_WIF_PRIVATE_KEY], + None ) .unwrap() ) @@ -346,8 +376,588 @@ mod tests { rand::rng().fill_bytes(&mut aux_rand); let witness = - create_message_signature_taproot(&to_spend, &to_sign, private_key, Some(aux_rand)); + create_message_signature_taproot(&to_spend, &to_sign, &private_key, Some(aux_rand)); assert!(verify_simple(&address, message, witness).is_ok()); } + + #[test] + fn roundtrip_p2wsh_2of2_simple() { + assert!(verify::verify_simple_encoded( + P2WSH_2OF2_ADDRESS, + P2WSH_2OF2_MESSAGE, + &sign::sign_simple_encoded( + P2WSH_2OF2_ADDRESS, + P2WSH_2OF2_MESSAGE, + &[P2WSH_2OF2_PRIVATE_KEY_1, P2WSH_2OF2_PRIVATE_KEY_2], + Some(P2WSH_2OF2_WITNESS_SCRIPT), + ) + .unwrap() + ) + .is_ok()); + } + + #[test] + fn roundtrip_p2sh_p2wsh_2of2_full() { + assert!(verify::verify_full_encoded( + P2SH_P2WSH_2OF2_ADDRESS, + P2SH_P2WSH_2OF2_MESSAGE, + &sign::sign_full_encoded( + P2SH_P2WSH_2OF2_ADDRESS, + P2SH_P2WSH_2OF2_MESSAGE, + &[P2SH_P2WSH_2OF2_PRIVATE_KEY_1, P2SH_P2WSH_2OF2_PRIVATE_KEY_2], + Some(P2SH_P2WSH_2OF2_WITNESS_SCRIPT), + ) + .unwrap() + ) + .is_ok()); + } + + #[test] + fn roundtrip_p2wsh_2of2_shuffled_keys() { + assert!(verify::verify_simple_encoded( + P2WSH_2OF2_ADDRESS, + P2WSH_2OF2_MESSAGE, + &sign::sign_simple_encoded( + P2WSH_2OF2_ADDRESS, + P2WSH_2OF2_MESSAGE, + &[P2WSH_2OF2_PRIVATE_KEY_2, P2WSH_2OF2_PRIVATE_KEY_1], + Some(P2WSH_2OF2_WITNESS_SCRIPT), + ) + .unwrap() + ) + .is_ok()); + } + + #[test] + fn roundtrip_p2sh_p2wsh_2of2_shuffled_keys() { + assert!(verify::verify_full_encoded( + P2SH_P2WSH_2OF2_ADDRESS, + P2SH_P2WSH_2OF2_MESSAGE, + &sign::sign_full_encoded( + P2SH_P2WSH_2OF2_ADDRESS, + P2SH_P2WSH_2OF2_MESSAGE, + &[P2SH_P2WSH_2OF2_PRIVATE_KEY_2, P2SH_P2WSH_2OF2_PRIVATE_KEY_1], + Some(P2SH_P2WSH_2OF2_WITNESS_SCRIPT), + ) + .unwrap() + ) + .is_ok()); + } + + #[test] + fn multisig_rejects_unknown_signer() { + assert!(matches!( + sign::sign_simple_encoded( + P2WSH_2OF2_ADDRESS, + P2WSH_2OF2_MESSAGE, + &[P2WSH_2OF2_PRIVATE_KEY_1, WIF_PRIVATE_KEY], + Some(P2WSH_2OF2_WITNESS_SCRIPT), + ), + Err(Error::UnknownSigner) + )); + } + + #[test] + fn multisig_rejects_mismatched_witness_script() { + assert!(matches!( + sign::sign_simple_encoded( + P2WSH_2OF2_ADDRESS, + "foo", + &[P2WSH_2OF2_PRIVATE_KEY_1, P2WSH_2OF2_PRIVATE_KEY_2], + Some(P2SH_P2WSH_2OF2_WITNESS_SCRIPT), + ), + Err(Error::UnsupportedAddress { .. }) + )); + } + + #[test] + fn single_sig_rejects_extra_keys() { + assert!(matches!( + sign::sign_simple_encoded( + SEGWIT_ADDRESS, + "foo", + &[WIF_PRIVATE_KEY, WIF_PRIVATE_KEY], + None + ), + Err(Error::SignatureCount { + required: 1, + provided: 2 + }) + )); + } + + #[test] + fn p2sh_rejects_uncompressed_key() { + assert!(matches!( + sign::sign_simple_encoded( + NESTED_SEGWIT_ADDRESS, + "foo", + &[UNCOMPRESSED_WIF_PRIVATE_KEY], + None + ), + Err(Error::UncompressedPublicKey { .. }) + )); + } + + #[test] + fn verify_full_rejects_noncanonical_to_sign() { + #[track_caller] + fn case(mutate: impl Fn(&mut Transaction)) { + let address = Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(); + + let mut to_sign = sign::sign_full( + &address, + "foo", + &[PrivateKey::from_wif(WIF_PRIVATE_KEY).unwrap()], + None, + ) + .unwrap(); + + mutate(&mut to_sign); + + assert!(matches!( + verify::verify_full(&address, "foo", to_sign), + Err(Error::ToSignInvalid) + )); + } + + case(|tx| tx.version = Version(3)); + case(|tx| { + let input = tx.input[0].clone(); + tx.input.push(input); + }); + case(|tx| { + let output = tx.output[0].clone(); + tx.output.push(output); + }); + case(|tx| tx.input[0].script_sig = ScriptBuf::from_hex("deadbeef").unwrap()); + case(|tx| tx.output[0].value = Amount::from_sat(1)); + case(|tx| tx.output[0].script_pubkey = ScriptBuf::new()); + } + + #[test] + fn verify_full_rejects_inputless_transaction() { + let to_sign = Transaction { + version: Version(0), + lock_time: LockTime::ZERO, + input: Vec::new(), + output: Vec::new(), + }; + + assert!(matches!( + verify::verify_full( + &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + "foo", + to_sign + ), + Err(Error::ToSignInvalid) + )); + } + + #[test] + fn official_vectors_simple() { + #[track_caller] + fn case(address: &str, message: &str, signature: &str) { + assert!(verify::verify_simple_encoded(address, message, signature).is_ok()); + } + + case("bc1qqthe0hz8klx90e7stf6shclhsvqd5ly96pn53v", "2V6TUTMSH4VQ3Z7WZWKYD7DFNH", "AkgwRQIhALC6hdfxNy1n45d7UXSskRBdfZW0Al259E1kDMpipdYkAiAJPfZqb+WurZuf1apU5xeE6Igui9dvt5tihQLDvxlY1AEhAqbnruyo677ktQjio7XOchO3w51Dh9AbRVngha5jtNfT"); + case( + "bc1pcquvhrqv0q68t4m0hfq6tpn006qrskyc7yrqnp2uyrf2emg3wynsdjyk38", + "PURVOQ544B6HUATVBJZN5EZJUU", + "AUB6B2Rbupzua8LTQIF06516wzl+cwKy1be8RgoiW0riyXdKwe6GTz/5Hnb37m67pJwIKCh+D5jDueG6KpvYpmu8", + ); + case("bc1qw6g0rgrpuxvj4edkwtvzpmt3c5m08mhp8nuk3mrk4erufvlczp5ssdscjd", "G7ZTXXOVJFHGDD6XYJAGBAMT5A", "BABIMEUCIQCKl1f9Cj26k0fFWE48+O4ibhYJYPytbDZWJRaaG9BybwIgCbk+3BViWkpuu2RI+41dwtlQ/m/01G860pTFCzDFfokBSDBFAiEA0O77DJsaM7IO+Ht06sp3umzXB64CNNOwf2isZuPfdmwCIGlggOwRSkXsqlPhE1gMdd5hf7ycL33Orfrr4v/XnMGSAUdSIQNsu/OwZurHvJMoiJoSAmmCHLoqIc5Wblh+rek+7rhASCECgYVkUspeAxwRfM6v4GRBhN/gGxTfpPqZuOlBIYZxTJZSrg=="); + case("bc1qazhmhwl9sxgjmwnd96hh926s3x5l0cf64yy6hvyn6qms438x550qy5sgva", "Z3SB7SRL555ZGOHVMYT5WG7RIZ", "BQBHMEQCICNI6H6b+VCZV9Z2H6EW5hPrE1buC6SJuy2ljSNmQlGfAiASbm5UrA8KH6TwF6evx7COV+i27ubiq2v9TyLYPOO63gFIMEUCIQCMOFnJbg0sy88G6wUXjv5stjVgfvAokOogWsisdkAnlAIgETfBw7kJhISFu9vIomFcEF/1NsN6c0h3KjNcpmNAZtMBSDBFAiEA/lUU+wBeA3prt8vHRpcQN763OYZ8L61DfN0QI/gkHpMCIB2qHwgoXNTH0sdqeAMD2ah7dSTie2bflax3Q3I/GibQAWlTIQKo003Zjj9Jg/kT7sOA7cTdIKmzsrIYBAuPd9hj8FZuHCEC6BdalMtwbjcx2vJjVelv7BJml0rY7Q5ze+PWen6tb5shAytperlYBs69tbn86t3pINqXy0NzttKZKi6Y8X+USQXaU64="); + } + + #[test] + fn official_vectors_full() { + #[track_caller] + fn case(address: &str, message: &str, signature: &str) { + assert!(verify::verify_full_encoded(address, message, signature).is_ok()); + } + + case("bc1qrqtlzcq86850yzgsyq9sssawx2qxlx5yq3xpkd", "KLE5MMJBTNF4AVZXIO3GIL5UWF", "AgAAAAABAUrfzHHOLAKmgCIFSTT3krp+cQxj1BDPBN4GBg3tRmFXAAAAAADgBwAAAQAAAAAAAAAAAWoCSDBFAiEAjYj85zyhQKa9DbMO0reDwdhkNwKJkF3q2qFcijXDgMUCIAaQ75s3fwqrCeYIUJugLvhxZFxQIVquGN90vIKCW3QLASEDMurnDzvc0zABUwVwCADfGXoDx/M3SQnYt7e3IHDoU3PgBwAA"); + case("bc1pve87s3l2levjmhetzr2f9xvep3y266xty0hnefmyv8tkxc3e4qssll2kdu", "XQMVC3YR6AOGZIHLSUQ2NSSBI2", "AgAAAAABAROFPNY6Zt8hFK0YQq5Wb6wk/CnUYEPtQ0HTHDyzNROrAAAAAADgBwAAAQAAAAAAAAAAAWoBQNRdLOo5XZY0SBqAsLZNr/z3Bqrmo3OxVn7e4tD/OOD4H9U/L1unq5Nmdz+S1w7SHtt46bFwnd8xnRVan8BofFfgBwAA"); + case("32Utb7Seg6EXq7UesMNJXhQ1gdohYNyzQ9", "EMYGZHEY3LIANYKCR7XJF3NMFQ", "AgAAAAABAe5xLNMlYQH4OGjJ3h4lqQaVp0Cic7mwxkvyWswqFMXeAAAAABcWABSy/hpDH/KLAi4x25Tmb2UaO1xtWeAHAAABAAAAAAAAAAABagJHMEQCIDEleqb0n1R5c21TGkWRXNFae98wbwI0QOyh/YmRuQX1AiAcv1MhyTzPOVgZ1VIwuu0tDxrVJUHK8lhOUOXpsZnGwwEhAsjeDEoWX8hvEC8A/692yGQsPh6JBO8Zf4aITEQsKAcJ4AcAAA=="); + case("bc1qg8r3cl47rrr75dwvr7jhzdukptegnmq8v0nmjd2jdn4qvlczqkts0rqtav", "QXYOWYWO7ZGJC4OPNC367HBUQF", "AgAAAAABAXshuDM6YKy1LClwk1ZOM5egX7RTFPOCvtxJkYFYk/FEAAAAAADgBwAAAQAAAAAAAAAAAWoEAEgwRQIhAI9uOxvqmBV0pldOoKWnSYhjobNhP4F+gxO0QlOdGtxFAiBROcNruLigZE4lj1DJEh8yGrqS00MeW463EO78TsaRFgFIMEUCIQCAhIqYuU4wDA2AYsU+QDVyucH4Tm/NSDP2+txyPMKEkAIgfuGlSh7ncxb2yV3S3aOF5uwHGqtIZjp3b4HW0d35EckBR1IhAkT3y4QqTOTzUs5AYq5eCl1g1vqgsHtiwgY0hKpSl7vOIQI07tYZDvxHcWuVOgULVj+LK1I63eqVWuQzUd0qkqpJ9FKu4AcAAA=="); + case("bc1q8vy6jhfe8ca0uruvr4aqkjk75dpg5m30rnwatg60uhya00dhlyqs2xvt2a", "3VJANNKSXPLND6YRKG6CUEUZXX", "AgAAAAABAU2vSmP5XYqecVKygaRRribDp5piMoVxUkxUnFSff8kRAAAAAADgBwAAAQAAAAAAAAAAAWoFAEgwRQIhAKBSw74gHlx272y4RzyU/ap7iNO5rmB6XXgBOy3Qsc/EAiBUnSrF/XhvuvAwi/mMme0JDpuCvl+oZ9C4f3H8OemXCAFIMEUCIQDK3wH0l2AvJ5FZ923ZMJkY1z0MBh1Nee9wjK7tVxFz5gIgdC1XBD/IdBPtx1xmyvSFhbIJlvnz98fPTm50K6KaXpEBSDBFAiEAyP3nXTzXrTmzq54x8jAY02ERHycEYzYqT9cpRTeEWg4CIHxRk3e3oPrM9oCZ8xcgNQ3lRhyc+G0qdDIl6qa8SN4AAWlTIQIlBvEshNuT7T6Ja01YgHs0G31etR15oRdjg2JJs6Hf5CEDBbFTr8NwzY8uUi5qQ1z16XJjdr9VZ1LBpDcElWryIwUhAkLyDL4FQMvj0TI89hZZ8ja4nCrlk9235CCAWXmU0hbgU67gBwAA"); + case("3PGZjFkYBL1m9WBWkWbCW5FEFTaS1Hj4EB", "NQVRV3DJYLKBANM3OPTNBULEU3", "AgAAAAABAVscdBvYDFN98A//Rt/fAWcN7mdM0x2yWzBjC33c7X5HAAAAACMiACDkkR/DseXy+GXBPtxHvHehUjHt+9XjRmZAgxuuomAC4eAHAAABAAAAAAAAAAABagQASDBFAiEA47YK5XeIGBMQC9bCfWb+IIfirIWlqAzQVc6E/lgBPZICIA0k/EO2t3YhqmYR5WdXUBGgAzR+IqgZ5/mxvj+4UoDTAUgwRQIhAPCIVZCSoIaOjY9BzYIXWEvbhpOl4JR88p/xYVoZObd6AiADyJXNqpDg/Lc2viPX14N2d0jQdEjamY4SmiU7GNbIOgFHUiED+4JBU/wACiE8VFbQF4DR8pKgz7+8X2+PHccTcGxVGdEhA9uIzp+4CB5QRgvrN1OXQbBmfW8kOd0cooPWMYJCHBCxUq7gBwAA"); + case("3Nye4j1GUFqCEBR3do2KEFZAs9oLe8NZ6X", "7OKFLKRXSP6J42VQOMSG7MVXEP", "AgAAAAEvAyd4zsoz8gcVU5H19GLYokTAN5PxuKCBlEPjODJ86gAAAADaAEcwRAIgT6rcfxgCmG6b3DpzNV6UG0jiCQGclG9sfiSpV45HDXMCIGgtqjFBuJ7rbi+cgnG0TZiKZaxMk0KI+gQd0pHJfEYCAUgwRQIhANCvCLjGMuZMzH+nCEkNhWhR45T6QRYMLin8utpuF9r1AiBTjG2NLjkre7ec+HPg8UUhK1jL1vgq7YKjq5ROv+h07AFHUiEDhKjcb/Pv1/7AYutzOXwgec08wwD/VwiPm58Lc0xjohghAhycjpwdBuP33orQXAH1CAsrgSkuspxM2+FPQ4OCVhQWUq7gBwAAAQAAAAAAAAAAAWrgBwAA"); + } + + #[test] + fn official_vectors_rejected() { + #[track_caller] + fn case(address: &str, message: &str, signature: &str, full: bool) { + let result = if full { + verify::verify_full_encoded(address, message, signature) + } else { + verify::verify_simple_encoded(address, message, signature) + }; + + assert!(result.is_err()); + } + + // time-locked script types are not supported + case("bc1p6vffkx7vcyezrjq7pg9qqdjv7vmtanfhk8ukwsn4syejwmarmhxqp0rw5x", "AY2VOQOXYI5CN2EHZKLOX7ZI37", "AgAAAAABAaza7/ukfX9ZdxCUvK7CPJgADDdPdF7ikXVKWctd5EHrAAAAAADgBwAAAQAAAAAAAAAAAWoEQPvuT0enYGwsab2lsPZU0U3OcRkGng+o/PAt4QU2lc8hG7lTUmflkt0To+eoipv2vptf0TlGOBCsKU5xE3kXKcMAS2MgrYfXhOkh0CvwuJpB+O3tal2ECfO0v7k1/A4PTlGcQiBnAuAHsnUgJjLn4tl5ytgC8CNTyITXmg4rx9ctxPedwRMPEBvfoUBorCHBJjLn4tl5ytgC8CNTyITXmg4rx9ctxPedwRMPEBvfoUDgBwAA", true); + case("bc1qhqcmw7ud03vqde3pe6hzajaylhucmlatrkcztzpnk8vpgvhg9dzq5ydark", "MGKMA2MJUBDHT55J7MHOLM7UPE", "AgAAAAABAYYJeOOOi3c33O+dholAwiF51Amy/E0qIf3ew2vFtDtTAAAAAADgBwAAAQAAAAAAAAAAAWoDSDBFAiEA64MwD2HkJjPLPAc2u5ia6ZdwCVO3okzVqGPEXnuJGZQCIE27BGOBQTdwJ2M/Wdsm6nFVunqaj+xZBSG/g/64FMbtAQBNYyEDrYfXhOkh0CvwuJpB+O3tal2ECfO0v7k1/A4PTlGcQiBnAuAHsnUhA4ZGGvodKgqeg/ZYffm6miaKaG57VkCSjmmRprCa+ulyaKzgBwAA", true); + + // wrong message for p2wsh-multisig-2of2 simple signature + case("bc1qw6g0rgrpuxvj4edkwtvzpmt3c5m08mhp8nuk3mrk4erufvlczp5ssdscjd", "DL2KXDPQAN63YIQPIP34O3XYVX", "BABIMEUCIQCKl1f9Cj26k0fFWE48+O4ibhYJYPytbDZWJRaaG9BybwIgCbk+3BViWkpuu2RI+41dwtlQ/m/01G860pTFCzDFfokBSDBFAiEA0O77DJsaM7IO+Ht06sp3umzXB64CNNOwf2isZuPfdmwCIGlggOwRSkXsqlPhE1gMdd5hf7ycL33Orfrr4v/XnMGSAUdSIQNsu/OwZurHvJMoiJoSAmmCHLoqIc5Wblh+rek+7rhASCECgYVkUspeAxwRfM6v4GRBhN/gGxTfpPqZuOlBIYZxTJZSrg==", false); + // wrong signer for p2wsh-multisig-2of2 simple signature + case("bc1q47vnwr6fsarstmmw89wrkvl89540sn5g79x75ms8aly9rrsnq8eqh3v9gz", "G7ZTXXOVJFHGDD6XYJAGBAMT5A", "BABIMEUCIQCKl1f9Cj26k0fFWE48+O4ibhYJYPytbDZWJRaaG9BybwIgCbk+3BViWkpuu2RI+41dwtlQ/m/01G860pTFCzDFfokBSDBFAiEA0O77DJsaM7IO+Ht06sp3umzXB64CNNOwf2isZuPfdmwCIGlggOwRSkXsqlPhE1gMdd5hf7ycL33Orfrr4v/XnMGSAUdSIQNsu/OwZurHvJMoiJoSAmmCHLoqIc5Wblh+rek+7rhASCECgYVkUspeAxwRfM6v4GRBhN/gGxTfpPqZuOlBIYZxTJZSrg==", false); + // wrong message for p2wsh-multisig-3of3 simple signature + case("bc1qazhmhwl9sxgjmwnd96hh926s3x5l0cf64yy6hvyn6qms438x550qy5sgva", "UKLIJM5HKKQEIFJ44R7UIQFMYL", "BQBHMEQCICNI6H6b+VCZV9Z2H6EW5hPrE1buC6SJuy2ljSNmQlGfAiASbm5UrA8KH6TwF6evx7COV+i27ubiq2v9TyLYPOO63gFIMEUCIQCMOFnJbg0sy88G6wUXjv5stjVgfvAokOogWsisdkAnlAIgETfBw7kJhISFu9vIomFcEF/1NsN6c0h3KjNcpmNAZtMBSDBFAiEA/lUU+wBeA3prt8vHRpcQN763OYZ8L61DfN0QI/gkHpMCIB2qHwgoXNTH0sdqeAMD2ah7dSTie2bflax3Q3I/GibQAWlTIQKo003Zjj9Jg/kT7sOA7cTdIKmzsrIYBAuPd9hj8FZuHCEC6BdalMtwbjcx2vJjVelv7BJml0rY7Q5ze+PWen6tb5shAytperlYBs69tbn86t3pINqXy0NzttKZKi6Y8X+USQXaU64=", false); + // wrong signer for p2wsh-multisig-3of3 simple signature + case("bc1qnckc2q6804depn2240l66p93e0mscp7vd9ndptfuznex86ehpcvseq6e8a", "Z3SB7SRL555ZGOHVMYT5WG7RIZ", "BQBHMEQCICNI6H6b+VCZV9Z2H6EW5hPrE1buC6SJuy2ljSNmQlGfAiASbm5UrA8KH6TwF6evx7COV+i27ubiq2v9TyLYPOO63gFIMEUCIQCMOFnJbg0sy88G6wUXjv5stjVgfvAokOogWsisdkAnlAIgETfBw7kJhISFu9vIomFcEF/1NsN6c0h3KjNcpmNAZtMBSDBFAiEA/lUU+wBeA3prt8vHRpcQN763OYZ8L61DfN0QI/gkHpMCIB2qHwgoXNTH0sdqeAMD2ah7dSTie2bflax3Q3I/GibQAWlTIQKo003Zjj9Jg/kT7sOA7cTdIKmzsrIYBAuPd9hj8FZuHCEC6BdalMtwbjcx2vJjVelv7BJml0rY7Q5ze+PWen6tb5shAytperlYBs69tbn86t3pINqXy0NzttKZKi6Y8X+USQXaU64=", false); + // wrong message for p2wsh-multisig-2of2 full signature + case("bc1qg8r3cl47rrr75dwvr7jhzdukptegnmq8v0nmjd2jdn4qvlczqkts0rqtav", "OANRY57VZNHOXZNYGCGZM5ADYG", "AgAAAAABAXshuDM6YKy1LClwk1ZOM5egX7RTFPOCvtxJkYFYk/FEAAAAAADgBwAAAQAAAAAAAAAAAWoEAEgwRQIhAI9uOxvqmBV0pldOoKWnSYhjobNhP4F+gxO0QlOdGtxFAiBROcNruLigZE4lj1DJEh8yGrqS00MeW463EO78TsaRFgFIMEUCIQCAhIqYuU4wDA2AYsU+QDVyucH4Tm/NSDP2+txyPMKEkAIgfuGlSh7ncxb2yV3S3aOF5uwHGqtIZjp3b4HW0d35EckBR1IhAkT3y4QqTOTzUs5AYq5eCl1g1vqgsHtiwgY0hKpSl7vOIQI07tYZDvxHcWuVOgULVj+LK1I63eqVWuQzUd0qkqpJ9FKu4AcAAA==", true); + // wrong signer for p2wsh-multisig-2of2 full signature + case("bc1qan5sys8u4cpvgutt70fn2tgweu7as9aw0slljuz86x4nyr8myvjsgz3q2v", "QXYOWYWO7ZGJC4OPNC367HBUQF", "AgAAAAABAXshuDM6YKy1LClwk1ZOM5egX7RTFPOCvtxJkYFYk/FEAAAAAADgBwAAAQAAAAAAAAAAAWoEAEgwRQIhAI9uOxvqmBV0pldOoKWnSYhjobNhP4F+gxO0QlOdGtxFAiBROcNruLigZE4lj1DJEh8yGrqS00MeW463EO78TsaRFgFIMEUCIQCAhIqYuU4wDA2AYsU+QDVyucH4Tm/NSDP2+txyPMKEkAIgfuGlSh7ncxb2yV3S3aOF5uwHGqtIZjp3b4HW0d35EckBR1IhAkT3y4QqTOTzUs5AYq5eCl1g1vqgsHtiwgY0hKpSl7vOIQI07tYZDvxHcWuVOgULVj+LK1I63eqVWuQzUd0qkqpJ9FKu4AcAAA==", true); + // wrong message for p2wsh-multisig-3of3 full signature + case("bc1q8vy6jhfe8ca0uruvr4aqkjk75dpg5m30rnwatg60uhya00dhlyqs2xvt2a", "UZQGB4YTYIS3PRT3UUOCO3YCX3", "AgAAAAABAU2vSmP5XYqecVKygaRRribDp5piMoVxUkxUnFSff8kRAAAAAADgBwAAAQAAAAAAAAAAAWoFAEgwRQIhAKBSw74gHlx272y4RzyU/ap7iNO5rmB6XXgBOy3Qsc/EAiBUnSrF/XhvuvAwi/mMme0JDpuCvl+oZ9C4f3H8OemXCAFIMEUCIQDK3wH0l2AvJ5FZ923ZMJkY1z0MBh1Nee9wjK7tVxFz5gIgdC1XBD/IdBPtx1xmyvSFhbIJlvnz98fPTm50K6KaXpEBSDBFAiEAyP3nXTzXrTmzq54x8jAY02ERHycEYzYqT9cpRTeEWg4CIHxRk3e3oPrM9oCZ8xcgNQ3lRhyc+G0qdDIl6qa8SN4AAWlTIQIlBvEshNuT7T6Ja01YgHs0G31etR15oRdjg2JJs6Hf5CEDBbFTr8NwzY8uUi5qQ1z16XJjdr9VZ1LBpDcElWryIwUhAkLyDL4FQMvj0TI89hZZ8ja4nCrlk9235CCAWXmU0hbgU67gBwAA", true); + // wrong signer for p2wsh-multisig-3of3 full signature + case("bc1qma94rw0f5t4l64wc6xfrjuuatqn6klcwmxvls86y9k4rjvva40wqr8u0cq", "3VJANNKSXPLND6YRKG6CUEUZXX", "AgAAAAABAU2vSmP5XYqecVKygaRRribDp5piMoVxUkxUnFSff8kRAAAAAADgBwAAAQAAAAAAAAAAAWoFAEgwRQIhAKBSw74gHlx272y4RzyU/ap7iNO5rmB6XXgBOy3Qsc/EAiBUnSrF/XhvuvAwi/mMme0JDpuCvl+oZ9C4f3H8OemXCAFIMEUCIQDK3wH0l2AvJ5FZ923ZMJkY1z0MBh1Nee9wjK7tVxFz5gIgdC1XBD/IdBPtx1xmyvSFhbIJlvnz98fPTm50K6KaXpEBSDBFAiEAyP3nXTzXrTmzq54x8jAY02ERHycEYzYqT9cpRTeEWg4CIHxRk3e3oPrM9oCZ8xcgNQ3lRhyc+G0qdDIl6qa8SN4AAWlTIQIlBvEshNuT7T6Ja01YgHs0G31etR15oRdjg2JJs6Hf5CEDBbFTr8NwzY8uUi5qQ1z16XJjdr9VZ1LBpDcElWryIwUhAkLyDL4FQMvj0TI89hZZ8ja4nCrlk9235CCAWXmU0hbgU67gBwAA", true); + // wrong message for p2sh-p2wsh-multisig-2of2 full signature + case("3PGZjFkYBL1m9WBWkWbCW5FEFTaS1Hj4EB", "FOQHOIFXJVPFSGGBRLAX53D6R2", "AgAAAAABAVscdBvYDFN98A//Rt/fAWcN7mdM0x2yWzBjC33c7X5HAAAAACMiACDkkR/DseXy+GXBPtxHvHehUjHt+9XjRmZAgxuuomAC4eAHAAABAAAAAAAAAAABagQASDBFAiEA47YK5XeIGBMQC9bCfWb+IIfirIWlqAzQVc6E/lgBPZICIA0k/EO2t3YhqmYR5WdXUBGgAzR+IqgZ5/mxvj+4UoDTAUgwRQIhAPCIVZCSoIaOjY9BzYIXWEvbhpOl4JR88p/xYVoZObd6AiADyJXNqpDg/Lc2viPX14N2d0jQdEjamY4SmiU7GNbIOgFHUiED+4JBU/wACiE8VFbQF4DR8pKgz7+8X2+PHccTcGxVGdEhA9uIzp+4CB5QRgvrN1OXQbBmfW8kOd0cooPWMYJCHBCxUq7gBwAA", true); + // wrong signer for p2sh-p2wsh-multisig-2of2 full signature + case("3DA7VZKYcuiaJFsDnzWjBDvh4VhBFZk6jg", "NQVRV3DJYLKBANM3OPTNBULEU3", "AgAAAAABAVscdBvYDFN98A//Rt/fAWcN7mdM0x2yWzBjC33c7X5HAAAAACMiACDkkR/DseXy+GXBPtxHvHehUjHt+9XjRmZAgxuuomAC4eAHAAABAAAAAAAAAAABagQASDBFAiEA47YK5XeIGBMQC9bCfWb+IIfirIWlqAzQVc6E/lgBPZICIA0k/EO2t3YhqmYR5WdXUBGgAzR+IqgZ5/mxvj+4UoDTAUgwRQIhAPCIVZCSoIaOjY9BzYIXWEvbhpOl4JR88p/xYVoZObd6AiADyJXNqpDg/Lc2viPX14N2d0jQdEjamY4SmiU7GNbIOgFHUiED+4JBU/wACiE8VFbQF4DR8pKgz7+8X2+PHccTcGxVGdEhA9uIzp+4CB5QRgvrN1OXQbBmfW8kOd0cooPWMYJCHBCxUq7gBwAA", true); + // wrong message for p2sh-multisig-2of2 full signature + case("3Nye4j1GUFqCEBR3do2KEFZAs9oLe8NZ6X", "DAOHN7TAL75XRHJILGIO3RXAEM", "AgAAAAEvAyd4zsoz8gcVU5H19GLYokTAN5PxuKCBlEPjODJ86gAAAADaAEcwRAIgT6rcfxgCmG6b3DpzNV6UG0jiCQGclG9sfiSpV45HDXMCIGgtqjFBuJ7rbi+cgnG0TZiKZaxMk0KI+gQd0pHJfEYCAUgwRQIhANCvCLjGMuZMzH+nCEkNhWhR45T6QRYMLin8utpuF9r1AiBTjG2NLjkre7ec+HPg8UUhK1jL1vgq7YKjq5ROv+h07AFHUiEDhKjcb/Pv1/7AYutzOXwgec08wwD/VwiPm58Lc0xjohghAhycjpwdBuP33orQXAH1CAsrgSkuspxM2+FPQ4OCVhQWUq7gBwAAAQAAAAAAAAAAAWrgBwAA", true); + // wrong signer for p2sh-multisig-2of2 full signature + case("3L9uCVRBUfLgKQQK366dmutgsp3GpCwYGE", "7OKFLKRXSP6J42VQOMSG7MVXEP", "AgAAAAEvAyd4zsoz8gcVU5H19GLYokTAN5PxuKCBlEPjODJ86gAAAADaAEcwRAIgT6rcfxgCmG6b3DpzNV6UG0jiCQGclG9sfiSpV45HDXMCIGgtqjFBuJ7rbi+cgnG0TZiKZaxMk0KI+gQd0pHJfEYCAUgwRQIhANCvCLjGMuZMzH+nCEkNhWhR45T6QRYMLin8utpuF9r1AiBTjG2NLjkre7ec+HPg8UUhK1jL1vgq7YKjq5ROv+h07AFHUiEDhKjcb/Pv1/7AYutzOXwgec08wwD/VwiPm58Lc0xjohghAhycjpwdBuP33orQXAH1CAsrgSkuspxM2+FPQ4OCVhQWUq7gBwAAAQAAAAAAAAAAAWrgBwAA", true); + } + + fn p2wsh_2of2_witness(address: &Address) -> Witness { + sign::sign_simple( + address, + P2WSH_2OF2_MESSAGE, + &[ + PrivateKey::from_wif(P2WSH_2OF2_PRIVATE_KEY_1).unwrap(), + PrivateKey::from_wif(P2WSH_2OF2_PRIVATE_KEY_2).unwrap(), + ], + Some(&ScriptBuf::from_hex(P2WSH_2OF2_WITNESS_SCRIPT).unwrap()), + ) + .unwrap() + } + + #[test] + fn verify_multisig_rejects_wrong_signature_count() { + let address = Address::from_str(P2WSH_2OF2_ADDRESS) + .unwrap() + .assume_checked(); + + let items = p2wsh_2of2_witness(&address).to_vec(); + + let mut witness = Witness::new(); + witness.push::<&[u8]>(&[]); + witness.push(&items[1]); + witness.push(&items[3]); + + assert!(matches!( + verify::verify_simple(&address, P2WSH_2OF2_MESSAGE, witness), + Err(Error::InvalidWitness) + )); + } + + #[test] + fn verify_multisig_rejects_out_of_order_signatures() { + let address = Address::from_str(P2WSH_2OF2_ADDRESS) + .unwrap() + .assume_checked(); + + let items = p2wsh_2of2_witness(&address).to_vec(); + + let mut witness = Witness::new(); + witness.push::<&[u8]>(&[]); + witness.push(&items[2]); + witness.push(&items[1]); + witness.push(&items[3]); + + assert!(matches!( + verify::verify_simple(&address, P2WSH_2OF2_MESSAGE, witness), + Err(Error::SignatureInvalid { .. }) + )); + } + + #[test] + fn verify_multisig_rejects_tampered_witness_script() { + let address = Address::from_str(P2WSH_2OF2_ADDRESS) + .unwrap() + .assume_checked(); + + let items = p2wsh_2of2_witness(&address).to_vec(); + + let mut script = items[3].clone(); + script[5] ^= 1; + + let mut witness = Witness::new(); + witness.push::<&[u8]>(&[]); + witness.push(&items[1]); + witness.push(&items[2]); + witness.push(script); + + assert!(matches!( + verify::verify_simple(&address, P2WSH_2OF2_MESSAGE, witness), + Err(Error::ToSignInvalid) + )); + } + + #[test] + fn verify_simple_rejects_legacy_p2sh_multisig() { + assert!(matches!( + verify::verify_simple( + &Address::from_str(P2SH_MULTISIG_2OF2_ADDRESS) + .unwrap() + .assume_checked(), + "foo", + Witness::new() + ), + Err(Error::InvalidWitness) + )); + } + + #[test] + fn verify_p2wpkh_rejects_mismatched_key() { + let victim = Address::from_str("bc1qqthe0hz8klx90e7stf6shclhsvqd5ly96pn53v") + .unwrap() + .assume_checked(); + + let witness = sign::sign_simple( + &victim, + "foo", + &[PrivateKey::from_wif(WIF_PRIVATE_KEY).unwrap()], + None, + ) + .unwrap(); + + assert!(matches!( + verify::verify_simple(&victim, "foo", witness), + Err(Error::PublicKeyMismatch) + )); + } + + #[test] + fn verify_p2sh_p2wpkh_rejects_mismatched_key() { + let victim = Address::from_str(P2SH_P2WSH_2OF2_ADDRESS) + .unwrap() + .assume_checked(); + + let to_spend = create_to_spend(&victim, "foo").unwrap(); + let to_sign = create_to_sign(&to_spend, None).unwrap(); + + let witness = create_message_signature_p2wpkh( + &to_spend, + &to_sign, + &PrivateKey::from_wif(WIF_PRIVATE_KEY).unwrap(), + true, + ); + + assert!(matches!( + verify::verify_simple(&victim, "foo", witness), + Err(Error::PublicKeyMismatch) + )); + } + + #[test] + fn verify_p2sh_p2wpkh_rejects_uncompressed_witness_key() { + let pub_key = PrivateKey::from_wif(UNCOMPRESSED_WIF_PRIVATE_KEY) + .unwrap() + .public_key(&Secp256k1::new()); + + let mut witness = Witness::new(); + witness.push(vec![0; 71]); + witness.push(pub_key.to_bytes()); + + assert!(matches!( + verify::verify_simple( + &Address::from_str(NESTED_SEGWIT_ADDRESS) + .unwrap() + .assume_checked(), + "foo", + witness + ), + Err(Error::UncompressedPublicKey { .. }) + )); + } + + #[test] + fn sign_simple_rejects_p2sh_p2wsh() { + assert!(matches!( + sign::sign_simple_encoded( + P2SH_P2WSH_2OF2_ADDRESS, + "foo", + &[P2SH_P2WSH_2OF2_PRIVATE_KEY_1, P2SH_P2WSH_2OF2_PRIVATE_KEY_2], + Some(P2SH_P2WSH_2OF2_WITNESS_SCRIPT), + ), + Err(Error::UnsupportedAddress { .. }) + )); + } + + #[test] + fn roundtrip_p2sh_multisig_full() { + assert!(verify::verify_full_encoded( + P2SH_MULTISIG_2OF2_ADDRESS, + "foo", + &sign::sign_full_encoded( + P2SH_MULTISIG_2OF2_ADDRESS, + "foo", + &[ + P2SH_MULTISIG_2OF2_PRIVATE_KEY_1, + P2SH_MULTISIG_2OF2_PRIVATE_KEY_2 + ], + Some(P2SH_MULTISIG_2OF2_REDEEM_SCRIPT), + ) + .unwrap() + ) + .is_ok()); + } + + #[test] + fn multisig_rejects_duplicate_signer() { + assert!(matches!( + sign::sign_simple_encoded( + P2WSH_2OF2_ADDRESS, + "foo", + &[P2WSH_2OF2_PRIVATE_KEY_1, P2WSH_2OF2_PRIVATE_KEY_1], + Some(P2WSH_2OF2_WITNESS_SCRIPT), + ), + Err(Error::DuplicateSigner) + )); + } + + #[test] + fn sign_rejects_no_private_keys() { + let keys: &[&str] = &[]; + + assert!(matches!( + sign::sign_simple_encoded(SEGWIT_ADDRESS, "foo", keys, None), + Err(Error::NoPrivateKeys) + )); + } + + #[test] + fn verify_rejects_non_sighash_all_signatures() { + let key = PrivateKey::from_wif(WIF_PRIVATE_KEY).unwrap(); + + let p2wpkh = Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(); + let mut items = sign::sign_simple(&p2wpkh, "foo", &[key], None) + .unwrap() + .to_vec(); + *items[0].last_mut().unwrap() = 0x02; + assert!(matches!( + verify::verify_simple(&p2wpkh, "foo", Witness::from_slice(&items)), + Err(Error::SigHashTypeUnsupported { .. }) + )); + + let p2tr = Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(); + let mut items = sign::sign_simple(&p2tr, "foo", &[key], None) + .unwrap() + .to_vec(); + *items[0].last_mut().unwrap() = 0x02; + assert!(matches!( + verify::verify_simple(&p2tr, "foo", Witness::from_slice(&items)), + Err(Error::SigHashTypeUnsupported { .. }) + )); + + let p2wsh = Address::from_str(P2WSH_2OF2_ADDRESS) + .unwrap() + .assume_checked(); + let mut items = p2wsh_2of2_witness(&p2wsh).to_vec(); + *items[1].last_mut().unwrap() = 0x02; + assert!(matches!( + verify::verify_simple(&p2wsh, P2WSH_2OF2_MESSAGE, Witness::from_slice(&items)), + Err(Error::SigHashTypeUnsupported { .. }) + )); + } + + #[test] + fn verify_full_p2tr_rejects_script_sig() { + let address = Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(); + + let mut to_sign = sign::sign_full( + &address, + "foo", + &[PrivateKey::from_wif(WIF_PRIVATE_KEY).unwrap()], + None, + ) + .unwrap(); + + to_sign.input[0].script_sig = ScriptBuf::from_hex("deadbeef").unwrap(); + + assert!(matches!( + verify::verify_full(&address, "foo", to_sign), + Err(Error::ToSignInvalid) + )); + } + + #[test] + fn verify_p2sh_p2wsh_rejects_tampered_script_sig() { + let address = Address::from_str(P2SH_P2WSH_2OF2_ADDRESS) + .unwrap() + .assume_checked(); + + let mut to_sign = sign::sign_full( + &address, + P2SH_P2WSH_2OF2_MESSAGE, + &[ + PrivateKey::from_wif(P2SH_P2WSH_2OF2_PRIVATE_KEY_1).unwrap(), + PrivateKey::from_wif(P2SH_P2WSH_2OF2_PRIVATE_KEY_2).unwrap(), + ], + Some(&ScriptBuf::from_hex(P2SH_P2WSH_2OF2_WITNESS_SCRIPT).unwrap()), + ) + .unwrap(); + + to_sign.input[0].script_sig = ScriptBuf::from_hex("deadbeef").unwrap(); + + assert!(matches!( + verify::verify_full(&address, P2SH_P2WSH_2OF2_MESSAGE, to_sign), + Err(Error::ToSignInvalid) + )); + } + + #[test] + fn multisig_rejects_wrong_key_count() { + assert!(matches!( + sign::sign_simple_encoded( + P2WSH_2OF2_ADDRESS, + "foo", + &[P2WSH_2OF2_PRIVATE_KEY_1], + Some(P2WSH_2OF2_WITNESS_SCRIPT), + ), + Err(Error::SignatureCount { + required: 2, + provided: 1 + }) + )); + } + + #[test] + fn sign_p2sh_rejects_mismatched_witness_script() { + assert!(matches!( + sign::sign_full_encoded( + P2SH_P2WSH_2OF2_ADDRESS, + "foo", + &[P2SH_P2WSH_2OF2_PRIVATE_KEY_1, P2SH_P2WSH_2OF2_PRIVATE_KEY_2], + Some(P2WSH_2OF2_WITNESS_SCRIPT), + ), + Err(Error::UnsupportedAddress { .. }) + )); + } + + #[test] + fn sign_p2wsh_rejects_missing_witness_script() { + assert!(matches!( + sign::sign_simple_encoded( + P2WSH_2OF2_ADDRESS, + "foo", + &[P2WSH_2OF2_PRIVATE_KEY_1, P2WSH_2OF2_PRIVATE_KEY_2], + None, + ), + Err(Error::InvalidWitness) + )); + } } diff --git a/src/sign.rs b/src/sign.rs index 489c73af..741ab292 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -2,14 +2,26 @@ use super::*; /// Signs the BIP-322 simple from spec-compliant string encodings. #[allow(clippy::result_large_err)] -pub fn sign_simple_encoded(address: &str, message: &str, wif_private_key: &str) -> Result { +pub fn sign_simple_encoded( + address: &str, + message: &str, + wif_private_keys: &[impl AsRef], + witness_script_hex: Option<&str>, +) -> Result { let address = Address::from_str(address) .context(error::AddressParse { address })? .assume_checked(); - let private_key = PrivateKey::from_wif(wif_private_key).context(error::PrivateKeyParse)?; + let private_keys: Vec = wif_private_keys + .iter() + .map(|private_key| PrivateKey::from_wif(private_key.as_ref()).context(error::PrivateKeyParse)) + .collect::>>()?; - let witness = sign_simple(&address, message, private_key)?; + let witness_script = witness_script_hex + .map(|hex| ScriptBuf::from_hex(hex).context(error::WitnessScriptParse)) + .transpose()?; + + let witness = sign_simple(&address, message, &private_keys, witness_script.as_ref())?; let mut buffer = Vec::new(); @@ -22,14 +34,26 @@ pub fn sign_simple_encoded(address: &str, message: &str, wif_private_key: &str) /// Signs the BIP-322 full from spec-compliant string encodings. #[allow(clippy::result_large_err)] -pub fn sign_full_encoded(address: &str, message: &str, wif_private_key: &str) -> Result { +pub fn sign_full_encoded( + address: &str, + message: &str, + wif_private_keys: &[impl AsRef], + witness_script_hex: Option<&str>, +) -> Result { let address = Address::from_str(address) .context(error::AddressParse { address })? .assume_checked(); - let private_key = PrivateKey::from_wif(wif_private_key).context(error::PrivateKeyParse)?; + let private_keys: Vec = wif_private_keys + .iter() + .map(|private_key| PrivateKey::from_wif(private_key.as_ref()).context(error::PrivateKeyParse)) + .collect::>>()?; + + let witness_script = witness_script_hex + .map(|hex| ScriptBuf::from_hex(hex).context(error::WitnessScriptParse)) + .transpose()?; - let tx = sign_full(&address, message, private_key)?; + let tx = sign_full(&address, message, &private_keys, witness_script.as_ref())?; let mut buffer = Vec::new(); @@ -44,13 +68,24 @@ pub fn sign_full_encoded(address: &str, message: &str, wif_private_key: &str) -> pub fn sign_simple( address: &Address, message: impl AsRef<[u8]>, - private_key: PrivateKey, + private_keys: &[PrivateKey], + witness_script: Option<&ScriptBuf>, ) -> Result { - Ok( - sign_full(address, message, private_key)?.input[0] - .witness - .clone(), - ) + if matches!(address.to_address_data(), AddressData::P2sh { .. }) && witness_script.is_some() { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + } + + let tx = sign_full(address, message, private_keys, witness_script)?; + + if tx.input[0].witness.is_empty() { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + } + + Ok(tx.input[0].witness.clone()) } /// Signs in the BIP-322 full format from proper Rust types and returns the full transaction. @@ -58,28 +93,44 @@ pub fn sign_simple( pub fn sign_full( address: &Address, message: impl AsRef<[u8]>, - private_key: PrivateKey, + private_keys: &[PrivateKey], + witness_script: Option<&ScriptBuf>, ) -> Result { let to_spend = create_to_spend(address, message)?; let mut to_sign = create_to_sign(&to_spend, None)?; + if private_keys.is_empty() { + return Err(Error::NoPrivateKeys); + } + let witness = match address.to_address_data() { AddressData::Segwit { witness_program } => { let version = witness_program.version().to_num(); let program_len = witness_program.program().len(); match version { - 0 => { - if program_len != 20 { - return Err(Error::NotKeyPathSpend); + 0 => match program_len { + 20 => { + create_message_signature_p2wpkh(&to_spend, &to_sign, single_key(private_keys)?, false) } - create_message_signature_p2wpkh(&to_spend, &to_sign, private_key, false) - } + 32 => { + let witness_script = witness_script.ok_or(Error::InvalidWitness)?; + + if address.script_pubkey() != ScriptBuf::new_p2wsh(&witness_script.wscript_hash()) { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + } + + create_message_signature_p2wsh(&to_spend, &to_sign, private_keys, witness_script)? + } + _ => return Err(Error::NotKeyPathSpend), + }, 1 => { if program_len != 32 { return Err(Error::NotKeyPathSpend); } - create_message_signature_taproot(&to_spend, &to_sign, private_key, None) + create_message_signature_taproot(&to_spend, &to_sign, single_key(private_keys)?, None) } _ => { return Err(Error::UnsupportedAddress { @@ -88,9 +139,48 @@ pub fn sign_full( } } } - AddressData::P2sh { script_hash: _ } => { - create_message_signature_p2wpkh(&to_spend, &to_sign, private_key, true) - } + AddressData::P2sh { script_hash: _ } => match witness_script { + Some(ws) => { + let p2wsh_redeem = ScriptBuf::new_p2wsh(&ws.wscript_hash()); + + if address.script_pubkey() == ScriptBuf::new_p2sh(&ws.script_hash()) { + create_message_signature_p2sh_multisig(&mut to_sign, private_keys, ws)? + } else if address.script_pubkey() == ScriptBuf::new_p2sh(&p2wsh_redeem.script_hash()) { + let witness = create_message_signature_p2wsh(&to_spend, &to_sign, private_keys, ws)?; + + to_sign.inputs[0].final_script_sig = Some(push_only_script(&p2wsh_redeem)); + + witness + } else { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + } + } + None => { + let secp = Secp256k1::new(); + + let private_key = single_key(private_keys)?; + + let wpkh = private_key + .public_key(&secp) + .wpubkey_hash() + .context(error::UncompressedPublicKey)?; + + let redeem = ScriptBuf::new_p2wpkh(&wpkh); + if address.script_pubkey() != ScriptBuf::new_p2sh(&redeem.script_hash()) { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + } + + let witness = create_message_signature_p2wpkh(&to_spend, &to_sign, private_key, true); + + to_sign.inputs[0].final_script_sig = Some(push_only_script(&redeem)); + + witness + } + }, _ => { return Err(Error::UnsupportedAddress { address: address.to_string(), @@ -98,8 +188,9 @@ pub fn sign_full( } }; - to_sign.inputs[0].final_script_witness = Some(witness); - + if !witness.is_empty() { + to_sign.inputs[0].final_script_witness = Some(witness); + } to_sign.extract_tx().context(error::TransactionExtract) } @@ -107,7 +198,7 @@ pub fn sign_full( pub fn create_message_signature_p2wpkh( to_spend_tx: &Transaction, to_sign: &Psbt, - private_key: PrivateKey, + private_key: &PrivateKey, is_p2sh: bool, ) -> Witness { let secp = Secp256k1::new(); @@ -156,7 +247,7 @@ pub fn create_message_signature_p2wpkh( pub fn create_message_signature_taproot( to_spend_tx: &Transaction, to_sign: &Psbt, - private_key: PrivateKey, + private_key: &PrivateKey, aux_rand: Option<[u8; 32]>, ) -> Witness { let mut to_sign = to_sign.clone(); @@ -215,3 +306,83 @@ pub fn create_message_signature_taproot( witness.to_owned() } + +/// Sign for multisig +#[allow(clippy::result_large_err)] +pub fn create_message_signature_p2wsh( + to_spend_tx: &Transaction, + to_sign: &Psbt, + private_keys: &[PrivateKey], + witness_script: &ScriptBuf, +) -> Result { + let secp = Secp256k1::new(); + let sighash_type = EcdsaSighashType::All; + let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx.clone()); + + let sighash = sighash_cache + .p2wsh_signature_hash(0, witness_script, to_spend_tx.output[0].value, sighash_type) + .expect("signature hash should compute"); + + let message = secp256k1::Message::from_digest_slice(sighash.as_ref()) + .expect("should be cryptographically secure hash"); + + let mut witness = Witness::new(); + witness.push::<&[u8]>(&[]); + + let signatures = ordered_multisig_signatures(&secp, witness_script, private_keys, &message)?; + + for signature in signatures { + witness.push(signature) + } + + witness.push(witness_script.as_bytes()); + + Ok(witness) +} + +/// Sign for p2sh multisig +#[allow(clippy::result_large_err)] +pub fn create_message_signature_p2sh_multisig( + to_sign: &mut Psbt, + private_keys: &[PrivateKey], + redeem_script: &ScriptBuf, +) -> Result { + let secp = Secp256k1::new(); + let sighash_type = EcdsaSighashType::All; + + let sighash = SighashCache::new(to_sign.unsigned_tx.clone()) + .legacy_signature_hash(0, redeem_script, sighash_type.to_u32()) + .expect("signature hash should compute"); + + let message = secp256k1::Message::from_digest_slice(sighash.as_ref()) + .expect("should be cryptographically secure hash"); + + let signatures = ordered_multisig_signatures(&secp, redeem_script, private_keys, &message)?; + + let mut builder = ScriptBuf::builder().push_opcode(opcodes::OP_0); + + for signature in signatures { + let mut pb = bitcoin::script::PushBytesBuf::new(); + pb.extend_from_slice(&signature).expect("sig fits in push"); + builder = builder.push_slice(pb); + } + + let mut pb = bitcoin::script::PushBytesBuf::new(); + pb.extend_from_slice(redeem_script.as_bytes()) + .expect("redeem fits in push"); + to_sign.inputs[0].final_script_sig = Some(builder.push_slice(pb).into_script()); + + Ok(Witness::new()) +} + +#[allow(clippy::result_large_err)] +fn single_key(private_keys: &[PrivateKey]) -> Result<&PrivateKey> { + if private_keys.len() != 1 { + return Err(Error::SignatureCount { + required: 1, + provided: private_keys.len(), + }); + } + + Ok(&private_keys[0]) +} diff --git a/src/util.rs b/src/util.rs index d79dd869..290d7ccf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -81,3 +81,122 @@ pub fn create_to_sign(to_spend: &Transaction, witness: Option) -> Resul Ok(psbt) } + +#[allow(clippy::result_large_err)] +pub(crate) fn parse_multisig(script: &bitcoin::Script) -> Result<(usize, Vec)> { + let instructions = script + .instructions() + .collect::, _>>() + .map_err(|_| Error::InvalidWitness)?; + + if instructions.len() < 4 { + return Err(Error::InvalidWitness); + } + + let pushnum = |op: bitcoin::opcodes::Opcode| -> Result { + let value = op.to_u8(); + if (opcodes::all::OP_PUSHNUM_1.to_u8()..=opcodes::all::OP_PUSHNUM_16.to_u8()).contains(&value) { + Ok((value - opcodes::all::OP_PUSHNUM_1.to_u8() + 1) as usize) + } else { + Err(Error::InvalidWitness) + } + }; + + if !matches!(instructions.last(), Some(Instruction::Op(op)) if *op == opcodes::all::OP_CHECKMULTISIG) + { + return Err(Error::InvalidWitness); + } + + let required_signatures = match &instructions[0] { + Instruction::Op(op) => pushnum(*op)?, + _ => return Err(Error::InvalidWitness), + }; + + let total_keys = match &instructions[instructions.len() - 2] { + Instruction::Op(op) => pushnum(*op)?, + _ => return Err(Error::InvalidWitness), + }; + + let key_instructions = &instructions[1..instructions.len() - 2]; + if key_instructions.len() != total_keys + || required_signatures < 1 + || required_signatures > total_keys + { + return Err(Error::InvalidWitness); + } + + let mut pubkeys = Vec::with_capacity(total_keys); + for instruction in key_instructions { + match instruction { + Instruction::PushBytes(bytes) => { + pubkeys.push(PublicKey::from_slice(bytes.as_bytes()).map_err(|_| Error::InvalidPublicKey)?) + } + _ => return Err(Error::InvalidWitness), + } + } + + Ok((required_signatures, pubkeys)) +} + +/// Sign with each private key, ordering signatures by the position of the +/// corresponding public key in the multisig script, as required by +/// OP_CHECKMULTISIG's forward-only matching. +#[allow(clippy::result_large_err)] +pub(crate) fn ordered_multisig_signatures( + secp: &Secp256k1, + script: &ScriptBuf, + private_keys: &[PrivateKey], + sighash_message: &secp256k1::Message, +) -> Result>> { + let (required, pubkeys) = parse_multisig(script)?; + + if private_keys.len() != required { + return Err(Error::SignatureCount { + required, + provided: private_keys.len(), + }); + } + + let signer_pubkeys: Vec = private_keys + .iter() + .map(|private_key| private_key.public_key(secp).inner) + .collect(); + + for (i, pubkey) in signer_pubkeys.iter().enumerate() { + if signer_pubkeys[..i].contains(pubkey) { + return Err(Error::DuplicateSigner); + } + } + + let mut used = vec![false; private_keys.len()]; + let mut signatures = Vec::with_capacity(required); + + for pubkey in &pubkeys { + if let Some(i) = + (0..signer_pubkeys.len()).find(|&i| !used[i] && signer_pubkeys[i] == pubkey.inner) + { + used[i] = true; + signatures.push( + bitcoin::ecdsa::Signature { + signature: secp.sign_ecdsa(sighash_message, &private_keys[i].inner), + sighash_type: EcdsaSighashType::All, + } + .to_vec(), + ); + } + } + + if signatures.len() != required { + return Err(Error::UnknownSigner); + } + + Ok(signatures) +} + +pub(crate) fn push_only_script(script: &ScriptBuf) -> ScriptBuf { + let mut push_bytes = bitcoin::script::PushBytesBuf::new(); + push_bytes + .extend_from_slice(script.as_bytes()) + .expect("witness program fits in push"); + ScriptBuf::builder().push_slice(push_bytes).into_script() +} diff --git a/src/verify.rs b/src/verify.rs index acb01138..9ee21880 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -73,6 +73,14 @@ pub fn verify_full( verify_full_p2tr(address, message, to_sign, pub_key) } + AddressData::Segwit { witness_program } + if witness_program.version().to_num() == 0 + && witness_program.program().len() == 32 + && !to_sign.input.is_empty() + && to_sign.input[0].witness.len() > 2 => + { + verify_full_p2wsh(address, message, to_sign) + } AddressData::Segwit { witness_program } if witness_program.version().to_num() == 0 && witness_program.program().len() == 20 @@ -84,13 +92,18 @@ pub fn verify_full( verify_full_p2wpkh(address, message, to_sign, pub_key, false) } - AddressData::P2sh { script_hash: _ } - if !to_sign.input.is_empty() && to_sign.input[0].witness.len() > 1 => - { - let pub_key = - PublicKey::from_slice(&to_sign.input[0].witness[1]).map_err(|_| Error::InvalidPublicKey)?; - - verify_full_p2wpkh(address, message, to_sign, pub_key, true) + AddressData::P2sh { script_hash: _ } => { + let input = to_sign.input.first().ok_or(Error::ToSignInvalid)?; + match input.witness.len() { + 0 => verify_full_p2sh_multisig(address, message, to_sign), + 2 => { + let pub_key = + PublicKey::from_slice(&input.witness[1]).map_err(|_| Error::InvalidPublicKey)?; + verify_full_p2wpkh(address, message, to_sign, pub_key, true) + } + n if n > 2 => verify_full_p2wsh(address, message, to_sign), + _ => Err(Error::InvalidWitness), + } } _ => Err(Error::UnsupportedAddress { address: address.to_string(), @@ -98,6 +111,30 @@ pub fn verify_full( } } +#[allow(clippy::result_large_err)] +fn check_to_sign(to_spend: &Transaction, to_sign: &Transaction) -> Result<()> { + let to_spend_outpoint = OutPoint { + txid: to_spend.compute_txid(), + vout: 0, + }; + + let op_return = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .into_script(); + + if !matches!(to_sign.version, Version(0) | Version(2)) + || to_sign.input.len() != 1 + || to_sign.input[0].previous_output != to_spend_outpoint + || to_sign.output.len() != 1 + || to_sign.output[0].value != Amount::from_sat(0) + || to_sign.output[0].script_pubkey != op_return + { + return Err(Error::ToSignInvalid); + } + + Ok(()) +} + #[allow(clippy::result_large_err)] fn verify_full_p2wpkh( address: &Address, @@ -107,22 +144,14 @@ fn verify_full_p2wpkh( is_p2sh: bool, ) -> Result<()> { let to_spend = create_to_spend(address, message)?; - let to_sign = create_to_sign(&to_spend, Some(to_sign.input[0].witness.clone()))?; - let to_spend_outpoint = OutPoint { - txid: to_spend.compute_txid(), - vout: 0, - }; + check_to_sign(&to_spend, &to_sign)?; - if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output { - return Err(Error::ToSignInvalid); - } + let witness = to_sign.input[0].witness.clone(); - let witness = if let Some(witness) = to_sign.inputs[0].final_script_witness.clone() { - witness - } else { + if witness.is_empty() { return Err(Error::WitnessEmpty); - }; + } if witness.len() != 2 { return Err(Error::InvalidWitness); @@ -135,6 +164,26 @@ fn verify_full_p2wpkh( return Err(Error::PublicKeyMismatch); } + let p2wpkh_script = ScriptBuf::new_p2wpkh( + &pub_key + .wpubkey_hash() + .context(error::UncompressedPublicKey)?, + ); + + let expected_script_pubkey = if is_p2sh { + ScriptBuf::new_p2sh(&p2wpkh_script.script_hash()) + } else { + p2wpkh_script.clone() + }; + + if address.script_pubkey() != expected_script_pubkey { + return Err(Error::PublicKeyMismatch); + } + + if !is_p2sh && !to_sign.input[0].script_sig.is_empty() { + return Err(Error::ToSignInvalid); + } + let signature_length = encoded_signature.len(); let (signature, sighash_type) = match signature_length { @@ -159,19 +208,10 @@ fn verify_full_p2wpkh( }); } - let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); + let mut sighash_cache = SighashCache::new(to_sign); let sighash = sighash_cache - .p2wpkh_signature_hash( - 0, - &if is_p2sh { - ScriptBuf::new_p2wpkh(&pub_key.wpubkey_hash().unwrap()) - } else { - to_spend.output[0].script_pubkey.clone() - }, - to_spend.output[0].value, - sighash_type, - ) + .p2wpkh_signature_hash(0, &p2wpkh_script, to_spend.output[0].value, sighash_type) .expect("signature hash should compute"); let message = @@ -192,22 +232,18 @@ fn verify_full_p2tr( pub_key: XOnlyPublicKey, ) -> Result<()> { let to_spend = create_to_spend(address, message)?; - let to_sign = create_to_sign(&to_spend, Some(to_sign.input[0].witness.clone()))?; - let to_spend_outpoint = OutPoint { - txid: to_spend.compute_txid(), - vout: 0, - }; + check_to_sign(&to_spend, &to_sign)?; - if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output { + if !to_sign.input[0].script_sig.is_empty() { return Err(Error::ToSignInvalid); } - let witness = if let Some(witness) = to_sign.inputs[0].final_script_witness.clone() { - witness - } else { + let witness = to_sign.input[0].witness.clone(); + + if witness.is_empty() { return Err(Error::WitnessEmpty); - }; + } let encoded_signature = witness.to_vec()[0].clone(); @@ -236,7 +272,7 @@ fn verify_full_p2tr( }); } - let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); + let mut sighash_cache = SighashCache::new(to_sign); let sighash = sighash_cache .taproot_key_spend_signature_hash( @@ -256,3 +292,181 @@ fn verify_full_p2tr( .verify_schnorr(&signature, &message, &pub_key) .context(error::SignatureInvalid) } + +/// Verify a BIP-322 proof for a P2WSH +#[allow(clippy::result_large_err)] +fn verify_full_p2wsh( + address: &Address, + message: impl AsRef<[u8]>, + to_sign: Transaction, +) -> Result<()> { + let to_spend = create_to_spend(address, message)?; + + check_to_sign(&to_spend, &to_sign)?; + + let items = to_sign.input[0].witness.to_vec(); + + if items.len() < 3 { + return Err(Error::InvalidWitness); + } + + if !items[0].is_empty() { + return Err(Error::InvalidWitness); + } + + let witness_script = ScriptBuf::from_bytes(items[items.len() - 1].clone()); + + let program = ScriptBuf::new_p2wsh(&witness_script.wscript_hash()); + let spk = address.script_pubkey(); + + let expected_script_sig = if spk == ScriptBuf::new_p2sh(&program.script_hash()) { + push_only_script(&program) + } else if spk == program { + ScriptBuf::new() + } else { + return Err(Error::ToSignInvalid); + }; + + if to_sign.input[0].script_sig != expected_script_sig { + return Err(Error::ToSignInvalid); + } + + let (required, pubkeys) = parse_multisig(&witness_script)?; + + let signatures = &items[1..items.len() - 1]; + if signatures.len() != required { + return Err(Error::InvalidWitness); + } + + let sighash = SighashCache::new(&to_sign) + .p2wsh_signature_hash( + 0, + &witness_script, + to_spend.output[0].value, + EcdsaSighashType::All, + ) + .expect("signature hash should compute"); + + let message = + Message::from_digest_slice(sighash.as_ref()).expect("should be cryptographically secure hash"); + + let secp = Secp256k1::verification_only(); + + let mut sig_index = 0usize; + for pub_key in &pubkeys { + if sig_index == signatures.len() { + break; + } + + let encoded = &signatures[sig_index]; + let length = encoded.len(); + if length < 1 { + return Err(Error::InvalidWitness); + } + + let sighash_type = EcdsaSighashType::from_standard(encoded[length - 1] as u32) + .context(error::SigHashTypeNonStandard)?; + + if sighash_type != EcdsaSighashType::All { + return Err(Error::SigHashTypeUnsupported { + sighash_type: sighash_type.to_string(), + }); + } + + if let Ok(signature) = bitcoin::secp256k1::ecdsa::Signature::from_der(&encoded[..length - 1]) { + if secp + .verify_ecdsa(&message, &signature, &pub_key.inner) + .is_ok() + { + sig_index += 1; + } + } + } + + if sig_index == signatures.len() { + Ok(()) + } else { + Err(Error::SignatureInvalid { + source: bitcoin::secp256k1::Error::IncorrectSignature, + }) + } +} + +/// Verify a BIP-322 proof for a P2SH multisig address +#[allow(clippy::result_large_err)] +fn verify_full_p2sh_multisig( + address: &Address, + message: impl AsRef<[u8]>, + to_sign: Transaction, +) -> Result<()> { + let to_spend = create_to_spend(address, message)?; + + check_to_sign(&to_spend, &to_sign)?; + + let mut pushes: Vec> = Vec::new(); + for instruction in to_sign.input[0].script_sig.instructions() { + match instruction.map_err(|_| Error::InvalidWitness)? { + Instruction::PushBytes(b) => pushes.push(b.as_bytes().to_vec()), + _ => return Err(Error::InvalidWitness), + } + } + + let Some((redeem_bytes, sig_pushes)) = pushes.split_last() else { + return Err(Error::InvalidWitness); + }; + let redeem_script = ScriptBuf::from_bytes(redeem_bytes.clone()); + + if address.script_pubkey() != ScriptBuf::new_p2sh(&redeem_script.script_hash()) { + return Err(Error::ToSignInvalid); + } + + let (required_signatures, pubkeys) = parse_multisig(&redeem_script)?; + + let Some((null_dummy, signatures)) = sig_pushes.split_first() else { + return Err(Error::InvalidWitness); + }; + + if !null_dummy.is_empty() + || signatures.iter().any(|signature| signature.is_empty()) + || signatures.len() != required_signatures + { + return Err(Error::InvalidWitness); + } + + let sighash = SighashCache::new(&to_sign) + .legacy_signature_hash(0, &redeem_script, EcdsaSighashType::All.to_u32()) + .expect("signature hash should compute"); + let message = + Message::from_digest_slice(sighash.as_ref()).expect("should be cryptographically secure hash"); + + let secp = Secp256k1::verification_only(); + + let mut key_index = 0usize; + for encoded in signatures { + let Some((sighash_byte, der)) = encoded.split_last() else { + return Err(Error::InvalidWitness); + }; + + let sighash_type = EcdsaSighashType::from_standard(*sighash_byte as u32) + .context(error::SigHashTypeNonStandard)?; + + if sighash_type != EcdsaSighashType::All { + return Err(Error::SigHashTypeUnsupported { + sighash_type: sighash_type.to_string(), + }); + } + + let signature = + bitcoin::secp256k1::ecdsa::Signature::from_der(der).context(error::SignatureInvalid)?; + + let offset = pubkeys[key_index..] + .iter() + .position(|pk| secp.verify_ecdsa(&message, &signature, &pk.inner).is_ok()) + .ok_or(Error::SignatureInvalid { + source: bitcoin::secp256k1::Error::IncorrectSignature, + })?; + key_index += offset + 1; + } + + Ok(()) +}