From abcf6c753a7d572b911db12ec322e303509fa069 Mon Sep 17 00:00:00 2001 From: Alan Szepieniec Date: Wed, 14 Feb 2024 21:44:12 +0100 Subject: [PATCH] add proptests for (de)compress --- falcon-rust/Cargo.toml | 10 ++ falcon-rust/proptest-regressions/encoding.txt | 7 ++ falcon-rust/src/encoding.rs | 96 ++++++++++++------- falcon-rust/src/falcon.rs | 6 +- 4 files changed, 83 insertions(+), 36 deletions(-) create mode 100644 falcon-rust/proptest-regressions/encoding.txt diff --git a/falcon-rust/Cargo.toml b/falcon-rust/Cargo.toml index eab0a27..5d6330e 100644 --- a/falcon-rust/Cargo.toml +++ b/falcon-rust/Cargo.toml @@ -23,3 +23,13 @@ hex = "0.4.3" sha3 = "0.10.8" num-complex = "0.4.4" num = "0.4.1" + +[dev-dependencies] +proptest = "1.4.0" +proptest-derive = "0.4.0" + +[profile.test.package.proptest] +opt-level = 3 + +[profile.test.package.rand_chacha] +opt-level = 3 diff --git a/falcon-rust/proptest-regressions/encoding.txt b/falcon-rust/proptest-regressions/encoding.txt new file mode 100644 index 0000000..f18e725 --- /dev/null +++ b/falcon-rust/proptest-regressions/encoding.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc af9d7e8e65decd95ddaced228a64a43c10b9aaa96a429adaa3576ead86ad186c # shrinks to v = [] diff --git a/falcon-rust/src/encoding.rs b/falcon-rust/src/encoding.rs index 28ea4f8..462d12c 100644 --- a/falcon-rust/src/encoding.rs +++ b/falcon-rust/src/encoding.rs @@ -34,7 +34,7 @@ pub(crate) fn compress_slow(v: &[i16], slen: usize) -> Option> { Some(bitvector.to_bytes()) } -/// Take as input a list of integers v and a bytelength slen, and +/// Take as input a list of integers v and a byte length slen, and /// return a bytestring of length slen that encode/compress v. /// If this is not possible, return False. /// @@ -52,17 +52,23 @@ pub(crate) fn compress_slow(v: &[i16], slen: usize) -> Option> { pub(crate) fn compress(v: &[i16], slen: usize) -> Option> { // encode each coefficient separately; join later let lengths_and_coefficients = v.iter().map(|c| compress_coefficient(*c)).collect_vec(); - if lengths_and_coefficients + let total_length = lengths_and_coefficients .iter() .map(|(l, _c)| *l) - .sum::() - > slen - { + .sum::(); + + // if we can't fit all coefficients in the allotted bytes + if total_length > slen * 8 { + return None; + } + + // no coefficients are given + if v.is_empty() { return None; } // join all but one coefficients assuming enough space - let mut bytes = vec![0u8; slen / 8]; + let mut bytes = vec![0u8; slen]; let mut counter = 0; for (length, coefficient) in lengths_and_coefficients.iter().take(v.len() - 1) { let (cdiv8, cmod8) = counter.div_mod_floor(&8); @@ -104,11 +110,7 @@ fn compress_coefficient(coeff: i16) -> (usize, u8) { /// This is a deprecated decompress routine used now only for testing /// compatibility with the new, faster implementation (below). #[allow(dead_code)] -pub(crate) fn decompress_slow(x: &[u8], bitlength: usize, n: usize) -> Option> { - if x.len() * 8 != bitlength { - return None; - } - +pub(crate) fn decompress_slow(x: &[u8], n: usize) -> Option> { let bitvector = BitVec::from_bytes(x); let mut index = 0; let mut result = Vec::with_capacity(n); @@ -151,11 +153,7 @@ pub(crate) fn decompress_slow(x: &[u8], bitlength: usize, n: usize) -> Option Option> { - if x.len() * 8 != bitlength { - return None; - } - +pub(crate) fn decompress(x: &[u8], n: usize) -> Option> { let bitvector = BitVec::from_bytes(x); let mut index = 0; let mut result = Vec::with_capacity(n); @@ -276,7 +274,6 @@ mod test { use crate::{ encoding::{compress, compress_slow, decompress, decompress_slow}, - falcon::FalconVariant, field::Q, }; use bit_vec::BitVec; @@ -284,6 +281,46 @@ mod test { use rand::{thread_rng, Rng}; use rand_distr::{num_traits::ToPrimitive, Distribution, Normal}; + use proptest::prelude::*; + + fn short_elements(n: usize) -> Vec { + let sigma = 1.5 * (Q.to_f64().unwrap()).sqrt(); + let distribution = Normal::::new(0.0, sigma).unwrap(); + let mut rng = thread_rng(); + (0..n) + .map(|_| { + (distribution.sample(&mut rng) + 0.5) + .floor() + .to_i32() + .unwrap() as i16 + }) + .collect::>() + } + proptest! { + #[test] + fn compress_does_not_crash(v in (0..2000usize).prop_map(short_elements)) { + compress(&v, 2*v.len()); + } + } + proptest! { + #[test] + fn decompress_recovers(v in (0..2000usize).prop_map(short_elements)) { + let slen = 2 * v.len(); + let n = v.len(); + if let Some(compressed) = compress(&v, slen) { + let recovered = decompress(&compressed, n).unwrap(); + prop_assert_eq!(v, recovered.clone()); + let recompressed = compress(&recovered, slen).unwrap(); + prop_assert_eq!(compressed, recompressed); + } + } + } + + #[test] + fn compress_empty_vec_does_not_crash() { + compress(&[], 0); + } + #[test] fn test_compress_decompress() { let num_iterations = 1000; @@ -315,12 +352,7 @@ mod test { .try_into() .unwrap(); if let Some(compressed) = compress(&initial, slen * 8) { - if let Some(decompressed) = decompress( - &compressed, - (FalconVariant::Falcon512.parameters().sig_bytelen - SALT_LEN - HEAD_LEN) - * 8, - N, - ) { + if let Some(decompressed) = decompress(&compressed, N) { assert_eq!(initial.to_vec(), decompressed); num_successes_512 += 1; } @@ -344,7 +376,7 @@ mod test { .try_into() .unwrap(); if let Some(compressed) = compress(&initial, slen * 8) { - if let Some(decompressed) = decompress(&compressed, slen * 8, N) { + if let Some(decompressed) = decompress(&compressed, N) { assert_eq!(initial.to_vec(), decompressed); num_successes_1024 += 1; } @@ -372,7 +404,7 @@ mod test { .collect::>(); let slen = 2 * n * 8; let compressed = compress_slow(&initial, slen).unwrap(); - let compressed_fast = compress(&initial, slen).unwrap(); + let compressed_fast = compress(&initial, slen / 8).unwrap(); assert_eq!( compressed, compressed_fast, @@ -403,8 +435,8 @@ mod test { let slen = 2 * n * 8; let compressed = compress(&initial, slen).unwrap(); - let decompressed = decompress(&compressed, slen, n); - let decompressed_fast = decompress_slow(&compressed, slen, n); + let decompressed = decompress(&compressed, n); + let decompressed_fast = decompress_slow(&compressed, n); assert_eq!(decompressed, decompressed_fast); } @@ -431,10 +463,8 @@ mod test { let slen = 2 * n * 8; let compressed = compress(&initial, slen).unwrap(); - assert!(decompress(&compressed, slen, n + 1).is_none()); - assert!(decompress(&compressed, slen + 1, n).is_none()); - // assert!(decompress(&compressed, slen, n - 1).is_none()); // should work - assert!(decompress(&compressed, slen - 1, n).is_none()); + assert!(decompress(&compressed, n + 1).is_none()); + // assert!(decompress(&compressed, n - 1).is_none()); // should work // flip last set bit -- should cause failure let mut compressed_bitvec = BitVec::from_bytes(&compressed); @@ -444,7 +474,7 @@ mod test { } compressed_bitvec.set(index - 1, false); let last_bit_flipped = compressed_bitvec.to_bytes(); - assert!(decompress(&last_bit_flipped, slen, n).is_none()); + assert!(decompress(&last_bit_flipped, n).is_none()); // try random string -- might fail, but if not must re-encode to the same // let random = (0..compressed.len()).map(|_| rng.gen::()).collect_vec(); @@ -460,7 +490,7 @@ mod test { for i in 0..num_trailing_zeros { random[len - 1 - i] = 0; } - if let Some(decompressed) = decompress(&random, slen, n) { + if let Some(decompressed) = decompress(&random, n) { let recompressed = compress(&decompressed, slen).unwrap(); assert_eq!( random, diff --git a/falcon-rust/src/falcon.rs b/falcon-rust/src/falcon.rs index 4783dd5..8fcdd2d 100644 --- a/falcon-rust/src/falcon.rs +++ b/falcon-rust/src/falcon.rs @@ -537,7 +537,7 @@ pub fn verify(m: &[u8], sig: &Signature, pk: &PublicKey) - let r_cat_m = [sig.r.to_vec(), m.to_vec()].concat(); let c = hash_to_point(&r_cat_m, n); - let s2 = match decompress(&sig.s, (params.sig_bytelen - 41) * 8, n) { + let s2 = match decompress(&sig.s, n) { Some(success) => success, None => { return false; @@ -1233,7 +1233,7 @@ mod test { r: nonce, s: compress( &sigvec, - (FalconVariant::Falcon1024.parameters().sig_bytelen - 41) * 8, + FalconVariant::Falcon1024.parameters().sig_bytelen - 41, ) .unwrap(), }; @@ -1250,7 +1250,7 @@ mod test { r: nonce, s: compress( &sigvec, - (FalconVariant::Falcon512.parameters().sig_bytelen - 41) * 8, + FalconVariant::Falcon512.parameters().sig_bytelen - 41, ) .unwrap(), };