From e96824a04f8127682883182e67b1c7091c3797e0 Mon Sep 17 00:00:00 2001 From: phayes Date: Wed, 11 Mar 2020 12:04:06 -0700 Subject: [PATCH] Completely refactoring movingwindow - much better now --- Cargo.toml | 13 +-- src/lib.rs | 3 + src/movingwindow/README.md | 10 +-- src/movingwindow/mod.rs | 172 ++++++++++++++++++++++++++++--------- 4 files changed, 148 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb3bb29..ba7bd93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,18 +12,21 @@ categories = ["cryptography", "no-std"] [dependencies] digest = "0.8.1" +failure = {version = "0.1.7", optional = true} +num-bigint = { version = "0.6", optional=true, features = ["zeroize"], package = "num-bigint-dig" } +bitvec = "0.17.3" +subtle = "2.2.2" + +# Should use same version as in "digest" generic-array = "0.12.3" -failure = {version = "0.1.6", optional = true} -num-bigint = {version="0.2.6", optional = true} -bitvec = "0.17.2" [dev-dependencies] sha2 = "0.8.1" sha-1 = "0.8.2" sha3 = "0.8.2" -hex = "0.4.1" +hex = "0.4.2" num-traits = "0.2.11" -num-integer = "0.1.42" +num-integer = "0.1.42" [badges] travis-ci = { repository = "phayes/fdh-rs", branch = "master" } diff --git a/src/lib.rs b/src/lib.rs index d549fc6..ba5f01d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,9 @@ use num_bigint::BigUint; #[cfg(feature = "std")] extern crate std; +#[cfg(feature = "std")] +pub mod movingwindow; + #[derive(Clone, Debug, Default)] pub struct FullDomainHash { output_size: usize, diff --git a/src/movingwindow/README.md b/src/movingwindow/README.md index d9d4c64..93ef931 100644 --- a/src/movingwindow/README.md +++ b/src/movingwindow/README.md @@ -1,11 +1,11 @@ -Moving Window FDH ------------------ - -**This experiment has been shown to be very broken and insecure - don't use it** +Moving Window Full Domain Hash (MWFDH) +-------------------------------------- This is an experimental Full Domain Hash (FDH) that is constructed of a moving-window applied against an extendable ouput (XOF) hash function. Unlike a regular Full Domain Hash, it is designed to be contant-time. -This was experiemental, and has been shown to be insecure and broken. +## A note on security + +Given the output of a MWFDH, the message hashed with a MWFDH is secure against recovery (in the normal way for one-way hash functions), however the moving window does not provide any level of secrecy to the underlying XOF hash digest outside of it's window. This means that the output of the underlying XOF should be considered "exposed" to anyone who has the final output of the MWFDH. For its intended purpose, this is acceptable, but protocol designers using the MWFDH should be aware of this poroperty of the MWFDH. ### Background and Rational diff --git a/src/movingwindow/mod.rs b/src/movingwindow/mod.rs index d18f112..7b61602 100644 --- a/src/movingwindow/mod.rs +++ b/src/movingwindow/mod.rs @@ -1,18 +1,37 @@ -use bitvec::*; +use bitvec::order::Msb0; +use bitvec::vec::*; use digest::{ExtendableOutput, Input, XofReader}; +use num_bigint::BigUint; use std::vec; use std::vec::Vec; +use subtle::Choice; -pub struct MWFullDomainHash { +pub struct MWFDH +where + H: ExtendableOutput + Input + Default + Clone, + C: Fn(&[u8]) -> Choice, +{ + iterations: usize, output_size: usize, inner_hash: H, + domain_function: C, } -impl MWFullDomainHash { - pub fn new(output_size: usize) -> Self { - MWFullDomainHash { +impl MWFDH +where + H: ExtendableOutput + Input + Default + Clone, + C: Fn(&[u8]) -> Choice, +{ + pub fn new(iterations: usize, output_size: usize, domain_function: C) -> Self { + if iterations % 8 != 0 { + panic!("fdh-rs: movingwindow: iterations must be multiple of 8"); + } + + MWFDH { + iterations: iterations, output_size: output_size, inner_hash: H::default(), + domain_function: domain_function, } } @@ -20,38 +39,73 @@ impl MWFullDomainHash { self.inner_hash.input(input); } - pub fn result_below(self, max: &[u8]) -> Vec { - let max = left_pad(max, self.output_size); // TODO: Can we skip this in the commong case when lengths match? - - let mut reader = self.inner_hash.xof_result(); - let mut result: Vec = vec![0x00; self.output_size]; - reader.read(&mut result); + /// Get a digest value that is within the domain specified by the passed closure. + /// + /// # Example + /// ```rust + /// use sha2::Sha512; + /// use fdh::{FullDomainHash, Input, VariableOutput}; + /// use num_bigint::BigUint; + /// use num_integer::Integer; + /// + /// // Get a full domain hash that is odd + /// let mut hasher = FullDomainHash::::new(64).unwrap(); + /// hasher.input(b"ATTACKATDAWN"); + /// + /// let (digest, iv) = hasher.results_in_domain(0, |digest| BigUint::from_bytes_be(digest).is_odd()).unwrap(); + /// ``` + pub fn results_in_domain(&self) -> Result, ()> { + let mut all_candidates = self.all_candidates(); - if &result as &[u8] < &max { - // Happy path - return result.into(); - } else { - let mut result: BitVec = result.into(); - let mut read_buf = BitVec::::with_capacity(8); - let max: BitVec = max.into(); - let mut read_buf_pos = 0; - while result > max { - if read_buf_pos == 0 { - let mut temp_buf: Vec = vec![0x00]; - reader.read(&mut temp_buf); - read_buf = temp_buf.into(); - } - result = result << 1; - result.push(read_buf[read_buf_pos]); - - read_buf_pos += 1; - if read_buf_pos == 8 { - read_buf_pos = 0; - } + let mut selection: usize = 0; + let mut in_domain: Choice = 0.into(); + for candidate in all_candidates.iter() { + in_domain |= (self.domain_function)(candidate); + if in_domain.into() { + selection += 0; + } else { + selection += 1; } - result.into() } + + let found_domain: bool = in_domain.into(); + if (!found_domain) { + return Err(()); + } + + let result: Vec = all_candidates.remove(selection); + Ok(result) + } + + fn all_candidates(&self) -> Vec> { + let inner_hash = self.inner_hash.clone(); + let mut reader = inner_hash.xof_result(); + let underlying_size = self.output_size * (self.iterations / 8); + let mut result: Vec = vec![0x00; underlying_size]; + reader.read(&mut result); + + compute_candidates(result, self.output_size, self.iterations) + } +} + +// Given a Vec (as you would get from an underlying digest), +// get a Vec of all moving windows applied against that input +fn compute_candidates( + input: Vec, + moving_window_size: usize, + num_iterations: usize, +) -> Vec> { + // TODO: debug assert input.len(), moving_window_size, and num_iterations all line up + let mut underlying_digest: BitVec = input.into(); + let mut all_candidates = Vec::>::with_capacity(num_iterations); + + for _ in 0..num_iterations { + let u8_view = underlying_digest.as_slice(); + all_candidates.push(u8_view[0..moving_window_size].iter().cloned().collect()); + underlying_digest = underlying_digest << 1; } + + all_candidates } fn left_pad(input: &[u8], size: usize) -> std::vec::Vec { @@ -66,24 +120,62 @@ fn left_pad(input: &[u8], size: usize) -> std::vec::Vec { out } +fn between(check: &[u8], min: &BigUint, max: &BigUint) -> Choice { + let check = BigUint::from_bytes_be(check); + ((&check < max && &check > min) as u8).into() +} + +fn lt(check: &[u8], max: &BigUint) -> Choice { + let check = BigUint::from_bytes_be(check); + ((&check < max) as u8).into() +} + +fn gt(check: &[u8], min: &BigUint) -> Choice { + let check = BigUint::from_bytes_be(check); + ((&check > min) as u8).into() +} + #[cfg(test)] mod tests { - use crate::movingwindow::MWFullDomainHash; + use super::*; use num_bigint::BigUint; use num_traits::Num; use sha3::Shake128; + use std::dbg; + + #[test] + fn all_candidates_test() { + let some_vec = vec![0, 0, 0, 0, 0, 255]; + let candidates = compute_candidates(some_vec, 5, 8); + + assert_eq!(candidates[0], vec![0, 0, 0, 0, 0]); + assert_eq!(candidates[7], vec![0, 0, 0, 0, 127]); + // IF we had shifted one more we would have [0, 0, 0, 0, 255] + } #[test] - fn bit_test() { + fn test_results_within() { + use num_bigint::BigUint; + use num_traits::Num; + + let min = BigUint::from_str_radix( + "51683095453715361952842063988888814558178328011011413557662527675023521115731", + 10, + ) + .unwrap(); let max = BigUint::from_str_radix( - "00523095453715361952842063988888814558178328011011413557662527675023521115731", + "63372381656167118369940880608146415619543459354936568979731399163319071519847", 10, ) .unwrap(); - let mut mwfdh = MWFullDomainHash::::new(256 / 8); - mwfdh.input(b"ATTACK AT DAWN"); - let _result = mwfdh.result_below(&max.to_bytes_be()); - } + let mut hasher = MWFDH::::new(128, 32, |check: _| between(check, &min, &max)); + hasher.input(b"ATTACK AT DAWN"); + let result = hasher.results_in_domain().unwrap(); + assert_eq!( + hex::encode(result), + "7ebe111e3d443145d87f7b574f67f92be291f19d747a489601e40bd6f3671008" + ); + } }