diff --git a/CHANGELOG.md b/CHANGELOG.md index a720dfb59..c5d8344b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - New MSRV set to `1.56.1` - Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced) +- Add `MessageSigner` and `MessageSignatureVerifier` with ECDSA implementations ## [v0.19.0] - [v0.18.0] diff --git a/src/keys/mod.rs b/src/keys/mod.rs index 20ff58184..e94c32f6f 100644 --- a/src/keys/mod.rs +++ b/src/keys/mod.rs @@ -36,6 +36,7 @@ use crate::wallet::utils::SecpCtx; #[cfg(feature = "keys-bip39")] #[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] pub mod bip39; +pub mod msgsig; /// Set of valid networks for a key pub type ValidNetworks = HashSet; diff --git a/src/keys/msgsig.rs b/src/keys/msgsig.rs new file mode 100644 index 000000000..7872ca4e4 --- /dev/null +++ b/src/keys/msgsig.rs @@ -0,0 +1,102 @@ +/// Signing arbitrary messages and verify message signatures +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::ecdsa::RecoverableSignature; +use bitcoin::secp256k1::{All, Message, Secp256k1, SecretKey}; +use bitcoin::util::misc::{signed_msg_hash, MessageSignature}; +use bitcoin::{Address, Network, PrivateKey, PublicKey}; + +/// A message signer using ECDSA +pub struct EcdsaMessageSigner<'a> { + secp: &'a Secp256k1, + secret_key: SecretKey, +} + +impl<'a> EcdsaMessageSigner<'a> { + /// Creates message signer from a bitcoin ECDSA private key + pub fn from_prv(prv: PrivateKey, secp: &'a Secp256k1) -> Self { + Self::from_secret_key(prv.inner, secp) + } + + /// Creates message signer from an ECDSA secret key + pub fn from_secret_key(secret_key: SecretKey, secp: &'a Secp256k1) -> Self { + EcdsaMessageSigner { secret_key, secp } + } + + fn sign(&self, message: &str) -> RecoverableSignature { + let msg_hash = signed_msg_hash(message); + self.secp.sign_ecdsa_recoverable( + &Message::from_slice(&msg_hash.into_inner()[..]) + .expect("Message to be signed is not a valid Hash"), + &self.secret_key, + ) + } +} + +/// A message signature verifier using ECDSA +pub struct EcdsaMessageSignatureVerifier<'a> { + secp: &'a Secp256k1, + address: Address, +} + +impl<'a> EcdsaMessageSignatureVerifier<'a> { + /// Creates a message signature verifier from a public key + pub fn from_pub(public_key: PublicKey, secp: &'a Secp256k1) -> Self { + let address = Address::p2pkh(&public_key, Network::Bitcoin); + Self::from_address(address, secp) + } + + /// Creates a message signature verifier from an address + pub fn from_address(address: Address, secp: &'a Secp256k1) -> Self { + EcdsaMessageSignatureVerifier { address, secp } + } + + fn verify(&self, sig: RecoverableSignature, msg: &str) -> bool { + let message_sig = MessageSignature::new(sig, false); + let msg_hash = signed_msg_hash(msg); + message_sig + .is_signed_by_address(self.secp, &self.address, msg_hash) + .unwrap_or(false) + } +} + +#[cfg(test)] +mod ecdsa_msg_sign { + use bitcoin::secp256k1::Secp256k1; + use bitcoin::util::misc::MessageSignature; + use bitcoin::{Address, Network}; + + use super::*; + + pub const PRV_WIF: &str = "5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss"; + + const MSG: &str = "fix the money"; + + #[test] + fn test_sign_message() { + let secp = Secp256k1::new(); + let prv = PrivateKey::from_wif(PRV_WIF).unwrap(); + + let signer = EcdsaMessageSigner::from_prv(prv, &secp); + let sig = signer.sign(MSG); + let pub_key = prv.public_key(&secp); + let address = Address::p2pkh(&pub_key, Network::Bitcoin); + let message_sig = MessageSignature::new(sig, false); + + assert_eq!( + message_sig + .is_signed_by_address(&secp, &address, signed_msg_hash(MSG)) + .unwrap(), + true + ); + } + + #[test] + fn test_verify_signed_message() { + let secp = Secp256k1::new(); + let prv = PrivateKey::from_wif(PRV_WIF).unwrap(); + let sig = EcdsaMessageSigner::from_prv(prv, &secp).sign(MSG); + let verifier = EcdsaMessageSignatureVerifier::from_pub(prv.public_key(&secp), &secp); + + assert_eq!(verifier.verify(sig, MSG), true); + } +}