use crate::error::{Error, Result};
use crate::keys::{PrivateKey, PublicKey};
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
use secp256k1::Message;
use sha2::{Digest, Sha256};
use std::fmt;
pub const SIGNATURE_LEN: usize = 65;
const HEADER_OFFSET: u8 = 4 + 27;
const MAX_CANONICAL_ATTEMPTS: u32 = 1_000;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Signature([u8; SIGNATURE_LEN]);
impl Signature {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() != SIGNATURE_LEN {
return Err(Error::sig(format!(
"signature must be {SIGNATURE_LEN} bytes, got {}",
bytes.len()
)));
}
let mut buf = [0u8; SIGNATURE_LEN];
buf.copy_from_slice(bytes);
let sig = Signature(buf);
sig.recovery_id()?;
if !is_canonical(sig.rs()) {
return Err(Error::sig("signature is not canonical"));
}
Ok(sig)
}
pub fn from_hex(s: &str) -> Result<Self> {
let s = s.trim();
if s.len() != SIGNATURE_LEN * 2 {
return Err(Error::sig(format!(
"signature hex must be {} characters, got {}",
SIGNATURE_LEN * 2,
s.len()
)));
}
let mut buf = [0u8; SIGNATURE_LEN];
crate::hex::decode_exact(s, &mut buf)
.map_err(|_| Error::sig("signature is not valid hex"))?;
Self::from_bytes(&buf)
}
pub fn as_bytes(&self) -> &[u8; SIGNATURE_LEN] {
&self.0
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn rs(&self) -> &[u8] {
&self.0[1..]
}
pub fn recovery_id(&self) -> Result<i32> {
let header = self.0[0];
if !(HEADER_OFFSET..HEADER_OFFSET + 4).contains(&header) {
return Err(Error::sig(format!(
"signature header byte {header} is outside the valid range {}..={}",
HEADER_OFFSET,
HEADER_OFFSET + 3
)));
}
Ok(i32::from(header - HEADER_OFFSET))
}
fn to_recoverable(self) -> Result<RecoverableSignature> {
let rec_id = RecoveryId::from_i32(self.recovery_id()?)
.map_err(|e| Error::sig(format!("bad recovery id: {e}")))?;
RecoverableSignature::from_compact(self.rs(), rec_id)
.map_err(|e| Error::sig(format!("malformed compact signature: {e}")))
}
}
impl fmt::Debug for Signature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Signature({})", self.to_hex())
}
}
impl fmt::Display for Signature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
pub fn is_canonical(rs: &[u8]) -> bool {
if rs.len() != 64 {
return false;
}
(rs[0] & 0x80) == 0
&& !(rs[0] == 0 && (rs[1] & 0x80) == 0)
&& (rs[32] & 0x80) == 0
&& !(rs[32] == 0 && (rs[33] & 0x80) == 0)
}
pub fn sign_digest(digest: &[u8; 32], key: &PrivateKey) -> Result<Signature> {
let secp = secp256k1::SECP256K1;
let msg =
Message::from_digest_slice(digest).map_err(|e| Error::sig(format!("bad digest: {e}")))?;
for counter in 1..=MAX_CANONICAL_ATTEMPTS {
let mut nonce_data = [0u8; 32];
nonce_data[28..].copy_from_slice(&counter.to_be_bytes());
let rec_sig = secp.sign_ecdsa_recoverable_with_noncedata(&msg, key.inner(), &nonce_data);
let (rec_id, compact) = rec_sig.serialize_compact();
if !is_canonical(&compact) {
continue;
}
let mut out = [0u8; SIGNATURE_LEN];
out[0] = u8::try_from(rec_id.to_i32())
.map_err(|_| Error::sig("recovery id out of range"))?
+ HEADER_OFFSET;
out[1..].copy_from_slice(&compact);
return Ok(Signature(out));
}
Err(Error::sig(format!(
"no canonical signature found in {MAX_CANONICAL_ATTEMPTS} attempts"
)))
}
pub fn sign_message(message: &[u8], key: &PrivateKey) -> Result<Signature> {
let digest: [u8; 32] = Sha256::digest(message).into();
sign_digest(&digest, key)
}
pub fn recover(digest: &[u8; 32], signature: &Signature) -> Result<PublicKey> {
let secp = secp256k1::SECP256K1;
let msg =
Message::from_digest_slice(digest).map_err(|e| Error::sig(format!("bad digest: {e}")))?;
let rec_sig = signature.to_recoverable()?;
let recovered = secp
.recover_ecdsa(&msg, &rec_sig)
.map_err(|e| Error::sig(format!("could not recover a public key: {e}")))?;
secp.verify_ecdsa(&msg, &rec_sig.to_standard(), &recovered)
.map_err(|e| Error::sig(format!("signature does not verify: {e}")))?;
Ok(PublicKey::from_inner(recovered))
}
pub fn verify(digest: &[u8; 32], signature: &Signature, expected: &PublicKey) -> Result<()> {
let recovered = recover(digest, signature)?;
if &recovered != expected {
return Err(Error::sig(
"signature is valid but was made by a different key",
));
}
Ok(())
}
pub fn verify_message(message: &[u8], signature: &Signature, expected: &PublicKey) -> Result<()> {
let digest: [u8; 32] = Sha256::digest(message).into();
verify(&digest, signature, expected)
}
pub fn recover_message(message: &[u8], signature: &Signature) -> Result<PublicKey> {
let digest: [u8; 32] = Sha256::digest(message).into();
recover(&digest, signature)
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_WIF: &str = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3";
fn key() -> PrivateKey {
PrivateKey::from_wif(TEST_WIF).unwrap()
}
#[test]
fn signs_and_recovers() {
let k = key();
let sig = sign_message(b"hello hive", &k).unwrap();
assert_eq!(sig.as_bytes().len(), 65);
assert_eq!(
recover_message(b"hello hive", &sig).unwrap(),
k.public_key()
);
verify_message(b"hello hive", &sig, &k.public_key()).unwrap();
}
#[test]
fn signatures_are_canonical() {
let k = key();
for i in 0..256u32 {
let sig = sign_message(format!("message {i}").as_bytes(), &k).unwrap();
assert!(
is_canonical(sig.rs()),
"attempt {i} produced a non-canonical signature"
);
let header = sig.as_bytes()[0];
assert!((31..=34).contains(&header), "bad header byte {header}");
}
}
#[test]
fn signing_is_deterministic() {
let k = key();
let a = sign_message(b"repeatable", &k).unwrap();
let b = sign_message(b"repeatable", &k).unwrap();
assert_eq!(a, b, "RFC 6979 with a counter must be reproducible");
}
#[test]
fn a_wrong_message_does_not_verify() {
let k = key();
let sig = sign_message(b"the real message", &k).unwrap();
assert!(verify_message(b"a different message", &sig, &k.public_key()).is_err());
}
#[test]
fn a_wrong_key_does_not_verify() {
let k = key();
let other = PrivateKey::generate();
let sig = sign_message(b"msg", &k).unwrap();
assert!(verify_message(b"msg", &sig, &other.public_key()).is_err());
}
#[test]
fn tampered_signatures_are_rejected() {
let k = key();
let sig = sign_message(b"msg", &k).unwrap();
let mut raw = *sig.as_bytes();
raw[40] ^= 0xff;
match Signature::from_bytes(&raw) {
Err(_) => {}
Ok(tampered) => {
assert!(
verify_message(b"msg", &tampered, &k.public_key()).is_err(),
"a tampered signature must not verify"
);
}
}
}
#[test]
fn header_byte_is_range_checked() {
let k = key();
let sig = sign_message(b"msg", &k).unwrap();
for bad_header in [0u8, 27, 30, 35, 200, 255] {
let mut raw = *sig.as_bytes();
raw[0] = bad_header;
assert!(
Signature::from_bytes(&raw).is_err(),
"header {bad_header} should be rejected"
);
}
}
#[test]
fn canonicality_predicate_matches_graphene() {
let mut rs = [0x01u8; 64];
assert!(is_canonical(&rs));
rs[0] = 0x80; assert!(!is_canonical(&rs));
rs = [0x01u8; 64];
rs[32] = 0x80; assert!(!is_canonical(&rs));
rs = [0x01u8; 64];
rs[0] = 0x00;
rs[1] = 0x01; assert!(!is_canonical(&rs));
rs = [0x01u8; 64];
rs[32] = 0x00;
rs[33] = 0x01; assert!(!is_canonical(&rs));
assert!(!is_canonical(&[0x01u8; 63]));
}
#[test]
fn hex_roundtrip() {
let sig = sign_message(b"msg", &key()).unwrap();
assert_eq!(Signature::from_hex(&sig.to_hex()).unwrap(), sig);
assert!(Signature::from_hex("deadbeef").is_err());
}
}