diff --git a/crates/wallet/src/bip322/mod.rs b/crates/wallet/src/bip322/mod.rs new file mode 100644 index 000000000..8d70997a1 --- /dev/null +++ b/crates/wallet/src/bip322/mod.rs @@ -0,0 +1,470 @@ +//! The BIP-322 module provides functionality for message signing +//! according to the BIP-322 standard. + +pub mod sign; +pub mod utils; +pub mod verify; +use alloc::{string::String, vec::Vec}; +use bitcoin::{OutPoint, ScriptBuf, Sequence, Witness}; +use core::fmt; + +/// BIP322Signature encapsulates all the data and functionality required to sign a message +/// according to the BIP322 specification. It supports multiple signature formats: +/// - **Legacy:** Produces a standard ECDSA signature for P2PKH addresses. +/// - **Simple:** Creates a simplified signature that encodes witness data. +/// - **Full:** Constructs a complete transaction with witness details and signs it. +/// +/// # Fields +/// - `private_key_str`: A WIF-encoded private key as a `String`. +/// - `message`: The message to be signed. +/// - `address_str`: The Bitcoin address associated with the signing process. +/// - `signature_type`: The signature format to use, defined by `Bip322SignatureFormat`. +/// - `proof_of_funds`: An optional vector of tuples providing additional UTXO information +/// (proof of funds) used in advanced signing scenarios. +pub struct BIP322Signature { + private_key_str: String, + message: String, + address_str: String, + signature_type: Bip322SignatureFormat, + proof_of_funds: Option>, +} + +/// BIP322Verification encapsulates the data and functionality required to verify a message +/// signature according to the BIP322 protocol. It supports verifying signatures produced +/// using different signature formats: +/// - **Legacy:** Standard ECDSA signatures. +/// - **Simple:** Simplified signatures that encapsulate witness data. +/// - **Full:** Fully signed transactions with witness details. +/// +/// # Fields +/// - `address_str`: The Bitcoin address as a string against which the signature will be verified. +/// - `signature`: A Base64-encoded signature string. +/// - `message`: The original message that was signed. +/// - `signature_type`: The signature format used during signing, defined by `Bip322SignatureFormat`. +/// - `priv_key`: An optional private key string. Required for verifying legacy signatures. +pub struct BIP322Verification { + address_str: String, + signature: String, + message: String, + signature_type: Bip322SignatureFormat, + private_key_str: Option, +} + +/// Represents the different formats supported by the BIP322 message signing protocol. +/// +/// BIP322 defines multiple formats for signatures to accommodate different use cases +/// and maintain backward compatibility with legacy signing methods. +#[derive(Debug, PartialEq)] +pub enum Bip322SignatureFormat { + /// The legacy Bitcoin message signature format used before BIP322. + Legacy, + /// A simplified version of the BIP322 format that includes only essential data. + Simple, + /// The Full BIP322 format that includes all signature data. + Full, +} + +/// Error types for BIP322 message signing and verification operations. +/// +/// This enum encompasses all possible errors that can occur during the BIP322 +/// message signing or verification process. +#[derive(Debug)] +pub enum BIP322Error { + /// Error encountered when extracting data, such as from a PSBT + ExtractionError(String), + /// Unable to compute the signature hash for signing + SighashError, + /// The message does not meet requirements + InvalidMessage, + /// The script or address type is not supported + UnsupportedType, + /// The format of the data is invalid for the given context + InvalidFormat(String), + /// The provided private key is invalid + InvalidPrivateKey, + /// The provided public key is invalid + InvalidPublicKey(String), + /// The provided Bitcoin address is invalid + InvalidAddress, + /// The address is not a Segwit address + NotSegwitAddress, + /// The Segwit version is not supported for the given context + UnsupportedSegwitVersion(String), + /// The digital signature is invalid + InvalidSignature(String), + /// The transaction witness data is invalid + InvalidWitness(String), + /// Error encountered when decoding Bitcoin consensus data + DecodeError(String), + /// Error encountered when decoding Base64 data + Base64DecodeError, + /// The provided sighash type is invalid for this context + InvalidSighashType, +} + +impl std::error::Error for BIP322Error {} + +impl std::fmt::Display for BIP322Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + BIP322Error::ExtractionError(e) => write!(f, "Unable to extract {}", e), + BIP322Error::SighashError => write!(f, "Unable to compute signature hash"), + BIP322Error::InvalidMessage => write!(f, "Message hash is not secure"), + BIP322Error::UnsupportedType => write!(f, "Type is not supported"), + BIP322Error::InvalidFormat(e) => write!(f, "Only valid for {} format", e), + BIP322Error::InvalidPrivateKey => write!(f, "Invalid private key"), + BIP322Error::InvalidPublicKey(e) => write!(f, "Invalid public key {}", e), + BIP322Error::InvalidAddress => write!(f, "Invalid address"), + BIP322Error::NotSegwitAddress => write!(f, "Not a Segwit address"), + BIP322Error::UnsupportedSegwitVersion(e) => write!(f, "Only Segwit {} is supported", e), + BIP322Error::InvalidSignature(e) => write!(f, "Invalid Signature - {}", e), + BIP322Error::InvalidWitness(e) => write!(f, "Invalid Witness - {}", e), + BIP322Error::InvalidSighashType => write!(f, "Sighash type is invalid"), + BIP322Error::DecodeError(e) => write!(f, "Consensus decode error - {}", e), + BIP322Error::Base64DecodeError => write!(f, "Base64 decoding failed"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::bip322::utils::{tagged_message_hash, to_sign, to_spend}; + + use super::*; + use bitcoin::Address; + use core::str::FromStr; + use std::string::ToString; + + // TEST VECTORS FROM + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#user-content-Test_vectors + // https://github.com/Peach2Peach/bip322-js/tree/main/test + + const PRIVATE_KEY: &str = "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k"; + const PRIVATE_KEY_TESTNET: &str = "cTrF79uahxMC7bQGWh2931vepWPWqS8KtF8EkqgWwv3KMGZNJ2yP"; + + const SEGWIT_ADDRESS: &str = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l"; + const SEGWIT_TESTNET_ADDRESS: &str = "tb1q9vza2e8x573nczrlzms0wvx3gsqjx7vaxwd45v"; + const TAPROOT_ADDRESS: &str = "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3"; + const TAPROOT_TESTNET_ADDRESS: &str = + "tb1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5s3g3s37"; + const LEGACY_ADDRESS: &str = "14vV3aCHBeStb5bkenkNHbe2YAFinYdXgc"; + const LEGACY_ADDRESS_TESTNET: &str = "mjSSLdHFzft9NC5NNMik7WrMQ9rRhMhNpT"; + + const HELLO_WORLD_MESSAGE: &str = "Hello World"; + + const NESTED_SEGWIT_PRIVATE_KEY: &str = "KwTbAxmBXjoZM3bzbXixEr9nxLhyYSM4vp2swet58i19bw9sqk5z"; + const NESTED_SEGWIT_TESTNET_PRIVATE_KEY: &str = + "cMpadsm2xoVpWV5FywY5cAeraa1PCtSkzrBM45Ladpf9rgDu6cMz"; + const NESTED_SEGWIT_ADDRESS: &str = "3HSVzEhCFuH9Z3wvoWTexy7BMVVp3PjS6f"; + const NESTED_SEGWIT_TESTNET_ADDRESS: &str = "2N8zi3ydDsMnVkqaUUe5Xav6SZqhyqEduap"; + + #[test] + fn test_message_hashing() { + let empty_hash = tagged_message_hash(b""); + let hello_world_hash = tagged_message_hash(b"Hello World"); + + assert_eq!( + empty_hash.to_string(), + "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1" + ); + assert_eq!( + hello_world_hash.to_string(), + "f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a" + ); + } + + #[test] + fn test_to_spend_and_to_sign() { + let script_pubkey = Address::from_str(SEGWIT_ADDRESS) + .unwrap() + .assume_checked() + .script_pubkey(); + + // Test case for empty message - to_spend + let tx_spend_empty_msg = to_spend(&script_pubkey, ""); + assert_eq!( + tx_spend_empty_msg.compute_txid().to_string(), + "c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7" + ); + + // Test case for "Hello World" - to_spend + let tx_spend_hello_world_msg = to_spend(&script_pubkey, HELLO_WORLD_MESSAGE); + assert_eq!( + tx_spend_hello_world_msg.compute_txid().to_string(), + "b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b" + ); + + // Test case for empty message - to_sign + let tx_sign_empty_msg = to_sign( + &tx_spend_empty_msg.output[0].script_pubkey, + tx_spend_empty_msg.compute_txid(), + tx_spend_empty_msg.lock_time, + tx_spend_empty_msg.input[0].sequence, + Some(tx_spend_empty_msg.input[0].witness.clone()), + ) + .unwrap(); + assert_eq!( + tx_sign_empty_msg.unsigned_tx.compute_txid().to_string(), + "1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6" + ); + + // Test case for HELLO_WORLD_MESSAGE - to_sign + let tx_sign_hw_msg = to_sign( + &tx_spend_hello_world_msg.output[0].script_pubkey, + tx_spend_hello_world_msg.compute_txid(), + tx_spend_hello_world_msg.lock_time, + tx_spend_hello_world_msg.input[0].sequence, + Some(tx_spend_hello_world_msg.input[0].witness.clone()), + ) + .unwrap(); + + assert_eq!( + tx_sign_hw_msg.unsigned_tx.compute_txid().to_string(), + "88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf" + ); + } + + #[test] + fn sign_and_verify_legacy_signature() { + let legacy_sign = BIP322Signature::new( + PRIVATE_KEY.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + LEGACY_ADDRESS.to_string(), + Bip322SignatureFormat::Legacy, + None, + ); + let sign_message = legacy_sign.sign().unwrap(); + + let legacy_sign_testnet = BIP322Signature::new( + PRIVATE_KEY_TESTNET.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + LEGACY_ADDRESS_TESTNET.to_string(), + Bip322SignatureFormat::Legacy, + None, + ); + let sign_message_testnet = legacy_sign_testnet.sign().unwrap(); + + let legacy_verify = BIP322Verification::new( + LEGACY_ADDRESS.to_string(), + sign_message, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Legacy, + Some(PRIVATE_KEY.to_string()), + ); + + let verify_message = legacy_verify.verify().unwrap(); + + let legacy_verify_testnet = BIP322Verification::new( + LEGACY_ADDRESS.to_string(), + sign_message_testnet, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Legacy, + Some(PRIVATE_KEY_TESTNET.to_string()), + ); + + let verify_message_testnet = legacy_verify_testnet.verify().unwrap(); + + assert!(verify_message); + assert!(verify_message_testnet); + } + + #[test] + fn sign_and_verify_legacy_signature_with_wrong_message() { + let legacy_sign = BIP322Signature::new( + PRIVATE_KEY.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + LEGACY_ADDRESS.to_string(), + Bip322SignatureFormat::Legacy, + None, + ); + let sign_message = legacy_sign.sign().unwrap(); + + let legacy_verify = BIP322Verification::new( + LEGACY_ADDRESS.to_string(), + sign_message, + "".to_string(), + Bip322SignatureFormat::Legacy, + Some(PRIVATE_KEY.to_string()), + ); + + let verify_message = legacy_verify.verify().unwrap(); + + assert_eq!(verify_message, false); + } + + #[test] + fn test_sign_and_verify_nested_segwit_address() { + let nested_segwit_simple_sign = BIP322Signature::new( + NESTED_SEGWIT_PRIVATE_KEY.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + NESTED_SEGWIT_ADDRESS.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + + let sign_message = nested_segwit_simple_sign.sign().unwrap(); + assert_eq!(sign_message.clone(), "AkgwRQIhAMd2wZSY3x0V9Kr/NClochoTXcgDaGl3OObOR17yx3QQAiBVWxqNSS+CKen7bmJTG6YfJjsggQ4Fa2RHKgBKrdQQ+gEhAxa5UDdQCHSQHfKQv14ybcYm1C9y6b12xAuukWzSnS+w"); + + let nested_segwit_full_sign_testnet = BIP322Signature::new( + NESTED_SEGWIT_TESTNET_PRIVATE_KEY.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + NESTED_SEGWIT_TESTNET_ADDRESS.to_string(), + Bip322SignatureFormat::Full, + None, + ); + + let sign_message_testnet = nested_segwit_full_sign_testnet.sign().unwrap(); + assert_eq!(sign_message_testnet.clone(), "AAAAAAABAVuR8vsJiiYj9+vO+8l7Ol3wt3Frz7SVyVSxn0ehOUb+AAAAAAAAAAAAAQAAAAAAAAAAAWoCSDBFAiEAx3bBlJjfHRX0qv80KWhyGhNdyANoaXc45s5HXvLHdBACIFVbGo1JL4Ip6ftuYlMbph8mOyCBDgVrZEcqAEqt1BD6ASEDFrlQN1AIdJAd8pC/XjJtxibUL3LpvXbEC66RbNKdL7AAAAAA"); + + let nested_segwit_full_verify = BIP322Verification::new( + NESTED_SEGWIT_ADDRESS.to_string(), + sign_message, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + + let verify_message = nested_segwit_full_verify.verify().unwrap(); + + let nested_segwit_full_verify_testnet = BIP322Verification::new( + NESTED_SEGWIT_TESTNET_ADDRESS.to_string(), + sign_message_testnet, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Full, + None, + ); + + let verify_message_testnet = nested_segwit_full_verify_testnet.verify().unwrap(); + + assert!(verify_message); + assert!(verify_message_testnet); + } + + #[test] + fn test_sign_and_verify_segwit_address() { + let full_sign = BIP322Signature::new( + PRIVATE_KEY.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + SEGWIT_ADDRESS.to_string(), + Bip322SignatureFormat::Full, + None, + ); + let sign_message = full_sign.sign().unwrap(); + + let simple_sign = BIP322Signature::new( + PRIVATE_KEY_TESTNET.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + SEGWIT_TESTNET_ADDRESS.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + let sign_message_testnet = simple_sign.sign().unwrap(); + + let full_verify = BIP322Verification::new( + SEGWIT_ADDRESS.to_string(), + sign_message, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Full, + None, + ); + + let verify_message = full_verify.verify().unwrap(); + + let simple_verify = BIP322Verification::new( + SEGWIT_TESTNET_ADDRESS.to_string(), + sign_message_testnet, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + + let verify_message_testnet = simple_verify.verify().unwrap(); + + assert!(verify_message); + assert!(verify_message_testnet); + } + + #[test] + fn test_sign_and_verify_taproot_address() { + let full_sign = BIP322Signature::new( + PRIVATE_KEY.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + TAPROOT_ADDRESS.to_string(), + Bip322SignatureFormat::Full, + None, + ); + let sign_message = full_sign.sign().unwrap(); + + let simple_sign = BIP322Signature::new( + PRIVATE_KEY.to_string(), + HELLO_WORLD_MESSAGE.to_string(), + TAPROOT_TESTNET_ADDRESS.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + + let sign_message_testnet = simple_sign.sign().unwrap(); + + let full_verify = BIP322Verification::new( + TAPROOT_ADDRESS.to_string(), + sign_message, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Full, + None, + ); + + let verify_message = full_verify.verify().unwrap(); + + let simple_verify = BIP322Verification::new( + TAPROOT_TESTNET_ADDRESS.to_string(), + sign_message_testnet, + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + + let verify_message_testnet = simple_verify.verify().unwrap(); + + assert!(verify_message); + assert!(verify_message_testnet); + } + + #[test] + fn test_simple_segwit_verification() { + let simple_verify = BIP322Verification::new( + SEGWIT_ADDRESS.to_string(), + "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=".to_string(), + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + assert!(simple_verify.verify().unwrap()); + + let simple_verify_2 = BIP322Verification::new( + SEGWIT_ADDRESS.to_string(), + "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy".to_string(), + HELLO_WORLD_MESSAGE.to_string(), + Bip322SignatureFormat::Simple, + None, + ); + assert!(simple_verify_2.verify().unwrap()); + + let simple_verify_empty_message = BIP322Verification::new( + SEGWIT_ADDRESS.to_string(), + "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy".to_string(), + "".to_string(), + Bip322SignatureFormat::Simple, + None, + ); + assert!(simple_verify_empty_message.verify().unwrap()); + + let simple_verify_empty_message_2 = BIP322Verification::new( + SEGWIT_ADDRESS.to_string(), + "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=".to_string(), + "".to_string(), + Bip322SignatureFormat::Simple, + None, + ); + assert!(simple_verify_empty_message_2.verify().unwrap()); + } +} diff --git a/crates/wallet/src/bip322/sign.rs b/crates/wallet/src/bip322/sign.rs new file mode 100644 index 000000000..bec1a6056 --- /dev/null +++ b/crates/wallet/src/bip322/sign.rs @@ -0,0 +1,286 @@ +//! The signature generation implementation for BIP-322 for message signing +//! according to the BIP-322 standard. + +use alloc::{string::String, vec::Vec}; +use bitcoin::{ + absolute::LockTime, + base64::{prelude::BASE64_STANDARD, Engine}, + consensus::encode::serialize, + key::{Keypair, TapTweak}, + psbt::Input, + secp256k1::{ecdsa::Signature, Message}, + sighash::{self, SighashCache}, + sign_message::signed_msg_hash, + transaction::Version, + Address, Amount, EcdsaSighashType, OutPoint, PrivateKey, Psbt, PublicKey, ScriptBuf, Sequence, + TapSighashType, Transaction, TxIn, TxOut, Witness, +}; +use core::str::FromStr; +use std::string::ToString; + +use crate::utils::SecpCtx; + +use super::{ + utils::{to_sign, to_spend}, + BIP322Error, BIP322Signature, Bip322SignatureFormat, +}; + +impl BIP322Signature { + /// Creates a new instance of `BIP322Signature` with the specified parameters. + /// + /// # Arguments + /// - `private_key_str`: A WIF-encoded private key as a `String`. + /// - `message`: The message to be signed. + /// - `address`: The Bitcoin address for which the signature is intended. + /// - `signature_type`: The BIP322 signature format to be used. Can be one of Legacy, Simple, or Full. + /// - `proof_of_funds`: An optional vector of UTXO information tuples, where each tuple contains: + /// - `OutPoint`: A reference to a previous transaction output. + /// - `ScriptBuf`: The script for the UTXO. + /// - `Witness`: The witness data associated with the UTXO. + /// - `Sequence`: The sequence number. + /// + /// # Returns + /// An instance of `BIP322Signature`. + /// + /// # Example + /// ``` + /// # use bdk_wallet::bip322::{BIP322Signature, Bip322SignatureFormat}; + /// + /// let signer = BIP322Signature::new( + /// "c...".to_string(), + /// "Hello, Bitcoin!".to_string(), + /// "1BitcoinAddress...".to_string(), + /// Bip322SignatureFormat::Legacy, + /// None, + /// ); + /// ``` + pub fn new( + private_key_str: String, + message: String, + address_str: String, + signature_type: Bip322SignatureFormat, + proof_of_funds: Option>, + ) -> Self { + Self { + private_key_str, + message, + address_str, + signature_type, + proof_of_funds, + } + } + + /// Signs a message using a provided private key, message, and address with a specified + /// BIP322 format (Legacy, Simple, or Full). + /// + /// - **Legacy:** Generates a traditional ECDSA signature for P2PKH addresses. + /// - **Simple:** Constructs a simplified signature by signing the message and encoding + /// the witness data. + /// - **Full:** Creates a comprehensive signature by building and signing an entire + /// transaction, including all witness details. + /// + /// The function extracts the necessary key and script information from the input, + /// processes any optional proof of funds, and returns the resulting signature as a + /// Base64-encoded string. + /// + /// # Errors + /// Returns a `BIP322Error` if any signing steps fail. + pub fn sign(&self) -> Result { + let secp = SecpCtx::new(); + let private_key = PrivateKey::from_wif(&self.private_key_str) + .map_err(|_| BIP322Error::InvalidPrivateKey)?; + let pubkey = private_key.public_key(&secp); + + let script_pubkey = Address::from_str(&self.address_str) + .map_err(|_| BIP322Error::InvalidAddress)? + .assume_checked() + .script_pubkey(); + + match &self.signature_type { + Bip322SignatureFormat::Legacy => { + if !script_pubkey.is_p2pkh() { + return Err(BIP322Error::InvalidFormat("legacy".to_string())); + } + + let sig_serialized = self.sign_legacy(&private_key)?; + Ok(BASE64_STANDARD.encode(sig_serialized)) + } + Bip322SignatureFormat::Simple => { + let witness = self.sign_message(&private_key, pubkey, &script_pubkey)?; + + Ok(BASE64_STANDARD.encode(serialize(&witness.input[0].witness.clone()))) + } + Bip322SignatureFormat::Full => { + let transaction = self.sign_message(&private_key, pubkey, &script_pubkey)?; + + Ok(BASE64_STANDARD.encode(serialize(&transaction))) + } + } + } + + /// Constructs a transaction that includes a signature for the provided message + /// according to the BIP322 message signing protocol. + /// + /// This function builds the transaction to be signed by selecting the appropriate + /// signing method based on the script type: + /// - P2WPKH + /// - P2TR + /// - P2SH + /// + /// Optionally, if a proof of funds is provided, additional inputs are appended + /// to support advanced verification scenarios. On success, the function returns a + /// complete transaction + /// + /// # Errors + /// Returns a `BIP322Error` if the script type is unsupported or if any part of + /// the signing process fails. + fn sign_message( + &self, + private_key: &PrivateKey, + pubkey: PublicKey, + script_pubkey: &ScriptBuf, + ) -> Result { + let to_spend = to_spend(script_pubkey, &self.message); + let mut to_sign = to_sign( + &to_spend.output[0].script_pubkey, + to_spend.compute_txid(), + to_spend.lock_time, + to_spend.input[0].sequence, + Some(to_spend.input[0].witness.clone()), + )?; + + if let Some(proofs) = self.proof_of_funds.clone() { + for (previous_output, script_sig, witness, sequence) in proofs { + to_sign.inputs.push(Input { + non_witness_utxo: Some(Transaction { + input: vec![TxIn { + previous_output, + script_sig, + sequence, + witness, + }], + output: vec![], + version: Version(2), + lock_time: LockTime::ZERO, + }), + ..Default::default() + }) + } + } + + let mut sighash_cache = SighashCache::new(&to_sign.unsigned_tx); + + let witness = if script_pubkey.is_p2wpkh() { + self.sign_p2sh_p2wpkh(&mut sighash_cache, to_spend, private_key, pubkey, true)? + } else if script_pubkey.is_p2tr() || script_pubkey.is_p2wsh() { + self.sign_p2tr(&mut sighash_cache, to_spend, to_sign.clone(), private_key)? + } else if script_pubkey.is_p2sh() { + self.sign_p2sh_p2wpkh(&mut sighash_cache, to_spend, private_key, pubkey, false)? + } else { + return Err(BIP322Error::UnsupportedType); + }; + + to_sign.inputs[0].final_script_witness = Some(witness); + + let transaction = to_sign + .extract_tx() + .map_err(|_| BIP322Error::ExtractionError("transaction".to_string()))?; + + Ok(transaction) + } + + fn sign_legacy(&self, private_key: &PrivateKey) -> Result, BIP322Error> { + let secp = SecpCtx::new(); + + let message_hash = signed_msg_hash(&self.message); + let message = &Message::from_digest_slice(message_hash.as_ref()) + .map_err(|_| BIP322Error::InvalidMessage)?; + + let mut signature: Signature = secp.sign_ecdsa(message, &private_key.inner); + signature.normalize_s(); + let mut sig_serialized = signature.serialize_der().to_vec(); + sig_serialized.push(EcdsaSighashType::All as u8); + + Ok(sig_serialized) + } + + fn sign_p2sh_p2wpkh( + &self, + sighash_cache: &mut SighashCache<&Transaction>, + to_spend: Transaction, + private_key: &PrivateKey, + pubkey: PublicKey, + is_segwit: bool, + ) -> Result { + let secp = SecpCtx::new(); + let sighash_type = EcdsaSighashType::All; + + let wpubkey_hash = &pubkey + .wpubkey_hash() + .map_err(|e| BIP322Error::InvalidPublicKey(e.to_string()))?; + + let sighash = sighash_cache + .p2wpkh_signature_hash( + 0, + &if is_segwit { + to_spend.output[0].script_pubkey.clone() + } else { + ScriptBuf::new_p2wpkh(wpubkey_hash) + }, + to_spend.output[0].value, + sighash_type, + ) + .map_err(|_| BIP322Error::SighashError)?; + + let msg = &Message::from_digest_slice(sighash.as_ref()) + .map_err(|_| BIP322Error::InvalidMessage)?; + + let signature = secp.sign_ecdsa(msg, &private_key.inner); + let mut sig_serialized = signature.serialize_der().to_vec(); + sig_serialized.push(sighash_type as u8); + + Ok(Witness::from(vec![ + sig_serialized, + pubkey.inner.serialize().to_vec(), + ])) + } + + fn sign_p2tr( + &self, + sighash_cache: &mut SighashCache<&Transaction>, + to_spend: Transaction, + mut to_sign: Psbt, + private_key: &PrivateKey, + ) -> Result { + let secp = SecpCtx::new(); + let keypair = Keypair::from_secret_key(&secp, &private_key.inner); + let key_pair = keypair + .tap_tweak(&secp, to_sign.inputs[0].tap_merkle_root) + .to_inner(); + let x_only_public_key = keypair.x_only_public_key().0; + + let sighash_type = TapSighashType::All; + + to_sign.inputs[0].tap_internal_key = Some(x_only_public_key); + + let sighash = sighash_cache + .taproot_key_spend_signature_hash( + 0, + &sighash::Prevouts::All(&[TxOut { + value: Amount::from_sat(0), + script_pubkey: to_spend.output[0].clone().script_pubkey, + }]), + sighash_type, + ) + .map_err(|_| BIP322Error::SighashError)?; + + let msg = &Message::from_digest_slice(sighash.as_ref()) + .map_err(|_| BIP322Error::InvalidMessage)?; + + let signature = secp.sign_schnorr_no_aux_rand(msg, &key_pair); + let mut sig_serialized = signature.serialize().to_vec(); + sig_serialized.push(sighash_type as u8); + + Ok(Witness::from(vec![sig_serialized])) + } +} diff --git a/crates/wallet/src/bip322/utils.rs b/crates/wallet/src/bip322/utils.rs new file mode 100644 index 000000000..c9b735a37 --- /dev/null +++ b/crates/wallet/src/bip322/utils.rs @@ -0,0 +1,103 @@ +//! The utility methods for BIP-322 for message signing +//! according to the BIP-322 standard. + +use bitcoin::{ + absolute::LockTime, + blockdata::opcodes::all::OP_RETURN, + hashes::{sha256, Hash, HashEngine}, + opcodes::OP_0, + script::Builder, + transaction::Version, + Amount, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, +}; +use std::string::ToString; + +use super::BIP322Error; + +/// Creates a tagged hash of a message according to the BIP322 specification. +pub fn tagged_message_hash(message: &[u8]) -> sha256::Hash { + let tag = "BIP0322-signed-message"; + let mut engine = sha256::Hash::engine(); + + let tag_hash = sha256::Hash::hash(tag.as_bytes()); + engine.input(&tag_hash[..]); + engine.input(&tag_hash[..]); + engine.input(message); + + sha256::Hash::from_engine(engine) +} + +/// Constructs the "to_spend" transaction according to the BIP322 specification. +pub fn to_spend(script_pubkey: &ScriptBuf, message: &str) -> Transaction { + let txid = Txid::from_slice(&[0u8; 32]).expect("Txid slice error"); + + let outpoint = OutPoint { + txid, + vout: 0xFFFFFFFF, + }; + let message_hash = tagged_message_hash(message.as_bytes()); + let script_sig = Builder::new() + .push_opcode(OP_0) + .push_slice(message_hash.to_byte_array()) + .into_script(); + + Transaction { + version: Version(0), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: outpoint, + script_sig, + sequence: Sequence::ZERO, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(0), + script_pubkey: script_pubkey.clone(), + }], + } +} + +/// Constructs a transaction according to the BIP322 specification. +/// +/// This transaction will be signed to prove ownership of the private key +/// corresponding to the script_pubkey. +/// +/// Returns a PSBT (Partially Signed Bitcoin Transaction) ready for signing +/// or a [`BIP322Error`] if something goes wrong. +pub fn to_sign( + script_pubkey: &ScriptBuf, + txid: Txid, + lock_time: LockTime, + sequence: Sequence, + witness: Option, +) -> Result { + let outpoint = OutPoint { txid, vout: 0x00 }; + let script_pub_key = Builder::new().push_opcode(OP_RETURN).into_script(); + + let tx = Transaction { + version: Version(0), + lock_time, + input: vec![TxIn { + previous_output: outpoint, + sequence, + script_sig: ScriptBuf::new(), + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(0), + script_pubkey: script_pub_key, + }], + }; + + let mut psbt = + Psbt::from_unsigned_tx(tx).map_err(|_| BIP322Error::ExtractionError("psbt".to_string()))?; + + psbt.inputs[0].witness_utxo = Some(TxOut { + value: Amount::from_sat(0), + script_pubkey: script_pubkey.clone(), + }); + + psbt.inputs[0].final_script_witness = witness; + + Ok(psbt) +} diff --git a/crates/wallet/src/bip322/verify.rs b/crates/wallet/src/bip322/verify.rs new file mode 100644 index 000000000..48797450b --- /dev/null +++ b/crates/wallet/src/bip322/verify.rs @@ -0,0 +1,375 @@ +//! The verification implementation of generated signature for BIP-322 for +//! message signing according to the BIP-322 standard. + +use alloc::string::String; +use bitcoin::{ + base64::{prelude::BASE64_STANDARD, Engine}, + blockdata::opcodes::all::OP_RETURN, + consensus::Decodable, + io::Cursor, + secp256k1::{ecdsa::Signature, schnorr, Message}, + sighash::{self, SighashCache}, + sign_message::signed_msg_hash, + Address, Amount, EcdsaSighashType, OutPoint, PrivateKey, Psbt, PublicKey, ScriptBuf, + TapSighashType, Transaction, TxOut, Witness, WitnessVersion, XOnlyPublicKey, +}; +use core::str::FromStr; +use std::string::ToString; + +use crate::utils::SecpCtx; + +use super::{ + utils::{to_sign, to_spend}, + BIP322Error, BIP322Verification, Bip322SignatureFormat, +}; + +impl BIP322Verification { + /// Creates a new instance of `BIP322Verification` with the given parameters. + /// + /// # Arguments + /// - `address_str`: The Bitcoin address (as a string) associated with the signature. + /// - `signature`: The Base64-encoded signature to verify. + /// - `message`: The original message that was signed. + /// - `signature_type`: The BIP322 signature format that was used (Legacy, Simple, or Full). + /// - `priv_key`: An optional private key string used for legacy verification. + /// + /// # Returns + /// An instance of `BIP322Verification`. + /// + /// # Example + /// ``` + /// # use bdk_wallet::bip322::{BIP322Verification, Bip322SignatureFormat}; + /// + /// let verifier = BIP322Verification::new( + /// "1BitcoinAddress...".to_string(), + /// "Base64EncodedSignature==".to_string(), + /// "Hello, Bitcoin!".to_string(), + /// Bip322SignatureFormat::Legacy, + /// Some("c...".to_string()), + /// ); + /// ``` + pub fn new( + address_str: String, + signature: String, + message: String, + signature_type: Bip322SignatureFormat, + private_key_str: Option, + ) -> Self { + Self { + address_str, + signature, + message, + signature_type, + private_key_str, + } + } + + /// Verifies a BIP322 message signature against the provided address and message. + /// + /// The verification logic differs depending on the signature format: + /// - Legacy + /// - Simple + /// - Full + /// + /// Returns `true` if the signature is valid, or an error if the decoding or verification + /// process fails. + pub fn verify(&self) -> Result { + let address = Address::from_str(&self.address_str) + .map_err(|_| BIP322Error::InvalidAddress)? + .assume_checked(); + + let script_pubkey = address.script_pubkey(); + + let signature_bytes = BASE64_STANDARD + .decode(&self.signature) + .map_err(|_| BIP322Error::Base64DecodeError)?; + + match &self.signature_type { + Bip322SignatureFormat::Legacy => { + let pk = &self + .private_key_str + .as_ref() + .ok_or(BIP322Error::InvalidPrivateKey)?; + let private_key = + PrivateKey::from_wif(pk).map_err(|_| BIP322Error::InvalidPrivateKey)?; + self.verify_legacy(&signature_bytes, private_key) + } + Bip322SignatureFormat::Simple => { + let mut cursor = Cursor::new(signature_bytes); + let witness = Witness::consensus_decode_from_finite_reader(&mut cursor) + .map_err(|_| BIP322Error::DecodeError("witness".to_string()))?; + + let to_spend_witness = to_spend(&script_pubkey, &self.message); + let to_sign_witness = to_sign( + &to_spend_witness.output[0].script_pubkey, + to_spend_witness.compute_txid(), + to_spend_witness.lock_time, + to_spend_witness.input[0].sequence, + Some(witness), + ) + .map_err(|_| BIP322Error::ExtractionError("psbt".to_string()))? + .extract_tx() + .map_err(|_| BIP322Error::ExtractionError("transaction".to_string()))?; + + self.verify_message(address, to_sign_witness) + } + Bip322SignatureFormat::Full => { + let mut cursor = Cursor::new(signature_bytes); + let transaction = Transaction::consensus_decode_from_finite_reader(&mut cursor) + .map_err(|_| BIP322Error::DecodeError("transaction".to_string()))?; + + self.verify_message(address, transaction) + } + } + } + + /// Verifies a BIP322-signed message by reconstructing the underlying transaction data + /// and checking the signature against the provided address and message. + /// + /// This function performs the following steps: + /// 1. Constructs a corresponding signing transaction (`to_sign`) using the witness data + /// from the given transaction. + /// 2. It delegates the verification process to the appropriate helper function: + /// - P2WPKH + /// - P2TR + /// - P2SH + /// 3. If none of the supported script types match, the function returns `Ok(false)`. + /// + /// # Returns + /// A `Result` containing: + /// - `Ok(true)` if the signature is valid. + /// - `Ok(false)` if the signature does not match the expected verification criteria. + /// - An error of type `BIP322Error` if the verification process fails at any step, + /// such as during transaction reconstruction or when decoding the witness data. + fn verify_message( + &self, + address: Address, + transaction: Transaction, + ) -> Result { + let script_pubkey = address.script_pubkey(); + + let to_spend = to_spend(&script_pubkey, &self.message); + let to_sign = to_sign( + &to_spend.output[0].script_pubkey, + to_spend.compute_txid(), + to_spend.lock_time, + to_spend.input[0].sequence, + Some(transaction.input[0].witness.clone()), + )?; + + if script_pubkey.is_p2wpkh() { + let verify = self.verify_p2sh_p2wpkh(address, transaction, to_spend, to_sign, true)?; + + return Ok(verify); + } else if script_pubkey.is_p2tr() || script_pubkey.is_p2wsh() { + let verify = self.verify_p2tr(address, to_spend, to_sign, transaction)?; + + return Ok(verify); + } else if script_pubkey.is_p2sh() { + let verify = self.verify_p2sh_p2wpkh(address, transaction, to_spend, to_sign, false)?; + + return Ok(verify); + } + + Ok(false) + } + + fn verify_legacy( + &self, + signature_bytes: &[u8], + private_key: PrivateKey, + ) -> Result { + let secp = SecpCtx::new(); + + let sig_without_sighash = &signature_bytes[..signature_bytes.len() - 1]; + + let pub_key = PublicKey::from_private_key(&secp, &private_key); + + let message_hash = signed_msg_hash(&self.message); + let msg = &Message::from_digest_slice(message_hash.as_ref()) + .map_err(|_| BIP322Error::InvalidMessage)?; + + let sig = Signature::from_der(sig_without_sighash) + .map_err(|e| BIP322Error::InvalidSignature(e.to_string()))?; + + let verify = secp.verify_ecdsa(msg, &sig, &pub_key.inner).is_ok(); + Ok(verify) + } + + fn verify_p2sh_p2wpkh( + &self, + address: Address, + to_sign_witness: Transaction, + to_spend: Transaction, + to_sign: Psbt, + is_segwit: bool, + ) -> Result { + let secp = SecpCtx::new(); + let pub_key = PublicKey::from_slice(&to_sign_witness.input[0].witness[1]) + .map_err(|e| BIP322Error::InvalidPublicKey(e.to_string()))?; + + if is_segwit { + let wp = address + .witness_program() + .ok_or(BIP322Error::NotSegwitAddress)?; + + if wp.version() != WitnessVersion::V0 { + return Err(BIP322Error::UnsupportedSegwitVersion("v0".to_string())); + } + } + + let to_spend_outpoint = OutPoint { + txid: to_spend.compute_txid(), + vout: 0, + }; + + if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output { + return Err(BIP322Error::InvalidSignature( + "to_sign must spend to_spend output".to_string(), + )); + } + + if to_sign.unsigned_tx.output[0].script_pubkey + != ScriptBuf::from_bytes(vec![OP_RETURN.to_u8()]) + { + return Err(BIP322Error::InvalidSignature( + "to_sign output must be OP_RETURN".to_string(), + )); + } + + let witness = to_sign.inputs[0] + .final_script_witness + .clone() + .ok_or(BIP322Error::InvalidWitness("missing data".to_string()))?; + + let encoded_signature = &witness.to_vec()[0]; + let witness_pub_key = &witness.to_vec()[1]; + let signature_length = encoded_signature.len(); + + if witness.len() != 2 { + return Err(BIP322Error::InvalidWitness( + "invalid witness length".to_string(), + )); + } + + if pub_key.to_bytes() != *witness_pub_key { + return Err(BIP322Error::InvalidPublicKey( + "public key mismatch".to_string(), + )); + } + + let signature = Signature::from_der(&encoded_signature.as_slice()[..signature_length - 1]) + .map_err(|e| BIP322Error::InvalidSignature(e.to_string()))?; + let sighash_type = + EcdsaSighashType::from_consensus(encoded_signature[signature_length - 1] as u32); + + if !(sighash_type == EcdsaSighashType::All) { + return Err(BIP322Error::InvalidSighashType); + } + + let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); + let wpubkey_hash = &pub_key + .wpubkey_hash() + .map_err(|e| BIP322Error::InvalidPublicKey(e.to_string()))?; + + let sighash = sighash_cache + .p2wpkh_signature_hash( + 0, + &if is_segwit { + to_spend.output[0].script_pubkey.clone() + } else { + ScriptBuf::new_p2wpkh(wpubkey_hash) + }, + to_spend.output[0].value, + sighash_type, + ) + .map_err(|_| BIP322Error::SighashError)?; + + let msg = &Message::from_digest_slice(sighash.as_ref()) + .map_err(|_| BIP322Error::InvalidMessage)?; + + Ok(secp.verify_ecdsa(msg, &signature, &pub_key.inner).is_ok()) + } + + fn verify_p2tr( + &self, + address: Address, + to_spend: Transaction, + to_sign: Psbt, + to_sign_witness: Transaction, + ) -> Result { + let secp = SecpCtx::new(); + let script_pubkey = address.script_pubkey(); + let witness_program = script_pubkey.as_bytes(); + + let pubkey_bytes = &witness_program[2..]; + + let pub_key = XOnlyPublicKey::from_slice(pubkey_bytes) + .map_err(|e| BIP322Error::InvalidPublicKey(e.to_string()))?; + + let wp = address + .witness_program() + .ok_or(BIP322Error::NotSegwitAddress)?; + + if wp.version() != WitnessVersion::V1 { + return Err(BIP322Error::UnsupportedSegwitVersion("v1".to_string())); + } + + let to_spend_outpoint = OutPoint { + txid: to_spend.compute_txid(), + vout: 0, + }; + + if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output { + return Err(BIP322Error::InvalidSignature( + "to_sign must spend to_spend output".to_string(), + )); + } + + if to_sign_witness.output[0].script_pubkey != ScriptBuf::from_bytes(vec![OP_RETURN.to_u8()]) + { + return Err(BIP322Error::InvalidSignature( + "to_sign output must be OP_RETURN".to_string(), + )); + } + + let witness = to_sign.inputs[0] + .final_script_witness + .clone() + .ok_or(BIP322Error::InvalidWitness("missing data".to_string()))?; + + let encoded_signature = &witness.to_vec()[0]; + if witness.len() != 1 { + return Err(BIP322Error::InvalidWitness( + "invalid witness length".to_string(), + )); + } + + let signature = schnorr::Signature::from_slice(&encoded_signature.as_slice()[..64]) + .map_err(|e| BIP322Error::InvalidSignature(e.to_string()))?; + let sighash_type = TapSighashType::from_consensus_u8(encoded_signature[64]) + .map_err(|_| BIP322Error::InvalidSighashType)?; + + if sighash_type != TapSighashType::All { + return Err(BIP322Error::InvalidSighashType); + } + + let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); + + let sighash = sighash_cache + .taproot_key_spend_signature_hash( + 0, + &sighash::Prevouts::All(&[TxOut { + value: Amount::from_sat(0), + script_pubkey: to_spend.output[0].clone().script_pubkey, + }]), + sighash_type, + ) + .map_err(|_| BIP322Error::SighashError)?; + + let msg = &Message::from_digest_slice(sighash.as_ref()) + .map_err(|_| BIP322Error::InvalidMessage)?; + + Ok(secp.verify_schnorr(&signature, msg, &pub_key).is_ok()) + } +} diff --git a/crates/wallet/src/lib.rs b/crates/wallet/src/lib.rs index d17cc468d..f19b4c37c 100644 --- a/crates/wallet/src/lib.rs +++ b/crates/wallet/src/lib.rs @@ -25,6 +25,7 @@ pub extern crate miniscript; pub extern crate serde; pub extern crate serde_json; +pub mod bip322; pub mod descriptor; pub mod keys; pub mod psbt;