From ca76d328de36075ffed8149e403cc0b2c5f590c4 Mon Sep 17 00:00:00 2001 From: Mike Mulchrone Date: Wed, 24 Sep 2025 20:38:51 -0400 Subject: [PATCH 1/2] crate and mod --- Cargo.toml | 1 + src/lib.rs | 4 ++++ src/pqc/ml_kem.rs | 0 3 files changed, 5 insertions(+) create mode 100644 src/pqc/ml_kem.rs diff --git a/Cargo.toml b/Cargo.toml index fa8d962..dbf1298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ pbkdf2 = "0.12.2" ed25519-dalek = { version = "2", features = ["rand_core"] } hkdf = "0.12.4" chacha20poly1305 = "0.10.1" +ml-kem = "0.2.1" [profile.dev.package.num-bigint-dig] opt-level = 3 diff --git a/src/lib.rs b/src/lib.rs index de843f1..d6099e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,4 +58,8 @@ pub mod compression { pub mod hybrid { pub mod hpke; pub mod cas_hybrid; +} + +pub mod pqc { + pub mod ml_kem; } \ No newline at end of file diff --git a/src/pqc/ml_kem.rs b/src/pqc/ml_kem.rs new file mode 100644 index 0000000..e69de29 From 4a34e5e152894673155a43fc2c22b56715097ee0 Mon Sep 17 00:00:00 2001 From: Mike Mulchrone Date: Wed, 24 Sep 2025 21:21:38 -0400 Subject: [PATCH 2/2] ml-kem implementation --- Cargo.toml | 2 +- src/lib.rs | 1 + src/pqc/cas_pqc.rs | 16 +++++++++++ src/pqc/ml_kem.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++ tests/pqc.rs | 12 +++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/pqc/cas_pqc.rs create mode 100644 tests/pqc.rs diff --git a/Cargo.toml b/Cargo.toml index dbf1298..05304ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cas-lib" -version = "0.2.59" +version = "0.2.60" edition = "2021" description = "Core lib for CAS" license = "Apache-2.0" diff --git a/src/lib.rs b/src/lib.rs index d6099e2..2a096f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,4 +62,5 @@ pub mod hybrid { pub mod pqc { pub mod ml_kem; + pub mod cas_pqc; } \ No newline at end of file diff --git a/src/pqc/cas_pqc.rs b/src/pqc/cas_pqc.rs new file mode 100644 index 0000000..ae8fbb5 --- /dev/null +++ b/src/pqc/cas_pqc.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone)] +pub struct MlKemKeyPair { + pub secret_key: Vec, + pub public_key: Vec, +} + +#[derive(Debug, Clone)] +pub struct MlKemEncapResult { + pub ciphertext: Vec, + pub shared_secret: Vec, +} + +#[derive(Debug, Clone)] +pub struct MlKemSharedSecret { + pub shared_secret: Vec, +} \ No newline at end of file diff --git a/src/pqc/ml_kem.rs b/src/pqc/ml_kem.rs index e69de29..72f6102 100644 --- a/src/pqc/ml_kem.rs +++ b/src/pqc/ml_kem.rs @@ -0,0 +1,66 @@ +use ml_kem::kem::{DecapsulationKey, EncapsulationKey, Encapsulate, Decapsulate}; +use ml_kem::*; +use rand::rngs::OsRng; + +use crate::pqc::cas_pqc::{MlKemEncapResult, MlKemKeyPair}; + +/// ML-KEM-1024 (Kyber-1024) byte lengths (public API sanity checks) +const MLKEM1024_PUBLIC_KEY_LEN: usize = 1568; +const MLKEM1024_SECRET_KEY_LEN: usize = 3168; +const MLKEM1024_CIPHERTEXT_LEN: usize = 1568; + +#[derive(Debug)] +pub enum MlKemError { + BadPublicKeyLength, + BadSecretKeyLength, + BadCiphertextLength, + DecodeFailed, +} + +pub type MlKemResult = Result; + +/// Generate (secret/decapsulation key, public/encapsulation key) +pub fn ml_kem_1024_generate() -> MlKemKeyPair { + let mut rng = OsRng; + let (dk, ek) = MlKem1024::generate(&mut rng); + MlKemKeyPair { + secret_key: dk.as_bytes().to_vec(), + public_key: ek.as_bytes().to_vec(), + } +} + +/// Encapsulate to a public key -> (ciphertext, shared_secret) +pub fn ml_kem_1024_encapsulate(public_key: Vec) -> MlKemResult { + if public_key.len() != MLKEM1024_PUBLIC_KEY_LEN { + return Err(MlKemError::BadPublicKeyLength); + } + let ek_bytes: Encoded> = + public_key.as_slice().try_into().map_err(|_| MlKemError::DecodeFailed)?; + let ek = EncapsulationKey::::from_bytes(&ek_bytes); + let mut rng = OsRng; + let (ct, ss) = ek.encapsulate(&mut rng).map_err(|_| MlKemError::DecodeFailed)?; + Ok(MlKemEncapResult { + ciphertext: ct.as_slice().to_vec(), + shared_secret: ss.as_slice().to_vec(), + }) +} + +/// Decapsulate a ciphertext with the secret key -> shared_secret +pub fn ml_kem_1024_decapsulate(secret_key: Vec, ciphertext: Vec) -> MlKemResult> { + if secret_key.len() != MLKEM1024_SECRET_KEY_LEN { + return Err(MlKemError::BadSecretKeyLength); + } + if ciphertext.len() != MLKEM1024_CIPHERTEXT_LEN { + return Err(MlKemError::BadCiphertextLength); + } + + let dk_bytes: Encoded> = + secret_key.as_slice().try_into().map_err(|_| MlKemError::DecodeFailed)?; + let dk = DecapsulationKey::::from_bytes(&dk_bytes); + + let ct: Ciphertext = + ciphertext.as_slice().try_into().map_err(|_| MlKemError::DecodeFailed)?; + + let ss = dk.decapsulate(&ct).map_err(|_| MlKemError::DecodeFailed)?; + Ok(ss.to_vec()) +} \ No newline at end of file diff --git a/tests/pqc.rs b/tests/pqc.rs new file mode 100644 index 0000000..fb6a523 --- /dev/null +++ b/tests/pqc.rs @@ -0,0 +1,12 @@ +#[cfg(test)] +mod pqc { + use cas_lib::pqc::{cas_pqc::MlKemKeyPair, ml_kem::{ml_kem_1024_decapsulate, ml_kem_1024_encapsulate, ml_kem_1024_generate}}; + + #[test] + pub fn round_trip_mlkem1024() { + let secret_key_public_key: MlKemKeyPair = ml_kem_1024_generate(); + let ct = ml_kem_1024_encapsulate(secret_key_public_key.public_key).expect("encapsulate failed"); + let ss_receiver = ml_kem_1024_decapsulate(secret_key_public_key.secret_key, ct.ciphertext).expect("decapsulate failed"); + assert_eq!(ss_receiver, ss_receiver); + } +} \ No newline at end of file