From 4e850ef07bcc534bee094483d466727b774de694 Mon Sep 17 00:00:00 2001 From: eschorn1 Date: Fri, 16 Feb 2024 16:11:01 -0600 Subject: [PATCH] fuzz up --- Cargo.toml | 2 +- fuzz/Cargo.toml | 5 ++ fuzz/README.md | 2 +- fuzz/fuzz_targets/fuzz_all.rs | 86 +++++++++++++++++++++++++++++++---- src/byte_fns.rs | 22 ++++----- src/helpers.rs | 4 +- src/k_pke.rs | 20 ++++---- src/ml_kem.rs | 21 +++++---- src/ntt.rs | 29 +++++++----- src/sampling.rs | 16 ++++--- 10 files changed, 146 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1999ce7..4e4b9ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ rand = "0.8.5" regex = "1.10.2" hex = "0.4.3" rand_chacha = "0.3.1" -criterion = "0.4.0" #"0.5.1" +criterion = "0.4.0" # Needed to keep MSRV back at 1.72, newer: "0.5.1" flate2 = "1.0.27" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 8151e1c..e95ab9a 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,10 +9,15 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" +rand_core = "0.6.4" [dependencies.fips203] path = ".." +[dev-dependencies] +rand = "0.8.5" + + # Prevent this from interfering with workspaces [workspace] members = ["."] diff --git a/fuzz/README.md b/fuzz/README.md index 0f4daed..872bd56 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -3,6 +3,6 @@ Fuzzing https://rust-fuzz.github.io/book/cargo-fuzz.html cd fuzz rustup default nightly - head -c 3200 corpus/seed1 + head -c 3328 corpus/fuzz_all/seed1 cargo fuzz run fuzz_all -j 4 ~~~ diff --git a/fuzz/fuzz_targets/fuzz_all.rs b/fuzz/fuzz_targets/fuzz_all.rs index 5de68d6..6e215a6 100644 --- a/fuzz/fuzz_targets/fuzz_all.rs +++ b/fuzz/fuzz_targets/fuzz_all.rs @@ -1,18 +1,84 @@ #![no_main] -// rustup default nightly -// head -c 3200 seed1 -// cargo fuzz run fuzz_all -j 4 use fips203::ml_kem_512; -use fips203::traits::{Decaps, Encaps, SerDes}; +use fips203::traits::{Decaps, Encaps, KeyGen, SerDes}; use libfuzzer_sys::fuzz_target; +use rand_core::{CryptoRng, RngCore}; -fuzz_target!(|data: [u8; 1632+800+768]| { // dk_len + ek+len + ct_len = 3200 +const RND_SIZE: usize = 32; + + +struct TestRng { + data: Vec>, +} + +impl RngCore for TestRng { + fn next_u32(&mut self) -> u32 { unimplemented!() } + + fn next_u64(&mut self) -> u64 { unimplemented!() } + + fn fill_bytes(&mut self, out: &mut [u8]) { + let x = self.data.pop().expect("TestRng problem"); + out.copy_from_slice(&x) + } + + fn try_fill_bytes(&mut self, out: &mut [u8]) -> Result<(), rand_core::Error> { + self.fill_bytes(out); + Ok(()) // panic on probs is OK + } +} + +impl CryptoRng for TestRng {} + +impl TestRng { + fn new() -> Self { TestRng { data: Vec::new() } } + + fn push(&mut self, new_data: &[u8]) { + let x = new_data.to_vec(); + self.data.push(x); + } +} + +fuzz_target!(|data: [u8; 3328]| { + + let mut rng = TestRng::new(); + let mut start = 0; + rng.push(&data[start..start+RND_SIZE]); + start += RND_SIZE; + rng.push(&data[start..start+RND_SIZE]); + start += RND_SIZE; + let keypair = ml_kem_512::KG::try_keygen_with_rng_vt(&mut rng); // consumes 2 rng values + let (ek1, dk1) = keypair.unwrap(); // only rng can fail, which it won't + + let ek2_bytes = &data[start..start+ml_kem_512::EK_LEN]; + start += ml_kem_512::EK_LEN; + let ek2 = ml_kem_512::EncapsKey::try_from_bytes(ek2_bytes.try_into().unwrap()); + + rng.push(&data[start..start+RND_SIZE]); + start += RND_SIZE; + rng.push(&data[start..start+RND_SIZE]); + start += RND_SIZE; + + if ek2.is_ok() { + let _res = ek2.unwrap().try_encaps_with_rng_vt(&mut rng); + } + let _res = ek1.try_encaps_with_rng_vt(&mut rng); + + + let dk2_bytes = &data[start..start+ml_kem_512::DK_LEN]; + start += ml_kem_512::DK_LEN; + let dk2 = ml_kem_512::DecapsKey::try_from_bytes(dk2_bytes.try_into().unwrap()); + + let ct_bytes = &data[start..start+ml_kem_512::CT_LEN]; + start += ml_kem_512::CT_LEN; + let ct = ml_kem_512::CipherText::try_from_bytes(ct_bytes.try_into().unwrap()).unwrap(); // always good + + if dk2.is_ok() { + let _res = dk2.unwrap().try_decaps_vt(&ct); + } + let _res = dk1.try_decaps_vt(&ct); + + assert_eq!(start, data.len()); // this doesn't appear to trigger (even when wrong) - let ek = ml_kem_512::EncapsKey::try_from_bytes(data[0..800].try_into().unwrap()).unwrap(); - let dk = ml_kem_512::DecapsKey::try_from_bytes(data[800..800+1632].try_into().unwrap()).unwrap(); - let ct = ml_kem_512::CipherText::try_from_bytes(data[800+1632..800+1632+768].try_into().unwrap()).unwrap(); - let _result = ek.try_encaps_vt(); - let _result = dk.try_decaps_vt(&ct); }); diff --git a/src/byte_fns.rs b/src/byte_fns.rs index 155074e..026b046 100644 --- a/src/byte_fns.rs +++ b/src/byte_fns.rs @@ -20,31 +20,31 @@ use crate::types::Z; // /// Output: bit array b ∈ {0,1}^{8·ℓ} -/// Algorithm 4 `ByteEncode(F)` on page 19. +/// Algorithm 4 `ByteEncode_d(F)` on page 19. /// Encodes an array of `d`-bit integers into a byte array, for `1 ≤ d ≤ 12`. -/// This is an optimized variant (which does not use individual bit functions) +/// This is an optimized variant (which does not use individual bit functions). /// -/// Input: integer array `F ∈ Z^256_m`, where `m = 2^d if d < 12` and `m = q if d = 12`
-/// Output: byte array B ∈ B^{32d} +/// Input: integer array `F ∈ Z^{256}_m`, where `m = 2^d if d < 12` and `m = q if d = 12`
+/// Output: byte array B ∈ B^{32·d} pub(crate) fn byte_encode( d: u32, integers_f: &[Z; 256], bytes_b: &mut [u8], ) -> Result<(), &'static str> { // // Our "working" register, from which to drop bytes out of - let mut temp = 0u64; + let mut temp = 0u32; // Bit index of current temp contents, and byte index of current output let mut bit_index = 0; let mut byte_index = 0; // Choose m per spec - let m = if d < 12 { 2u64.pow(d) } else { u64::from(Q) }; + let m = if d < 12 { 2u32.pow(d) } else { Q }; // Work through each of the input integers for coeff in integers_f { // // Get coeff as u64, check magnitude, and clean off top bits - let coeff = u64::from(coeff.get_u16()); + let coeff = coeff.get_u32(); ensure!(coeff <= m, "Alg4: Coeff out of range"); - let coeff = coeff & (2u64.pow(d) - 1); + let coeff = coeff & (2u32.pow(d) - 1); // Drop coeff into the upper unused bit positions of coeff; adjust bit index temp |= coeff << bit_index; @@ -67,11 +67,11 @@ pub(crate) fn byte_encode( } -/// Algorithm 5 `ByteDecode(B)` on page 19. +/// Algorithm 5 `ByteDecode_d(B)` on page 19. /// Decodes a byte array into an array of d-bit integers, for 1 ≤ d ≤ 12. -/// This is an optimized variant (which does not use individual bit functions) +/// This is an optimized variant (which does not use individual bit functions). /// -/// Input: byte array B ∈ B^{32d}
+/// Input: byte array B ∈ B^{32·d}
/// Output: integer array `F ∈ Z^256_m`, where `m = 2^d if d < 12` and `m = q if d = 12` pub(crate) fn byte_decode( d: u32, bytes_b: &[u8], integers_f: &mut [Z; 256], diff --git a/src/helpers.rs b/src/helpers.rs index 1e801e9..32e7e04 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -153,11 +153,11 @@ pub(crate) fn j(bytes: &[&[u8]]) -> [u8; 32] { #[allow(clippy::cast_possible_truncation)] pub(crate) fn compress(d: u32, inout: &mut [Z]) { for x_ref in &mut *inout { - // Barrett constants should be sorted out at compile time + // Barrett constants should be resolved at compile time let q64 = u64::from(Q); let k = 32; let m = 2u64.pow(k) / q64; - // Barrett division/reduction, quotient could be too small by one + // Barrett division, quotient could be too small by one let top = u64::from(x_ref.get_u32()) << d; let quot = (top * m) >> k; let bump = (top - quot * q64) > u64::from(Q); diff --git a/src/k_pke.rs b/src/k_pke.rs index cfa2478..093ed97 100644 --- a/src/k_pke.rs +++ b/src/k_pke.rs @@ -11,8 +11,8 @@ use crate::types::Z; /// Algorithm 12 `K-PKE.KeyGen()` on page 26. /// Generates an encryption key and a corresponding decryption key. /// -/// Output: encryption key `ekPKE ∈ B^{384*k+32}`
-/// Output: decryption key `dkPKE ∈ B^{384*k}` +/// Output: encryption key `ekPKE ∈ B^{384·k+32}`
+/// Output: decryption key `dkPKE ∈ B^{384·k}` #[allow(clippy::similar_names, clippy::module_name_repetitions)] pub fn k_pke_key_gen( rng: &mut impl CryptoRngCore, eta1: u32, ek_pke: &mut [u8], dk_pke: &mut [u8], @@ -115,15 +115,16 @@ pub fn k_pke_key_gen( /// Algorithm 13 `K-PKE.Encrypt(ekPKE , m, r)` on page 27. /// Uses the encryption key to encrypt a plaintext message using the randomness r. +/// +/// Input: encryption key `ekPKE` ∈ `B^{384·k+32}`
+/// Input: message `m` ∈ `B^{32}`
+/// Input: encryption randomness `r` ∈ `B^{32}`
+/// Output: ciphertext `c` ∈ `B^{32(du·k+dv)}`
#[allow(clippy::many_single_char_names, clippy::too_many_arguments)] pub(crate) fn k_pke_encrypt( du: u32, dv: u32, eta1: u32, eta2: u32, ek: &[u8], m: &[u8], randomness: &[u8; 32], ct: &mut [u8], ) -> Result<(), &'static str> { - // Input: encryption key ekPKE ∈ B^{384k+32} - // Input: message m ∈ B^{32} - // Input: encryption randomness r ∈ B^{32} - // Output: ciphertext c ∈ B^{32(du k+dv )} ensure!(ek.len() == 384 * K + 32, "Alg13: ek len not 384 * K + 32"); ensure!(m.len() == 32, "Alg13: m len not 32"); ensure!(eta1 as usize * 64 == ETA1_64, "Alg13: const probs 1"); @@ -233,12 +234,13 @@ pub(crate) fn k_pke_encrypt( du: u32, dv: u32, dk: &[u8], ct: &[u8], ) -> Result<[u8; 32], &'static str> { - // Input: decryption key dk_{PKE} ∈ B^{384*k} - // Input: ciphertext c ∈ B^{32(du*k+dv)} - // Output: message m ∈ B^{32} ensure!(dk.len() == 384 * K, "Alg14: dk len not 384 * K"); ensure!(ct.len() == 32 * (du as usize * K + dv as usize), "Alg14: 32 * (DU * K + DV)"); diff --git a/src/ml_kem.rs b/src/ml_kem.rs index 4ab62b6..e29b333 100644 --- a/src/ml_kem.rs +++ b/src/ml_kem.rs @@ -8,11 +8,12 @@ use crate::types::Z; /// Algorithm 15 `ML-KEM.KeyGen()` on page 29. /// Generates an encapsulation key and a corresponding decapsulation key. +/// +/// Output: Encapsulation key `ek` ∈ `B^{384·k+32}`
+/// Output: Decapsulation key `dk` ∈ `B^{768·k+96}` pub(crate) fn ml_kem_key_gen( rng: &mut impl CryptoRngCore, eta1: u32, ek: &mut [u8], dk: &mut [u8], ) -> Result<(), &'static str> { - // Output: Encapsulation key ek ∈ B^{384k+32} - // Output: Decapsulation key dk ∈ B^{768k+96} ensure!(ek.len() == 384 * K + 32, "Alg15: ek len not 384 * K + 32"); ensure!(dk.len() == 768 * K + 96, "Alg15: dk len not 768 * K + 96"); @@ -40,12 +41,13 @@ pub(crate) fn ml_kem_key_gen( /// Algorithm 16 `ML-KEM.Encaps(ek)` on page 30. /// Uses the encapsulation key to generate a shared key and an associated ciphertext. +/// +/// Validated input: encapsulation key `ek` ∈ `B^{384·k+32}`
+/// Output: shared key `K` ∈ `B^{32}`
+/// Output: ciphertext `c` ∈ `B^{32(du·k+dv)}`
pub(crate) fn ml_kem_encaps( rng: &mut impl CryptoRngCore, du: u32, dv: u32, eta1: u32, eta2: u32, ek: &[u8], ct: &mut [u8], ) -> Result { - // Validated input: encapsulation key ek ∈ B^{384k+32} - // Output: shared key K ∈ B^{32} - // Output: ciphertext c ∈ B^{32(du k+dv)} ensure!(ek.len() == 384 * K + 32, "Alg16: ek len not 384 * K + 32"); // type check: array of length 384k + 32 // modulus check: perform the computation ek ← ByteEncode12 (ByteDecode12(ek_tidle) @@ -60,7 +62,7 @@ pub(crate) fn ml_kem_encaps(); + rng.fill_bytes(&mut m); // 2: (K, r) ← G(m∥H(ek)) ▷ derive shared secret key K and randomness r let h_ek = h(ek); @@ -76,6 +78,10 @@ pub(crate) fn ml_kem_encaps +/// Validated input: decapsulation key `dk` ∈ `B^{768·k+96}`
+/// Output: shared key `K` ∈ `B^{32}` #[allow(clippy::similar_names)] pub(crate) fn ml_kem_decaps< const K: usize, @@ -86,9 +92,6 @@ pub(crate) fn ml_kem_decaps< >( du: u32, dv: u32, eta1: u32, eta2: u32, dk: &[u8], ct: &[u8], ) -> Result { - // Validated input: ciphertext c ∈ B^{32(du k+dv )} - // Validated input: decapsulation key dk ∈ B^{768k+96} - // Output: shared key K ∈ B^{32} // These length checks are a bit redundant...but present for completeness and paranoia ensure!(ct.len() == 32 * (du as usize * K + dv as usize), "Alg17: ct len not 32 * ..."); // Ciphertext type check diff --git a/src/ntt.rs b/src/ntt.rs index f85db7a..aea3209 100644 --- a/src/ntt.rs +++ b/src/ntt.rs @@ -3,11 +3,12 @@ use crate::types::Z; /// Algorithm 8 `NTT(f)` on page 22. /// Computes the NTT representation `f_hat` of the given polynomial f ∈ `R_q`. +/// +/// Input: array `f` ∈ `Z^{256}_q` ▷ the coefficients of the input polynomial
+/// Output: array `f_hat` ∈ `Z^{256}_q` ▷ the coefficients of the NTT of the input polynomial #[must_use] #[allow(clippy::module_name_repetitions)] pub fn ntt(array_f: &[Z; 256]) -> [Z; 256] { - // Input: array f ∈ Z^{256}_q ▷ the coeffcients of the input polynomial - // Output: array f_hat ∈ Z^{256}_q ▷ the coeffcients of the NTT of the input polynomial // 1: f_hat ← f ▷ will compute NTT in-place on a copy of input array let mut f_hat = [Z::default(); 256]; @@ -56,12 +57,13 @@ pub fn ntt(array_f: &[Z; 256]) -> [Z; 256] { /// Algorithm 9 `NTTinv(f)` on page 23. -/// Computes the polynomial f ∈ `R_q` corresponding to the given NTT representation `f_hat` ∈ `T_q`. +/// Computes the polynomial `f` ∈ `R_q` corresponding to the given NTT representation `f_hat` ∈ `T_q`. +/// +/// Input: array `f_hat` ∈ `Z^{256}` ▷ the coefficients of input NTT representation
+/// Output: array `f` ∈ `Z^{256}` ▷ the coefficients of the inverse-NTT of the input #[must_use] #[allow(clippy::module_name_repetitions)] pub fn ntt_inv(f_hat: &[Z; 256]) -> [Z; 256] { - // Input: array f_hat ∈ Z^{256} ▷ the coeffcients of input NTT representation - // Output: array f ∈ Z^{256} ▷ the coeffcients of the inverse-NTT of the input // 1: f ← f_hat ▷ will compute in-place on a copy of input array let mut f: [Z; 256] = [Z::default(); 256]; @@ -115,12 +117,13 @@ pub fn ntt_inv(f_hat: &[Z; 256]) -> [Z; 256] { /// Algorithm 10 `MultiplyNTTs(f, g)` on page 24. -/// Computes the product (in the ring Tq ) of two NTT representations. +/// Computes the product (in the ring `T_q` ) of two NTT representations. +/// +/// Input: Two arrays `f_hat` ∈ `Z^{256}_q` and `g_hat` ∈ `Z^{256}_q` ▷ the coefficients of two NTT representations
+/// Output: An array `h_hat` ∈ `Z^{256}_q` ▷ the coefficients of the product of the inputs #[must_use] #[allow(clippy::cast_possible_truncation)] pub fn multiply_ntts(f_hat: &[Z; 256], g_hat: &[Z; 256]) -> [Z; 256] { - // Input: Two arrays f_hat ∈ Z^{256}_q and g_hat ∈ Z^{256}_q ▷ the coeffcients of two NTT representations - // Output: An array h_hat ∈ Z^{256}_q ▷ the coeffcients of the product of the inputs let mut h_hat: [Z; 256] = [Z::default(); 256]; // for (i ← 0; i < 128; i ++) @@ -144,11 +147,12 @@ pub fn multiply_ntts(f_hat: &[Z; 256], g_hat: &[Z; 256]) -> [Z; 256] { /// Algorithm 11 `BaseCaseMultiply(a0, a1, b0, b1, gamma)` on page 24. /// Computes the product of two degree-one polynomials with respect to a quadratic modulus. +/// +/// Input: `a0`, `a1`, `b0`, `b1` ∈ `Z_q` ▷ the coefficients of `a0` + `a1` X and `b0` + `b1` X +/// Input: `γ` ∈ `Z_q` ▷ the modulus is `X^2 − γ` +/// Output: `c0`, `c1` ∈ `Z_q` ▷ the coefficients of the product of the two polynomials #[must_use] pub fn base_case_multiply(a0: Z, a1: Z, b0: Z, b1: Z, gamma: Z) -> (Z, Z) { - // Input: a0 , a1 , b0 , b1 ∈ Z_q ▷ the coefficients of a0 + a1 X and b0 + b1 X - // Input: γ ∈ Z_q ▷ the modulus is X^2 − γ - // Output: c0 , c1 ∈ Z_q ▷ the coeffcients of the product of the two polynomials // 1: c0 ← a0 · b0 + a1 · b1 · γ ▷ steps 1-2 done modulo q let c0 = a0.mul(b0).add(a1.mul(b1).mul(gamma)); @@ -161,7 +165,7 @@ pub fn base_case_multiply(a0: Z, a1: Z, b0: Z, b1: Z, gamma: Z) -> (Z, Z) { // ---------- -// The functionality below calculates the Zeta array at compile-time. Thus, not particularly optimum or CT. +// The functionality below calculates the Zeta array at compile-time. Thus, not particularly optimal or CT. /// HAC Algorithm 14.76 Right-to-left binary exponentiation mod Q. @@ -199,6 +203,7 @@ const fn gen_zeta_table() -> [u16; 256] { pub(crate) static ZETA_TABLE: [u16; 256] = gen_zeta_table(); + #[cfg(test)] mod tests { use crate::ntt::{gen_zeta_table, pow_mod_q}; diff --git a/src/sampling.rs b/src/sampling.rs index b863501..b4c4001 100644 --- a/src/sampling.rs +++ b/src/sampling.rs @@ -5,10 +5,12 @@ use crate::types::Z; /// Algorithm 6 `SampleNTT(B)` on page 20. /// If the input is a stream of uniformly random bytes, the output is a uniformly random element of `T_q`. +/// +/// Input: byte stream B ∈ B^{∗}
+/// Output: array `a_hat` ∈ `Z^{256}_q` ▷ the coefficients of the NTT of a polynomial #[must_use] pub fn sample_ntt(mut byte_stream_b: impl XofReader) -> [Z; 256] { - // Input: byte stream B ∈ B^{∗} - // Output: array a_hat ∈ Z^{256}_q ▷ the coeffcients of the NTT of a polynomial + // let mut array_a_hat = [Z::default(); 256]; let mut bbb = [0u8; 3]; // Space for 3 random (byte) draws @@ -66,11 +68,12 @@ pub fn sample_ntt(mut byte_stream_b: impl XofReader) -> [Z; 256] { /// Algorithm 7 `SamplePolyCBDη(B)` on page 20. -/// If the input is a stream of uniformly random bytes, outputs a sample from the distribution Dη (Rq ).
+/// If the input is a stream of uniformly random bytes, outputs a sample from the distribution `D_η(R_q)`.
/// This function is an optimized version that avoids the `BytesToBits` function (algorithm 3). /// -/// Input: byte array B ∈ B^{64η}
+/// Input: byte array B ∈ B^{64·η}
/// Output: array f ∈ `Z^{256}_q` +#[must_use] pub fn sample_poly_cbd(eta: u32, byte_array_b: &[u8]) -> [Z; 256] { let mut array_f: [Z; 256] = [Z::default(); 256]; let mut temp = 0; @@ -97,11 +100,12 @@ pub fn sample_poly_cbd(eta: u32, byte_array_b: &[u8]) -> [Z; 256] { array_f } + // The original pseudocode for Algorithm 7 follows... // Algorithm 7 `SamplePolyCBDη(B)` on page 20. -// If the input is a stream of uniformly random bytes, outputs a sample from the distribution Dη (Rq ). +// If the input is a stream of uniformly random bytes, outputs a sample from the distribution `D_η(R_q)`. // -// Input: byte array B ∈ B^{64η} +// Input: byte array B ∈ B^{64·η} // Output: array f ∈ Z^{256}_q // 1: b ← BytesToBits(B) // 2: for (i ← 0; i < 256; i ++)