use kyberlib::{
decapsulate, encapsulate, keypair, KemCore, KyberLibError,
MlKem768, MlKem768Ciphertext, MlKem768EncapKey,
KYBER_CIPHERTEXT_BYTES, KYBER_PUBLIC_KEY_BYTES,
KYBER_SECRET_KEY_BYTES,
};
use proptest::prelude::*;
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;
proptest! {
#[test]
fn encap_key_try_from_slice_total(bytes in prop::collection::vec(any::<u8>(), 0..4096)) {
match MlKem768EncapKey::try_from_slice(&bytes) {
Ok(_) => prop_assert_eq!(bytes.len(), KYBER_PUBLIC_KEY_BYTES),
Err(KyberLibError::InvalidLength) => {
prop_assert_ne!(bytes.len(), KYBER_PUBLIC_KEY_BYTES);
}
Err(other) => prop_assert!(
false,
"unexpected error variant: {:?}", other
),
}
}
#[test]
fn ciphertext_try_from_slice_total(bytes in prop::collection::vec(any::<u8>(), 0..4096)) {
match MlKem768Ciphertext::try_from_slice(&bytes) {
Ok(_) => prop_assert_eq!(bytes.len(), KYBER_CIPHERTEXT_BYTES),
Err(KyberLibError::InvalidLength) => {
prop_assert_ne!(bytes.len(), KYBER_CIPHERTEXT_BYTES);
}
Err(other) => prop_assert!(false, "unexpected: {:?}", other),
}
}
#[test]
fn encap_decap_panic_free(
pk in prop::collection::vec(any::<u8>(), 0..4096),
ct in prop::collection::vec(any::<u8>(), 0..4096),
sk in prop::collection::vec(any::<u8>(), 0..8192),
) {
let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
let _ = encapsulate(&pk, &mut rng);
let _ = decapsulate(&ct, &sk);
}
#[test]
fn public_extraction_panic_free(
sk in prop::collection::vec(any::<u8>(), KYBER_SECRET_KEY_BYTES..=KYBER_SECRET_KEY_BYTES + 16)
) {
let _ = kyberlib::public(&sk);
prop_assert!(true);
}
}
proptest! {
#[test]
fn ml_kem_768_round_trip_property(seed in any::<[u8; 32]>()) {
let mut rng = ChaCha20Rng::from_seed(seed);
let (dk, ek) = MlKem768::generate(&mut rng)
.expect("keygen — RNG cannot fail under ChaCha20");
let (ct, ss_a) = ek.encapsulate(&mut rng)
.expect("encap — RNG cannot fail under ChaCha20");
let ss_b = dk.decapsulate(&ct);
prop_assert_eq!(
ss_a.as_bytes(),
ss_b.as_bytes(),
"round-trip shared secret mismatch — KEM correctness violated"
);
}
#[test]
fn implicit_rejection_is_total(
seed in any::<[u8; 32]>(),
flip_index in 0usize..KYBER_CIPHERTEXT_BYTES,
flip_mask in 1u8..=255u8,
) {
let mut rng = ChaCha20Rng::from_seed(seed);
let keys = keypair(&mut rng).expect("keygen");
let (mut ct, ss_a) = encapsulate(&keys.public, &mut rng).expect("encap");
ct[flip_index] ^= flip_mask;
let ss_b = decapsulate(&ct, &keys.secret)
.expect("implicit rejection — decapsulate returns Ok with pseudorandom SS");
prop_assert_ne!(
ss_a, ss_b,
"modified ciphertext yielded the original shared secret — IND-CCA broken"
);
}
}