From eda4b09334f784c04e911e96719c806ab03f2948 Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Thu, 21 Aug 2025 11:46:19 +0100 Subject: [PATCH 1/5] Add naive implementation of K-PKE, foundation of ML-KEM --- src/construction/pk_encryption.rs | 2 + src/construction/pk_encryption/k_pke.rs | 305 ++++++++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 src/construction/pk_encryption/k_pke.rs diff --git a/src/construction/pk_encryption.rs b/src/construction/pk_encryption.rs index db93c44..9557c41 100644 --- a/src/construction/pk_encryption.rs +++ b/src/construction/pk_encryption.rs @@ -36,6 +36,7 @@ mod ccs_from_ibe; mod dual_regev; mod dual_regev_discrete_gauss; +mod k_pke; mod lpr; mod regev; mod regev_discrete_gauss; @@ -44,6 +45,7 @@ mod ring_lpr; pub use ccs_from_ibe::CCSfromIBE; pub use dual_regev::DualRegev; pub use dual_regev_discrete_gauss::DualRegevWithDiscreteGaussianRegularity; +pub use k_pke::KPKE; pub use lpr::LPR; use qfall_math::integer::Z; pub use regev::Regev; diff --git a/src/construction/pk_encryption/k_pke.rs b/src/construction/pk_encryption/k_pke.rs new file mode 100644 index 0000000..322d1cb --- /dev/null +++ b/src/construction/pk_encryption/k_pke.rs @@ -0,0 +1,305 @@ +// Copyright © 2025 Niklas Siemer +// +// This file is part of qFALL-crypto. +// +// qFALL-crypto is free software: you can redistribute it and/or modify it under +// the terms of the Mozilla Public License Version 2.0 as published by the +// Mozilla Foundation. See . + +//! This module contains a naive implementation of the K-PKE scheme +//! used as foundation for ML-KEM. +//! +//! **WARNING:** This implementation is a toy implementation of the basics below +//! ML-KEM and mostly supposed to showcase the prototyping capabilities of the `qfall`-library. + +use crate::{ + construction::pk_encryption::PKEncryptionScheme, utils::common_moduli::new_anticyclic, +}; +use qfall_math::{ + integer::{PolyOverZ, Z}, + integer_mod_q::{MatPolynomialRingZq, ModulusPolynomialRingZq, PolynomialRingZq, Zq}, + traits::{Distance, GetCoefficient, SetCoefficient}, +}; +use serde::{Deserialize, Serialize}; + +/// This is a naive toy-implementation of the [`PKEncryptionScheme`] used +/// as a basis for ML-KEM. +/// +/// This implementation is not supposed to be an implementation of the FIPS 203 standard, but +/// is supposed to showcase the prototyping capabilities of `qfall` and does not cover compression algorithms +/// as specified in the FIPS 203 document or might deviate for the choice of matrix multiplication algorithms. +/// +/// Attributes: +/// - `q`: defines the modulus polynomial `(X^n + 1) mod p` +/// - `k`: defines the width and height of matrix `A` +/// - `eta_1`: defines that vectors `s`, `e`, and `y` are sampled according to Bin(eta_1, 1/2) centered around 0 +/// - `eta_2`: defines that vector `e_1` and `e_2` are sampled according to Bin(eta_2, 1/2) centered around 0 +/// +/// # Examples +/// ``` +/// use qfall_crypto::construction::pk_encryption::{KPKE, PKEncryptionScheme}; +/// +/// // setup public parameters +/// let k_pke = KPKE::ml_kem_512(); +/// +/// // generate (pk, sk) pair +/// let (pk, sk) = k_pke.gen(); +/// +/// // encrypt a message +/// let msg = 250; +/// let cipher = k_pke.enc(&pk, &msg); +/// +/// // decrypt the ciphertext +/// let m = k_pke.dec(&sk, &cipher); +/// +/// assert_eq!(msg, m); +/// ``` +#[derive(Debug, Serialize, Deserialize)] +pub struct KPKE { + q: ModulusPolynomialRingZq, // modulus (X^n + 1) mod q + k: i64, // defines both dimensions of matrix A + eta_1: i64, // defines the binomial distribution of the secret and error drawn in `gen` + eta_2: i64, // defines the binomial distribution of the error drawn in `enc` +} + +impl KPKE { + /// Turns a [`Z`] instance into its bit representation, converts this bit representation + /// into a [`PolynomialRingZq`] with entries q/2 for any 1-bit and 0 as coefficient for any 0-bit. + fn encode_z_bitwise_in_polynomialringzq(&self, mu: &Z) -> PolynomialRingZq { + let bits = mu.to_bits(); + let mut mu_q_half = PolynomialRingZq::from((PolyOverZ::default(), &self.q)); + let q_half = self.q.get_q().div_floor(2); + for (i, bit) in bits.iter().enumerate() { + if *bit { + mu_q_half.set_coeff(i, &q_half).unwrap(); + } + } + + mu_q_half + } + + /// Checks for each coefficient of `poly` whether its value is closer to q/2 or 0 + /// and sets the corresponding bit in the returned [`Z`] value to 1 or 0 respectively. + fn decode_z_bitwise_from_polynomialringzq(&self, poly: PolynomialRingZq) -> Z { + let q_half = self.q.get_q().div_floor(2); + + // check for each coefficient whether it's closer to 0 or q/2 + // if closer to q/2 -> add 2^i to result + let mut vec = vec![]; + for i in 0..self.q.get_degree() { + let coeff: Zq = poly.get_coeff(i).unwrap(); + let coeff: Z = coeff.get_representative_least_absolute_residue(); + + if coeff.distance(&q_half) < coeff.distance(Z::ZERO) { + vec.push(true); + } else { + vec.push(false); + } + } + + Z::from_bits(&vec) + } + + /// Returns a [`KPKE`] instance with public parameters according to the ML-KEM-512 specification. + pub fn ml_kem_512() -> Self { + let q = new_anticyclic(256, 3329).unwrap(); + Self { + q, + k: 2, + eta_1: 3, + eta_2: 2, + } + } + + /// Returns a [`KPKE`] instance with public parameters according to the ML-KEM-768 specification. + pub fn ml_kem_768() -> Self { + let q = new_anticyclic(256, 3329).unwrap(); + Self { + q, + k: 3, + eta_1: 2, + eta_2: 2, + } + } + + /// Returns a [`KPKE`] instance with public parameters according to the ML-KEM-1024 specification. + pub fn ml_kem_1024() -> Self { + let q = new_anticyclic(256, 3329).unwrap(); + Self { + q, + k: 4, + eta_1: 2, + eta_2: 2, + } + } +} + +impl PKEncryptionScheme for KPKE { + type PublicKey = (MatPolynomialRingZq, MatPolynomialRingZq); + type SecretKey = MatPolynomialRingZq; + type Cipher = (MatPolynomialRingZq, PolynomialRingZq); + + /// Generates a `(pk, sk)` pair by following these steps: + /// - A <- R_q^{k x k} + /// - s <- Bin(eta_1, 0.5)^k centered around 0 + /// - e <- Bin(eta_1, 0.5)^k centered around 0 + /// - t = A * s + e + /// + /// Then, `pk = (A, t)` and `sk = s` are returned. + /// + /// # Examples + /// ``` + /// use qfall_crypto::construction::pk_encryption::{PKEncryptionScheme, KPKE}; + /// let k_pke = KPKE::ml_kem_512(); + /// + /// let (pk, sk) = k_pke.gen(); + /// ``` + fn gen(&self) -> (Self::PublicKey, Self::SecretKey) { + // 5 𝐀[𝑖,𝑗] ← SampleNTT(𝜌‖𝑗‖𝑖) + let mat_a = MatPolynomialRingZq::sample_uniform(self.k, self.k, &self.q); + // 9 𝐬[𝑖] ← SamplePolyCBD_𝜂_1(PRF_𝜂_1 (𝜎, 𝑁)) + let vec_s = MatPolynomialRingZq::sample_binomial_with_offset( + self.k, + 1, + &self.q, + -self.eta_1, + 2 * self.eta_1, + 0.5, + ) + .unwrap(); + // 13 𝐞[𝑖] ← SamplePolyCBD_𝜂_1(PRF_𝜂_1 (𝜎, 𝑁)) + let vec_e = MatPolynomialRingZq::sample_binomial_with_offset( + self.k, + 1, + &self.q, + -self.eta_1, + 2 * self.eta_1, + 0.5, + ) + .unwrap(); + + // 18 𝐭 ← 𝐀 ∘ 𝐬 + 𝐞 + let vec_t = &mat_a * &vec_s + vec_e; + + let pk = (mat_a, vec_t); + let sk = vec_s; + (pk, sk) + } + + /// Encrypts a `message` with the provided public key by following these steps: + /// - y <- Bin(eta_1, 0.5)^k centered around 0 + /// - e_1 <- Bin(eta_2, 0.5)^k centered around 0 + /// - e_2 <- Bin(eta_2, 0.5) centered around 0 + /// - u = A^T * y + e_1 + /// - v = t^T * y + e_2 + 𝜇, where 𝜇 is the {q/2, 0} encoding of the bits of `message` + /// + /// Then, ciphertext `(u, v)` is returned. + /// + /// Parameters: + /// - `pk`: specifies the public key `pk = (A, t)` + /// - `message`: specifies the message that should be encrypted, which should not extend 256 bits (and be positive) + /// + /// Returns a ciphertext `(u, v)` of type [`MatPolynomialRingZq`] and [`PolynomialRingZq`]. + /// + /// # Examples + /// ``` + /// use qfall_crypto::construction::pk_encryption::{PKEncryptionScheme, KPKE}; + /// let k_pke = KPKE::ml_kem_512(); + /// let (pk, sk) = k_pke.gen(); + /// + /// let c = k_pke.enc(&pk, 1); + /// ``` + fn enc(&self, pk: &Self::PublicKey, message: impl Into) -> Self::Cipher { + // 10 𝐲[𝑖] ← SamplePolyCBD_𝜂_1(PRF_𝜂_1 (𝑟, 𝑁)) + let vec_y = MatPolynomialRingZq::sample_binomial_with_offset( + self.k, + 1, + &self.q, + -self.eta_1, + 2 * self.eta_1, + 0.5, + ) + .unwrap(); + // 𝐞_𝟏[𝑖] ← SamplePolyCBD_𝜂_2(PRF_𝜂_2 (𝑟, 𝑁)) + let vec_e_1 = MatPolynomialRingZq::sample_binomial_with_offset( + self.k, + 1, + &self.q, + -self.eta_2, + 2 * self.eta_2, + 0.5, + ) + .unwrap(); + // 𝑒_2 ← SamplePolyCBD_𝜂_2(PRF_𝜂_2 (𝑟, 𝑁)) + let e_2 = PolynomialRingZq::sample_binomial_with_offset( + &self.q, + -self.eta_2, + 2 * self.eta_2, + 0.5, + ) + .unwrap(); + + // 19 𝐮 ← NTT^−1(𝐀^⊺ ∘ 𝐲) + 𝐞_𝟏 + let vec_u = pk.0.transpose() * &vec_y + vec_e_1; + + // 20 𝜇 ← Decompress_1(ByteDecode_1(𝑚)) + let mu = self.encode_z_bitwise_in_polynomialringzq(&message.into()); + + // 21 𝑣 ← NTT^−1(𝐭^⊺ ∘ 𝐲) + 𝑒_2 + 𝜇 + let v = pk.1.dot_product(&vec_y).unwrap() + e_2 + mu; + + (vec_u, v) + } + + /// Decrypts the provided `cipher` using the secret key `sk` by following these steps: + /// - w = v - s^T * u + /// - returns the decoding of `w` with 1 and 0 set in the returned [`Z`] instance + /// if the corresponding coefficient was closer to q/2 or 0 respectively + /// + /// Parameters: + /// - `sk`: specifies the secret key `sk = s` + /// - `cipher`: specifies the ciphertext containing `cipher = (u, v)` + /// + /// Returns the decryption of `cipher` as a [`Z`] instance. + /// + /// # Examples + /// ``` + /// use qfall_crypto::construction::pk_encryption::{PKEncryptionScheme, KPKE}; + /// let k_pke = KPKE::ml_kem_512(); + /// let (pk, sk) = k_pke.gen(); + /// let c = k_pke.enc(&pk, 1); + /// + /// let m = k_pke.dec(&sk, &c); + /// + /// assert_eq!(1, m); + /// ``` + fn dec(&self, sk: &Self::SecretKey, cipher: &Self::Cipher) -> Z { + // 6 𝑤 ← 𝑣 − NTT^−1(𝐬^⊺ ∘ NTT(𝐮)) + let w = &cipher.1 - sk.dot_product(&cipher.0).unwrap(); + + // 7 𝑚 ← ByteEncode_1(Compress_1(𝑤)) + self.decode_z_bitwise_from_polynomialringzq(w) + } +} + +#[cfg(test)] +mod test_kpke { + use crate::construction::pk_encryption::{k_pke::KPKE, PKEncryptionScheme}; + + #[test] + fn correctness() { + let k_pkes = [KPKE::ml_kem_512(), KPKE::ml_kem_768(), KPKE::ml_kem_1024()]; + for k_pke in k_pkes { + let messages = [0, 1, 13, 255, 2047, 4294967295_u32]; + + for message in messages { + let (pk, sk) = k_pke.gen(); + let c = k_pke.enc(&pk, message); + let m = k_pke.dec(&sk, &c); + + println!("{m}"); + assert_eq!(message, m); + } + } + } +} From a7a7c435633f7f34345a6db005afeea0cd20fe53 Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Thu, 21 Aug 2025 14:30:02 +0100 Subject: [PATCH 2/5] Move reused encoding to utils --- src/construction/pk_encryption/k_pke.rs | 54 ++++---------------- src/construction/pk_encryption/ring_lpr.rs | 48 ++++-------------- src/utils.rs | 1 + src/utils/common_encodings.rs | 58 ++++++++++++++++++++++ 4 files changed, 81 insertions(+), 80 deletions(-) create mode 100644 src/utils/common_encodings.rs diff --git a/src/construction/pk_encryption/k_pke.rs b/src/construction/pk_encryption/k_pke.rs index 322d1cb..4e56917 100644 --- a/src/construction/pk_encryption/k_pke.rs +++ b/src/construction/pk_encryption/k_pke.rs @@ -13,12 +13,17 @@ //! ML-KEM and mostly supposed to showcase the prototyping capabilities of the `qfall`-library. use crate::{ - construction::pk_encryption::PKEncryptionScheme, utils::common_moduli::new_anticyclic, + construction::pk_encryption::PKEncryptionScheme, + utils::{ + common_encodings::{ + decode_z_bitwise_from_polynomialringzq, encode_z_bitwise_in_polynomialringzq, + }, + common_moduli::new_anticyclic, + }, }; use qfall_math::{ - integer::{PolyOverZ, Z}, - integer_mod_q::{MatPolynomialRingZq, ModulusPolynomialRingZq, PolynomialRingZq, Zq}, - traits::{Distance, GetCoefficient, SetCoefficient}, + integer::Z, + integer_mod_q::{MatPolynomialRingZq, ModulusPolynomialRingZq, PolynomialRingZq}, }; use serde::{Deserialize, Serialize}; @@ -63,43 +68,6 @@ pub struct KPKE { } impl KPKE { - /// Turns a [`Z`] instance into its bit representation, converts this bit representation - /// into a [`PolynomialRingZq`] with entries q/2 for any 1-bit and 0 as coefficient for any 0-bit. - fn encode_z_bitwise_in_polynomialringzq(&self, mu: &Z) -> PolynomialRingZq { - let bits = mu.to_bits(); - let mut mu_q_half = PolynomialRingZq::from((PolyOverZ::default(), &self.q)); - let q_half = self.q.get_q().div_floor(2); - for (i, bit) in bits.iter().enumerate() { - if *bit { - mu_q_half.set_coeff(i, &q_half).unwrap(); - } - } - - mu_q_half - } - - /// Checks for each coefficient of `poly` whether its value is closer to q/2 or 0 - /// and sets the corresponding bit in the returned [`Z`] value to 1 or 0 respectively. - fn decode_z_bitwise_from_polynomialringzq(&self, poly: PolynomialRingZq) -> Z { - let q_half = self.q.get_q().div_floor(2); - - // check for each coefficient whether it's closer to 0 or q/2 - // if closer to q/2 -> add 2^i to result - let mut vec = vec![]; - for i in 0..self.q.get_degree() { - let coeff: Zq = poly.get_coeff(i).unwrap(); - let coeff: Z = coeff.get_representative_least_absolute_residue(); - - if coeff.distance(&q_half) < coeff.distance(Z::ZERO) { - vec.push(true); - } else { - vec.push(false); - } - } - - Z::from_bits(&vec) - } - /// Returns a [`KPKE`] instance with public parameters according to the ML-KEM-512 specification. pub fn ml_kem_512() -> Self { let q = new_anticyclic(256, 3329).unwrap(); @@ -243,7 +211,7 @@ impl PKEncryptionScheme for KPKE { let vec_u = pk.0.transpose() * &vec_y + vec_e_1; // 20 𝜇 ← Decompress_1(ByteDecode_1(𝑚)) - let mu = self.encode_z_bitwise_in_polynomialringzq(&message.into()); + let mu = encode_z_bitwise_in_polynomialringzq(&self.q, &message.into()); // 21 𝑣 ← NTT^−1(𝐭^⊺ ∘ 𝐲) + 𝑒_2 + 𝜇 let v = pk.1.dot_product(&vec_y).unwrap() + e_2 + mu; @@ -278,7 +246,7 @@ impl PKEncryptionScheme for KPKE { let w = &cipher.1 - sk.dot_product(&cipher.0).unwrap(); // 7 𝑚 ← ByteEncode_1(Compress_1(𝑤)) - self.decode_z_bitwise_from_polynomialringzq(w) + decode_z_bitwise_from_polynomialringzq(self.q.get_q(), &w) } } diff --git a/src/construction/pk_encryption/ring_lpr.rs b/src/construction/pk_encryption/ring_lpr.rs index 7e6f10e..1ff38ee 100644 --- a/src/construction/pk_encryption/ring_lpr.rs +++ b/src/construction/pk_encryption/ring_lpr.rs @@ -10,13 +10,18 @@ //! public key Ring-LPR encryption scheme. use super::PKEncryptionScheme; -use crate::utils::common_moduli::new_anticyclic; +use crate::utils::{ + common_encodings::{ + decode_z_bitwise_from_polynomialringzq, encode_z_bitwise_in_polynomialringzq, + }, + common_moduli::new_anticyclic, +}; use qfall_math::{ error::MathError, - integer::{PolyOverZ, Z}, - integer_mod_q::{Modulus, ModulusPolynomialRingZq, PolynomialRingZq, Zq}, + integer::Z, + integer_mod_q::{Modulus, ModulusPolynomialRingZq, PolynomialRingZq}, rational::Q, - traits::{Distance, GetCoefficient, Pow, SetCoefficient}, + traits::Pow, }; use serde::{Deserialize, Serialize}; @@ -312,21 +317,6 @@ impl RingLPR { pub fn secure128() -> Self { Self::new(512, 92897729, 0.000005) } - - /// Turns a [`Z`] instance into its bit representation, converts this bit representation - /// into a [`PolynomialRingZq`] with entries q/2 for any 1-bit and 0 as coefficient for any 0-bit. - fn z_into_polynomialringzq(&self, mu: &Z) -> PolynomialRingZq { - let bits = mu.to_bits(); - let mut mu_q_half = PolynomialRingZq::from((PolyOverZ::default(), &self.q)); - let q_half = self.q.get_q().div_floor(2); - for (i, bit) in bits.iter().enumerate() { - if *bit { - mu_q_half.set_coeff(i, &q_half).unwrap(); - } - } - - mu_q_half - } } impl Default for RingLPR { @@ -424,7 +414,7 @@ impl PKEncryptionScheme for RingLPR { let message: Z = message.into().abs(); let mu = message % Z::from(2).pow(&self.n).unwrap(); // set mu_q_half to polynomial with n {0,1} coefficients - let mu_q_half = self.z_into_polynomialringzq(&mu); + let mu_q_half = encode_z_bitwise_in_polynomialringzq(&self.q, &mu); // r <- χ let r = PolynomialRingZq::sample_discrete_gauss( @@ -490,23 +480,7 @@ impl PKEncryptionScheme for RingLPR { // res = v - s * u let result = &cipher.1 - sk * &cipher.0; - let q_half = self.q.get_q().div_floor(2); - - // check for each coefficient whether it's closer to 0 or q/2 - // if closer to q/2 -> add 2^i to result - let mut vec = vec![]; - for i in 0..self.q.get_degree() { - let coeff: Zq = result.get_coeff(i).unwrap(); - let coeff: Z = coeff.get_representative_least_absolute_residue().abs(); - - if coeff.distance(&q_half) < coeff.distance(Z::ZERO) { - vec.push(true); - } else { - vec.push(false); - } - } - - Z::from_bits(&vec) + decode_z_bitwise_from_polynomialringzq(self.q.get_q(), &result) } } diff --git a/src/utils.rs b/src/utils.rs index d050050..4bfed21 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,5 +10,6 @@ //! //! This can include specialized implementations for certain parameter sets, such as rotation matrices. +pub mod common_encodings; pub mod common_moduli; pub mod rotation_matrix; diff --git a/src/utils/common_encodings.rs b/src/utils/common_encodings.rs new file mode 100644 index 0000000..63c80d8 --- /dev/null +++ b/src/utils/common_encodings.rs @@ -0,0 +1,58 @@ +// Copyright © 2025 Niklas Siemer +// +// This file is part of qFALL-crypto. +// +// qFALL-crypto is free software: you can redistribute it and/or modify it under +// the terms of the Mozilla Public License Version 2.0 as published by the +// Mozilla Foundation. See . + +//! This module contains functions to encode and decode data with commonly used +//! encodings. + +use qfall_math::{ + integer::{PolyOverZ, Z}, + integer_mod_q::{ModulusPolynomialRingZq, PolynomialRingZq, Zq}, + traits::{Distance, GetCoefficient, SetCoefficient}, +}; + +/// Turns a [`Z`] instance into its bit representation, converts this bit representation +/// into a [`PolynomialRingZq`] with entries q/2 for any 1-bit and 0 as coefficient for any 0-bit. +pub fn encode_z_bitwise_in_polynomialringzq( + modulus: &ModulusPolynomialRingZq, + mu: &Z, +) -> PolynomialRingZq { + let modulus: ModulusPolynomialRingZq = modulus.into(); + let q_half = modulus.get_q().div_floor(2); + + let bits = mu.to_bits(); + let mut mu_q_half = PolynomialRingZq::from((PolyOverZ::default(), &modulus)); + for (i, bit) in bits.iter().enumerate() { + if *bit { + mu_q_half.set_coeff(i, &q_half).unwrap(); + } + } + + mu_q_half +} + +/// Checks for each coefficient of `poly` whether its value is closer to q/2 or 0 +/// and sets the corresponding bit in the returned [`Z`] value to 1 or 0 respectively. +pub fn decode_z_bitwise_from_polynomialringzq(modulus: impl Into, poly: &PolynomialRingZq) -> Z { + let q_half = modulus.into().div_floor(2); + + // check for each coefficient whether it's closer to 0 or q/2 + // if closer to q/2 -> add 2^i to result + let mut vec = vec![]; + for i in 0..=poly.get_degree() { + let coeff: Zq = poly.get_coeff(i).unwrap(); + let coeff: Z = coeff.get_representative_least_absolute_residue(); + + if coeff.distance(&q_half) < coeff.distance(Z::ZERO) { + vec.push(true); + } else { + vec.push(false); + } + } + + Z::from_bits(&vec) +} From e794fe38dc11f023f4cbeaccd0f078cf1aa6941a Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Thu, 21 Aug 2025 15:29:46 +0100 Subject: [PATCH 3/5] Add benchmarks --- benches/benchmarks.rs | 3 +- benches/k_pke.rs | 145 ++++++++++++++++++++++++ src/construction/pk_encryption/k_pke.rs | 8 +- 3 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 benches/k_pke.rs diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 080f0b8..c20d019 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -9,7 +9,8 @@ use criterion::criterion_main; +pub mod k_pke; pub mod pfdh; pub mod regev; -criterion_main! {regev::benches, pfdh::benches} +criterion_main! {regev::benches, pfdh::benches, k_pke::benches} diff --git a/benches/k_pke.rs b/benches/k_pke.rs new file mode 100644 index 0000000..e42622d --- /dev/null +++ b/benches/k_pke.rs @@ -0,0 +1,145 @@ +// Copyright © 2025 Niklas Siemer +// +// This file is part of qFALL-crypto. +// +// qFALL-crypto is free software: you can redistribute it and/or modify it under +// the terms of the Mozilla Public License Version 2.0 as published by the +// Mozilla Foundation. See . + +use criterion::*; +use qfall_crypto::construction::pk_encryption::PKEncryptionScheme; +use qfall_crypto::construction::pk_encryption::KPKE; + +/// Performs a full-cycle of gen, enc, dec with [`KPKE`]. +fn kpke_cycle(k_pke: &KPKE) { + let (pk, sk) = k_pke.gen(); + let cipher = k_pke.enc(&pk, 1); + let _ = k_pke.dec(&sk, &cipher); +} + +/// Benchmark [kpke_cycle] with [KPKE::ml_kem_512]. +/// +/// This benchmark can be run with for example: +/// - `cargo criterion K-PKE\ cycle\ 512` +/// - `cargo bench --bench benchmarks K-PKE\ cycle\ 512` +/// - `cargo flamegraph --bench benchmarks -- --bench K-PKE\ cycle\ 512` +fn bench_kpke_cycle_512(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_512(); + + c.bench_function("K-PKE cycle 512", |b| b.iter(|| kpke_cycle(&k_pke))); +} + +/// Benchmark [KPKE::gen] with [KPKE::ml_kem_512]. +fn bench_kpke_gen_512(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_512(); + + c.bench_function("K-PKE gen 512", |b| b.iter(|| k_pke.gen())); +} + +/// Benchmark [KPKE::enc] with [KPKE::ml_kem_512]. +fn bench_kpke_enc_512(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_512(); + let (pk, _) = k_pke.gen(); + let msg = i64::MAX; + + c.bench_function("K-PKE enc 512", |b| b.iter(|| k_pke.enc(&pk, msg))); +} + +/// Benchmark [KPKE::dec] with [KPKE::ml_kem_512]. +fn bench_kpke_dec_512(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_512(); + let (pk, sk) = k_pke.gen(); + let cipher = k_pke.enc(&pk, i64::MAX); + + c.bench_function("K-PKE dec 512", |b| b.iter(|| k_pke.dec(&sk, &cipher))); +} + +/// Benchmark [kpke_cycle] with [KPKE::ml_kem_768]. +/// +/// This benchmark can be run with for example: +/// - `cargo criterion K-PKE\ cycle\ 768` +/// - `cargo bench --bench benchmarks K-PKE\ cycle\ 768` +/// - `cargo flamegraph --bench benchmarks -- --bench K-PKE\ cycle\ 768` +fn bench_kpke_cycle_768(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_768(); + + c.bench_function("K-PKE cycle 768", |b| b.iter(|| kpke_cycle(&k_pke))); +} + +/// Benchmark [KPKE::gen] with [KPKE::ml_kem_768]. +fn bench_kpke_gen_768(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_768(); + + c.bench_function("K-PKE gen 768", |b| b.iter(|| k_pke.gen())); +} + +/// Benchmark [KPKE::enc] with [KPKE::ml_kem_768]. +fn bench_kpke_enc_768(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_768(); + let (pk, _) = k_pke.gen(); + let msg = i64::MAX; + + c.bench_function("K-PKE enc 768", |b| b.iter(|| k_pke.enc(&pk, msg))); +} + +/// Benchmark [KPKE::dec] with [KPKE::ml_kem_768]. +fn bench_kpke_dec_768(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_768(); + let (pk, sk) = k_pke.gen(); + let cipher = k_pke.enc(&pk, i64::MAX); + + c.bench_function("K-PKE dec 768", |b| b.iter(|| k_pke.dec(&sk, &cipher))); +} + +/// Benchmark [kpke_cycle] with [KPKE::ml_kem_1024]. +/// +/// This benchmark can be run with for example: +/// - `cargo criterion K-PKE\ cycle\ 1024` +/// - `cargo bench --bench benchmarks K-PKE\ cycle\ 1024` +/// - `cargo flamegraph --bench benchmarks -- --bench K-PKE\ cycle\ 1024` +fn bench_kpke_cycle_1024(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_1024(); + + c.bench_function("K-PKE cycle 1024", |b| b.iter(|| kpke_cycle(&k_pke))); +} + +/// Benchmark [KPKE::gen] with [KPKE::ml_kem_1024]. +fn bench_kpke_gen_1024(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_1024(); + + c.bench_function("K-PKE gen 1024", |b| b.iter(|| k_pke.gen())); +} + +/// Benchmark [KPKE::enc] with [KPKE::ml_kem_1024]. +fn bench_kpke_enc_1024(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_1024(); + let (pk, _) = k_pke.gen(); + let msg = i64::MAX; + + c.bench_function("K-PKE enc 1024", |b| b.iter(|| k_pke.enc(&pk, msg))); +} + +/// Benchmark [KPKE::dec] with [KPKE::ml_kem_1024]. +fn bench_kpke_dec_1024(c: &mut Criterion) { + let k_pke = KPKE::ml_kem_1024(); + let (pk, sk) = k_pke.gen(); + let cipher = k_pke.enc(&pk, i64::MAX); + + c.bench_function("K-PKE dec 1024", |b| b.iter(|| k_pke.dec(&sk, &cipher))); +} + +criterion_group!( + benches, + bench_kpke_cycle_512, + bench_kpke_gen_512, + bench_kpke_enc_512, + bench_kpke_dec_512, + bench_kpke_cycle_768, + bench_kpke_gen_768, + bench_kpke_enc_768, + bench_kpke_dec_768, + bench_kpke_cycle_1024, + bench_kpke_gen_1024, + bench_kpke_enc_1024, + bench_kpke_dec_1024, +); diff --git a/src/construction/pk_encryption/k_pke.rs b/src/construction/pk_encryption/k_pke.rs index 4e56917..ef790bc 100644 --- a/src/construction/pk_encryption/k_pke.rs +++ b/src/construction/pk_encryption/k_pke.rs @@ -113,7 +113,7 @@ impl PKEncryptionScheme for KPKE { /// - e <- Bin(eta_1, 0.5)^k centered around 0 /// - t = A * s + e /// - /// Then, `pk = (A, t)` and `sk = s` are returned. + /// Then, `pk = (A^T, t)` and `sk = s` are returned. /// /// # Examples /// ``` @@ -149,7 +149,7 @@ impl PKEncryptionScheme for KPKE { // 18 𝐭 ← 𝐀 ∘ 𝐬 + 𝐞 let vec_t = &mat_a * &vec_s + vec_e; - let pk = (mat_a, vec_t); + let pk = (mat_a.transpose(), vec_t); let sk = vec_s; (pk, sk) } @@ -208,7 +208,7 @@ impl PKEncryptionScheme for KPKE { .unwrap(); // 19 𝐮 ← NTT^−1(𝐀^⊺ ∘ 𝐲) + 𝐞_𝟏 - let vec_u = pk.0.transpose() * &vec_y + vec_e_1; + let vec_u = &pk.0 * &vec_y + vec_e_1; // 20 𝜇 ← Decompress_1(ByteDecode_1(𝑚)) let mu = encode_z_bitwise_in_polynomialringzq(&self.q, &message.into()); @@ -254,6 +254,8 @@ impl PKEncryptionScheme for KPKE { mod test_kpke { use crate::construction::pk_encryption::{k_pke::KPKE, PKEncryptionScheme}; + /// Ensures that [`KPKE`] works for all ML-KEM specifications by + /// performing a round trip of several messages. #[test] fn correctness() { let k_pkes = [KPKE::ml_kem_512(), KPKE::ml_kem_768(), KPKE::ml_kem_1024()]; From 51e3698ab5db79fdfc35338c7278bac56fd4afc5 Mon Sep 17 00:00:00 2001 From: Jan Niklas Siemer Date: Tue, 7 Oct 2025 09:22:14 +0100 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Marvin Beckmann --- src/construction/pk_encryption/k_pke.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/construction/pk_encryption/k_pke.rs b/src/construction/pk_encryption/k_pke.rs index ef790bc..681555f 100644 --- a/src/construction/pk_encryption/k_pke.rs +++ b/src/construction/pk_encryption/k_pke.rs @@ -61,7 +61,7 @@ use serde::{Deserialize, Serialize}; /// ``` #[derive(Debug, Serialize, Deserialize)] pub struct KPKE { - q: ModulusPolynomialRingZq, // modulus (X^n + 1) mod q + q: ModulusPolynomialRingZq, // modulus (X^n + 1) mod p k: i64, // defines both dimensions of matrix A eta_1: i64, // defines the binomial distribution of the secret and error drawn in `gen` eta_2: i64, // defines the binomial distribution of the error drawn in `enc` @@ -241,9 +241,9 @@ impl PKEncryptionScheme for KPKE { /// /// assert_eq!(1, m); /// ``` - fn dec(&self, sk: &Self::SecretKey, cipher: &Self::Cipher) -> Z { + fn dec(&self, sk: &Self::SecretKey, (u, v): &Self::Cipher) -> Z { // 6 𝑤 ← 𝑣 − NTT^−1(𝐬^⊺ ∘ NTT(𝐮)) - let w = &cipher.1 - sk.dot_product(&cipher.0).unwrap(); + let w = &v - sk.dot_product(&u).unwrap(); // 7 𝑚 ← ByteEncode_1(Compress_1(𝑤)) decode_z_bitwise_from_polynomialringzq(self.q.get_q(), &w) @@ -267,7 +267,6 @@ mod test_kpke { let c = k_pke.enc(&pk, message); let m = k_pke.dec(&sk, &c); - println!("{m}"); assert_eq!(message, m); } } From 67e812e96893a2b0f9735c4a6c5109c082252ccc Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Tue, 7 Oct 2025 09:34:18 +0100 Subject: [PATCH 5/5] Apply further suggestions from code review --- src/construction/pk_encryption.rs | 4 ++++ src/construction/pk_encryption/k_pke.rs | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/construction/pk_encryption.rs b/src/construction/pk_encryption.rs index 9557c41..acb84bc 100644 --- a/src/construction/pk_encryption.rs +++ b/src/construction/pk_encryption.rs @@ -32,6 +32,10 @@ //! Chosen-ciphertext security from identity-based encryption. //! In: Advances in Cryptology - EUROCRYPT 2004. //! +//! - \[6\] National Institute of Standards and Technology (2024). +//! Module-Lattice-Based Key-Encapsulation Mechanism Standard. +//! Federal Information Processing Standards Publication (FIPS 203). +//! mod ccs_from_ibe; mod dual_regev; diff --git a/src/construction/pk_encryption/k_pke.rs b/src/construction/pk_encryption/k_pke.rs index 681555f..b47ec55 100644 --- a/src/construction/pk_encryption/k_pke.rs +++ b/src/construction/pk_encryption/k_pke.rs @@ -30,9 +30,10 @@ use serde::{Deserialize, Serialize}; /// This is a naive toy-implementation of the [`PKEncryptionScheme`] used /// as a basis for ML-KEM. /// -/// This implementation is not supposed to be an implementation of the FIPS 203 standard, but +/// This implementation is not supposed to be an implementation of the FIPS 203 standard in [\[6\]](), but /// is supposed to showcase the prototyping capabilities of `qfall` and does not cover compression algorithms /// as specified in the FIPS 203 document or might deviate for the choice of matrix multiplication algorithms. +/// Especially, NTT-representation, sampling and multiplication are not part of this prototype. /// /// Attributes: /// - `q`: defines the modulus polynomial `(X^n + 1) mod p` @@ -124,6 +125,7 @@ impl PKEncryptionScheme for KPKE { /// ``` fn gen(&self) -> (Self::PublicKey, Self::SecretKey) { // 5 𝐀[𝑖,𝑗] ← SampleNTT(𝜌‖𝑗‖𝑖) + // Reminder: NTT-representation, sampling and multiplication are not part of this prototype let mat_a = MatPolynomialRingZq::sample_uniform(self.k, self.k, &self.q); // 9 𝐬[𝑖] ← SamplePolyCBD_𝜂_1(PRF_𝜂_1 (𝜎, 𝑁)) let vec_s = MatPolynomialRingZq::sample_binomial_with_offset( @@ -243,7 +245,7 @@ impl PKEncryptionScheme for KPKE { /// ``` fn dec(&self, sk: &Self::SecretKey, (u, v): &Self::Cipher) -> Z { // 6 𝑤 ← 𝑣 − NTT^−1(𝐬^⊺ ∘ NTT(𝐮)) - let w = &v - sk.dot_product(&u).unwrap(); + let w = v - sk.dot_product(u).unwrap(); // 7 𝑚 ← ByteEncode_1(Compress_1(𝑤)) decode_z_bitwise_from_polynomialringzq(self.q.get_q(), &w)