From 4b8feda2d06c2d176a25d7cbfa346cbcae4f7e93 Mon Sep 17 00:00:00 2001 From: raph Date: Sat, 10 Aug 2024 23:12:26 +0200 Subject: [PATCH] Add Snafu error types and proper Rust typed functions (#21) --- Cargo.lock | 63 ++++++++++++++++ Cargo.toml | 1 + src/error.rs | 69 +++++++++++++++-- src/lib.rs | 176 +++++++++++++++++++------------------------ src/sign.rs | 121 ++++++++++++++++++++---------- src/verify.rs | 199 ++++++++++++++++++++++++++----------------------- www/Cargo.lock | 32 +++++++- www/Cargo.toml | 5 +- www/index.js | 13 +--- www/src/lib.rs | 14 +--- 10 files changed, 428 insertions(+), 265 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e9a079..d29affd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ dependencies = [ "hex", "miniscript", "pretty_assertions", + "snafu", ] [[package]] @@ -90,6 +91,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -138,6 +145,24 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "secp256k1" version = "0.28.2" @@ -157,6 +182,44 @@ dependencies = [ "cc", ] +[[package]] +name = "snafu" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b835cb902660db3415a672d862905e791e54d306c6e8189168c7f3d9ae1c79d" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index e10a8f7..78dd613 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ bitcoin = { version = "0.31.2" } bitcoin_hashes = "0.14.0" hex = "0.4.3" miniscript = "11.0.0" +snafu = "0.8.4" [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/error.rs b/src/error.rs index 23956fd..b3cb59d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,63 @@ -#[derive(Debug, PartialEq, Eq)] -pub enum Bip322Error { - InvalidAddress, // for legacy addresses 1... (p2pkh) not supported, also any non taproot - Invalid, // Address no key; pubkey not recovered, invalid signature - MalformedSignature, // wrong length, etc. - InvalidSigHash, // only sighash All and Default supported - NotKeyPathSpend, // only single key path spend supported +use super::*; + +#[derive(Debug, Snafu)] +#[snafu(context(suffix(false)), visibility(pub))] +pub enum Error { + #[snafu(display("Failed to parse address `{address}`"))] + AddressParse { + source: bitcoin::address::ParseError, + address: String, + }, + #[snafu(display("Failed to parse private key"))] + PrivateKeyParse { source: bitcoin::key::Error }, + #[snafu(display("Unsuported address `{address}`, only P2TR allowed"))] + UnsupportedAddress { address: String }, + #[snafu(display("Decode error for signature `{signature}`"))] + SignatureDecode { + source: base64::DecodeError, + signature: String, + }, + #[snafu(display("Transaction encode error"))] + TransactionEncode { source: std::io::Error }, + #[snafu(display("Transaction extract error"))] + TransactionExtract { + source: bitcoin::psbt::ExtractTxError, + }, + #[snafu(display("To sign transaction invalid"))] + ToSignInvalid, + #[snafu(display("PSBT extract error"))] + PsbtExtract { source: bitcoin::psbt::Error }, + #[snafu(display("Base64 decode error for transaction `{transaction}`"))] + TransactionBase64Decode { + source: base64::DecodeError, + transaction: String, + }, + #[snafu(display("Consensus decode error for transaction `{transaction}`"))] + TransactionConsensusDecode { + source: bitcoin::consensus::encode::Error, + transaction: String, + }, + #[snafu(display("Witness malformed"))] + WitnessMalformed { + source: bitcoin::consensus::encode::Error, + }, + #[snafu(display("Witness empty"))] + WitnessEmpty, + #[snafu(display("Encode witness error"))] + WitnessEncoding { source: std::io::Error }, + #[snafu(display("Signature of wrong length `{length}`"))] + SignatureLength { + length: usize, + encoded_signature: Vec, + }, + #[snafu(display("Invalid signature"))] + SignatureInvalid { source: bitcoin::secp256k1::Error }, + #[snafu(display("Invalid sighash"))] + SigHashTypeInvalid { + source: bitcoin::sighash::InvalidSighashTypeError, + }, + #[snafu(display("Unsupported sighash type `{sighash_type}`"))] + SigHashTypeUnsupported { sighash_type: String }, + #[snafu(display("Not key path spend"))] + NotKeyPathSpend, } diff --git a/src/lib.rs b/src/lib.rs index 2a519d6..46a9135 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,24 @@ use { + base64::{engine::general_purpose, Engine}, bitcoin::{ - absolute::LockTime, blockdata::script, opcodes, psbt::Psbt, script::PushBytes, - secp256k1::Secp256k1, transaction::Version, Address, Amount, Network, OutPoint, PrivateKey, - PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, + absolute::LockTime, + address::AddressType, + blockdata::script, + consensus::Decodable, + consensus::Encodable, + key::{Keypair, TapTweak}, + opcodes, + psbt::Psbt, + script::PushBytes, + secp256k1::{self, schnorr::Signature, Message, Secp256k1, XOnlyPublicKey}, + sighash::{self, SighashCache, TapSighashType}, + transaction::Version, + Address, Amount, OutPoint, PrivateKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }, bitcoin_hashes::{sha256, Hash}, + error::Error, + snafu::{ResultExt, Snafu}, + std::{io::Cursor, str::FromStr}, }; mod error; @@ -12,53 +26,26 @@ mod sign; mod verify; pub use { - sign::{full_sign, simple_sign}, - verify::{full_verify, simple_verify}, + sign::{sign_full, sign_full_encoded, sign_simple, sign_simple_encoded}, + verify::{verify_full, verify_full_encoded, verify_simple, verify_simple_encoded}, }; -pub struct Wallet { - pub btc_address: Address, - pub descriptor: miniscript::Descriptor, - pub ordinal_address: Address, - pub private_key: PrivateKey, -} - -impl Wallet { - pub fn new(wif_private_key: &str) -> Self { - let secp = Secp256k1::new(); - let private_key = PrivateKey::from_wif(wif_private_key).unwrap(); - let public_key = private_key.public_key(&secp); - let descriptor = miniscript::Descriptor::new_tr(public_key, None).unwrap(); - - Self { - btc_address: miniscript::Descriptor::new_sh_wpkh(public_key) - .unwrap() - .address(Network::Regtest) - .unwrap(), - descriptor: descriptor.clone(), - ordinal_address: descriptor.address(Network::Regtest).unwrap(), - private_key, - } - } -} - const TAG: &str = "BIP0322-signed-message"; -type Result = std::result::Result; +type Result = std::result::Result; -// message_hash = sha256(sha256(tag) || sha256(tag) || message); see BIP340 -fn message_hash(message: &str) -> Vec { +pub(crate) fn message_hash(message: &[u8]) -> Vec { let mut tag_hash = sha256::Hash::hash(TAG.as_bytes()).to_byte_array().to_vec(); tag_hash.extend(tag_hash.clone()); - tag_hash.extend(message.as_bytes()); + tag_hash.extend(message); sha256::Hash::hash(tag_hash.as_slice()) .to_byte_array() .to_vec() } -fn create_to_spend(address: &Address, message: &str) -> Transaction { - Transaction { +pub(crate) fn create_to_spend(address: &Address, message: &[u8]) -> Result { + Ok(Transaction { version: Version(0), lock_time: LockTime::ZERO, input: vec![TxIn { @@ -79,10 +66,10 @@ fn create_to_spend(address: &Address, message: &str) -> Transaction { value: Amount::from_sat(0), script_pubkey: address.script_pubkey(), }], - } + }) } -fn create_to_sign(to_spend: &Transaction) -> Psbt { +pub(crate) fn create_to_sign(to_spend: &Transaction, witness: Option) -> Result { let inputs = vec![TxIn { previous_output: OutPoint { txid: to_spend.txid(), @@ -105,18 +92,21 @@ fn create_to_sign(to_spend: &Transaction) -> Psbt { }], }; - let mut psbt = Psbt::from_unsigned_tx(to_sign).unwrap(); // TODO + let mut psbt = Psbt::from_unsigned_tx(to_sign).context(error::PsbtExtract)?; + psbt.inputs[0].witness_utxo = Some(TxOut { value: Amount::from_sat(0), script_pubkey: to_spend.output[0].script_pubkey.clone(), }); - psbt + psbt.inputs[0].final_script_witness = witness; + + Ok(psbt) } #[cfg(test)] mod tests { - use {super::*, error::Bip322Error, pretty_assertions::assert_eq, std::str::FromStr}; + use {super::*, pretty_assertions::assert_eq}; /// 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 @@ -129,12 +119,12 @@ mod tests { #[test] fn message_hashes_are_correct() { assert_eq!( - hex::encode(message_hash("")), + hex::encode(message_hash("".as_bytes())), "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1" ); assert_eq!( - hex::encode(message_hash("Hello World")), + hex::encode(message_hash("Hello World".as_bytes())), "f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a" ); } @@ -144,8 +134,9 @@ mod tests { assert_eq!( create_to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), - "" + "".as_bytes() ) + .unwrap() .txid() .to_string(), "c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7" @@ -154,8 +145,9 @@ mod tests { assert_eq!( create_to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), - "Hello World" + "Hello World".as_bytes() ) + .unwrap() .txid() .to_string(), "b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b" @@ -166,9 +158,12 @@ mod tests { fn to_sign_txids_correct() { let to_spend = create_to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), - "", - ); - let to_sign = create_to_sign(&to_spend); + "".as_bytes(), + ) + .unwrap(); + + let to_sign = create_to_sign(&to_spend, None).unwrap(); + assert_eq!( to_sign.unsigned_tx.txid().to_string(), "1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6" @@ -176,9 +171,12 @@ mod tests { let to_spend = create_to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), - "Hello World", - ); - let to_sign = create_to_sign(&to_spend); + "Hello World".as_bytes(), + ) + .unwrap(); + + let to_sign = create_to_sign(&to_spend, None).unwrap(); + assert_eq!( to_sign.unsigned_tx.txid().to_string(), "88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf" @@ -188,99 +186,81 @@ mod tests { #[test] fn simple_verify_and_falsify_taproot() { assert!( - simple_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + verify_simple_encoded( + TAPROOT_ADDRESS, "Hello World", "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" ).is_ok() ); assert_eq!( - simple_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + verify_simple_encoded( + TAPROOT_ADDRESS, "Hello World -- This should fail", "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" - ), - Err(Bip322Error::Invalid) + ).unwrap_err().to_string(), + "Invalid signature" ); } #[test] fn simple_sign_taproot() { - let wallet = Wallet::new(WIF_PRIVATE_KEY); - - let signature = simple_sign( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), - "Hello World", - &wallet, - ); - assert_eq!( - signature, + sign_simple_encoded(TAPROOT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap(), "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" ); } #[test] fn roundtrip_taproot_simple() { - let wallet = Wallet::new(WIF_PRIVATE_KEY); - - assert!(simple_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + assert!(verify_simple_encoded( + TAPROOT_ADDRESS, "Hello World", - &simple_sign( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), - "Hello World", - &wallet - ) + &sign_simple_encoded(TAPROOT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap() ) .is_ok()); } #[test] fn roundtrip_taproot_full() { - let wallet = Wallet::new(WIF_PRIVATE_KEY); - - assert!(full_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + assert!(verify_full_encoded( + TAPROOT_ADDRESS, "Hello World", - &full_sign( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), - "Hello World", - &wallet - ) + &sign_full_encoded(TAPROOT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap() ) .is_ok()); } #[test] fn invalid_address() { - assert_eq!(simple_verify( - &Address::from_str("3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV").unwrap().assume_checked(), + assert_eq!(verify_simple_encoded( + "3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV", "", - "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI="), - Err(Bip322Error::InvalidAddress) + "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=").unwrap_err().to_string(), + "Unsuported address `3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV`, only P2TR allowed" ) } #[test] - fn malformed_signature() { + fn signature_decode_error() { assert_eq!( - simple_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + verify_simple_encoded( + TAPROOT_ADDRESS, "Hello World", "invalid signature not in base64 encoding" - ), - Err(Bip322Error::MalformedSignature) + ) + .unwrap_err() + .to_string(), + "Decode error for signature `invalid signature not in base64 encoding`" ); assert_eq!( - simple_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + verify_simple_encoded( + TAPROOT_ADDRESS, "Hello World", "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViH" - ), - Err(Bip322Error::MalformedSignature) + ).unwrap_err().to_string(), + "Decode error for signature `AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViH`" ) } } diff --git a/src/sign.rs b/src/sign.rs index 719b350..cd6129b 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -1,15 +1,83 @@ -use { - crate::{create_to_sign, create_to_spend, Wallet}, - base64::{engine::general_purpose, Engine}, - bitcoin::{ - consensus::Encodable, - key::{Keypair, TapTweak}, - psbt::Psbt, - secp256k1::{self, Secp256k1, XOnlyPublicKey}, - sighash::{self, SighashCache, TapSighashType}, - Address, Amount, PrivateKey, Transaction, TxOut, Witness, - }, -}; +use super::*; + +/// Signs the BIP-322 simple from encoded values, i.e. address encoding, message string and +/// WIF private key string. Returns a base64 encoded witness stack. +pub fn sign_simple_encoded(address: &str, message: &str, wif_private_key: &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 witness = sign_simple(&address, message.as_bytes(), private_key)?; + + let mut buffer = Vec::new(); + + witness + .consensus_encode(&mut buffer) + .context(error::WitnessEncoding)?; + + Ok(general_purpose::STANDARD.encode(buffer)) +} + +/// Signs the BIP-322 full from encoded values, i.e. address encoding, message string and +/// WIF private key string. Returns a base64 encoded transaction. +pub fn sign_full_encoded(address: &str, message: &str, wif_private_key: &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 tx = sign_full(&address, message.as_bytes(), private_key)?; + + let mut buffer = Vec::new(); + + tx.consensus_encode(&mut buffer) + .context(error::TransactionEncode)?; + + Ok(general_purpose::STANDARD.encode(buffer)) +} + +/// Signs in the BIP-322 simple format from proper Rust types and returns the witness. +pub fn sign_simple(address: &Address, message: &[u8], private_key: PrivateKey) -> Result { + Ok( + sign_full(address, message, private_key)?.input[0] + .witness + .clone(), + ) +} + +/// Signs in the BIP-322 full format from proper Rust types and returns the full transaction. +pub fn sign_full( + address: &Address, + message: &[u8], + private_key: PrivateKey, +) -> Result { + if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() { + if witness_program.version().to_num() != 1 { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + } + + if witness_program.program().len() != 32 { + return Err(Error::NotKeyPathSpend); + } + } else { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + }; + + let to_spend = create_to_spend(address, message)?; + let mut to_sign = create_to_sign(&to_spend, None)?; + + let witness = create_message_signature(&to_spend, &to_sign, private_key); + to_sign.inputs[0].final_script_witness = Some(witness); + + to_sign.extract_tx().context(error::TransactionExtract) +} fn create_message_signature( to_spend_tx: &Transaction, @@ -62,32 +130,3 @@ fn create_message_signature( witness.to_owned() } - -pub fn simple_sign(address: &Address, message: &str, wallet: &Wallet) -> String { - let to_spend = create_to_spend(address, message); - let to_sign = create_to_sign(&to_spend); - - let witness = create_message_signature(&to_spend, &to_sign, wallet.private_key); - - let mut buffer = Vec::new(); - witness.consensus_encode(&mut buffer).unwrap(); - - general_purpose::STANDARD.encode(buffer) -} - -pub fn full_sign(address: &Address, message: &str, wallet: &Wallet) -> String { - let to_spend = create_to_spend(address, message); - let mut to_sign = create_to_sign(&to_spend); - - let witness = create_message_signature(&to_spend, &to_sign, wallet.private_key); - to_sign.inputs[0].final_script_witness = Some(witness); - - let mut buffer = Vec::new(); - to_sign - .extract_tx() - .unwrap() - .consensus_encode(&mut buffer) - .unwrap(); - - general_purpose::STANDARD.encode(buffer) -} diff --git a/src/verify.rs b/src/verify.rs index d32c35d..10f4a3b 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,69 +1,132 @@ -use { - super::*, - crate::{create_to_sign, create_to_spend, error::Bip322Error}, - base64::{engine::general_purpose, Engine}, - bitcoin::{ - address::AddressType, - consensus::Decodable, - secp256k1::{schnorr::Signature, Message, Secp256k1, XOnlyPublicKey}, - sighash::{self, SighashCache, TapSighashType}, - Address, Amount, OutPoint, Transaction, TxOut, Witness, - }, - std::io::Cursor, -}; - -fn extract_pub_key(address: &Address) -> Result { +use super::*; + +/// Verifies the BIP-322 simple from encoded values, i.e. address encoding, message string and +/// signature base64 string. +pub fn verify_simple_encoded(address: &str, message: &str, signature: &str) -> Result<()> { + let address = Address::from_str(address) + .context(error::AddressParse { address })? + .assume_checked(); + + let mut cursor = Cursor::new( + general_purpose::STANDARD + .decode(signature) + .context(error::SignatureDecode { signature })?, + ); + + let witness = + Witness::consensus_decode_from_finite_reader(&mut cursor).context(error::WitnessMalformed)?; + + verify_simple(&address, message.as_bytes(), witness) +} + +/// Verifies the BIP-322 full from encoded values, i.e. address encoding, message string and +/// transaction base64 string. +pub fn verify_full_encoded(address: &str, message: &str, to_sign: &str) -> Result<()> { + let address = Address::from_str(address) + .context(error::AddressParse { address })? + .assume_checked(); + + let mut cursor = Cursor::new(general_purpose::STANDARD.decode(to_sign).context( + error::TransactionBase64Decode { + transaction: to_sign, + }, + )?); + + let to_sign = Transaction::consensus_decode_from_finite_reader(&mut cursor).context( + error::TransactionConsensusDecode { + transaction: to_sign, + }, + )?; + + verify_full(&address, message.as_bytes(), to_sign) +} + +/// Verifies the BIP-322 simple from proper Rust types. +pub fn verify_simple(address: &Address, message: &[u8], signature: Witness) -> Result<()> { + verify_full( + address, + message, + create_to_sign(&create_to_spend(address, message)?, Some(signature))? + .extract_tx() + .context(error::TransactionExtract)?, + ) +} + +/// Verifies the BIP-322 full from proper Rust types. +pub fn verify_full(address: &Address, message: &[u8], to_sign: Transaction) -> Result<()> { if address .address_type() .is_some_and(|addr| addr != AddressType::P2tr) { - return Err(Bip322Error::InvalidAddress); + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); } - if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() { - if witness_program.version().to_num() != 1 { - return Err(Bip322Error::InvalidAddress); - } + let pub_key = + if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() { + if witness_program.version().to_num() != 1 { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + } - if witness_program.program().len() != 32 { - return Err(Bip322Error::NotKeyPathSpend); - } + if witness_program.program().len() != 32 { + return Err(Error::NotKeyPathSpend); + } - Ok( XOnlyPublicKey::from_slice(witness_program.program().as_bytes()) - .expect("should extract an xonly public key"), - ) - } else { - Err(Bip322Error::InvalidAddress) + .expect("should extract an xonly public key") + } else { + return Err(Error::UnsupportedAddress { + address: address.to_string(), + }); + }; + + 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.txid(), + vout: 0, + }; + + if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output { + return Err(Error::ToSignInvalid); } -} -fn decode_and_verify( - encoded_signature: &Vec, - pub_key: &XOnlyPublicKey, - to_spend: Transaction, - to_sign: Transaction, -) -> Result<()> { + let Some(witness) = to_sign.inputs[0].final_script_witness.clone() else { + return Err(Error::WitnessEmpty); + }; + + let encoded_signature = witness.to_vec()[0].clone(); + let (signature, sighash_type) = match encoded_signature.len() { 65 => ( Signature::from_slice(&encoded_signature.as_slice()[..64]) - .map_err(|_| Bip322Error::MalformedSignature)?, + .context(error::SignatureInvalid)?, TapSighashType::from_consensus_u8(encoded_signature[64]) - .map_err(|_| Bip322Error::InvalidSigHash)?, + .context(error::SigHashTypeInvalid)?, ), 64 => ( - Signature::from_slice(encoded_signature.as_slice()) - .map_err(|_| Bip322Error::MalformedSignature)?, + Signature::from_slice(encoded_signature.as_slice()).context(error::SignatureInvalid)?, TapSighashType::Default, ), - _ => return Err(Bip322Error::MalformedSignature), + _ => { + return Err(Error::SignatureLength { + length: encoded_signature.len(), + encoded_signature, + }) + } }; if !(sighash_type == TapSighashType::All || sighash_type == TapSighashType::Default) { - return Err(Bip322Error::InvalidSigHash); + return Err(Error::SigHashTypeUnsupported { + sighash_type: sighash_type.to_string(), + }); } - let mut sighash_cache = SighashCache::new(to_sign); + let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); let sighash = sighash_cache .taproot_key_spend_signature_hash( @@ -80,54 +143,6 @@ fn decode_and_verify( Message::from_digest_slice(sighash.as_ref()).expect("should be cryptographically secure hash"); Secp256k1::verification_only() - .verify_schnorr(&signature, &message, pub_key) - .map_err(|_| Bip322Error::Invalid) -} - -pub fn simple_verify(address: &Address, message: &str, signature: &str) -> Result<()> { - let pub_key = extract_pub_key(address)?; - let to_spend = create_to_spend(address, message); - let to_sign = create_to_sign(&to_spend); - - let mut cursor = Cursor::new( - general_purpose::STANDARD - .decode(signature) - .map_err(|_| Bip322Error::MalformedSignature)?, - ); - - let witness = match Witness::consensus_decode_from_finite_reader(&mut cursor) { - Ok(witness) => witness, - Err(_) => return Err(Bip322Error::MalformedSignature), - }; - - let encoded_signature = &witness.to_vec()[0]; - - decode_and_verify(encoded_signature, &pub_key, to_spend, to_sign.unsigned_tx) -} - -pub fn full_verify(address: &Address, message: &str, to_sign_base64: &str) -> Result<()> { - let pub_key = extract_pub_key(address)?; - let to_spend = create_to_spend(address, message); - - let mut cursor = Cursor::new( - general_purpose::STANDARD - .decode(to_sign_base64) - .map_err(|_| Bip322Error::MalformedSignature)?, - ); - - let to_sign = Transaction::consensus_decode_from_finite_reader(&mut cursor) - .map_err(|_| Bip322Error::MalformedSignature)?; - - let to_spend_out_point = OutPoint { - txid: to_spend.txid(), - vout: 0, - }; - - if to_spend_out_point != to_sign.input[0].previous_output { - return Err(Bip322Error::Invalid); - } - - let encoded_signature = &to_sign.input[0].witness.to_vec()[0]; - - decode_and_verify(encoded_signature, &pub_key, to_spend, to_sign) + .verify_schnorr(&signature, &message, &pub_key) + .context(error::SignatureInvalid) } diff --git a/www/Cargo.lock b/www/Cargo.lock index a480482..deaf5e2 100644 --- a/www/Cargo.lock +++ b/www/Cargo.lock @@ -22,15 +22,14 @@ checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" [[package]] name = "bip322" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048ae0b251cd57e77fc4ec31b0fe916b5fbdb6d0b9d20c8fd1ccb6fcc7e15f7" +version = "0.0.4" dependencies = [ "base64", "bitcoin", "bitcoin_hashes 0.14.0", "hex", "miniscript", + "snafu", ] [[package]] @@ -97,6 +96,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -184,6 +189,27 @@ dependencies = [ "cc", ] +[[package]] +name = "snafu" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b835cb902660db3415a672d862905e791e54d306c6e8189168c7f3d9ae1c79d" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.72" diff --git a/www/Cargo.toml b/www/Cargo.toml index 58d906f..ea57c2b 100644 --- a/www/Cargo.toml +++ b/www/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -bip322 = "0.0.3" +bip322 = "0.0.4" bitcoin = "0.31.2" wasm-bindgen = "0.2.92" + +[patch.crates-io] +bip322 = { path = '../' } diff --git a/www/index.js b/www/index.js index e4048bf..2706698 100644 --- a/www/index.js +++ b/www/index.js @@ -1,18 +1,7 @@ import init, { verify } from './bip322.js'; -let wasmInitialized = false; - -async function initializeWasm() { - if (!wasmInitialized) { - await init(); - console.log('WASM module loaded'); - wasmInitialized = true; - } -} - async function runVerification(event) { event.preventDefault(); - await initializeWasm(); const address = document.getElementById('address').value; const message = document.getElementById('message').value; @@ -52,6 +41,8 @@ function handleKeyPress(event) { } } +await init(); + document.getElementById('bip').addEventListener('click', showForm); document.getElementById('verify-form').addEventListener('submit', runVerification); diff --git a/www/src/lib.rs b/www/src/lib.rs index 53f08d1..3f5fabc 100644 --- a/www/src/lib.rs +++ b/www/src/lib.rs @@ -1,16 +1,6 @@ -use { - bitcoin::{address::NetworkUnchecked, Address}, - wasm_bindgen::prelude::*, -}; +use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn verify(address: &str, message: &str, signature: &str) -> bool { - bip322::simple_verify( - &address - .parse::>() - .unwrap() - .assume_checked(), - message, - signature, - ) + bip322::verify_simple_encoded(&address, message, signature).is_ok() }