1use ml_kem::{Decapsulate, MlKem512, MlKem768, MlKem1024};
86use ml_kem::{DecapsulationKey, EncapsulationKey, KeyExport};
87use sha3::Shake256;
88use sha3::digest::{ExtendableOutput, Update, XofReader};
89use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519StaticSecret};
90use zeroize::Zeroize;
91
92use crypto_secretbox::aead::Aead;
93use crypto_secretbox::aead::generic_array::GenericArray;
94use crypto_secretbox::{KeyInit, XSalsa20Poly1305};
95
96use crate::CryptoError;
97use crate::b64;
98use crate::ecc;
99use crate::suite::{
100 self, GCM_NONCE_LEN, GCM_TAG_LEN, SEAL_CONTEXT_V1, Suite, TAG_KEM_MATCHED_CAT3,
101 TAG_KEM_MATCHED_CAT5, TAG_KEM_PURE_CNSA2,
102};
103
104const VERSION_HYBRID_512: u8 = 0x01;
108const VERSION_HYBRID_768: u8 = 0x02;
110const VERSION_HYBRID_1024: u8 = 0x03;
112const NONCE_LEN: usize = 24;
114const X25519_LEN: usize = 32;
116const SEED_LEN: usize = 32;
118const MAC_LEN: usize = 16;
120const LABEL: &[u8] = b"\\.//^\\";
122
123const MLKEM512_EK_LEN: usize = 800;
126const MLKEM512_CT_LEN: usize = 768;
128const MLKEM512_SEED_LEN: usize = 64;
130const EXPANDED_SEED_512_LEN: usize = 96;
132const COMBINED_PK_512_LEN: usize = MLKEM512_EK_LEN + X25519_LEN;
134const COMBINED_CT_512_LEN: usize = MLKEM512_CT_LEN + X25519_LEN;
136const MIN_HYBRID_512_LEN: usize = 1 + COMBINED_CT_512_LEN + NONCE_LEN + MAC_LEN;
142
143const MLKEM768_EK_LEN: usize = 1184;
146const MLKEM768_CT_LEN: usize = 1088;
148const MLKEM768_SEED_LEN: usize = 64;
150const EXPANDED_SEED_768_LEN: usize = 96;
152const COMBINED_PK_768_LEN: usize = MLKEM768_EK_LEN + X25519_LEN;
154const COMBINED_CT_768_LEN: usize = MLKEM768_CT_LEN + X25519_LEN;
156const MIN_HYBRID_768_LEN: usize = 1 + COMBINED_CT_768_LEN + NONCE_LEN + MAC_LEN;
159
160const MLKEM1024_EK_LEN: usize = 1568;
163const MLKEM1024_CT_LEN: usize = 1568;
165const MLKEM1024_SEED_LEN: usize = 64;
167const EXPANDED_SEED_1024_LEN: usize = 96;
169const COMBINED_PK_1024_LEN: usize = MLKEM1024_EK_LEN + X25519_LEN;
171const COMBINED_CT_1024_LEN: usize = MLKEM1024_CT_LEN + X25519_LEN;
173const MIN_HYBRID_1024_LEN: usize = 1 + COMBINED_CT_1024_LEN + NONCE_LEN + MAC_LEN;
176
177const MIN_PURE_CNSA2_LEN: usize = 1 + MLKEM1024_CT_LEN + GCM_NONCE_LEN + GCM_TAG_LEN;
188const MIN_MATCHED_CAT3_LEN: usize =
191 1 + MLKEM768_CT_LEN + ecc::X448_LEN + GCM_NONCE_LEN + GCM_TAG_LEN;
192const MIN_MATCHED_CAT5_LEN: usize =
195 1 + MLKEM1024_CT_LEN + ecc::P521_PK_LEN + GCM_NONCE_LEN + GCM_TAG_LEN;
196
197const PURE_CNSA2_PK_LEN: usize = MLKEM1024_EK_LEN;
199const MATCHED_CAT3_PK_LEN: usize = MLKEM768_EK_LEN + ecc::X448_LEN;
201const MATCHED_CAT5_PK_LEN: usize = MLKEM1024_EK_LEN + ecc::P521_PK_LEN;
203
204const EXPANDED_SEED_MATCHED_CAT3_LEN: usize = MLKEM768_SEED_LEN + ecc::X448_LEN;
206const EXPANDED_SEED_MATCHED_CAT5_LEN: usize = MLKEM1024_SEED_LEN + ecc::P521_SK_LEN;
208
209#[derive(Debug, Clone)]
213pub struct HybridKeyPair {
214 pub public_key: String,
216 pub secret_key: String,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub enum SecurityLevel {
223 Cat1,
225 #[default]
227 Cat3,
228 Cat5,
230}
231
232#[inline]
236fn random_bytes(buf: &mut [u8]) {
237 getrandom::getrandom(buf).expect("OS CSPRNG unavailable");
238}
239
240fn expand_seed(seed: &[u8; SEED_LEN], output_len: usize) -> Vec<u8> {
242 let mut hasher = Shake256::default();
243 hasher.update(seed);
244 let mut reader = hasher.finalize_xof();
245 let mut out = vec![0u8; output_len];
246 reader.read(&mut out);
247 out
248}
249
250fn combine(
252 ss_mlkem: &[u8],
253 ss_x25519: &[u8],
254 ct_x25519: &[u8; X25519_LEN],
255 pk_x25519: &[u8; X25519_LEN],
256) -> [u8; 32] {
257 use sha3::Digest;
258 let mut hasher = sha3::Sha3_256::new();
259 Digest::update(&mut hasher, ss_mlkem);
260 Digest::update(&mut hasher, ss_x25519);
261 Digest::update(&mut hasher, ct_x25519);
262 Digest::update(&mut hasher, pk_x25519);
263 Digest::update(&mut hasher, LABEL);
264 hasher.finalize().into()
265}
266
267fn secretbox_encrypt(
269 shared_secret: &[u8; 32],
270 plaintext: &[u8],
271) -> Result<(Vec<u8>, [u8; NONCE_LEN]), CryptoError> {
272 let cipher = XSalsa20Poly1305::new(GenericArray::from_slice(shared_secret));
273 let mut nonce_buf = [0u8; NONCE_LEN];
274 random_bytes(&mut nonce_buf);
275 let nonce = GenericArray::from_slice(&nonce_buf);
276 let ct = cipher
277 .encrypt(nonce, plaintext)
278 .map_err(|_| CryptoError::Hybrid("secretbox encrypt failed".into()))?;
279 Ok((ct, nonce_buf))
280}
281
282fn secretbox_decrypt(
284 shared_secret: &[u8; 32],
285 nonce: &[u8],
286 ciphertext: &[u8],
287) -> Result<Vec<u8>, CryptoError> {
288 let cipher = XSalsa20Poly1305::new(GenericArray::from_slice(shared_secret));
289 let nonce = GenericArray::from_slice(nonce);
290 cipher
291 .decrypt(nonce, ciphertext)
292 .map_err(|_| CryptoError::Decryption)
293}
294
295pub fn generate_hybrid_keypair_512() -> HybridKeyPair {
299 generate_hybrid_keypair_with_level(SecurityLevel::Cat1)
300}
301
302pub fn hybrid_seal_512(plaintext: &[u8], combined_pk_b64: &str) -> Result<String, CryptoError> {
306 hybrid_seal_with_level(plaintext, combined_pk_b64, SecurityLevel::Cat1)
307}
308
309pub fn generate_hybrid_keypair() -> HybridKeyPair {
313 generate_hybrid_keypair_with_level(SecurityLevel::Cat3)
314}
315
316pub fn hybrid_seal(plaintext: &[u8], combined_pk_b64: &str) -> Result<String, CryptoError> {
320 hybrid_seal_with_level(plaintext, combined_pk_b64, SecurityLevel::Cat3)
321}
322
323pub fn hybrid_open(ct_b64: &str, seed_b64: &str) -> Result<Vec<u8>, CryptoError> {
335 hybrid_open_with_context(ct_b64, seed_b64, SEAL_CONTEXT_V1)
336}
337
338pub fn hybrid_open_with_context(
342 ct_b64: &str,
343 seed_b64: &str,
344 context_label: &str,
345) -> Result<Vec<u8>, CryptoError> {
346 let combined = b64::decode(ct_b64)?;
347 match combined.first() {
348 Some(&VERSION_HYBRID_512) => hybrid_open_512(&combined, seed_b64),
349 Some(&VERSION_HYBRID_768) => hybrid_open_768(&combined, seed_b64),
350 Some(&VERSION_HYBRID_1024) => hybrid_open_1024(&combined, seed_b64),
351 Some(&TAG_KEM_PURE_CNSA2) => open_pure_cnsa2(&combined, seed_b64, context_label),
352 Some(&TAG_KEM_MATCHED_CAT3) => open_matched_cat3(&combined, seed_b64, context_label),
353 Some(&TAG_KEM_MATCHED_CAT5) => open_matched_cat5(&combined, seed_b64, context_label),
354 _ => Err(CryptoError::Hybrid(
355 "not a hybrid ciphertext (bad version tag)".into(),
356 )),
357 }
358}
359
360pub fn is_hybrid_ciphertext(ct_b64: &str) -> bool {
373 let Ok(bytes) = b64::decode(ct_b64) else {
374 return false;
375 };
376 match bytes.first() {
377 Some(&VERSION_HYBRID_512) => bytes.len() >= MIN_HYBRID_512_LEN,
378 Some(&VERSION_HYBRID_768) => bytes.len() >= MIN_HYBRID_768_LEN,
379 Some(&VERSION_HYBRID_1024) => bytes.len() >= MIN_HYBRID_1024_LEN,
380 Some(&TAG_KEM_PURE_CNSA2) => bytes.len() >= MIN_PURE_CNSA2_LEN,
381 Some(&TAG_KEM_MATCHED_CAT3) => bytes.len() >= MIN_MATCHED_CAT3_LEN,
382 Some(&TAG_KEM_MATCHED_CAT5) => bytes.len() >= MIN_MATCHED_CAT5_LEN,
383 _ => false,
384 }
385}
386
387pub fn generate_hybrid_keypair_1024() -> HybridKeyPair {
391 generate_hybrid_keypair_with_level(SecurityLevel::Cat5)
392}
393
394pub fn hybrid_seal_1024(plaintext: &[u8], combined_pk_b64: &str) -> Result<String, CryptoError> {
398 hybrid_seal_with_level(plaintext, combined_pk_b64, SecurityLevel::Cat5)
399}
400
401pub fn generate_hybrid_keypair_with_level(level: SecurityLevel) -> HybridKeyPair {
405 let mut seed = [0u8; SEED_LEN];
406 random_bytes(&mut seed);
407
408 let expanded_len = match level {
409 SecurityLevel::Cat1 => EXPANDED_SEED_512_LEN,
410 SecurityLevel::Cat3 => EXPANDED_SEED_768_LEN,
411 SecurityLevel::Cat5 => EXPANDED_SEED_1024_LEN,
412 };
413 let mlkem_seed_len = match level {
414 SecurityLevel::Cat1 => MLKEM512_SEED_LEN,
415 SecurityLevel::Cat3 => MLKEM768_SEED_LEN,
416 SecurityLevel::Cat5 => MLKEM1024_SEED_LEN,
417 };
418
419 let mut expanded = expand_seed(&seed, expanded_len);
420 let x25519_sk_bytes: [u8; X25519_LEN] = expanded[mlkem_seed_len..].try_into().unwrap();
421
422 let x25519_sk = X25519StaticSecret::from(x25519_sk_bytes);
424 let x25519_pk = X25519PublicKey::from(&x25519_sk);
425
426 let combined_pk = match level {
427 SecurityLevel::Cat1 => {
428 let mlkem_seed: [u8; MLKEM512_SEED_LEN] =
429 expanded[..MLKEM512_SEED_LEN].try_into().unwrap();
430 let dk = DecapsulationKey::<MlKem512>::from_seed(mlkem_seed.into());
431 let ek = dk.encapsulation_key();
432 let ek_bytes = ek.to_bytes();
433 let mut pk = Vec::with_capacity(COMBINED_PK_512_LEN);
434 pk.extend_from_slice(&ek_bytes);
435 pk.extend_from_slice(x25519_pk.as_bytes());
436 pk
437 }
438 SecurityLevel::Cat3 => {
439 let mlkem_seed: [u8; MLKEM768_SEED_LEN] =
440 expanded[..MLKEM768_SEED_LEN].try_into().unwrap();
441 let dk = DecapsulationKey::<MlKem768>::from_seed(mlkem_seed.into());
442 let ek = dk.encapsulation_key();
443 let ek_bytes = ek.to_bytes();
444 let mut pk = Vec::with_capacity(COMBINED_PK_768_LEN);
445 pk.extend_from_slice(&ek_bytes);
446 pk.extend_from_slice(x25519_pk.as_bytes());
447 pk
448 }
449 SecurityLevel::Cat5 => {
450 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] =
451 expanded[..MLKEM1024_SEED_LEN].try_into().unwrap();
452 let dk = DecapsulationKey::<MlKem1024>::from_seed(mlkem_seed.into());
453 let ek = dk.encapsulation_key();
454 let ek_bytes = ek.to_bytes();
455 let mut pk = Vec::with_capacity(COMBINED_PK_1024_LEN);
456 pk.extend_from_slice(&ek_bytes);
457 pk.extend_from_slice(x25519_pk.as_bytes());
458 pk
459 }
460 };
461
462 let pair = HybridKeyPair {
463 public_key: b64::encode(&combined_pk),
464 secret_key: b64::encode(&seed),
465 };
466
467 seed.zeroize();
468 expanded.zeroize();
469 pair
470}
471
472pub fn hybrid_seal_with_level(
474 plaintext: &[u8],
475 combined_pk_b64: &str,
476 level: SecurityLevel,
477) -> Result<String, CryptoError> {
478 let pk_bytes = b64::decode(combined_pk_b64)?;
479
480 let (expected_pk_len, mlkem_ek_len, version_tag) = match level {
481 SecurityLevel::Cat1 => (COMBINED_PK_512_LEN, MLKEM512_EK_LEN, VERSION_HYBRID_512),
482 SecurityLevel::Cat3 => (COMBINED_PK_768_LEN, MLKEM768_EK_LEN, VERSION_HYBRID_768),
483 SecurityLevel::Cat5 => (COMBINED_PK_1024_LEN, MLKEM1024_EK_LEN, VERSION_HYBRID_1024),
484 };
485
486 if pk_bytes.len() != expected_pk_len {
487 return Err(CryptoError::InvalidLength {
488 expected: expected_pk_len,
489 got: pk_bytes.len(),
490 });
491 }
492
493 let mlkem_ek_bytes = &pk_bytes[..mlkem_ek_len];
495 let x25519_pk_bytes: [u8; X25519_LEN] = pk_bytes[mlkem_ek_len..].try_into().unwrap();
496
497 let mut mlkem_coins = [0u8; 32];
499 random_bytes(&mut mlkem_coins);
500
501 let (mlkem_ct_bytes, ss_mlkem_bytes) = match level {
502 SecurityLevel::Cat1 => {
503 let ek = EncapsulationKey::<MlKem512>::new(
504 mlkem_ek_bytes
505 .try_into()
506 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-512 ek".into()))?,
507 )
508 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-512 encapsulation key".into()))?;
509 let (ct, ss) = ek.encapsulate_deterministic(&mlkem_coins.into());
510 (ct.as_slice().to_vec(), ss.as_slice().to_vec())
511 }
512 SecurityLevel::Cat3 => {
513 let ek = EncapsulationKey::<MlKem768>::new(
514 mlkem_ek_bytes
515 .try_into()
516 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 ek".into()))?,
517 )
518 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 encapsulation key".into()))?;
519 let (ct, ss) = ek.encapsulate_deterministic(&mlkem_coins.into());
520 (ct.as_slice().to_vec(), ss.as_slice().to_vec())
521 }
522 SecurityLevel::Cat5 => {
523 let ek = EncapsulationKey::<MlKem1024>::new(
524 mlkem_ek_bytes
525 .try_into()
526 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 ek".into()))?,
527 )
528 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 encapsulation key".into()))?;
529 let (ct, ss) = ek.encapsulate_deterministic(&mlkem_coins.into());
530 (ct.as_slice().to_vec(), ss.as_slice().to_vec())
531 }
532 };
533 mlkem_coins.zeroize();
534
535 let mut x25519_eph_bytes = [0u8; X25519_LEN];
537 random_bytes(&mut x25519_eph_bytes);
538 let x25519_eph_sk = X25519StaticSecret::from(x25519_eph_bytes);
539 let x25519_eph_pk = X25519PublicKey::from(&x25519_eph_sk);
540 let x25519_recipient_pk = X25519PublicKey::from(x25519_pk_bytes);
541 let ss_x25519 = x25519_eph_sk.diffie_hellman(&x25519_recipient_pk);
542 x25519_eph_bytes.zeroize();
543
544 let ct_x25519: [u8; X25519_LEN] = *x25519_eph_pk.as_bytes();
546 let mut shared_secret = combine(
547 &ss_mlkem_bytes,
548 ss_x25519.as_bytes(),
549 &ct_x25519,
550 &x25519_pk_bytes,
551 );
552
553 let (secretbox_ct, nonce_buf) = secretbox_encrypt(&shared_secret, plaintext)?;
555 shared_secret.zeroize();
556
557 let combined_ct_len = mlkem_ct_bytes.len() + X25519_LEN;
559 let mut out = Vec::with_capacity(1 + combined_ct_len + NONCE_LEN + secretbox_ct.len());
560 out.push(version_tag);
561 out.extend_from_slice(&mlkem_ct_bytes);
562 out.extend_from_slice(&ct_x25519);
563 out.extend_from_slice(&nonce_buf);
564 out.extend_from_slice(&secretbox_ct);
565
566 Ok(b64::encode(&out))
567}
568
569fn hybrid_open_512(combined: &[u8], seed_b64: &str) -> Result<Vec<u8>, CryptoError> {
572 let seed_bytes = b64::decode(seed_b64)?;
573 if seed_bytes.len() != SEED_LEN {
574 return Err(CryptoError::InvalidLength {
575 expected: SEED_LEN,
576 got: seed_bytes.len(),
577 });
578 }
579 if combined.len() < MIN_HYBRID_512_LEN {
580 return Err(CryptoError::TooShort);
581 }
582
583 let seed: [u8; SEED_LEN] = seed_bytes.try_into().unwrap();
584 let mut expanded = expand_seed(&seed, EXPANDED_SEED_512_LEN);
585 let mlkem_seed: [u8; MLKEM512_SEED_LEN] = expanded[..MLKEM512_SEED_LEN].try_into().unwrap();
586 let x25519_sk_bytes: [u8; X25519_LEN] = expanded[MLKEM512_SEED_LEN..].try_into().unwrap();
587 expanded.zeroize();
588
589 let mlkem_ct = &combined[1..1 + MLKEM512_CT_LEN];
591 let x25519_eph_pk_bytes: [u8; X25519_LEN] = combined
592 [1 + MLKEM512_CT_LEN..1 + COMBINED_CT_512_LEN]
593 .try_into()
594 .unwrap();
595 let nonce_slice = &combined[1 + COMBINED_CT_512_LEN..1 + COMBINED_CT_512_LEN + NONCE_LEN];
596 let encrypted = &combined[1 + COMBINED_CT_512_LEN + NONCE_LEN..];
597
598 let dk = DecapsulationKey::<MlKem512>::from_seed(mlkem_seed.into());
600 let kem_ct = mlkem_ct
601 .try_into()
602 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-512 ciphertext".into()))?;
603 let ss_mlkem = dk.decapsulate(kem_ct);
604
605 let x25519_sk = X25519StaticSecret::from(x25519_sk_bytes);
607 let x25519_eph_pk = X25519PublicKey::from(x25519_eph_pk_bytes);
608 let ss_x25519 = x25519_sk.diffie_hellman(&x25519_eph_pk);
609
610 let x25519_pk = X25519PublicKey::from(&x25519_sk);
611 let pk_x25519: [u8; X25519_LEN] = *x25519_pk.as_bytes();
612
613 let mut shared_secret = combine(
614 ss_mlkem.as_slice(),
615 ss_x25519.as_bytes(),
616 &x25519_eph_pk_bytes,
617 &pk_x25519,
618 );
619
620 let result = secretbox_decrypt(&shared_secret, nonce_slice, encrypted);
621 shared_secret.zeroize();
622 result
623}
624
625fn hybrid_open_768(combined: &[u8], seed_b64: &str) -> Result<Vec<u8>, CryptoError> {
628 let seed_bytes = b64::decode(seed_b64)?;
629 if seed_bytes.len() != SEED_LEN {
630 return Err(CryptoError::InvalidLength {
631 expected: SEED_LEN,
632 got: seed_bytes.len(),
633 });
634 }
635 if combined.len() < MIN_HYBRID_768_LEN {
636 return Err(CryptoError::TooShort);
637 }
638
639 let seed: [u8; SEED_LEN] = seed_bytes.try_into().unwrap();
640 let mut expanded = expand_seed(&seed, EXPANDED_SEED_768_LEN);
641 let mlkem_seed: [u8; MLKEM768_SEED_LEN] = expanded[..MLKEM768_SEED_LEN].try_into().unwrap();
642 let x25519_sk_bytes: [u8; X25519_LEN] = expanded[MLKEM768_SEED_LEN..].try_into().unwrap();
643 expanded.zeroize();
644
645 let mlkem_ct = &combined[1..1 + MLKEM768_CT_LEN];
647 let x25519_eph_pk_bytes: [u8; X25519_LEN] = combined
648 [1 + MLKEM768_CT_LEN..1 + COMBINED_CT_768_LEN]
649 .try_into()
650 .unwrap();
651 let nonce_slice = &combined[1 + COMBINED_CT_768_LEN..1 + COMBINED_CT_768_LEN + NONCE_LEN];
652 let encrypted = &combined[1 + COMBINED_CT_768_LEN + NONCE_LEN..];
653
654 let dk = DecapsulationKey::<MlKem768>::from_seed(mlkem_seed.into());
656 let kem_ct = mlkem_ct
657 .try_into()
658 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 ciphertext".into()))?;
659 let ss_mlkem = dk.decapsulate(kem_ct);
660
661 let x25519_sk = X25519StaticSecret::from(x25519_sk_bytes);
663 let x25519_eph_pk = X25519PublicKey::from(x25519_eph_pk_bytes);
664 let ss_x25519 = x25519_sk.diffie_hellman(&x25519_eph_pk);
665
666 let x25519_pk = X25519PublicKey::from(&x25519_sk);
667 let pk_x25519: [u8; X25519_LEN] = *x25519_pk.as_bytes();
668
669 let mut shared_secret = combine(
670 ss_mlkem.as_slice(),
671 ss_x25519.as_bytes(),
672 &x25519_eph_pk_bytes,
673 &pk_x25519,
674 );
675
676 let result = secretbox_decrypt(&shared_secret, nonce_slice, encrypted);
677 shared_secret.zeroize();
678 result
679}
680
681fn hybrid_open_1024(combined: &[u8], seed_b64: &str) -> Result<Vec<u8>, CryptoError> {
684 let seed_bytes = b64::decode(seed_b64)?;
685 if seed_bytes.len() != SEED_LEN {
686 return Err(CryptoError::InvalidLength {
687 expected: SEED_LEN,
688 got: seed_bytes.len(),
689 });
690 }
691 if combined.len() < MIN_HYBRID_1024_LEN {
692 return Err(CryptoError::TooShort);
693 }
694
695 let seed: [u8; SEED_LEN] = seed_bytes.try_into().unwrap();
696 let mut expanded = expand_seed(&seed, EXPANDED_SEED_1024_LEN);
697 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] = expanded[..MLKEM1024_SEED_LEN].try_into().unwrap();
698 let x25519_sk_bytes: [u8; X25519_LEN] = expanded[MLKEM1024_SEED_LEN..].try_into().unwrap();
699 expanded.zeroize();
700
701 let mlkem_ct = &combined[1..1 + MLKEM1024_CT_LEN];
703 let x25519_eph_pk_bytes: [u8; X25519_LEN] = combined
704 [1 + MLKEM1024_CT_LEN..1 + COMBINED_CT_1024_LEN]
705 .try_into()
706 .unwrap();
707 let nonce_slice = &combined[1 + COMBINED_CT_1024_LEN..1 + COMBINED_CT_1024_LEN + NONCE_LEN];
708 let encrypted = &combined[1 + COMBINED_CT_1024_LEN + NONCE_LEN..];
709
710 let dk = DecapsulationKey::<MlKem1024>::from_seed(mlkem_seed.into());
712 let kem_ct = mlkem_ct
713 .try_into()
714 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 ciphertext".into()))?;
715 let ss_mlkem = dk.decapsulate(kem_ct);
716
717 let x25519_sk = X25519StaticSecret::from(x25519_sk_bytes);
719 let x25519_eph_pk = X25519PublicKey::from(x25519_eph_pk_bytes);
720 let ss_x25519 = x25519_sk.diffie_hellman(&x25519_eph_pk);
721
722 let x25519_pk = X25519PublicKey::from(&x25519_sk);
723 let pk_x25519: [u8; X25519_LEN] = *x25519_pk.as_bytes();
724
725 let mut shared_secret = combine(
726 ss_mlkem.as_slice(),
727 ss_x25519.as_bytes(),
728 &x25519_eph_pk_bytes,
729 &pk_x25519,
730 );
731
732 let result = secretbox_decrypt(&shared_secret, nonce_slice, encrypted);
733 shared_secret.zeroize();
734 result
735}
736
737fn mlkem768_encapsulate(ek_bytes: &[u8]) -> Result<(Vec<u8>, [u8; 32]), CryptoError> {
741 let ek = EncapsulationKey::<MlKem768>::new(
742 ek_bytes
743 .try_into()
744 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 ek".into()))?,
745 )
746 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 encapsulation key".into()))?;
747 let mut coins = [0u8; 32];
748 random_bytes(&mut coins);
749 let (ct, ss) = ek.encapsulate_deterministic(&coins.into());
750 coins.zeroize();
751 let ss32: [u8; 32] = ss
752 .as_slice()
753 .try_into()
754 .map_err(|_| CryptoError::Hybrid("unexpected ML-KEM shared-secret length".into()))?;
755 Ok((ct.as_slice().to_vec(), ss32))
756}
757
758fn mlkem1024_encapsulate(ek_bytes: &[u8]) -> Result<(Vec<u8>, [u8; 32]), CryptoError> {
760 let ek = EncapsulationKey::<MlKem1024>::new(
761 ek_bytes
762 .try_into()
763 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 ek".into()))?,
764 )
765 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 encapsulation key".into()))?;
766 let mut coins = [0u8; 32];
767 random_bytes(&mut coins);
768 let (ct, ss) = ek.encapsulate_deterministic(&coins.into());
769 coins.zeroize();
770 let ss32: [u8; 32] = ss
771 .as_slice()
772 .try_into()
773 .map_err(|_| CryptoError::Hybrid("unexpected ML-KEM shared-secret length".into()))?;
774 Ok((ct.as_slice().to_vec(), ss32))
775}
776
777fn mlkem768_decapsulate(
779 seed64: &[u8; MLKEM768_SEED_LEN],
780 ct: &[u8],
781) -> Result<[u8; 32], CryptoError> {
782 let dk = DecapsulationKey::<MlKem768>::from_seed((*seed64).into());
783 let kem_ct = ct
784 .try_into()
785 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 ciphertext".into()))?;
786 let ss = dk.decapsulate(kem_ct);
787 ss.as_slice()
788 .try_into()
789 .map_err(|_| CryptoError::Hybrid("unexpected ML-KEM shared-secret length".into()))
790}
791
792fn mlkem1024_decapsulate(
794 seed64: &[u8; MLKEM1024_SEED_LEN],
795 ct: &[u8],
796) -> Result<[u8; 32], CryptoError> {
797 let dk = DecapsulationKey::<MlKem1024>::from_seed((*seed64).into());
798 let kem_ct = ct
799 .try_into()
800 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 ciphertext".into()))?;
801 let ss = dk.decapsulate(kem_ct);
802 ss.as_slice()
803 .try_into()
804 .map_err(|_| CryptoError::Hybrid("unexpected ML-KEM shared-secret length".into()))
805}
806
807fn mlkem1024_ek_from_seed(seed64: &[u8; MLKEM1024_SEED_LEN]) -> Vec<u8> {
809 let dk = DecapsulationKey::<MlKem1024>::from_seed((*seed64).into());
810 dk.encapsulation_key().to_bytes().as_slice().to_vec()
811}
812
813fn mlkem768_ek_from_seed(seed64: &[u8; MLKEM768_SEED_LEN]) -> Vec<u8> {
815 let dk = DecapsulationKey::<MlKem768>::from_seed((*seed64).into());
816 dk.encapsulation_key().to_bytes().as_slice().to_vec()
817}
818
819pub fn generate_hybrid_keypair_suite(
831 suite: Suite,
832 level: SecurityLevel,
833) -> Result<HybridKeyPair, CryptoError> {
834 match (suite, level) {
835 (Suite::Hybrid, _) | (Suite::HybridMatched, SecurityLevel::Cat1) => {
836 Ok(generate_hybrid_keypair_with_level(level))
837 }
838 (Suite::HybridMatched, SecurityLevel::Cat3) => Ok(generate_matched_cat3_keypair()),
839 (Suite::HybridMatched, SecurityLevel::Cat5) => Ok(generate_matched_cat5_keypair()),
840 (Suite::PureCnsa2, SecurityLevel::Cat5) => Ok(generate_pure_cnsa2_keypair()),
841 (Suite::PureCnsa2, _) => Err(CryptoError::Hybrid(
842 "PureCnsa2 is Cat-5 only in v0.7.0".into(),
843 )),
844 }
845}
846
847fn generate_pure_cnsa2_keypair() -> HybridKeyPair {
848 let mut seed = [0u8; SEED_LEN];
849 random_bytes(&mut seed);
850 let mut expanded = expand_seed(&seed, MLKEM1024_SEED_LEN);
851 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] = expanded[..].try_into().unwrap();
852 let pk = mlkem1024_ek_from_seed(&mlkem_seed);
853 let pair = HybridKeyPair {
854 public_key: b64::encode(&pk),
855 secret_key: b64::encode(&seed),
856 };
857 seed.zeroize();
858 expanded.zeroize();
859 pair
860}
861
862fn generate_matched_cat3_keypair() -> HybridKeyPair {
863 let mut seed = [0u8; SEED_LEN];
864 random_bytes(&mut seed);
865 let mut expanded = expand_seed(&seed, EXPANDED_SEED_MATCHED_CAT3_LEN);
866 let mlkem_seed: [u8; MLKEM768_SEED_LEN] = expanded[..MLKEM768_SEED_LEN].try_into().unwrap();
867 let x448_secret: [u8; ecc::X448_LEN] = expanded[MLKEM768_SEED_LEN..].try_into().unwrap();
868 let mut pk = mlkem768_ek_from_seed(&mlkem_seed);
869 pk.extend_from_slice(&ecc::x448_public_from_secret(&x448_secret));
870 let pair = HybridKeyPair {
871 public_key: b64::encode(&pk),
872 secret_key: b64::encode(&seed),
873 };
874 seed.zeroize();
875 expanded.zeroize();
876 pair
877}
878
879fn generate_matched_cat5_keypair() -> HybridKeyPair {
880 let mut seed = [0u8; SEED_LEN];
881 random_bytes(&mut seed);
882 let mut expanded = expand_seed(&seed, EXPANDED_SEED_MATCHED_CAT5_LEN);
883 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] = expanded[..MLKEM1024_SEED_LEN].try_into().unwrap();
884 let p521_secret: [u8; ecc::P521_SK_LEN] = expanded[MLKEM1024_SEED_LEN..].try_into().unwrap();
885 let mut pk = mlkem1024_ek_from_seed(&mlkem_seed);
886 pk.extend_from_slice(
887 &ecc::p521_public_from_secret(&p521_secret)
888 .expect("deterministic P-521 public-key derivation"),
889 );
890 let pair = HybridKeyPair {
891 public_key: b64::encode(&pk),
892 secret_key: b64::encode(&seed),
893 };
894 seed.zeroize();
895 expanded.zeroize();
896 pair
897}
898
899pub fn hybrid_seal_suite(
906 plaintext: &[u8],
907 combined_pk_b64: &str,
908 suite: Suite,
909 level: SecurityLevel,
910) -> Result<String, CryptoError> {
911 hybrid_seal_suite_with_context(plaintext, combined_pk_b64, suite, level, SEAL_CONTEXT_V1)
912}
913
914pub fn hybrid_seal_suite_with_context(
922 plaintext: &[u8],
923 combined_pk_b64: &str,
924 suite: Suite,
925 level: SecurityLevel,
926 context_label: &str,
927) -> Result<String, CryptoError> {
928 match (suite, level) {
929 (Suite::Hybrid, _) => hybrid_seal_with_level(plaintext, combined_pk_b64, level),
930 (Suite::HybridMatched, SecurityLevel::Cat1) => {
931 hybrid_seal_with_level(plaintext, combined_pk_b64, SecurityLevel::Cat1)
932 }
933 (Suite::HybridMatched, SecurityLevel::Cat3) => {
934 seal_matched_cat3(plaintext, combined_pk_b64, context_label)
935 }
936 (Suite::HybridMatched, SecurityLevel::Cat5) => {
937 seal_matched_cat5(plaintext, combined_pk_b64, context_label)
938 }
939 (Suite::PureCnsa2, SecurityLevel::Cat5) => {
940 seal_pure_cnsa2(plaintext, combined_pk_b64, context_label)
941 }
942 (Suite::PureCnsa2, _) => Err(CryptoError::Hybrid(
943 "PureCnsa2 is Cat-5 only in v0.7.0".into(),
944 )),
945 }
946}
947
948fn check_pk_len(pk: &[u8], expected: usize) -> Result<(), CryptoError> {
949 if pk.len() != expected {
950 return Err(CryptoError::InvalidLength {
951 expected,
952 got: pk.len(),
953 });
954 }
955 Ok(())
956}
957
958fn assemble_envelope(
959 tag: u8,
960 kem_ct: &[u8],
961 ecc_eph_pk: Option<&[u8]>,
962 nonce: &[u8; GCM_NONCE_LEN],
963 aead_ct: &[u8],
964) -> String {
965 let ecc_len = ecc_eph_pk.map_or(0, |p| p.len());
966 let mut out = Vec::with_capacity(1 + kem_ct.len() + ecc_len + GCM_NONCE_LEN + aead_ct.len());
967 out.push(tag);
968 out.extend_from_slice(kem_ct);
969 if let Some(p) = ecc_eph_pk {
970 out.extend_from_slice(p);
971 }
972 out.extend_from_slice(nonce);
973 out.extend_from_slice(aead_ct);
974 b64::encode(&out)
975}
976
977fn random_nonce() -> [u8; GCM_NONCE_LEN] {
978 let mut nonce = [0u8; GCM_NONCE_LEN];
979 random_bytes(&mut nonce);
980 nonce
981}
982
983fn seal_pure_cnsa2(
984 plaintext: &[u8],
985 combined_pk_b64: &str,
986 context_label: &str,
987) -> Result<String, CryptoError> {
988 let pk = b64::decode(combined_pk_b64)?;
989 check_pk_len(&pk, PURE_CNSA2_PK_LEN)?;
990 let (kem_ct, ss_mlkem) = mlkem1024_encapsulate(&pk)?;
991 let nonce = random_nonce();
992 let aead_ct = suite::envelope_seal(
993 &ss_mlkem,
994 TAG_KEM_PURE_CNSA2,
995 context_label,
996 &nonce,
997 plaintext,
998 )?;
999 Ok(assemble_envelope(
1000 TAG_KEM_PURE_CNSA2,
1001 &kem_ct,
1002 None,
1003 &nonce,
1004 &aead_ct,
1005 ))
1006}
1007
1008fn seal_matched_cat3(
1009 plaintext: &[u8],
1010 combined_pk_b64: &str,
1011 context_label: &str,
1012) -> Result<String, CryptoError> {
1013 let pk = b64::decode(combined_pk_b64)?;
1014 check_pk_len(&pk, MATCHED_CAT3_PK_LEN)?;
1015 let (mlkem_ek, x448_pk) = pk.split_at(MLKEM768_EK_LEN);
1016 let (kem_ct, ss_mlkem) = mlkem768_encapsulate(mlkem_ek)?;
1017 let (x448_eph_pk, ss_x448) = ecc::x448_encapsulate(x448_pk)?;
1018 let mut ikm = Vec::with_capacity(32 + ecc::X448_LEN);
1019 ikm.extend_from_slice(&ss_mlkem);
1020 ikm.extend_from_slice(&ss_x448);
1021 let nonce = random_nonce();
1022 let aead_ct =
1023 suite::envelope_seal(&ikm, TAG_KEM_MATCHED_CAT3, context_label, &nonce, plaintext)?;
1024 ikm.zeroize();
1025 Ok(assemble_envelope(
1026 TAG_KEM_MATCHED_CAT3,
1027 &kem_ct,
1028 Some(&x448_eph_pk),
1029 &nonce,
1030 &aead_ct,
1031 ))
1032}
1033
1034fn seal_matched_cat5(
1035 plaintext: &[u8],
1036 combined_pk_b64: &str,
1037 context_label: &str,
1038) -> Result<String, CryptoError> {
1039 let pk = b64::decode(combined_pk_b64)?;
1040 check_pk_len(&pk, MATCHED_CAT5_PK_LEN)?;
1041 let (mlkem_ek, p521_pk) = pk.split_at(MLKEM1024_EK_LEN);
1042 let (kem_ct, ss_mlkem) = mlkem1024_encapsulate(mlkem_ek)?;
1043 let (p521_eph_pk, ss_p521) = ecc::p521_encapsulate(p521_pk)?;
1044 let mut ikm = Vec::with_capacity(32 + ecc::P521_SS_LEN);
1045 ikm.extend_from_slice(&ss_mlkem);
1046 ikm.extend_from_slice(&ss_p521);
1047 let nonce = random_nonce();
1048 let aead_ct =
1049 suite::envelope_seal(&ikm, TAG_KEM_MATCHED_CAT5, context_label, &nonce, plaintext)?;
1050 ikm.zeroize();
1051 Ok(assemble_envelope(
1052 TAG_KEM_MATCHED_CAT5,
1053 &kem_ct,
1054 Some(&p521_eph_pk),
1055 &nonce,
1056 &aead_ct,
1057 ))
1058}
1059
1060fn load_seed(seed_b64: &str) -> Result<[u8; SEED_LEN], CryptoError> {
1063 let seed_bytes = b64::decode(seed_b64)?;
1064 if seed_bytes.len() != SEED_LEN {
1065 return Err(CryptoError::InvalidLength {
1066 expected: SEED_LEN,
1067 got: seed_bytes.len(),
1068 });
1069 }
1070 Ok(seed_bytes.try_into().unwrap())
1071}
1072
1073fn open_pure_cnsa2(
1074 combined: &[u8],
1075 seed_b64: &str,
1076 context_label: &str,
1077) -> Result<Vec<u8>, CryptoError> {
1078 if combined.len() < MIN_PURE_CNSA2_LEN {
1079 return Err(CryptoError::TooShort);
1080 }
1081 let seed = load_seed(seed_b64)?;
1082 let mut expanded = expand_seed(&seed, MLKEM1024_SEED_LEN);
1083 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] = expanded[..].try_into().unwrap();
1084
1085 let kem_ct = &combined[1..1 + MLKEM1024_CT_LEN];
1086 let nonce: [u8; GCM_NONCE_LEN] = combined
1087 [1 + MLKEM1024_CT_LEN..1 + MLKEM1024_CT_LEN + GCM_NONCE_LEN]
1088 .try_into()
1089 .unwrap();
1090 let aead_ct = &combined[1 + MLKEM1024_CT_LEN + GCM_NONCE_LEN..];
1091
1092 let ss_mlkem = mlkem1024_decapsulate(&mlkem_seed, kem_ct)?;
1093 expanded.zeroize();
1094 suite::envelope_open(
1095 &ss_mlkem,
1096 TAG_KEM_PURE_CNSA2,
1097 context_label,
1098 &nonce,
1099 aead_ct,
1100 )
1101}
1102
1103fn open_matched_cat3(
1104 combined: &[u8],
1105 seed_b64: &str,
1106 context_label: &str,
1107) -> Result<Vec<u8>, CryptoError> {
1108 if combined.len() < MIN_MATCHED_CAT3_LEN {
1109 return Err(CryptoError::TooShort);
1110 }
1111 let seed = load_seed(seed_b64)?;
1112 let mut expanded = expand_seed(&seed, EXPANDED_SEED_MATCHED_CAT3_LEN);
1113 let mlkem_seed: [u8; MLKEM768_SEED_LEN] = expanded[..MLKEM768_SEED_LEN].try_into().unwrap();
1114 let x448_secret: [u8; ecc::X448_LEN] = expanded[MLKEM768_SEED_LEN..].try_into().unwrap();
1115
1116 let kem_ct = &combined[1..1 + MLKEM768_CT_LEN];
1117 let ecc_start = 1 + MLKEM768_CT_LEN;
1118 let x448_eph_pk = &combined[ecc_start..ecc_start + ecc::X448_LEN];
1119 let nonce_start = ecc_start + ecc::X448_LEN;
1120 let nonce: [u8; GCM_NONCE_LEN] = combined[nonce_start..nonce_start + GCM_NONCE_LEN]
1121 .try_into()
1122 .unwrap();
1123 let aead_ct = &combined[nonce_start + GCM_NONCE_LEN..];
1124
1125 let ss_mlkem = mlkem768_decapsulate(&mlkem_seed, kem_ct)?;
1126 let ss_x448 = ecc::x448_decapsulate(&x448_secret, x448_eph_pk)?;
1127 expanded.zeroize();
1128 let mut ikm = Vec::with_capacity(32 + ecc::X448_LEN);
1129 ikm.extend_from_slice(&ss_mlkem);
1130 ikm.extend_from_slice(&ss_x448);
1131 let out = suite::envelope_open(&ikm, TAG_KEM_MATCHED_CAT3, context_label, &nonce, aead_ct);
1132 ikm.zeroize();
1133 out
1134}
1135
1136fn open_matched_cat5(
1137 combined: &[u8],
1138 seed_b64: &str,
1139 context_label: &str,
1140) -> Result<Vec<u8>, CryptoError> {
1141 if combined.len() < MIN_MATCHED_CAT5_LEN {
1142 return Err(CryptoError::TooShort);
1143 }
1144 let seed = load_seed(seed_b64)?;
1145 let mut expanded = expand_seed(&seed, EXPANDED_SEED_MATCHED_CAT5_LEN);
1146 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] = expanded[..MLKEM1024_SEED_LEN].try_into().unwrap();
1147 let p521_secret: [u8; ecc::P521_SK_LEN] = expanded[MLKEM1024_SEED_LEN..].try_into().unwrap();
1148
1149 let kem_ct = &combined[1..1 + MLKEM1024_CT_LEN];
1150 let ecc_start = 1 + MLKEM1024_CT_LEN;
1151 let p521_eph_pk = &combined[ecc_start..ecc_start + ecc::P521_PK_LEN];
1152 let nonce_start = ecc_start + ecc::P521_PK_LEN;
1153 let nonce: [u8; GCM_NONCE_LEN] = combined[nonce_start..nonce_start + GCM_NONCE_LEN]
1154 .try_into()
1155 .unwrap();
1156 let aead_ct = &combined[nonce_start + GCM_NONCE_LEN..];
1157
1158 let ss_mlkem = mlkem1024_decapsulate(&mlkem_seed, kem_ct)?;
1159 let ss_p521 = ecc::p521_decapsulate(&p521_secret, p521_eph_pk)?;
1160 expanded.zeroize();
1161 let mut ikm = Vec::with_capacity(32 + ecc::P521_SS_LEN);
1162 ikm.extend_from_slice(&ss_mlkem);
1163 ikm.extend_from_slice(&ss_p521);
1164 let out = suite::envelope_open(&ikm, TAG_KEM_MATCHED_CAT5, context_label, &nonce, aead_ct);
1165 ikm.zeroize();
1166 out
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171 use super::*;
1172
1173 #[test]
1176 fn cat3_roundtrip() {
1177 let kp = generate_hybrid_keypair();
1178 let pt = b"32-byte symmetric context key!!!";
1179 let ct = hybrid_seal(pt, &kp.public_key).unwrap();
1180 assert!(is_hybrid_ciphertext(&ct));
1181 let opened = hybrid_open(&ct, &kp.secret_key).unwrap();
1182 assert_eq!(opened, pt);
1183 }
1184
1185 #[test]
1186 fn cat3_wrong_key_fails() {
1187 let kp1 = generate_hybrid_keypair();
1188 let kp2 = generate_hybrid_keypair();
1189 let ct = hybrid_seal(b"x", &kp1.public_key).unwrap();
1190 assert!(hybrid_open(&ct, &kp2.secret_key).is_err());
1191 }
1192
1193 #[test]
1194 fn cat3_version_tag() {
1195 let kp = generate_hybrid_keypair();
1196 let raw = b64::decode(&hybrid_seal(b"x", &kp.public_key).unwrap()).unwrap();
1197 assert_eq!(raw[0], VERSION_HYBRID_768);
1198 }
1199
1200 #[test]
1201 fn cat3_nondeterministic() {
1202 let kp = generate_hybrid_keypair();
1203 let c1 = hybrid_seal(b"x", &kp.public_key).unwrap();
1204 let c2 = hybrid_seal(b"x", &kp.public_key).unwrap();
1205 assert_ne!(c1, c2);
1206 }
1207
1208 #[test]
1209 fn cat3_empty_plaintext() {
1210 let kp = generate_hybrid_keypair();
1211 let ct = hybrid_seal(b"", &kp.public_key).unwrap();
1212 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"");
1213 }
1214
1215 #[test]
1216 fn cat3_key_sizes() {
1217 let kp = generate_hybrid_keypair();
1218 let pk = b64::decode(&kp.public_key).unwrap();
1219 let sk = b64::decode(&kp.secret_key).unwrap();
1220 assert_eq!(pk.len(), COMBINED_PK_768_LEN); assert_eq!(sk.len(), SEED_LEN); }
1223
1224 #[test]
1225 fn cat3_ciphertext_size() {
1226 let kp = generate_hybrid_keypair();
1227 let pt = b"exactly 32 bytes of key material";
1228 let raw = b64::decode(&hybrid_seal(pt, &kp.public_key).unwrap()).unwrap();
1229 assert_eq!(
1231 raw.len(),
1232 1 + COMBINED_CT_768_LEN + NONCE_LEN + 32 + MAC_LEN
1233 );
1234 }
1235
1236 #[test]
1239 fn cat1_roundtrip() {
1240 let kp = generate_hybrid_keypair_512();
1241 let pt = b"32-byte symmetric context key!!!";
1242 let ct = hybrid_seal_512(pt, &kp.public_key).unwrap();
1243 assert!(is_hybrid_ciphertext(&ct));
1244 let opened = hybrid_open(&ct, &kp.secret_key).unwrap();
1245 assert_eq!(opened, pt);
1246 }
1247
1248 #[test]
1249 fn cat1_version_tag() {
1250 let kp = generate_hybrid_keypair_512();
1251 let raw = b64::decode(&hybrid_seal_512(b"x", &kp.public_key).unwrap()).unwrap();
1252 assert_eq!(raw[0], VERSION_HYBRID_512);
1253 }
1254
1255 #[test]
1256 fn cat1_wrong_key_fails() {
1257 let kp1 = generate_hybrid_keypair_512();
1258 let kp2 = generate_hybrid_keypair_512();
1259 let ct = hybrid_seal_512(b"x", &kp1.public_key).unwrap();
1260 assert!(hybrid_open(&ct, &kp2.secret_key).is_err());
1261 }
1262
1263 #[test]
1264 fn cat1_key_sizes() {
1265 let kp = generate_hybrid_keypair_512();
1266 let pk = b64::decode(&kp.public_key).unwrap();
1267 let sk = b64::decode(&kp.secret_key).unwrap();
1268 assert_eq!(pk.len(), COMBINED_PK_512_LEN); assert_eq!(sk.len(), SEED_LEN); }
1271
1272 #[test]
1273 fn cat1_ciphertext_size() {
1274 let kp = generate_hybrid_keypair_512();
1275 let pt = b"exactly 32 bytes of key material";
1276 let raw = b64::decode(&hybrid_seal_512(pt, &kp.public_key).unwrap()).unwrap();
1277 assert_eq!(
1279 raw.len(),
1280 1 + COMBINED_CT_512_LEN + NONCE_LEN + 32 + MAC_LEN
1281 );
1282 }
1283
1284 #[test]
1285 fn cat1_nondeterministic() {
1286 let kp = generate_hybrid_keypair_512();
1287 let c1 = hybrid_seal_512(b"x", &kp.public_key).unwrap();
1288 let c2 = hybrid_seal_512(b"x", &kp.public_key).unwrap();
1289 assert_ne!(c1, c2);
1290 }
1291
1292 #[test]
1293 fn cat1_empty_plaintext() {
1294 let kp = generate_hybrid_keypair_512();
1295 let ct = hybrid_seal_512(b"", &kp.public_key).unwrap();
1296 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"");
1297 }
1298
1299 #[test]
1302 fn cat5_roundtrip() {
1303 let kp = generate_hybrid_keypair_1024();
1304 let pt = b"32-byte symmetric context key!!!";
1305 let ct = hybrid_seal_1024(pt, &kp.public_key).unwrap();
1306 assert!(is_hybrid_ciphertext(&ct));
1307 let opened = hybrid_open(&ct, &kp.secret_key).unwrap();
1308 assert_eq!(opened, pt);
1309 }
1310
1311 #[test]
1312 fn cat5_version_tag() {
1313 let kp = generate_hybrid_keypair_1024();
1314 let raw = b64::decode(&hybrid_seal_1024(b"x", &kp.public_key).unwrap()).unwrap();
1315 assert_eq!(raw[0], VERSION_HYBRID_1024);
1316 }
1317
1318 #[test]
1319 fn cat5_wrong_key_fails() {
1320 let kp1 = generate_hybrid_keypair_1024();
1321 let kp2 = generate_hybrid_keypair_1024();
1322 let ct = hybrid_seal_1024(b"x", &kp1.public_key).unwrap();
1323 assert!(hybrid_open(&ct, &kp2.secret_key).is_err());
1324 }
1325
1326 #[test]
1327 fn cat5_key_sizes() {
1328 let kp = generate_hybrid_keypair_1024();
1329 let pk = b64::decode(&kp.public_key).unwrap();
1330 let sk = b64::decode(&kp.secret_key).unwrap();
1331 assert_eq!(pk.len(), COMBINED_PK_1024_LEN); assert_eq!(sk.len(), SEED_LEN); }
1334
1335 #[test]
1336 fn cat5_ciphertext_size() {
1337 let kp = generate_hybrid_keypair_1024();
1338 let pt = b"exactly 32 bytes of key material";
1339 let raw = b64::decode(&hybrid_seal_1024(pt, &kp.public_key).unwrap()).unwrap();
1340 assert_eq!(
1342 raw.len(),
1343 1 + COMBINED_CT_1024_LEN + NONCE_LEN + 32 + MAC_LEN
1344 );
1345 }
1346
1347 #[test]
1348 fn cat5_nondeterministic() {
1349 let kp = generate_hybrid_keypair_1024();
1350 let c1 = hybrid_seal_1024(b"x", &kp.public_key).unwrap();
1351 let c2 = hybrid_seal_1024(b"x", &kp.public_key).unwrap();
1352 assert_ne!(c1, c2);
1353 }
1354
1355 #[test]
1356 fn cat5_empty_plaintext() {
1357 let kp = generate_hybrid_keypair_1024();
1358 let ct = hybrid_seal_1024(b"", &kp.public_key).unwrap();
1359 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"");
1360 }
1361
1362 #[test]
1365 fn cat3_ct_cannot_open_with_cat5_key() {
1366 let kp3 = generate_hybrid_keypair();
1367 let kp5 = generate_hybrid_keypair_1024();
1368 let ct = hybrid_seal(b"test", &kp3.public_key).unwrap();
1369 assert!(hybrid_open(&ct, &kp5.secret_key).is_err());
1370 }
1371
1372 #[test]
1373 fn cat5_ct_cannot_open_with_cat3_key() {
1374 let kp3 = generate_hybrid_keypair();
1375 let kp5 = generate_hybrid_keypair_1024();
1376 let ct = hybrid_seal_1024(b"test", &kp5.public_key).unwrap();
1377 assert!(hybrid_open(&ct, &kp3.secret_key).is_err());
1378 }
1379
1380 #[test]
1381 fn cat1_ct_cannot_open_with_cat3_key() {
1382 let kp1 = generate_hybrid_keypair_512();
1383 let kp3 = generate_hybrid_keypair();
1384 let ct = hybrid_seal_512(b"test", &kp1.public_key).unwrap();
1385 assert!(hybrid_open(&ct, &kp3.secret_key).is_err());
1386 }
1387
1388 #[test]
1389 fn cat1_ct_cannot_open_with_cat5_key() {
1390 let kp1 = generate_hybrid_keypair_512();
1391 let kp5 = generate_hybrid_keypair_1024();
1392 let ct = hybrid_seal_512(b"test", &kp1.public_key).unwrap();
1393 assert!(hybrid_open(&ct, &kp5.secret_key).is_err());
1394 }
1395
1396 #[test]
1397 fn legacy_not_hybrid() {
1398 let legacy = b64::encode(&[0x42, 0x02, 0x03]);
1400 assert!(!is_hybrid_ciphertext(&legacy));
1401 }
1402
1403 #[test]
1404 fn legacy_starting_with_0x01_not_misdetected_as_cat1() {
1405 let mut legacy = vec![0x01u8]; legacy.extend_from_slice(&[0u8; 79]); let legacy_b64 = b64::encode(&legacy);
1413 assert!(!is_hybrid_ciphertext(&legacy_b64));
1414 let kp = generate_hybrid_keypair_512();
1416 let err = hybrid_open(&legacy_b64, &kp.secret_key).unwrap_err();
1417 assert!(matches!(err, CryptoError::TooShort));
1418 }
1419
1420 #[test]
1421 fn long_0x01_blob_below_cat1_min_not_hybrid() {
1422 let min_cat1 = MIN_HYBRID_512_LEN; let mut blob = vec![0x01u8];
1425 blob.extend_from_slice(&vec![0u8; min_cat1 - 2]); assert!(!is_hybrid_ciphertext(&b64::encode(&blob)));
1427 let mut at_min = vec![0x01u8];
1431 at_min.extend_from_slice(&vec![0u8; min_cat1 - 1]); assert!(is_hybrid_ciphertext(&b64::encode(&at_min)));
1433 }
1434
1435 #[test]
1436 fn seed_expansion_deterministic() {
1437 let seed = [0x42u8; SEED_LEN];
1438 let expanded = expand_seed(&seed, 96);
1439 let expanded2 = expand_seed(&seed, 96);
1440 assert_eq!(expanded, expanded2);
1441 }
1442
1443 #[test]
1444 fn combiner_uses_label() {
1445 let ss_mlkem = [0xAAu8; 32];
1446 let ss_x25519 = [0xBBu8; 32];
1447 let ct_x25519 = [0xCCu8; 32];
1448 let pk_x25519 = [0xDDu8; 32];
1449 let result = combine(&ss_mlkem, &ss_x25519, &ct_x25519, &pk_x25519);
1450 assert_eq!(result.len(), 32);
1451
1452 let ss_mlkem2 = [0xEEu8; 32];
1453 let result2 = combine(&ss_mlkem2, &ss_x25519, &ct_x25519, &pk_x25519);
1454 assert_ne!(result, result2);
1455 }
1456
1457 #[test]
1460 fn pure_cnsa2_roundtrip() {
1461 let kp = generate_hybrid_keypair_suite(Suite::PureCnsa2, SecurityLevel::Cat5).unwrap();
1462 let pt = b"32-byte symmetric context key!!!";
1463 let ct =
1464 hybrid_seal_suite(pt, &kp.public_key, Suite::PureCnsa2, SecurityLevel::Cat5).unwrap();
1465 assert!(is_hybrid_ciphertext(&ct));
1466 assert_eq!(b64::decode(&ct).unwrap()[0], TAG_KEM_PURE_CNSA2);
1467 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), pt);
1468 }
1469
1470 #[test]
1471 fn pure_cnsa2_pk_len_and_no_classical_half() {
1472 let kp = generate_hybrid_keypair_suite(Suite::PureCnsa2, SecurityLevel::Cat5).unwrap();
1473 assert_eq!(
1474 b64::decode(&kp.public_key).unwrap().len(),
1475 PURE_CNSA2_PK_LEN
1476 );
1477 assert_eq!(b64::decode(&kp.secret_key).unwrap().len(), SEED_LEN);
1478 }
1479
1480 #[test]
1481 fn pure_cnsa2_only_cat5() {
1482 assert!(generate_hybrid_keypair_suite(Suite::PureCnsa2, SecurityLevel::Cat3).is_err());
1483 assert!(generate_hybrid_keypair_suite(Suite::PureCnsa2, SecurityLevel::Cat1).is_err());
1484 }
1485
1486 #[test]
1487 fn matched_cat3_roundtrip() {
1488 let kp = generate_hybrid_keypair_suite(Suite::HybridMatched, SecurityLevel::Cat3).unwrap();
1489 assert_eq!(
1490 b64::decode(&kp.public_key).unwrap().len(),
1491 MATCHED_CAT3_PK_LEN
1492 );
1493 let pt = b"matched cat-3 (ML-KEM-768 + X448)";
1494 let ct = hybrid_seal_suite(
1495 pt,
1496 &kp.public_key,
1497 Suite::HybridMatched,
1498 SecurityLevel::Cat3,
1499 )
1500 .unwrap();
1501 assert!(is_hybrid_ciphertext(&ct));
1502 assert_eq!(b64::decode(&ct).unwrap()[0], TAG_KEM_MATCHED_CAT3);
1503 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), pt);
1504 }
1505
1506 #[test]
1507 fn matched_cat5_roundtrip() {
1508 let kp = generate_hybrid_keypair_suite(Suite::HybridMatched, SecurityLevel::Cat5).unwrap();
1509 assert_eq!(
1510 b64::decode(&kp.public_key).unwrap().len(),
1511 MATCHED_CAT5_PK_LEN
1512 );
1513 let pt = b"matched cat-5 (ML-KEM-1024 + P-521)";
1514 let ct = hybrid_seal_suite(
1515 pt,
1516 &kp.public_key,
1517 Suite::HybridMatched,
1518 SecurityLevel::Cat5,
1519 )
1520 .unwrap();
1521 assert!(is_hybrid_ciphertext(&ct));
1522 assert_eq!(b64::decode(&ct).unwrap()[0], TAG_KEM_MATCHED_CAT5);
1523 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), pt);
1524 }
1525
1526 #[test]
1527 fn matched_cat1_is_plain_hybrid() {
1528 let kp = generate_hybrid_keypair_suite(Suite::HybridMatched, SecurityLevel::Cat1).unwrap();
1531 let ct = hybrid_seal_suite(
1532 b"x",
1533 &kp.public_key,
1534 Suite::HybridMatched,
1535 SecurityLevel::Cat1,
1536 )
1537 .unwrap();
1538 assert_eq!(b64::decode(&ct).unwrap()[0], VERSION_HYBRID_512);
1539 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"x");
1540 }
1541
1542 #[test]
1543 fn hybrid_suite_is_unchanged_legacy_format() {
1544 for (level, tag) in [
1546 (SecurityLevel::Cat1, VERSION_HYBRID_512),
1547 (SecurityLevel::Cat3, VERSION_HYBRID_768),
1548 (SecurityLevel::Cat5, VERSION_HYBRID_1024),
1549 ] {
1550 let kp = generate_hybrid_keypair_suite(Suite::Hybrid, level).unwrap();
1551 let ct = hybrid_seal_suite(b"x", &kp.public_key, Suite::Hybrid, level).unwrap();
1552 assert_eq!(b64::decode(&ct).unwrap()[0], tag);
1553 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"x");
1554 }
1555 }
1556
1557 #[test]
1558 fn new_suites_empty_plaintext() {
1559 for (suite, level) in [
1560 (Suite::PureCnsa2, SecurityLevel::Cat5),
1561 (Suite::HybridMatched, SecurityLevel::Cat3),
1562 (Suite::HybridMatched, SecurityLevel::Cat5),
1563 ] {
1564 let kp = generate_hybrid_keypair_suite(suite, level).unwrap();
1565 let ct = hybrid_seal_suite(b"", &kp.public_key, suite, level).unwrap();
1566 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"");
1567 }
1568 }
1569
1570 #[test]
1571 fn new_suites_nondeterministic_and_wrong_key_fails() {
1572 for (suite, level) in [
1573 (Suite::PureCnsa2, SecurityLevel::Cat5),
1574 (Suite::HybridMatched, SecurityLevel::Cat3),
1575 (Suite::HybridMatched, SecurityLevel::Cat5),
1576 ] {
1577 let kp = generate_hybrid_keypair_suite(suite, level).unwrap();
1578 let kp2 = generate_hybrid_keypair_suite(suite, level).unwrap();
1579 let c1 = hybrid_seal_suite(b"x", &kp.public_key, suite, level).unwrap();
1580 let c2 = hybrid_seal_suite(b"x", &kp.public_key, suite, level).unwrap();
1581 assert_ne!(c1, c2, "fresh nonce + KEM => non-deterministic");
1582 assert!(
1583 hybrid_open(&c1, &kp2.secret_key).is_err(),
1584 "wrong key fails"
1585 );
1586 }
1587 }
1588
1589 #[test]
1590 fn context_label_is_bound() {
1591 let kp = generate_hybrid_keypair_suite(Suite::PureCnsa2, SecurityLevel::Cat5).unwrap();
1592 let ct = hybrid_seal_suite_with_context(
1593 b"secret",
1594 &kp.public_key,
1595 Suite::PureCnsa2,
1596 SecurityLevel::Cat5,
1597 "mosslet/seal/v1",
1598 )
1599 .unwrap();
1600 assert!(hybrid_open_with_context(&ct, &kp.secret_key, "metamorphic/seal/v1").is_err());
1602 assert_eq!(
1604 hybrid_open_with_context(&ct, &kp.secret_key, "mosslet/seal/v1").unwrap(),
1605 b"secret"
1606 );
1607 }
1608
1609 #[test]
1610 fn new_suite_tampered_ciphertext_fails() {
1611 let kp = generate_hybrid_keypair_suite(Suite::HybridMatched, SecurityLevel::Cat5).unwrap();
1612 let ct = hybrid_seal_suite(
1613 b"data",
1614 &kp.public_key,
1615 Suite::HybridMatched,
1616 SecurityLevel::Cat5,
1617 )
1618 .unwrap();
1619 let mut raw = b64::decode(&ct).unwrap();
1620 let last = raw.len() - 1;
1621 raw[last] ^= 0xFF; assert!(hybrid_open(&b64::encode(&raw), &kp.secret_key).is_err());
1623 }
1624
1625 #[test]
1631 fn mlkem1024_fips203_kat() {
1632 use crate::hash::sha3_512;
1633 fn hx(b: &[u8]) -> String {
1634 b.iter().map(|x| format!("{x:02x}")).collect()
1635 }
1636 let seed = [0x07u8; MLKEM1024_SEED_LEN];
1637 let dk = DecapsulationKey::<MlKem1024>::from_seed(seed.into());
1638 let ek = dk.encapsulation_key();
1639 let ek_bytes = ek.to_bytes();
1640 assert_eq!(ek_bytes.len(), MLKEM1024_EK_LEN);
1641 assert_eq!(
1642 hx(&sha3_512(ek_bytes.as_slice())),
1643 "21d44f22f8467cde9040b3e6161c9353f9dd48e6854d3125c2690826a06ad707\
1644 8fa79245d715430afcca6bbd94a352e95081bd0b65aa210661f4deafdfc2fee4"
1645 );
1646 let coins = [0x09u8; 32];
1647 let (ct, ss) = ek.encapsulate_deterministic(&coins.into());
1648 assert_eq!(ct.as_slice().len(), MLKEM1024_CT_LEN);
1649 assert_eq!(
1650 hx(&sha3_512(ct.as_slice())),
1651 "79ca73f654930548ecedd30019fcd19f4ca6b653aef0bc647df8389945d04f81\
1652 47d5c45c8c8b93b679f3c15a4424c6c38c57e23d3383fd1e72964e98c1f19475"
1653 );
1654 assert_eq!(
1655 hx(ss.as_slice()),
1656 "a6b0741c68de147722d30abc60415c846f7130a51611c0de65cfe019cd9913f4"
1657 );
1658 }
1659}