From fb20bbc5384dfbad0b5892f8069a34d74d8b0b23 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Tue, 27 Dec 2022 10:22:30 -0500 Subject: [PATCH] implemented unfinished architecture --- zssp/src/constants.rs | 1 - zssp/src/counter.rs | 146 +++++++++--------------------------------- zssp/src/tests.rs | 42 ++++-------- zssp/src/zssp.rs | 48 +++++++------- 4 files changed, 68 insertions(+), 169 deletions(-) diff --git a/zssp/src/constants.rs b/zssp/src/constants.rs index b2f03a3d1..bd632f8f3 100644 --- a/zssp/src/constants.rs +++ b/zssp/src/constants.rs @@ -78,7 +78,6 @@ pub(crate) const SESSION_ID_SIZE: usize = 6; 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 diff --git a/zssp/src/counter.rs b/zssp/src/counter.rs index 45d8baf22..828785220 100644 --- a/zssp/src/counter.rs +++ b/zssp/src/counter.rs @@ -1,11 +1,10 @@ -use std::sync::{ - atomic::{AtomicU64, Ordering}, - Mutex, RwLock, -}; +use std::{sync::{ + atomic::{AtomicU64, Ordering, AtomicU32, AtomicI32, AtomicBool} +}, mem}; use zerotier_crypto::random; -use crate::constants::{COUNTER_MAX_DELTA, COUNTER_MAX_ALLOWED_OOO}; +use crate::constants::COUNTER_MAX_ALLOWED_OOO; /// Outgoing packet counter with strictly ordered atomic semantics. /// @@ -20,7 +19,7 @@ impl Counter { pub fn new() -> Self { // Using a random value has no security implication. Zero would be fine. This just // helps randomize packet contents a bit. - Self(AtomicU64::new(random::next_u32_secure() as u64)) + Self(AtomicU64::new((random::next_u32_secure()/2) as u64)) } /// Get the value most recently used to send a packet. @@ -59,132 +58,51 @@ impl CounterValue { } /// Incoming packet deduplication and replay protection window. -pub(crate) struct CounterWindowAlt(RwLock<(u32, [u32; COUNTER_MAX_DELTA as usize])>); - -impl CounterWindowAlt { - #[inline(always)] - pub fn new(initial: u32) -> Self { - Self(RwLock::new((initial, std::array::from_fn(|_| initial)))) - } - - #[inline(always)] - pub fn message_received(&self, received_counter_value: u32) -> bool { - let idx = (received_counter_value % COUNTER_MAX_DELTA) as usize; - let data = self.0.read().unwrap(); - let max_counter_seen = data.0; - let lower_window = max_counter_seen.wrapping_sub(COUNTER_MAX_DELTA / 2); - let upper_window = max_counter_seen.wrapping_add(COUNTER_MAX_DELTA / 2); - if lower_window < upper_window { - if (lower_window <= received_counter_value) & (received_counter_value < upper_window) { - if data.1[idx] != received_counter_value { - return true; - } - } - } else if (lower_window <= received_counter_value) | (received_counter_value < upper_window) { - if data.1[idx] != received_counter_value { - return true; - } - } - return false; - } - - #[inline(always)] - pub fn message_authenticated(&self, received_counter_value: u32) -> bool { - let idx = (received_counter_value % COUNTER_MAX_DELTA) as usize; - let mut data = self.0.write().unwrap(); - let max_counter_seen = data.0; - let lower_window = max_counter_seen.wrapping_sub(COUNTER_MAX_DELTA / 2); - let upper_window = max_counter_seen.wrapping_add(COUNTER_MAX_DELTA / 2); - if lower_window < upper_window { - if (lower_window <= received_counter_value) & (received_counter_value < upper_window) { - if data.1[idx] != received_counter_value { - data.1[idx] = received_counter_value; - data.0 = max_counter_seen.max(received_counter_value); - return true; - } - } - } else if (lower_window <= received_counter_value) | (received_counter_value < upper_window) { - if data.1[idx] != received_counter_value { - data.1[idx] = received_counter_value; - data.0 = (max_counter_seen as i32).max(received_counter_value as i32) as u32; - return true; - } - } - return false; - } -} - -pub(crate) struct CounterWindow(Mutex>); +pub(crate) struct CounterWindow(AtomicBool, AtomicBool, [AtomicU32; COUNTER_MAX_ALLOWED_OOO]); 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(Some([initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize]))) + Self(AtomicBool::new(true), AtomicBool::new(false), std::array::from_fn(|_| AtomicU32::new(initial))) } #[inline(always)] pub fn new_uninit() -> Self { - Self(Mutex::new(None)) + Self(AtomicBool::new(false), AtomicBool::new(false), std::array::from_fn(|_| AtomicU32::new(0))) } #[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 = Some([initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize]); + pub fn init_authenticated(&self, received_counter_value: u32) { + self.1.store((u32::MAX/4 < received_counter_value) & (received_counter_value <= u32::MAX/4*3), Ordering::SeqCst); + for i in 1..COUNTER_MAX_ALLOWED_OOO { + self.2[i].store(received_counter_value, Ordering::SeqCst); + } + self.0.store(true, Ordering::SeqCst); } - #[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(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 - if let Some(history) = self.0.lock().unwrap().as_mut() { - let mut is_in = false; - let mut idx = 0; - let mut smallest = fragment_nonce; - for i in 0..history.len() { - let nonce = history[i]; - is_in |= nonce == fragment_nonce; - let delta = (smallest as i64).wrapping_sub(nonce as i64); - if delta > 0 { - smallest = nonce; - idx = i; - } + pub fn message_received(&self, received_counter_value: u32) -> bool { + if self.0.load(Ordering::SeqCst) { + let idx = (received_counter_value % COUNTER_MAX_ALLOWED_OOO as u32) as usize; + let pre = self.2[idx].load(Ordering::SeqCst); + if self.1.load(Ordering::Relaxed) { + return pre < received_counter_value; + } else { + return (pre as i32) < (received_counter_value as i32); } - if !is_in & (smallest != fragment_nonce) { - history[idx] = fragment_nonce; - return true - } - return false } else { - return true + return true; } } #[inline(always)] - pub fn purge(&self, inauthentic_counter_value: u32, inauthentic_fragment_no: u8) { - let inauthentic_nonce = (inauthentic_counter_value as u64).wrapping_shl(32) | (inauthentic_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 - if let Some(history) = self.0.lock().unwrap().as_mut() { - let mut idx = 0; - let mut smallest = history[0]; - for i in 0..history.len() { - let nonce = history[i]; - if nonce == inauthentic_nonce { - idx = i; - } else { - let delta = (smallest as i64).wrapping_sub(nonce as i64); - if delta > 0 { - smallest = nonce; - } - } - } - history[idx] = smallest; + pub fn message_authenticated(&self, received_counter_value: u32) -> bool { + //if a valid message is received but one of its fragments was lost, it can technically be replayed, since the message is incomplete, we know it still exists in the gather array, so the gather array will deduplicate the replayed message + //eventually the counter of that message will be too OOO to be accepted anymore so it can't be used to DOS + let idx = (received_counter_value % COUNTER_MAX_ALLOWED_OOO as u32) as usize; + if self.1.swap((u32::MAX/4 < received_counter_value) & (received_counter_value <= u32::MAX/4*3), Ordering::SeqCst) { + return self.2[idx].fetch_max(received_counter_value, Ordering::SeqCst) < received_counter_value; + } else { + let pre_as_signed: &AtomicI32 = unsafe {mem::transmute(&self.2[idx])}; + return pre_as_signed.fetch_max(received_counter_value as i32, Ordering::SeqCst) < received_counter_value as i32; } } } diff --git a/zssp/src/tests.rs b/zssp/src/tests.rs index dd95fae2e..47cd058e8 100644 --- a/zssp/src/tests.rs +++ b/zssp/src/tests.rs @@ -229,56 +229,36 @@ mod tests { #[test] fn counter_window() { let mut rng = 84632; - let mut counter = u32::MAX - 16; - let mut fragment_no: u8 = 0; - let mut history = Vec::<(u32, u8)>::new(); + let mut counter = 1u32; + let mut history = Vec::new(); - let mut w = CounterWindow::new(counter.wrapping_sub(1)); + let mut w = CounterWindow::new(counter); for i in 0..1000000 { let p = xorshift64(&mut rng) as f32/(u32::MAX as f32 + 1.0); let c; - let f; if p < 0.5 { let r = xorshift64(&mut rng); c = counter.wrapping_add(r%(COUNTER_MAX_ALLOWED_OOO - 2) as u32 + 1); - f = 0; - } else if p < 0.7 { - fragment_no = u8::min(fragment_no + 1, 63); - c = counter; - f = fragment_no; } else if p < 0.8 { counter = counter.wrapping_add(1); - fragment_no = 0; c = counter; - f = fragment_no; } else if p < 0.9 { if history.len() > 0 { let idx = xorshift64(&mut rng) as usize%history.len(); - let (c, f) = history[idx]; - assert!(!w.message_received(c, f)); + let c = history[idx]; + assert!(!w.message_authenticated(c)); } continue; - } else if p < 0.9995 { + } else { c = xorshift64(&mut rng); - f = (xorshift64(&mut rng)%64) as u8; - if w.message_received(c, f) { - w.purge(c, f); - } + w.message_received(c); continue; - } 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)); + if history.contains(&c) { + assert!(!w.message_authenticated(c)); } else { - assert!(w.message_received(c, f)); - history.push((c, f)); + assert!(w.message_authenticated(c)); + history.push(c); } } } diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index f9e56972d..eb2adf1be 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -487,7 +487,7 @@ impl ReceiveContext { { if let Some(session) = app.lookup_session(local_session_id) { if verify_header_check_code(incoming_packet, &session.header_check_cipher) { - if session.receive_window.message_received(counter, fragment_no) { + if session.receive_window.message_received(counter) { let canonical_header = CanonicalHeader::make(local_session_id, packet_type, counter); if fragment_count > 1 { if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count { @@ -664,31 +664,33 @@ impl ReceiveContext { session_key.return_receive_cipher(c); if aead_authentication_ok { - // Select this key as the new default if it's newer than the current key. - if p > 0 - && state.session_keys[state.cur_session_key_idx] - .as_ref() - .map_or(true, |old| old.creation_counter < session_key.creation_counter) - { - drop(state); - let mut state = session.state.write().unwrap(); - state.cur_session_key_idx = key_idx; - for i in 0..KEY_HISTORY_SIZE { - if i != key_idx { - if let Some(old_key) = state.session_keys[key_idx].as_ref() { - // Release pooled cipher memory from old keys. - old_key.receive_cipher_pool.lock().unwrap().clear(); - old_key.send_cipher_pool.lock().unwrap().clear(); + if session.receive_window.message_authenticated(counter) { + // Select this key as the new default if it's newer than the current key. + if p > 0 + && state.session_keys[state.cur_session_key_idx] + .as_ref() + .map_or(true, |old| old.creation_counter < session_key.creation_counter) + { + drop(state); + let mut state = session.state.write().unwrap(); + state.cur_session_key_idx = key_idx; + for i in 0..KEY_HISTORY_SIZE { + if i != key_idx { + if let Some(old_key) = state.session_keys[key_idx].as_ref() { + // Release pooled cipher memory from old keys. + old_key.receive_cipher_pool.lock().unwrap().clear(); + old_key.send_cipher_pool.lock().unwrap().clear(); + } } } } - } - if packet_type == PACKET_TYPE_DATA { - return Ok(ReceiveResult::OkData(&mut data_buf[..data_len])); - } else { - unlikely_branch(); - return Ok(ReceiveResult::Ok); + if packet_type == PACKET_TYPE_DATA { + return Ok(ReceiveResult::OkData(&mut data_buf[..data_len])); + } else { + unlikely_branch(); + return Ok(ReceiveResult::Ok); + } } } } @@ -1158,7 +1160,7 @@ impl ReceiveContext { last_ratchet_count + 1, hybrid_kk.is_some(), ); - session.receive_window.init(counter); + session.receive_window.init_authenticated(counter); //////////////////////////////////////////////////////////////// // packet encoding for post-noise session start ack