1use crate::attestation::Signature;
31use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey, SECRET_KEY_LENGTH};
32
33#[derive(Debug, thiserror::Error)]
35pub enum SigningError {
36 #[error("system entropy unavailable: {0}")]
37 Entropy(String),
38 #[error("public key must be {expected} hex chars (32 bytes), got {got}")]
39 PublicKeyLength { expected: usize, got: usize },
40 #[error("secret key must be {expected} hex chars (32 bytes), got {got}")]
41 SecretKeyLength { expected: usize, got: usize },
42 #[error("signature must be {expected} hex chars (64 bytes), got {got}")]
43 SignatureLength { expected: usize, got: usize },
44 #[error("invalid hex: {0}")]
45 BadHex(String),
46 #[error("invalid public key bytes")]
47 BadPublicKey,
48 #[error("signature did not verify against the given public key and stage id")]
49 VerifyFailed,
50}
51
52pub struct Keypair {
57 inner: SigningKey,
58}
59
60impl std::fmt::Debug for Keypair {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("Keypair")
65 .field("public_key", &self.public_hex())
66 .field("secret_key", &"<redacted>")
67 .finish()
68 }
69}
70
71impl Keypair {
72 pub fn generate() -> Result<Self, SigningError> {
76 let mut seed = [0u8; SECRET_KEY_LENGTH];
77 getrandom::fill(&mut seed)
78 .map_err(|e| SigningError::Entropy(e.to_string()))?;
79 Ok(Self::from_seed(&seed))
80 }
81
82 pub fn from_seed(seed: &[u8; SECRET_KEY_LENGTH]) -> Self {
86 Self { inner: SigningKey::from_bytes(seed) }
87 }
88
89 pub fn from_secret_hex(hex_str: &str) -> Result<Self, SigningError> {
91 const EXPECTED: usize = SECRET_KEY_LENGTH * 2;
92 if hex_str.len() != EXPECTED {
93 return Err(SigningError::SecretKeyLength {
94 expected: EXPECTED, got: hex_str.len()
95 });
96 }
97 let bytes = hex::decode(hex_str)
98 .map_err(|e| SigningError::BadHex(e.to_string()))?;
99 let arr: [u8; SECRET_KEY_LENGTH] = bytes.try_into()
100 .expect("length-checked above");
101 Ok(Self::from_seed(&arr))
102 }
103
104 pub fn secret_hex(&self) -> String {
107 hex::encode(self.inner.to_bytes())
108 }
109
110 pub fn public_hex(&self) -> String {
112 hex::encode(self.inner.verifying_key().to_bytes())
113 }
114
115 pub fn sign_stage_id(&self, stage_id: &str) -> Signature {
119 let sig = self.inner.sign(stage_id.as_bytes());
120 Signature {
121 public_key: self.public_hex(),
122 signature: hex::encode(sig.to_bytes()),
123 }
124 }
125
126 pub fn sign_message(&self, msg: &[u8]) -> String {
132 hex::encode(self.inner.sign(msg).to_bytes())
133 }
134}
135
136pub fn verify_stage_id(stage_id: &str, signature: &Signature) -> Result<(), SigningError> {
145 const PK_HEX_LEN: usize = 64;
146 const SIG_HEX_LEN: usize = 128;
147 if signature.public_key.len() != PK_HEX_LEN {
148 return Err(SigningError::PublicKeyLength {
149 expected: PK_HEX_LEN, got: signature.public_key.len(),
150 });
151 }
152 if signature.signature.len() != SIG_HEX_LEN {
153 return Err(SigningError::SignatureLength {
154 expected: SIG_HEX_LEN, got: signature.signature.len(),
155 });
156 }
157 let pk_bytes = hex::decode(&signature.public_key)
158 .map_err(|e| SigningError::BadHex(e.to_string()))?;
159 let sig_bytes = hex::decode(&signature.signature)
160 .map_err(|e| SigningError::BadHex(e.to_string()))?;
161 let pk_arr: [u8; 32] = pk_bytes.try_into().expect("length-checked");
162 let sig_arr: [u8; 64] = sig_bytes.try_into().expect("length-checked");
163 let pk = VerifyingKey::from_bytes(&pk_arr)
164 .map_err(|_| SigningError::BadPublicKey)?;
165 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
166 pk.verify(stage_id.as_bytes(), &sig)
167 .map_err(|_| SigningError::VerifyFailed)
168}
169
170pub fn verify_message(
175 public_key_hex: &str,
176 msg: &[u8],
177 signature_hex: &str,
178) -> Result<(), SigningError> {
179 const PK_HEX_LEN: usize = 64;
180 const SIG_HEX_LEN: usize = 128;
181 if public_key_hex.len() != PK_HEX_LEN {
182 return Err(SigningError::PublicKeyLength {
183 expected: PK_HEX_LEN,
184 got: public_key_hex.len(),
185 });
186 }
187 if signature_hex.len() != SIG_HEX_LEN {
188 return Err(SigningError::SignatureLength {
189 expected: SIG_HEX_LEN,
190 got: signature_hex.len(),
191 });
192 }
193 let pk_bytes =
194 hex::decode(public_key_hex).map_err(|e| SigningError::BadHex(e.to_string()))?;
195 let sig_bytes =
196 hex::decode(signature_hex).map_err(|e| SigningError::BadHex(e.to_string()))?;
197 let pk_arr: [u8; 32] = pk_bytes.try_into().expect("length-checked");
198 let sig_arr: [u8; 64] = sig_bytes.try_into().expect("length-checked");
199 let pk = VerifyingKey::from_bytes(&pk_arr).map_err(|_| SigningError::BadPublicKey)?;
200 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
201 pk.verify(msg, &sig).map_err(|_| SigningError::VerifyFailed)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn fixture() -> Keypair {
209 Keypair::from_seed(&[7u8; 32])
211 }
212
213 #[test]
214 fn generate_produces_distinct_keys() {
215 let a = Keypair::generate().unwrap();
216 let b = Keypair::generate().unwrap();
217 assert_ne!(a.public_hex(), b.public_hex(),
218 "two keygen calls must not produce identical keys");
219 assert_ne!(a.secret_hex(), b.secret_hex());
220 }
221
222 #[test]
223 fn public_and_secret_hex_lengths_are_canonical() {
224 let kp = fixture();
225 assert_eq!(kp.public_hex().len(), 64);
226 assert_eq!(kp.secret_hex().len(), 64);
227 assert!(kp.public_hex().chars().all(|c| c.is_ascii_hexdigit()));
228 assert!(kp.secret_hex().chars().all(|c| c.is_ascii_hexdigit()));
229 }
230
231 #[test]
232 fn sign_then_verify_round_trips() {
233 let kp = fixture();
234 let stage_id = "deadbeefcafef00d";
235 let sig = kp.sign_stage_id(stage_id);
236 assert_eq!(sig.public_key, kp.public_hex());
237 assert_eq!(sig.signature.len(), 128);
238 verify_stage_id(stage_id, &sig).expect("signature must verify");
239 }
240
241 #[test]
242 fn signing_is_deterministic_for_same_input() {
243 let kp = fixture();
248 let s1 = kp.sign_stage_id("stage-abc");
249 let s2 = kp.sign_stage_id("stage-abc");
250 assert_eq!(s1, s2);
251 }
252
253 #[test]
254 fn different_stage_ids_produce_different_signatures() {
255 let kp = fixture();
256 let a = kp.sign_stage_id("stage-A");
257 let b = kp.sign_stage_id("stage-B");
258 assert_ne!(a.signature, b.signature);
259 }
260
261 #[test]
262 fn verification_rejects_tampered_stage_id() {
263 let kp = fixture();
264 let sig = kp.sign_stage_id("real-stage-id");
265 let err = verify_stage_id("forged-stage-id", &sig).unwrap_err();
266 assert!(matches!(err, SigningError::VerifyFailed),
267 "tampered stage_id must fail verification, got {err:?}");
268 }
269
270 #[test]
271 fn verification_rejects_wrong_public_key() {
272 let kp_a = Keypair::from_seed(&[1u8; 32]);
275 let kp_b = Keypair::from_seed(&[2u8; 32]);
276 let mut sig = kp_a.sign_stage_id("stage-id");
277 sig.public_key = kp_b.public_hex();
278 let err = verify_stage_id("stage-id", &sig).unwrap_err();
279 assert!(matches!(err, SigningError::VerifyFailed));
280 }
281
282 #[test]
283 fn from_secret_hex_round_trips() {
284 let original = Keypair::from_seed(&[42u8; 32]);
285 let hex_secret = original.secret_hex();
286 let parsed = Keypair::from_secret_hex(&hex_secret).unwrap();
287 assert_eq!(original.public_hex(), parsed.public_hex());
288 assert_eq!(
290 original.sign_stage_id("x"),
291 parsed.sign_stage_id("x"),
292 );
293 }
294
295 #[test]
296 fn from_secret_hex_rejects_wrong_length() {
297 let err = Keypair::from_secret_hex("deadbeef").unwrap_err();
298 assert!(matches!(err, SigningError::SecretKeyLength { .. }));
299 }
300
301 #[test]
302 fn from_secret_hex_rejects_invalid_hex() {
303 let bad = "z".repeat(64);
304 let err = Keypair::from_secret_hex(&bad).unwrap_err();
305 assert!(matches!(err, SigningError::BadHex(_)));
306 }
307
308 #[test]
309 fn verify_rejects_malformed_signature_lengths() {
310 let mut sig = fixture().sign_stage_id("x");
311 sig.signature = "deadbeef".into();
312 let err = verify_stage_id("x", &sig).unwrap_err();
313 assert!(matches!(err, SigningError::SignatureLength { .. }));
314 }
315
316 #[test]
317 fn verify_rejects_malformed_public_key_lengths() {
318 let mut sig = fixture().sign_stage_id("x");
319 sig.public_key = "deadbeef".into();
320 let err = verify_stage_id("x", &sig).unwrap_err();
321 assert!(matches!(err, SigningError::PublicKeyLength { .. }));
322 }
323}