Skip to main content

zap/
crypto.rs

1//! Post-Quantum Cryptography Module for ZAP
2//!
3//! Provides ML-KEM-768 key exchange, ML-DSA-65 signatures, and hybrid X25519+ML-KEM handshake.
4//!
5//! # Security
6//!
7//! This module implements NIST FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) standards
8//! for post-quantum cryptographic protection. The hybrid handshake combines
9//! classical X25519 with ML-KEM-768 for defense-in-depth.
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use zap::crypto::{PQKeyExchange, PQSignature, HybridHandshake};
15//!
16//! // Key exchange
17//! let alice = PQKeyExchange::generate()?;
18//! let (ciphertext, shared_alice) = alice.encapsulate(&bob_pk)?;
19//! let shared_bob = bob.decapsulate(&ciphertext)?;
20//! assert_eq!(shared_alice, shared_bob);
21//!
22//! // Signatures
23//! let signer = PQSignature::generate()?;
24//! let sig = signer.sign(b"message")?;
25//! signer.verify(b"message", &sig)?;
26//!
27//! // Hybrid handshake
28//! let initiator = HybridHandshake::initiate()?;
29//! let (responder, response) = HybridHandshake::respond(&initiator.public_data())?;
30//! let shared = initiator.finalize(&response)?;
31//! ```
32
33use crate::error::{Error, Result};
34
35// Constants for key/ciphertext sizes
36// ML-KEM-768 sizes
37/// ML-KEM-768 public key size in bytes
38pub const MLKEM_PUBLIC_KEY_SIZE: usize = 1184;
39/// ML-KEM-768 ciphertext size in bytes
40pub const MLKEM_CIPHERTEXT_SIZE: usize = 1088;
41/// ML-KEM-768 shared secret size in bytes
42pub const MLKEM_SHARED_SECRET_SIZE: usize = 32;
43
44// ML-DSA-65 (Dilithium3) sizes
45/// ML-DSA-65 public key size in bytes
46pub const MLDSA_PUBLIC_KEY_SIZE: usize = 1952;
47/// ML-DSA-65 signature size in bytes
48pub const MLDSA_SIGNATURE_SIZE: usize = 3309;
49/// ML-DSA-65 secret key size in bytes
50pub const MLDSA_SECRET_KEY_SIZE: usize = 4000;
51
52// X25519 sizes
53/// X25519 public key size in bytes
54pub const X25519_PUBLIC_KEY_SIZE: usize = 32;
55/// Hybrid shared secret size after HKDF
56pub const HYBRID_SHARED_SECRET_SIZE: usize = 32;
57
58#[cfg(feature = "pq")]
59mod pq_impl {
60    use super::*;
61    use hkdf::Hkdf;
62    use pqcrypto_dilithium::dilithium3;
63    use pqcrypto_mlkem::mlkem768;
64    use pqcrypto_traits::kem::{Ciphertext, PublicKey as KemPublicKey, SharedSecret};
65    use pqcrypto_traits::sign::{
66        DetachedSignature as DetachedSignatureTrait, PublicKey as SignPublicKey,
67        SecretKey as SignSecretKey,
68    };
69    use rand::rngs::OsRng;
70    use sha2::Sha256;
71    use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
72    use zeroize::Zeroize;
73
74    /// ML-KEM-768 Key Encapsulation Mechanism
75    ///
76    /// Implements NIST FIPS 203 ML-KEM-768 for post-quantum key exchange.
77    /// Security level: NIST Level 3 (~AES-192 equivalent).
78    pub struct PQKeyExchange {
79        public_key: mlkem768::PublicKey,
80        secret_key: mlkem768::SecretKey,
81    }
82
83    impl PQKeyExchange {
84        /// Generate a new ML-KEM-768 keypair
85        pub fn generate() -> Result<Self> {
86            let (pk, sk) = mlkem768::keypair();
87            Ok(Self {
88                public_key: pk,
89                secret_key: sk,
90            })
91        }
92
93        /// Get the public key bytes
94        pub fn public_key_bytes(&self) -> Vec<u8> {
95            self.public_key.as_bytes().to_vec()
96        }
97
98        /// Create from existing public key bytes (for encapsulation only)
99        pub fn from_public_key(bytes: &[u8]) -> Result<Self> {
100            if bytes.len() != MLKEM_PUBLIC_KEY_SIZE {
101                return Err(Error::Crypto(format!(
102                    "invalid ML-KEM public key size: expected {}, got {}",
103                    MLKEM_PUBLIC_KEY_SIZE,
104                    bytes.len()
105                )));
106            }
107            let pk = mlkem768::PublicKey::from_bytes(bytes)
108                .map_err(|e| Error::Crypto(format!("invalid ML-KEM public key: {e:?}")))?;
109            // Create dummy secret key - this instance can only encapsulate
110            let (_, dummy_sk) = mlkem768::keypair();
111            Ok(Self {
112                public_key: pk,
113                secret_key: dummy_sk,
114            })
115        }
116
117        /// Encapsulate: generate ciphertext and shared secret for a recipient's public key
118        pub fn encapsulate(&self, recipient_pk: &[u8]) -> Result<(Vec<u8>, [u8; 32])> {
119            let pk = mlkem768::PublicKey::from_bytes(recipient_pk)
120                .map_err(|e| Error::Crypto(format!("invalid recipient public key: {e:?}")))?;
121            let (ss, ct) = mlkem768::encapsulate(&pk);
122            let mut shared = [0u8; 32];
123            shared.copy_from_slice(ss.as_bytes());
124            Ok((ct.as_bytes().to_vec(), shared))
125        }
126
127        /// Decapsulate: recover shared secret from ciphertext
128        pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<[u8; 32]> {
129            if ciphertext.len() != MLKEM_CIPHERTEXT_SIZE {
130                return Err(Error::Crypto(format!(
131                    "invalid ML-KEM ciphertext size: expected {}, got {}",
132                    MLKEM_CIPHERTEXT_SIZE,
133                    ciphertext.len()
134                )));
135            }
136            let ct = mlkem768::Ciphertext::from_bytes(ciphertext)
137                .map_err(|e| Error::Crypto(format!("invalid ciphertext: {e:?}")))?;
138            let ss = mlkem768::decapsulate(&ct, &self.secret_key);
139            let mut shared = [0u8; 32];
140            shared.copy_from_slice(ss.as_bytes());
141            Ok(shared)
142        }
143    }
144
145    /// ML-DSA-65 Digital Signature Algorithm
146    ///
147    /// Implements NIST FIPS 204 ML-DSA-65 (Dilithium3) for post-quantum signatures.
148    /// Security level: NIST Level 3 (~AES-192 equivalent).
149    pub struct PQSignature {
150        public_key: dilithium3::PublicKey,
151        secret_key: Option<dilithium3::SecretKey>,
152    }
153
154    impl std::fmt::Debug for PQSignature {
155        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156            f.debug_struct("PQSignature")
157                .field("public_key", &"<public_key>")
158                .field("secret_key", &self.secret_key.as_ref().map(|_| "<secret_key>"))
159                .finish()
160        }
161    }
162
163    impl Clone for PQSignature {
164        fn clone(&self) -> Self {
165            // Clone by re-parsing the bytes
166            let pk_bytes = self.public_key.as_bytes().to_vec();
167            let public_key = dilithium3::PublicKey::from_bytes(&pk_bytes).unwrap();
168            let secret_key = self.secret_key.as_ref().map(|sk| {
169                let sk_bytes = sk.as_bytes().to_vec();
170                dilithium3::SecretKey::from_bytes(&sk_bytes).unwrap()
171            });
172            Self { public_key, secret_key }
173        }
174    }
175
176    impl PQSignature {
177        /// Generate a new ML-DSA-65 keypair
178        pub fn generate() -> Result<Self> {
179            let (pk, sk) = dilithium3::keypair();
180            Ok(Self {
181                public_key: pk,
182                secret_key: Some(sk),
183            })
184        }
185
186        /// Get the public key bytes
187        pub fn public_key_bytes(&self) -> Vec<u8> {
188            self.public_key.as_bytes().to_vec()
189        }
190
191        /// Create from existing public key bytes (for verification only)
192        pub fn from_public_key(bytes: &[u8]) -> Result<Self> {
193            if bytes.len() != MLDSA_PUBLIC_KEY_SIZE {
194                return Err(Error::Crypto(format!(
195                    "invalid ML-DSA public key size: expected {}, got {}",
196                    MLDSA_PUBLIC_KEY_SIZE,
197                    bytes.len()
198                )));
199            }
200            let pk = dilithium3::PublicKey::from_bytes(bytes)
201                .map_err(|e| Error::Crypto(format!("invalid ML-DSA public key: {e:?}")))?;
202            Ok(Self {
203                public_key: pk,
204                secret_key: None,
205            })
206        }
207
208        /// Sign a message
209        pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
210            let sk = self
211                .secret_key
212                .as_ref()
213                .ok_or_else(|| Error::Crypto("no secret key available for signing".into()))?;
214            let sig = dilithium3::detached_sign(message, sk);
215            Ok(sig.as_bytes().to_vec())
216        }
217
218        /// Verify a signature
219        pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<()> {
220            if signature.len() != MLDSA_SIGNATURE_SIZE {
221                return Err(Error::Crypto(format!(
222                    "invalid ML-DSA signature size: expected {}, got {}",
223                    MLDSA_SIGNATURE_SIZE,
224                    signature.len()
225                )));
226            }
227            let sig = dilithium3::DetachedSignature::from_bytes(signature)
228                .map_err(|e| Error::Crypto(format!("invalid signature format: {e:?}")))?;
229            dilithium3::verify_detached_signature(&sig, message, &self.public_key)
230                .map_err(|_| Error::Crypto("signature verification failed".into()))
231        }
232    }
233
234    /// Public data from the initiator for the responder
235    #[derive(Debug, Clone)]
236    pub struct HybridInitiatorData {
237        pub x25519_public_key: [u8; 32],
238        pub mlkem_public_key: Vec<u8>,
239    }
240
241    /// Response data from the responder for the initiator
242    #[derive(Debug, Clone)]
243    pub struct HybridResponderData {
244        pub x25519_public_key: [u8; 32],
245        pub mlkem_ciphertext: Vec<u8>,
246    }
247
248    /// Completed hybrid handshake result
249    #[derive(Clone)]
250    pub struct HybridSharedSecret {
251        secret: [u8; HYBRID_SHARED_SECRET_SIZE],
252    }
253
254    impl HybridSharedSecret {
255        /// Get the shared secret bytes
256        pub fn as_bytes(&self) -> &[u8; HYBRID_SHARED_SECRET_SIZE] {
257            &self.secret
258        }
259
260        /// Consume and return the shared secret
261        pub fn into_bytes(self) -> [u8; HYBRID_SHARED_SECRET_SIZE] {
262            self.secret
263        }
264    }
265
266    impl Drop for HybridSharedSecret {
267        fn drop(&mut self) {
268            self.secret.zeroize();
269        }
270    }
271
272    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
273    enum HandshakeRole {
274        Initiator,
275        Responder,
276    }
277
278    /// Hybrid X25519 + ML-KEM-768 Handshake
279    ///
280    /// Combines classical elliptic curve Diffie-Hellman (X25519) with post-quantum
281    /// ML-KEM-768 for defense-in-depth. Even if one algorithm is broken, the other
282    /// provides protection.
283    ///
284    /// The final shared secret is derived using HKDF-SHA256 over both shared secrets.
285    pub struct HybridHandshake {
286        x25519_secret: Option<EphemeralSecret>,
287        x25519_public: X25519PublicKey,
288        mlkem: PQKeyExchange,
289        role: HandshakeRole,
290    }
291
292    impl HybridHandshake {
293        /// Initiate a hybrid handshake (client side)
294        pub fn initiate() -> Result<Self> {
295            let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
296            let x25519_public = X25519PublicKey::from(&x25519_secret);
297            let mlkem = PQKeyExchange::generate()?;
298
299            Ok(Self {
300                x25519_secret: Some(x25519_secret),
301                x25519_public,
302                mlkem,
303                role: HandshakeRole::Initiator,
304            })
305        }
306
307        /// Get the public data to send to the responder
308        pub fn public_data(&self) -> HybridInitiatorData {
309            HybridInitiatorData {
310                x25519_public_key: self.x25519_public.to_bytes(),
311                mlkem_public_key: self.mlkem.public_key_bytes(),
312            }
313        }
314
315        /// Respond to a hybrid handshake (server side)
316        pub fn respond(
317            initiator_data: &HybridInitiatorData,
318        ) -> Result<(Self, HybridResponderData)> {
319            // Validate input sizes
320            if initiator_data.mlkem_public_key.len() != MLKEM_PUBLIC_KEY_SIZE {
321                return Err(Error::Crypto(format!(
322                    "invalid initiator ML-KEM public key size: expected {}, got {}",
323                    MLKEM_PUBLIC_KEY_SIZE,
324                    initiator_data.mlkem_public_key.len()
325                )));
326            }
327
328            // Generate responder's X25519 keypair
329            let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
330            let x25519_public = X25519PublicKey::from(&x25519_secret);
331
332            // Generate ML-KEM keypair and encapsulate to initiator
333            let mlkem = PQKeyExchange::generate()?;
334            let (mlkem_ciphertext, _) = mlkem.encapsulate(&initiator_data.mlkem_public_key)?;
335
336            let response = HybridResponderData {
337                x25519_public_key: x25519_public.to_bytes(),
338                mlkem_ciphertext,
339            };
340
341            let handshake = Self {
342                x25519_secret: Some(x25519_secret),
343                x25519_public,
344                mlkem,
345                role: HandshakeRole::Responder,
346            };
347
348            Ok((handshake, response))
349        }
350
351        /// Finalize the handshake and derive the shared secret (initiator side)
352        pub fn finalize(mut self, responder_data: &HybridResponderData) -> Result<HybridSharedSecret> {
353            if self.role != HandshakeRole::Initiator {
354                return Err(Error::Crypto(
355                    "finalize() can only be called by initiator".into(),
356                ));
357            }
358
359            // X25519 key exchange
360            let x25519_secret = self
361                .x25519_secret
362                .take()
363                .ok_or_else(|| Error::Crypto("X25519 secret already consumed".into()))?;
364            let peer_x25519_public = X25519PublicKey::from(responder_data.x25519_public_key);
365            let x25519_shared = x25519_secret.diffie_hellman(&peer_x25519_public);
366
367            // ML-KEM decapsulation
368            let mlkem_shared = self.mlkem.decapsulate(&responder_data.mlkem_ciphertext)?;
369
370            // Combine shared secrets with HKDF
371            Self::derive_hybrid_secret(x25519_shared.as_bytes(), &mlkem_shared)
372        }
373
374        /// Complete the handshake and derive the shared secret (responder side)
375        pub fn complete(
376            mut self,
377            initiator_data: &HybridInitiatorData,
378            mlkem_shared: &[u8; 32],
379        ) -> Result<HybridSharedSecret> {
380            if self.role != HandshakeRole::Responder {
381                return Err(Error::Crypto(
382                    "complete() can only be called by responder".into(),
383                ));
384            }
385
386            // X25519 key exchange
387            let x25519_secret = self
388                .x25519_secret
389                .take()
390                .ok_or_else(|| Error::Crypto("X25519 secret already consumed".into()))?;
391            let peer_x25519_public = X25519PublicKey::from(initiator_data.x25519_public_key);
392            let x25519_shared = x25519_secret.diffie_hellman(&peer_x25519_public);
393
394            // Combine shared secrets with HKDF
395            Self::derive_hybrid_secret(x25519_shared.as_bytes(), mlkem_shared)
396        }
397
398        /// Derive hybrid shared secret using HKDF-SHA256
399        fn derive_hybrid_secret(
400            x25519_shared: &[u8],
401            mlkem_shared: &[u8; 32],
402        ) -> Result<HybridSharedSecret> {
403            // Concatenate both shared secrets
404            let mut ikm = Vec::with_capacity(x25519_shared.len() + mlkem_shared.len());
405            ikm.extend_from_slice(x25519_shared);
406            ikm.extend_from_slice(mlkem_shared);
407
408            // HKDF extract and expand
409            let hkdf = Hkdf::<Sha256>::new(Some(b"ZAP-HYBRID-HANDSHAKE-v1"), &ikm);
410            let mut secret = [0u8; HYBRID_SHARED_SECRET_SIZE];
411            hkdf.expand(b"shared-secret", &mut secret)
412                .map_err(|_| Error::Crypto("HKDF expansion failed".into()))?;
413
414            // Zeroize intermediate material
415            ikm.zeroize();
416
417            Ok(HybridSharedSecret { secret })
418        }
419    }
420
421    /// Perform a complete hybrid handshake between two parties
422    ///
423    /// This is a convenience function for testing and simple use cases.
424    pub fn hybrid_handshake() -> Result<(
425        [u8; HYBRID_SHARED_SECRET_SIZE],
426        [u8; HYBRID_SHARED_SECRET_SIZE],
427    )> {
428        // Initiator starts
429        let initiator = HybridHandshake::initiate()?;
430        let init_data = initiator.public_data();
431
432        // Responder receives and responds
433        let (responder, resp_data) = HybridHandshake::respond(&init_data)?;
434
435        // Responder also needs to encapsulate to get their copy of the ML-KEM shared secret
436        let mlkem_for_responder = PQKeyExchange::generate()?;
437        let (_, mlkem_shared_responder) =
438            mlkem_for_responder.encapsulate(&init_data.mlkem_public_key)?;
439
440        // Initiator finalizes
441        let initiator_secret = initiator.finalize(&resp_data)?;
442
443        // Responder completes
444        let responder_secret = responder.complete(&init_data, &mlkem_shared_responder)?;
445
446        Ok((initiator_secret.into_bytes(), responder_secret.into_bytes()))
447    }
448
449    #[cfg(test)]
450    mod tests {
451        use super::*;
452
453        #[test]
454        fn test_mlkem_key_exchange() {
455            let alice = PQKeyExchange::generate().unwrap();
456            let bob = PQKeyExchange::generate().unwrap();
457
458            // Alice encapsulates to Bob's public key
459            let (ciphertext, alice_shared) = alice.encapsulate(&bob.public_key_bytes()).unwrap();
460
461            // Bob decapsulates
462            let bob_shared = bob.decapsulate(&ciphertext).unwrap();
463
464            assert_eq!(alice_shared, bob_shared);
465        }
466
467        #[test]
468        fn test_mlkem_invalid_public_key() {
469            let alice = PQKeyExchange::generate().unwrap();
470            let bad_pk = vec![0u8; 100]; // Wrong size
471            assert!(alice.encapsulate(&bad_pk).is_err());
472        }
473
474        #[test]
475        fn test_mldsa_signature() {
476            let signer = PQSignature::generate().unwrap();
477
478            let message = b"The quick brown fox jumps over the lazy dog";
479            let signature = signer.sign(message).unwrap();
480
481            // Verify with same key
482            signer.verify(message, &signature).unwrap();
483
484            // Verify with public key only
485            let verifier = PQSignature::from_public_key(&signer.public_key_bytes()).unwrap();
486            verifier.verify(message, &signature).unwrap();
487        }
488
489        #[test]
490        fn test_mldsa_invalid_signature() {
491            let signer = PQSignature::generate().unwrap();
492            let message = b"Hello, World!";
493            let signature = signer.sign(message).unwrap();
494
495            // Wrong message
496            assert!(signer.verify(b"Wrong message", &signature).is_err());
497
498            // Corrupted signature
499            let mut bad_sig = signature.clone();
500            bad_sig[0] ^= 0xFF;
501            assert!(signer.verify(message, &bad_sig).is_err());
502        }
503
504        #[test]
505        fn test_mldsa_verify_only() {
506            let verifier = PQSignature::from_public_key(
507                &PQSignature::generate().unwrap().public_key_bytes(),
508            )
509            .unwrap();
510            assert!(verifier.sign(b"test").is_err());
511        }
512
513        #[test]
514        fn test_hybrid_handshake_basic() {
515            // Initiator starts
516            let initiator = HybridHandshake::initiate().unwrap();
517            let init_data = initiator.public_data();
518
519            // Responder receives init_data and creates response
520            let responder_mlkem = PQKeyExchange::generate().unwrap();
521            let (mlkem_ct, _mlkem_shared_responder) = responder_mlkem
522                .encapsulate(&init_data.mlkem_public_key)
523                .unwrap();
524
525            let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
526            let x25519_public = X25519PublicKey::from(&x25519_secret);
527
528            let resp_data = HybridResponderData {
529                x25519_public_key: x25519_public.to_bytes(),
530                mlkem_ciphertext: mlkem_ct,
531            };
532
533            // Initiator finalizes
534            let _initiator_secret = initiator.finalize(&resp_data).unwrap();
535
536            // Note: In real use, both parties derive the same secret
537            // This test just verifies the API works
538        }
539
540        #[test]
541        fn test_hybrid_handshake_sizes() {
542            let initiator = HybridHandshake::initiate().unwrap();
543            let init_data = initiator.public_data();
544
545            assert_eq!(init_data.x25519_public_key.len(), X25519_PUBLIC_KEY_SIZE);
546            assert_eq!(init_data.mlkem_public_key.len(), MLKEM_PUBLIC_KEY_SIZE);
547        }
548    }
549}
550
551// Re-export PQ types when feature is enabled
552#[cfg(feature = "pq")]
553pub use pq_impl::{
554    hybrid_handshake, HybridHandshake, HybridInitiatorData, HybridResponderData,
555    HybridSharedSecret, PQKeyExchange, PQSignature,
556};
557
558// Feature-gated fallbacks when pq feature is not enabled
559// These types exist for API compatibility but return errors on use
560#[cfg(not(feature = "pq"))]
561pub struct PQKeyExchange;
562
563#[cfg(not(feature = "pq"))]
564impl PQKeyExchange {
565    pub fn generate() -> Result<Self> {
566        Err(Error::Crypto("PQ crypto requires 'pq' feature".into()))
567    }
568}
569
570#[cfg(not(feature = "pq"))]
571pub struct PQSignature;
572
573#[cfg(not(feature = "pq"))]
574impl PQSignature {
575    pub fn generate() -> Result<Self> {
576        Err(Error::Crypto("PQ crypto requires 'pq' feature".into()))
577    }
578}
579
580#[cfg(not(feature = "pq"))]
581pub struct HybridHandshake;
582
583#[cfg(not(feature = "pq"))]
584impl HybridHandshake {
585    pub fn initiate() -> Result<Self> {
586        Err(Error::Crypto("PQ crypto requires 'pq' feature".into()))
587    }
588}