use crate::tn_signature::{self, SignatureDomain};
use ed25519_dalek::{Signer as _, SigningKey};
use std::fmt;
#[derive(Debug)]
pub enum SignError {
KeyMismatch { expected: [u8; 32], got: [u8; 32] },
InvalidSignature,
Rejected,
Backend(String),
}
impl fmt::Display for SignError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SignError::KeyMismatch { .. } => write!(f, "signer public key does not match expected key"),
SignError::InvalidSignature => write!(f, "invalid signature"),
SignError::Rejected => write!(f, "signing rejected"),
SignError::Backend(s) => write!(f, "signer backend error: {s}"),
}
}
}
impl std::error::Error for SignError {}
const TXN_FEE_PAYER_OFFSET: usize = 48;
const TXN_HEADER_SZ: usize = 112;
#[derive(Clone)]
pub struct SigningMessage {
m: Vec<u8>,
expected_pubkey: [u8; 32],
body: Vec<u8>,
}
impl SigningMessage {
pub fn as_bytes(&self) -> &[u8] {
&self.m
}
pub fn expected_pubkey(&self) -> &[u8; 32] {
&self.expected_pubkey
}
pub fn body(&self) -> &[u8] {
&self.body
}
}
pub fn build_transaction_signing_message(body: &[u8]) -> Result<SigningMessage, SignError> {
if body.len() < TXN_HEADER_SZ {
return Err(SignError::Backend(format!(
"transaction body {} bytes is shorter than the {}-byte header",
body.len(),
TXN_HEADER_SZ
)));
}
let mut expected_pubkey = [0u8; 32];
expected_pubkey.copy_from_slice(&body[TXN_FEE_PAYER_OFFSET..TXN_FEE_PAYER_OFFSET + 32]);
Ok(SigningMessage {
m: tn_signature::signing_message(SignatureDomain::Transaction, body),
expected_pubkey,
body: body.to_vec(),
})
}
pub fn attach_signature(msg: &SigningMessage, sig: [u8; 64]) -> Result<Vec<u8>, SignError> {
tn_signature::verify_message(&msg.m, &sig, &msg.expected_pubkey)
.map_err(|_| SignError::InvalidSignature)?;
let mut signed = Vec::with_capacity(msg.body.len() + 64);
signed.extend_from_slice(&msg.body);
signed.extend_from_slice(&sig);
Ok(signed)
}
pub trait Signer {
fn public_key(&self) -> [u8; 32];
fn sign(&self, msg: &SigningMessage) -> Result<[u8; 64], SignError>;
}
pub fn sign_with(signer: &dyn Signer, msg: &SigningMessage) -> Result<Vec<u8>, SignError> {
let pk = signer.public_key();
if pk != *msg.expected_pubkey() {
return Err(SignError::KeyMismatch {
expected: *msg.expected_pubkey(),
got: pk,
});
}
let sig = signer.sign(msg)?;
attach_signature(msg, sig)
}
pub struct LocalSigner {
signing_key: SigningKey,
}
impl LocalSigner {
pub fn from_seed(seed: [u8; 32]) -> Self {
Self {
signing_key: SigningKey::from_bytes(&seed),
}
}
}
impl Signer for LocalSigner {
fn public_key(&self) -> [u8; 32] {
self.signing_key.verifying_key().to_bytes()
}
fn sign(&self, msg: &SigningMessage) -> Result<[u8; 64], SignError> {
Ok(self.signing_key.sign(msg.as_bytes()).to_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn body_with_fee_payer(pk: &[u8; 32]) -> Vec<u8> {
let mut body = vec![0u8; TXN_HEADER_SZ + 8];
body[TXN_FEE_PAYER_OFFSET..TXN_FEE_PAYER_OFFSET + 32].copy_from_slice(pk);
body
}
#[test]
fn local_signer_round_trip_produces_a_valid_signed_wire() {
let s = LocalSigner::from_seed([7u8; 32]);
let pk = s.public_key();
let body = body_with_fee_payer(&pk);
let msg = build_transaction_signing_message(&body).unwrap();
assert_eq!(msg.as_bytes().len(), 48, "M = DST_TXN ‖ SHA-256(body)");
assert_eq!(msg.expected_pubkey(), &pk, "expected key is the body's fee-payer");
let signed = sign_with(&s, &msg).unwrap();
assert_eq!(signed.len(), body.len() + 64);
assert_eq!(&signed[..body.len()], &body[..]);
let sig: [u8; 64] = signed[body.len()..].try_into().unwrap();
assert!(crate::tn_signature::verify_transaction(&body, &sig, &pk).is_ok());
}
#[test]
fn key_mismatch_is_fail_fast() {
let s = LocalSigner::from_seed([1u8; 32]);
let body = body_with_fee_payer(&[2u8; 32]);
let msg = build_transaction_signing_message(&body).unwrap();
assert!(matches!(sign_with(&s, &msg), Err(SignError::KeyMismatch { .. })));
}
#[test]
fn attach_rejects_tampered_signature() {
let s = LocalSigner::from_seed([3u8; 32]);
let pk = s.public_key();
let body = body_with_fee_payer(&pk);
let msg = build_transaction_signing_message(&body).unwrap();
let mut sig = s.sign(&msg).unwrap();
sig[40] ^= 1;
assert!(attach_signature(&msg, sig).is_err());
}
#[test]
fn short_body_is_rejected() {
assert!(build_transaction_signing_message(b"too short").is_err());
}
}