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::getrandom(&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
127pub fn verify_stage_id(stage_id: &str, signature: &Signature) -> Result<(), SigningError> {
136 const PK_HEX_LEN: usize = 64;
137 const SIG_HEX_LEN: usize = 128;
138 if signature.public_key.len() != PK_HEX_LEN {
139 return Err(SigningError::PublicKeyLength {
140 expected: PK_HEX_LEN, got: signature.public_key.len(),
141 });
142 }
143 if signature.signature.len() != SIG_HEX_LEN {
144 return Err(SigningError::SignatureLength {
145 expected: SIG_HEX_LEN, got: signature.signature.len(),
146 });
147 }
148 let pk_bytes = hex::decode(&signature.public_key)
149 .map_err(|e| SigningError::BadHex(e.to_string()))?;
150 let sig_bytes = hex::decode(&signature.signature)
151 .map_err(|e| SigningError::BadHex(e.to_string()))?;
152 let pk_arr: [u8; 32] = pk_bytes.try_into().expect("length-checked");
153 let sig_arr: [u8; 64] = sig_bytes.try_into().expect("length-checked");
154 let pk = VerifyingKey::from_bytes(&pk_arr)
155 .map_err(|_| SigningError::BadPublicKey)?;
156 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
157 pk.verify(stage_id.as_bytes(), &sig)
158 .map_err(|_| SigningError::VerifyFailed)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn fixture() -> Keypair {
166 Keypair::from_seed(&[7u8; 32])
168 }
169
170 #[test]
171 fn generate_produces_distinct_keys() {
172 let a = Keypair::generate().unwrap();
173 let b = Keypair::generate().unwrap();
174 assert_ne!(a.public_hex(), b.public_hex(),
175 "two keygen calls must not produce identical keys");
176 assert_ne!(a.secret_hex(), b.secret_hex());
177 }
178
179 #[test]
180 fn public_and_secret_hex_lengths_are_canonical() {
181 let kp = fixture();
182 assert_eq!(kp.public_hex().len(), 64);
183 assert_eq!(kp.secret_hex().len(), 64);
184 assert!(kp.public_hex().chars().all(|c| c.is_ascii_hexdigit()));
185 assert!(kp.secret_hex().chars().all(|c| c.is_ascii_hexdigit()));
186 }
187
188 #[test]
189 fn sign_then_verify_round_trips() {
190 let kp = fixture();
191 let stage_id = "deadbeefcafef00d";
192 let sig = kp.sign_stage_id(stage_id);
193 assert_eq!(sig.public_key, kp.public_hex());
194 assert_eq!(sig.signature.len(), 128);
195 verify_stage_id(stage_id, &sig).expect("signature must verify");
196 }
197
198 #[test]
199 fn signing_is_deterministic_for_same_input() {
200 let kp = fixture();
205 let s1 = kp.sign_stage_id("stage-abc");
206 let s2 = kp.sign_stage_id("stage-abc");
207 assert_eq!(s1, s2);
208 }
209
210 #[test]
211 fn different_stage_ids_produce_different_signatures() {
212 let kp = fixture();
213 let a = kp.sign_stage_id("stage-A");
214 let b = kp.sign_stage_id("stage-B");
215 assert_ne!(a.signature, b.signature);
216 }
217
218 #[test]
219 fn verification_rejects_tampered_stage_id() {
220 let kp = fixture();
221 let sig = kp.sign_stage_id("real-stage-id");
222 let err = verify_stage_id("forged-stage-id", &sig).unwrap_err();
223 assert!(matches!(err, SigningError::VerifyFailed),
224 "tampered stage_id must fail verification, got {err:?}");
225 }
226
227 #[test]
228 fn verification_rejects_wrong_public_key() {
229 let kp_a = Keypair::from_seed(&[1u8; 32]);
232 let kp_b = Keypair::from_seed(&[2u8; 32]);
233 let mut sig = kp_a.sign_stage_id("stage-id");
234 sig.public_key = kp_b.public_hex();
235 let err = verify_stage_id("stage-id", &sig).unwrap_err();
236 assert!(matches!(err, SigningError::VerifyFailed));
237 }
238
239 #[test]
240 fn from_secret_hex_round_trips() {
241 let original = Keypair::from_seed(&[42u8; 32]);
242 let hex_secret = original.secret_hex();
243 let parsed = Keypair::from_secret_hex(&hex_secret).unwrap();
244 assert_eq!(original.public_hex(), parsed.public_hex());
245 assert_eq!(
247 original.sign_stage_id("x"),
248 parsed.sign_stage_id("x"),
249 );
250 }
251
252 #[test]
253 fn from_secret_hex_rejects_wrong_length() {
254 let err = Keypair::from_secret_hex("deadbeef").unwrap_err();
255 assert!(matches!(err, SigningError::SecretKeyLength { .. }));
256 }
257
258 #[test]
259 fn from_secret_hex_rejects_invalid_hex() {
260 let bad = "z".repeat(64);
261 let err = Keypair::from_secret_hex(&bad).unwrap_err();
262 assert!(matches!(err, SigningError::BadHex(_)));
263 }
264
265 #[test]
266 fn verify_rejects_malformed_signature_lengths() {
267 let mut sig = fixture().sign_stage_id("x");
268 sig.signature = "deadbeef".into();
269 let err = verify_stage_id("x", &sig).unwrap_err();
270 assert!(matches!(err, SigningError::SignatureLength { .. }));
271 }
272
273 #[test]
274 fn verify_rejects_malformed_public_key_lengths() {
275 let mut sig = fixture().sign_stage_id("x");
276 sig.public_key = "deadbeef".into();
277 let err = verify_stage_id("x", &sig).unwrap_err();
278 assert!(matches!(err, SigningError::PublicKeyLength { .. }));
279 }
280}