peat_mesh/security/
keypair.rs1use super::device_id::DeviceId;
4use super::error::SecurityError;
5use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
6use rand_core::OsRng;
7use std::path::Path;
8
9#[derive(Clone)]
36pub struct DeviceKeypair {
37 signing_key: SigningKey,
38}
39
40impl DeviceKeypair {
41 pub fn generate() -> Self {
43 let signing_key = SigningKey::generate(&mut OsRng);
44 DeviceKeypair { signing_key }
45 }
46
47 pub fn from_signing_key(signing_key: SigningKey) -> Self {
49 DeviceKeypair { signing_key }
50 }
51
52 pub fn from_seed(seed: &[u8], context: &str) -> Result<Self, SecurityError> {
61 use hkdf::Hkdf;
62 use sha2::Sha256;
63
64 let hk = Hkdf::<Sha256>::new(None, seed);
65 let mut okm = [0u8; 32];
66 hk.expand(context.as_bytes(), &mut okm)
67 .map_err(|e| SecurityError::KeypairError(format!("HKDF expand failed: {}", e)))?;
68
69 let signing_key = SigningKey::from_bytes(&okm);
70 Ok(DeviceKeypair { signing_key })
71 }
72
73 pub fn from_secret_bytes(bytes: &[u8]) -> Result<Self, SecurityError> {
75 if bytes.len() != 32 {
76 return Err(SecurityError::KeypairError(format!(
77 "expected 32 bytes, got {}",
78 bytes.len()
79 )));
80 }
81
82 let signing_key = SigningKey::from_bytes(bytes.try_into().unwrap());
83 Ok(DeviceKeypair { signing_key })
84 }
85
86 pub fn load_from_file(path: &Path) -> Result<Self, SecurityError> {
88 let bytes = std::fs::read(path)?;
89 Self::from_secret_bytes(&bytes)
90 }
91
92 pub fn save_to_file(&self, path: &Path) -> Result<(), SecurityError> {
99 std::fs::write(path, self.signing_key.to_bytes())?;
100 Ok(())
101 }
102
103 pub fn device_id(&self) -> DeviceId {
105 DeviceId::from_public_key(&self.signing_key.verifying_key())
106 }
107
108 pub fn verifying_key(&self) -> VerifyingKey {
110 self.signing_key.verifying_key()
111 }
112
113 pub fn public_key_bytes(&self) -> [u8; 32] {
115 self.signing_key.verifying_key().to_bytes()
116 }
117
118 pub fn secret_key_bytes(&self) -> [u8; 32] {
126 self.signing_key.to_bytes()
127 }
128
129 pub fn sign(&self, message: &[u8]) -> Signature {
131 self.signing_key.sign(message)
132 }
133
134 pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), SecurityError> {
136 self.signing_key
137 .verifying_key()
138 .verify(message, signature)
139 .map_err(|e| SecurityError::InvalidSignature(e.to_string()))
140 }
141
142 pub fn verify_with_key(
144 public_key: &VerifyingKey,
145 message: &[u8],
146 signature: &Signature,
147 ) -> Result<(), SecurityError> {
148 public_key
149 .verify(message, signature)
150 .map_err(|e| SecurityError::InvalidSignature(e.to_string()))
151 }
152
153 pub fn signature_from_bytes(bytes: &[u8]) -> Result<Signature, SecurityError> {
155 if bytes.len() != 64 {
156 return Err(SecurityError::InvalidSignature(format!(
157 "expected 64 bytes, got {}",
158 bytes.len()
159 )));
160 }
161
162 Ok(Signature::from_bytes(bytes.try_into().unwrap()))
164 }
165
166 pub fn verifying_key_from_bytes(bytes: &[u8]) -> Result<VerifyingKey, SecurityError> {
168 if bytes.len() != 32 {
169 return Err(SecurityError::InvalidPublicKey(format!(
170 "expected 32 bytes, got {}",
171 bytes.len()
172 )));
173 }
174
175 VerifyingKey::from_bytes(bytes.try_into().unwrap())
176 .map_err(|e| SecurityError::InvalidPublicKey(e.to_string()))
177 }
178}
179
180impl std::fmt::Debug for DeviceKeypair {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 f.debug_struct("DeviceKeypair")
183 .field("device_id", &self.device_id())
184 .field("public_key", &"[REDACTED]")
185 .finish()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use tempfile::tempdir;
193
194 #[test]
195 fn test_generate_keypair() {
196 let keypair = DeviceKeypair::generate();
197 let device_id = keypair.device_id();
198 assert_eq!(device_id.to_hex().len(), 32);
199 }
200
201 #[test]
202 fn test_sign_and_verify() {
203 let keypair = DeviceKeypair::generate();
204 let message = b"test message";
205
206 let signature = keypair.sign(message);
207 assert!(keypair.verify(message, &signature).is_ok());
208 }
209
210 #[test]
211 fn test_verify_wrong_message_fails() {
212 let keypair = DeviceKeypair::generate();
213 let signature = keypair.sign(b"original message");
214
215 let result = keypair.verify(b"different message", &signature);
216 assert!(result.is_err());
217 }
218
219 #[test]
220 fn test_verify_wrong_key_fails() {
221 let keypair1 = DeviceKeypair::generate();
222 let keypair2 = DeviceKeypair::generate();
223
224 let message = b"test message";
225 let signature = keypair1.sign(message);
226
227 let result = keypair2.verify(message, &signature);
228 assert!(result.is_err());
229 }
230
231 #[test]
232 fn test_save_and_load_keypair() {
233 let dir = tempdir().unwrap();
234 let path = dir.path().join("test_key.bin");
235
236 let keypair1 = DeviceKeypair::generate();
237 keypair1.save_to_file(&path).unwrap();
238
239 let keypair2 = DeviceKeypair::load_from_file(&path).unwrap();
240
241 assert_eq!(keypair1.device_id(), keypair2.device_id());
242
243 let message = b"test";
244 let sig = keypair1.sign(message);
245 assert!(keypair2.verify(message, &sig).is_ok());
246 }
247
248 #[test]
249 fn test_from_secret_bytes() {
250 let keypair1 = DeviceKeypair::generate();
251 let secret_bytes = keypair1.signing_key.to_bytes();
252
253 let keypair2 = DeviceKeypair::from_secret_bytes(&secret_bytes).unwrap();
254 assert_eq!(keypair1.device_id(), keypair2.device_id());
255 }
256
257 #[test]
258 fn test_from_secret_bytes_wrong_length() {
259 let result = DeviceKeypair::from_secret_bytes(&[0u8; 16]);
260 assert!(result.is_err());
261 }
262
263 #[test]
264 fn test_from_signing_key() {
265 let key = SigningKey::generate(&mut OsRng);
266 let expected_id = DeviceId::from_public_key(&key.verifying_key());
267
268 let keypair = DeviceKeypair::from_signing_key(key);
269 assert_eq!(keypair.device_id(), expected_id);
270 }
271
272 #[test]
273 fn test_verifying_key() {
274 let keypair = DeviceKeypair::generate();
275 let vk = keypair.verifying_key();
276 let sig = keypair.sign(b"test");
278 assert!(vk.verify(b"test", &sig).is_ok());
279 }
280
281 #[test]
282 fn test_public_key_bytes() {
283 let keypair = DeviceKeypair::generate();
284 let bytes = keypair.public_key_bytes();
285 assert_eq!(bytes.len(), 32);
286 assert_eq!(bytes, keypair.verifying_key().to_bytes());
287 }
288
289 #[test]
290 fn test_secret_key_bytes() {
291 let keypair = DeviceKeypair::generate();
292 let bytes = keypair.secret_key_bytes();
293 assert_eq!(bytes.len(), 32);
294
295 let keypair2 = DeviceKeypair::from_secret_bytes(&bytes).unwrap();
297 assert_eq!(keypair.device_id(), keypair2.device_id());
298 }
299
300 #[test]
301 fn test_verify_with_key() {
302 let keypair = DeviceKeypair::generate();
303 let message = b"hello";
304 let sig = keypair.sign(message);
305
306 let vk = keypair.verifying_key();
307 assert!(DeviceKeypair::verify_with_key(&vk, message, &sig).is_ok());
308
309 assert!(DeviceKeypair::verify_with_key(&vk, b"wrong", &sig).is_err());
311 }
312
313 #[test]
314 fn test_verifying_key_from_bytes() {
315 let keypair = DeviceKeypair::generate();
316 let pk_bytes = keypair.public_key_bytes();
317
318 let vk = DeviceKeypair::verifying_key_from_bytes(&pk_bytes).unwrap();
319 assert_eq!(vk, keypair.verifying_key());
320 }
321
322 #[test]
323 fn test_verifying_key_from_bytes_wrong_length() {
324 let result = DeviceKeypair::verifying_key_from_bytes(&[0u8; 16]);
325 assert!(result.is_err());
326 }
327
328 #[test]
329 fn test_signature_from_bytes_roundtrip() {
330 let keypair = DeviceKeypair::generate();
331 let signature = keypair.sign(b"test");
332
333 let sig_bytes = signature.to_bytes();
334 let parsed = DeviceKeypair::signature_from_bytes(&sig_bytes).unwrap();
335
336 assert_eq!(signature, parsed);
337 }
338
339 #[test]
340 fn test_signature_from_bytes_wrong_length() {
341 let result = DeviceKeypair::signature_from_bytes(&[0u8; 32]);
342 assert!(result.is_err());
343 }
344
345 #[test]
346 fn test_load_from_nonexistent_file() {
347 let result = DeviceKeypair::load_from_file(Path::new("/nonexistent/key.bin"));
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn test_load_from_file_wrong_length() {
353 let dir = tempdir().unwrap();
354 let path = dir.path().join("bad_key.bin");
355 std::fs::write(&path, [0u8; 10]).unwrap();
356
357 let result = DeviceKeypair::load_from_file(&path);
358 assert!(result.is_err());
359 }
360
361 #[test]
362 fn test_from_seed_deterministic() {
363 let seed = b"my-kubernetes-secret";
364 let context = "pod-alpha";
365
366 let kp1 = DeviceKeypair::from_seed(seed, context).unwrap();
367 let kp2 = DeviceKeypair::from_seed(seed, context).unwrap();
368
369 assert_eq!(kp1.device_id(), kp2.device_id());
370 assert_eq!(kp1.public_key_bytes(), kp2.public_key_bytes());
371 }
372
373 #[test]
374 fn test_from_seed_different_context_different_key() {
375 let seed = b"shared-seed";
376
377 let kp1 = DeviceKeypair::from_seed(seed, "context-a").unwrap();
378 let kp2 = DeviceKeypair::from_seed(seed, "context-b").unwrap();
379
380 assert_ne!(kp1.device_id(), kp2.device_id());
381 }
382
383 #[test]
384 fn test_from_seed_different_seed_different_key() {
385 let kp1 = DeviceKeypair::from_seed(b"seed-one", "same-context").unwrap();
386 let kp2 = DeviceKeypair::from_seed(b"seed-two", "same-context").unwrap();
387
388 assert_ne!(kp1.device_id(), kp2.device_id());
389 }
390
391 #[test]
392 fn test_from_seed_sign_verify() {
393 let kp = DeviceKeypair::from_seed(b"test-seed", "test-ctx").unwrap();
394 let message = b"hello kubernetes";
395 let sig = kp.sign(message);
396 assert!(kp.verify(message, &sig).is_ok());
397 }
398
399 #[test]
400 fn test_from_seed_empty_seed() {
401 let kp = DeviceKeypair::from_seed(b"", "some-context");
402 assert!(kp.is_ok());
403 let kp = kp.unwrap();
405 let sig = kp.sign(b"msg");
406 assert!(kp.verify(b"msg", &sig).is_ok());
407 }
408
409 #[test]
410 fn test_debug_redacts_key() {
411 let keypair = DeviceKeypair::generate();
412 let debug = format!("{:?}", keypair);
413 assert!(debug.contains("DeviceKeypair"));
414 assert!(debug.contains("REDACTED"));
415 assert!(!debug.contains("signing_key"));
417 }
418}