diff --git a/.github/workflows/ml-kem.yml b/.github/workflows/ml-kem.yml index f54a2761..24792ed9 100644 --- a/.github/workflows/ml-kem.yml +++ b/.github/workflows/ml-kem.yml @@ -87,6 +87,7 @@ jobs: with: directory: ml-kem timeout: 120 + jobs: 1 cross: needs: set-msrv diff --git a/Cargo.lock b/Cargo.lock index b4707747..3a0f168b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,9 +313,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" +checksum = "a2376ffd6f96d8a9db59875b4b6b0237d6900b5baf64279714e05493ce64828a" dependencies = [ "cmov", "subtle", diff --git a/ml-kem/checkct/.cargo/config.toml b/ml-kem/checkct/.cargo/config.toml index 5f483583..969ef510 100644 --- a/ml-kem/checkct/.cargo/config.toml +++ b/ml-kem/checkct/.cargo/config.toml @@ -1,10 +1,10 @@ [build] target = [ - # TODO(tarcieri): investigate codegen problems on commented out platforms - #"riscv32imac-unknown-none-elf", - #"thumbv6m-none-eabi", + # TODO(tarcieri): full encapsulation exceeds analysis budget on `riscv32` + # "riscv32imac-unknown-none-elf", + "thumbv6m-none-eabi", "thumbv7em-none-eabihf", - #"x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", ] [target.x86_64-unknown-linux-gnu] diff --git a/ml-kem/checkct/Cargo.lock b/ml-kem/checkct/Cargo.lock index e2a759d9..010728fc 100644 --- a/ml-kem/checkct/Cargo.lock +++ b/ml-kem/checkct/Cargo.lock @@ -50,9 +50,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3e9d21d39e6f39ff46e03d5b621424f88ee23d8a1672c6bfc9ccc7f2da4f6a" +checksum = "a2376ffd6f96d8a9db59875b4b6b0237d6900b5baf64279714e05493ce64828a" dependencies = [ "cmov", ] @@ -72,6 +72,7 @@ version = "0.0.0" dependencies = [ "checkct_macros", "ml-kem", + "module-lattice", "rand_core 0.10.1", "rand_core 0.6.4", "rand_core 0.9.5", diff --git a/ml-kem/checkct/driver/Cargo.toml b/ml-kem/checkct/driver/Cargo.toml index 90155e9b..c631c229 100644 --- a/ml-kem/checkct/driver/Cargo.toml +++ b/ml-kem/checkct/driver/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] checkct_macros = { path = "../checkct_macros" } +module-lattice = { path = "../../../module-lattice", default-features = false } ml-kem = { path = "../..", default-features = false } # PrivateRng and PublicRng implement the traits of all these rand_core versions, # so that they can be passed to libraries depending on any of them. diff --git a/ml-kem/checkct/driver/src/driver.rs b/ml-kem/checkct/driver/src/driver.rs index 88fec944..cfe860ec 100644 --- a/ml-kem/checkct/driver/src/driver.rs +++ b/ml-kem/checkct/driver/src/driver.rs @@ -1,14 +1,42 @@ use crate::rng::{PrivateRng, PublicRng}; use checkct_macros::checkct; use core::hint::black_box; -use ml_kem::{B32, EncapsulationKey, MlKem512}; +use ml_kem::{B32, EncapsulationKey}; + +macro_rules! check_encapsulation { + ($name:ident, $kem:ident, $key_len:expr) => { + #[checkct] + pub fn $name() { + // The generated BINSEC script references both RNG hooks. + black_box(PublicRng.next_u32()); + + // One accepted public key per parameter set. Keeping rho concrete + // avoids symbolic SHAKE/rejection sampling during public-key expansion. + let ek = EncapsulationKey::::new(&[0u8; $key_len].into()).unwrap(); + let mut m = B32::default(); + PrivateRng.fill_bytes(&mut m); + black_box(ek.encapsulate_deterministic(&m)); + } + }; +} + +check_encapsulation!(encapsulate_deterministic, MlKem512, 800); +check_encapsulation!(encapsulate_768, MlKem768, 1184); +check_encapsulation!(encapsulate_1024, MlKem1024, 1568); + +module_lattice::define_field!(KyberField, u16, u32, u64, 3329); +module_lattice::define_field!(DilithiumField, u32, u64, u128, 8_380_417); + +#[checkct] +pub fn small_reduce_u16() { + use module_lattice::Field; + black_box(PublicRng.next_u32()); + black_box(KyberField::small_reduce(PrivateRng.next_u32() as u16)); +} #[checkct] -pub fn encapsulate_deterministic() { - // The generated BINSEC script references both RNG hooks unconditionally. +pub fn small_reduce_u32() { + use module_lattice::Field; black_box(PublicRng.next_u32()); - let ek = EncapsulationKey::::new(&[0u8; 800].into()).unwrap(); - let mut m = B32::default(); - PrivateRng.fill_bytes(&mut m); - black_box(ek.encapsulate_deterministic(&m)); + black_box(DilithiumField::small_reduce(PrivateRng.next_u32())); } diff --git a/module-lattice/Cargo.toml b/module-lattice/Cargo.toml index 22dd78c8..08339418 100644 --- a/module-lattice/Cargo.toml +++ b/module-lattice/Cargo.toml @@ -16,11 +16,11 @@ categories = ["cryptography", "no-std"] keywords = ["crypto", "kyber", "lattice", "post-quantum"] [dependencies] -array = { version = "0.4.8", package = "hybrid-array", features = ["extra-sizes"] } +array = { version = "0.4.8", package = "hybrid-array", features = ["ctutils", "extra-sizes"] } +ctutils = "0.4.7" num-traits = { version = "0.2", default-features = false } # optional dependencies -ctutils = { version = "0.4.5", optional = true } zeroize = { version = "1.8.1", optional = true, default-features = false } [dev-dependencies] @@ -28,7 +28,7 @@ getrandom = { version = "0.4", features = ["sys_rng"] } [features] alloc = [] -ctutils = ["dep:ctutils", "array/ctutils"] +ctutils = [] # TODO(tarcieri): remove in next breaking release zeroize = ["array/zeroize", "dep:zeroize"] [lints] diff --git a/module-lattice/src/algebra.rs b/module-lattice/src/algebra.rs index 113eb72b..f088842e 100644 --- a/module-lattice/src/algebra.rs +++ b/module-lattice/src/algebra.rs @@ -72,13 +72,8 @@ macro_rules! define_field { const BARRETT_MULTIPLIER: Self::LongLong = (1 << Self::BARRETT_SHIFT) / Self::QLL; fn small_reduce(x: Self::Int) -> Self::Int { - // Branchless conditional subtraction: if x >= Q, subtract Q; else - // leave x alone. Compilers already emit `csel` here at O2, but the - // explicit mask form removes the dependency on optimizer choices - // and keeps the generated assembly free of secret-dependent control - // flow at every optimization level. - let mask = ((x >= Self::Q) as $int).wrapping_neg(); - x - (Self::Q & mask) + use $crate::ctutils::{CtLt, CtSelect}; + x - Self::Q.ct_select(&0, x.ct_lt(&Self::Q)) } fn barrett_reduce(x: Self::Long) -> Self::Int { diff --git a/module-lattice/src/lib.rs b/module-lattice/src/lib.rs index 0e3203b1..46a8b4ed 100644 --- a/module-lattice/src/lib.rs +++ b/module-lattice/src/lib.rs @@ -32,5 +32,4 @@ pub use encoding::{ pub use maybe_box::MaybeBox; pub use truncate::Truncate; -#[cfg(feature = "ctutils")] pub use ctutils; diff --git a/module-lattice/tests/algebra.rs b/module-lattice/tests/algebra.rs index 3b6e929b..dacf891f 100644 --- a/module-lattice/tests/algebra.rs +++ b/module-lattice/tests/algebra.rs @@ -14,9 +14,19 @@ module_lattice::define_field!(DilithiumField, u32, u64, u128, 8_380_417); #[test] fn small_reduce() { + for x in 0..=u16::MAX { + let expected = if x >= 3329 { x - 3329 } else { x }; + assert_eq!(KyberField::small_reduce(x), expected); + } + assert_eq!(KyberField::small_reduce(3328), 3328); assert_eq!(KyberField::small_reduce(3329), 0); + for x in [0, 1, 8_380_416, 8_380_417, 16_760_833, u32::MAX] { + let expected = if x >= 8_380_417 { x - 8_380_417 } else { x }; + assert_eq!(DilithiumField::small_reduce(x), expected); + } + assert_eq!(DilithiumField::small_reduce(8_380_416), 8_380_416); assert_eq!(DilithiumField::small_reduce(8_380_417), 0); }