1use crate::error::{Error, Result};
34
35pub const MLKEM_PUBLIC_KEY_SIZE: usize = 1184;
39pub const MLKEM_CIPHERTEXT_SIZE: usize = 1088;
41pub const MLKEM_SHARED_SECRET_SIZE: usize = 32;
43
44pub const MLDSA_PUBLIC_KEY_SIZE: usize = 1952;
47pub const MLDSA_SIGNATURE_SIZE: usize = 3309;
49pub const MLDSA_SECRET_KEY_SIZE: usize = 4000;
51
52pub const X25519_PUBLIC_KEY_SIZE: usize = 32;
55pub 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 pub struct PQKeyExchange {
79 public_key: mlkem768::PublicKey,
80 secret_key: mlkem768::SecretKey,
81 }
82
83 impl PQKeyExchange {
84 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 pub fn public_key_bytes(&self) -> Vec<u8> {
95 self.public_key.as_bytes().to_vec()
96 }
97
98 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 let (_, dummy_sk) = mlkem768::keypair();
111 Ok(Self {
112 public_key: pk,
113 secret_key: dummy_sk,
114 })
115 }
116
117 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 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 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 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 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 pub fn public_key_bytes(&self) -> Vec<u8> {
188 self.public_key.as_bytes().to_vec()
189 }
190
191 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 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 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 #[derive(Debug, Clone)]
236 pub struct HybridInitiatorData {
237 pub x25519_public_key: [u8; 32],
238 pub mlkem_public_key: Vec<u8>,
239 }
240
241 #[derive(Debug, Clone)]
243 pub struct HybridResponderData {
244 pub x25519_public_key: [u8; 32],
245 pub mlkem_ciphertext: Vec<u8>,
246 }
247
248 #[derive(Clone)]
250 pub struct HybridSharedSecret {
251 secret: [u8; HYBRID_SHARED_SECRET_SIZE],
252 }
253
254 impl HybridSharedSecret {
255 pub fn as_bytes(&self) -> &[u8; HYBRID_SHARED_SECRET_SIZE] {
257 &self.secret
258 }
259
260 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 pub struct HybridHandshake {
286 x25519_secret: Option<EphemeralSecret>,
287 x25519_public: X25519PublicKey,
288 mlkem: PQKeyExchange,
289 role: HandshakeRole,
290 }
291
292 impl HybridHandshake {
293 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 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 pub fn respond(
317 initiator_data: &HybridInitiatorData,
318 ) -> Result<(Self, HybridResponderData)> {
319 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 let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
330 let x25519_public = X25519PublicKey::from(&x25519_secret);
331
332 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 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 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 let mlkem_shared = self.mlkem.decapsulate(&responder_data.mlkem_ciphertext)?;
369
370 Self::derive_hybrid_secret(x25519_shared.as_bytes(), &mlkem_shared)
372 }
373
374 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 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 Self::derive_hybrid_secret(x25519_shared.as_bytes(), mlkem_shared)
396 }
397
398 fn derive_hybrid_secret(
400 x25519_shared: &[u8],
401 mlkem_shared: &[u8; 32],
402 ) -> Result<HybridSharedSecret> {
403 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 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 ikm.zeroize();
416
417 Ok(HybridSharedSecret { secret })
418 }
419 }
420
421 pub fn hybrid_handshake() -> Result<(
425 [u8; HYBRID_SHARED_SECRET_SIZE],
426 [u8; HYBRID_SHARED_SECRET_SIZE],
427 )> {
428 let initiator = HybridHandshake::initiate()?;
430 let init_data = initiator.public_data();
431
432 let (responder, resp_data) = HybridHandshake::respond(&init_data)?;
434
435 let mlkem_for_responder = PQKeyExchange::generate()?;
437 let (_, mlkem_shared_responder) =
438 mlkem_for_responder.encapsulate(&init_data.mlkem_public_key)?;
439
440 let initiator_secret = initiator.finalize(&resp_data)?;
442
443 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 let (ciphertext, alice_shared) = alice.encapsulate(&bob.public_key_bytes()).unwrap();
460
461 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]; 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 signer.verify(message, &signature).unwrap();
483
484 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 assert!(signer.verify(b"Wrong message", &signature).is_err());
497
498 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 let initiator = HybridHandshake::initiate().unwrap();
517 let init_data = initiator.public_data();
518
519 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 let _initiator_secret = initiator.finalize(&resp_data).unwrap();
535
536 }
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#[cfg(feature = "pq")]
553pub use pq_impl::{
554 hybrid_handshake, HybridHandshake, HybridInitiatorData, HybridResponderData,
555 HybridSharedSecret, PQKeyExchange, PQSignature,
556};
557
558#[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}