exo_federation/crypto.rs
1//! Post-quantum cryptography primitives
2//!
3//! This module provides cryptographic primitives for federation security:
4//! - CRYSTALS-Kyber-1024 key exchange (NIST FIPS 203)
5//! - ChaCha20-Poly1305 AEAD encryption
6//! - HKDF-SHA256 key derivation
7//! - Constant-time operations
8//! - Secure memory zeroization
9//!
10//! # Security Level
11//!
12//! All primitives provide 256-bit classical security and 128+ bit post-quantum security.
13//!
14//! # Threat Model
15//!
16//! See /docs/SECURITY.md for comprehensive threat model and security architecture.
17
18use serde::{Deserialize, Serialize};
19use crate::{Result, FederationError};
20use zeroize::{Zeroize, ZeroizeOnDrop};
21
22// Re-export for convenience
23pub use pqcrypto_kyber::kyber1024;
24use pqcrypto_traits::kem::{PublicKey, SecretKey, SharedSecret as PqSharedSecret, Ciphertext};
25
26/// Post-quantum cryptographic keypair
27///
28/// Uses CRYSTALS-Kyber-1024 for IND-CCA2 secure key encapsulation.
29///
30/// # Security Properties
31///
32/// - Public key: 1568 bytes (safe to distribute)
33/// - Secret key: 3168 bytes (MUST be protected, auto-zeroized on drop)
34/// - Post-quantum security: 256 bits (NIST Level 5)
35///
36/// # Example
37///
38/// ```ignore
39/// let keypair = PostQuantumKeypair::generate();
40/// let public_bytes = keypair.public_key();
41/// // Send public_bytes to peer
42/// ```
43#[derive(Clone)]
44pub struct PostQuantumKeypair {
45 /// Public key (safe to share)
46 pub public: Vec<u8>,
47 /// Secret key (automatically zeroized on drop)
48 secret: SecretKeyWrapper,
49}
50
51impl std::fmt::Debug for PostQuantumKeypair {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("PostQuantumKeypair")
54 .field("public", &format!("{}bytes", self.public.len()))
55 .field("secret", &"[REDACTED]")
56 .finish()
57 }
58}
59
60/// Wrapper for secret key with automatic zeroization
61#[derive(Clone, Zeroize, ZeroizeOnDrop)]
62struct SecretKeyWrapper(Vec<u8>);
63
64impl PostQuantumKeypair {
65 /// Generate a new post-quantum keypair using CRYSTALS-Kyber-1024
66 ///
67 /// # Security
68 ///
69 /// Uses OS CSPRNG (via `rand::thread_rng()`). Ensure OS has sufficient entropy.
70 ///
71 /// # Panics
72 ///
73 /// Never panics. Kyber key generation is deterministic after RNG sampling.
74 pub fn generate() -> Self {
75 let (public, secret) = kyber1024::keypair();
76
77 Self {
78 public: public.as_bytes().to_vec(),
79 secret: SecretKeyWrapper(secret.as_bytes().to_vec()),
80 }
81 }
82
83 /// Get the public key bytes
84 ///
85 /// Safe to transmit over insecure channels.
86 pub fn public_key(&self) -> &[u8] {
87 &self.public
88 }
89
90 /// Encapsulate: generate shared secret and ciphertext for recipient's public key
91 ///
92 /// # Arguments
93 ///
94 /// * `public_key` - Recipient's Kyber-1024 public key (1568 bytes)
95 ///
96 /// # Returns
97 ///
98 /// * `SharedSecret` - 32-byte shared secret (use for key derivation)
99 /// * `Vec<u8>` - 1568-byte ciphertext (send to recipient)
100 ///
101 /// # Errors
102 ///
103 /// Returns `CryptoError` if public key is invalid (wrong size or corrupted).
104 ///
105 /// # Security
106 ///
107 /// The shared secret is cryptographically strong (256-bit entropy).
108 /// The ciphertext is IND-CCA2 secure against quantum adversaries.
109 pub fn encapsulate(public_key: &[u8]) -> Result<(SharedSecret, Vec<u8>)> {
110 // Validate public key size (Kyber1024 = 1568 bytes)
111 if public_key.len() != 1568 {
112 return Err(FederationError::CryptoError(
113 format!("Invalid public key size: expected 1568 bytes, got {}", public_key.len())
114 ));
115 }
116
117 // Parse public key
118 let pk = kyber1024::PublicKey::from_bytes(public_key)
119 .map_err(|e| FederationError::CryptoError(
120 format!("Failed to parse Kyber public key: {:?}", e)
121 ))?;
122
123 // Perform KEM encapsulation
124 let (shared_secret, ciphertext) = kyber1024::encapsulate(&pk);
125
126 Ok((
127 SharedSecret(SecretBytes(shared_secret.as_bytes().to_vec())),
128 ciphertext.as_bytes().to_vec()
129 ))
130 }
131
132 /// Decapsulate: extract shared secret from ciphertext
133 ///
134 /// # Arguments
135 ///
136 /// * `ciphertext` - 1568-byte Kyber-1024 ciphertext
137 ///
138 /// # Returns
139 ///
140 /// * `SharedSecret` - 32-byte shared secret (same as encapsulator's)
141 ///
142 /// # Errors
143 ///
144 /// Returns `CryptoError` if:
145 /// - Ciphertext is wrong size
146 /// - Ciphertext is invalid or corrupted
147 /// - Decapsulation fails (should never happen with valid inputs)
148 ///
149 /// # Security
150 ///
151 /// Timing-safe: execution time independent of secret key or ciphertext validity.
152 pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<SharedSecret> {
153 // Validate ciphertext size
154 if ciphertext.len() != 1568 {
155 return Err(FederationError::CryptoError(
156 format!("Invalid ciphertext size: expected 1568 bytes, got {}", ciphertext.len())
157 ));
158 }
159
160 // Parse secret key
161 let sk = kyber1024::SecretKey::from_bytes(&self.secret.0)
162 .map_err(|e| FederationError::CryptoError(
163 format!("Failed to parse secret key: {:?}", e)
164 ))?;
165
166 // Parse ciphertext
167 let ct = kyber1024::Ciphertext::from_bytes(ciphertext)
168 .map_err(|e| FederationError::CryptoError(
169 format!("Failed to parse Kyber ciphertext: {:?}", e)
170 ))?;
171
172 // Perform KEM decapsulation
173 let shared_secret = kyber1024::decapsulate(&ct, &sk);
174
175 Ok(SharedSecret(SecretBytes(shared_secret.as_bytes().to_vec())))
176 }
177}
178
179/// Secret bytes wrapper with automatic zeroization
180#[derive(Clone, Zeroize, ZeroizeOnDrop)]
181struct SecretBytes(Vec<u8>);
182
183/// Shared secret derived from Kyber KEM
184///
185/// # Security
186///
187/// - Automatically zeroized on drop
188/// - 32 bytes of cryptographically strong key material
189/// - Suitable for HKDF key derivation
190#[derive(Clone, Zeroize, ZeroizeOnDrop)]
191pub struct SharedSecret(SecretBytes);
192
193impl std::fmt::Debug for SharedSecret {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.debug_struct("SharedSecret")
196 .field("bytes", &"[REDACTED]")
197 .finish()
198 }
199}
200
201impl SharedSecret {
202 /// Derive encryption and MAC keys from shared secret using HKDF-SHA256
203 ///
204 /// # Key Derivation
205 ///
206 /// ```text
207 /// shared_secret (32 bytes from Kyber)
208 /// ↓
209 /// HKDF-Extract(salt=zeros, ikm=shared_secret) → PRK
210 /// ↓
211 /// HKDF-Expand(PRK, info="encryption") → encryption_key (32 bytes)
212 /// HKDF-Expand(PRK, info="mac") → mac_key (32 bytes)
213 /// ```
214 ///
215 /// # Returns
216 ///
217 /// - Encryption key: 256-bit key for ChaCha20
218 /// - MAC key: 256-bit key for Poly1305
219 ///
220 /// # Security
221 ///
222 /// Keys are cryptographically independent. Compromise of one does not affect the other.
223 pub fn derive_keys(&self) -> (Vec<u8>, Vec<u8>) {
224 use hmac::{Hmac, Mac};
225 use sha2::Sha256;
226
227 type HmacSha256 = Hmac<Sha256>;
228
229 // HKDF-Extract: PRK = HMAC-SHA256(salt=zeros, ikm=shared_secret)
230 let salt = [0u8; 32]; // Zero salt is acceptable for Kyber output
231 let mut extract_hmac = HmacSha256::new_from_slice(&salt)
232 .expect("HMAC-SHA256 accepts any key size");
233 extract_hmac.update(&self.0.0);
234 let prk = extract_hmac.finalize().into_bytes();
235
236 // HKDF-Expand for encryption key
237 let mut enc_hmac = HmacSha256::new_from_slice(&prk)
238 .expect("PRK is valid HMAC key");
239 enc_hmac.update(b"encryption");
240 enc_hmac.update(&[1u8]); // Counter = 1
241 let encrypt_key = enc_hmac.finalize().into_bytes().to_vec();
242
243 // HKDF-Expand for MAC key
244 let mut mac_hmac = HmacSha256::new_from_slice(&prk)
245 .expect("PRK is valid HMAC key");
246 mac_hmac.update(b"mac");
247 mac_hmac.update(&[1u8]); // Counter = 1
248 let mac_key = mac_hmac.finalize().into_bytes().to_vec();
249
250 (encrypt_key, mac_key)
251 }
252}
253
254/// Encrypted communication channel using ChaCha20-Poly1305 AEAD
255///
256/// # Security Properties
257///
258/// - Confidentiality: ChaCha20 stream cipher (IND-CPA)
259/// - Integrity: Poly1305 MAC (SUF-CMA)
260/// - AEAD: Combined mode (IND-CCA2)
261/// - Nonce: 96-bit random + 32-bit counter (unique per message)
262///
263/// # Example
264///
265/// ```ignore
266/// let channel = EncryptedChannel::new(peer_id, shared_secret);
267/// let ciphertext = channel.encrypt(b"secret message")?;
268/// let plaintext = channel.decrypt(&ciphertext)?;
269/// ```
270#[derive(Debug, Serialize, Deserialize)]
271pub struct EncryptedChannel {
272 /// Peer identifier
273 pub peer_id: String,
274 /// Encryption key (not serialized - ephemeral)
275 #[serde(skip)]
276 encrypt_key: Vec<u8>,
277 /// MAC key for authentication (not serialized - ephemeral)
278 #[serde(skip)]
279 mac_key: Vec<u8>,
280 /// Message counter for nonce generation
281 #[serde(skip)]
282 counter: std::sync::atomic::AtomicU32,
283}
284
285impl Clone for EncryptedChannel {
286 fn clone(&self) -> Self {
287 Self {
288 peer_id: self.peer_id.clone(),
289 encrypt_key: self.encrypt_key.clone(),
290 mac_key: self.mac_key.clone(),
291 counter: std::sync::atomic::AtomicU32::new(
292 self.counter.load(std::sync::atomic::Ordering::SeqCst)
293 ),
294 }
295 }
296}
297
298impl EncryptedChannel {
299 /// Create a new encrypted channel from a shared secret
300 ///
301 /// # Arguments
302 ///
303 /// * `peer_id` - Identifier for the peer (for auditing/logging)
304 /// * `shared_secret` - Shared secret from Kyber KEM
305 ///
306 /// # Security
307 ///
308 /// Keys are derived using HKDF-SHA256 with domain separation.
309 pub fn new(peer_id: String, shared_secret: SharedSecret) -> Self {
310 let (encrypt_key, mac_key) = shared_secret.derive_keys();
311
312 Self {
313 peer_id,
314 encrypt_key,
315 mac_key,
316 counter: std::sync::atomic::AtomicU32::new(0),
317 }
318 }
319
320 /// Encrypt a message using ChaCha20-Poly1305
321 ///
322 /// # Arguments
323 ///
324 /// * `plaintext` - Message to encrypt
325 ///
326 /// # Returns
327 ///
328 /// Ciphertext format: `[nonce: 12 bytes][ciphertext][tag: 16 bytes]`
329 ///
330 /// # Errors
331 ///
332 /// Returns `CryptoError` if encryption fails (should never happen).
333 ///
334 /// # Security
335 ///
336 /// - Unique nonce per message (96-bit random + 32-bit counter)
337 /// - Authenticated encryption (modify ciphertext = detection)
338 /// - Quantum resistance: 128-bit security (Grover bound)
339 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
340 use chacha20poly1305::{
341 aead::{Aead, KeyInit},
342 ChaCha20Poly1305, Nonce,
343 };
344
345 // Create cipher instance
346 let key_array: [u8; 32] = self.encrypt_key.as_slice().try_into()
347 .map_err(|_| FederationError::CryptoError("Invalid key size".into()))?;
348 let cipher = ChaCha20Poly1305::new(&key_array.into());
349
350 // Generate unique nonce: [random: 8 bytes][counter: 4 bytes]
351 let mut nonce_bytes = [0u8; 12];
352 nonce_bytes[0..8].copy_from_slice(&rand::random::<[u8; 8]>());
353 let counter = self.counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
354 nonce_bytes[8..12].copy_from_slice(&counter.to_le_bytes());
355 let nonce = Nonce::from_slice(&nonce_bytes);
356
357 // Encrypt with AEAD
358 let ciphertext = cipher.encrypt(nonce, plaintext)
359 .map_err(|e| FederationError::CryptoError(
360 format!("ChaCha20-Poly1305 encryption failed: {}", e)
361 ))?;
362
363 // Prepend nonce to ciphertext (needed for decryption)
364 let mut result = nonce_bytes.to_vec();
365 result.extend_from_slice(&ciphertext);
366
367 Ok(result)
368 }
369
370 /// Decrypt a message using ChaCha20-Poly1305
371 ///
372 /// # Arguments
373 ///
374 /// * `ciphertext` - Encrypted message (format: `[nonce: 12][ciphertext][tag: 16]`)
375 ///
376 /// # Returns
377 ///
378 /// Decrypted plaintext
379 ///
380 /// # Errors
381 ///
382 /// Returns `CryptoError` if:
383 /// - Ciphertext is too short (< 28 bytes)
384 /// - Authentication tag verification fails (tampering detected)
385 /// - Decryption fails
386 ///
387 /// # Security
388 ///
389 /// - **Constant-time**: Timing independent of plaintext content
390 /// - **Tamper-evident**: Any modification causes authentication failure
391 pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
392 use chacha20poly1305::{
393 aead::{Aead, KeyInit},
394 ChaCha20Poly1305, Nonce,
395 };
396
397 // Validate minimum size: nonce(12) + tag(16) = 28 bytes
398 if ciphertext.len() < 28 {
399 return Err(FederationError::CryptoError(
400 format!("Ciphertext too short: {} bytes (minimum 28)", ciphertext.len())
401 ));
402 }
403
404 // Extract nonce and ciphertext
405 let (nonce_bytes, ct) = ciphertext.split_at(12);
406 let nonce = Nonce::from_slice(nonce_bytes);
407
408 // Create cipher instance
409 let key_array: [u8; 32] = self.encrypt_key.as_slice().try_into()
410 .map_err(|_| FederationError::CryptoError("Invalid key size".into()))?;
411 let cipher = ChaCha20Poly1305::new(&key_array.into());
412
413 // Decrypt with AEAD (authentication happens here)
414 let plaintext = cipher.decrypt(nonce, ct)
415 .map_err(|e| FederationError::CryptoError(
416 format!("ChaCha20-Poly1305 decryption failed (tampering?): {}", e)
417 ))?;
418
419 Ok(plaintext)
420 }
421
422 /// Sign a message with HMAC-SHA256
423 ///
424 /// # Arguments
425 ///
426 /// * `message` - Message to authenticate
427 ///
428 /// # Returns
429 ///
430 /// 32-byte HMAC tag
431 ///
432 /// # Security
433 ///
434 /// - PRF security: tag reveals nothing about key
435 /// - Quantum resistance: 128-bit security (Grover)
436 ///
437 /// # Note
438 ///
439 /// If using `encrypt()`, signatures are redundant (Poly1305 provides authentication).
440 /// Use this for non-encrypted authenticated messages.
441 pub fn sign(&self, message: &[u8]) -> Vec<u8> {
442 use hmac::{Hmac, Mac};
443 use sha2::Sha256;
444
445 let mut mac = Hmac::<Sha256>::new_from_slice(&self.mac_key)
446 .expect("HMAC-SHA256 accepts any key size");
447 mac.update(message);
448 mac.finalize().into_bytes().to_vec()
449 }
450
451 /// Verify a message signature using constant-time comparison
452 ///
453 /// # Arguments
454 ///
455 /// * `message` - Original message
456 /// * `signature` - HMAC tag to verify
457 ///
458 /// # Returns
459 ///
460 /// `true` if signature is valid, `false` otherwise
461 ///
462 /// # Security
463 ///
464 /// - **Constant-time**: Execution time independent of signature validity
465 /// - **Timing-attack resistant**: No early termination on mismatch
466 ///
467 /// # Critical Security Property
468 ///
469 /// This function MUST use constant-time comparison to prevent timing side-channels.
470 pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
471 use subtle::ConstantTimeEq;
472
473 let expected = self.sign(message);
474
475 // Constant-time comparison (critical for security)
476 if expected.len() != signature.len() {
477 return false;
478 }
479
480 expected.ct_eq(signature).into()
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
489 fn test_keypair_generation() {
490 let keypair = PostQuantumKeypair::generate();
491 assert_eq!(keypair.public.len(), 1568); // Kyber-1024 public key size
492 }
493
494 #[test]
495 fn test_key_exchange() {
496 let alice = PostQuantumKeypair::generate();
497 let bob = PostQuantumKeypair::generate();
498
499 // Alice encapsulates to Bob
500 let (alice_secret, ciphertext) = PostQuantumKeypair::encapsulate(bob.public_key()).unwrap();
501
502 // Bob decapsulates
503 let bob_secret = bob.decapsulate(&ciphertext).unwrap();
504
505 // Derive keys and verify they match
506 let (alice_enc, alice_mac) = alice_secret.derive_keys();
507 let (bob_enc, bob_mac) = bob_secret.derive_keys();
508
509 assert_eq!(alice_enc, bob_enc, "Encryption keys must match");
510 assert_eq!(alice_mac, bob_mac, "MAC keys must match");
511 }
512
513 #[test]
514 fn test_encrypted_channel() {
515 let keypair = PostQuantumKeypair::generate();
516 let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
517
518 let channel = EncryptedChannel::new("peer1".to_string(), secret);
519
520 let plaintext = b"Hello, post-quantum federation!";
521 let ciphertext = channel.encrypt(plaintext).unwrap();
522
523 // Verify ciphertext is different
524 assert_ne!(&ciphertext[12..], plaintext);
525
526 // Decrypt and verify
527 let decrypted = channel.decrypt(&ciphertext).unwrap();
528 assert_eq!(plaintext, &decrypted[..]);
529 }
530
531 #[test]
532 fn test_message_signing() {
533 let keypair = PostQuantumKeypair::generate();
534 let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
535 let channel = EncryptedChannel::new("peer1".to_string(), secret);
536
537 let message = b"Important authenticated message";
538 let signature = channel.sign(message);
539
540 // Verify valid signature
541 assert!(channel.verify(message, &signature));
542
543 // Verify invalid signature
544 assert!(!channel.verify(b"Different message", &signature));
545
546 // Verify tampered signature
547 let mut bad_sig = signature.clone();
548 bad_sig[0] ^= 1; // Flip one bit
549 assert!(!channel.verify(message, &bad_sig));
550 }
551
552 #[test]
553 fn test_decryption_tamper_detection() {
554 let keypair = PostQuantumKeypair::generate();
555 let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
556 let channel = EncryptedChannel::new("peer1".to_string(), secret);
557
558 let plaintext = b"Secret message";
559 let mut ciphertext = channel.encrypt(plaintext).unwrap();
560
561 // Tamper with ciphertext (flip one bit in encrypted data)
562 ciphertext[20] ^= 1;
563
564 // Decryption should fail due to authentication
565 let result = channel.decrypt(&ciphertext);
566 assert!(result.is_err(), "Tampered ciphertext should fail authentication");
567 }
568
569 #[test]
570 fn test_invalid_public_key_size() {
571 let bad_pk = vec![0u8; 100]; // Wrong size
572 let result = PostQuantumKeypair::encapsulate(&bad_pk);
573 assert!(result.is_err());
574 }
575
576 #[test]
577 fn test_invalid_ciphertext_size() {
578 let keypair = PostQuantumKeypair::generate();
579 let bad_ct = vec![0u8; 100]; // Wrong size
580 let result = keypair.decapsulate(&bad_ct);
581 assert!(result.is_err());
582 }
583
584 #[test]
585 fn test_nonce_uniqueness() {
586 let keypair = PostQuantumKeypair::generate();
587 let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
588 let channel = EncryptedChannel::new("peer1".to_string(), secret);
589
590 let plaintext = b"Test message";
591
592 // Encrypt same message twice
593 let ct1 = channel.encrypt(plaintext).unwrap();
594 let ct2 = channel.encrypt(plaintext).unwrap();
595
596 // Ciphertexts should be different (different nonces)
597 assert_ne!(ct1, ct2, "Nonces must be unique");
598
599 // Both should decrypt correctly
600 assert_eq!(channel.decrypt(&ct1).unwrap(), plaintext);
601 assert_eq!(channel.decrypt(&ct2).unwrap(), plaintext);
602 }
603}