From c2c2a269ae5bb09f9bbd2eb3f016cd915f0823a6 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 14:27:10 -0400 Subject: [PATCH 01/25] fixed bug --- src/applicationlayer.rs | 104 ++------- src/crypto/p384.rs | 2 +- src/lib.rs | 4 +- src/proto.rs | 31 +-- src/symmetric_state.rs | 16 +- src/zssp.rs | 485 +++++++++++++++++++++------------------- 6 files changed, 307 insertions(+), 335 deletions(-) diff --git a/src/applicationlayer.rs b/src/applicationlayer.rs index 3658be2..251e825 100644 --- a/src/applicationlayer.rs +++ b/src/applicationlayer.rs @@ -11,9 +11,10 @@ use std::sync::Arc; use crate::crypto::aes::{AesDec, AesEnc}; use crate::crypto::aes_gcm::{AesGcmDec, AesGcmEnc}; use crate::crypto::p384::{P384KeyPair, P384PublicKey}; -use crate::crypto::sha512::{HmacSha512, Sha512}; use crate::crypto::rand_core::{CryptoRng, RngCore}; -use crate::{log_event::LogEvent, Session, RATCHET_FINGERPRINT_SIZE, RATCHET_KEY_SIZE}; +use crate::crypto::secret::Secret; +use crate::crypto::sha512::{HmacSha512, Sha512}; +use crate::{log_event::LogEvent, Session, RATCHET_SIZE}; /// Trait to implement to integrate the session into an application. /// @@ -162,8 +163,8 @@ pub trait ApplicationLayer: Sized { /// to the zero ratchet key, restarting the ratchet chain. /// If `RatchetAction::FailAuthentication` is returned Alice's connection will be silently dropped. #[allow(unused)] - fn restore_ratchet(&self, ratchet_fingerprint: &[u8; RATCHET_FINGERPRINT_SIZE], current_time: i64) -> Result { - Ok(RestoreAction::DowngradeRatchet) + fn restore_ratchet(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result, ()> { + Ok(None) } /// Atomically save the given ratchet key, fingerprint and number to persistent storage. /// @@ -184,14 +185,11 @@ pub trait ApplicationLayer: Sized { /// (`initiator_disallows_downgrade` returns false and/or`restore_ratchet` returns `DowngradeRatchet`). /// Otherwise, when we restart, we will not be allowed to reconnect. #[allow(unused)] - fn save_ratchet_state( + fn save_ratchet_state<'a>( &self, remote_static_key: &Self::PublicKey, application_data: &Self::Data, - ratchet_action: SaveAction, - latest_ratchet_number: u64, - latest_ratchet_fingerprint: &[u8; RATCHET_FINGERPRINT_SIZE], - latest_ratchet_key: &[u8; RATCHET_KEY_SIZE], + save_action: SaveAction<'a>, current_time: i64, ) -> Result<(), ()> { Ok(()) @@ -200,84 +198,20 @@ pub trait ApplicationLayer: Sized { #[inline] fn event_log<'a>(&self, event: LogEvent<'a, Self>, current_time: i64) {} } -pub enum RestoreAction { - RestoreRatchet(u64, [u8; RATCHET_KEY_SIZE]), - DowngradeRatchet, - FailAuthentication, + +#[derive(Default, Clone)] +pub struct RatchetState { + pub fingerprint: [u8; RATCHET_SIZE], + pub key: Secret, + pub ratchet_count: u64, } /// Ratchet keys and fingerprints should be saved *per remote peer*. It is up to the application to /// enforce separate storage for each remote peer based on `remote_static_key` and `application_data`. /// -/// Only up to 2 ratchet keys and fingerprints may be saved at one time. -/// If a 3rd needs to be saved the 1st should be deleted, if it was not already deleted. -pub enum SaveAction { - /// Save the given `latest_ratchet_fingerprint` and `latest_ratchet_key`, - /// but do not update the confirmed ratchet number. - /// - /// If a ratchet key and fingerprint already exist with ratchet number `latest_ratchet_number`, - /// then `latest_ratchet_fingerprint` and `latest_ratchet_key` should overwrite them. - /// - /// Keep the previous ratchet state saved and searchable until it is explicitly deleted. - /// If there are two saved ratchet keys and fingerprints, replace the oldest pair with - /// the new pair. - SaveAsUnconfirmed, - /// Save the given `latest_ratchet_fingerprint` and `latest_ratchet_key`, - /// and set the confirmed ratchet number to `latest_ratchet_number`. - /// The confirmed ratchet number should be set equal to `latest_ratchet_number`. - /// - /// If a ratchet key and fingerprint already exist with ratchet number `latest_ratchet_number`, - /// then `latest_ratchet_fingerprint` and `latest_ratchet_key` should overwrite them. - /// - /// Keep the previous ratchet state saved and searchable until it is explicitly deleted. - /// If there are two saved ratchet keys and fingerprints, replace the oldest pair with - /// the new pair. - SaveAsConfirmed, - /// Set the confirmed ratchet number to `latest_ratchet_number`, - /// and permanently delete the previous (oldest) ratchet key and fingerprint. - /// - /// The given `latest_ratchet_fingerprint` and `latest_ratchet_key` will be identical to those - /// saved during a prior `SaveAsUnconfirmed` call. - /// These two must be the only saved ratchet key and fingerprint when this call completes. - ConfirmLatestAndDeletePrevious, - /// Permanently delete the previous (oldest) ratchet key and fingerprint. - /// - /// The given `latest_ratchet_fingerprint` and `latest_ratchet_key` will be identical to those - /// saved during a prior `SaveAsConfirmed` call. - /// These two must be the only saved ratchet key and fingerprint when this call completes. - DeletePrevious, -} -use SaveAction::*; -impl SaveAction { - /// If this is true then the given `latest_ratchet_fingerprint` and `latest_ratchet_key` - /// must be saved to permanent storage. - /// `latest_ratchet_key` should be searchable using `latest_ratchet_fingerprint`. - pub fn save_latest(&self) -> bool { - match self { - SaveAsUnconfirmed => true, - SaveAsConfirmed => true, - _ => false, - } - } - /// If this is true then the previous ratchet key and fingerprint should be deleted. - /// If `latest_ratchet_number > 1`, then the previous ratchet key and fingerprint will have - /// ratchet number `latest_ratchet_number - 1`. - pub fn delete_previous(&self) -> bool { - match self { - ConfirmLatestAndDeletePrevious => true, - DeletePrevious => true, - _ => false, - } - } - /// If this is true then set the confirmed ratchet number to `latest_ratchet_number`. - /// - /// The confirmed ratchet number is a single 64-bit number saved to persistent storage that denotes its - /// associated ratchet key is "confirmed". Only the confirmed ratchet key should be used to - /// `open` a new session. - pub fn confirm_latest(&self) -> bool { - match self { - ConfirmLatestAndDeletePrevious => true, - SaveAsConfirmed => true, - _ => false, - } - } +/// Only up to 2 ratchet keys and fingerprints will be saved at one time. +pub enum SaveAction<'a> { + AddRatchet(&'a RatchetState), + DeleteRatchet(&'a RatchetState), + DeleteThenAddRatchet(&'a RatchetState, &'a RatchetState), + VerifyThenOverwriteRatchet(Option<&'a RatchetState>, &'a RatchetState), } diff --git a/src/crypto/p384.rs b/src/crypto/p384.rs index 38b3ec2..2a14067 100644 --- a/src/crypto/p384.rs +++ b/src/crypto/p384.rs @@ -1,6 +1,6 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. -use super::rand_core::{RngCore, CryptoRng}; +use super::rand_core::{CryptoRng, RngCore}; pub const P384_PUBLIC_KEY_SIZE: usize = 49; pub const P384_ECDH_SHARED_SECRET_SIZE: usize = 48; diff --git a/src/lib.rs b/src/lib.rs index 16a3afa..6439e61 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ mod symmetric_state; mod zssp; pub mod error; -pub use crate::applicationlayer::{ApplicationLayer, RestoreAction, SaveAction}; +pub use crate::applicationlayer::{ApplicationLayer, SaveAction}; pub use crate::log_event::LogEvent; -pub use crate::proto::{MAX_IDENTITY_BLOB_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU, RATCHET_FINGERPRINT_SIZE, RATCHET_KEY_SIZE}; +pub use crate::proto::{MAX_IDENTITY_BLOB_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU, RATCHET_SIZE}; pub use crate::zssp::{AcceptSessionAction, Context, ContextInner, IncomingSessionAction, ReceiveResult, Session, SessionEvent}; diff --git a/src/proto.rs b/src/proto.rs index 789e63f..634e8d5 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -21,9 +21,7 @@ pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; /// Minimum physical MTU for ZSSP to function. pub const MIN_TRANSPORT_MTU: usize = 128; -pub const RATCHET_KEY_SIZE: usize = 32; - -pub const RATCHET_FINGERPRINT_SIZE: usize = 32; +pub const RATCHET_SIZE: usize = 32; /// The application has the ability to attach a data payload to Alice's handshake. /// It will be the first payload Bob receives from Alice. @@ -48,7 +46,7 @@ pub(crate) const PACKET_TYPE_NOISE_XK_PATTERN_1: u8 = 0; pub(crate) const PACKET_TYPE_NOISE_XK_PATTERN_2: u8 = 1; pub(crate) const PACKET_TYPE_NOISE_XK_PATTERN_3: u8 = 2; pub(crate) const PACKET_TYPE_KEY_CONFIRM: u8 = 3; -pub(crate) const PACKET_TYPE_KEY_DELETE: u8 = 4; +pub(crate) const PACKET_TYPE_ACK: u8 = 4; pub(crate) const PACKET_TYPE_NOISE_KK_PATTERN_1: u8 = 5; pub(crate) const PACKET_TYPE_NOISE_KK_PATTERN_2: u8 = 6; pub(crate) const PACKET_TYPE_SESSION_REJECTED: u8 = 7; @@ -126,7 +124,7 @@ pub(crate) const MAX_UNASSOCIATED_HANDSHAKE_STATES: usize = 32; /// The maximum size a packet that is not associated to a session may be. /// Excludes the size of headers for fragmentation. -pub(crate) const MAX_UNASSOCIATED_PACKET_SIZE: usize = NoiseXKPattern1::SIZE - HEADER_SIZE; +pub(crate) const MAX_UNASSOCIATED_PACKET_SIZE: usize = NoiseXKPattern1::MAX_SIZE - HEADER_SIZE; /* XKhfs+psk2: @@ -168,10 +166,11 @@ pub(crate) struct NoiseXKPattern1 { pub noise_e1: [u8; KYBER_PUBLICKEYBYTES], /// -- end encrypted section pub e1_gcm_tag: [u8; AES_GCM_TAG_SIZE], - /// -- start AES-GCM(k_es) encrypted section - pub ratchet_fingerprint: [u8; RATCHET_FINGERPRINT_SIZE], - /// -- end encrypted section - pub p_gcm_tag: [u8; AES_GCM_TAG_SIZE], + pub payload: [u8; RATCHET_SIZE + RATCHET_SIZE + AES_GCM_TAG_SIZE + ChallengeResponse::SIZE], +} + +#[repr(C, packed)] +pub(crate) struct ChallengeResponse { pub challenge_counter: [u8; CHALLENGE_COUNTER_SIZE], pub challenge_mac: [u8; CHALLENGE_MAC_SIZE], pub challenge_pow: [u8; CHALLENGE_POW_SIZE], @@ -183,9 +182,12 @@ impl NoiseXKPattern1 { pub const E1_ENC_START: usize = Self::PROLOGUE_END + P384_PUBLIC_KEY_SIZE; pub const E1_AUTH_START: usize = Self::E1_ENC_START + KYBER_PUBLICKEYBYTES; pub const P_ENC_START: usize = Self::E1_AUTH_START + AES_GCM_TAG_SIZE; - pub const P_AUTH_START: usize = Self::P_ENC_START + RATCHET_FINGERPRINT_SIZE; - pub const P_AUTH_END: usize = Self::P_AUTH_START + AES_GCM_TAG_SIZE; - pub const SIZE: usize = Self::P_AUTH_END + CHALLENGE_COUNTER_SIZE + CHALLENGE_MAC_SIZE + CHALLENGE_POW_SIZE; + + pub const MIN_SIZE: usize = Self::P_ENC_START + AES_GCM_TAG_SIZE + ChallengeResponse::SIZE; + pub const MAX_SIZE: usize = Self::MIN_SIZE + RATCHET_SIZE + RATCHET_SIZE; +} +impl ChallengeResponse { + pub const SIZE: usize = CHALLENGE_COUNTER_SIZE + CHALLENGE_MAC_SIZE + CHALLENGE_POW_SIZE; } #[repr(C, packed)] @@ -269,16 +271,17 @@ impl ProtocolFlatBuffer for NoiseXKPattern1 {} impl ProtocolFlatBuffer for NoiseXKPattern2 {} impl ProtocolFlatBuffer for NoiseKKPattern1or2 {} impl ProtocolFlatBuffer for BobDOSChallenge {} +impl ProtocolFlatBuffer for ChallengeResponse {} #[inline(always)] pub(crate) fn byte_array_as_proto_buffer(b: &[u8]) -> &B { - assert_eq!(b.len(), size_of::()); + assert!(b.len() >= size_of::()); unsafe { &*b.as_ptr().cast() } } #[inline(always)] pub(crate) fn byte_array_as_proto_buffer_mut(b: &mut [u8]) -> &mut B { - assert_eq!(b.len(), size_of::()); + assert!(b.len() >= size_of::()); unsafe { &mut *b.as_mut_ptr().cast() } } /// Trick rust into letting us use a hasher that returns more than 64 bits. diff --git a/src/symmetric_state.rs b/src/symmetric_state.rs index 0c474d2..339123c 100644 --- a/src/symmetric_state.rs +++ b/src/symmetric_state.rs @@ -56,12 +56,17 @@ impl SymmetricState { temp_h } /// Corresponds to Noise `MixKeyAndHash` followed by `InitializeKey`. - pub(crate) fn mix_key_and_hash_initialize_key(&mut self, hm: &mut impl HmacSha512, input_key_material: &[u8]) -> ([u8; NOISE_HASHLEN], Secret) { + pub(crate) fn mix_key_and_hash_initialize_key( + &mut self, + hm: &mut impl HmacSha512, + input_key_material: &[u8], + ) -> ([u8; NOISE_HASHLEN], Secret) { let mut next_ck = Secret::new(); let mut temp_h = [0u8; NOISE_HASHLEN]; let mut temp_k = [0u8; NOISE_HASHLEN]; - self.kbkdf(hm, + self.kbkdf( + hm, input_key_material, self.label(), 3, @@ -79,7 +84,12 @@ impl SymmetricState { /// Based on Noise's unstable ASK mechanism, using KBKDF instead of HKDF. /// https://github.com/noiseprotocol/noise_wiki/wiki/Additional-Symmetric-Keys. #[inline(always)] - pub(crate) fn get_ask2(&self, hm: &mut impl HmacSha512, label: u8, noise_h: &[u8; NOISE_HASHLEN]) -> (Secret, Secret) { + pub(crate) fn get_ask2( + &self, + hm: &mut impl HmacSha512, + label: u8, + noise_h: &[u8; NOISE_HASHLEN], + ) -> (Secret, Secret) { let mut temp_k1 = [0u8; NOISE_HASHLEN]; let mut temp_k2 = [0u8; NOISE_HASHLEN]; self.kbkdf(hm, noise_h, [b'A', b'S', b'K', label], 2, &mut temp_k1, Some(&mut temp_k2), None); diff --git a/src/zssp.rs b/src/zssp.rs index dd750d6..8c4abef 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -20,9 +20,9 @@ use crate::crypto::aes::{AesDec, AesEnc}; use crate::crypto::aes_gcm::{AesGcmDec, AesGcmEnc, AES_GCM_IV_SIZE, AES_GCM_KEY_SIZE, AES_GCM_TAG_SIZE}; use crate::crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE, P384_PUBLIC_KEY_SIZE}; use crate::crypto::pqc_kyber::KYBER_SECRETKEYBYTES; -use crate::crypto::secret::{secure_eq, Secret}; -use crate::crypto::sha512::{Sha512, HmacSha512}; use crate::crypto::rand_core::RngCore; +use crate::crypto::secret::{secure_eq, Secret}; +use crate::crypto::sha512::{HmacSha512, Sha512}; use crate::applicationlayer::*; use crate::error::{FaultType, OpenError, ReceiveError, SendError}; @@ -77,14 +77,12 @@ pub enum ReceiveResult<'b, Application: ApplicationLayer> { pub enum SessionEvent<'b> { /// The received packet was valid, and it contained the necessary keys to fully establish a new /// session with Alice, the handshake initiator. - /// Contains the current ratchet number for metric purposes. /// /// If the session Arc returned is dropped, the session with this peer will be immediately /// terminated. Save the session Arc to some long lived datastructure to keep it alive. - NewSession(u64), + NewSession, /// When Alice calls `Context::open`, a session will be created, but Bob will not yet have /// received this session. They will have to successfully complete a handshake first. - /// Contains the current ratchet number for metric purposes. /// /// Alice will receive this return value when the received packet confirms both parties /// have completed the initial handshake and now have a shared session with each other. @@ -93,7 +91,7 @@ pub enum SessionEvent<'b> { /// /// This return value can only occur once per session, only for session objects that were /// created with `Context::open`. - Established(u64), + Established, /// Bob explicitly refused to establish a session with Alice, and sent us an error code. /// The application should immediately drop this session as Bob will not allow us to connect. /// @@ -101,9 +99,6 @@ pub enum SessionEvent<'b> { Rejected, /// The received packet was valid and a data payload was decoded and authenticated. Data(&'b mut [u8]), - /// The received packet completed a rekey event and a new ratchet key was derived. - /// Contains the current ratchet number for metric purposes. - Ratchet(u64), /// The received packet was some authentic protocol control packet. No action needs to be taken. Control, } @@ -163,9 +158,7 @@ unsafe impl Sync for Session {} /// Session state may only be mutated during atomic transitions of the offer state machine. struct SessionMutableState { - ratchet_number: u64, - ratchet_fingerprint: Secret, - ratchet_key: Secret, + ratchet_state: [Option; 2], /// For OOO rekeying reliability we allow our version of Noise CipherState to hold the last two /// session keys, instead of just the most recent one. cipher_states: [Option>; 2], @@ -199,9 +192,6 @@ enum OfferStateMachine { timeout: i64, noise_message: [u8; NoiseKKPattern1or2::SIZE], kex_send_key: Secret, - new_ratchet_number: u64, - new_ratchet_fingerprint: Secret, - new_ratchet_key: Secret, }, // -> Normal KeyConfirm { next_retry_time: AtomicI64, @@ -214,8 +204,7 @@ pub(crate) struct NoiseXKBobHandshakeState { local_key_id: NonZeroU32, header_receive_key: Secret, header_send_key: Secret, - ratchet_number: u64, - ratchet_fingerprint: Option<[u8; RATCHET_FINGERPRINT_SIZE]>, + ratchet_state: Option, noise_h_ee1peekem1pskp: [u8; NOISE_HASHLEN], noise_e_secret: Application::KeyPair, noise_ck_eseeekem1psk: SymmetricState, @@ -241,15 +230,14 @@ enum NoiseXKAliceHandshakeState { noise_ck_es: SymmetricState, /// ZSSP assumes an unreliable, out-of-order physical transport environment, so for that /// reason we have to resend key offers. - noise_message: [u8; NoiseXKPattern1::SIZE], + noise_message: [u8; NoiseXKPattern1::MAX_SIZE], + noise_message_len: usize, message_id: u64, }, NoiseXKPattern3 { noise_message: [u8; NoiseXKPattern3::MAX_SIZE], noise_message_len: usize, - new_ratchet_number: u64, - new_ratchet_fingerprint: Secret, - new_ratchet_key: Secret, + new_ratchet_state: RatchetState, }, } @@ -360,17 +348,17 @@ impl Context { } else { // We have to eventually time out NoiseXKPattern3 because of unreliable network conditions. if handshake_state.timeout <= current_time { + let ratchet_state = state.ratchet_state.clone(); drop(state); let _kex_lock = session.state_machine_lock.lock().unwrap(); let mut state = session.state.write().unwrap(); - let ratchet_fingerprint = state.ratchet_fingerprint.clone(); // Since we dropped the lock we must re-check if we are in the correct state. if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { if handshake_state.timeout <= current_time { app.event_log(LogEvent::ServiceXKTimeout(&session), current_time); if !handshake_state.reinitialize( &session, - &ratchet_fingerprint, + &ratchet_state, &mut self.0.session_map.write().unwrap(), &mut self.0.rng.lock().unwrap(), current_time, @@ -382,13 +370,13 @@ impl Context { } else if let Some((mut send, mut mtu)) = send_to(&session) { mtu = mtu.max(MIN_TRANSPORT_MTU); match &handshake_state.offer { - NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, message_id, .. } => { + NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } => { app.event_log(LogEvent::ServiceXK1Resend(&session), current_time); // We are in state NoiseXKPattern1 so resend noise_pattern1. send_with_fragmentation( &mut send, mtu, - &mut noise_message.clone(), + &mut noise_message.clone()[..*noise_message_len], PACKET_TYPE_NOISE_XK_PATTERN_1, None, *message_id, @@ -492,7 +480,7 @@ impl Context { mut mtu: usize, remote_s_public_key: Application::PublicKey, application_data: Application::Data, - ratchet_state: Option<(u64, [u8; RATCHET_FINGERPRINT_SIZE], [u8; RATCHET_KEY_SIZE])>, + mut ratchet_state: [Option; 2], local_identity_blob: Application::LocalIdentityBlob, current_time: i64, ) -> Result>, OpenError> { @@ -500,8 +488,17 @@ impl Context { if local_identity_blob.as_ref().len() > MAX_IDENTITY_BLOB_SIZE { return Err(OpenError::DataTooLarge); } - let (ratchet_number, mut ratchet_fingerprint, mut ratchet_key) = - ratchet_state.unwrap_or((0, [0; RATCHET_FINGERPRINT_SIZE], [0; RATCHET_KEY_SIZE])); + // Double check that the application gave us these in the correct order. + if let Some(first) = ratchet_state[0] { + if let Some(second) = ratchet_state[1] { + if first.ratchet_count < second.ratchet_count { + ratchet_state.swap(0, 1); + } + } + } else { + ratchet_state.swap(0, 1); + } + let sha512 = &mut Application::Hash::new(); let mut noise_kk_ss = Secret::new(); @@ -517,12 +514,8 @@ impl Context { let mut session_map = self.0.session_map.write().unwrap(); let local_key_id = generate_key_id(&session_map, &mut self.0.rng.lock().unwrap()); // Begin Noise XKhfs+psk2. - let (offer, a2b_header_key, b2a_header_key) = NoiseXKAliceHandshake::::initialize( - local_key_id, - &remote_s_public_key, - &ratchet_fingerprint, - &mut self.0.rng.lock().unwrap(), - )?; + let (offer, a2b_header_key, b2a_header_key) = + NoiseXKAliceHandshake::::initialize(local_key_id, &remote_s_public_key, &ratchet_state, &mut self.0.rng.lock().unwrap())?; let handshake_state = Box::new(NoiseXKAliceHandshake { next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), timeout: current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS), @@ -553,9 +546,7 @@ impl Context { counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), state_machine_lock: Mutex::new(()), state: RwLock::new(SessionMutableState { - ratchet_number, - ratchet_fingerprint: Secret::from_bytes_then_nuke(&mut ratchet_fingerprint), - ratchet_key: Secret::from_bytes_then_nuke(&mut ratchet_key), + ratchet_state: ratchet_state.clone(), cipher_states: [None, None], // Points at 1 until the first key is confirmed. current_key: 1, @@ -619,7 +610,7 @@ impl Context { &self, app: &Application, check_allow_incoming_session: impl FnOnce() -> IncomingSessionAction, - check_accept_session: impl FnOnce(&Application::PublicKey, &[u8], Option<&[u8; RATCHET_FINGERPRINT_SIZE]>) -> AcceptSessionAction, + check_accept_session: impl FnOnce(&Application::PublicKey, &[u8]) -> AcceptSessionAction, mut send_unassociated_reply: impl FnMut(&mut [u8]) -> bool, mut send_unassociated_mtu: usize, mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, @@ -876,31 +867,37 @@ impl Context { debug_assert!(fragments.len() >= 1); debug_assert!(incoming.is_none() || session.is_none()); - let mut pkt_assembly_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; - let message_size = assemble_fragments_into::(fragments, &mut pkt_assembly_buffer)?; + let message = &mut [0u8; MAX_NOISE_HANDSHAKE_SIZE]; + let message_size = assemble_fragments_into::(fragments, message)?; if message_size < MIN_PACKET_SIZE { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } - let message = &mut pkt_assembly_buffer[..message_size]; use OfferStateMachine::*; match packet_type { PACKET_TYPE_NOISE_XK_PATTERN_1 => { - app.event_log(LogEvent::ReceiveUncheckedXK1, current_time); // Alice (remote) --> Bob (local) // -> e, es, e1 + app.event_log(LogEvent::ReceiveUncheckedXK1, current_time); + if session.is_some() || incoming.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); } - if message.len() != NoiseXKPattern1::SIZE { + if message.len() < NoiseXKPattern1::MIN_SIZE || message.len() > NoiseXKPattern1::MIN_SIZE { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } - let noise_pattern1: &NoiseXKPattern1 = byte_array_as_proto_buffer(message); // The message id must be the first 8 bytes of the gcm tag. // This forces the message id to be authenticated along with the entire message. - if noise_pattern1.header[8..] != noise_pattern1.p_gcm_tag[8..] { + let challenge_start_idx = message_size - ChallengeResponse::SIZE; + if message[8..] != message[challenge_start_idx - 8..challenge_start_idx] { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } + let total_ratchet_fingerprints = (challenge_start_idx - AES_GCM_TAG_SIZE) / RATCHET_SIZE; + if (challenge_start_idx - AES_GCM_TAG_SIZE) % RATCHET_SIZE != 0 || total_ratchet_fingerprints > 2 { + return Err(byzantine_fault!(FaultType::InvalidPacket, false)); + } + + let noise_pattern1: &NoiseXKPattern1 = byte_array_as_proto_buffer(message); if let Some(remote_key_id) = NonZeroU32::new(u32::from_ne_bytes(noise_pattern1.alice_key_id)) { let sha512 = &mut Application::Hash::new(); // Let application filter incoming connection attempts by whatever criteria it wants. @@ -908,20 +905,21 @@ impl Context { match check_allow_incoming_session() { IncomingSessionAction::Allow => {} IncomingSessionAction::Challenge => { + let response: &ChallengeResponse = byte_array_as_proto_buffer(&message[challenge_start_idx..message_size]); let mut counter = 0u64.to_ne_bytes(); - counter.copy_from_slice(&noise_pattern1.challenge_counter); + counter.copy_from_slice(&response.challenge_counter); let counter = u64::from_be_bytes(counter); sha512.reset(); let mut hasher = ShaHasher(sha512); let mut output = [0u8; NOISE_HASHLEN]; - hasher.0.update(&noise_pattern1.challenge_counter); + hasher.0.update(&response.challenge_counter); remote_address.hash(&mut hasher); hasher.0.update(&self.0.challenge_salt); hasher.0.finish(&mut output); let is_valid = self.check_challenge_window(counter) - && secure_eq(&output[..CHALLENGE_MAC_SIZE], &noise_pattern1.challenge_mac) - && verify_pow::(&mut hasher.0, &message) + && secure_eq(&output[..CHALLENGE_MAC_SIZE], &response.challenge_mac) + && verify_pow::(&mut hasher.0, &message[challenge_start_idx..message_size]) && self.update_challenge_window(counter); app.event_log(LogEvent::ReceiveCheckXK1Challenge(is_valid), current_time); if !is_valid { @@ -940,7 +938,7 @@ impl Context { hasher.0.update(&self.0.challenge_salt); hasher.0.finish(&mut output); challenge.challenge_mac.copy_from_slice(&output[..CHALLENGE_MAC_SIZE]); - challenge.prior_challenge_pow = noise_pattern1.challenge_pow; + challenge.prior_challenge_pow = response.challenge_pow; // We haven't decrypted any of Alice's packet so we don't know the // header protection cipher. // For DOS resistance Alice will not accept unencrypted headers directly @@ -973,9 +971,8 @@ impl Context { let mut noise_ck = SymmetricState::new(INITIAL_H); let hmac = &mut Application::HmacHash::new(); let mut noise_es = Secret::new(); - let noise_e_pattern1 = - from_bytes_agreement::(&noise_pattern1.noise_e, &self.0.static_keypair, noise_es.as_mut()) - .ok_or(byzantine_fault!(FaultType::FailedAuthentication, false))?; + let noise_e_pattern1 = from_bytes_agreement::(&noise_pattern1.noise_e, &self.0.static_keypair, noise_es.as_mut()) + .ok_or(byzantine_fault!(FaultType::FailedAuthentication, false))?; let noise_h_e = mix_hash(sha512, &noise_h, &noise_pattern1.noise_e); noise_ck.mix_key(hmac, &noise_pattern1.noise_e); // Noise process pattern1 es token. @@ -1006,7 +1003,7 @@ impl Context { &noise_h_ee1, packet_type, 1, - &mut message[NoiseXKPattern1::P_ENC_START..NoiseXKPattern1::P_AUTH_END], + &mut message[NoiseXKPattern1::P_ENC_START..challenge_start_idx], ); drop(noise_k_es); if !is_auth { @@ -1014,22 +1011,24 @@ impl Context { } let (header_b2a_key, header_a2b_key) = noise_ck.get_ask2(hmac, LABEL_HEADER_KEY, &noise_h_ee1p); // Get ratchet key. - let noise_pattern1: &mut NoiseXKPattern1 = byte_array_as_proto_buffer_mut(message); - use crate::RestoreAction::*; - let (sent_zero, ratchet_number, ratchet_key) = if noise_pattern1.ratchet_fingerprint == [0u8; RATCHET_FINGERPRINT_SIZE] { - if app.hello_requires_recognized_ratchet(current_time) { - return Ok(ReceiveResult::Rejected); - } else { - (true, 0, [0u8; RATCHET_KEY_SIZE]) - } - } else { - match app.restore_ratchet(&noise_pattern1.ratchet_fingerprint, current_time) { - Ok(RestoreRatchet(ratchet_number, ratchet_key)) => (false, ratchet_number, ratchet_key), - Ok(DowngradeRatchet) => (false, 0, [0u8; RATCHET_KEY_SIZE]), - Ok(FailAuthentication) => return Err(byzantine_fault!(FaultType::FailedAuthentication, false)), + let noise_pattern1: &NoiseXKPattern1 = byte_array_as_proto_buffer(message); + let mut ratchet_state = None; + for i in 0..total_ratchet_fingerprints { + match app.restore_ratchet( + (&noise_pattern1.payload[i * RATCHET_SIZE..(i + 1) * RATCHET_SIZE]).try_into().unwrap(), + current_time, + ) { + Ok(Some(rs)) => { + ratchet_state = Some(rs); + break; + } + Ok(None) => {} Err(()) => return Err(ReceiveError::RatchetIoError), } - }; + } + if ratchet_state.is_none() && app.hello_requires_recognized_ratchet(current_time) { + return Ok(ReceiveResult::Rejected); + } // Start of Noise XKhfs+psk2 pattern2. let mut message2 = [0u8; NoiseXKPattern2::SIZE]; @@ -1064,7 +1063,8 @@ impl Context { noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); drop(noise_ekem1_secret); // Noise process pattern2 psk token. - let (temp_h, noise_k_eseeekem1psk) = noise_ck.mix_key_and_hash_initialize_key(hmac, &ratchet_key); + let ratchet_key = ratchet_state.map(|rs| rs.key.as_ref()).unwrap_or(&[0; RATCHET_SIZE]); + let (temp_h, noise_k_eseeekem1psk) = noise_ck.mix_key_and_hash_initialize_key(hmac, ratchet_key); let noise_h_ee1peekem1psk = mix_hash(sha512, &noise_h_ee1peekem1, &temp_h); // Noise process pattern2 payload. // We try to prevent the id we generate from colliding with another session but @@ -1087,8 +1087,7 @@ impl Context { let handshake = Arc::new(NoiseXKBobHandshakeState { local_key_id, remote_key_id, - ratchet_number, - ratchet_fingerprint: (!sent_zero).then(|| noise_pattern1.ratchet_fingerprint), + ratchet_state, noise_h_ee1peekem1pskp, noise_ck_eseeekem1psk: noise_ck.clone(), noise_k_eseeekem1psk: noise_k_eseeekem1psk.clone(), @@ -1121,7 +1120,9 @@ impl Context { } } PACKET_TYPE_BOB_DOS_CHALLENGE => { + let message = &mut message[..message_size]; app.event_log(LogEvent::ReceiveUncheckedDOSChallenge, current_time); + // We expect Bob to only send this to us through our unassociated defrag cache. if incoming.is_some() || session.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); @@ -1136,23 +1137,25 @@ impl Context { // We don't need to hold the kex lock because we are not transitioning state. let mut state = session.state.write().unwrap(); if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { - if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, .. } = &mut handshake_state.offer { - let pattern1: &mut NoiseXKPattern1 = byte_array_as_proto_buffer_mut(noise_message); + if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, .. } = &mut handshake_state.offer { + let response_raw = &mut noise_message[*noise_message_len - ChallengeResponse::SIZE..]; + + let response: &ChallengeResponse = byte_array_as_proto_buffer_mut(response_raw); // Only people who know what Alice's prior pow was can convince us to // compute a new pow. - if challenge.prior_challenge_pow != pattern1.challenge_pow { + if challenge.prior_challenge_pow != response.challenge_pow { // This can occur if Bob sends us multiple challenges and they // arrive OOO. return Err(byzantine_fault!(FaultType::FailedAuthentication, true)); } - pattern1.challenge_counter.copy_from_slice(&challenge.challenge_counter); - pattern1.challenge_mac.copy_from_slice(&challenge.challenge_mac); + response.challenge_counter.copy_from_slice(&challenge.challenge_counter); + response.challenge_mac.copy_from_slice(&challenge.challenge_mac); let mut pow = self.0.rng.lock().unwrap().next_u64(); let sha512 = &mut Application::Hash::new(); loop { - let pattern1: &mut NoiseXKPattern1 = byte_array_as_proto_buffer_mut(noise_message); - pattern1.challenge_pow.copy_from_slice(&pow.to_be_bytes()); - if verify_pow::(sha512, noise_message) { + let response: &ChallengeResponse = byte_array_as_proto_buffer(response_raw); + response.challenge_pow.copy_from_slice(&pow.to_be_bytes()); + if verify_pow::(sha512, response_raw) { break; } pow = pow.wrapping_add(1); @@ -1179,9 +1182,11 @@ impl Context { } } PACKET_TYPE_NOISE_XK_PATTERN_2 => { - app.event_log(LogEvent::ReceiveUncheckedXK2, current_time); // Bob (remote) --> Alice (local) // <- e, ee, ekem1, psk + let message = &mut message[..message_size]; + app.event_log(LogEvent::ReceiveUncheckedXK2, current_time); + if incoming.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); } @@ -1231,51 +1236,81 @@ impl Context { if let Some(Ok(noise_ekem1_secret)) = is_auth.then_some(noise_ekem1_secret) { noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); drop(noise_ekem1_secret); - // We attempt to decrypt the payload at most twice. First time with - // the ratchet key Alice last remembers, and second time with a ratchet + // We attempt to decrypt the payload at most three times. First two times with + // the ratchet key Alice remembers, and final time with a ratchet // key of zero if Alice allows ratchet downgrades. - let mut ratchet_number = state.ratchet_number; - let mut ratchet_key = state.ratchet_key.as_ref(); - let mut ratchet_result = None; - for i in 0..2 { - // Constant time ratchet key downgrade check. - let mut noise_ck_ratchet = noise_ck.clone(); + + let test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState, Secret, [u8; 64])> { + // Check for which ratchet key Bob wants to use. + let mut noise_ck = noise_ck.clone(); let mut payload = [0u8; NoiseXKPattern2::P_AUTH_END - NoiseXKPattern2::P_ENC_START]; payload.copy_from_slice(&message[NoiseXKPattern2::P_ENC_START..NoiseXKPattern2::P_AUTH_END]); // Noise process pattern2 psk token. - let (temp_h, noise_k_ratchet) = noise_ck_ratchet.mix_key_and_hash_initialize_key(hmac, ratchet_key); + let (temp_h, noise_k_eseeekem1psk) = noise_ck.mix_key_and_hash_initialize_key(hmac, ratchet_key); let noise_h_ee1peekem1psk = mix_hash(sha512, &noise_h_ee1peekem1, &temp_h); // Noise process pattern2 payload. - let (is_auth, noise_h_ratchet) = decrypt_and_hash::( + let (is_auth, noise_h_ee1peekem1pskp) = decrypt_and_hash::( sha512, - &noise_k_ratchet, + &noise_k_eseeekem1psk, &noise_h_ee1peekem1psk, packet_type, 0, &mut payload, ); - let mut key_id = 0u32.to_ne_bytes(); - key_id.copy_from_slice(&payload[..NoiseXKPattern2::P_AUTH_START - NoiseXKPattern2::P_ENC_START]); - if is_auth { - if i > 0 { - ratchet_result = - NonZeroU32::new(u32::from_ne_bytes(key_id)).map(|id| (id, noise_k_ratchet, noise_h_ratchet)); - noise_ck = noise_ck_ratchet.clone(); - break; - } + let key_id = NonZeroU32::new(u32::from_ne_bytes( + (&payload[..NoiseXKPattern2::P_AUTH_START - NoiseXKPattern2::P_ENC_START]) + .try_into() + .unwrap(), + )); + key_id.map(|kid| (kid, noise_ck, noise_k_eseeekem1psk, noise_h_ee1peekem1pskp)) } else { - if i > 0 || app.initiator_disallows_downgrade(&session, current_time) { - break; + None + } + }; + // Check first key. + let mut ratchet_i = 0; + let mut is_auth = false; + let mut ratchet_number = 0; + let remote_key_id; + let noise_k_eseeekem1psk; + let noise_h_ee1peekem1pskp; + if let Some(rs) = state.ratchet_state[0] { + if let Some((key_id, ck, k, h)) = test_ratchet_key(rs.key.as_ref()) { + ratchet_number = rs.ratchet_count; + remote_key_id = key_id; + noise_ck = ck; + noise_k_eseeekem1psk = k; + noise_h_ee1peekem1pskp = h; + is_auth = true; + } + } + // Check second key. + if !is_auth { + ratchet_i = 1; + if let Some(rs) = state.ratchet_state[1] { + if let Some((key_id, ck, k, h)) = test_ratchet_key(rs.key.as_ref()) { + ratchet_number = rs.ratchet_count; + remote_key_id = key_id; + noise_ck = ck; + noise_k_eseeekem1psk = k; + noise_h_ee1peekem1pskp = h; + is_auth = true; } - // If auth failed maybe Bob wants to downgrade the ratchet, - // retry decryption with no ratchet if we have not already. - ratchet_number = 0; - ratchet_key = &[0u8; RATCHET_KEY_SIZE]; + } + } + // Check zero key. + if !is_auth { + if let Some((key_id, ck, k, h)) = test_ratchet_key(&[0u8; RATCHET_SIZE]) { + noise_ck = ck; + remote_key_id = key_id; + noise_k_eseeekem1psk = k; + noise_h_ee1peekem1pskp = h; + is_auth = true; } } - if let Some((remote_key_id, noise_k_eseeekem1psk, noise_h_ee1peekem1pskp)) = ratchet_result { + if is_auth { // Start of Noise XKhfs+psk2 pattern3. let mut message3 = [0u8; NoiseXKPattern3::MAX_SIZE]; // Noise process pattern3 s token. @@ -1316,21 +1351,24 @@ impl Context { drop(noise_k_eseeekem1pskse); // Alice finished Noise XKhfs+psk2 handshake. // Transition offer state machine to the NoiseXKPattern3 state. - let new_ratchet_number = ratchet_number + 1; let (new_ratchet_key, new_ratchet_fingerprint) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); - let result = app.save_ratchet_state( - &session.remote_s_public_key, - &session.application_data, - SaveAction::SaveAsUnconfirmed, - new_ratchet_number, - new_ratchet_fingerprint.as_ref(), - new_ratchet_key.as_ref(), - current_time, - ); + let new_ratchet_state = RatchetState { + fingerprint: new_ratchet_fingerprint.0, + key: new_ratchet_key, + ratchet_count: ratchet_number + 1, + }; + let action = if state.ratchet_state[0].is_some() && state.ratchet_state[1].is_some() { + SaveAction::DeleteThenAddRatchet(&state.ratchet_state[1 - ratchet_i].unwrap(), &new_ratchet_state) + } else { + SaveAction::AddRatchet(&new_ratchet_state) + }; + let result = + app.save_ratchet_state(&session.remote_s_public_key, &session.application_data, action, current_time); if result.is_err() { return Err(ReceiveError::RatchetIoError); } + let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_ee1peekem1pskpsp); let local_key_id = handshake_state.local_key_id; @@ -1346,8 +1384,17 @@ impl Context { .lock() .unwrap() .replace(Application::AeadDec::new(kex_key_b2a.as_ref())); + state.ratchet_state[1] = state.ratchet_state[ratchet_i]; + state.ratchet_state[0] = Some(new_ratchet_state.clone()); - state.cipher_states[0].replace(SessionKey::new(hmac, noise_ck, local_key_id, remote_key_id, INIT_COUNTER, false)); + state.cipher_states[0].replace(SessionKey::new( + hmac, + noise_ck, + local_key_id, + remote_key_id, + INIT_COUNTER, + false, + )); debug_assert!(state.cipher_states[1].is_none()); if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { handshake_state.next_retry_time = @@ -1356,9 +1403,7 @@ impl Context { handshake_state.offer = NoiseXKAliceHandshakeState::NoiseXKPattern3 { noise_message: message3, noise_message_len: p_auth_end, - new_ratchet_number, - new_ratchet_fingerprint: new_ratchet_fingerprint.clone(), - new_ratchet_key: new_ratchet_key.clone(), + new_ratchet_state: new_ratchet_state.clone(), }; } drop(state); @@ -1386,11 +1431,10 @@ impl Context { // We restart the offer instead of dropping the session to defend against DOS. drop(state); let mut state = session.state.write().unwrap(); - let ratchet_fingerprint = state.ratchet_fingerprint.clone(); if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { if !handshake_state.reinitialize( &session, - &ratchet_fingerprint, + &state.ratchet_state, &mut self.0.session_map.write().unwrap(), &mut self.0.rng.lock().unwrap(), current_time, @@ -1472,11 +1516,7 @@ impl Context { // Bob finished Noise XKhfs+psk2 handshake. let header_send_cipher = Application::PrpEnc::new(handshake_state.header_send_key.as_ref()); let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_ee1peekem1pskpsp); - match check_accept_session( - &remote_s_public_key, - &message[p_enc_start..p_auth_start], - handshake_state.ratchet_fingerprint.as_ref(), - ) { + match check_accept_session(&remote_s_public_key, &message[p_enc_start..p_auth_start]) { AcceptSessionAction::Accept(application_data) => { let mut noise_kk_ss = Secret::new(); if !self.0.static_keypair.agree(&remote_s_public_key, noise_kk_ss.as_mut()) { @@ -1489,14 +1529,15 @@ impl Context { let (new_ratchet_key, new_ratchet_fingerprint) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); // We must make sure the ratchet key is saved before we transition. - let new_ratchet_number = handshake_state.ratchet_number + 1; + let new_ratchet_state = RatchetState { + fingerprint: new_ratchet_fingerprint.0, + key: new_ratchet_key, + ratchet_count: handshake_state.ratchet_state.map(|rs| rs.ratchet_count + 1).unwrap_or(1), + }; let result = app.save_ratchet_state( &remote_s_public_key, &application_data, - SaveAction::SaveAsConfirmed, - new_ratchet_number, - new_ratchet_fingerprint.as_ref(), - new_ratchet_key.as_ref(), + SaveAction::VerifyThenOverwriteRatchet(handshake_state.ratchet_state.as_ref(), &new_ratchet_state), current_time, ); if result.is_err() { @@ -1515,9 +1556,7 @@ impl Context { counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), state_machine_lock: Mutex::new(()), state: RwLock::new(SessionMutableState { - ratchet_number: new_ratchet_number, - ratchet_fingerprint: new_ratchet_fingerprint.clone(), - ratchet_key: new_ratchet_key.clone(), + ratchet_state: [Some(new_ratchet_state.clone()), None], cipher_states: [ Some(SessionKey::new( hmac, @@ -1558,7 +1597,7 @@ impl Context { let _ = session.send_control(&session.state.read().unwrap(), send_unassociated_reply, PACKET_TYPE_KEY_CONFIRM, &[]); app.event_log(LogEvent::ReceiveValidXK3(&session.application_data), current_time); - return Ok(ReceiveResult::Session(session, SessionEvent::NewSession(new_ratchet_number))); + return Ok(ReceiveResult::Session(session, SessionEvent::NewSession)); } else { // This can occur if we accidentally generate a key id collision. // There is an extremely short amount of time during which @@ -1717,7 +1756,7 @@ fn initiate_rekey( // Start of Noise KKpsk0 pattern1. // Noise process pattern1 psk0 token. let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_key.as_ref()); + let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].unwrap().key.as_ref()); let noise_h_psk = mix_hash(sha512, &session.noise_kk_local_init_h, &noise_temp_h); // Noise process pattern1 e token. let noise_e_secret = Application::KeyPair::generate(&mut context.rng.lock().unwrap()); @@ -1806,41 +1845,30 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let mut state = session.state.write().unwrap(); // We only want to stop sending NoiseKKPattern2 offers when the latest derived // key is confirmed. And we only want to do that once. - let (used_latest_key, key_confirmed, ret) = match &state.outgoing_offer { - NoiseKKPattern2 { - new_ratchet_number, new_ratchet_fingerprint, new_ratchet_key, .. - } => ( - true, - Some((*new_ratchet_number, new_ratchet_fingerprint.clone(), new_ratchet_key.clone())), - SessionEvent::Ratchet(*new_ratchet_number), - ), + let (used_latest_key, try_delete, ret) = match &state.outgoing_offer { + NoiseKKPattern2 { .. } => (true, true, SessionEvent::Control), NoiseXKPattern1or3(handshake_state) => { - if let NoiseXKAliceHandshakeState::NoiseXKPattern3 { - new_ratchet_number, new_ratchet_fingerprint, new_ratchet_key, .. - } = &handshake_state.offer - { - ( - true, - Some((*new_ratchet_number, new_ratchet_fingerprint.clone(), new_ratchet_key.clone())), - SessionEvent::Established(*new_ratchet_number), - ) + if let NoiseXKAliceHandshakeState::NoiseXKPattern3 { .. } = &handshake_state.offer { + (true, true, SessionEvent::Established) } else { - (false, None, SessionEvent::Control) + (false, false, SessionEvent::Control) } } - _ => (true, None, SessionEvent::Control), + _ => (true, false, SessionEvent::Control), }; - if let Some(ratchet) = key_confirmed { - let result = app.save_ratchet_state( - &session.remote_s_public_key, - &session.application_data, - SaveAction::ConfirmLatestAndDeletePrevious, - ratchet.0, - ratchet.1.as_ref(), - ratchet.2.as_ref(), - current_time, - ); - if result.is_ok() { + if try_delete { + let is_ok = if let Some(rs) = &state.ratchet_state[1] { + app.save_ratchet_state( + &session.remote_s_public_key, + &session.application_data, + SaveAction::DeleteRatchet(rs), + current_time, + ) + .is_ok() + } else { + true + }; + if is_ok { if let NoiseKKPattern2 { kex_send_key, .. } = &state.outgoing_offer { session .kex_send_cipher @@ -1848,9 +1876,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu .unwrap() .replace(Application::AeadEnc::new(kex_send_key.as_ref())); } - state.ratchet_number = ratchet.0; - state.ratchet_fingerprint.overwrite(&ratchet.1); - state.ratchet_key.overwrite(&ratchet.2); + state.ratchet_state[1] = None; state.current_key ^= 1; state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); } else { @@ -1861,34 +1887,20 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu drop(kex_lock); if used_latest_key { if let Some((send, _)) = send_to(&session) { - let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_KEY_DELETE, &[]); + let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_ACK, &[]); } } Ok(ReceiveResult::Session(session, ret)) } - PACKET_TYPE_KEY_DELETE => { + PACKET_TYPE_ACK => { drop(state); app.event_log(LogEvent::ReceiveValidKeyDelete(&session), current_time); let kex_lock = session.state_machine_lock.lock().unwrap(); let mut state = session.state.write().unwrap(); - // Check if we should end any current offers and transition - // back to the None state + // Check if we should end any current offers and transition back to Normal state match &state.outgoing_offer { KeyConfirm { .. } => { - let result = app.save_ratchet_state( - &session.remote_s_public_key, - &session.application_data, - SaveAction::DeletePrevious, - state.ratchet_number, - state.ratchet_fingerprint.as_ref(), - state.ratchet_key.as_ref(), - current_time, - ); - if result.is_ok() { - state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); - } else { - return Err(ReceiveError::RatchetIoError); - } + state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); } _ => (), } @@ -1926,16 +1938,14 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let sha512 = &mut Application::Hash::new(); let hmac = &mut Application::HmacHash::new(); let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_key.as_ref()); + let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].unwrap().key.as_ref()); let noise_h_psk = mix_hash(sha512, &session.noise_kk_remote_init_h, &noise_temp_h); // Noise process pattern1 e token. // Get public key validation out of the way early let mut noise_es = Secret::new(); let mut noise_ee = Secret::new(); let mut noise_se = Secret::new(); - if let Some(alice_e) = - from_bytes_agreement::(&noise_pattern1.noise_e, &context.0.static_keypair, noise_es.as_mut()) - { + if let Some(alice_e) = from_bytes_agreement::(&noise_pattern1.noise_e, &context.0.static_keypair, noise_es.as_mut()) { let bob_e_secret = Application::KeyPair::generate(&mut context.0.rng.lock().unwrap()); if bob_e_secret.agree(&alice_e, noise_ee.as_mut()) && bob_e_secret.agree(&session.remote_s_public_key, noise_se.as_mut()) { let noise_h_pske = mix_hash(sha512, &noise_h_psk, alice_e.as_bytes()); @@ -1987,15 +1997,16 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu ); drop(noise_k_pskessseese); // Bob finished Noise KKpsk0 handshake. - let new_ratchet_number = state.ratchet_number + 1; let (new_ratchet_key, new_ratchet_fingerprint) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); + let new_ratchet_state = RatchetState { + fingerprint: new_ratchet_fingerprint.0, + key: new_ratchet_key, + ratchet_count: state.ratchet_state[0].map(|rs| rs.ratchet_count + 1).unwrap_or(1), + }; let result = app.save_ratchet_state( &session.remote_s_public_key, &session.application_data, - SaveAction::SaveAsUnconfirmed, - new_ratchet_number, - new_ratchet_fingerprint.as_ref(), - new_ratchet_key.as_ref(), + SaveAction::AddRatchet(&new_ratchet_state), current_time, ); if result.is_err() { @@ -2021,17 +2032,23 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu .lock() .unwrap() .replace(Application::AeadDec::new(kex_key_a2b.as_ref())); + state.ratchet_state[1] = state.ratchet_state[0]; + state.ratchet_state[0] = Some(new_ratchet_state.clone()); - state.cipher_states[next_key_index].replace(SessionKey::new(hmac, noise_ck, new_key_id, remote_key_id, current_counter, true)); + state.cipher_states[next_key_index].replace(SessionKey::new( + hmac, + noise_ck, + new_key_id, + remote_key_id, + current_counter, + true, + )); let timer = current_time.saturating_add(Application::RETRY_INTERVAL_MS); state.outgoing_offer = NoiseKKPattern2 { next_retry_time: AtomicI64::new(timer), timeout: current_time.saturating_add(Application::EXPIRATION_TIMEOUT_MS), noise_message: message2, kex_send_key: kex_key_b2a.clone(), - new_ratchet_number, - new_ratchet_fingerprint: new_ratchet_fingerprint.clone(), - new_ratchet_key: new_ratchet_key.clone(), }; drop(state); drop(kex_lock); @@ -2085,16 +2102,16 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu if let (true, Some(remote_key_id)) = (is_auth, NonZeroU32::new(u32::from_ne_bytes(noise_pattern2.key_id))) { // Bob fully authenticated. // Alice finished Noise KKpsk0 handshake. - let new_ratchet_number = state.ratchet_number + 1; let (new_ratchet_key, new_ratchet_fingerprint) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); - + let new_ratchet_state = RatchetState { + fingerprint: new_ratchet_fingerprint.0, + key: new_ratchet_key, + ratchet_count: state.ratchet_state[0].map(|rs| rs.ratchet_count + 1).unwrap_or(1), + }; let result = app.save_ratchet_state( &session.remote_s_public_key, &session.application_data, - SaveAction::SaveAsConfirmed, - new_ratchet_number, - new_ratchet_fingerprint.as_ref(), - new_ratchet_key.as_ref(), + SaveAction::DeleteThenAddRatchet(&state.ratchet_state[0].unwrap(), &new_ratchet_state), current_time, ); if result.is_err() { @@ -2122,10 +2139,8 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu .lock() .unwrap() .replace(Application::AeadEnc::new(kex_key_a2b.as_ref())); - - state.ratchet_number = new_ratchet_number; - state.ratchet_fingerprint.overwrite_first_n(&new_ratchet_fingerprint); - state.ratchet_key.overwrite(&new_ratchet_key); + state.ratchet_state[1] = None; + state.ratchet_state[0] = Some(new_ratchet_state.clone()); state.cipher_states[next_key_index].replace(SessionKey::new( hmac, @@ -2146,7 +2161,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_KEY_CONFIRM, &[]); } app.event_log(LogEvent::ReceiveValidKK2(&session), current_time); - return Ok(ReceiveResult::Session(session, SessionEvent::Ratchet(new_ratchet_number))); + return Ok(ReceiveResult::Session(session, SessionEvent::Control)); } } } @@ -2207,14 +2222,14 @@ impl Session { /// The most recent confirmed ratchet state of this session. /// The returned values are sensitive and should be securely erased before being dropped. #[inline] - pub fn ratchet_state(&self) -> (u64, [u8; RATCHET_FINGERPRINT_SIZE], [u8; RATCHET_KEY_SIZE]) { + pub fn ratchet_state(&self) -> [Option; 2] { let state = self.state.read().unwrap(); - (state.ratchet_number, *state.ratchet_fingerprint.as_ref(), *state.ratchet_key.as_ref()) + state.ratchet_state.clone() } /// The most recent confirmed ratchet number of this session. #[inline] - pub fn ratchet_number(&self) -> u64 { - self.state.read().unwrap().ratchet_number + pub fn ratchet_count(&self) -> u64 { + self.state.read().unwrap().ratchet_state[0].map(|rs| rs.ratchet_count).unwrap_or(0) } /// Mark a session as expired. This will make it impossible for this session to successfully /// receive or send data. It is recommended to simply `drop` the session instead, but this can @@ -2298,7 +2313,7 @@ impl NoiseXKAliceHandshake { fn initialize( local_key_id: NonZeroU32, remote_s_public_key: &Application::PublicKey, - ratchet_fingerprint: &[u8; RATCHET_FINGERPRINT_SIZE], + ratchet_state: &[Option; 2], rng: &mut Application::Rng, ) -> Result< ( @@ -2308,7 +2323,7 @@ impl NoiseXKAliceHandshake { ), OpenError, > { - let mut message = [0u8; NoiseXKPattern1::SIZE]; + let mut message = [0u8; NoiseXKPattern1::MAX_SIZE]; let sha512 = &mut Application::Hash::new(); let hmac = &mut Application::HmacHash::new(); // Start of Noise XKhfs+psk2 pattern1. @@ -2318,7 +2333,6 @@ impl NoiseXKAliceHandshake { noise_pattern1.alice_key_id = local_key_id.get().to_ne_bytes(); noise_pattern1.noise_e = noise_e_secret.public_key_bytes().clone(); noise_pattern1.noise_e1 = noise_e1_secret.public; - noise_pattern1.ratchet_fingerprint = *ratchet_fingerprint; // Noise process prologue. let noise_h = mix_hash( sha512, @@ -2347,26 +2361,39 @@ impl NoiseXKAliceHandshake { &mut message[NoiseXKPattern1::E1_ENC_START..NoiseXKPattern1::P_ENC_START], ); // Noise process pattern1 payload. + let mut idx = 0; + for r in ratchet_state { + if let Some(rs) = r { + let next_idx = idx + RATCHET_SIZE; + noise_pattern1.payload[idx..next_idx].copy_from_slice(&rs.fingerprint); + idx = next_idx; + } + } + idx += AES_GCM_TAG_SIZE; + let noise_h_ee1p = encrypt_and_hash::( sha512, &noise_k_es, &noise_h_ee1, PACKET_TYPE_NOISE_XK_PATTERN_1, 1, - &mut message[NoiseXKPattern1::P_ENC_START..NoiseXKPattern1::P_AUTH_END], + &mut message[NoiseXKPattern1::P_ENC_START..idx], ); drop(noise_k_es); let (header_b2a_key, header_a2b_key) = noise_ck.get_ask2(hmac, LABEL_HEADER_KEY, &noise_h_ee1p); - let mut pattern1_id = 0u64.to_ne_bytes(); - pattern1_id.copy_from_slice(&message[NoiseXKPattern1::P_AUTH_START + 8..NoiseXKPattern1::P_AUTH_END]); + let message_id = u64::from_be_bytes(message[idx - 8..idx].try_into().unwrap()); + + idx += ChallengeResponse::SIZE; + message[idx - CHALLENGE_POW_SIZE..idx].copy_from_slice(&rng.next_u64().to_ne_bytes()); Ok(( NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_h_ee1p, noise_e_secret, noise_e1_secret: Secret(noise_e1_secret.secret), noise_ck_es: noise_ck, + noise_message_len: idx, noise_message: message, - message_id: u64::from_be_bytes(pattern1_id), + message_id, }, header_a2b_key, header_b2a_key, @@ -2376,15 +2403,13 @@ impl NoiseXKAliceHandshake { fn reinitialize( &mut self, session: &Arc>, - ratchet_fingerprint: &Secret, + ratchet_state: &[Option; 2], session_map: &mut HashMap>, bool)>, rng: &mut Application::Rng, current_time: i64, ) -> bool { let local_key_id = generate_key_id(session_map, rng); - if let Ok((offer, a2b_header_key, b2a_header_key)) = - Self::initialize(local_key_id, &session.remote_s_public_key, ratchet_fingerprint.as_ref(), rng) - { + if let Ok((offer, a2b_header_key, b2a_header_key)) = Self::initialize(local_key_id, &session.remote_s_public_key, ratchet_state, rng) { self.timeout = current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS); session_map.remove(&self.local_key_id); session_map.insert(local_key_id, (Arc::downgrade(session), false)); @@ -2684,12 +2709,12 @@ fn mix_hash(hasher: &mut impl Sha512, h: &[u8; NOISE_HASHLEN], m: &[u8]) -> [u8; /// Check if the proof of work attached to the first message contains the correct number of leading /// zeros. #[inline(always)] -fn verify_pow(hasher: &mut Application::Hash, message: &[u8]) -> bool { +fn verify_pow(hasher: &mut Application::Hash, response: &[u8]) -> bool { if Application::PROOF_OF_WORK_BIT_DIFFICULTY == 0 { return true; } hasher.reset(); - hasher.update(&message[NoiseXKPattern1::P_AUTH_END..NoiseXKPattern1::SIZE]); + hasher.update(response); let mut output = [0u8; NOISE_HASHLEN]; hasher.finish(&mut output); let n = u32::from_be_bytes(output[..4].try_into().unwrap()); From 0c76ec872700348460e1f2ffc21dffd3afd46761 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 14:28:31 -0400 Subject: [PATCH 02/25] fixed bug --- src/zssp.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/zssp.rs b/src/zssp.rs index 8c4abef..beddd96 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -1452,9 +1452,11 @@ impl Context { } } PACKET_TYPE_NOISE_XK_PATTERN_3 => { - app.event_log(LogEvent::ReceiveUncheckedXK3, current_time); // Alice (remote) --> Bob (local) // -> s, se + let message = &mut message[..message_size]; + app.event_log(LogEvent::ReceiveUncheckedXK3, current_time); + if session.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); } From 0332e218031cd68e3ca1b1e91d69194d65e182b9 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 14:40:42 -0400 Subject: [PATCH 03/25] removed errors --- src/zssp.rs | 81 +++++++++++++++++++---------------------------------- 1 file changed, 29 insertions(+), 52 deletions(-) diff --git a/src/zssp.rs b/src/zssp.rs index beddd96..2567e22 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -237,7 +237,6 @@ enum NoiseXKAliceHandshakeState { NoiseXKPattern3 { noise_message: [u8; NoiseXKPattern3::MAX_SIZE], noise_message_len: usize, - new_ratchet_state: RatchetState, }, } @@ -489,8 +488,8 @@ impl Context { return Err(OpenError::DataTooLarge); } // Double check that the application gave us these in the correct order. - if let Some(first) = ratchet_state[0] { - if let Some(second) = ratchet_state[1] { + if let Some(first) = &ratchet_state[0] { + if let Some(second) = &ratchet_state[1] { if first.ratchet_count < second.ratchet_count { ratchet_state.swap(0, 1); } @@ -1063,7 +1062,7 @@ impl Context { noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); drop(noise_ekem1_secret); // Noise process pattern2 psk token. - let ratchet_key = ratchet_state.map(|rs| rs.key.as_ref()).unwrap_or(&[0; RATCHET_SIZE]); + let ratchet_key = ratchet_state.as_ref().map(|rs| rs.key.as_ref()).unwrap_or(&[0; RATCHET_SIZE]); let (temp_h, noise_k_eseeekem1psk) = noise_ck.mix_key_and_hash_initialize_key(hmac, ratchet_key); let noise_h_ee1peekem1psk = mix_hash(sha512, &noise_h_ee1peekem1, &temp_h); // Noise process pattern2 payload. @@ -1140,7 +1139,7 @@ impl Context { if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, .. } = &mut handshake_state.offer { let response_raw = &mut noise_message[*noise_message_len - ChallengeResponse::SIZE..]; - let response: &ChallengeResponse = byte_array_as_proto_buffer_mut(response_raw); + let response: &mut ChallengeResponse = byte_array_as_proto_buffer_mut(response_raw); // Only people who know what Alice's prior pow was can convince us to // compute a new pow. if challenge.prior_challenge_pow != response.challenge_pow { @@ -1153,7 +1152,7 @@ impl Context { let mut pow = self.0.rng.lock().unwrap().next_u64(); let sha512 = &mut Application::Hash::new(); loop { - let response: &ChallengeResponse = byte_array_as_proto_buffer(response_raw); + let response: &mut ChallengeResponse = byte_array_as_proto_buffer_mut(response_raw); response.challenge_pow.copy_from_slice(&pow.to_be_bytes()); if verify_pow::(sha512, response_raw) { break; @@ -1240,7 +1239,7 @@ impl Context { // the ratchet key Alice remembers, and final time with a ratchet // key of zero if Alice allows ratchet downgrades. - let test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState, Secret, [u8; 64])> { + let mut test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState, Secret, [u8; 64])> { // Check for which ratchet key Bob wants to use. let mut noise_ck = noise_ck.clone(); let mut payload = [0u8; NoiseXKPattern2::P_AUTH_END - NoiseXKPattern2::P_ENC_START]; @@ -1270,47 +1269,24 @@ impl Context { }; // Check first key. let mut ratchet_i = 0; - let mut is_auth = false; - let mut ratchet_number = 0; - let remote_key_id; - let noise_k_eseeekem1psk; - let noise_h_ee1peekem1pskp; - if let Some(rs) = state.ratchet_state[0] { - if let Some((key_id, ck, k, h)) = test_ratchet_key(rs.key.as_ref()) { - ratchet_number = rs.ratchet_count; - remote_key_id = key_id; - noise_ck = ck; - noise_k_eseeekem1psk = k; - noise_h_ee1peekem1pskp = h; - is_auth = true; - } + let mut result = None; + let ratchet_number = 0; + if let Some(rs) = &state.ratchet_state[0] { + result = test_ratchet_key(rs.key.as_ref()); } // Check second key. - if !is_auth { + if result.is_none() { ratchet_i = 1; - if let Some(rs) = state.ratchet_state[1] { - if let Some((key_id, ck, k, h)) = test_ratchet_key(rs.key.as_ref()) { - ratchet_number = rs.ratchet_count; - remote_key_id = key_id; - noise_ck = ck; - noise_k_eseeekem1psk = k; - noise_h_ee1peekem1pskp = h; - is_auth = true; - } + if let Some(rs) = &state.ratchet_state[1] { + result = test_ratchet_key(rs.key.as_ref()); } } // Check zero key. - if !is_auth { - if let Some((key_id, ck, k, h)) = test_ratchet_key(&[0u8; RATCHET_SIZE]) { - noise_ck = ck; - remote_key_id = key_id; - noise_k_eseeekem1psk = k; - noise_h_ee1peekem1pskp = h; - is_auth = true; - } + if result.is_none() { + result = test_ratchet_key(&[0u8; RATCHET_SIZE]); } - if is_auth { + if let Some((remote_key_id, mut noise_ck, noise_k_eseeekem1psk, noise_h_ee1peekem1pskp)) = result { // Start of Noise XKhfs+psk2 pattern3. let mut message3 = [0u8; NoiseXKPattern3::MAX_SIZE]; // Noise process pattern3 s token. @@ -1359,7 +1335,7 @@ impl Context { ratchet_count: ratchet_number + 1, }; let action = if state.ratchet_state[0].is_some() && state.ratchet_state[1].is_some() { - SaveAction::DeleteThenAddRatchet(&state.ratchet_state[1 - ratchet_i].unwrap(), &new_ratchet_state) + SaveAction::DeleteThenAddRatchet(state.ratchet_state[1 - ratchet_i].as_ref().unwrap(), &new_ratchet_state) } else { SaveAction::AddRatchet(&new_ratchet_state) }; @@ -1384,7 +1360,7 @@ impl Context { .lock() .unwrap() .replace(Application::AeadDec::new(kex_key_b2a.as_ref())); - state.ratchet_state[1] = state.ratchet_state[ratchet_i]; + state.ratchet_state[1] = state.ratchet_state[ratchet_i].clone(); state.ratchet_state[0] = Some(new_ratchet_state.clone()); state.cipher_states[0].replace(SessionKey::new( @@ -1403,7 +1379,6 @@ impl Context { handshake_state.offer = NoiseXKAliceHandshakeState::NoiseXKPattern3 { noise_message: message3, noise_message_len: p_auth_end, - new_ratchet_state: new_ratchet_state.clone(), }; } drop(state); @@ -1429,12 +1404,13 @@ impl Context { } // Bob failed authentication so we must restart our offer according to Noise. // We restart the offer instead of dropping the session to defend against DOS. + let ratchet_state = state.ratchet_state.clone(); drop(state); let mut state = session.state.write().unwrap(); if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { if !handshake_state.reinitialize( &session, - &state.ratchet_state, + &ratchet_state, &mut self.0.session_map.write().unwrap(), &mut self.0.rng.lock().unwrap(), current_time, @@ -1534,7 +1510,7 @@ impl Context { let new_ratchet_state = RatchetState { fingerprint: new_ratchet_fingerprint.0, key: new_ratchet_key, - ratchet_count: handshake_state.ratchet_state.map(|rs| rs.ratchet_count + 1).unwrap_or(1), + ratchet_count: handshake_state.ratchet_state.as_ref().map(|rs| rs.ratchet_count + 1).unwrap_or(1), }; let result = app.save_ratchet_state( &remote_s_public_key, @@ -1758,7 +1734,7 @@ fn initiate_rekey( // Start of Noise KKpsk0 pattern1. // Noise process pattern1 psk0 token. let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].unwrap().key.as_ref()); + let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].as_ref().unwrap().key.as_ref()); let noise_h_psk = mix_hash(sha512, &session.noise_kk_local_init_h, &noise_temp_h); // Noise process pattern1 e token. let noise_e_secret = Application::KeyPair::generate(&mut context.rng.lock().unwrap()); @@ -1940,7 +1916,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let sha512 = &mut Application::Hash::new(); let hmac = &mut Application::HmacHash::new(); let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].unwrap().key.as_ref()); + let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].as_ref().unwrap().key.as_ref()); let noise_h_psk = mix_hash(sha512, &session.noise_kk_remote_init_h, &noise_temp_h); // Noise process pattern1 e token. // Get public key validation out of the way early @@ -2003,7 +1979,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let new_ratchet_state = RatchetState { fingerprint: new_ratchet_fingerprint.0, key: new_ratchet_key, - ratchet_count: state.ratchet_state[0].map(|rs| rs.ratchet_count + 1).unwrap_or(1), + ratchet_count: state.ratchet_state[0].as_ref().map(|rs| rs.ratchet_count + 1).unwrap_or(1), }; let result = app.save_ratchet_state( &session.remote_s_public_key, @@ -2034,7 +2010,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu .lock() .unwrap() .replace(Application::AeadDec::new(kex_key_a2b.as_ref())); - state.ratchet_state[1] = state.ratchet_state[0]; + state.ratchet_state[1] = state.ratchet_state[0].clone(); state.ratchet_state[0] = Some(new_ratchet_state.clone()); state.cipher_states[next_key_index].replace(SessionKey::new( @@ -2108,12 +2084,12 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let new_ratchet_state = RatchetState { fingerprint: new_ratchet_fingerprint.0, key: new_ratchet_key, - ratchet_count: state.ratchet_state[0].map(|rs| rs.ratchet_count + 1).unwrap_or(1), + ratchet_count: state.ratchet_state[0].as_ref().map(|rs| rs.ratchet_count + 1).unwrap_or(1), }; let result = app.save_ratchet_state( &session.remote_s_public_key, &session.application_data, - SaveAction::DeleteThenAddRatchet(&state.ratchet_state[0].unwrap(), &new_ratchet_state), + SaveAction::DeleteThenAddRatchet(&state.ratchet_state[0].as_ref().unwrap(), &new_ratchet_state), current_time, ); if result.is_err() { @@ -2231,7 +2207,7 @@ impl Session { /// The most recent confirmed ratchet number of this session. #[inline] pub fn ratchet_count(&self) -> u64 { - self.state.read().unwrap().ratchet_state[0].map(|rs| rs.ratchet_count).unwrap_or(0) + self.state.read().unwrap().ratchet_state[0].as_ref().map(|rs| rs.ratchet_count).unwrap_or(0) } /// Mark a session as expired. This will make it impossible for this session to successfully /// receive or send data. It is recommended to simply `drop` the session instead, but this can @@ -2363,6 +2339,7 @@ impl NoiseXKAliceHandshake { &mut message[NoiseXKPattern1::E1_ENC_START..NoiseXKPattern1::P_ENC_START], ); // Noise process pattern1 payload. + let noise_pattern1: &mut NoiseXKPattern1 = byte_array_as_proto_buffer_mut(&mut message); let mut idx = 0; for r in ratchet_state { if let Some(rs) = r { From 9324a9b3c42620752a3639644696ce8d2897ce53 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 14:46:13 -0400 Subject: [PATCH 04/25] exported ratchet state --- src/lib.rs | 2 +- src/zssp.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6439e61..e5aee5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ mod symmetric_state; mod zssp; pub mod error; -pub use crate::applicationlayer::{ApplicationLayer, SaveAction}; +pub use crate::applicationlayer::{ApplicationLayer, RatchetState, SaveAction}; pub use crate::log_event::LogEvent; pub use crate::proto::{MAX_IDENTITY_BLOB_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU, RATCHET_SIZE}; pub use crate::zssp::{AcceptSessionAction, Context, ContextInner, IncomingSessionAction, ReceiveResult, Session, SessionEvent}; diff --git a/src/zssp.rs b/src/zssp.rs index 2567e22..ebdaab1 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -2207,7 +2207,10 @@ impl Session { /// The most recent confirmed ratchet number of this session. #[inline] pub fn ratchet_count(&self) -> u64 { - self.state.read().unwrap().ratchet_state[0].as_ref().map(|rs| rs.ratchet_count).unwrap_or(0) + self.state.read().unwrap().ratchet_state[0] + .as_ref() + .map(|rs| rs.ratchet_count) + .unwrap_or(0) } /// Mark a session as expired. This will make it impossible for this session to successfully /// receive or send data. It is recommended to simply `drop` the session instead, but this can From 2abd65ff2c03cdd6adeec40ae666a5ee7311940b Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 14:57:22 -0400 Subject: [PATCH 05/25] improved struct --- src/applicationlayer.rs | 6 +++--- src/zssp.rs | 38 +++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/applicationlayer.rs b/src/applicationlayer.rs index 251e825..ae18b62 100644 --- a/src/applicationlayer.rs +++ b/src/applicationlayer.rs @@ -199,11 +199,11 @@ pub trait ApplicationLayer: Sized { fn event_log<'a>(&self, event: LogEvent<'a, Self>, current_time: i64) {} } -#[derive(Default, Clone)] +#[derive(Default, Clone, PartialEq, Eq)] pub struct RatchetState { - pub fingerprint: [u8; RATCHET_SIZE], pub key: Secret, - pub ratchet_count: u64, + pub fingerprint: Secret, + pub chain_len: u64, } /// Ratchet keys and fingerprints should be saved *per remote peer*. It is up to the application to /// enforce separate storage for each remote peer based on `remote_static_key` and `application_data`. diff --git a/src/zssp.rs b/src/zssp.rs index ebdaab1..e4929a2 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -490,7 +490,7 @@ impl Context { // Double check that the application gave us these in the correct order. if let Some(first) = &ratchet_state[0] { if let Some(second) = &ratchet_state[1] { - if first.ratchet_count < second.ratchet_count { + if first.chain_len < second.chain_len { ratchet_state.swap(0, 1); } } @@ -1327,12 +1327,12 @@ impl Context { drop(noise_k_eseeekem1pskse); // Alice finished Noise XKhfs+psk2 handshake. // Transition offer state machine to the NoiseXKPattern3 state. - let (new_ratchet_key, new_ratchet_fingerprint) = + let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); let new_ratchet_state = RatchetState { - fingerprint: new_ratchet_fingerprint.0, - key: new_ratchet_key, - ratchet_count: ratchet_number + 1, + key: rk, + fingerprint: rf, + chain_len: ratchet_number + 1, }; let action = if state.ratchet_state[0].is_some() && state.ratchet_state[1].is_some() { SaveAction::DeleteThenAddRatchet(state.ratchet_state[1 - ratchet_i].as_ref().unwrap(), &new_ratchet_state) @@ -1505,12 +1505,12 @@ impl Context { let noise_kk_remote_init_h = mix_hash(sha512, &INITIAL_H_REKEY, remote_s_public_key.as_bytes()); let noise_kk_remote_init_h = mix_hash(sha512, &noise_kk_remote_init_h, self.0.static_keypair.public_key_bytes()); - let (new_ratchet_key, new_ratchet_fingerprint) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); + let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); // We must make sure the ratchet key is saved before we transition. let new_ratchet_state = RatchetState { - fingerprint: new_ratchet_fingerprint.0, - key: new_ratchet_key, - ratchet_count: handshake_state.ratchet_state.as_ref().map(|rs| rs.ratchet_count + 1).unwrap_or(1), + key: rk, + fingerprint: rf, + chain_len: handshake_state.ratchet_state.as_ref().map(|rs| rs.chain_len + 1).unwrap_or(1), }; let result = app.save_ratchet_state( &remote_s_public_key, @@ -1975,11 +1975,11 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu ); drop(noise_k_pskessseese); // Bob finished Noise KKpsk0 handshake. - let (new_ratchet_key, new_ratchet_fingerprint) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); + let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); let new_ratchet_state = RatchetState { - fingerprint: new_ratchet_fingerprint.0, - key: new_ratchet_key, - ratchet_count: state.ratchet_state[0].as_ref().map(|rs| rs.ratchet_count + 1).unwrap_or(1), + key: rk, + fingerprint: rf, + chain_len: state.ratchet_state[0].as_ref().map(|rs| rs.chain_len + 1).unwrap_or(1), }; let result = app.save_ratchet_state( &session.remote_s_public_key, @@ -2080,11 +2080,11 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu if let (true, Some(remote_key_id)) = (is_auth, NonZeroU32::new(u32::from_ne_bytes(noise_pattern2.key_id))) { // Bob fully authenticated. // Alice finished Noise KKpsk0 handshake. - let (new_ratchet_key, new_ratchet_fingerprint) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); + let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); let new_ratchet_state = RatchetState { - fingerprint: new_ratchet_fingerprint.0, - key: new_ratchet_key, - ratchet_count: state.ratchet_state[0].as_ref().map(|rs| rs.ratchet_count + 1).unwrap_or(1), + key: rk, + fingerprint: rf, + chain_len: state.ratchet_state[0].as_ref().map(|rs| rs.chain_len + 1).unwrap_or(1), }; let result = app.save_ratchet_state( &session.remote_s_public_key, @@ -2209,7 +2209,7 @@ impl Session { pub fn ratchet_count(&self) -> u64 { self.state.read().unwrap().ratchet_state[0] .as_ref() - .map(|rs| rs.ratchet_count) + .map(|rs| rs.chain_len) .unwrap_or(0) } /// Mark a session as expired. This will make it impossible for this session to successfully @@ -2347,7 +2347,7 @@ impl NoiseXKAliceHandshake { for r in ratchet_state { if let Some(rs) = r { let next_idx = idx + RATCHET_SIZE; - noise_pattern1.payload[idx..next_idx].copy_from_slice(&rs.fingerprint); + noise_pattern1.payload[idx..next_idx].copy_from_slice(rs.fingerprint.as_ref()); idx = next_idx; } } From 4a5d1065c1f57737428ab821be727fa513a3d6c0 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 15:20:51 -0400 Subject: [PATCH 06/25] fixed aob bug --- src/zssp.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/zssp.rs b/src/zssp.rs index e4929a2..8c03a3d 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -2351,7 +2351,8 @@ impl NoiseXKAliceHandshake { idx = next_idx; } } - idx += AES_GCM_TAG_SIZE; + let p_auth_end = NoiseXKPattern1::P_ENC_START + idx + AES_GCM_TAG_SIZE; + let noise_message_len = p_auth_end + ChallengeResponse::SIZE; let noise_h_ee1p = encrypt_and_hash::( sha512, @@ -2359,21 +2360,20 @@ impl NoiseXKAliceHandshake { &noise_h_ee1, PACKET_TYPE_NOISE_XK_PATTERN_1, 1, - &mut message[NoiseXKPattern1::P_ENC_START..idx], + &mut message[NoiseXKPattern1::P_ENC_START..p_auth_end], ); drop(noise_k_es); let (header_b2a_key, header_a2b_key) = noise_ck.get_ask2(hmac, LABEL_HEADER_KEY, &noise_h_ee1p); - let message_id = u64::from_be_bytes(message[idx - 8..idx].try_into().unwrap()); + let message_id = u64::from_be_bytes(message[p_auth_end - 8..p_auth_end].try_into().unwrap()); - idx += ChallengeResponse::SIZE; - message[idx - CHALLENGE_POW_SIZE..idx].copy_from_slice(&rng.next_u64().to_ne_bytes()); + message[noise_message_len - CHALLENGE_POW_SIZE..noise_message_len].copy_from_slice(&rng.next_u64().to_ne_bytes()); Ok(( NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_h_ee1p, noise_e_secret, noise_e1_secret: Secret(noise_e1_secret.secret), noise_ck_es: noise_ck, - noise_message_len: idx, + noise_message_len, noise_message: message, message_id, }, From 13e3f865b37ade09a806dd71fb86877b41be5193 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 15:23:09 -0400 Subject: [PATCH 07/25] fixed aob bug --- src/zssp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zssp.rs b/src/zssp.rs index 8c03a3d..d56d9a7 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -882,7 +882,7 @@ impl Context { if session.is_some() || incoming.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); } - if message.len() < NoiseXKPattern1::MIN_SIZE || message.len() > NoiseXKPattern1::MIN_SIZE { + if message_size < NoiseXKPattern1::MIN_SIZE || message_size > NoiseXKPattern1::MIN_SIZE { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } // The message id must be the first 8 bytes of the gcm tag. From c2275bf72c8581fb46f5436e5de234b1ebf8ccf4 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 15:30:56 -0400 Subject: [PATCH 08/25] fixed typo --- src/zssp.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zssp.rs b/src/zssp.rs index d56d9a7..77377d0 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -522,11 +522,11 @@ impl Context { alice_identity_blob: local_identity_blob, offer, }); - if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, message_id, .. } = &handshake_state.offer { + if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } = &handshake_state.offer { send_with_fragmentation( &mut send, mtu, - &mut noise_message.clone(), + &mut noise_message.clone()[..*noise_message_len], PACKET_TYPE_NOISE_XK_PATTERN_1, None, *message_id, @@ -882,7 +882,7 @@ impl Context { if session.is_some() || incoming.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); } - if message_size < NoiseXKPattern1::MIN_SIZE || message_size > NoiseXKPattern1::MIN_SIZE { + if message_size < NoiseXKPattern1::MIN_SIZE || message_size > NoiseXKPattern1::MAX_SIZE { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } // The message id must be the first 8 bytes of the gcm tag. From 6fb629edefe5b96f8c089b12daab6d9cf6081963 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 15:36:54 -0400 Subject: [PATCH 09/25] fixed typo --- src/zssp.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/zssp.rs b/src/zssp.rs index 77377d0..83ec35a 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -887,12 +887,12 @@ impl Context { } // The message id must be the first 8 bytes of the gcm tag. // This forces the message id to be authenticated along with the entire message. - let challenge_start_idx = message_size - ChallengeResponse::SIZE; - if message[8..] != message[challenge_start_idx - 8..challenge_start_idx] { + let p_auth_end = message_size - ChallengeResponse::SIZE; + if message[8..16] != message[p_auth_end - 8..p_auth_end] { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } - let total_ratchet_fingerprints = (challenge_start_idx - AES_GCM_TAG_SIZE) / RATCHET_SIZE; - if (challenge_start_idx - AES_GCM_TAG_SIZE) % RATCHET_SIZE != 0 || total_ratchet_fingerprints > 2 { + let total_ratchet_fingerprints = (p_auth_end - AES_GCM_TAG_SIZE) / RATCHET_SIZE; + if (p_auth_end - AES_GCM_TAG_SIZE) % RATCHET_SIZE != 0 || total_ratchet_fingerprints > 2 { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } @@ -904,7 +904,7 @@ impl Context { match check_allow_incoming_session() { IncomingSessionAction::Allow => {} IncomingSessionAction::Challenge => { - let response: &ChallengeResponse = byte_array_as_proto_buffer(&message[challenge_start_idx..message_size]); + let response: &ChallengeResponse = byte_array_as_proto_buffer(&message[p_auth_end..message_size]); let mut counter = 0u64.to_ne_bytes(); counter.copy_from_slice(&response.challenge_counter); let counter = u64::from_be_bytes(counter); @@ -918,7 +918,7 @@ impl Context { hasher.0.finish(&mut output); let is_valid = self.check_challenge_window(counter) && secure_eq(&output[..CHALLENGE_MAC_SIZE], &response.challenge_mac) - && verify_pow::(&mut hasher.0, &message[challenge_start_idx..message_size]) + && verify_pow::(&mut hasher.0, &message[p_auth_end..message_size]) && self.update_challenge_window(counter); app.event_log(LogEvent::ReceiveCheckXK1Challenge(is_valid), current_time); if !is_valid { @@ -1002,7 +1002,7 @@ impl Context { &noise_h_ee1, packet_type, 1, - &mut message[NoiseXKPattern1::P_ENC_START..challenge_start_idx], + &mut message[NoiseXKPattern1::P_ENC_START..p_auth_end], ); drop(noise_k_es); if !is_auth { From 95a97a756669690f03ec008f114c033865992d4f Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 15:37:11 -0400 Subject: [PATCH 10/25] fixed typo --- src/zssp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zssp.rs b/src/zssp.rs index 83ec35a..938762d 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -1204,7 +1204,7 @@ impl Context { { let noise_pattern2: &NoiseXKPattern2 = byte_array_as_proto_buffer(message); // Authenticate header counter. - if noise_pattern2.header[13..] != noise_pattern2.p_gcm_tag[13..] { + if noise_pattern2.header[13..16] != noise_pattern2.p_gcm_tag[13..16] { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } From ce5ff0dab4b91f216ec9138f39791c8ce0cb22c4 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Thu, 13 Jul 2023 15:40:37 -0400 Subject: [PATCH 11/25] fixed typo --- src/zssp.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/zssp.rs b/src/zssp.rs index 938762d..c2dfedf 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -891,8 +891,9 @@ impl Context { if message[8..16] != message[p_auth_end - 8..p_auth_end] { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } - let total_ratchet_fingerprints = (p_auth_end - AES_GCM_TAG_SIZE) / RATCHET_SIZE; - if (p_auth_end - AES_GCM_TAG_SIZE) % RATCHET_SIZE != 0 || total_ratchet_fingerprints > 2 { + let p_size = p_auth_end - NoiseXKPattern1::P_ENC_START - AES_GCM_TAG_SIZE; + let total_ratchet_fingerprints = p_size / RATCHET_SIZE; + if p_size % RATCHET_SIZE != 0 || total_ratchet_fingerprints > 2 { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } From 48ff1fc61202b76783526a8a58dc3a5cc0f610e1 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 12:04:40 -0400 Subject: [PATCH 12/25] improved API --- src/applicationlayer.rs | 89 +++++++-- src/error.rs | 2 + src/lib.rs | 2 +- src/zssp.rs | 392 +++++++++++++++++++++------------------- 4 files changed, 280 insertions(+), 205 deletions(-) diff --git a/src/applicationlayer.rs b/src/applicationlayer.rs index ae18b62..ed9e020 100644 --- a/src/applicationlayer.rs +++ b/src/applicationlayer.rs @@ -6,6 +6,7 @@ * https://www.zerotier.com/ */ +use std::num::NonZeroU64; use std::sync::Arc; use crate::crypto::aes::{AesDec, AesEnc}; @@ -163,8 +164,12 @@ pub trait ApplicationLayer: Sized { /// to the zero ratchet key, restarting the ratchet chain. /// If `RatchetAction::FailAuthentication` is returned Alice's connection will be silently dropped. #[allow(unused)] - fn restore_ratchet(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result, ()> { - Ok(None) + fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result { + Ok(RatchetState::Null) + } + #[allow(unused)] + fn restore_by_identity(&self, remote_static_key: &Self::PublicKey, application_data: &Self::Data, current_time: i64) -> Result<[RatchetState; 2], ()> { + Ok([RatchetState::Null, RatchetState::Null]) } /// Atomically save the given ratchet key, fingerprint and number to persistent storage. /// @@ -189,7 +194,8 @@ pub trait ApplicationLayer: Sized { &self, remote_static_key: &Self::PublicKey, application_data: &Self::Data, - save_action: SaveAction<'a>, + previous_ratchet_states: [&RatchetState; 2], + new_ratchet_states: [&RatchetState; 2], current_time: i64, ) -> Result<(), ()> { Ok(()) @@ -199,19 +205,70 @@ pub trait ApplicationLayer: Sized { fn event_log<'a>(&self, event: LogEvent<'a, Self>, current_time: i64) {} } -#[derive(Default, Clone, PartialEq, Eq)] -pub struct RatchetState { +#[derive(Clone, PartialEq, Eq)] +pub enum RatchetState { + Null, + Empty, + NonEmpty(NonEmptyRatchetState), +} +use RatchetState::*; +impl RatchetState { + pub fn new_nonempty( + key: Secret, + fingerprint: Secret, + chain_len: NonZeroU64, + ) -> Self { + NonEmpty(NonEmptyRatchetState { + key, + fingerprint, + chain_len, + }) + } + pub fn new_initial_states() -> [RatchetState; 2] { + [RatchetState::Empty, RatchetState::Null] + } + pub fn is_null(&self) -> bool { + match self { + Null => true, + _ => false, + } + } + pub fn is_empty(&self) -> bool { + match self { + Empty => true, + _ => false, + } + } + #[inline] + pub fn nonempty(&self) -> Option<&NonEmptyRatchetState> { + match self { + NonEmpty(rs) => Some(&rs), + _ => None, + } + } + #[inline] + pub fn chain_len(&self) -> u64 { + self.nonempty().map_or(0, |rs| rs.chain_len.get()) + } + #[inline] + pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { + self.nonempty().map_or(None, |rs| Some(&rs.fingerprint.as_ref())) + } + #[inline] + pub fn key(&self) -> Option<&[u8; RATCHET_SIZE]> { + const ZERO_KEY: [u8; RATCHET_SIZE] = [0u8; RATCHET_SIZE]; + match self { + Null => None, + Empty => Some(&ZERO_KEY), + NonEmpty(rs) => Some(rs.key.as_ref()), + } + } +} +/// A ratchet key and fingerprint, +/// along with the length of the ratchet chain the keys were derived from. +#[derive(Clone, PartialEq, Eq)] +pub struct NonEmptyRatchetState { pub key: Secret, pub fingerprint: Secret, - pub chain_len: u64, -} -/// Ratchet keys and fingerprints should be saved *per remote peer*. It is up to the application to -/// enforce separate storage for each remote peer based on `remote_static_key` and `application_data`. -/// -/// Only up to 2 ratchet keys and fingerprints will be saved at one time. -pub enum SaveAction<'a> { - AddRatchet(&'a RatchetState), - DeleteRatchet(&'a RatchetState), - DeleteThenAddRatchet(&'a RatchetState, &'a RatchetState), - VerifyThenOverwriteRatchet(Option<&'a RatchetState>, &'a RatchetState), + pub chain_len: NonZeroU64, } diff --git a/src/error.rs b/src/error.rs index 2183abd..34e2dbc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,8 @@ pub enum OpenError { /// Local identity blob is too large to send, even with fragmentation. DataTooLarge, + + RatchetIoError } #[derive(Debug, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index e5aee5a..d0025df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ mod symmetric_state; mod zssp; pub mod error; -pub use crate::applicationlayer::{ApplicationLayer, RatchetState, SaveAction}; +pub use crate::applicationlayer::{ApplicationLayer, RatchetState}; pub use crate::log_event::LogEvent; pub use crate::proto::{MAX_IDENTITY_BLOB_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU, RATCHET_SIZE}; pub use crate::zssp::{AcceptSessionAction, Context, ContextInner, IncomingSessionAction, ReceiveResult, Session, SessionEvent}; diff --git a/src/zssp.rs b/src/zssp.rs index c2dfedf..e35bdd5 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -11,7 +11,7 @@ use std::cmp::Reverse; use std::collections::HashMap; use std::hash::Hash; -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroU64}; use std::ops::DerefMut; use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; @@ -128,7 +128,7 @@ pub struct Session { /// Handle into the session queue for changing the update timer. queue_idx: BinaryHeapIndex, - remote_s_public_key: Application::PublicKey, + remote_static_key: Application::PublicKey, send_counter: AtomicU64, /// This bool signals to all threads to stop incrementing the counter and instead error out. session_has_expired: AtomicBool, @@ -158,7 +158,7 @@ unsafe impl Sync for Session {} /// Session state may only be mutated during atomic transitions of the offer state machine. struct SessionMutableState { - ratchet_state: [Option; 2], + ratchet_states: [RatchetState; 2], /// For OOO rekeying reliability we allow our version of Noise CipherState to hold the last two /// session keys, instead of just the most recent one. cipher_states: [Option>; 2], @@ -200,11 +200,12 @@ enum OfferStateMachine { } pub(crate) struct NoiseXKBobHandshakeState { + /// Can never be Null. + ratchet_state: RatchetState, remote_key_id: NonZeroU32, local_key_id: NonZeroU32, header_receive_key: Secret, header_send_key: Secret, - ratchet_state: Option, noise_h_ee1peekem1pskp: [u8; NOISE_HASHLEN], noise_e_secret: Application::KeyPair, noise_ck_eseeekem1psk: SymmetricState, @@ -347,10 +348,10 @@ impl Context { } else { // We have to eventually time out NoiseXKPattern3 because of unreliable network conditions. if handshake_state.timeout <= current_time { - let ratchet_state = state.ratchet_state.clone(); drop(state); let _kex_lock = session.state_machine_lock.lock().unwrap(); let mut state = session.state.write().unwrap(); + let ratchet_state = state.ratchet_states.clone(); // Since we dropped the lock we must re-check if we are in the correct state. if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { if handshake_state.timeout <= current_time { @@ -463,7 +464,7 @@ impl Context { /// * `send` - Function to be called to send one or more initial packets to the remote being /// contacted /// * `mtu` - MTU for initial packets - /// * `remote_s_public_key` - Remote side's static public NIST P-384 key + /// * `remote_static_key` - Remote side's static public NIST P-384 key /// * `application_data` - Arbitrary data meaningful to the application to include with session /// object /// * `ratchet_state` - The last saved and confirmed ratchet state associated with this remote @@ -475,11 +476,11 @@ impl Context { #[inline] pub fn open( &self, + app: &Application, mut send: impl FnMut(&mut [u8]) -> bool, mut mtu: usize, - remote_s_public_key: Application::PublicKey, + remote_static_key: Application::PublicKey, application_data: Application::Data, - mut ratchet_state: [Option; 2], local_identity_blob: Application::LocalIdentityBlob, current_time: i64, ) -> Result>, OpenError> { @@ -487,88 +488,82 @@ impl Context { if local_identity_blob.as_ref().len() > MAX_IDENTITY_BLOB_SIZE { return Err(OpenError::DataTooLarge); } - // Double check that the application gave us these in the correct order. - if let Some(first) = &ratchet_state[0] { - if let Some(second) = &ratchet_state[1] { - if first.chain_len < second.chain_len { - ratchet_state.swap(0, 1); - } + let result = app.restore_by_identity(&remote_static_key, &application_data, current_time); + if let Ok(ratchet_states) = result { + let sha512 = &mut Application::Hash::new(); + + let mut noise_kk_ss = Secret::new(); + if !self.0.static_keypair.agree(&remote_static_key, noise_kk_ss.as_mut()) { + return Err(OpenError::InvalidPublicKey); } - } else { - ratchet_state.swap(0, 1); - } + let noise_kk_local_init_h = mix_hash(sha512, &INITIAL_H_REKEY, self.0.static_keypair.public_key_bytes()); + let noise_kk_local_init_h = mix_hash(sha512, &noise_kk_local_init_h, remote_static_key.as_bytes()); + let noise_kk_remote_init_h = mix_hash(sha512, &INITIAL_H_REKEY, remote_static_key.as_bytes()); + let noise_kk_remote_init_h = mix_hash(sha512, &noise_kk_remote_init_h, self.0.static_keypair.public_key_bytes()); - let sha512 = &mut Application::Hash::new(); + let mut session_queue = self.0.session_queue.lock().unwrap(); + let mut session_map = self.0.session_map.write().unwrap(); + let local_key_id = generate_key_id(&session_map, &mut self.0.rng.lock().unwrap()); + // Begin Noise XKhfs+psk2. + let (offer, a2b_header_key, b2a_header_key) = + NoiseXKAliceHandshake::::initialize(local_key_id, &remote_static_key, &ratchet_states, &mut self.0.rng.lock().unwrap())?; + let handshake_state = Box::new(NoiseXKAliceHandshake { + next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), + timeout: current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS), + local_key_id, + alice_identity_blob: local_identity_blob, + offer, + }); + if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } = &handshake_state.offer { + send_with_fragmentation( + &mut send, + mtu, + &mut noise_message.clone()[..*noise_message_len], + PACKET_TYPE_NOISE_XK_PATTERN_1, + None, + *message_id, + None::<&Application::PrpEnc>, + ); + } - let mut noise_kk_ss = Secret::new(); - if !self.0.static_keypair.agree(&remote_s_public_key, noise_kk_ss.as_mut()) { - return Err(OpenError::InvalidPublicKey); - } - let noise_kk_local_init_h = mix_hash(sha512, &INITIAL_H_REKEY, self.0.static_keypair.public_key_bytes()); - let noise_kk_local_init_h = mix_hash(sha512, &noise_kk_local_init_h, remote_s_public_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &INITIAL_H_REKEY, remote_s_public_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &noise_kk_remote_init_h, self.0.static_keypair.public_key_bytes()); - - let mut session_queue = self.0.session_queue.lock().unwrap(); - let mut session_map = self.0.session_map.write().unwrap(); - let local_key_id = generate_key_id(&session_map, &mut self.0.rng.lock().unwrap()); - // Begin Noise XKhfs+psk2. - let (offer, a2b_header_key, b2a_header_key) = - NoiseXKAliceHandshake::::initialize(local_key_id, &remote_s_public_key, &ratchet_state, &mut self.0.rng.lock().unwrap())?; - let handshake_state = Box::new(NoiseXKAliceHandshake { - next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - timeout: current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS), - local_key_id, - alice_identity_blob: local_identity_blob, - offer, - }); - if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } = &handshake_state.offer { - send_with_fragmentation( - &mut send, - mtu, - &mut noise_message.clone()[..*noise_message_len], - PACKET_TYPE_NOISE_XK_PATTERN_1, - None, - *message_id, - None::<&Application::PrpEnc>, + let queue_idx = session_queue.reserve_index(); + let session = Arc::new(Session { + context: Arc::downgrade(&self.0), + queue_idx, + application_data, + remote_static_key, + send_counter: AtomicU64::new(INIT_COUNTER), + session_has_expired: AtomicBool::new(false), + counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), + state_machine_lock: Mutex::new(()), + state: RwLock::new(SessionMutableState { + ratchet_states: ratchet_states.clone(), + cipher_states: [None, None], + // Points at 1 until the first key is confirmed. + current_key: 1, + outgoing_offer: OfferStateMachine::NoiseXKPattern1or3(handshake_state), + }), + header_send_cipher: Application::PrpEnc::new(a2b_header_key.as_ref()), + header_receive_cipher: Application::PrpDec::new(b2a_header_key.as_ref()), + kex_receive_cipher: Mutex::new(None), + kex_send_cipher: Mutex::new(None), + noise_kk_ss: noise_kk_ss.clone(), + noise_kk_local_init_h, + noise_kk_remote_init_h, + defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), + was_bob: false, + }); + session_map.insert(local_key_id, (Arc::downgrade(&session), false)); + session_queue.push_reserved( + queue_idx, + Arc::downgrade(&session), + Reverse(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), ); + + return Ok(session); + } else { + return Err(OpenError::RatchetIoError); } - - let queue_idx = session_queue.reserve_index(); - let session = Arc::new(Session { - context: Arc::downgrade(&self.0), - queue_idx, - application_data, - remote_s_public_key, - send_counter: AtomicU64::new(INIT_COUNTER), - session_has_expired: AtomicBool::new(false), - counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), - state_machine_lock: Mutex::new(()), - state: RwLock::new(SessionMutableState { - ratchet_state: ratchet_state.clone(), - cipher_states: [None, None], - // Points at 1 until the first key is confirmed. - current_key: 1, - outgoing_offer: OfferStateMachine::NoiseXKPattern1or3(handshake_state), - }), - header_send_cipher: Application::PrpEnc::new(a2b_header_key.as_ref()), - header_receive_cipher: Application::PrpDec::new(b2a_header_key.as_ref()), - kex_receive_cipher: Mutex::new(None), - kex_send_cipher: Mutex::new(None), - noise_kk_ss: noise_kk_ss.clone(), - noise_kk_local_init_h, - noise_kk_remote_init_h, - defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), - was_bob: false, - }); - session_map.insert(local_key_id, (Arc::downgrade(&session), false)); - session_queue.push_reserved( - queue_idx, - Arc::downgrade(&session), - Reverse(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - ); - - return Ok(session); } /// Receive, authenticate, decrypt, and process a physical wire packet. @@ -588,11 +583,7 @@ impl Context { /// * `check_accept_session` - Function to accept sessions after final negotiation. /// The second argument is the identity blob that the remote peer sent us. The application /// must verify this identity is associated with the remote peer's static key. - /// If the third argument is `Some`, it is a ratchet fingerprint. The application must verify - /// that it is associated with the remote peer's static key and identity. - /// If the third argument is `None` it means the remote peer connected to us with the zero - /// ratchet. The application should decide whether or not this remote peer is allowed to - /// connect with the zero ratchet. + /// The third argument is true if the remote peer connected to us with a recognized ratchet fingerprint. /// * `send_unassociated_reply` - Function to send reply packets directly when no session exists /// * `send_unassociated_mtu` - MTU for unassociated replies /// * `send_to` - Function to get senders for existing sessions, permitting MTU and path lookup @@ -609,7 +600,7 @@ impl Context { &self, app: &Application, check_allow_incoming_session: impl FnOnce() -> IncomingSessionAction, - check_accept_session: impl FnOnce(&Application::PublicKey, &[u8]) -> AcceptSessionAction, + check_accept_session: impl FnOnce(&Application::PublicKey, &[u8], u64) -> (Option<(bool, Application::Data)>, bool), mut send_unassociated_reply: impl FnMut(&mut [u8]) -> bool, mut send_unassociated_mtu: usize, mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, @@ -1012,22 +1003,25 @@ impl Context { let (header_b2a_key, header_a2b_key) = noise_ck.get_ask2(hmac, LABEL_HEADER_KEY, &noise_h_ee1p); // Get ratchet key. let noise_pattern1: &NoiseXKPattern1 = byte_array_as_proto_buffer(message); - let mut ratchet_state = None; + let mut ratchet_state = RatchetState::Null; for i in 0..total_ratchet_fingerprints { - match app.restore_ratchet( + match app.restore_by_fingerprint( (&noise_pattern1.payload[i * RATCHET_SIZE..(i + 1) * RATCHET_SIZE]).try_into().unwrap(), current_time, ) { - Ok(Some(rs)) => { - ratchet_state = Some(rs); + Ok(RatchetState::Null) | Ok(RatchetState::Empty) => {} + Ok(rs) => { + ratchet_state = rs; break; } - Ok(None) => {} Err(()) => return Err(ReceiveError::RatchetIoError), } } - if ratchet_state.is_none() && app.hello_requires_recognized_ratchet(current_time) { - return Ok(ReceiveResult::Rejected); + if ratchet_state.is_null() { + if app.hello_requires_recognized_ratchet(current_time) { + return Ok(ReceiveResult::Rejected); + } + ratchet_state = RatchetState::Empty; } // Start of Noise XKhfs+psk2 pattern2. @@ -1063,7 +1057,7 @@ impl Context { noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); drop(noise_ekem1_secret); // Noise process pattern2 psk token. - let ratchet_key = ratchet_state.as_ref().map(|rs| rs.key.as_ref()).unwrap_or(&[0; RATCHET_SIZE]); + let ratchet_key = ratchet_state.key().unwrap(); let (temp_h, noise_k_eseeekem1psk) = noise_ck.mix_key_and_hash_initialize_key(hmac, ratchet_key); let noise_h_ee1peekem1psk = mix_hash(sha512, &noise_h_ee1peekem1, &temp_h); // Noise process pattern2 payload. @@ -1236,10 +1230,13 @@ impl Context { if let Some(Ok(noise_ekem1_secret)) = is_auth.then_some(noise_ekem1_secret) { noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); drop(noise_ekem1_secret); + // We attempt to decrypt the payload at most three times. First two times with // the ratchet key Alice remembers, and final time with a ratchet // key of zero if Alice allows ratchet downgrades. - + // The following code is not constant time, meaning we leak to an + // attacker whether or not we downgraded. + // We don't currently consider this sensitive enough information to hide. let mut test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState, Secret, [u8; 64])> { // Check for which ratchet key Bob wants to use. let mut noise_ck = noise_ck.clone(); @@ -1271,19 +1268,22 @@ impl Context { // Check first key. let mut ratchet_i = 0; let mut result = None; - let ratchet_number = 0; - if let Some(rs) = &state.ratchet_state[0] { - result = test_ratchet_key(rs.key.as_ref()); + let mut chain_len = 0; + if let Some(key) = state.ratchet_states[0].key() { + chain_len = state.ratchet_states[0].chain_len(); + result = test_ratchet_key(key); } // Check second key. if result.is_none() { ratchet_i = 1; - if let Some(rs) = &state.ratchet_state[1] { - result = test_ratchet_key(rs.key.as_ref()); + if let Some(key) = state.ratchet_states[1].key() { + chain_len = state.ratchet_states[0].chain_len(); + result = test_ratchet_key(key); } } // Check zero key. - if result.is_none() { + if result.is_none() && !app.initiator_disallows_downgrade(&session, current_time) { + chain_len = 0; result = test_ratchet_key(&[0u8; RATCHET_SIZE]); } @@ -1330,18 +1330,16 @@ impl Context { // Transition offer state machine to the NoiseXKPattern3 state. let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); - let new_ratchet_state = RatchetState { - key: rk, - fingerprint: rf, - chain_len: ratchet_number + 1, - }; - let action = if state.ratchet_state[0].is_some() && state.ratchet_state[1].is_some() { - SaveAction::DeleteThenAddRatchet(state.ratchet_state[1 - ratchet_i].as_ref().unwrap(), &new_ratchet_state) - } else { - SaveAction::AddRatchet(&new_ratchet_state) - }; - let result = - app.save_ratchet_state(&session.remote_s_public_key, &session.application_data, action, current_time); + let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(chain_len + 1).unwrap()); + + let ratchet_to_preserve = &state.ratchet_states[ratchet_i]; + let result = app.save_ratchet_state( + &session.remote_static_key, + &session.application_data, + [&state.ratchet_states[0], &state.ratchet_states[1]], + [&new_ratchet_state, ratchet_to_preserve], + current_time + ); if result.is_err() { return Err(ReceiveError::RatchetIoError); } @@ -1361,8 +1359,8 @@ impl Context { .lock() .unwrap() .replace(Application::AeadDec::new(kex_key_b2a.as_ref())); - state.ratchet_state[1] = state.ratchet_state[ratchet_i].clone(); - state.ratchet_state[0] = Some(new_ratchet_state.clone()); + state.ratchet_states[1] = state.ratchet_states[ratchet_i].clone(); + state.ratchet_states[0] = new_ratchet_state; state.cipher_states[0].replace(SessionKey::new( hmac, @@ -1405,9 +1403,9 @@ impl Context { } // Bob failed authentication so we must restart our offer according to Noise. // We restart the offer instead of dropping the session to defend against DOS. - let ratchet_state = state.ratchet_state.clone(); drop(state); let mut state = session.state.write().unwrap(); + let ratchet_state = state.ratchet_states.clone(); if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { if !handshake_state.reinitialize( &session, @@ -1495,8 +1493,46 @@ impl Context { // Bob finished Noise XKhfs+psk2 handshake. let header_send_cipher = Application::PrpEnc::new(handshake_state.header_send_key.as_ref()); let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_ee1peekem1pskpsp); - match check_accept_session(&remote_s_public_key, &message[p_enc_start..p_auth_start]) { - AcceptSessionAction::Accept(application_data) => { + let mut send_reject = || { + // We just used a counter with this key, but we are not storing + // the fact we used it in memory. This is currently ok because the + // handshake is being dropped, so nonce reuse can't happen. + let (mut fragment, len) = encrypt_control( + &mut Application::AeadEnc::new(kex_key_b2a.as_ref()), + &header_send_cipher, + PACKET_TYPE_SESSION_REJECTED, + INIT_COUNTER, + handshake_state.remote_key_id.get(), + &[], + ); + send_unassociated_reply(&mut fragment[..len]); + }; + + let (responder_disallows_downgrade, responder_silently_rejects) = check_accept_session(&remote_s_public_key, &message[p_enc_start..p_auth_start], handshake_state.ratchet_state.chain_len()); + if let Some((responder_disallows_downgrade, application_data)) = responder_disallows_downgrade { + let result = app.restore_by_identity( + &remote_s_public_key, + &application_data, + current_time, + ); + if let Ok(true_ratchet_states) = result { + let mut has_match = false; + for rs in &true_ratchet_states { + if !rs.is_null() { + has_match |= &handshake_state.ratchet_state == rs; + } + } + if !has_match { + if !responder_disallows_downgrade && handshake_state.ratchet_state.is_empty() { + // TODO: add some kind of warning callback or signal. + } else { + if !responder_silently_rejects { + send_reject(); + } + return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); + } + } + let mut noise_kk_ss = Secret::new(); if !self.0.static_keypair.agree(&remote_s_public_key, noise_kk_ss.as_mut()) { return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); @@ -1508,15 +1544,12 @@ impl Context { let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); // We must make sure the ratchet key is saved before we transition. - let new_ratchet_state = RatchetState { - key: rk, - fingerprint: rf, - chain_len: handshake_state.ratchet_state.as_ref().map(|rs| rs.chain_len + 1).unwrap_or(1), - }; + let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(handshake_state.ratchet_state.chain_len() + 1).unwrap()); let result = app.save_ratchet_state( &remote_s_public_key, &application_data, - SaveAction::VerifyThenOverwriteRatchet(handshake_state.ratchet_state.as_ref(), &new_ratchet_state), + [&true_ratchet_states[0], &true_ratchet_states[1]], + [&new_ratchet_state, &RatchetState::Null], current_time, ); if result.is_err() { @@ -1529,13 +1562,13 @@ impl Context { context: Arc::downgrade(&self.0), queue_idx, application_data, - remote_s_public_key, + remote_static_key: remote_s_public_key, send_counter: AtomicU64::new(INIT_COUNTER), session_has_expired: AtomicBool::new(false), counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), state_machine_lock: Mutex::new(()), state: RwLock::new(SessionMutableState { - ratchet_state: [Some(new_ratchet_state.clone()), None], + ratchet_states: [new_ratchet_state.clone(), RatchetState::Null], cipher_states: [ Some(SessionKey::new( hmac, @@ -1584,23 +1617,14 @@ impl Context { // restart the handshake in this case. return Err(byzantine_fault!(FaultType::UnknownLocalKeyId, true)); } + } else { + return Err(ReceiveError::RatchetIoError); } - AcceptSessionAction::SendReject => { - // We just used a counter with this key, but we are not storing - // the fact we used it in memory. This is currently ok because the - // handshake is being dropped, so nonce reuse can't happen. - let (mut fragment, len) = encrypt_control( - &mut Application::AeadEnc::new(kex_key_b2a.as_ref()), - &header_send_cipher, - PACKET_TYPE_SESSION_REJECTED, - INIT_COUNTER, - handshake_state.remote_key_id.get(), - &[], - ); - send_unassociated_reply(&mut fragment[..len]); - return Ok(ReceiveResult::Rejected); + } else { + if !responder_silently_rejects { + send_reject(); } - AcceptSessionAction::SilentlyReject => return Ok(ReceiveResult::Rejected), + return Ok(ReceiveResult::Rejected); } } else { return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); @@ -1735,7 +1759,7 @@ fn initiate_rekey( // Start of Noise KKpsk0 pattern1. // Noise process pattern1 psk0 token. let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].as_ref().unwrap().key.as_ref()); + let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_states[0].key().unwrap()); let noise_h_psk = mix_hash(sha512, &session.noise_kk_local_init_h, &noise_temp_h); // Noise process pattern1 e token. let noise_e_secret = Application::KeyPair::generate(&mut context.rng.lock().unwrap()); @@ -1746,7 +1770,7 @@ fn initiate_rekey( noise_pattern1.noise_e = *noise_e_secret.public_key_bytes(); // Noise process pattern1 es token. let mut noise_es = Secret::new(); - if !noise_e_secret.agree(&session.remote_s_public_key, noise_es.as_mut()) { + if !noise_e_secret.agree(&session.remote_static_key, noise_es.as_mut()) { return Err(()); } noise_ck.mix_key(hmac, noise_es.as_ref()); @@ -1836,11 +1860,12 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu _ => (true, false, SessionEvent::Control), }; if try_delete { - let is_ok = if let Some(rs) = &state.ratchet_state[1] { + let is_ok = if !state.ratchet_states[1].is_null() { app.save_ratchet_state( - &session.remote_s_public_key, + &session.remote_static_key, &session.application_data, - SaveAction::DeleteRatchet(rs), + [&state.ratchet_states[0], &state.ratchet_states[1]], + [&state.ratchet_states[0], &RatchetState::Null], current_time, ) .is_ok() @@ -1855,7 +1880,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu .unwrap() .replace(Application::AeadEnc::new(kex_send_key.as_ref())); } - state.ratchet_state[1] = None; + state.ratchet_states[1] = RatchetState::Null; state.current_key ^= 1; state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); } else { @@ -1917,7 +1942,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let sha512 = &mut Application::Hash::new(); let hmac = &mut Application::HmacHash::new(); let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_state[0].as_ref().unwrap().key.as_ref()); + let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_states[0].key().unwrap()); let noise_h_psk = mix_hash(sha512, &session.noise_kk_remote_init_h, &noise_temp_h); // Noise process pattern1 e token. // Get public key validation out of the way early @@ -1926,7 +1951,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let mut noise_se = Secret::new(); if let Some(alice_e) = from_bytes_agreement::(&noise_pattern1.noise_e, &context.0.static_keypair, noise_es.as_mut()) { let bob_e_secret = Application::KeyPair::generate(&mut context.0.rng.lock().unwrap()); - if bob_e_secret.agree(&alice_e, noise_ee.as_mut()) && bob_e_secret.agree(&session.remote_s_public_key, noise_se.as_mut()) { + if bob_e_secret.agree(&alice_e, noise_ee.as_mut()) && bob_e_secret.agree(&session.remote_static_key, noise_se.as_mut()) { let noise_h_pske = mix_hash(sha512, &noise_h_psk, alice_e.as_bytes()); noise_ck.mix_key(hmac, alice_e.as_bytes()); // Noise process pattern1 es token. @@ -1977,15 +2002,12 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu drop(noise_k_pskessseese); // Bob finished Noise KKpsk0 handshake. let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); - let new_ratchet_state = RatchetState { - key: rk, - fingerprint: rf, - chain_len: state.ratchet_state[0].as_ref().map(|rs| rs.chain_len + 1).unwrap_or(1), - }; + let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(state.ratchet_states[0].chain_len() + 1).unwrap()); let result = app.save_ratchet_state( - &session.remote_s_public_key, + &session.remote_static_key, &session.application_data, - SaveAction::AddRatchet(&new_ratchet_state), + [&state.ratchet_states[0], &state.ratchet_states[1]], + [&new_ratchet_state, &state.ratchet_states[0]], current_time, ); if result.is_err() { @@ -2011,8 +2033,8 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu .lock() .unwrap() .replace(Application::AeadDec::new(kex_key_a2b.as_ref())); - state.ratchet_state[1] = state.ratchet_state[0].clone(); - state.ratchet_state[0] = Some(new_ratchet_state.clone()); + state.ratchet_states[1] = state.ratchet_states[0].clone(); + state.ratchet_states[0] = new_ratchet_state.clone(); state.cipher_states[next_key_index].replace(SessionKey::new( hmac, @@ -2082,15 +2104,12 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu // Bob fully authenticated. // Alice finished Noise KKpsk0 handshake. let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); - let new_ratchet_state = RatchetState { - key: rk, - fingerprint: rf, - chain_len: state.ratchet_state[0].as_ref().map(|rs| rs.chain_len + 1).unwrap_or(1), - }; + let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(state.ratchet_states[0].chain_len() + 1).unwrap()); let result = app.save_ratchet_state( - &session.remote_s_public_key, + &session.remote_static_key, &session.application_data, - SaveAction::DeleteThenAddRatchet(&state.ratchet_state[0].as_ref().unwrap(), &new_ratchet_state), + [&state.ratchet_states[0], &state.ratchet_states[1]], + [&new_ratchet_state, &RatchetState::Null], current_time, ); if result.is_err() { @@ -2118,8 +2137,8 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu .lock() .unwrap() .replace(Application::AeadEnc::new(kex_key_a2b.as_ref())); - state.ratchet_state[1] = None; - state.ratchet_state[0] = Some(new_ratchet_state.clone()); + state.ratchet_states[1] = RatchetState::Null; + state.ratchet_states[0] = new_ratchet_state.clone(); state.cipher_states[next_key_index].replace(SessionKey::new( hmac, @@ -2196,22 +2215,19 @@ impl Session { /// The static public key of the remote peer. #[inline] pub fn remote_s_public_key(&self) -> &Application::PublicKey { - &self.remote_s_public_key + &self.remote_static_key } - /// The most recent confirmed ratchet state of this session. + /// The current ratchet state of this session. /// The returned values are sensitive and should be securely erased before being dropped. #[inline] - pub fn ratchet_state(&self) -> [Option; 2] { + pub fn ratchet_states(&self) -> [RatchetState; 2] { let state = self.state.read().unwrap(); - state.ratchet_state.clone() + state.ratchet_states.clone() } - /// The most recent confirmed ratchet number of this session. + /// The current ratchet count of this session. #[inline] pub fn ratchet_count(&self) -> u64 { - self.state.read().unwrap().ratchet_state[0] - .as_ref() - .map(|rs| rs.chain_len) - .unwrap_or(0) + self.state.read().unwrap().ratchet_states[0].chain_len() } /// Mark a session as expired. This will make it impossible for this session to successfully /// receive or send data. It is recommended to simply `drop` the session instead, but this can @@ -2295,7 +2311,7 @@ impl NoiseXKAliceHandshake { fn initialize( local_key_id: NonZeroU32, remote_s_public_key: &Application::PublicKey, - ratchet_state: &[Option; 2], + ratchet_state: &[RatchetState; 2], rng: &mut Application::Rng, ) -> Result< ( @@ -2345,10 +2361,10 @@ impl NoiseXKAliceHandshake { // Noise process pattern1 payload. let noise_pattern1: &mut NoiseXKPattern1 = byte_array_as_proto_buffer_mut(&mut message); let mut idx = 0; - for r in ratchet_state { - if let Some(rs) = r { + for rs in ratchet_state { + if let Some(rf) = rs.fingerprint() { let next_idx = idx + RATCHET_SIZE; - noise_pattern1.payload[idx..next_idx].copy_from_slice(rs.fingerprint.as_ref()); + noise_pattern1.payload[idx..next_idx].copy_from_slice(rf); idx = next_idx; } } @@ -2386,13 +2402,13 @@ impl NoiseXKAliceHandshake { fn reinitialize( &mut self, session: &Arc>, - ratchet_state: &[Option; 2], + ratchet_state: &[RatchetState; 2], session_map: &mut HashMap>, bool)>, rng: &mut Application::Rng, current_time: i64, ) -> bool { let local_key_id = generate_key_id(session_map, rng); - if let Ok((offer, a2b_header_key, b2a_header_key)) = Self::initialize(local_key_id, &session.remote_s_public_key, ratchet_state, rng) { + if let Ok((offer, a2b_header_key, b2a_header_key)) = Self::initialize(local_key_id, &session.remote_static_key, ratchet_state, rng) { self.timeout = current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS); session_map.remove(&self.local_key_id); session_map.insert(local_key_id, (Arc::downgrade(session), false)); @@ -2580,7 +2596,7 @@ fn send_with_fragmentation( fragment_count as u8, fragment_no as u8, packet_type, - remote_key_id.map(|n| n.get()).unwrap_or(0), + remote_key_id.map_or(0, |n| n.get()), counter_or_id, ); if let Some(hcc) = header_cipher { From 67f7bb5891c8847e94bfe89862e24745270f8270 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 12:17:37 -0400 Subject: [PATCH 13/25] cargo fmt --- src/applicationlayer.rs | 19 ++++++++----------- src/error.rs | 2 +- src/lib.rs | 2 +- src/zssp.rs | 29 ++++++++++++----------------- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/applicationlayer.rs b/src/applicationlayer.rs index ed9e020..7ac2bf2 100644 --- a/src/applicationlayer.rs +++ b/src/applicationlayer.rs @@ -168,7 +168,12 @@ pub trait ApplicationLayer: Sized { Ok(RatchetState::Null) } #[allow(unused)] - fn restore_by_identity(&self, remote_static_key: &Self::PublicKey, application_data: &Self::Data, current_time: i64) -> Result<[RatchetState; 2], ()> { + fn restore_by_identity( + &self, + remote_static_key: &Self::PublicKey, + application_data: &Self::Data, + current_time: i64, + ) -> Result<[RatchetState; 2], ()> { Ok([RatchetState::Null, RatchetState::Null]) } /// Atomically save the given ratchet key, fingerprint and number to persistent storage. @@ -213,16 +218,8 @@ pub enum RatchetState { } use RatchetState::*; impl RatchetState { - pub fn new_nonempty( - key: Secret, - fingerprint: Secret, - chain_len: NonZeroU64, - ) -> Self { - NonEmpty(NonEmptyRatchetState { - key, - fingerprint, - chain_len, - }) + pub fn new_nonempty(key: Secret, fingerprint: Secret, chain_len: NonZeroU64) -> Self { + NonEmpty(NonEmptyRatchetState { key, fingerprint, chain_len }) } pub fn new_initial_states() -> [RatchetState; 2] { [RatchetState::Empty, RatchetState::Null] diff --git a/src/error.rs b/src/error.rs index 34e2dbc..84ead7e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,7 +14,7 @@ pub enum OpenError { /// Local identity blob is too large to send, even with fragmentation. DataTooLarge, - RatchetIoError + RatchetIoError, } #[derive(Debug, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index d0025df..221d7a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,4 +21,4 @@ pub mod error; pub use crate::applicationlayer::{ApplicationLayer, RatchetState}; pub use crate::log_event::LogEvent; pub use crate::proto::{MAX_IDENTITY_BLOB_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU, RATCHET_SIZE}; -pub use crate::zssp::{AcceptSessionAction, Context, ContextInner, IncomingSessionAction, ReceiveResult, Session, SessionEvent}; +pub use crate::zssp::{Context, ContextInner, IncomingSessionAction, ReceiveResult, Session, SessionEvent}; diff --git a/src/zssp.rs b/src/zssp.rs index e35bdd5..c4ee0a3 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -110,12 +110,6 @@ pub enum IncomingSessionAction { Drop, } -pub enum AcceptSessionAction { - Accept(Application::Data), - SendReject, - SilentlyReject, -} - /// ZeroTier Secure Session Protocol (ZSSP) Session /// /// A FIPS/NIST compliant variant of Noise_XK with hybrid Kyber1024 PQ data forward secrecy. @@ -1328,8 +1322,7 @@ impl Context { drop(noise_k_eseeekem1pskse); // Alice finished Noise XKhfs+psk2 handshake. // Transition offer state machine to the NoiseXKPattern3 state. - let (rk, rf) = - noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); + let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(chain_len + 1).unwrap()); let ratchet_to_preserve = &state.ratchet_states[ratchet_i]; @@ -1338,7 +1331,7 @@ impl Context { &session.application_data, [&state.ratchet_states[0], &state.ratchet_states[1]], [&new_ratchet_state, ratchet_to_preserve], - current_time + current_time, ); if result.is_err() { return Err(ReceiveError::RatchetIoError); @@ -1508,13 +1501,13 @@ impl Context { send_unassociated_reply(&mut fragment[..len]); }; - let (responder_disallows_downgrade, responder_silently_rejects) = check_accept_session(&remote_s_public_key, &message[p_enc_start..p_auth_start], handshake_state.ratchet_state.chain_len()); + let (responder_disallows_downgrade, responder_silently_rejects) = check_accept_session( + &remote_s_public_key, + &message[p_enc_start..p_auth_start], + handshake_state.ratchet_state.chain_len(), + ); if let Some((responder_disallows_downgrade, application_data)) = responder_disallows_downgrade { - let result = app.restore_by_identity( - &remote_s_public_key, - &application_data, - current_time, - ); + let result = app.restore_by_identity(&remote_s_public_key, &application_data, current_time); if let Ok(true_ratchet_states) = result { let mut has_match = false; for rs in &true_ratchet_states { @@ -1544,7 +1537,8 @@ impl Context { let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); // We must make sure the ratchet key is saved before we transition. - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(handshake_state.ratchet_state.chain_len() + 1).unwrap()); + let new_ratchet_state = + RatchetState::new_nonempty(rk, rf, NonZeroU64::new(handshake_state.ratchet_state.chain_len() + 1).unwrap()); let result = app.save_ratchet_state( &remote_s_public_key, &application_data, @@ -2104,7 +2098,8 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu // Bob fully authenticated. // Alice finished Noise KKpsk0 handshake. let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(state.ratchet_states[0].chain_len() + 1).unwrap()); + let new_ratchet_state = + RatchetState::new_nonempty(rk, rf, NonZeroU64::new(state.ratchet_states[0].chain_len() + 1).unwrap()); let result = app.save_ratchet_state( &session.remote_static_key, &session.application_data, From 85b15b4d240031a945359f00556e44644e565902 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 12:39:37 -0400 Subject: [PATCH 14/25] updated readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bdb94cd..d11113b 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,15 @@ Specifically ZSSP implements the [Noise XK](http://noiseprotocol.org/noise.html# Hybrid post-quantum forward secrecy using Kyber1024 is performed alongside Noise with the result being mixed in alongside an optional pre-shared key at the end of session negotiation. -ZSSP is designed for use in ZeroTier 2 but is payload-agnostic and could easily be adapted for use in other projects. +ZSSP is designed for use in ZeroTier but is payload-agnostic and could easily be adapted for use in other projects. + +Further information can be found in the ZSSP whitepaper (pending official release). ## Cryptographic Primitives Used - AES-256-GCM: Authenticated encryption - - HMAC-SHA384: Key mixing, sub-key derivation in key-based KDF construction + - SHA512: Used with the KBKDF construction, also used in a proof of work and ip ownership DOS mitigation scheme + - KBKDF: Key mixing, sub-key derivation - NIST P-384 ECDH: Elliptic curve key exchange during initial handshake and for periodic re-keying during the session - Kyber1024: Quantum attack resistant lattice-based key exchange during initial handshake - - AES-256-ECB: Single 128-bit block encryption of header information to harden the fragmentation protocol against denial of service attack (see section on header protection) - + - AES-256: 128-bit PRP for authenticated encryption of header information to harden the fragmentation protocol against DOS (see section on header protection) From ffca778c79cf91974571f377be788b355ee933e5 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 12:47:27 -0400 Subject: [PATCH 15/25] updated readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d11113b..6164906 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ Further information can be found in the ZSSP whitepaper (pending official releas ## Cryptographic Primitives Used - AES-256-GCM: Authenticated encryption - - SHA512: Used with the KBKDF construction, also used in a proof of work and ip ownership DOS mitigation scheme + - SHA512: Used with the KBKDF construction, also used in a proof of work and IP ownership DOS mitigation scheme - KBKDF: Key mixing, sub-key derivation - NIST P-384 ECDH: Elliptic curve key exchange during initial handshake and for periodic re-keying during the session - Kyber1024: Quantum attack resistant lattice-based key exchange during initial handshake - - AES-256: 128-bit PRP for authenticated encryption of header information to harden the fragmentation protocol against DOS (see section on header protection) + - AES-256: 128-bit PRP for AES-256-GCM and for authenticated encryption of headera to harden fragmentation against DOS (see section on header protection) From 308fc3da70ec80fb5eeebab31393b2280668a508 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 12:57:46 -0400 Subject: [PATCH 16/25] added comment --- src/crypto/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index b033569..0c7f601 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -6,5 +6,7 @@ pub mod p384; pub mod secret; pub mod sha512; +// We re-export our dependencies so it is less of a headache for the implementor to use the same +// exact version of them. pub use pqc_kyber; pub use rand_core; From 99556bb1eb69f2b85f92095ee3fcbdf1e16a9b82 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 13:38:03 -0400 Subject: [PATCH 17/25] updated docs --- Cargo.toml | 2 +- src/applicationlayer.rs | 84 ++++++----------------------------------- src/crypto/secret.rs | 18 +++------ src/lib.rs | 4 +- src/ratchet_state.rs | 76 +++++++++++++++++++++++++++++++++++++ src/symmetric_state.rs | 12 +++--- src/zssp.rs | 7 +++- 7 files changed, 108 insertions(+), 95 deletions(-) create mode 100644 src/ratchet_state.rs diff --git a/Cargo.toml b/Cargo.toml index e6078c3..74aa8d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["ZeroTier, Inc. ", "Adam Ierymenko ; - /// This function will be called whenever Alice's initial Hello packet contains the zero ratchet - /// fingerprint. Brand new peers will always connect to Bob with the zero ratchet key, but from - /// then on they should be using non-zero ratchet keys. + /// This function will be called whenever Alice's initial Hello packet contains the empty ratchet + /// fingerprint. Brand new peers will always connect to Bob with the empty ratchet, but from + /// then on they should be using non-empty ratchet states. /// - /// If this returns false, we will attempt to connect to Alice with the zero ratchet key. + /// If this returns false, we will attempt to connect to Alice with the empty ratchet state. /// If this returns true, Alice's connection will be silently dropped. /// If this function is configured to always return true, it means peers will not be able to /// connect to us unless they had a prior-established ratchet key with us. This is the best way @@ -130,11 +128,11 @@ pub trait ApplicationLayer: Sized { false } /// This function is called if we, as Alice, attempted to open a session with Bob using a - /// non-zero ratchet key, but Bob does not have this ratchet key and wants to downgrade + /// non-empty ratchet key, but Bob does not have this ratchet key and wants to downgrade /// to the zero ratchet key. /// - /// If it returns true Alice will downgrade their ratchet number to 0, potentially ending their - /// current ratchet chain. + /// If it returns true Alice will downgrade their ratchet state to emtpy, potentially ending + /// their current ratchet chain. /// If it returns false then we will consider Bob as having failed authentication, and this /// packet will be dropped. The session will continue attempting to connect to Bob. /// @@ -152,7 +150,7 @@ pub trait ApplicationLayer: Sized { false } /// Lookup a specific ratchet key based on its ratchet fingerprint. - /// This function will be called whenever Alice attempts to connect to us with a non-zero + /// This function will be called whenever Alice attempts to connect to us with a non-empty /// ratchet fingerprint. /// /// If the ratchet key was found, the function should return `RestoreAction::RestoreRatchet`. This will @@ -161,7 +159,7 @@ pub trait ApplicationLayer: Sized { /// If the ratchet key could not be found, the application may choose between returning /// `RatchetAction::DowngradeRatchet` or `RatchetAction::FailAuthentication`. /// If `RatchetAction::DowngradeRatchet` is returned we will attempt to convince Alice to downgrade - /// to the zero ratchet key, restarting the ratchet chain. + /// to the empty ratchet key, restarting the ratchet chain. /// If `RatchetAction::FailAuthentication` is returned Alice's connection will be silently dropped. #[allow(unused)] fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result { @@ -188,7 +186,7 @@ pub trait ApplicationLayer: Sized { /// If persistent storage is supported, this function should not return until the ratchet state /// is saved, otherwise it is possible, albeit unlikely, for a sudden restart of the local /// machine to put our ratchet state out of sync with the remote peer. If this happens the only - /// fix is to reset both ratchet keys to zero. + /// fix is to reset both ratchet keys to empty. /// /// This function may also save state to volatile storage, in which case all peers which connect /// to us will have to allow downgrade @@ -209,63 +207,3 @@ pub trait ApplicationLayer: Sized { #[inline] fn event_log<'a>(&self, event: LogEvent<'a, Self>, current_time: i64) {} } - -#[derive(Clone, PartialEq, Eq)] -pub enum RatchetState { - Null, - Empty, - NonEmpty(NonEmptyRatchetState), -} -use RatchetState::*; -impl RatchetState { - pub fn new_nonempty(key: Secret, fingerprint: Secret, chain_len: NonZeroU64) -> Self { - NonEmpty(NonEmptyRatchetState { key, fingerprint, chain_len }) - } - pub fn new_initial_states() -> [RatchetState; 2] { - [RatchetState::Empty, RatchetState::Null] - } - pub fn is_null(&self) -> bool { - match self { - Null => true, - _ => false, - } - } - pub fn is_empty(&self) -> bool { - match self { - Empty => true, - _ => false, - } - } - #[inline] - pub fn nonempty(&self) -> Option<&NonEmptyRatchetState> { - match self { - NonEmpty(rs) => Some(&rs), - _ => None, - } - } - #[inline] - pub fn chain_len(&self) -> u64 { - self.nonempty().map_or(0, |rs| rs.chain_len.get()) - } - #[inline] - pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - self.nonempty().map_or(None, |rs| Some(&rs.fingerprint.as_ref())) - } - #[inline] - pub fn key(&self) -> Option<&[u8; RATCHET_SIZE]> { - const ZERO_KEY: [u8; RATCHET_SIZE] = [0u8; RATCHET_SIZE]; - match self { - Null => None, - Empty => Some(&ZERO_KEY), - NonEmpty(rs) => Some(rs.key.as_ref()), - } - } -} -/// A ratchet key and fingerprint, -/// along with the length of the ratchet chain the keys were derived from. -#[derive(Clone, PartialEq, Eq)] -pub struct NonEmptyRatchetState { - pub key: Secret, - pub fingerprint: Secret, - pub chain_len: NonZeroU64, -} diff --git a/src/crypto/secret.rs b/src/crypto/secret.rs index 9e00abd..53aa9dc 100644 --- a/src/crypto/secret.rs +++ b/src/crypto/secret.rs @@ -1,5 +1,4 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. - use std::convert::TryInto; /// Constant time byte slice equality. @@ -36,20 +35,15 @@ impl Secret { pub fn new() -> Self { Self([0_u8; L]) } - - /// Moves bytes into secret, will panic if the slice does not match the size of this secret. - #[inline(always)] - pub fn move_bytes(b: [u8; L]) -> Self { - Self(b) - } - - /// Copy bytes into secret, then nuke the previous value, will panic if the slice does not match the size of this secret. - #[inline(always)] - pub fn from_bytes_then_nuke(b: &mut [u8]) -> Self { + /// Copy bytes into secret, then delete the previous value, will panic if the slice does not match the size of this secret. + #[inline(never)] + pub fn from_bytes_then_delete(b: &mut [u8]) -> Self { let ret = Self(b.try_into().unwrap()); b.fill(0); ret } + /// Moves bytes into secret, will panic if the slice does not match the size of this secret. + /// This is unsafe because it will not destroy the contents of its input. #[inline(always)] pub unsafe fn from_bytes(b: &[u8]) -> Self { Self(b.try_into().unwrap()) @@ -92,7 +86,7 @@ impl Secret { } impl Drop for Secret { - #[inline(always)] + #[inline(never)] fn drop(&mut self) { self.0.fill(0); } diff --git a/src/lib.rs b/src/lib.rs index 221d7a6..46d0713 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,11 +14,13 @@ mod handshake_cache; mod indexed_heap; mod log_event; mod proto; +mod ratchet_state; mod symmetric_state; mod zssp; pub mod error; -pub use crate::applicationlayer::{ApplicationLayer, RatchetState}; +pub use crate::applicationlayer::ApplicationLayer; pub use crate::log_event::LogEvent; pub use crate::proto::{MAX_IDENTITY_BLOB_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU, RATCHET_SIZE}; +pub use crate::ratchet_state::RatchetState; pub use crate::zssp::{Context, ContextInner, IncomingSessionAction, ReceiveResult, Session, SessionEvent}; diff --git a/src/ratchet_state.rs b/src/ratchet_state.rs new file mode 100644 index 0000000..c78d0f0 --- /dev/null +++ b/src/ratchet_state.rs @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +use std::num::NonZeroU64; + +use crate::crypto::secret::Secret; +use crate::RATCHET_SIZE; + +#[derive(Clone, PartialEq, Eq)] +pub enum RatchetState { + Null, + Empty, + NonEmpty(NonEmptyRatchetState), +} +use RatchetState::*; +impl RatchetState { + #[inline] + pub fn new_nonempty(key: Secret, fingerprint: Secret, chain_len: NonZeroU64) -> Self { + NonEmpty(NonEmptyRatchetState { key, fingerprint, chain_len }) + } + #[inline] + pub fn new_initial_states() -> [RatchetState; 2] { + [RatchetState::Empty, RatchetState::Null] + } + #[inline] + pub fn is_null(&self) -> bool { + match self { + Null => true, + _ => false, + } + } + #[inline] + pub fn is_empty(&self) -> bool { + match self { + Empty => true, + _ => false, + } + } + #[inline] + pub fn nonempty(&self) -> Option<&NonEmptyRatchetState> { + match self { + NonEmpty(rs) => Some(&rs), + _ => None, + } + } + #[inline] + pub fn chain_len(&self) -> u64 { + self.nonempty().map_or(0, |rs| rs.chain_len.get()) + } + #[inline] + pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { + self.nonempty().map_or(None, |rs| Some(&rs.fingerprint.as_ref())) + } + #[inline] + pub fn key(&self) -> Option<&[u8; RATCHET_SIZE]> { + const ZERO_KEY: [u8; RATCHET_SIZE] = [0u8; RATCHET_SIZE]; + match self { + Null => None, + Empty => Some(&ZERO_KEY), + NonEmpty(rs) => Some(rs.key.as_ref()), + } + } +} +/// A ratchet key and fingerprint, +/// along with the length of the ratchet chain the keys were derived from. +#[derive(Clone, PartialEq, Eq)] +pub struct NonEmptyRatchetState { + pub key: Secret, + pub fingerprint: Secret, + pub chain_len: NonZeroU64, +} diff --git a/src/symmetric_state.rs b/src/symmetric_state.rs index 339123c..6f6ac6f 100644 --- a/src/symmetric_state.rs +++ b/src/symmetric_state.rs @@ -42,7 +42,7 @@ impl SymmetricState { self.token_counter += 1; self.chaining_key.overwrite(&next_ck); - Secret::from_bytes_then_nuke(&mut temp_k[..AES_256_KEY_SIZE]) + Secret::from_bytes_then_delete(&mut temp_k[..AES_256_KEY_SIZE]) } /// Corresponds to Noise `MixKeyAndHash`. pub(crate) fn mix_key_and_hash(&mut self, hm: &mut impl HmacSha512, input_key_material: &[u8]) -> [u8; NOISE_HASHLEN] { @@ -77,7 +77,7 @@ impl SymmetricState { self.token_counter += 1; self.chaining_key.overwrite(&next_ck); - (temp_h, Secret::from_bytes_then_nuke(&mut temp_k[..AES_256_KEY_SIZE])) + (temp_h, Secret::from_bytes_then_delete(&mut temp_k[..AES_256_KEY_SIZE])) } /// Get an additional symmetric key (ASK) that is a collision resistant hash of the transcript, /// is forward secrect and is cryptographically independent from all other produced keys. @@ -94,8 +94,8 @@ impl SymmetricState { let mut temp_k2 = [0u8; NOISE_HASHLEN]; self.kbkdf(hm, noise_h, [b'A', b'S', b'K', label], 2, &mut temp_k1, Some(&mut temp_k2), None); ( - Secret::from_bytes_then_nuke(&mut temp_k1[..AES_256_KEY_SIZE]), - Secret::from_bytes_then_nuke(&mut temp_k2[..AES_256_KEY_SIZE]), + Secret::from_bytes_then_delete(&mut temp_k1[..AES_256_KEY_SIZE]), + Secret::from_bytes_then_delete(&mut temp_k2[..AES_256_KEY_SIZE]), ) } /// Corresponds to Noise `Split`. @@ -107,8 +107,8 @@ impl SymmetricState { // Normally KBKDF would not truncate to derive the correct length of AES keys, // but Noise specifies that the AES keys be truncated from NOISE_HASHLEN to AES_256_KEY_SIZE. ( - Secret::from_bytes_then_nuke(&mut temp_k1[..AES_256_KEY_SIZE]), - Secret::from_bytes_then_nuke(&mut temp_k2[..AES_256_KEY_SIZE]), + Secret::from_bytes_then_delete(&mut temp_k1[..AES_256_KEY_SIZE]), + Secret::from_bytes_then_delete(&mut temp_k2[..AES_256_KEY_SIZE]), ) } #[inline(always)] diff --git a/src/zssp.rs b/src/zssp.rs index c4ee0a3..4eaeb95 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -24,7 +24,6 @@ use crate::crypto::rand_core::RngCore; use crate::crypto::secret::{secure_eq, Secret}; use crate::crypto::sha512::{HmacSha512, Sha512}; -use crate::applicationlayer::*; use crate::error::{FaultType, OpenError, ReceiveError, SendError}; use crate::frag_cache::UnassociatedFragCache; use crate::fragged::{Assembled, Fragged}; @@ -33,6 +32,7 @@ use crate::indexed_heap::{BinaryHeapIndex, IndexedBinaryHeap}; use crate::log_event::LogEvent; use crate::proto::*; use crate::symmetric_state::SymmetricState; +use crate::{applicationlayer::*, RatchetState}; /// Session context for local application. /// @@ -1036,7 +1036,7 @@ impl Context { // Noise process pattern2 ekem1 token. let (noise_ekem1, noise_ekem1_secret) = pqc_kyber::encapsulate(&noise_pattern1.noise_e1, self.0.rng.lock().unwrap().deref_mut()) .map_err(|_| byzantine_fault!(FaultType::FailedAuthentication, false)) - .map(|(ct, ekem1)| (ct, Secret::move_bytes(ekem1)))?; + .map(|(ct, ekem1)| (ct, Secret(ekem1)))?; // Alice fully authenticated. noise_pattern2.noise_ekem1 = noise_ekem1; let noise_h_ee1peekem1 = encrypt_and_hash::( @@ -1279,6 +1279,9 @@ impl Context { if result.is_none() && !app.initiator_disallows_downgrade(&session, current_time) { chain_len = 0; result = test_ratchet_key(&[0u8; RATCHET_SIZE]); + if result.is_some() { + // TODO: add some kind of warning callback or signal. + } } if let Some((remote_key_id, mut noise_ck, noise_k_eseeekem1psk, noise_h_ee1peekem1pskp)) = result { From e5a7901482541ebe320190834da1c4e52eb2f15b Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 13:42:39 -0400 Subject: [PATCH 18/25] cargo clippy --- src/zssp.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/zssp.rs b/src/zssp.rs index 4eaeb95..a4c8991 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -2499,7 +2499,6 @@ fn encrypt_control( c.encrypt_in_place(&mut fragment[HEADER_SIZE..fragment_len - AES_GCM_TAG_SIZE]); } c.finish_encrypt((&mut fragment[fragment_len - AES_GCM_TAG_SIZE..fragment_len]).try_into().unwrap()); - drop(c); set_packet_header(&mut fragment, 1, 0, packet_type, remote_key_id, counter); header_cipher.encrypt_in_place((&mut fragment[HEADER_PROTECT_ENC_START..HEADER_PROTECT_ENC_END]).try_into().unwrap()); (fragment, fragment_len) @@ -2678,7 +2677,7 @@ impl SessionKey { } #[inline(always)] - fn get_send_cipher<'a>(&'a self, counter: u64) -> Result, SendError> { + fn get_send_cipher(&self, counter: u64) -> Result, SendError> { if counter < self.expire_at_counter { Ok(self.send_cipher_pool[(counter as usize) % self.send_cipher_pool.len()].lock().unwrap()) } else { @@ -2687,7 +2686,7 @@ impl SessionKey { } #[inline(always)] - fn get_receive_cipher<'a>(&'a self, counter: u64) -> MutexGuard<'a, Application::AeadDec> { + fn get_receive_cipher(&self, counter: u64) -> MutexGuard { let idx = (counter as usize) % self.receive_cipher_pool.len(); self.receive_cipher_pool[idx].lock().unwrap() } From 14d62543e3e37fc7060f274ec3248600b619ddc2 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 14:16:07 -0400 Subject: [PATCH 19/25] cargo clippy --- src/applicationlayer.rs | 14 ++- src/crypto/secret.rs | 4 +- src/error.rs | 8 +- src/handshake_cache.rs | 2 +- src/indexed_heap.rs | 2 +- src/ratchet_state.rs | 14 +-- src/zssp.rs | 265 ++++++++++++++++++++-------------------- 7 files changed, 152 insertions(+), 157 deletions(-) diff --git a/src/applicationlayer.rs b/src/applicationlayer.rs index d880129..4d77d9f 100644 --- a/src/applicationlayer.rs +++ b/src/applicationlayer.rs @@ -98,6 +98,8 @@ pub trait ApplicationLayer: Sized { type PublicKey: P384PublicKey; type KeyPair: P384KeyPair; + type IoError: std::fmt::Debug; + /// Type for arbitrary opaque object for use by the application that is attached to /// each session. type Data; @@ -162,7 +164,7 @@ pub trait ApplicationLayer: Sized { /// to the empty ratchet key, restarting the ratchet chain. /// If `RatchetAction::FailAuthentication` is returned Alice's connection will be silently dropped. #[allow(unused)] - fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result { + fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result { Ok(RatchetState::Null) } #[allow(unused)] @@ -171,7 +173,7 @@ pub trait ApplicationLayer: Sized { remote_static_key: &Self::PublicKey, application_data: &Self::Data, current_time: i64, - ) -> Result<[RatchetState; 2], ()> { + ) -> Result<[RatchetState; 2], Self::IoError> { Ok([RatchetState::Null, RatchetState::Null]) } /// Atomically save the given ratchet key, fingerprint and number to persistent storage. @@ -179,7 +181,7 @@ pub trait ApplicationLayer: Sized { /// See the documentation of `SaveAction` for more details on how to save them to storage, /// and how to handle any pre-existing ratchet keys, fingerprints and numbers. /// - /// If this returns `Err(())`, the packet which triggered this function to be called will be + /// If this returns `Err(IoError)`, the packet which triggered this function to be called will be /// dropped, and no session state will be mutated, preserving synchronization. The remote peer /// will eventually resend that packet and so this function will be called again. /// @@ -193,17 +195,17 @@ pub trait ApplicationLayer: Sized { /// (`initiator_disallows_downgrade` returns false and/or`restore_ratchet` returns `DowngradeRatchet`). /// Otherwise, when we restart, we will not be allowed to reconnect. #[allow(unused)] - fn save_ratchet_state<'a>( + fn save_ratchet_state( &self, remote_static_key: &Self::PublicKey, application_data: &Self::Data, previous_ratchet_states: [&RatchetState; 2], new_ratchet_states: [&RatchetState; 2], current_time: i64, - ) -> Result<(), ()> { + ) -> Result<(), Self::IoError> { Ok(()) } #[allow(unused)] #[inline] - fn event_log<'a>(&self, event: LogEvent<'a, Self>, current_time: i64) {} + fn event_log(&self, event: LogEvent, current_time: i64) {} } diff --git a/src/crypto/secret.rs b/src/crypto/secret.rs index 53aa9dc..60399b3 100644 --- a/src/crypto/secret.rs +++ b/src/crypto/secret.rs @@ -36,7 +36,6 @@ impl Secret { Self([0_u8; L]) } /// Copy bytes into secret, then delete the previous value, will panic if the slice does not match the size of this secret. - #[inline(never)] pub fn from_bytes_then_delete(b: &mut [u8]) -> Self { let ret = Self(b.try_into().unwrap()); b.fill(0); @@ -44,6 +43,8 @@ impl Secret { } /// Moves bytes into secret, will panic if the slice does not match the size of this secret. /// This is unsafe because it will not destroy the contents of its input. + /// # Safety + /// Make sure the contents of the input are securely deleted. #[inline(always)] pub unsafe fn from_bytes(b: &[u8]) -> Self { Self(b.try_into().unwrap()) @@ -86,7 +87,6 @@ impl Secret { } impl Drop for Secret { - #[inline(never)] fn drop(&mut self) { self.0.fill(0); } diff --git a/src/error.rs b/src/error.rs index 84ead7e..63c31c0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,14 +7,14 @@ */ #[derive(Debug, PartialEq, Eq)] -pub enum OpenError { +pub enum OpenError { /// An invalid parameter was supplied to the function. InvalidPublicKey, /// Local identity blob is too large to send, even with fragmentation. DataTooLarge, - RatchetIoError, + RatchetIoError(IoError), } #[derive(Debug, PartialEq, Eq)] @@ -61,7 +61,7 @@ pub enum FaultType { } #[derive(Debug, PartialEq, Eq)] -pub enum ReceiveError { +pub enum ReceiveError { /// A type of fault that can occur because a remote peer sent us a bad packet. /// Such packets will be ignored by ZSSP but a user of ZSSP might want to log /// them for debugging or tracing. @@ -106,5 +106,5 @@ pub enum ReceiveError { /// One of the ratchet saving or lookup functions returned an error, so the packet had to be /// dropped. - RatchetIoError, + RatchetIoError(IoError), } diff --git a/src/handshake_cache.rs b/src/handshake_cache.rs index a69ce68..25f219a 100644 --- a/src/handshake_cache.rs +++ b/src/handshake_cache.rs @@ -73,7 +73,7 @@ impl UnassociatedHandshakeCache { return true; } } - return false; + false } pub(crate) fn service(&self, current_time: i64) { // Only check for expiration if we have a pending packet. diff --git a/src/indexed_heap.rs b/src/indexed_heap.rs index 721f287..8a0547c 100644 --- a/src/indexed_heap.rs +++ b/src/indexed_heap.rs @@ -94,7 +94,7 @@ impl IndexedBinaryHeap { (idx.0 < self.map.len() && self.map[idx.0].1 == idx.1).then(|| self.map[idx.0].0) } pub fn pop(&mut self) -> Option<(T, P)> { - (self.data.len() > 0).then(|| self.remove_idx(0)) + (!self.data.is_empty()).then(|| self.remove_idx(0)) } /// Add an item to the queue and get back a generational index which allows for quick updating /// of this item and its priority. diff --git a/src/ratchet_state.rs b/src/ratchet_state.rs index c78d0f0..fddbc57 100644 --- a/src/ratchet_state.rs +++ b/src/ratchet_state.rs @@ -29,22 +29,16 @@ impl RatchetState { } #[inline] pub fn is_null(&self) -> bool { - match self { - Null => true, - _ => false, - } + matches!(self, Null) } #[inline] pub fn is_empty(&self) -> bool { - match self { - Empty => true, - _ => false, - } + matches!(self, Empty) } #[inline] pub fn nonempty(&self) -> Option<&NonEmptyRatchetState> { match self { - NonEmpty(rs) => Some(&rs), + NonEmpty(rs) => Some(rs), _ => None, } } @@ -54,7 +48,7 @@ impl RatchetState { } #[inline] pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - self.nonempty().map_or(None, |rs| Some(&rs.fingerprint.as_ref())) + self.nonempty().map(|rs| rs.fingerprint.as_ref()) } #[inline] pub fn key(&self) -> Option<&[u8; RATCHET_SIZE]> { diff --git a/src/zssp.rs b/src/zssp.rs index a4c8991..0bc0763 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -395,7 +395,7 @@ impl Context { } } NoiseKKPattern1 { next_retry_time, timeout, noise_message, .. } | NoiseKKPattern2 { next_retry_time, timeout, noise_message, .. } => { - if let Some(ts) = process_timer(&next_retry_time, Application::RETRY_INTERVAL_MS, current_time) { + if let Some(ts) = process_timer(next_retry_time, Application::RETRY_INTERVAL_MS, current_time) { ts } else { if *timeout <= current_time { @@ -418,7 +418,7 @@ impl Context { } } KeyConfirm { next_retry_time, timeout, .. } => { - if let Some(ts) = process_timer(&next_retry_time, Application::RETRY_INTERVAL_MS, current_time) { + if let Some(ts) = process_timer(next_retry_time, Application::RETRY_INTERVAL_MS, current_time) { ts } else { if *timeout <= current_time { @@ -477,86 +477,89 @@ impl Context { application_data: Application::Data, local_identity_blob: Application::LocalIdentityBlob, current_time: i64, - ) -> Result>, OpenError> { + ) -> Result>, OpenError> { mtu = mtu.max(MIN_TRANSPORT_MTU); if local_identity_blob.as_ref().len() > MAX_IDENTITY_BLOB_SIZE { return Err(OpenError::DataTooLarge); } let result = app.restore_by_identity(&remote_static_key, &application_data, current_time); - if let Ok(ratchet_states) = result { - let sha512 = &mut Application::Hash::new(); + match result { + Ok(ratchet_states) => { + let sha512 = &mut Application::Hash::new(); - let mut noise_kk_ss = Secret::new(); - if !self.0.static_keypair.agree(&remote_static_key, noise_kk_ss.as_mut()) { - return Err(OpenError::InvalidPublicKey); - } - let noise_kk_local_init_h = mix_hash(sha512, &INITIAL_H_REKEY, self.0.static_keypair.public_key_bytes()); - let noise_kk_local_init_h = mix_hash(sha512, &noise_kk_local_init_h, remote_static_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &INITIAL_H_REKEY, remote_static_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &noise_kk_remote_init_h, self.0.static_keypair.public_key_bytes()); + let mut noise_kk_ss = Secret::new(); + if !self.0.static_keypair.agree(&remote_static_key, noise_kk_ss.as_mut()) { + return Err(OpenError::InvalidPublicKey); + } + let noise_kk_local_init_h = mix_hash(sha512, &INITIAL_H_REKEY, self.0.static_keypair.public_key_bytes()); + let noise_kk_local_init_h = mix_hash(sha512, &noise_kk_local_init_h, remote_static_key.as_bytes()); + let noise_kk_remote_init_h = mix_hash(sha512, &INITIAL_H_REKEY, remote_static_key.as_bytes()); + let noise_kk_remote_init_h = mix_hash(sha512, &noise_kk_remote_init_h, self.0.static_keypair.public_key_bytes()); - let mut session_queue = self.0.session_queue.lock().unwrap(); - let mut session_map = self.0.session_map.write().unwrap(); - let local_key_id = generate_key_id(&session_map, &mut self.0.rng.lock().unwrap()); - // Begin Noise XKhfs+psk2. - let (offer, a2b_header_key, b2a_header_key) = - NoiseXKAliceHandshake::::initialize(local_key_id, &remote_static_key, &ratchet_states, &mut self.0.rng.lock().unwrap())?; - let handshake_state = Box::new(NoiseXKAliceHandshake { - next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - timeout: current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS), - local_key_id, - alice_identity_blob: local_identity_blob, - offer, - }); - if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } = &handshake_state.offer { - send_with_fragmentation( - &mut send, - mtu, - &mut noise_message.clone()[..*noise_message_len], - PACKET_TYPE_NOISE_XK_PATTERN_1, - None, - *message_id, - None::<&Application::PrpEnc>, + let mut session_queue = self.0.session_queue.lock().unwrap(); + let mut session_map = self.0.session_map.write().unwrap(); + let local_key_id = generate_key_id(&session_map, &mut self.0.rng.lock().unwrap()); + // Begin Noise XKhfs+psk2. + let (offer, a2b_header_key, b2a_header_key) = + NoiseXKAliceHandshake::::initialize(local_key_id, &remote_static_key, &ratchet_states, &mut self.0.rng.lock().unwrap())?; + let handshake_state = Box::new(NoiseXKAliceHandshake { + next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), + timeout: current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS), + local_key_id, + alice_identity_blob: local_identity_blob, + offer, + }); + if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } = &handshake_state.offer { + send_with_fragmentation( + &mut send, + mtu, + &mut noise_message.clone()[..*noise_message_len], + PACKET_TYPE_NOISE_XK_PATTERN_1, + None, + *message_id, + None::<&Application::PrpEnc>, + ); + } + + let queue_idx = session_queue.reserve_index(); + let session = Arc::new(Session { + context: Arc::downgrade(&self.0), + queue_idx, + application_data, + remote_static_key, + send_counter: AtomicU64::new(INIT_COUNTER), + session_has_expired: AtomicBool::new(false), + counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), + state_machine_lock: Mutex::new(()), + state: RwLock::new(SessionMutableState { + ratchet_states: ratchet_states.clone(), + cipher_states: [None, None], + // Points at 1 until the first key is confirmed. + current_key: 1, + outgoing_offer: OfferStateMachine::NoiseXKPattern1or3(handshake_state), + }), + header_send_cipher: Application::PrpEnc::new(a2b_header_key.as_ref()), + header_receive_cipher: Application::PrpDec::new(b2a_header_key.as_ref()), + kex_receive_cipher: Mutex::new(None), + kex_send_cipher: Mutex::new(None), + noise_kk_ss: noise_kk_ss.clone(), + noise_kk_local_init_h, + noise_kk_remote_init_h, + defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), + was_bob: false, + }); + session_map.insert(local_key_id, (Arc::downgrade(&session), false)); + session_queue.push_reserved( + queue_idx, + Arc::downgrade(&session), + Reverse(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), ); + + Ok(session) + } + Err(e) => { + Err(OpenError::RatchetIoError(e)) } - - let queue_idx = session_queue.reserve_index(); - let session = Arc::new(Session { - context: Arc::downgrade(&self.0), - queue_idx, - application_data, - remote_static_key, - send_counter: AtomicU64::new(INIT_COUNTER), - session_has_expired: AtomicBool::new(false), - counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), - state_machine_lock: Mutex::new(()), - state: RwLock::new(SessionMutableState { - ratchet_states: ratchet_states.clone(), - cipher_states: [None, None], - // Points at 1 until the first key is confirmed. - current_key: 1, - outgoing_offer: OfferStateMachine::NoiseXKPattern1or3(handshake_state), - }), - header_send_cipher: Application::PrpEnc::new(a2b_header_key.as_ref()), - header_receive_cipher: Application::PrpDec::new(b2a_header_key.as_ref()), - kex_receive_cipher: Mutex::new(None), - kex_send_cipher: Mutex::new(None), - noise_kk_ss: noise_kk_ss.clone(), - noise_kk_local_init_h, - noise_kk_remote_init_h, - defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), - was_bob: false, - }); - session_map.insert(local_key_id, (Arc::downgrade(&session), false)); - session_queue.push_reserved( - queue_idx, - Arc::downgrade(&session), - Reverse(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - ); - - return Ok(session); - } else { - return Err(OpenError::RatchetIoError); } } @@ -602,7 +605,7 @@ impl Context { data_buf: &'a mut [u8], mut incoming_physical_packet_buf: Application::IncomingPacketBuffer, current_time: i64, - ) -> Result, ReceiveError> { + ) -> Result, ReceiveError> { send_unassociated_mtu = send_unassociated_mtu.max(MIN_TRANSPORT_MTU); let incoming_physical_packet: &mut [u8] = incoming_physical_packet_buf.as_mut(); let incoming_physical_packet_len = incoming_physical_packet.len(); @@ -629,7 +632,7 @@ impl Context { .try_into() .unwrap(), ); - let (fragment_count, fragment_no, packet_type, incoming_counter, header_nonce) = parse_packet_header(&incoming_physical_packet); + let (fragment_count, fragment_no, packet_type, incoming_counter, header_nonce) = parse_packet_header(incoming_physical_packet); // Handle replay protection. if PACKET_TYPE_RANGE_TRANSPORT.contains(&packet_type) { // For DOS resistant reply-protection we need to check that the given counter is @@ -777,7 +780,7 @@ impl Context { .try_into() .unwrap(), ); - let (fragment_count, fragment_no, packet_type, _, header_nonce) = parse_packet_header(&incoming_physical_packet); + let (fragment_count, fragment_no, packet_type, _, header_nonce) = parse_packet_header(incoming_physical_packet); app.event_log( LogEvent::ReceiveUnassociatedFragment(fragment_count, fragment_no, packet_type), current_time, @@ -816,7 +819,7 @@ impl Context { } } } else { - let (fragment_count, fragment_no, packet_type, _, header_nonce) = parse_packet_header(&incoming_physical_packet); + let (fragment_count, fragment_no, packet_type, _, header_nonce) = parse_packet_header(incoming_physical_packet); app.event_log( LogEvent::ReceiveUnassociatedFragment(fragment_count, fragment_no, packet_type), current_time, @@ -848,7 +851,7 @@ impl Context { } }; - debug_assert!(fragments.len() >= 1); + debug_assert!(!fragments.is_empty()); debug_assert!(incoming.is_none() || session.is_none()); let message = &mut [0u8; MAX_NOISE_HANDSHAKE_SIZE]; @@ -867,7 +870,7 @@ impl Context { if session.is_some() || incoming.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); } - if message_size < NoiseXKPattern1::MIN_SIZE || message_size > NoiseXKPattern1::MAX_SIZE { + if (NoiseXKPattern1::MIN_SIZE..=NoiseXKPattern1::MAX_SIZE).contains(&message_size) { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } // The message id must be the first 8 bytes of the gcm tag. @@ -904,7 +907,7 @@ impl Context { hasher.0.finish(&mut output); let is_valid = self.check_challenge_window(counter) && secure_eq(&output[..CHALLENGE_MAC_SIZE], &response.challenge_mac) - && verify_pow::(&mut hasher.0, &message[p_auth_end..message_size]) + && verify_pow::(hasher.0, &message[p_auth_end..message_size]) && self.update_challenge_window(counter); app.event_log(LogEvent::ReceiveCheckXK1Challenge(is_valid), current_time); if !is_valid { @@ -1008,7 +1011,7 @@ impl Context { ratchet_state = rs; break; } - Err(()) => return Err(ReceiveError::RatchetIoError), + Err(e) => return Err(ReceiveError::RatchetIoError(e)), } } if ratchet_state.is_null() { @@ -1023,7 +1026,7 @@ impl Context { let noise_pattern2: &mut NoiseXKPattern2 = byte_array_as_proto_buffer_mut(&mut message2); // Noise process pattern2 e token. let noise_e_pattern2_secret = Application::KeyPair::generate(&mut self.0.rng.lock().unwrap()); - noise_pattern2.noise_e = noise_e_pattern2_secret.public_key_bytes().clone(); + noise_pattern2.noise_e = *noise_e_pattern2_secret.public_key_bytes(); let noise_h_ee1pe = mix_hash(sha512, &noise_h_ee1p, &noise_pattern2.noise_e); noise_ck.mix_key(hmac, &noise_pattern2.noise_e); // Noise process pattern2 ee token. @@ -1220,7 +1223,7 @@ impl Context { &mut message[NoiseXKPattern2::EKEM1_ENC_START..NoiseXKPattern2::P_ENC_START], ); let noise_pattern2: &NoiseXKPattern2 = byte_array_as_proto_buffer(message); - let noise_ekem1_secret = pqc_kyber::decapsulate(&noise_pattern2.noise_ekem1, noise_e1_secret.as_ref()).map(|k| Secret(k)); + let noise_ekem1_secret = pqc_kyber::decapsulate(&noise_pattern2.noise_ekem1, noise_e1_secret.as_ref()).map(Secret); if let Some(Ok(noise_ekem1_secret)) = is_auth.then_some(noise_ekem1_secret) { noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); drop(noise_ekem1_secret); @@ -1336,8 +1339,8 @@ impl Context { [&new_ratchet_state, ratchet_to_preserve], current_time, ); - if result.is_err() { - return Err(ReceiveError::RatchetIoError); + if let Err(e) = result { + return Err(ReceiveError::RatchetIoError(e)); } let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_ee1peekem1pskpsp); @@ -1511,7 +1514,8 @@ impl Context { ); if let Some((responder_disallows_downgrade, application_data)) = responder_disallows_downgrade { let result = app.restore_by_identity(&remote_s_public_key, &application_data, current_time); - if let Ok(true_ratchet_states) = result { + match result { + Ok(true_ratchet_states) => { let mut has_match = false; for rs in &true_ratchet_states { if !rs.is_null() { @@ -1549,10 +1553,11 @@ impl Context { [&new_ratchet_state, &RatchetState::Null], current_time, ); - if result.is_err() { - return Err(ReceiveError::RatchetIoError); + if let Err(e) = result { + return Err(ReceiveError::RatchetIoError(e)); } + let mut session_queue = self.0.session_queue.lock().unwrap(); let queue_idx = session_queue.reserve_index(); let session = Arc::new(Session { @@ -1600,8 +1605,8 @@ impl Context { // in use, in which case we have to drop this session like // nothing ever happened. let mut session_map = self.0.session_map.write().unwrap(); - if !session_map.contains_key(&handshake_state.local_key_id) { - session_map.insert(handshake_state.local_key_id, (Arc::downgrade(&session), false)); + if let std::collections::hash_map::Entry::Vacant(e) = session_map.entry(handshake_state.local_key_id) { + e.insert((Arc::downgrade(&session), false)); drop(session_map); let _ = session.send_control(&session.state.read().unwrap(), send_unassociated_reply, PACKET_TYPE_KEY_CONFIRM, &[]); @@ -1614,9 +1619,11 @@ impl Context { // restart the handshake in this case. return Err(byzantine_fault!(FaultType::UnknownLocalKeyId, true)); } - } else { - return Err(ReceiveError::RatchetIoError); } + Err(e) => { + return Err(ReceiveError::RatchetIoError(e)); + } + } } else { if !responder_silently_rejects { send_reject(); @@ -1715,7 +1722,7 @@ impl Context { } } } - return Ok(()); + Ok(()) } /// Update the challenge window, returning true if the challenge is still valid. #[inline(always)] @@ -1807,7 +1814,7 @@ fn initiate_rekey( drop(state); drop(kex_lock); let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_NOISE_KK_PATTERN_1, &message); - return Ok(current_time.saturating_add(Application::RETRY_INTERVAL_MS)); + Ok(current_time.saturating_add(Application::RETRY_INTERVAL_MS)) } fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mut [u8]) -> bool>( context: &Context, @@ -1818,7 +1825,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu counter: u64, fragment: &mut [u8], current_time: i64, -) -> Result, ReceiveError> { +) -> Result, ReceiveError> { let state = session.state.read().unwrap(); let mut c = session.kex_receive_cipher.lock().unwrap(); let message = decrypt_control( @@ -1857,7 +1864,7 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu _ => (true, false, SessionEvent::Control), }; if try_delete { - let is_ok = if !state.ratchet_states[1].is_null() { + let result = if !state.ratchet_states[1].is_null() { app.save_ratchet_state( &session.remote_static_key, &session.application_data, @@ -1865,24 +1872,22 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu [&state.ratchet_states[0], &RatchetState::Null], current_time, ) - .is_ok() } else { - true + Ok(()) }; - if is_ok { - if let NoiseKKPattern2 { kex_send_key, .. } = &state.outgoing_offer { - session - .kex_send_cipher - .lock() - .unwrap() - .replace(Application::AeadEnc::new(kex_send_key.as_ref())); - } - state.ratchet_states[1] = RatchetState::Null; - state.current_key ^= 1; - state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); - } else { - return Err(ReceiveError::RatchetIoError); + if let Err(e) = result { + return Err(ReceiveError::RatchetIoError(e)); } + if let NoiseKKPattern2 { kex_send_key, .. } = &state.outgoing_offer { + session + .kex_send_cipher + .lock() + .unwrap() + .replace(Application::AeadEnc::new(kex_send_key.as_ref())); + } + state.ratchet_states[1] = RatchetState::Null; + state.current_key ^= 1; + state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); } drop(state); drop(kex_lock); @@ -1899,11 +1904,8 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu let kex_lock = session.state_machine_lock.lock().unwrap(); let mut state = session.state.write().unwrap(); // Check if we should end any current offers and transition back to Normal state - match &state.outgoing_offer { - KeyConfirm { .. } => { - state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); - } - _ => (), + if let KeyConfirm { .. } = &state.outgoing_offer { + state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); } drop(state); drop(kex_lock); @@ -2007,10 +2009,10 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu [&new_ratchet_state, &state.ratchet_states[0]], current_time, ); - if result.is_err() { + if let Err(e) = result { drop(state); drop(kex_lock); - return Err(ReceiveError::RatchetIoError); + return Err(ReceiveError::RatchetIoError(e)); } let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_pskepep); // The new "Bob" doesn't know yet if Alice has received the new key, so the @@ -2110,10 +2112,10 @@ fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mu [&new_ratchet_state, &RatchetState::Null], current_time, ); - if result.is_err() { + if let Err(e) = result { drop(state); drop(kex_lock); - return Err(ReceiveError::RatchetIoError); + return Err(ReceiveError::RatchetIoError(e)); } let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_pskepep); @@ -2199,16 +2201,13 @@ impl Session { packet, ); send(&mut fragment[..len]); - return Ok(()); + Ok(()) } /// Check whether this session is established. #[inline] pub fn established(&self) -> bool { let state = self.state.read().unwrap(); - return match &state.outgoing_offer { - OfferStateMachine::NoiseXKPattern1or3(_) => false, - _ => true, - }; + !matches!(&state.outgoing_offer, OfferStateMachine::NoiseXKPattern1or3(_)) } /// The static public key of the remote peer. #[inline] @@ -2317,7 +2316,7 @@ impl NoiseXKAliceHandshake { Secret, Secret, ), - OpenError, + OpenError, > { let mut message = [0u8; NoiseXKPattern1::MAX_SIZE]; let sha512 = &mut Application::Hash::new(); @@ -2327,7 +2326,7 @@ impl NoiseXKAliceHandshake { let noise_e_secret = Application::KeyPair::generate(rng); let noise_e1_secret = pqc_kyber::keypair(rng); noise_pattern1.alice_key_id = local_key_id.get().to_ne_bytes(); - noise_pattern1.noise_e = noise_e_secret.public_key_bytes().clone(); + noise_pattern1.noise_e = *noise_e_secret.public_key_bytes(); noise_pattern1.noise_e1 = noise_e1_secret.public; // Noise process prologue. let noise_h = mix_hash( @@ -2494,7 +2493,7 @@ fn encrypt_control( let fragment_len = packet.len() + HEADER_SIZE + AES_GCM_TAG_SIZE; c.set_iv(&create_message_nonce(packet_type, counter)); - if packet.len() > 0 { + if !packet.is_empty(){ fragment[HEADER_SIZE..fragment_len - AES_GCM_TAG_SIZE].copy_from_slice(packet); c.encrypt_in_place(&mut fragment[HEADER_SIZE..fragment_len - AES_GCM_TAG_SIZE]); } @@ -2504,9 +2503,9 @@ fn encrypt_control( (fragment, fragment_len) } #[inline] -fn decrypt_control<'a>(c: &mut impl AesGcmDec, packet_type: u8, counter: u64, fragment: &'a mut [u8]) -> Result<&'a mut [u8], ReceiveError> { +fn decrypt_control<'a, IoError>(c: &mut impl AesGcmDec, packet_type: u8, counter: u64, fragment: &'a mut [u8]) -> Result<&'a mut [u8], ReceiveError> { let fragment_len = fragment.len(); - if fragment_len < CONTROL_PACKET_MIN_SIZE || fragment_len > CONTROL_PACKET_MAX_SIZE { + if (CONTROL_PACKET_MIN_SIZE..=CONTROL_PACKET_MAX_SIZE).contains(&fragment_len) { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } c.set_iv(&create_message_nonce(packet_type, counter)); @@ -2610,7 +2609,7 @@ fn send_with_fragmentation( break; } } - return true; + true } /// Assemble a series of fragments into a buffer and return the length of the assembled packet in @@ -2618,7 +2617,7 @@ fn send_with_fragmentation( /// /// This is also only used for key exchange and control packets. For data packets decryption and /// assembly happen in one pass for better performance. -fn assemble_fragments_into(fragments: &[A::IncomingPacketBuffer], d: &mut [u8]) -> Result { +fn assemble_fragments_into(fragments: &[A::IncomingPacketBuffer], d: &mut [u8]) -> Result> { let mut l = 0; for i in 0..fragments.len() { let mut ff = fragments[i].as_ref(); @@ -2632,7 +2631,7 @@ fn assemble_fragments_into(fragments: &[A::IncomingPacketBu d[l..j].copy_from_slice(ff); l = j; } - return Ok(l); + Ok(l) } /// Generate a random local key id that is currently unused. fn generate_key_id( From b4fab13d9a4db041ddbbe1ec004d1449902ebcff Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 14:19:08 -0400 Subject: [PATCH 20/25] fixed typo --- src/zssp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zssp.rs b/src/zssp.rs index 0bc0763..d6f1b44 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -870,7 +870,7 @@ impl Context { if session.is_some() || incoming.is_some() { return Err(byzantine_fault!(FaultType::OutOfSequence, false)); } - if (NoiseXKPattern1::MIN_SIZE..=NoiseXKPattern1::MAX_SIZE).contains(&message_size) { + if !(NoiseXKPattern1::MIN_SIZE..=NoiseXKPattern1::MAX_SIZE).contains(&message_size) { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } // The message id must be the first 8 bytes of the gcm tag. @@ -2505,7 +2505,7 @@ fn encrypt_control( #[inline] fn decrypt_control<'a, IoError>(c: &mut impl AesGcmDec, packet_type: u8, counter: u64, fragment: &'a mut [u8]) -> Result<&'a mut [u8], ReceiveError> { let fragment_len = fragment.len(); - if (CONTROL_PACKET_MIN_SIZE..=CONTROL_PACKET_MAX_SIZE).contains(&fragment_len) { + if !(CONTROL_PACKET_MIN_SIZE..=CONTROL_PACKET_MAX_SIZE).contains(&fragment_len) { return Err(byzantine_fault!(FaultType::InvalidPacket, false)); } c.set_iv(&create_message_nonce(packet_type, counter)); From c3c66ef49bfc477411c809ea7edfd286de79f9c2 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 14:54:26 -0400 Subject: [PATCH 21/25] updated API --- src/applicationlayer.rs | 47 ++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/src/applicationlayer.rs b/src/applicationlayer.rs index 4d77d9f..0485f7f 100644 --- a/src/applicationlayer.rs +++ b/src/applicationlayer.rs @@ -125,10 +125,7 @@ pub trait ApplicationLayer: Sized { /// If this function is configured to always return true, it means peers will not be able to /// connect to us unless they had a prior-established ratchet key with us. This is the best way /// for the paranoid to enforce a manual allow-list. - #[allow(unused)] - fn hello_requires_recognized_ratchet(&self, current_time: i64) -> bool { - false - } + fn hello_requires_recognized_ratchet(&self, current_time: i64) -> bool; /// This function is called if we, as Alice, attempted to open a session with Bob using a /// non-empty ratchet key, but Bob does not have this ratchet key and wants to downgrade /// to the zero ratchet key. @@ -144,14 +141,8 @@ pub trait ApplicationLayer: Sized { /// least one party is misconfigured and got their ratchet keys corrupted or lost, or Bob has /// been compromised and is being impersonated. An attacker must at least have Bob's private /// static key to be able to ask Alice to downgrade. - /// - /// If Alice does decide to reconnect without a ratchet key, be sure to generate some warning - /// that something has gone wrong and Bob could not be fully authenticated. - #[allow(unused)] - fn initiator_disallows_downgrade(&self, session: &Arc>, current_time: i64) -> bool { - false - } - /// Lookup a specific ratchet key based on its ratchet fingerprint. + fn initiator_disallows_downgrade(&self, session: &Arc>, current_time: i64) -> bool; + /// Lookup a specific ratchet state based on its ratchet fingerprint. /// This function will be called whenever Alice attempts to connect to us with a non-empty /// ratchet fingerprint. /// @@ -163,23 +154,19 @@ pub trait ApplicationLayer: Sized { /// If `RatchetAction::DowngradeRatchet` is returned we will attempt to convince Alice to downgrade /// to the empty ratchet key, restarting the ratchet chain. /// If `RatchetAction::FailAuthentication` is returned Alice's connection will be silently dropped. - #[allow(unused)] - fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result { - Ok(RatchetState::Null) - } - #[allow(unused)] + fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result; + + /// Lookup a specific ratchet state based on the identity of the peer being communicated with. + /// This function will be called whenever Alice attempts to open a session, or Bob attempts + /// to verify Alice's identity. fn restore_by_identity( &self, remote_static_key: &Self::PublicKey, application_data: &Self::Data, current_time: i64, - ) -> Result<[RatchetState; 2], Self::IoError> { - Ok([RatchetState::Null, RatchetState::Null]) - } - /// Atomically save the given ratchet key, fingerprint and number to persistent storage. - /// - /// See the documentation of `SaveAction` for more details on how to save them to storage, - /// and how to handle any pre-existing ratchet keys, fingerprints and numbers. + ) -> Result<[RatchetState; 2], Self::IoError>; + /// Atomically save the given `new_ratchet_states` to persistent storage. + /// `pre_ratchet_states` contains what should be the previous contents of persistent storage. /// /// If this returns `Err(IoError)`, the packet which triggered this function to be called will be /// dropped, and no session state will be mutated, preserving synchronization. The remote peer @@ -191,20 +178,18 @@ pub trait ApplicationLayer: Sized { /// fix is to reset both ratchet keys to empty. /// /// This function may also save state to volatile storage, in which case all peers which connect - /// to us will have to allow downgrade - /// (`initiator_disallows_downgrade` returns false and/or`restore_ratchet` returns `DowngradeRatchet`). + /// to us will have to allow downgrade, i.e. `initiator_disallows_downgrade` returns false + /// and/or `check_accept_session` returns `(Some(true, _), _)`. /// Otherwise, when we restart, we will not be allowed to reconnect. - #[allow(unused)] fn save_ratchet_state( &self, remote_static_key: &Self::PublicKey, application_data: &Self::Data, - previous_ratchet_states: [&RatchetState; 2], + pre_ratchet_states: [&RatchetState; 2], new_ratchet_states: [&RatchetState; 2], current_time: i64, - ) -> Result<(), Self::IoError> { - Ok(()) - } + ) -> Result<(), Self::IoError>; + #[allow(unused)] #[inline] fn event_log(&self, event: LogEvent, current_time: i64) {} From b3d6330dd01bd342a26e3fd60aad1d4a73bc5a4c Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 15:12:54 -0400 Subject: [PATCH 22/25] fixed typo --- src/zssp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zssp.rs b/src/zssp.rs index d6f1b44..aa0a77c 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -1274,7 +1274,7 @@ impl Context { if result.is_none() { ratchet_i = 1; if let Some(key) = state.ratchet_states[1].key() { - chain_len = state.ratchet_states[0].chain_len(); + chain_len = state.ratchet_states[1].chain_len(); result = test_ratchet_key(key); } } From 62a8ff4e4c023a5c8e124c2c3886b53d63c80b8e Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 15:24:29 -0400 Subject: [PATCH 23/25] updated docs --- src/zssp.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/zssp.rs b/src/zssp.rs index aa0a77c..897391b 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -69,7 +69,8 @@ pub enum ReceiveResult<'b, Application: ApplicationLayer> { Session(Arc>, SessionEvent<'b>), /// Packet was a part of a handshake, and while it superficially appeared valid the application /// explicitly rejected it. - /// Relates to callbacks `check_allow_incoming_session` and `check_accept_session`. + /// Relates to callbacks `check_allow_incoming_session`, `hello_requires_recognized_ratchet` + /// and `check_accept_session`. Rejected, } From 5116fcbb78398a2ec1fe374884ae6f71854343e0 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 15:31:50 -0400 Subject: [PATCH 24/25] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6164906..dc61d4b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ZeroTier Secure Socket Protocol +ZeroTier Secure Sessions Protocol ====== # Introduction From d085e2ab5fb4b42701c0c2494c662b1d0ce378f6 Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Fri, 14 Jul 2023 15:46:05 -0400 Subject: [PATCH 25/25] updated readme --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dc61d4b..39d3988 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ ZeroTier Secure Sessions Protocol ====== -# Introduction +## Introduction ZeroTier Secure Socket Protocol (ZSSP) is a [Noise](http://noiseprotocol.org) protocol implementation using NIST/FIPS/CfSC compliant cryptographic primitives plus post-quantum forward secrecy via [Kyber1024](https://pq-crystals.org/kyber/). It also includes built-in support for fragmentation and defragmentation of large messages with strong resistance against denial of service attacks targeted against the fragmentation protocol. -Specifically ZSSP implements the [Noise XK](http://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental) interactive handshake pattern which provides strong forward secrecy not only for data but for the identities of the two participants in the sesssion. The XK pattern was chosen instead of the more popular IK pattern used in popular Noise implementations like Wireguard due to ZeroTier identities being long lived and potentially tied to the real world identity of the user. As a result a Noise pattern providing identity forward secrecy was considered preferable as it offers some level of deniability for recorded traffic even after secrec key compromise. +Specifically ZSSP implements the [Noise XK](http://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental) interactive handshake pattern which provides strong forward secrecy not only for data but for the identities of the two participants in the session. The XK pattern was chosen instead of the more popular IK pattern used in popular Noise implementations like Wireguard due to ZeroTier identities being long lived and potentially tied to the real world identity of the user. As a result a Noise pattern providing identity forward secrecy was considered preferable as it offers some level of deniability for recorded traffic even after secret key compromise. Hybrid post-quantum forward secrecy using Kyber1024 is performed alongside Noise with the result being mixed in alongside an optional pre-shared key at the end of session negotiation. @@ -15,9 +15,46 @@ Further information can be found in the ZSSP whitepaper (pending official releas ## Cryptographic Primitives Used - - AES-256-GCM: Authenticated encryption - - SHA512: Used with the KBKDF construction, also used in a proof of work and IP ownership DOS mitigation scheme - - KBKDF: Key mixing, sub-key derivation - - NIST P-384 ECDH: Elliptic curve key exchange during initial handshake and for periodic re-keying during the session - - Kyber1024: Quantum attack resistant lattice-based key exchange during initial handshake - - AES-256: 128-bit PRP for AES-256-GCM and for authenticated encryption of headera to harden fragmentation against DOS (see section on header protection) + - **NIST P-384 ECDH**: Elliptic curve key exchange during initial handshake and for periodic re-keying during the session + - **Kyber1024**: Quantum attack resistant lattice-based key exchange during initial handshake + - **SHA-512**: Used to construct KBKDF, also used in a proof of work and IP ownership DOS mitigation scheme + - **KBKDF**: Key mixing, sub-key derivation + - **AES-256**: 128-bit PRP for AES-256-GCM and for authenticated encryption of header to harden fragmentation against DOS (see section on header protection) + - **AES-256-GCM**: Authenticated encryption + +## Security Properties + +| | Persistent ZSSP | Opportunistic ZSSP| WireGuard | ZeroTier Legacy Transport | +| --- | --- | --- | --- | --- | +|**Construction**|Noise\_XKhfs+psk2|Noise\_XKhfs+psk2|Noise\_IKpsk2|Static Diffie-Helman| +|**Perfect Forward Secrecy**|Yes|Yes|Yes|No| +|**Forward Secret Identity Hiding**|Yes|Yes|No|No| +|**Quantum Forward Secret**|Yes|Yes|No|No| +|**Ratcheted Forward Secrecy**|Yes|Yes|No|No| +|**Silence is a Virtue**|Yes|No|Yes|No| +|**Key-Compromise Impersonation**|Resistant|Resistant|Resistant|Vulnerable| +|**Compromise-and-Impersonate**|Resistant|Detectable|Vulnerable|Vulnerable| +|**Single Key-Compromise MitM**|Resistant|Resistant|Resistant|Vulnerable| +|**Double Key-Compromise MitM**|Resistant|Detectable|Vulnerable|Vulnerable| +|**DOS Mitigation**|Yes|Yes|Yes|No| +|**Supports Fragmentation**|Yes|Yes|No|Yes| +|**FIPS Compliant**|Yes|Yes|No|No| +|**Small Code Footprint**|Yes|Yes|Yes|No| +|**RTT**|2|2|1|1| + +### Definitions + +* **Construction**: The mathematical construction the protocol is based upon. +* **Perfect Forward Secrecy**: An attacker with the static private keys of both party cannot decrypt recordings of messages sent between those parties. +* **Forward Secret Identity Hiding**: An attacker with the static private key of one or more parties cannot determine the identity of everyone they have previously communicated with. +* **Quantum Forward Secret**: A quantum computer powerful enough to break Elliptic-curve cryptography is not sufficient in order to decrypt recordings of messages sent between parties. +* **Ratcheted Forward Secrecy**: In order to break forward secrecy an attacker must record and break every single key exchange two parties perform, in order, starting from the first time they began communicating. Improves secrecy under weak or compromised RNG. +* **Key-Compromise Impersonation**: The attacker has a memory image of a single party, and attempts to create a brand new session with that party, pretending to be someone else. +* **Compromise-and-Impersonate**: The attacker has a memory image of a single party, and attempts to impersonate them on a brand new session with the other party. +* **Single Key-Compromise MitM**: The attacker has a memory image of a single party, and attempts to become a Man-in-the-Middle between them and any other party. +* **Double Key-Compromise MitM**: The attacker has a memory image of both parties, and attempts to become a Man-in-the-Middle between them. +* **Silence is a Virtue**: A server running the protocol can be configured in such a way that it will not respond to an unauthenticated, anonymous or replayed message. +* **Supports Fragmentation**: Transmission data can be fragmented into smaller units to support jumbo-sized data or MTU discovery. +* **FIPS Compliant**: The protocol uses FIPS approved cryptographic algorithms. +* **Small Code Footprint**: The Codebase implementing the protocol can be easily audited by anyone on the internet. +* **RTT**: "Round-Trip-Time" - How many round trips from initiator to responder it takes to establish a session.