From f7422fb8d12714884533981eeb0b127240c9230c Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 7 Oct 2020 17:46:48 +0200 Subject: [PATCH] Add MessageSignature type for dealing with signed messages --- Cargo.toml | 9 +- contrib/test.sh | 4 +- src/lib.rs | 1 + src/util/misc.rs | 210 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 219 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 50540eb2..19b95490 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,18 +12,21 @@ readme = "README.md" [features] +default = [ "secp-recovery" ] +base64 = [ "base64-compat" ] fuzztarget = ["secp256k1/fuzztarget", "bitcoin_hashes/fuzztarget"] unstable = [] rand = ["secp256k1/rand-std"] use-serde = ["serde", "bitcoin_hashes/serde", "secp256k1/serde"] -secp-recovery = ["secp256k1/recovery"] secp-endomorphism = ["secp256k1/endomorphism"] secp-lowmemory = ["secp256k1/lowmemory"] +secp-recovery = ["secp256k1/recovery"] [dependencies] +base64-compat = { version = "1.0.0", optional = true } bech32 = "0.7.2" bitcoin_hashes = "0.9.0" -secp256k1 = "0.19.0" +secp256k1 = { version = "0.19.0", features = [ "recovery" ] } bitcoinconsensus = { version = "0.19.0-1", optional = true } serde = { version = "1", optional = true } @@ -32,6 +35,6 @@ serde = { version = "1", optional = true } serde_derive = "<1.0.99" serde_json = "<1.0.45" serde_test = "1" -secp256k1 = { version = "0.19.0", features = ["rand-std"] } +secp256k1 = { version = "0.19.0", features = [ "recovery", "rand-std" ] } # We need to pin ryu (transitive dep from serde_json) to stay compatible with Rust 1.22.0 ryu = "<1.0.5" diff --git a/contrib/test.sh b/contrib/test.sh index c29e0259..ce440f6b 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -1,6 +1,6 @@ #!/bin/sh -ex -FEATURES="bitcoinconsensus use-serde rand" +FEATURES="base64 bitcoinconsensus use-serde rand" if [ "$DO_COV" = true ] then @@ -15,6 +15,8 @@ then fi # Test without any features first +cargo test --verbose --no-default-features +# Then test with the default features cargo test --verbose # Test each feature diff --git a/src/lib.rs b/src/lib.rs index 6e62249c..8215418c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ #[macro_use] pub extern crate bitcoin_hashes as hashes; pub extern crate secp256k1; pub extern crate bech32; +#[cfg(feature = "base64")] pub extern crate base64; #[cfg(feature="bitcoinconsensus")] extern crate bitcoinconsensus; #[cfg(feature = "serde")] extern crate serde; diff --git a/src/util/misc.rs b/src/util/misc.rs index 98e0f709..37ea2f73 100644 --- a/src/util/misc.rs +++ b/src/util/misc.rs @@ -17,11 +17,188 @@ //! Various utility functions use hashes::{sha256d, Hash, HashEngine}; + use blockdata::opcodes; use consensus::{encode, Encodable}; +#[cfg(feature = "secp-recovery")] +pub use self::message_signing::{MessageSignature, MessageSignatureError}; + static MSG_SIGN_PREFIX: &[u8] = b"\x18Bitcoin Signed Message:\n"; +#[cfg(feature = "secp-recovery")] +mod message_signing { + use std::{error, fmt}; + + use hashes::sha256d; + use secp256k1; + use secp256k1::recovery::{RecoveryId, RecoverableSignature}; + + use util::key::PublicKey; + use util::address::{Address, AddressType}; + + /// An error used for dealing with Bitcoin Signed Messages. + #[derive(Debug, PartialEq, Eq)] + pub enum MessageSignatureError { + /// Signature is expected to be 65 bytes. + InvalidLength, + /// The signature is invalidly constructed. + InvalidEncoding(secp256k1::Error), + /// Invalid base64 encoding. + InvalidBase64, + } + + impl fmt::Display for MessageSignatureError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + MessageSignatureError::InvalidLength => write!(f, "length not 65 bytes"), + MessageSignatureError::InvalidEncoding(ref e) => write!(f, "invalid encoding: {}", e), + MessageSignatureError::InvalidBase64 => write!(f, "invalid base64"), + } + } + } + + impl error::Error for MessageSignatureError { + fn cause(&self) -> Option<&dyn error::Error> { + match *self { + MessageSignatureError::InvalidEncoding(ref e) => Some(e), + _ => None, + } + } + } + + #[doc(hidden)] + impl From for MessageSignatureError { + fn from(e: secp256k1::Error) -> MessageSignatureError { + MessageSignatureError::InvalidEncoding(e) + } + } + + /// A signature on a Bitcoin Signed Message. + /// + /// In order to use the `to_base64` and `from_base64` methods, as well as the + /// `fmt::Display` and `str::FromStr` implementations, the `base64` feature + /// must be enabled. + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + pub struct MessageSignature { + /// The inner recoverable signature. + pub signature: RecoverableSignature, + /// Whether or not this signature was created with a compressed key. + pub compressed: bool, + } + + impl MessageSignature { + /// Create a new [MessageSignature]. + pub fn new(signature: RecoverableSignature, compressed: bool) -> MessageSignature { + MessageSignature { + signature: signature, + compressed: compressed, + } + } + + /// Serialize to bytes. + pub fn serialize(&self) -> [u8; 65] { + let (recid, raw) = self.signature.serialize_compact(); + let mut serialized = [0u8; 65]; + serialized[0] = 27; + serialized[0] += recid.to_i32() as u8; + if self.compressed { + serialized[0] += 4; + } + serialized[1..].copy_from_slice(&raw[..]); + serialized + } + + /// Create from a byte slice. + pub fn from_slice(bytes: &[u8]) -> Result { + if bytes.len() != 65 { + return Err(MessageSignatureError::InvalidLength); + } + // We just check this here so we can safely subtract further. + if bytes[0] < 27 { + return Err(MessageSignatureError::InvalidEncoding(secp256k1::Error::InvalidRecoveryId)); + }; + let recid = RecoveryId::from_i32(((bytes[0] - 27) & 0x03) as i32)?; + Ok(MessageSignature { + signature: RecoverableSignature::from_compact(&bytes[1..], recid)?, + compressed: ((bytes[0] - 27) & 0x04) != 0, + }) + } + + /// Attempt to recover a public key from the signature and the signed message. + /// + /// To get the message hash from a message, use [signed_msg_hash]. + pub fn recover_pubkey( + &self, + secp_ctx: &secp256k1::Secp256k1, + msg_hash: sha256d::Hash + ) -> Result { + let msg = secp256k1::Message::from_slice(&msg_hash[..])?; + let pubkey = secp_ctx.recover(&msg, &self.signature)?; + Ok(PublicKey { + key: pubkey, + compressed: self.compressed, + }) + } + + /// Verify that the signature signs the message and was signed by the given address. + /// + /// To get the message hash from a message, use [signed_msg_hash]. + pub fn is_signed_by_address( + &self, + secp_ctx: &secp256k1::Secp256k1, + address: &Address, + msg_hash: sha256d::Hash + ) -> Result { + let pubkey = self.recover_pubkey(&secp_ctx, msg_hash)?; + Ok(match address.address_type() { + Some(AddressType::P2pkh) => { + *address == Address::p2pkh(&pubkey, address.network) + } + Some(AddressType::P2sh) => false, + Some(AddressType::P2wpkh) => { + // Only compressed pubkeys are allowed in p2wpkh. + pubkey.compressed && + *address == Address::p2wpkh(&pubkey, address.network).expect("compressed pk") + } + Some(AddressType::P2wsh) => false, + None => false, + }) + } + + #[cfg(feature = "base64")] + /// Convert a signature from base64 encoding. + pub fn from_base64(s: &str) -> Result { + let bytes = ::base64::decode(s).map_err(|_| MessageSignatureError::InvalidBase64)?; + MessageSignature::from_slice(&bytes) + } + + #[cfg(feature = "base64")] + /// Convert to base64 encoding. + pub fn to_base64(&self) -> String { + ::base64::encode(&self.serialize()[..]) + } + } + + #[cfg(feature = "base64")] + impl fmt::Display for MessageSignature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let bytes = self.serialize(); + // This avoids the allocation of a String. + write!(f, "{}", ::base64::display::Base64Display::with_config( + &bytes[..], ::base64::STANDARD)) + } + } + + #[cfg(feature = "base64")] + impl ::std::str::FromStr for MessageSignature { + type Err = MessageSignatureError; + fn from_str(s: &str) -> Result { + MessageSignature::from_base64(s) + } + } +} + /// Search for `needle` in the vector `haystack` and remove every /// instance of it, returning the number of instances removed. /// Loops through the vector opcode by opcode, skipping pushed data. @@ -59,7 +236,6 @@ pub fn script_find_and_remove(haystack: &mut Vec, needle: &[u8]) -> usize { /// Hash message for signature using Bitcoin's message signing format pub fn signed_msg_hash(msg: &str) -> sha256d::Hash { - let mut engine = sha256d::Hash::engine(); engine.input(MSG_SIGN_PREFIX); let msg_len = encode::VarInt(msg.len() as u64); @@ -119,5 +295,37 @@ mod tests { let hash = signed_msg_hash("test"); assert_eq!(hash.to_hex(), "a6f87fe6d58a032c320ff8d1541656f0282c2c7bfcc69d61af4c8e8ed528e49c"); } + + #[test] + #[cfg(all(feature = "secp-recovery", feature = "base64"))] + fn test_message_signature() { + use std::str::FromStr; + use secp256k1; + + let secp = secp256k1::Secp256k1::new(); + let message = "rust-bitcoin MessageSignature test"; + let msg_hash = super::signed_msg_hash(&message); + let msg = secp256k1::Message::from_slice(&msg_hash).unwrap(); + + let privkey = secp256k1::SecretKey::new(&mut secp256k1::rand::thread_rng()); + let secp_sig = secp.sign_recoverable(&msg, &privkey); + let signature = super::MessageSignature { + signature: secp_sig, + compressed: true, + }; + + assert_eq!(signature.to_base64(), signature.to_string()); + let signature2 = super::MessageSignature::from_str(&signature.to_string()).unwrap(); + let pubkey = signature2.recover_pubkey(&secp, msg_hash).unwrap(); + assert_eq!(pubkey.compressed, true); + assert_eq!(pubkey.key, secp256k1::PublicKey::from_secret_key(&secp, &privkey)); + + let p2pkh = ::Address::p2pkh(&pubkey, ::Network::Bitcoin); + assert_eq!(signature2.is_signed_by_address(&secp, &p2pkh, msg_hash), Ok(true)); + let p2wpkh = ::Address::p2wpkh(&pubkey, ::Network::Bitcoin).unwrap(); + assert_eq!(signature2.is_signed_by_address(&secp, &p2wpkh, msg_hash), Ok(true)); + let p2shwpkh = ::Address::p2shwpkh(&pubkey, ::Network::Bitcoin).unwrap(); + assert_eq!(signature2.is_signed_by_address(&secp, &p2shwpkh, msg_hash), Ok(false)); + } }