diff --git a/zssp/src/constants.rs b/zssp/src/constants.rs index 523b6692e..b2f03a3d1 100644 --- a/zssp/src/constants.rs +++ b/zssp/src/constants.rs @@ -79,6 +79,7 @@ pub(crate) const KEY_HISTORY_SIZE: usize = 3; /// Maximum difference between out-of-order incoming packet counters, and size of deduplication buffer. pub(crate) const COUNTER_MAX_DELTA: u32 = 16; +pub(crate) const COUNTER_MAX_ALLOWED_OOO: usize = 16; // Packet types can range from 0 to 15 (4 bits) -- 0-3 are defined and 4-15 are reserved for future use pub(crate) const PACKET_TYPE_DATA: u8 = 0; diff --git a/zssp/src/counter.rs b/zssp/src/counter.rs index b368b81f2..e200b9015 100644 --- a/zssp/src/counter.rs +++ b/zssp/src/counter.rs @@ -5,7 +5,7 @@ use std::sync::{ use zerotier_crypto::random; -use crate::constants::COUNTER_MAX_DELTA; +use crate::constants::{COUNTER_MAX_DELTA, COUNTER_MAX_ALLOWED_OOO}; /// Outgoing packet counter with strictly ordered atomic semantics. /// @@ -114,34 +114,56 @@ impl CounterWindowAlt { } } -pub(crate) struct CounterWindow(Mutex<(usize, [u64; COUNTER_MAX_DELTA as usize])>); +pub(crate) struct CounterWindow(Mutex<(usize, [u64; COUNTER_MAX_ALLOWED_OOO as usize])>); impl CounterWindow { #[inline(always)] pub fn new(initial: u32) -> Self { + // we want our nonces to wrap at the exact same time that the counter wraps, so we shift them up to the u64 boundary let initial_nonce = (initial as u64).wrapping_shl(32); - Self(Mutex::new((0, std::array::from_fn(|_| initial_nonce)))) + Self(Mutex::new((0, [initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize]))) } + #[inline(always)] + pub fn new_uninit() -> Self { + Self(Mutex::new((usize::MAX, [0; COUNTER_MAX_ALLOWED_OOO as usize]))) + } + #[inline(always)] + pub fn init(&self, initial: u32) { + let initial_nonce = (initial as u64).wrapping_shl(6); + let mut data = self.0.lock().unwrap(); + data.0 = 0; + data.1 = [initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize]; + } + #[inline(always)] pub fn message_received(&self, received_counter_value: u32, received_fragment_no: u8) -> bool { - let fragment_nonce = (received_counter_value as u64).wrapping_shl(6) | (received_fragment_no as u64); + let fragment_nonce = (received_counter_value as u64).wrapping_shl(32) | (received_fragment_no as u64); //everything past this point must be atomic, i.e. these instructions must be run mutually exclusive to completion; //atomic instructions are only ever atomic within themselves; //sequentially consistent atomics do not guarantee that the thread is not preempted between individual atomic instructions let mut data = self.0.lock().unwrap(); - let mut is_in = false; - let mut is_gt_min = false; - for nonce in data.1 { - is_in |= nonce == fragment_nonce; - is_gt_min |= nonce.wrapping_sub(fragment_nonce) > 0; + if data.0 == usize::MAX { + return true + } else { + const NONCE_MAX_DELTA: i64 = (2*COUNTER_MAX_ALLOWED_OOO as i64).wrapping_shl(32); + let mut is_in = false; + let mut idx = 0; + let mut smallest = fragment_nonce; + for i in 0..data.1.len() { + let nonce = data.1[i]; + is_in |= nonce == fragment_nonce; + let delta = (smallest as i64).wrapping_sub(nonce as i64); + if delta > 0 { + smallest = nonce; + idx = i; + } + } + if !is_in & (smallest != fragment_nonce) & ((fragment_nonce as i64).wrapping_sub(smallest as i64) < NONCE_MAX_DELTA) { + data.1[idx] = fragment_nonce; + return true + } + return false } - if !is_in & is_gt_min { - let idx = data.0; - data.1[idx] = fragment_nonce; - data.0 = (idx + 1) % (COUNTER_MAX_DELTA as usize); - return true; - } - return false; } } diff --git a/zssp/src/tests.rs b/zssp/src/tests.rs index 45de756df..a58d23498 100644 --- a/zssp/src/tests.rs +++ b/zssp/src/tests.rs @@ -218,14 +218,62 @@ mod tests { } } + + #[inline(always)] + pub fn xorshift64(x: &mut u64) -> u32 { + *x ^= x.wrapping_shl(13); + *x ^= x.wrapping_shr(7); + *x ^= x.wrapping_shl(17); + *x as u32 + } #[test] fn counter_window() { - let w = CounterWindow::new(0xffffffff); - assert!(!w.message_received(0xffffffff)); - assert!(w.message_received(0)); - assert!(w.message_received(1)); - assert!(w.message_received(COUNTER_MAX_DELTA * 2)); - assert!(!w.message_received(0xffffffff)); - assert!(w.message_received(0xfffffffe)); + let sqrt_out_of_order_max = (COUNTER_MAX_ALLOWED_OOO as f32).sqrt() as u32; + let mut rng = 8234; + let mut counter = u32::MAX - 16; + let mut fragment_no: u8 = 0; + let mut history = Vec::<(u32, u8)>::new(); + + let mut w = CounterWindow::new(counter.wrapping_sub(1)); + for i in 1..1000000 { + let p = xorshift64(&mut rng)%1000; + let c; + let f; + if p < 250 { + let r = xorshift64(&mut rng); + c = counter.wrapping_add(r%sqrt_out_of_order_max); + f = fragment_no + 1 + ((r/sqrt_out_of_order_max)%sqrt_out_of_order_max) as u8; + } else if p < 500 { + if history.len() > 0 { + let idx = xorshift64(&mut rng) as usize%history.len(); + let (c, f) = history[idx]; + assert!(!w.message_received(c, f)); + } + continue; + } else if p < 750 { + fragment_no = u8::min(fragment_no + 1, 63); + c = counter; + f = fragment_no; + } else if p < 999 { + counter = counter.wrapping_add(1); + fragment_no = 0; + c = counter; + f = fragment_no; + } else { + //simulate rekeying + counter = xorshift64(&mut rng); + fragment_no = 0; + w = CounterWindow::new(counter.wrapping_sub(1)); + history = Vec::<(u32, u8)>::new(); + c = counter; + f = fragment_no; + } + if history.contains(&(c, f)) { + assert!(!w.message_received(c, f)); + } else { + assert!(w.message_received(c, f)); + history.push((c, f)); + } + } } } diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 837c81cf8..f9e56972d 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -254,6 +254,7 @@ impl Session { id: local_session_id, application_data, send_counter, + receive_window: CounterWindow::new_uninit(),//alice does not know bob's counter yet psk: psk.clone(), noise_ss, header_check_cipher, @@ -879,6 +880,7 @@ impl ReceiveContext { id: new_session_id, application_data: associated_object, send_counter: Counter::new(), + receive_window: CounterWindow::new(counter), psk, noise_ss, header_check_cipher, @@ -1030,7 +1032,6 @@ impl ReceiveContext { Role::Bob, current_time, reply_counter, - counter, last_ratchet_count + 1, hybrid_kk.is_some(), ); @@ -1154,10 +1155,10 @@ impl ReceiveContext { Role::Alice, current_time, reply_counter, - counter, last_ratchet_count + 1, hybrid_kk.is_some(), ); + session.receive_window.init(counter); //////////////////////////////////////////////////////////////// // packet encoding for post-noise session start ack @@ -1497,7 +1498,6 @@ impl SessionKey { role: Role, current_time: i64, current_counter: CounterValue, - remote_counter: u32, ratchet_count: u64, jedi: bool, ) -> Self { @@ -1511,7 +1511,6 @@ impl SessionKey { secret_fingerprint: public_fingerprint_of_secret(key.as_bytes())[..16].try_into().unwrap(), creation_time: current_time, creation_counter: current_counter, - receive_window: CounterWindow::new(remote_counter), lifetime: KeyLifetime::new(current_counter, current_time), ratchet_key: kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_RATCHETING), receive_key,