1use crate::tn_signature::{self, SignatureDomain};
16use ed25519_dalek::{Signer as _, SigningKey};
17use std::fmt;
18
19#[derive(Debug)]
20pub enum SignError {
21 KeyMismatch { expected: [u8; 32], got: [u8; 32] },
23 InvalidSignature,
26 Rejected,
28 Backend(String),
30}
31
32impl fmt::Display for SignError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 SignError::KeyMismatch { .. } => write!(f, "signer public key does not match expected key"),
36 SignError::InvalidSignature => write!(f, "invalid signature"),
37 SignError::Rejected => write!(f, "signing rejected"),
38 SignError::Backend(s) => write!(f, "signer backend error: {s}"),
39 }
40 }
41}
42impl std::error::Error for SignError {}
43
44const TXN_FEE_PAYER_OFFSET: usize = 48;
47const TXN_HEADER_SZ: usize = 112;
49
50#[derive(Clone)]
56pub struct SigningMessage {
57 m: Vec<u8>,
58 expected_pubkey: [u8; 32],
59 body: Vec<u8>,
60}
61
62impl SigningMessage {
63 pub fn as_bytes(&self) -> &[u8] {
65 &self.m
66 }
67 pub fn expected_pubkey(&self) -> &[u8; 32] {
70 &self.expected_pubkey
71 }
72 pub fn body(&self) -> &[u8] {
74 &self.body
75 }
76}
77
78pub fn build_transaction_signing_message(body: &[u8]) -> Result<SigningMessage, SignError> {
85 if body.len() < TXN_HEADER_SZ {
86 return Err(SignError::Backend(format!(
87 "transaction body {} bytes is shorter than the {}-byte header",
88 body.len(),
89 TXN_HEADER_SZ
90 )));
91 }
92 let mut expected_pubkey = [0u8; 32];
93 expected_pubkey.copy_from_slice(&body[TXN_FEE_PAYER_OFFSET..TXN_FEE_PAYER_OFFSET + 32]);
94 Ok(SigningMessage {
95 m: tn_signature::signing_message(SignatureDomain::Transaction, body),
96 expected_pubkey,
97 body: body.to_vec(),
98 })
99}
100
101pub fn attach_signature(msg: &SigningMessage, sig: [u8; 64]) -> Result<Vec<u8>, SignError> {
107 tn_signature::verify_message(&msg.m, &sig, &msg.expected_pubkey)
108 .map_err(|_| SignError::InvalidSignature)?;
109 let mut signed = Vec::with_capacity(msg.body.len() + 64);
110 signed.extend_from_slice(&msg.body);
111 signed.extend_from_slice(&sig);
112 Ok(signed)
113}
114
115pub trait Signer {
118 fn public_key(&self) -> [u8; 32];
120 fn sign(&self, msg: &SigningMessage) -> Result<[u8; 64], SignError>;
121}
122
123pub fn sign_with(signer: &dyn Signer, msg: &SigningMessage) -> Result<Vec<u8>, SignError> {
127 let pk = signer.public_key();
128 if pk != *msg.expected_pubkey() {
129 return Err(SignError::KeyMismatch {
130 expected: *msg.expected_pubkey(),
131 got: pk,
132 });
133 }
134 let sig = signer.sign(msg)?;
135 attach_signature(msg, sig)
136}
137
138pub struct LocalSigner {
142 signing_key: SigningKey,
143}
144
145impl LocalSigner {
146 pub fn from_seed(seed: [u8; 32]) -> Self {
147 Self {
148 signing_key: SigningKey::from_bytes(&seed),
149 }
150 }
151}
152
153impl Signer for LocalSigner {
154 fn public_key(&self) -> [u8; 32] {
155 self.signing_key.verifying_key().to_bytes()
156 }
157 fn sign(&self, msg: &SigningMessage) -> Result<[u8; 64], SignError> {
158 Ok(self.signing_key.sign(msg.as_bytes()).to_bytes())
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn body_with_fee_payer(pk: &[u8; 32]) -> Vec<u8> {
170 let mut body = vec![0u8; TXN_HEADER_SZ + 8];
171 body[TXN_FEE_PAYER_OFFSET..TXN_FEE_PAYER_OFFSET + 32].copy_from_slice(pk);
172 body
173 }
174
175 #[test]
176 fn local_signer_round_trip_produces_a_valid_signed_wire() {
177 let s = LocalSigner::from_seed([7u8; 32]);
178 let pk = s.public_key();
179 let body = body_with_fee_payer(&pk);
180 let msg = build_transaction_signing_message(&body).unwrap();
181 assert_eq!(msg.as_bytes().len(), 48, "M = DST_TXN ‖ SHA-256(body)");
182 assert_eq!(msg.expected_pubkey(), &pk, "expected key is the body's fee-payer");
183 let signed = sign_with(&s, &msg).unwrap();
184 assert_eq!(signed.len(), body.len() + 64);
186 assert_eq!(&signed[..body.len()], &body[..]);
187 let sig: [u8; 64] = signed[body.len()..].try_into().unwrap();
188 assert!(crate::tn_signature::verify_transaction(&body, &sig, &pk).is_ok());
189 }
190
191 #[test]
192 fn key_mismatch_is_fail_fast() {
193 let s = LocalSigner::from_seed([1u8; 32]);
195 let body = body_with_fee_payer(&[2u8; 32]);
196 let msg = build_transaction_signing_message(&body).unwrap();
197 assert!(matches!(sign_with(&s, &msg), Err(SignError::KeyMismatch { .. })));
198 }
199
200 #[test]
201 fn attach_rejects_tampered_signature() {
202 let s = LocalSigner::from_seed([3u8; 32]);
203 let pk = s.public_key();
204 let body = body_with_fee_payer(&pk);
205 let msg = build_transaction_signing_message(&body).unwrap();
206 let mut sig = s.sign(&msg).unwrap();
207 sig[40] ^= 1;
208 assert!(attach_signature(&msg, sig).is_err());
209 }
210
211 #[test]
212 fn short_body_is_rejected() {
213 assert!(build_transaction_signing_message(b"too short").is_err());
214 }
215}