Skip to main content

peat_mesh/security/
keypair.rs

1//! Ed25519 keypair for device identity and signing.
2
3use 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/// Ed25519 keypair for device identity and authentication.
10///
11/// The keypair consists of:
12/// - A 32-byte secret (signing) key
13/// - A 32-byte public (verifying) key
14///
15/// The [`DeviceId`] is derived from the public key.
16///
17/// # Example
18///
19/// ```ignore
20/// use peat_mesh::security::DeviceKeypair;
21///
22/// // Generate a new keypair
23/// let keypair = DeviceKeypair::generate();
24///
25/// // Get the device ID
26/// let device_id = keypair.device_id();
27///
28/// // Sign a message
29/// let message = b"hello world";
30/// let signature = keypair.sign(message);
31///
32/// // Verify the signature
33/// assert!(keypair.verify(message, &signature).is_ok());
34/// ```
35#[derive(Clone)]
36pub struct DeviceKeypair {
37    signing_key: SigningKey,
38}
39
40impl DeviceKeypair {
41    /// Generate a new random keypair.
42    pub fn generate() -> Self {
43        let signing_key = SigningKey::generate(&mut OsRng);
44        DeviceKeypair { signing_key }
45    }
46
47    /// Create from an existing signing key.
48    pub fn from_signing_key(signing_key: SigningKey) -> Self {
49        DeviceKeypair { signing_key }
50    }
51
52    /// Create a deterministic keypair from a seed and context string.
53    ///
54    /// Uses HKDF-SHA256 to derive 32 bytes from `seed` (IKM) with
55    /// `context` as the info parameter. Same seed + context always
56    /// produces the same keypair; different context → different key.
57    ///
58    /// Useful for Kubernetes deployments where pods derive stable
59    /// identities from a shared secret + pod-specific context.
60    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    /// Create from raw secret key bytes (32 bytes).
74    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    /// Load keypair from a file (raw 32-byte secret key).
87    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    /// Save keypair to a file (raw 32-byte secret key).
93    ///
94    /// # Security Note
95    ///
96    /// In MVP, this saves the key unencrypted. Production deployments
97    /// should use encrypted key storage (Phase 2).
98    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    /// Get the device ID derived from this keypair's public key.
104    pub fn device_id(&self) -> DeviceId {
105        DeviceId::from_public_key(&self.signing_key.verifying_key())
106    }
107
108    /// Get the public (verifying) key.
109    pub fn verifying_key(&self) -> VerifyingKey {
110        self.signing_key.verifying_key()
111    }
112
113    /// Get the public key as bytes.
114    pub fn public_key_bytes(&self) -> [u8; 32] {
115        self.signing_key.verifying_key().to_bytes()
116    }
117
118    /// Get the secret key bytes (32 bytes).
119    ///
120    /// # Security Warning
121    ///
122    /// This exposes the private key material. Only use for:
123    /// - Secure storage/persistence
124    /// - Cross-crate interop (e.g., converting to peat_btle::DeviceIdentity)
125    pub fn secret_key_bytes(&self) -> [u8; 32] {
126        self.signing_key.to_bytes()
127    }
128
129    /// Sign a message with the secret key.
130    pub fn sign(&self, message: &[u8]) -> Signature {
131        self.signing_key.sign(message)
132    }
133
134    /// Verify a signature against this keypair's public key.
135    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    /// Verify a signature against a specific public key.
143    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    /// Parse a signature from bytes.
154    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        // ed25519-dalek v2 from_bytes returns Signature directly (infallible after length check)
163        Ok(Signature::from_bytes(bytes.try_into().unwrap()))
164    }
165
166    /// Parse a verifying key from bytes.
167    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        // Verify the public key can verify signatures
277        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        // Reconstruct from secret bytes and verify identity
296        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        // Wrong message
310        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        // Should still produce a valid keypair
404        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        // Should NOT contain raw key bytes
416        assert!(!debug.contains("signing_key"));
417    }
418}