Merge pull request #413 from stevenroose/message-signature
Add MessageSignature type for dealing with signed messages
This commit is contained in:
commit
e7980ac8a5
|
@ -12,18 +12,21 @@ readme = "README.md"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = [ "secp-recovery" ]
|
||||||
|
base64 = [ "base64-compat" ]
|
||||||
fuzztarget = ["secp256k1/fuzztarget", "bitcoin_hashes/fuzztarget"]
|
fuzztarget = ["secp256k1/fuzztarget", "bitcoin_hashes/fuzztarget"]
|
||||||
unstable = []
|
unstable = []
|
||||||
rand = ["secp256k1/rand-std"]
|
rand = ["secp256k1/rand-std"]
|
||||||
use-serde = ["serde", "bitcoin_hashes/serde", "secp256k1/serde"]
|
use-serde = ["serde", "bitcoin_hashes/serde", "secp256k1/serde"]
|
||||||
secp-recovery = ["secp256k1/recovery"]
|
|
||||||
secp-endomorphism = ["secp256k1/endomorphism"]
|
secp-endomorphism = ["secp256k1/endomorphism"]
|
||||||
secp-lowmemory = ["secp256k1/lowmemory"]
|
secp-lowmemory = ["secp256k1/lowmemory"]
|
||||||
|
secp-recovery = ["secp256k1/recovery"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64-compat = { version = "1.0.0", optional = true }
|
||||||
bech32 = "0.7.2"
|
bech32 = "0.7.2"
|
||||||
bitcoin_hashes = "0.9.0"
|
bitcoin_hashes = "0.9.0"
|
||||||
secp256k1 = "0.19.0"
|
secp256k1 = { version = "0.19.0", features = [ "recovery" ] }
|
||||||
|
|
||||||
bitcoinconsensus = { version = "0.19.0-1", optional = true }
|
bitcoinconsensus = { version = "0.19.0-1", optional = true }
|
||||||
serde = { version = "1", optional = true }
|
serde = { version = "1", optional = true }
|
||||||
|
@ -32,6 +35,6 @@ serde = { version = "1", optional = true }
|
||||||
serde_derive = "<1.0.99"
|
serde_derive = "<1.0.99"
|
||||||
serde_json = "<1.0.45"
|
serde_json = "<1.0.45"
|
||||||
serde_test = "1"
|
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
|
# We need to pin ryu (transitive dep from serde_json) to stay compatible with Rust 1.22.0
|
||||||
ryu = "<1.0.5"
|
ryu = "<1.0.5"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/sh -ex
|
#!/bin/sh -ex
|
||||||
|
|
||||||
FEATURES="bitcoinconsensus use-serde rand"
|
FEATURES="base64 bitcoinconsensus use-serde rand"
|
||||||
|
|
||||||
if [ "$DO_COV" = true ]
|
if [ "$DO_COV" = true ]
|
||||||
then
|
then
|
||||||
|
@ -15,6 +15,8 @@ then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test without any features first
|
# Test without any features first
|
||||||
|
cargo test --verbose --no-default-features
|
||||||
|
# Then test with the default features
|
||||||
cargo test --verbose
|
cargo test --verbose
|
||||||
|
|
||||||
# Test each feature
|
# Test each feature
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
#[macro_use] pub extern crate bitcoin_hashes as hashes;
|
#[macro_use] pub extern crate bitcoin_hashes as hashes;
|
||||||
pub extern crate secp256k1;
|
pub extern crate secp256k1;
|
||||||
pub extern crate bech32;
|
pub extern crate bech32;
|
||||||
|
#[cfg(feature = "base64")] pub extern crate base64;
|
||||||
|
|
||||||
#[cfg(feature="bitcoinconsensus")] extern crate bitcoinconsensus;
|
#[cfg(feature="bitcoinconsensus")] extern crate bitcoinconsensus;
|
||||||
#[cfg(feature = "serde")] extern crate serde;
|
#[cfg(feature = "serde")] extern crate serde;
|
||||||
|
|
206
src/util/misc.rs
206
src/util/misc.rs
|
@ -17,11 +17,184 @@
|
||||||
//! Various utility functions
|
//! Various utility functions
|
||||||
|
|
||||||
use hashes::{sha256d, Hash, HashEngine};
|
use hashes::{sha256d, Hash, HashEngine};
|
||||||
|
|
||||||
use blockdata::opcodes;
|
use blockdata::opcodes;
|
||||||
use consensus::{encode, Encodable};
|
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";
|
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<secp256k1::Error> 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<MessageSignature, MessageSignatureError> {
|
||||||
|
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<C: secp256k1::Verification>(
|
||||||
|
&self,
|
||||||
|
secp_ctx: &secp256k1::Secp256k1<C>,
|
||||||
|
msg_hash: sha256d::Hash
|
||||||
|
) -> Result<PublicKey, secp256k1::Error> {
|
||||||
|
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<C: secp256k1::Verification>(
|
||||||
|
&self,
|
||||||
|
secp_ctx: &secp256k1::Secp256k1<C>,
|
||||||
|
address: &Address,
|
||||||
|
msg_hash: sha256d::Hash
|
||||||
|
) -> Result<bool, secp256k1::Error> {
|
||||||
|
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) => false,
|
||||||
|
Some(AddressType::P2wsh) => false,
|
||||||
|
None => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "base64")]
|
||||||
|
/// Convert a signature from base64 encoding.
|
||||||
|
pub fn from_base64(s: &str) -> Result<MessageSignature, MessageSignatureError> {
|
||||||
|
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, MessageSignatureError> {
|
||||||
|
MessageSignature::from_base64(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Search for `needle` in the vector `haystack` and remove every
|
/// Search for `needle` in the vector `haystack` and remove every
|
||||||
/// instance of it, returning the number of instances removed.
|
/// instance of it, returning the number of instances removed.
|
||||||
/// Loops through the vector opcode by opcode, skipping pushed data.
|
/// Loops through the vector opcode by opcode, skipping pushed data.
|
||||||
|
@ -59,7 +232,6 @@ pub fn script_find_and_remove(haystack: &mut Vec<u8>, needle: &[u8]) -> usize {
|
||||||
|
|
||||||
/// Hash message for signature using Bitcoin's message signing format
|
/// Hash message for signature using Bitcoin's message signing format
|
||||||
pub fn signed_msg_hash(msg: &str) -> sha256d::Hash {
|
pub fn signed_msg_hash(msg: &str) -> sha256d::Hash {
|
||||||
|
|
||||||
let mut engine = sha256d::Hash::engine();
|
let mut engine = sha256d::Hash::engine();
|
||||||
engine.input(MSG_SIGN_PREFIX);
|
engine.input(MSG_SIGN_PREFIX);
|
||||||
let msg_len = encode::VarInt(msg.len() as u64);
|
let msg_len = encode::VarInt(msg.len() as u64);
|
||||||
|
@ -119,5 +291,37 @@ mod tests {
|
||||||
let hash = signed_msg_hash("test");
|
let hash = signed_msg_hash("test");
|
||||||
assert_eq!(hash.to_hex(), "a6f87fe6d58a032c320ff8d1541656f0282c2c7bfcc69d61af4c8e8ed528e49c");
|
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(false));
|
||||||
|
let p2shwpkh = ::Address::p2shwpkh(&pubkey, ::Network::Bitcoin).unwrap();
|
||||||
|
assert_eq!(signature2.is_signed_by_address(&secp, &p2shwpkh, msg_hash), Ok(false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue