diff --git a/.drone.yml b/.drone.yml index df2348c..4b0b898 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,7 +16,7 @@ steps: from_secret: codecov_token commands: - cargo build - - CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage/cargo-test-%p-%m.profraw' cargo test + - CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage/cargo-test-%p-%m.profraw' cargo test --all-targets - mkdir -p target/coverage - grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov - codecov -B ${DRONE_BRANCH} -b ${DRONE_BUILD_NUMBER} -f target/coverage/tests.lcov -F amd64 @@ -42,7 +42,7 @@ steps: from_secret: codecov_token commands: - cargo build - - CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage/cargo-test-%p-%m.profraw' cargo test + - CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage/cargo-test-%p-%m.profraw' cargo test --all-targets - mkdir -p target/coverage - grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov - codecov -B ${DRONE_BRANCH} -b ${DRONE_BUILD_NUMBER} -f target/coverage/tests.lcov -F arm64 diff --git a/performance/Cargo.toml b/performance/Cargo.toml index 2c25885..8fbc4bf 100644 --- a/performance/Cargo.toml +++ b/performance/Cargo.toml @@ -3,7 +3,7 @@ authors = ["ZeroTier, Inc. ", "Adam Ierymenko for &TestApplication { &mut self, remote_static_key: &CrateP384PublicKey, session_data: &u128, - update_data: RatchetUpdate<'_>, - ) -> Result<(), std::io::Error> { + update_data: CompareAndSwap<'_>, + ) -> Result { let mut ratchets = self.ratchets.lock().unwrap(); - ratchets.peer_map.insert(*session_data, update_data.to_states()); + match ratchets.peer_map.entry(*session_data) { + Entry::Occupied(mut entry) => { + if update_data.compare(&entry.get()) { + entry.insert(update_data.to_new_states()); + } else { + return Ok(false); + } + } + Entry::Vacant(entry) => { + if update_data.cur_is_initial_states() { + entry.insert(update_data.to_new_states()); + } else { + return Ok(false); + } + } + } if let Some(rf) = update_data.added_fingerprint() { - ratchets.rf_map.insert(*rf, update_data.state1.clone()); - println!("[{}] new ratchet #{}", self.name, update_data.state1.chain_len()); + ratchets.rf_map.insert(*rf, update_data.new_state1.clone()); + println!("[{}] new ratchet #{}", self.name, update_data.new_state1.chain_len()); } if let Some(rf) = update_data.deleted_fingerprint1() { ratchets.rf_map.remove(rf); @@ -129,7 +145,7 @@ impl ApplicationLayer for &TestApplication { if let Some(rf) = update_data.deleted_fingerprint2() { ratchets.rf_map.remove(rf); } - Ok(()) + Ok(true) } fn time(&mut self) -> i64 { @@ -211,8 +227,6 @@ fn alice_main( } } } - //} else if OsRng.next_u32() | 1 > 0 { - // let _ = recursive_out.send(pkt); } } else { break; @@ -310,8 +324,6 @@ fn bob_main( } } } - //} else if OsRng.next_u32() | 1 > 0 { - // let _ = recursive_out.try_send(pkt); } } @@ -403,6 +415,16 @@ fn main() { } #[test] -fn test_main() { - core(2, u32::MAX / 2) +fn test_50() { + core(10, u32::MAX / 2) +} + +#[test] +fn test_75() { + core(10, u32::MAX / 4 * 3) +} + +#[test] +fn test_99() { + core(10, u32::MAX / 100 * 99) } diff --git a/performance/examples/benchmark.rs b/performance/examples/benchmark.rs index 02c060f..220c46d 100644 --- a/performance/examples/benchmark.rs +++ b/performance/examples/benchmark.rs @@ -8,7 +8,7 @@ use rand_core::OsRng; use rand_core::RngCore; use zssp::application::{ - AcceptAction, ApplicationLayer, IncomingSessionAction, RatchetState, RatchetStates, RatchetUpdate, RATCHET_SIZE, + AcceptAction, ApplicationLayer, CompareAndSwap, IncomingSessionAction, RatchetState, RatchetStates, RATCHET_SIZE, }; use zssp::crypto::P384KeyPair; use zssp::crypto_impl::*; @@ -53,7 +53,6 @@ impl AsRef<[u8]> for PooledVec { #[allow(unused)] impl DefaultCrypto for TestApplication { type SessionData = (); - type LookupData = (); type IncomingPacketBuffer = PooledVec; } @@ -106,9 +105,9 @@ impl ApplicationLayer for &TestApplication { &mut self, remote_static_key: &CrateP384PublicKey, session_data: &(), - update_data: RatchetUpdate<'_>, - ) -> Result<(), std::io::Error> { - Ok(()) + update_data: CompareAndSwap<'_>, + ) -> Result { + Ok(true) } fn time(&mut self) -> i64 { diff --git a/performance/src/application.rs b/performance/src/application.rs index 100e418..d568e97 100644 --- a/performance/src/application.rs +++ b/performance/src/application.rs @@ -277,8 +277,9 @@ pub trait ApplicationLayer: Sized { /// This function will be called whenever Alice attempts to open a session, or Bob attempts /// to verify Alice's identity. /// - /// If the peer's ratchet states could not be could, this function should return - /// `RatchetState::new_initial_states()`. + /// If the peer's ratchet states could not be found, this function should return `None`. + /// A return value of `None` is equivalent to a return value of + /// `Some(RatchetState::new_initial_states())`. /// /// If a one-time-password has been pre-shared with this peer, `RatchetState::new_otp_states(...)` /// should be pre-saved to the storage backend as if it is a normal ratchet state. @@ -295,11 +296,16 @@ pub trait ApplicationLayer: Sized { session_data: &C::SessionData, fingerprint_data: Option<&C::FingerprintData>, ) -> Result, std::io::Error>; - /// Atomically commit the update specified by `update_data` to storage, or return an error if - /// the update could not be made. - /// The implementor is free to choose how to apply these updates to storage. + /// Atomically compare-and-swap (a.k.a. compare-exchange) `update` to storage. /// - /// If this returns `Err(())`, the packet which triggered this function to be called will be + /// If `update.cur_state1` and `update.cur_state2` are currently in storage, they must + /// be swapped with `update.new_state1` and `update.new_state2`. + /// Otherwise, storage must remain unchanged. + /// + /// `Ok(true)` must be returned if the compare-and-swap was successful. + /// `Ok(false)` must be returned if comparison failed and the swap was cancelled. + /// + /// If this returns `Err`, 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. /// @@ -311,12 +317,20 @@ pub trait ApplicationLayer: Sized { /// This function may also save state to volatile storage, in which case all peers which connect /// to us will have to allow downgrade across the board. /// Otherwise, when we restart, we will not be allowed to reconnect. + /// + /// # Security + /// Implementations must not perform comparison operations (equals, less than, etc.) directly + /// on two ratchet keys. Instead, comparison operations must be performed indirectly + /// upon their ratchet fingerprints. If two ratchet states have the same ratchet fingerprint, + /// it should be assumed that they also have the same ratchet key. + /// + /// The implementations of `PartialEq` for `RatchetState` and `RatchetStates` do this by default. fn save_ratchet_state( &mut self, remote_static_key: &C::PublicKey, session_data: &C::SessionData, - update_data: RatchetUpdate<'_>, - ) -> Result<(), std::io::Error>; + update: CompareAndSwap<'_>, + ) -> Result; /// Receives a stream of events that occur during an execution of ZSSP. /// These are provided for debugging, logging or metrics purposes, and must be used for diff --git a/performance/src/crypto/aes.rs b/performance/src/crypto/aes.rs index 6b0dec4..7fc91fe 100644 --- a/performance/src/crypto/aes.rs +++ b/performance/src/crypto/aes.rs @@ -47,29 +47,28 @@ pub trait Aes256Dec: Sized + Send + Sync { fn decrypt_in_place(&self, block: &mut [u8; AES_256_BLOCK_SIZE]); } -pub trait AesGcmEncContext { - fn encrypt(&mut self, input: &[u8], output: &mut [u8]); - - fn finish(self) -> [u8; AES_GCM_TAG_SIZE]; -} - -pub trait AesGcmDecContext { - fn decrypt_in_place(&mut self, data: &mut [u8]); - - #[must_use] - fn finish(self, tag: &[u8; AES_GCM_TAG_SIZE]) -> bool; -} - /// A trait for implementing AES-GCM-256 in a way that allows for extremely high throughput. /// One instance of this trait is created whenever a new pair of noise keys are created, -/// and it handles all data encryption that passes through that session. +/// and it handles all data encryption and decryption that passes through that session. +/// +/// It is highly recommended to implement this trait such that encryption and decryption +/// are hardware accelerated and parallelized. +/// ZSSP's throughput is near 90% determined by this trait. /// /// Instances must securely delete their keys when dropped. pub trait HighThroughputAesGcmPool: Send + Sync { - type EncContext<'a>: AesGcmEncContext + /// This type represents the state needed to stream a single plaintext for + /// AES-GCM Authenticated Encryption. + /// It should be set to whatever object, struct, handle or pointer your chosen library + /// uses for its stream encryption API. + type EncContext<'a> where Self: 'a; - type DecContext<'a>: AesGcmDecContext + /// This type represents the state needed to stream a single ciphertext for + /// AES-GCM Authenticated Decryption. + /// It should be set to whatever object, struct, handle or pointer your chosen library + /// uses for its stream decryption API. + type DecContext<'a> where Self: 'a; @@ -86,17 +85,65 @@ pub trait HighThroughputAesGcmPool: Send + Sync { /// `nonce` must be set as the AEAD nonce. /// There is no additional associated data to be used. fn start_dec<'a>(&'a self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> Self::DecContext<'a>; + + /// Stream-encrypt `input` using the specified encryption context `enc`, and write the + /// resulting ciphertext to `output`. + /// + /// `input` and `output` are guaranteed to have the same length. + /// + /// Be sure to update the internal state of `enc` so the authentication tag can be correctly + /// computed. + fn encrypt<'a>(&'a self, enc: &mut Self::EncContext<'a>, input: &[u8], output: &mut [u8]); + + /// Stream-decrypt `data` using the specified encryption context `dec`. + /// The resulting plaintext should be written back into `data`. + /// + /// Be sure to update the internal state of `dec` so the authentication tag can be correctly + /// computed. + fn decrypt_in_place<'a>(&'a self, dec: &mut Self::DecContext<'a>, data: &mut [u8]); + + /// Finish streaming to `enc` and output the resulting authentication tag. + /// + /// Perform any pooling or cleanup required on `enc`. + /// For many libraries it is much faster to return `enc` to some pool to be reused + /// by `start_enc`, rather than dropping it. + /// Be sure to perform your own benchmark. + fn finish_enc<'a>(&'a self, enc: Self::EncContext<'a>) -> [u8; AES_GCM_TAG_SIZE]; + /// Finish streaming to `dec` and check that the expected authentication tag matches `tag`. + /// Make sure that comparing `tag` to the expected authentication tag is performed + /// in constant-time. Many libraries provide functions which will do this for you. + /// Output `true` only if `tag` is correct. + /// + /// Afterwards, perform any pooling or cleanup required on `dec`. + /// For many libraries it is much faster to return `dec` to some pool to be reused + /// by `start_dec`, rather than dropping it. + /// Be sure to perform your own benchmark. + #[must_use] + fn finish_dec<'a>(&'a self, dec: Self::DecContext<'a>, tag: &[u8; AES_GCM_TAG_SIZE]) -> bool; } /// A trait for implementing AES-GCM-256 to handle the more varied, but much lower throughput /// requirements of a Noise handshake. pub trait LowThroughputAesGcm { + /// A pure function (no side effects) that implements AESGCM AEAD enryption. + /// + /// Encryption must be performed on `data` in-place. + /// The initial plaintext of `data` must be overwriten with its ciphertext. + /// + /// The resulting GCM authentication tag must be returned. fn encrypt_in_place( key: &[u8; AES_256_KEY_SIZE], nonce: &[u8; AES_GCM_NONCE_SIZE], aad: &[u8], data: &mut [u8], ) -> [u8; AES_GCM_TAG_SIZE]; + /// A pure function (no side effects) that implements AESGCM AEAD decryption. + /// + /// Encryption must be performed on `data` in-place. + /// The initial ciphertext of `data` must be overwriten with its plaintext. + /// + /// This function must check that the expected authentication tag matches `tag`, + /// and only return `true` if they match. This must be done in constant-time. #[must_use] fn decrypt_in_place( key: &[u8; AES_256_KEY_SIZE], diff --git a/performance/src/crypto_impl/mod.rs b/performance/src/crypto_impl/mod.rs index acf86e2..5402aef 100644 --- a/performance/src/crypto_impl/mod.rs +++ b/performance/src/crypto_impl/mod.rs @@ -35,10 +35,22 @@ pub use openssl::*; #[cfg(feature = "openssl-sys")] pub use openssl_sys; +/// Any type which implements this trait will also auto-implement the `CryptoLayer` trait, +/// using the default set of cryptography implementations. +/// +/// If you want to use this library, and don't care which specific implementations it uses, +/// then implement this trait instead of the `CryptoLayer` trait. #[cfg(feature = "default-crypto")] pub trait DefaultCrypto { + /// Type for arbitrary opaque object for use by the application that is attached to + /// each session. type SessionData; - type LookupData; + /// Data type for incoming packet buffers. + /// + /// This can be something like `Vec` or `Box<[u8]>` or it can be something like a pooled + /// reusable buffer that automatically returns to its pool when ZSSP is done with it. ZSSP may + /// hold these for a short period of time when assembling fragmented packets on the receive + /// path. type IncomingPacketBuffer: AsMut<[u8]> + AsRef<[u8]>; } #[cfg(feature = "default-crypto")] @@ -53,8 +65,8 @@ impl crate::application::CryptoLayer for C { type PublicKey = CrateP384PublicKey; type KeyPair = CrateP384KeyPair; type Kem = CrateKyber1024PrivateKey; + type FingerprintData = (); type SessionData = C::SessionData; - type FingerprintData = C::LookupData; type IncomingPacketBuffer = C::IncomingPacketBuffer; } diff --git a/performance/src/crypto_impl/openssl.rs b/performance/src/crypto_impl/openssl.rs index 852cb1e..9a396db 100644 --- a/performance/src/crypto_impl/openssl.rs +++ b/performance/src/crypto_impl/openssl.rs @@ -1,12 +1,14 @@ -use std::{ - ptr::{self, NonNull}, - sync::{Mutex, MutexGuard}, -}; +use std::ptr::{self, NonNull}; +use std::sync::Mutex; +use arrayvec::ArrayVec; use openssl_sys::*; +use zeroize::Zeroizing; use crate::crypto::*; +/// A wrapper for a `EVP_CIPHER_CTX` that will free itself on drop. +/// Users are encouraged to not use one of these directly. pub struct OpenSSLCtx(NonNull); impl Drop for OpenSSLCtx { fn drop(&mut self) { @@ -21,6 +23,8 @@ impl OpenSSLCtx { unsafe { Some(OpenSSLCtx(NonNull::new(EVP_CIPHER_CTX_new())?)) } } + /// Initialize a cipher context for encryption or decryption using the specified `key` and `iv`. + /// If `key` is null then the previous key assigned to this context will be used. pub unsafe fn cipher_init( &self, t: *const openssl_sys::EVP_CIPHER, @@ -36,7 +40,15 @@ impl OpenSSLCtx { // OpenSSL will usually leak a static amount of memory per cipher given here. evp_f(self.0.as_ptr(), t, ptr::null_mut(), key, iv) > 0 } - + /// Stream a portion of text to be encrypted or decrypted. + /// `input` will be the input to the cipher stream, and the resulting plaintext or ciphertext + /// will be written to output. Both buffers must be of size `len`. + /// + /// If `output == input`, then the operation will be performed "in-place". + /// `output` and `input` must not overlap otherwise. + /// + /// If `output` is null, then `input` will be treated as AAD rather than plaintext or ciphertext. + /// `input` must not be null. pub unsafe fn update(&self, input: &[u8], output: *mut u8) -> bool { let evp_f = if ENCRYPT { EVP_EncryptUpdate @@ -55,6 +67,8 @@ impl OpenSSLCtx { ) > 0 } + /// Finish encryption or decryption. + /// If performing decryption this will return whether the set tag is correct. pub unsafe fn finalize(&self) -> bool { let evp_f = if ENCRYPT { EVP_EncryptFinal_ex @@ -66,6 +80,8 @@ impl OpenSSLCtx { evp_f(self.0.as_ptr(), ptr::null_mut(), &mut outl) > 0 } + /// Retreive the authentication tag from this context. + /// This must be called after `finalize` is called. pub unsafe fn get_tag(&self, tag: &mut [u8]) -> bool { EVP_CIPHER_CTX_ctrl( self.0.as_ptr(), @@ -74,6 +90,10 @@ impl OpenSSLCtx { tag.as_mut_ptr() as *mut _, ) > 0 } + + /// Set the authentication tag that was assigned to the input ciphertext. + /// Once set, OpenSLL will check whether it matches the expected authentication tag + /// produced by decryption. #[allow(unused)] pub unsafe fn set_tag(&self, tag: &[u8]) -> bool { EVP_CIPHER_CTX_ctrl( @@ -83,11 +103,17 @@ impl OpenSSLCtx { tag.as_ptr() as *mut _, ) > 0 } + /// Returns the raw pointer to the `EVP_CIPHER_CTX` + /// object used internally with OpenSSL. + /// + /// This function is guaranteed to return a non-null pointer. pub fn as_ptr(&self) -> *mut openssl_sys::EVP_CIPHER_CTX { self.0.as_ptr() } } +/// An `OpenSSLCtx` wrapped in a mutex for thread-safety. +/// This `OpenSSLCtx` struct only supports AES256 block operations, and implements `OpenSSLAes256Enc`. pub struct OpenSSLAes256Enc(Mutex); unsafe impl Send for OpenSSLAes256Enc {} unsafe impl Sync for OpenSSLAes256Enc {} @@ -118,6 +144,8 @@ impl Aes256Enc for OpenSSLAes256Enc { unsafe { assert!(ctx.update::(block, ptr)) } } } +/// An `OpenSSLCtx` wrapped in a mutex for thread-safety. +/// This `OpenSSLCtx` struct only supports AES256 block operations, and implements `OpenSSLAes256Dec`. pub struct OpenSSLAes256Dec(Mutex); unsafe impl Send for OpenSSLAes256Dec {} unsafe impl Sync for OpenSSLAes256Dec {} @@ -149,87 +177,87 @@ impl Aes256Dec for OpenSSLAes256Dec { } } -pub struct OpenSSLAesGcmEnc<'a>(MutexGuard<'a, OpenSSLCtx>); -impl<'a> AesGcmEncContext for OpenSSLAesGcmEnc<'a> { - fn encrypt(&mut self, input: &[u8], output: &mut [u8]) { - unsafe { assert!(self.0.update::(input, output.as_mut_ptr())) }; - } - - fn finish(self) -> [u8; AES_GCM_TAG_SIZE] { - let mut output = [0u8; AES_GCM_TAG_SIZE]; - unsafe { - assert!(self.0.finalize::()); - assert!(self.0.get_tag(&mut output)); - } - output - } -} - -pub struct OpenSSLAesGcmDec<'a>(MutexGuard<'a, OpenSSLCtx>); -impl<'a> AesGcmDecContext for OpenSSLAesGcmDec<'a> { - fn decrypt_in_place(&mut self, data: &mut [u8]) { - let p = data.as_mut_ptr(); - unsafe { assert!(self.0.update::(data, p)) }; - } - - fn finish(self, tag: &[u8; AES_GCM_TAG_SIZE]) -> bool { - unsafe { self.0.set_tag(tag) && self.0.finalize::() } - } -} - /// A pool of OpenSSL AES-GCM ciphers. pub struct OpenSSLAesGcmPool { - enc: [Mutex; 8], - dec: [Mutex; 8], + enc: Mutex>, + dec: Mutex>, + enc_key: Zeroizing<[u8; AES_256_KEY_SIZE]>, + dec_key: Zeroizing<[u8; AES_256_KEY_SIZE]>, } unsafe impl Send for OpenSSLAesGcmPool {} unsafe impl Sync for OpenSSLAesGcmPool {} impl HighThroughputAesGcmPool for OpenSSLAesGcmPool { - type EncContext<'a> = OpenSSLAesGcmEnc<'a>; + type EncContext<'a> = OpenSSLCtx; - type DecContext<'a> = OpenSSLAesGcmDec<'a>; + type DecContext<'a> = OpenSSLCtx; fn new(encrypt_key: &[u8; AES_256_KEY_SIZE], decrypt_key: &[u8; AES_256_KEY_SIZE]) -> Self { + Self { + enc: Default::default(), + dec: Default::default(), + enc_key: Zeroizing::new(*encrypt_key), + dec_key: Zeroizing::new(*decrypt_key), + } + } + + fn start_enc(&self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> OpenSSLCtx { + let ctx = self.enc.lock().unwrap().pop(); unsafe { - OpenSSLAesGcmPool { - enc: std::array::from_fn(|_| { - let ctx = OpenSSLCtx::new().unwrap(); - let t = openssl_sys::EVP_aes_256_gcm(); - assert!(ctx.cipher_init::(t, encrypt_key.as_ptr(), ptr::null())); - openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); - Mutex::new(ctx) - }), - dec: std::array::from_fn(|_| { - let ctx = OpenSSLCtx::new().unwrap(); - let t = openssl_sys::EVP_aes_256_gcm(); - assert!(ctx.cipher_init::(t, decrypt_key.as_ptr(), ptr::null())); - openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); - Mutex::new(ctx) - }), + if let Some(ctx) = ctx { + assert!(ctx.cipher_init::(ptr::null(), ptr::null(), nonce.as_ptr())); + ctx + } else { + let ctx = OpenSSLCtx::new().unwrap(); + let t = openssl_sys::EVP_aes_256_gcm(); + assert!(ctx.cipher_init::(t, self.enc_key.as_ptr(), nonce.as_ptr())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + ctx + } + } + } + fn start_dec(&self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> OpenSSLCtx { + let ctx = self.dec.lock().unwrap().pop(); + unsafe { + if let Some(ctx) = ctx { + assert!(ctx.cipher_init::(ptr::null(), ptr::null(), nonce.as_ptr())); + ctx + } else { + let ctx = OpenSSLCtx::new().unwrap(); + let t = openssl_sys::EVP_aes_256_gcm(); + assert!(ctx.cipher_init::(t, self.dec_key.as_ptr(), nonce.as_ptr())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + ctx } } } - fn start_enc<'a>(&'a self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> OpenSSLAesGcmEnc<'a> { - let i = u64::from_be_bytes(nonce[4..].try_into().unwrap()); - let g = self.enc[(i as usize) % self.enc.len()].lock().unwrap(); - unsafe { - assert!(g.cipher_init::(ptr::null(), ptr::null(), nonce.as_ptr())); - } - OpenSSLAesGcmEnc(g) + fn encrypt(&self, ctx: &mut OpenSSLCtx, input: &[u8], output: &mut [u8]) { + unsafe { assert!(ctx.update::(input, output.as_mut_ptr())) }; + } + fn decrypt_in_place(&self, ctx: &mut OpenSSLCtx, data: &mut [u8]) { + let p = data.as_mut_ptr(); + unsafe { assert!(ctx.update::(data, p)) }; } - fn start_dec<'a>(&'a self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> OpenSSLAesGcmDec<'a> { - let i = u64::from_be_bytes(nonce[4..].try_into().unwrap()); - let g = self.dec[(i as usize) % self.enc.len()].lock().unwrap(); + fn finish_enc(&self, ctx: OpenSSLCtx) -> [u8; AES_GCM_TAG_SIZE] { + let mut output = [0u8; AES_GCM_TAG_SIZE]; unsafe { - assert!(g.cipher_init::(ptr::null(), ptr::null(), nonce.as_ptr())); + assert!(ctx.finalize::()); + assert!(ctx.get_tag(&mut output)); } - OpenSSLAesGcmDec(g) + let _ = self.enc.lock().unwrap().try_push(ctx); + output + } + fn finish_dec(&self, ctx: OpenSSLCtx, tag: &[u8; AES_GCM_TAG_SIZE]) -> bool { + let output = unsafe { ctx.set_tag(tag) && ctx.finalize::() }; + let _ = self.dec.lock().unwrap().try_push(ctx); + output } } - +/// An empty struct which implements `LowThroughputAesGcm` using OpenSSL. +/// +/// It is just a namespace and wrapper for OpenSSL. pub struct OpenSSLAesGcm; impl LowThroughputAesGcm for OpenSSLAesGcm { fn encrypt_in_place( diff --git a/performance/src/lib.rs b/performance/src/lib.rs index 4e2f78c..d4e151c 100644 --- a/performance/src/lib.rs +++ b/performance/src/lib.rs @@ -36,9 +36,34 @@ //! - **KBKDF**: Key mixing, sub-key derivation //! - **AES-256**: Single block encryption of header to harden packet fragmentation protocol //! - **AES-256-GCM**: Authenticated encryption -//#![warn(missing_docs, rust_2018_idioms)] +#![warn(missing_docs, rust_2018_idioms)] #![allow(clippy::too_many_arguments, clippy::type_complexity, clippy::assertions_on_constants)] + +/// A collection of implementation-independent traits for the various specific cryptographic +/// algorithms ZSSP depends on. +/// +/// Each trait is very specific about the semantics of the algorithms and the lengths of their +/// inputs and outputs. +/// This is to enforced a basic sanity-check upon anyone trying to connect this library to +/// someone else's implementation. +/// +/// You should not use this module to implement your own crypto from scratch. +/// +/// The `crypto_impl` module contains implementations of these traits in terms of popular Rust +/// crates. pub mod crypto; +/// A module containing optional implementations of the ZSSP `crypto` traits in terms of popular +/// Rust crates. Some of these crates are not thoroughly audited, so use at your own risk. +/// +/// The current version of the AES crate does not support stream encryption. For this reason +/// ZSSP's AES trait is implemented with OpenSSL instead. It has been optimized for +/// hardware accelerated, parrallel encryption and decryption. +/// +/// Note that none of these crates are FIPS certified, meaning a build of ZSSP using them will not +/// be FIPS compliant. However lack of FIPS compliance by no means implies lack of security or lack +/// of confidence. +/// +/// This module contains the trait implementations as well as re-exports of those crates. pub mod crypto_impl; mod antireplay; @@ -58,8 +83,14 @@ mod symmetric_state; mod zeta; mod zssp; +/// An abstraction over OS and use-case specific resources and queries. +/// This allows this library to be platform independent. +/// A user of this library will need to implement the `ApplicationLayer` trait. pub mod application; +/// This module contains several ZSSP constants that a user might want to know for key management, +/// or in order to avoid "data too large" or "mtu too small" errors. pub mod proto; +/// The collection of the major return types for ZSSP. pub mod result; pub use crate::log_event::*; diff --git a/performance/src/proto.rs b/performance/src/proto.rs index 5b1c3cb..648f838 100644 --- a/performance/src/proto.rs +++ b/performance/src/proto.rs @@ -124,14 +124,12 @@ pub(crate) const PACKET_TYPE_DATA: u8 = 8; pub(crate) const PACKET_TYPE_CHALLENGE: u8 = 9; pub(crate) const PACKET_TYPE_USES_COUNTER_RANGE: std::ops::Range = 3..9; -pub(crate) const HANDSHAKE_HELLO_MIN_SIZE: usize = - KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE; -pub(crate) const HANDSHAKE_HELLO_MAX_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + RATCHET_SIZE + RATCHET_SIZE; +pub(crate) const HANDSHAKE_HELLO_SIZE: usize = + KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + 2 * RATCHET_SIZE + AES_GCM_TAG_SIZE; -pub(crate) const HANDSHAKE_HELLO_CHALLENGE_MIN_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + CHALLENGE_SIZE; -pub(crate) const HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE: usize = HANDSHAKE_HELLO_MAX_SIZE + CHALLENGE_SIZE; +pub(crate) const HANDSHAKE_HELLO_CHALLENGE_SIZE: usize = HANDSHAKE_HELLO_SIZE + CHALLENGE_SIZE; -pub(crate) const HEADERED_HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE + HEADER_SIZE; +pub(crate) const HEADERED_HANDSHAKE_HELLO_CHALLENGE_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_SIZE + HEADER_SIZE; pub(crate) const HANDSHAKE_RESPONSE_SIZE: usize = P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE; @@ -184,6 +182,8 @@ pub(crate) const MAX_UNASSOCIATED_FRAGMENTS: usize = 32 * 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 = HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE; +pub(crate) const MAX_UNASSOCIATED_PACKET_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_SIZE; +/// This number determines how many defragmentation buffers are created per session. +/// Each defragmentation buffer handles one packet at a time. pub(crate) const SESSION_MAX_FRAGMENTS_OOO: usize = 64; diff --git a/performance/src/ratchet_state.rs b/performance/src/ratchet_state.rs index b12aa38..3f07290 100644 --- a/performance/src/ratchet_state.rs +++ b/performance/src/ratchet_state.rs @@ -14,30 +14,23 @@ use crate::proto::*; #[derive(Clone, Eq)] pub struct RatchetState { pub(crate) key: Zeroizing<[u8; RATCHET_SIZE]>, - pub(crate) fingerprint: Option>, + pub(crate) fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, pub(crate) chain_len: u64, } impl PartialEq for RatchetState { fn eq(&self, other: &Self) -> bool { - let ret = match (self.fingerprint.as_ref(), other.fingerprint.as_ref()) { - (Some(rf1), Some(rf2)) => secure_eq(rf1, rf2), - (None, None) => true, - _ => false, - }; - ret & secure_eq(&self.key, &other.key) & (self.chain_len == other.chain_len) + self.fingerprint.eq(&other.fingerprint) & (self.chain_len == other.chain_len) } } impl std::hash::Hash for RatchetState { fn hash(&self, state: &mut H) { - if let Some(rf) = &self.fingerprint { - state.write_u64(u64::from_ne_bytes(rf[..8].try_into().unwrap())) - } + state.write(self.fingerprint.as_ref()) } } impl RatchetState { /// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length. pub fn new(key: Zeroizing<[u8; RATCHET_SIZE]>, fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, chain_len: u64) -> Self { - RatchetState { key, fingerprint: Some(fingerprint), chain_len } + RatchetState { key, fingerprint, chain_len } } /// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length. /// @@ -46,28 +39,10 @@ impl RatchetState { pub fn new_raw(key: [u8; RATCHET_SIZE], fingerprint: [u8; RATCHET_SIZE], chain_len: u64) -> Self { RatchetState { key: Zeroizing::new(key), - fingerprint: Some(Zeroizing::new(fingerprint)), + fingerprint: Zeroizing::new(fingerprint), chain_len, } } - /// The ratchet key for this ratchet state. This is directly mixed into the master secret of a - /// session and so is very sensitive. All operations upon a ratchet key must be implemented - /// in constant time. The user should prefer to do nothing with the ratchet key besides copying - /// it to or from a storage device. - /// - /// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be - /// all zeros. - pub fn key(&self) -> &[u8; RATCHET_SIZE] { - &self.key - } - /// Ratchet keys and fingerprints are "chained together", where each set is derived from the - /// previous set. - /// - /// This function outputs the total length of that chain, as in the total number of previous - /// ratchet states that this ratchet state was derived from. - pub fn chain_len(&self) -> u64 { - self.chain_len - } /// Creates a new "empty" ratchet state, where the ratchet fingerprint is the /// empty string, the ratchet key is all zeros, and the chain length is 0. /// @@ -75,7 +50,7 @@ impl RatchetState { pub fn empty() -> Self { RatchetState { key: Zeroizing::new([0u8; RATCHET_SIZE]), - fingerprint: None, + fingerprint: Zeroizing::new([0u8; RATCHET_SIZE]), chain_len: 0, } } @@ -99,15 +74,15 @@ impl RatchetState { Self::new(rk, rf, 1) } - /// Returns true if this is the "empty" ratchet state, where the ratchet fingerprint is the - /// empty string, the ratchet key is all zeros, and the chain length is 0. - pub fn is_empty(&self) -> bool { - self.fingerprint.is_none() - } - /// Checks if the fingerprint of this ratchet state equals the fingerprint contained in argument - /// `rf`. Uses constant time equality. - pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { - self.fingerprint.as_ref().map_or(false, |rf0| secure_eq(rf0, rf)) + /// The ratchet key for this ratchet state. This is directly mixed into the master secret of a + /// session and so is very sensitive. All operations upon a ratchet key must be implemented + /// in constant time. The user should prefer to do nothing with the ratchet key besides copying + /// it to or from a storage device. + /// + /// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be + /// all zeros. + pub fn key(&self) -> &[u8; RATCHET_SIZE] { + &self.key } /// The ratchet fingerprint for this ratchet state. /// @@ -118,8 +93,43 @@ impl RatchetState { /// but the security of ZSSP can survive having this value leaked. /// Operations on a ratchet fingerprint should be implemented in constant time, /// but it is ok if they are not. - pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - self.fingerprint.as_deref() + pub fn fingerprint(&self) -> &[u8; RATCHET_SIZE] { + &self.fingerprint + } + /// Ratchet keys and fingerprints are "chained together", where each set is derived from the + /// previous set. + /// + /// This function outputs the total length of that chain, as in the total number of previous + /// ratchet states that this ratchet state was derived from. + pub fn chain_len(&self) -> u64 { + self.chain_len + } + /// Returns true if this is the "empty" ratchet state, where the ratchet fingerprint is the + /// empty string, the ratchet key is all zeros, and the chain length is 0. + pub fn is_empty(&self) -> bool { + secure_eq(self.fingerprint(), &[0u8; RATCHET_SIZE]) + } + /// Checks if the fingerprint of this ratchet state equals the + /// fingerprint contained in argument `rf`. + /// + /// Uses constant time equality. + pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { + secure_eq(self.fingerprint(), rf) + } + /// Returns true if the fingerprint of argument `this` equals the fingerprint represented by + /// argument `rf`. + /// If `this` is `None`, `this` is considered to be "null", as in a ratchet state that does not + /// exist or has been deleted. + /// If `rf` is `None`, `rf` is considered to be "null" as well. + /// If `rf` is `Some(None)`, `rf` is considered to be the empty ratchet fingerprint. + /// + /// Uses constant time equality. + pub fn fingerprint_eq_nullable(this: Option<&Self>, rf: Option<&[u8; RATCHET_SIZE]>) -> bool { + match (this, rf) { + (Some(this), Some(rf)) => this.fingerprint_eq(rf), + (None, None) => true, + _ => false, + } } } impl Default for RatchetState { @@ -137,7 +147,8 @@ impl Default for RatchetState { pub struct RatchetStates { /// The first ratchet state from the pair. pub state1: RatchetState, - /// The second ratchet state from the pair. It can, and usually will be `None`. + /// The second ratchet state from the pair. + /// It can, and usually will be `None`, which means that this ratchet state is "null". pub state2: Option, } impl RatchetStates { @@ -172,7 +183,6 @@ impl Default for RatchetStates { Self::new_initial_states() } } - /// A set of references to ratchet states specifying how a remote peer's persistent storage should /// be updated. This struct is designed to provide any and all potentially needed data for /// maintaining a store of these ratchet states. It should be straightforward to commit these updates @@ -181,69 +191,136 @@ impl Default for RatchetStates { /// There will only be up to two ratchet states saved to storage at a time per peer. /// Every time a third ratchet state is generated, a previous ratchet state will be deleted. /// -/// These are sensitive values should they ought to be securely stored, with restricted read-write -/// permissions if stored on disk. +/// As the name implies, these updates should be applied as one atomic compare-and-swap operation. +/// If the two ratchet states currently in storage equal `cur_state1` and `cur_state2`, +/// then `new_state1` and `new_state2` should overwrite them. +/// If not, then storage must not be modified. /// -/// To prevent desync or resource leakage, these updates should be committed atomically. If that is -/// not possible, then new ratchet states should be written before old ratchet states are deleted +/// Ratchet keys must never be checked for equality. Instead, if two ratchet states have equal +/// ratchet fingerprints, it should be assumed that they also have equal ratchet keys. +/// This policy exists to reduce the security impact of timing and other side-channel attacks. +/// +/// These are sensitive values and ought to be securely stored, with restricted read-write +/// permissions if stored on disk. #[derive(Clone)] -pub struct RatchetUpdate<'a> { - /// The ratchet key and fingerprint to store in the first slot. - pub state1: &'a RatchetState, - /// The ratchet key and fingerprint to store in the second slot. - pub state2: Option<&'a RatchetState>, - /// Whether `state1` is a brand new ratchet state, or if it was previously saved. - pub state1_was_just_added: bool, - /// A previous ratchet key and fingerprint that now must be deleted from storage. - /// This will have been a previously given value of `state1` or `state2`. - pub deleted_state1: Option<&'a RatchetState>, - /// A previous ratchet key and fingerprint that now must be deleted from storage. - /// It is extremely rare that this field is occupied. - pub deleted_state2: Option<&'a RatchetState>, +pub struct CompareAndSwap<'a> { + /// The ratchet state to store in the first slot. + pub new_state1: &'a RatchetState, + /// The ratchet state to store in the second slot. + /// A value of `None` implies that the second slot should be set to null. + pub new_state2: Option<&'a RatchetState>, + /// This field is `true` if and only if `new_state1 != cur_state1` and `new_state1 != cur_state2`. + /// + /// This implies whether `state1` is a brand new ratchet state, or if it was previously saved. + pub new_state1_was_just_added: bool, + /// The ratchet state that we expect to see in the first slot. + /// If this value is not currently stored in the first slot, the entire update must be aborted. + pub cur_state1: &'a RatchetState, + /// The ratchet state that we expect to see in the second slot. + /// A value of `None` implies we expect the second slot to be set to "null". + /// + /// If this value is not currently stored in the second slot, the entire update must be aborted. + pub cur_state2: Option<&'a RatchetState>, + /// This field is `true` if and only if `cur_state1 != new_state1` and `cur_state1 != new_state2`. + pub cur_state1_was_just_deleted: bool, + /// This field is `true` if and only if `cur_state2 != new_state1` and `cur_state2 != new_state2`. + pub cur_state2_was_just_deleted: bool, } -impl<'a> RatchetUpdate<'a> { - /// Returns the final `RatchetStates` that should be the only thing saved after this update is - /// fully committed. Future calls to `ApplicationLayer::restore_by_identity` should return this struct. - pub fn to_states(&self) -> RatchetStates { - RatchetStates::new(self.state1.clone(), self.state2.cloned()) +impl<'a> CompareAndSwap<'a> { + pub(crate) fn new( + new_state1: &'a RatchetState, + new_state2: Option<&'a RatchetState>, + new_state1_was_just_added: bool, + cur_state1: &'a RatchetState, + cur_state2: Option<&'a RatchetState>, + cur_state1_was_just_deleted: bool, + cur_state2_was_just_deleted: bool, + ) -> Self { + Self { + new_state1, + new_state2, + new_state1_was_just_added, + cur_state1, + cur_state2, + cur_state1_was_just_deleted, + cur_state2_was_just_deleted, + } + } + /// Returns the final `RatchetStates` that must be swapped into storage if this update is + /// fully committed. + /// Future calls to `ApplicationLayer::restore_by_identity` should return this struct. + pub fn to_new_states(&self) -> RatchetStates { + RatchetStates::new(self.new_state1.clone(), self.new_state2.cloned()) + } + /// Returns the `RatchetStates` that is expected to currently be in storage for this peer. + /// This value must be compared with the current value in storage, and if they are equal, + /// the return value of `CompareAndSwap::to_new_states` must overwrite it. + pub fn to_cur_states(&self) -> RatchetStates { + RatchetStates::new(self.cur_state1.clone(), self.cur_state2.cloned()) } /// If this update specifies adding a brand new ratchet fingerprint, this function will return it. /// The returned ratchet fingerprint will always be the ratchet fingerprint of field `state1`. /// - /// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be true. + /// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be `true`. pub fn added_fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - if self.state1_was_just_added { - self.state1.fingerprint() - } else { - None - } + self.new_state1_was_just_added.then_some(self.new_state1.fingerprint()) } - /// If this updated specifies to delete an old ratchet fingerprint, this function will return it. - /// The returned ratchet fingerprint will always be the ratchet fingerprint of field - /// `deleted_state1`. + /// If this updated specifies to delete `cur_state1`, and `cur_state1` has a non-zero ratchet + /// fingerprint, this function will return the ratchet fingerprint. + /// + /// If `cur_state1` is not being deleted (i.e. `cur_state1 == new_state1` or + /// `cur_state1 == new_state2`), then this will return `None`. /// /// There may be a second ratchet fingerprint to be deleted, which function /// `deleted_fingerprint2` will return. pub fn deleted_fingerprint1(&self) -> Option<&[u8; RATCHET_SIZE]> { - if let Some(rs) = &self.deleted_state1 { - rs.fingerprint() - } else { - None + if self.cur_state1_was_just_deleted && !self.cur_state1.is_empty() { + return Some(self.cur_state1.fingerprint()); } + None } - /// If this updated specifies to delete two ratchet fingerprints, this function will return the - /// second one. `deleted_fingerprint1` will return the first one. - /// The returned ratchet fingerprint will always be the ratchet fingerprint of field - /// `deleted_state2`. + /// If this updated specifies to delete `cur_state2`, and `cur_state2` has a non-zero ratchet + /// fingerprint, this function will return the ratchet fingerprint. /// - /// It is exceptionally rare that there will be more than one ratchet fingerprint to be deleted. - /// Care should be taken to make sure that this update will still be correctly committed in the - /// rare event that this returns `Some`. + /// If `cur_state2` is not being deleted (i.e. `cur_state2 == new_state1` or + /// `cur_state2 == new_state2`), then this will return `None`. + /// + /// There may be a second ratchet fingerprint to be deleted, which function + /// `deleted_fingerprint1` will return. pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> { - if let Some(rs) = &self.deleted_state2 { - rs.fingerprint() - } else { - None + if self.cur_state2_was_just_deleted { + if let Some(rf) = self.cur_state2 { + if !rf.is_empty() { + return Some(rf.fingerprint()); + } + } } + None + } + /// Returns true if the currently stored ratchet states are expected to be the initial ratchet + /// states. This is the default value for a peer's ratchet states in the event they could not + /// be found in storage. + /// + /// If a peer could not be found in storage, and this function returns true, + /// then the peer should be added to storage with the new ratchet states specified by this + /// `CompareAndSwap` struct. + pub fn cur_is_initial_states(&self) -> bool { + self.cur_state1.is_empty() && self.cur_state2.is_none() + } + /// Compares the ratchet fingerprints of `cur_state1` and `cur_state2` with `rf1` and `rf2`. + /// If they are equal this function will return `true`. + /// + /// If this function returns `true`, then the implementation may proceed to swap out the ratchet + /// states these fingerprints come from with `new_state1` and `new_state2`. + pub fn compare_fingerprints(&self, rf1: &[u8; RATCHET_SIZE], rf2: Option<&[u8; RATCHET_SIZE]>) -> bool { + self.cur_state1.fingerprint_eq(rf1) & RatchetState::fingerprint_eq_nullable(self.cur_state2, rf2) + } + /// Compares `cur_state1` and `cur_state2` with `other.state1` and `other.state2`. + /// If they are equal this function will return `true`. + /// + /// If this function returns `true`, then the implementation may proceed to swap `other.state1` + /// and `other.state2` with `new_state1` and `new_state2`. + pub fn compare(&self, other: &RatchetStates) -> bool { + self.cur_state1.eq(&other.state1) & self.cur_state2.eq(&other.state2.as_ref()) } } diff --git a/performance/src/result.rs b/performance/src/result.rs index f65387b..1c8bbba 100644 --- a/performance/src/result.rs +++ b/performance/src/result.rs @@ -190,7 +190,7 @@ pub enum ReceiveOk { Unassociated, /// The received packet was authentic and belongs to this specific session. Associated(Arc>, SessionEvent), - /// The received packet was a fragment of a larger packet. + /// The received packet was an incomplete fragment of a larger packet. /// /// ***The authenticity of this fragment cannot be fully known yet.*** /// This return value should only be used for debugging and tracing purposes. diff --git a/performance/src/zeta.rs b/performance/src/zeta.rs index 0bcdd60..bd8f7d2 100644 --- a/performance/src/zeta.rs +++ b/performance/src/zeta.rs @@ -97,7 +97,7 @@ pub(crate) struct StateA1 { e_secret: C::KeyPair, e1_secret: C::Kem, identity: ArrayVec, - x1: ArrayVec, + x1: ArrayVec, } pub(crate) struct StateA3 { @@ -280,7 +280,7 @@ fn create_a1_state( // ... // -> e, es, e1 let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); - let mut x1 = ArrayVec::::new(); + let mut x1 = ArrayVec::::new(); x1.extend([0u8; HEADER_SIZE]); // Noise process prologue. let kid = kid_recv.get().to_ne_bytes(); @@ -299,12 +299,9 @@ fn create_a1_state( x1.extend(tag); // Process message pattern 1 payload. let i = x1.len(); - if let Some(rf) = ratchet_state1.fingerprint() { - x1.try_extend_from_slice(rf).unwrap(); - } - if let Some(Some(rf)) = ratchet_state2.map(|rs| rs.fingerprint()) { - x1.try_extend_from_slice(rf).unwrap(); - } + x1.try_extend_from_slice(ratchet_state1.fingerprint()).unwrap(); + x1.try_extend_from_slice(ratchet_state2.map_or(&[0u8; RATCHET_SIZE], |r| r.fingerprint())) + .unwrap(); let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), &mut x1[i..]); x1.extend(tag); @@ -325,12 +322,10 @@ pub(crate) fn trans_to_a1>( s_remote: C::PublicKey, session_data: C::SessionData, identity: &[u8], + ratchet_states: RatchetStates, send: impl FnOnce(&mut [u8], Option<&C::PrpEnc>), ) -> Result<(Arc>, Option), OpenError> { - let RatchetStates { state1, state2 } = app - .restore_by_identity(&s_remote, &session_data, None) - .map_err(OpenError::StorageError)? - .unwrap_or_default(); + let RatchetStates { state1, state2 } = ratchet_states; let mut session_queue = ctx.session_queue.lock().unwrap(); let mut session_map = ctx.session_map.write().unwrap(); @@ -428,11 +423,11 @@ pub(crate) fn received_x1_trans>( // ... // -> e, es, e1 // <- e, ee, ekem1, psk - if !(HANDSHAKE_HELLO_MIN_SIZE..=HANDSHAKE_HELLO_MAX_SIZE).contains(&x1.len()) { + if x1.len() != HANDSHAKE_HELLO_SIZE { return Err(fault!(InvalidPacket, true)); } - if n[AES_GCM_NONCE_SIZE - 8..] != x1[x1.len() - 8..] { + if !secure_eq(&n[AES_GCM_NONCE_SIZE - 8..], &x1[x1.len() - 8..]) { return Err(fault!(FailedAuth, true)); } let hmac = &mut C::Hmac::new(); @@ -462,35 +457,46 @@ pub(crate) fn received_x1_trans>( let e1_end = j; i = k; // Process message pattern 1 payload. - let k = x1.len(); - let j = k - AES_GCM_TAG_SIZE; + let j = i + RATCHET_SIZE + RATCHET_SIZE; + let k = j + AES_GCM_TAG_SIZE; let tag = x1[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), &mut x1[i..j], tag) { return Err(fault!(FailedAuth, true)); } + debug_assert_eq!(k, x1.len()); + let rf1 = &x1[i..i + RATCHET_SIZE]; + let rf2 = &x1[i + RATCHET_SIZE..j]; let mut lookup_data = None; let mut ratchet_state = None; - while i + RATCHET_SIZE <= j { - match app.restore_by_fingerprint((&x1[i..i + RATCHET_SIZE]).try_into().unwrap()) { + if !secure_eq(rf1, &[0u8; RATCHET_SIZE]) { + match app.restore_by_fingerprint(rf1.try_into().unwrap()) { Ok(None) => {} Ok(Some((rs, data))) => { lookup_data = Some(data); ratchet_state = Some(rs); - break; } Err(e) => return Err(ReceiveError::StorageError(e)), } - i += RATCHET_SIZE; } - let ratchet_state = if let Some(rs) = ratchet_state { - rs - } else { - if app.hello_requires_recognized_ratchet() { + if ratchet_state.is_none() { + if !secure_eq(rf2, &[0u8; RATCHET_SIZE]) { + match app.restore_by_fingerprint(rf1.try_into().unwrap()) { + Ok(None) => {} + Ok(Some((rs, data))) => { + lookup_data = Some(data); + ratchet_state = Some(rs); + } + Err(e) => return Err(ReceiveError::StorageError(e)), + } + } + if ratchet_state.is_none() && app.hello_requires_recognized_ratchet() { return Err(fault!(FailedAuth, true)); } - RatchetState::empty() - }; + } + // If we get to this point and haven't found a full ratchet state, + // set it to the empty ratchet state. + let ratchet_state = ratchet_state.unwrap_or_default(); let mut hk_recv = Zeroizing::new([0u8; HASHLEN]); let mut hk_send = Zeroizing::new([0u8; HASHLEN]); @@ -591,7 +597,7 @@ pub(crate) fn received_x2_trans>( return Err(fault!(UnknownLocalKeyId, true, session)); } let (_, c) = from_nonce(n); - if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || n[AES_GCM_NONCE_SIZE - 3..] != x2[x2.len() - 3..] { + if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || !secure_eq(&n[AES_GCM_NONCE_SIZE - 3..], &x2[x2.len() - 3..]) { return Err(fault!(FailedAuth, true, session)); } @@ -692,23 +698,27 @@ pub(crate) fn received_x2_trans>( let new_ratchet_state = create_ratchet_state(hmac, &noise, chain_len); - let (ratchet_to_preserve, ratchet_to_delete) = if ratchet_i == 1 { - (Some(&state.ratchet_state1), state.ratchet_state2.as_ref()) + let ratchet_to_preserve = if ratchet_i == 1 { + Some(&state.ratchet_state1) } else { - (state.ratchet_state2.as_ref(), Some(&state.ratchet_state1)) + state.ratchet_state2.as_ref() }; - app.save_ratchet_state( + let result = app.save_ratchet_state( &session.s_remote, &session.session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: ratchet_to_preserve, - state1_was_just_added: true, - deleted_state1: ratchet_to_delete, - deleted_state2: None, - }, - ) - .map_err(|e| ReceiveError::StorageError(e))?; + CompareAndSwap::new( + &new_ratchet_state, + ratchet_to_preserve, + true, + &state.ratchet_state1, + state.ratchet_state2.as_ref(), + ratchet_i == 2, + ratchet_i == 1, + ), + ); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true, session, true)); + } let mut kek_recv = Zeroizing::new([0u8; HASHLEN]); let mut kek_send = Zeroizing::new([0u8; HASHLEN]); @@ -738,7 +748,7 @@ pub(crate) fn received_x2_trans>( a1 } else { // This return is unreachable. - return Err(fault!(FailedAuth, true, session)); + return Err(fault!(FailedAuth, true, session, true)); }; state.beta = ZetaAutomata::A3(Box::new(StateA3 { identity: a1.identity.clone(), x3: x3.clone() })); resend_timer @@ -860,7 +870,7 @@ pub(crate) fn received_x3_trans>( let mut should_warn_missing_ratchet = false; if (zeta.ratchet_state != state1) & (Some(&zeta.ratchet_state) != state2.as_ref()) { - if !responder_disallows_downgrade && zeta.ratchet_state.fingerprint().is_none() { + if !responder_disallows_downgrade && zeta.ratchet_state.is_empty() { should_warn_missing_ratchet = true; } else { if !responder_silently_rejects { @@ -879,18 +889,14 @@ pub(crate) fn received_x3_trans>( noise.split(hmac, &mut nk_send, &mut nk_recv); // We must make sure the ratchet key is saved before we transition. - app.save_ratchet_state( + let result = app.save_ratchet_state( &s_remote, &session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: None, - state1_was_just_added: true, - deleted_state1: Some(&state1), - deleted_state2: state2.as_ref(), - }, - ) - .map_err(|e| ReceiveError::StorageError(e))?; + CompareAndSwap::new(&new_ratchet_state, None, true, &state1, state2.as_ref(), true, true), + ); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true)); + } let (session, reduced_service_time) = { let mut session_map = ctx.session_map.write().unwrap(); @@ -1006,18 +1012,25 @@ pub(crate) fn received_c1_trans>( if is_other { if let ZetaAutomata::A3 { .. } | ZetaAutomata::R2 { .. } = &state.beta { if state.ratchet_state2.is_some() { - app.save_ratchet_state( + let result = app.save_ratchet_state( &session.s_remote, &session.session_data, - RatchetUpdate { - state1: &state.ratchet_state1, - state2: None, - state1_was_just_added: false, - deleted_state1: state.ratchet_state2.as_ref(), - deleted_state2: None, - }, - ) - .map_err(|e| ReceiveError::StorageError(e))?; + CompareAndSwap::new( + &state.ratchet_state1, + None, + false, + &state.ratchet_state1, + state.ratchet_state2.as_ref(), + false, + true, + ), + ); + if !result.map_err(ReceiveError::StorageError)? { + drop(state); + drop(kex_lock); + session.expire(); + return Err(fault!(OutOfSequence, true, session, true)); + } } drop(state); let timeout_timer = { @@ -1417,18 +1430,22 @@ pub(crate) fn received_k1_trans>( k2.extend(tag); let new_ratchet_state = create_ratchet_state(hmac, &noise, state.ratchet_state1.chain_len); - app.save_ratchet_state( + let result = app.save_ratchet_state( &session.s_remote, &session.session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: Some(&state.ratchet_state1), - state1_was_just_added: true, - deleted_state1: state.ratchet_state2.as_ref(), - deleted_state2: None, - }, - ) - .map_err(|e| ReceiveError::StorageError(e))?; + CompareAndSwap::new( + &new_ratchet_state, + Some(&state.ratchet_state1), + true, + &state.ratchet_state1, + state.ratchet_state2.as_ref(), + false, + true, + ), + ); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true, session, true)); + } let mut kek_recv = Zeroizing::new([0u8; HASHLEN]); let mut kek_send = Zeroizing::new([0u8; HASHLEN]); @@ -1536,18 +1553,22 @@ pub(crate) fn received_k2_trans>( .ok_or_else(|| fault!(InvalidPacket, true, session, true))?; let new_ratchet_state = create_ratchet_state(hmac, &noise, state.ratchet_state1.chain_len); - app.save_ratchet_state( + let result = app.save_ratchet_state( &session.s_remote, &session.session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: None, - state1_was_just_added: true, - deleted_state1: Some(&state.ratchet_state1), - deleted_state2: state.ratchet_state2.as_ref(), - }, - ) - .map_err(|e| ReceiveError::StorageError(e))?; + CompareAndSwap::new( + &new_ratchet_state, + None, + true, + &state.ratchet_state1, + state.ratchet_state2.as_ref(), + true, + true, + ), + ); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true, session, true)); + } let mut kek_recv = Zeroizing::new([0u8; HASHLEN]); let mut kek_send = Zeroizing::new([0u8; HASHLEN]); @@ -1630,15 +1651,6 @@ pub(crate) fn send_payload( }; let nonce = to_nonce(PACKET_TYPE_DATA, c); - let key = state.key_ref(false); - let kid_send = key.send.kid.ok_or(SessionNotEstablished)?.get().to_ne_bytes(); - let mut cipher = key.nk.as_ref().ok_or(SessionNotEstablished)?.start_enc(&nonce); - - debug_assert!(matches!( - &state.beta, - ZetaAutomata::S1 | ZetaAutomata::S2 | ZetaAutomata::R1 { .. } | ZetaAutomata::R2 { .. } - )); - let payload_mtu = mtu - HEADER_SIZE; debug_assert!(payload_mtu >= 4); let tagged_payload_len = payload.len() + AES_GCM_TAG_SIZE; @@ -1649,6 +1661,16 @@ pub(crate) fn send_payload( return Err(DataTooLarge); } + debug_assert!(matches!( + &state.beta, + ZetaAutomata::S1 | ZetaAutomata::S2 | ZetaAutomata::R1 { .. } | ZetaAutomata::R2 { .. } + )); + + let key = state.key_ref(false); + let kid_send = key.send.kid.ok_or(SessionNotEstablished)?.get().to_ne_bytes(); + let cipher_pool = key.nk.as_ref().ok_or(SessionNotEstablished)?; + let mut cipher = cipher_pool.start_enc(&nonce); + let mut header = [0u8; HEADER_SIZE]; header[..KID_SIZE].copy_from_slice(&kid_send); header[FRAGMENT_COUNT_IDX] = fragment_count as u8; @@ -1662,12 +1684,15 @@ pub(crate) fn send_payload( mtu_sized_buffer[..HEADER_SIZE].copy_from_slice(&header); mtu_sized_buffer[FRAGMENT_NO_IDX] = fragment_no as u8; let fragment_start = &mut mtu_sized_buffer[HEADER_SIZE..HEADER_SIZE + fragment_len]; - cipher.encrypt(&payload[i..j], fragment_start); + cipher_pool.encrypt(&mut cipher, &payload[i..j], fragment_start); let header_auth = &mut mtu_sized_buffer[HEADER_AUTH_START..HEADER_AUTH_END]; state.hk_send.encrypt_in_place(header_auth.try_into().unwrap()); if !send.send_frag(&mut mtu_sized_buffer[..HEADER_SIZE + fragment_len]) { + // We need to give the cipher back to the pool instead of dropping it, + // so it can do memory cleanup. + cipher_pool.finish_enc(cipher); return Ok(false); } i = j; @@ -1680,8 +1705,9 @@ pub(crate) fn send_payload( mtu_sized_buffer[..HEADER_SIZE].copy_from_slice(&header); mtu_sized_buffer[FRAGMENT_NO_IDX] = fragment_no as u8; let fragment_start = &mut mtu_sized_buffer[HEADER_SIZE..HEADER_SIZE + payload_rem]; - cipher.encrypt(&payload[i..], fragment_start); - mtu_sized_buffer[HEADER_SIZE + payload_rem..HEADER_SIZE + fragment_len].copy_from_slice(&cipher.finish()); + cipher_pool.encrypt(&mut cipher, &payload[i..], fragment_start); + mtu_sized_buffer[HEADER_SIZE + payload_rem..HEADER_SIZE + fragment_len] + .copy_from_slice(&cipher_pool.finish_enc(cipher)); let header_auth = &mut mtu_sized_buffer[HEADER_AUTH_START..HEADER_AUTH_END]; state.hk_send.encrypt_in_place(header_auth.try_into().unwrap()); @@ -1728,9 +1754,8 @@ pub(crate) fn receive_payload_in_place( return Err(fault!(UnknownLocalKeyId, true, session)); }; - let mut cipher = specified_key - .ok_or_else(|| fault!(OutOfSequence, true, session))? - .start_dec(nonce); + let cipher_pool = specified_key.ok_or_else(|| fault!(OutOfSequence, true, session))?; + let mut cipher = cipher_pool.start_dec(nonce); let (_, c) = from_nonce(nonce); // NOTE: This only works because we check the size of every received fragment in the receive @@ -1738,14 +1763,14 @@ pub(crate) fn receive_payload_in_place( for i in 0..fragments.len() - 1 { let fragment = &mut fragments[i].as_mut()[HEADER_SIZE..]; debug_assert!(fragment.len() >= AES_GCM_TAG_SIZE); - cipher.decrypt_in_place(fragment); + cipher_pool.decrypt_in_place(&mut cipher, fragment); } let fragment = &mut fragments[fragments.len() - 1].as_mut()[HEADER_SIZE..]; debug_assert!(fragment.len() >= AES_GCM_TAG_SIZE); let tag_idx = fragment.len() - AES_GCM_TAG_SIZE; - cipher.decrypt_in_place(&mut fragment[..tag_idx]); + cipher_pool.decrypt_in_place(&mut cipher, &mut fragment[..tag_idx]); - if !cipher.finish((&fragment[tag_idx..]).try_into().unwrap()) { + if !cipher_pool.finish_dec(cipher, (&fragment[tag_idx..]).try_into().unwrap()) { return Err(fault!(FailedAuth, true, session)); } diff --git a/performance/src/zssp.rs b/performance/src/zssp.rs index 9d7fc18..8825834 100644 --- a/performance/src/zssp.rs +++ b/performance/src/zssp.rs @@ -161,6 +161,63 @@ impl Context { /// * `identity` - Payload to be sent to Bob that contains the information necessary /// for the upper protocol to authenticate and approve of Alice's identity. pub fn open>( + &self, + mut app: App, + send: impl Sender, + mtu: usize, + static_remote_key: C::PublicKey, + session_data: C::SessionData, + identity: &[u8], + ) -> Result<(Arc>, Option), OpenError> { + let ratchet_states = app + .restore_by_identity(&static_remote_key, &session_data, None) + .map_err(OpenError::StorageError)? + .unwrap_or_default(); + self.open_with_ratchet( + app, + send, + mtu, + static_remote_key, + session_data, + identity, + ratchet_states, + ) + } + /// Create a new session and send initialization packets to Bob, our remote peer. + /// This function will use the specified `ratchet_states` to connect to Bob, as opposed to + /// calling `app.restore_by_identity`. This can be used to open a session with a specific + /// one-time-password using `RatchetStates::new_otp_states()`, or in situations where it is + /// desireable to avoid having Alice call `app.restore_by_identity`. Keep in mind that + /// `save_ratchet_state` likely will eventually be called to delete this ratchet state. + /// + /// If using a one-time-password, a call to `app.initiator_disallows_downgrade` on the created + /// session **must** return `true`. Otherwise the remote peer would be able to avoid having + /// to demonstrate knowledge of the otp by requesting a ratchet downgrade. + /// Only after the created session is established may a call to + /// `app.initiator_disallows_downgrade` return false. + /// + /// The session will not be "established" right away, and so will not be able to send data to + /// the remote peer until they respond and finish the handshake. A `SessionEvent` variant of + /// `Established` will be returned by `Context::receive` when this session is able to send data. + /// + /// This function returns an `Option`, which can safely be ignored if not using + /// `Context::service_scheduled`. `Context::service_scheduled` contains documentation on how to + /// handle the return value. + /// + /// To prevent desync, when this function is called, no other open session with the same remote + /// peer must exist. Drop or call expire on any pre-existing sessions before calling. + /// + /// * `app` - Application layer instance + /// * `send` - Function to be called to send one or more initial packets to the remote being + /// contacted + /// * `mtu` - MTU for initial packets + /// * `static_remote_key` - Remote side's static public NIST P-384 key + /// * `session_data` - Arbitrary data meaningful to the application to include with session + /// object + /// * `identity` - Payload to be sent to Bob that contains the information necessary + /// for the upper protocol to authenticate and approve of Alice's identity. + /// * `ratchet_states` - The set of ratchet states that Alice should use to connect to Bob. + pub fn open_with_ratchet>( &self, app: App, send: impl Sender, @@ -168,6 +225,7 @@ impl Context { static_remote_key: C::PublicKey, session_data: C::SessionData, identity: &[u8], + ratchet_states: RatchetStates, ) -> Result<(Arc>, Option), OpenError> { mtu = mtu.max(MIN_TRANSPORT_MTU); if identity.len() > IDENTITY_MAX_SIZE { @@ -180,6 +238,7 @@ impl Context { static_remote_key, session_data, identity, + ratchet_states, |packet, hk_send| { send_with_fragmentation(send, mtu, packet, hk_send); }, @@ -496,7 +555,7 @@ impl Context { return Err(fault!(InvalidPacket, true)); } - let mut buffer = ArrayVec::::new(); + let mut buffer = ArrayVec::::new(); let assembled_packet = if fragment_count > 1 { let mut next_service_time = self.0.unassociated_defrag_cache.lock().unwrap().assemble( &nonce, @@ -528,9 +587,7 @@ impl Context { if packet_type == PACKET_TYPE_HANDSHAKE_HELLO { log!(app, ReceivedRawX1); - if !(HANDSHAKE_HELLO_CHALLENGE_MIN_SIZE..=HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE) - .contains(&assembled_packet.len()) - { + if HANDSHAKE_HELLO_CHALLENGE_SIZE != assembled_packet.len() { return Err(fault!(InvalidPacket, true)); } // Process recv challenge layer. diff --git a/reference/Cargo.toml b/reference/Cargo.toml index b6438ed..8dd2b22 100644 --- a/reference/Cargo.toml +++ b/reference/Cargo.toml @@ -3,7 +3,7 @@ authors = ["ZeroTier, Inc. ", "Adam Ierymenko for &mut TestApplication { &mut self, remote_static_key: &P384CratePublicKey, session_data: &u128, - update_data: RatchetUpdate<'_>, - ) -> Result<(), std::io::Error> { - self.ratchets.peer_map.insert(*session_data, update_data.to_states()); + update_data: CompareAndSwap<'_>, + ) -> Result { + let mut ratchets = &mut self.ratchets; + match ratchets.peer_map.entry(*session_data) { + Entry::Occupied(mut entry) => { + if update_data.compare(&entry.get()) { + entry.insert(update_data.to_new_states()); + } else { + return Ok(false); + } + } + Entry::Vacant(entry) => { + if update_data.cur_is_initial_states() { + entry.insert(update_data.to_new_states()); + } else { + return Ok(false); + } + } + } if let Some(rf) = update_data.added_fingerprint() { - self.ratchets.rf_map.insert(*rf, update_data.state1.clone()); - println!("[{}] new ratchet #{}", self.name, update_data.state1.chain_len()); + ratchets.rf_map.insert(*rf, update_data.new_state1.clone()); + println!("[{}] new ratchet #{}", self.name, update_data.new_state1.chain_len()); } if let Some(rf) = update_data.deleted_fingerprint1() { - self.ratchets.rf_map.remove(rf); + ratchets.rf_map.remove(rf); } if let Some(rf) = update_data.deleted_fingerprint2() { - self.ratchets.rf_map.remove(rf); + ratchets.rf_map.remove(rf); } - Ok(()) + Ok(true) } fn time(&mut self) -> i64 { @@ -202,7 +219,8 @@ fn alice_main( } } } - } else if OsRng.next_u32() | 1 > 0 { + } else if (OsRng.next_u32() & 1) > 0 { + // There is a 50% chance that a packet will be re-ordered instead of dropped. let _ = recursive_out.send(pkt); } } else { @@ -290,7 +308,8 @@ fn bob_main( } } } - } else if OsRng.next_u32() | 1 > 0 { + } else if (OsRng.next_u32() & 1) > 0 { + // There is a 50% chance that a packet will be re-ordered instead of dropped. let _ = recursive_out.try_send(pkt); } } @@ -377,6 +396,16 @@ fn main() { } #[test] -fn test_main() { - core(2, u32::MAX / 2) +fn test_50() { + core(10, u32::MAX / 2) +} + +#[test] +fn test_75() { + core(10, u32::MAX / 4 * 3) +} + +#[test] +fn test_99() { + core(10, u32::MAX / 100 * 99) } diff --git a/reference/examples/ping_pong.rs b/reference/examples/ping_pong.rs index cb007d7..85b633e 100644 --- a/reference/examples/ping_pong.rs +++ b/reference/examples/ping_pong.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use std::time::Instant; use zssp_proto::application::{ - AcceptAction, ApplicationLayer, CryptoLayer, RatchetState, RatchetStates, RatchetUpdate, RATCHET_SIZE, + AcceptAction, ApplicationLayer, CompareAndSwap, CryptoLayer, RatchetState, RatchetStates, RATCHET_SIZE, }; use zssp_proto::crypto::{rand_core::OsRng, P384KeyPair}; use zssp_proto::crypto_impl::{ @@ -81,9 +81,9 @@ impl ApplicationLayer for &mut MyApp { &mut self, remote_static_key: &P384CratePublicKey, session_data: &(), - update_data: RatchetUpdate<'_>, - ) -> Result<(), std::io::Error> { - Ok(()) + update_data: CompareAndSwap<'_>, + ) -> Result { + Ok(true) } fn time(&mut self) -> i64 { diff --git a/reference/src/application.rs b/reference/src/application.rs index 5758acc..67c0151 100644 --- a/reference/src/application.rs +++ b/reference/src/application.rs @@ -213,11 +213,16 @@ pub trait ApplicationLayer: Sized { remote_static_key: &::PublicKey, session_data: &::SessionData, ) -> Result, std::io::Error>; - /// Atomically commit the update specified by `update_data` to storage, or return an error if - /// the update could not be made. - /// The implementor is free to choose how to apply these updates to storage. + /// Atomically compare-and-swap (a.k.a. compare-exchange) `update` to storage. /// - /// If this returns `Err(IoError)`, the packet which triggered this function to be called will be + /// If `update.cur_state1` and `update.cur_state2` are currently in storage, they must + /// be swapped with `update.new_state1` and `update.new_state2`. + /// Otherwise, storage must remain unchanged. + /// + /// `Ok(true)` must be returned if the compare-and-swap was successful. + /// `Ok(false)` must be returned if comparison failed and the swap was cancelled. + /// + /// If this returns `Err`, 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. /// @@ -229,12 +234,20 @@ pub trait ApplicationLayer: Sized { /// This function may also save state to volatile storage, in which case all peers which connect /// to us will have to allow downgrade across the board. /// Otherwise, when we restart, we will not be allowed to reconnect. + /// + /// # Security + /// Implementations must not perform comparison operations (equals, less than, etc.) directly + /// on two ratchet keys. Instead, comparison operations must be performed indirectly + /// upon their ratchet fingerprints. If two ratchet states have the same ratchet fingerprint, + /// it should be assumed that they also have the same ratchet key. + /// + /// The implementations of `PartialEq` for `RatchetState` and `RatchetStates` do this by default. fn save_ratchet_state( &mut self, remote_static_key: &::PublicKey, session_data: &::SessionData, - update_data: RatchetUpdate<'_>, - ) -> Result<(), std::io::Error>; + update_data: CompareAndSwap<'_>, + ) -> Result; /// Receives a stream of events that occur during an execution of ZSSP. /// These are provided for debugging, logging or metrics purposes, and must be used for diff --git a/reference/src/context.rs b/reference/src/context.rs index e6cbd58..ce4d096 100644 --- a/reference/src/context.rs +++ b/reference/src/context.rs @@ -11,7 +11,7 @@ use crate::challenge::ChallengeContext; use crate::crypto::{AES_256_KEY_SIZE, AES_GCM_NONCE_SIZE}; use crate::fragmentation::{send_with_fragmentation, DefragBuffer}; use crate::proto::*; -use crate::result::{byzantine_fault, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent}; +use crate::result::{fault, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent}; use crate::zeta::*; #[cfg(feature = "logging")] use crate::LogEvent::*; @@ -161,24 +161,25 @@ impl Context { if !matches!(&zeta.beta, ZetaAutomata::A1(_)) { // A resent handshake response from Bob may have arrived out of order, // after we already received one. - return Err(byzantine_fault!(OutOfSequence, false)); + return Err(fault!(OutOfSequence, false)); } if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD { - return Err(byzantine_fault!(ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, true)); } Ok(()) } else if PACKET_TYPE_USES_COUNTER_RANGE.contains(&p) { if !zeta.check_counter_window(c) { + println!("{} {} {}", c, p, kid_recv); // The counter window has finite memory and so will occasionally give // false positives on very out-of-order packets. - return Err(byzantine_fault!(ExpiredCounter, false)); + return Err(fault!(ExpiredCounter, false)); } Ok(()) } else if p == PACKET_TYPE_HANDSHAKE_COMPLETION { // The handshake completion packet could have been resent. - return Err(byzantine_fault!(InvalidPacket, false)); + return Err(fault!(InvalidPacket, false)); } else { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } })?; if let Some((pn, mut assembled_packet)) = result { @@ -295,7 +296,7 @@ impl Context { log!(app, DIsAuthClosedSession(&session)); SessionEvent::Rejected } - _ => return Err(byzantine_fault!(InvalidPacket, true)), // This is unreachable. + _ => return Err(fault!(InvalidPacket, true)), // This is unreachable. }; drop(zeta); Ok(ReceiveOk::Session(session, ret)) @@ -315,7 +316,7 @@ impl Context { if p == PACKET_TYPE_HANDSHAKE_COMPLETION && c == 0 { Ok(()) } else { - Err(byzantine_fault!(InvalidPacket, true)) + Err(fault!(InvalidPacket, true)) } })?; if let Some((_, assembled_packet)) = result { @@ -351,7 +352,7 @@ impl Context { } else { // When sessions are added or dropped or packets arrive extremely delayed it is // possible to receive no longer recognized key ids. - Err(byzantine_fault!(UnknownLocalKeyId, false)) + Err(fault!(UnknownLocalKeyId, false)) } } } else { @@ -365,7 +366,7 @@ impl Context { if p == PACKET_TYPE_HANDSHAKE_HELLO || p == PACKET_TYPE_CHALLENGE { Ok(()) } else { - Err(byzantine_fault!(InvalidPacket, true)) + Err(fault!(InvalidPacket, true)) } }, )?; @@ -394,7 +395,7 @@ impl Context { None, ); // If we issue a challenge the first hello packet will always fail. - return Err(byzantine_fault!(FailedAuth, false)); + return Err(fault!(FailedAuth, false)); } else if let Ok(true) = result { log!(app, X1SucceededChallenge); } @@ -423,7 +424,7 @@ impl Context { log!(app, ReceivedRawChallenge); // Process recv challenge layer. if assembled_packet.len() != KID_SIZE + CHALLENGE_SIZE { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if let Some(kid_recv) = NonZeroU32::new(u32::from_be_bytes(assembled_packet[..KID_SIZE].try_into().unwrap())) @@ -439,9 +440,9 @@ impl Context { return Ok(ReceiveOk::Unassociated); } } - Err(byzantine_fault!(UnknownLocalKeyId, true)) + Err(fault!(UnknownLocalKeyId, true)) } else { - Err(byzantine_fault!(InvalidPacket, true)) + Err(fault!(InvalidPacket, true)) } } else { Ok(ReceiveOk::Unassociated) diff --git a/reference/src/fragmentation.rs b/reference/src/fragmentation.rs index 626fc69..34a847b 100644 --- a/reference/src/fragmentation.rs +++ b/reference/src/fragmentation.rs @@ -7,7 +7,7 @@ use zeroize::Zeroizing; use crate::application::CryptoLayer; use crate::crypto::{Aes256Prp, AES_256_KEY_SIZE}; use crate::proto::*; -use crate::result::{byzantine_fault, ReceiveError}; +use crate::result::{fault, ReceiveError}; /// Corresponds to Figure 13 found in Section 6. fn create_fragment_header( @@ -94,7 +94,7 @@ impl DefragBuffer { ) -> Result)>, ReceiveError> { use crate::result::FaultType::*; if raw_fragment.len() < MIN_PACKET_SIZE { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if let Some(hk_recv) = self.hk_recv.as_ref() { @@ -109,7 +109,7 @@ impl DefragBuffer { let fragment_no = raw_fragment[FRAGMENT_NO_IDX] as usize; let fragment_count = raw_fragment[FRAGMENT_COUNT_IDX] as usize; if fragment_no >= fragment_count || fragment_count > MAX_FRAGMENTS { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } let n = raw_fragment[PACKET_NONCE_START..HEADER_SIZE].try_into().unwrap(); @@ -125,7 +125,7 @@ impl DefragBuffer { || raw_fragment.len() > buffer.fragment_max_size { // Some parts of the protocol can cause duplicate fragments to be sent. - return Err(byzantine_fault!(InvalidPacket, false)); + return Err(fault!(InvalidPacket, false)); } buffer.fragments[fragment_no] = Some(raw_fragment); buffer.total += 1; diff --git a/reference/src/lib.rs b/reference/src/lib.rs index 0d656e0..5467573 100644 --- a/reference/src/lib.rs +++ b/reference/src/lib.rs @@ -50,18 +50,21 @@ mod symmetric_state; mod zeta; /// An abstraction over OS and use-case specific resources and queries. -/// This allows this library to be platform independent, but a user of this library must implement -/// the `ApplicationLayer` trait. +/// This allows this library to be platform independent. +/// A user of this library will need to implement the `ApplicationLayer` trait. pub mod application; /// A collection of implementation-independent traits for the various specific cryptographic /// algorithms ZSSP depends on. /// -/// Each trait is hyper-specific about the semantics of the algorithms and the lengths of their +/// Each trait is very specific about the semantics of the algorithms and the lengths of their /// inputs and outputs. -/// This is to enforced a basic sanity-check upon anyone trying to use their own implementations. +/// This is to enforced a basic sanity-check upon anyone trying to connect this library to +/// someone else's implementation. +/// +/// You should not use this module to implement your own crypto from scratch. /// /// The `crypto_impl` module contains implementations of these traits in terms of popular Rust -/// implementations of these algorithms. +/// crates. pub mod crypto; /// A module containing optional implementations of the ZSSP `crypto` traits in terms of popular /// Rust crates. Some of these crates are not thoroughly audited, so use at your own risk. @@ -72,7 +75,7 @@ pub mod crypto; /// /// This module contains the trait implementations as well as re-exports of those crates. pub mod crypto_impl; -/// The collection of major return types of this library. +/// The collection of the major return types for ZSSP. pub mod result; pub use context::Context; diff --git a/reference/src/proto.rs b/reference/src/proto.rs index 65947e9..4499edc 100644 --- a/reference/src/proto.rs +++ b/reference/src/proto.rs @@ -88,7 +88,7 @@ pub(crate) const HARD_EXPIRATION: u64 = u64::MAX; /// this amount out of order relative to other received counters, it is likely to be /// rejected on the basis that the session can't remember if this counter was replayed. /// Increasing this value makes a session consume more memory. -pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 64; +pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 128; /// Maximum number of counter steps that the counter is allowed to skip ahead. /// This cannot be changed away from 2^24 without changing the header nonce handling code. pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 1 << 24; @@ -112,9 +112,8 @@ pub(crate) const PACKET_TYPE_DATA: u8 = 8; pub(crate) const PACKET_TYPE_CHALLENGE: u8 = 9; pub(crate) const PACKET_TYPE_USES_COUNTER_RANGE: std::ops::Range = 3..9; -pub(crate) const HANDSHAKE_HELLO_MIN_SIZE: usize = - KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE; -pub(crate) const HANDSHAKE_HELLO_MAX_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + RATCHET_SIZE + RATCHET_SIZE; +pub(crate) const HANDSHAKE_HELLO_SIZE: usize = + KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + 2 * RATCHET_SIZE + AES_GCM_TAG_SIZE; pub(crate) const HANDSHAKE_RESPONSE_SIZE: usize = P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE; diff --git a/reference/src/ratchet_state.rs b/reference/src/ratchet_state.rs index fc2c85d..204852e 100644 --- a/reference/src/ratchet_state.rs +++ b/reference/src/ratchet_state.rs @@ -12,31 +12,24 @@ use crate::proto::*; /// Corresponds to the Ratchet Key and Ratchet Fingerprint described in Section 3. #[derive(Clone, Eq)] pub struct RatchetState { - key: Zeroizing<[u8; RATCHET_SIZE]>, - fingerprint: Option>, - chain_len: u64, + pub(crate) key: Zeroizing<[u8; RATCHET_SIZE]>, + pub(crate) fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, + pub(crate) chain_len: u64, } impl PartialEq for RatchetState { fn eq(&self, other: &Self) -> bool { - let ret = match (self.fingerprint.as_ref(), other.fingerprint.as_ref()) { - (Some(rf1), Some(rf2)) => secure_eq(rf1, rf2), - (None, None) => true, - _ => false, - }; - ret & secure_eq(&self.key, &other.key) & (self.chain_len == other.chain_len) + self.fingerprint.eq(&other.fingerprint) & (self.chain_len == other.chain_len) } } impl std::hash::Hash for RatchetState { fn hash(&self, state: &mut H) { - if let Some(rf) = &self.fingerprint { - state.write_u64(u64::from_ne_bytes(rf[..8].try_into().unwrap())) - } + state.write(self.fingerprint.as_ref()) } } impl RatchetState { /// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length. pub fn new(key: Zeroizing<[u8; RATCHET_SIZE]>, fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, chain_len: u64) -> Self { - RatchetState { key, fingerprint: Some(fingerprint), chain_len } + RatchetState { key, fingerprint, chain_len } } /// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length. /// @@ -45,28 +38,10 @@ impl RatchetState { pub fn new_raw(key: [u8; RATCHET_SIZE], fingerprint: [u8; RATCHET_SIZE], chain_len: u64) -> Self { RatchetState { key: Zeroizing::new(key), - fingerprint: Some(Zeroizing::new(fingerprint)), + fingerprint: Zeroizing::new(fingerprint), chain_len, } } - /// The ratchet key for this ratchet state. This is directly mixed into the master secret of a - /// session and so is very sensitive. All operations upon a ratchet key must be implemented - /// in constant time. The user should prefer to do nothing with the ratchet key besides copying - /// it to or from a storage device. - /// - /// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be - /// all zeros. - pub fn key(&self) -> &[u8; RATCHET_SIZE] { - &self.key - } - /// Ratchet keys and fingerprints are "chained together", where each set is derived from the - /// previous set. - /// - /// This function outputs the total length of that chain, as in the total number of previous - /// ratchet states that this ratchet state was derived from. - pub fn chain_len(&self) -> u64 { - self.chain_len - } /// Creates a new "empty" ratchet state, where the ratchet fingerprint is the /// empty string, the ratchet key is all zeros, and the chain length is 0. /// @@ -74,7 +49,7 @@ impl RatchetState { pub fn empty() -> Self { RatchetState { key: Zeroizing::new([0u8; RATCHET_SIZE]), - fingerprint: None, + fingerprint: Zeroizing::new([0u8; RATCHET_SIZE]), chain_len: 0, } } @@ -84,7 +59,7 @@ impl RatchetState { pub fn new_from_otp(otp: &[u8]) -> RatchetState { let mut buffer = Vec::new(); buffer.push(1); - buffer.extend(LABEL_OTP_TO_RATCHET); + buffer.extend(*LABEL_OTP_TO_RATCHET); buffer.push(0x00); buffer.extend((1024u16).to_be_bytes()); let r1 = Hmac::hmac(otp, &buffer); @@ -96,15 +71,15 @@ impl RatchetState { 1, ) } - /// Returns true if this is the "empty" ratchet state, where the ratchet fingerprint is the - /// empty string, the ratchet key is all zeros, and the chain length is 0. - pub fn is_empty(&self) -> bool { - self.fingerprint.is_none() - } - /// Checks if the fingerprint of this ratchet state equals the fingerprint contained in argument - /// `rf`. Uses constant time equality. - pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { - self.fingerprint.as_ref().map_or(false, |rf0| secure_eq(rf0, rf)) + /// The ratchet key for this ratchet state. This is directly mixed into the master secret of a + /// session and so is very sensitive. All operations upon a ratchet key must be implemented + /// in constant time. The user should prefer to do nothing with the ratchet key besides copying + /// it to or from a storage device. + /// + /// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be + /// all zeros. + pub fn key(&self) -> &[u8; RATCHET_SIZE] { + &self.key } /// The ratchet fingerprint for this ratchet state. /// @@ -115,8 +90,43 @@ impl RatchetState { /// but the security of ZSSP can survive having this value leaked. /// Operations on a ratchet fingerprint should be implemented in constant time, /// but it is ok if they are not. - pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - self.fingerprint.as_deref() + pub fn fingerprint(&self) -> &[u8; RATCHET_SIZE] { + &self.fingerprint + } + /// Ratchet keys and fingerprints are "chained together", where each set is derived from the + /// previous set. + /// + /// This function outputs the total length of that chain, as in the total number of previous + /// ratchet states that this ratchet state was derived from. + pub fn chain_len(&self) -> u64 { + self.chain_len + } + /// Returns true if this is the "empty" ratchet state, where the ratchet fingerprint is the + /// empty string, the ratchet key is all zeros, and the chain length is 0. + pub fn is_empty(&self) -> bool { + secure_eq(self.fingerprint(), &[0u8; RATCHET_SIZE]) + } + /// Checks if the fingerprint of this ratchet state equals the + /// fingerprint contained in argument `rf`. + /// + /// Uses constant time equality. + pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { + secure_eq(self.fingerprint(), rf) + } + /// Returns true if the fingerprint of argument `this` equals the fingerprint represented by + /// argument `rf`. + /// If `this` is `None`, `this` is considered to be "null", as in a ratchet state that does not + /// exist or has been deleted. + /// If `rf` is `None`, `rf` is considered to be "null" as well. + /// If `rf` is `Some(None)`, `rf` is considered to be the empty ratchet fingerprint. + /// + /// Uses constant time equality. + pub fn fingerprint_eq_nullable(this: Option<&Self>, rf: Option<&[u8; RATCHET_SIZE]>) -> bool { + match (this, rf) { + (Some(this), Some(rf)) => this.fingerprint_eq(rf), + (None, None) => true, + _ => false, + } } } impl Default for RatchetState { @@ -134,7 +144,8 @@ impl Default for RatchetState { pub struct RatchetStates { /// The first ratchet state from the pair. pub state1: RatchetState, - /// The second ratchet state from the pair. It can, and usually will be `None`. + /// The second ratchet state from the pair. + /// It can, and usually will be `None`, which means that this ratchet state is "null". pub state2: Option, } impl RatchetStates { @@ -169,7 +180,6 @@ impl Default for RatchetStates { Self::new_initial_states() } } - /// A set of references to ratchet states specifying how a remote peer's persistent storage should /// be updated. This struct is designed to provide any and all potentially needed data for /// maintaining a store of these ratchet states. It should be straightforward to commit these updates @@ -178,69 +188,136 @@ impl Default for RatchetStates { /// There will only be up to two ratchet states saved to storage at a time per peer. /// Every time a third ratchet state is generated, a previous ratchet state will be deleted. /// -/// These are sensitive values should they ought to be securely stored, with restricted read-write -/// permissions if stored on disk. +/// As the name implies, these updates should be applied as one atomic compare-and-swap operation. +/// If the two ratchet states currently in storage equal `cur_state1` and `cur_state2`, +/// then `new_state1` and `new_state2` should overwrite them. +/// If not, then storage must not be modified. /// -/// To prevent desync or resource leakage, these updates should be committed atomically. If that is -/// not possible, then new ratchet states should be written before old ratchet states are deleted -#[derive(Clone, Copy)] -pub struct RatchetUpdate<'a> { - /// The ratchet key and fingerprint to store in the first slot. - pub state1: &'a RatchetState, - /// The ratchet key and fingerprint to store in the second slot. - pub state2: Option<&'a RatchetState>, - /// Whether `state1` is a brand new ratchet state, or if it was previously saved. - pub state1_was_just_added: bool, - /// A previous ratchet key and fingerprint that now must be deleted from storage. - /// This will have been a previously given value of `state1` or `state2`. - pub deleted_state1: Option<&'a RatchetState>, - /// A previous ratchet key and fingerprint that now must be deleted from storage. - /// It is extremely rare that this field is occupied. - pub deleted_state2: Option<&'a RatchetState>, +/// Ratchet keys must never be checked for equality. Instead, if two ratchet states have equal +/// ratchet fingerprints, it should be assumed that they also have equal ratchet keys. +/// This policy exists to reduce the security impact of timing and other side-channel attacks. +/// +/// These are sensitive values and ought to be securely stored, with restricted read-write +/// permissions if stored on disk. +#[derive(Clone)] +pub struct CompareAndSwap<'a> { + /// The ratchet state to store in the first slot. + pub new_state1: &'a RatchetState, + /// The ratchet state to store in the second slot. + /// A value of `None` implies that the second slot should be set to null. + pub new_state2: Option<&'a RatchetState>, + /// This field is `true` if and only if `new_state1 != cur_state1` and `new_state1 != cur_state2`. + /// + /// This implies whether `state1` is a brand new ratchet state, or if it was previously saved. + pub new_state1_was_just_added: bool, + /// The ratchet state that we expect to see in the first slot. + /// If this value is not currently stored in the first slot, the entire update must be aborted. + pub cur_state1: &'a RatchetState, + /// The ratchet state that we expect to see in the second slot. + /// A value of `None` implies we expect the second slot to be set to "null". + /// + /// If this value is not currently stored in the second slot, the entire update must be aborted. + pub cur_state2: Option<&'a RatchetState>, + /// This field is `true` if and only if `cur_state1 != new_state1` and `cur_state1 != new_state2`. + pub cur_state1_was_just_deleted: bool, + /// This field is `true` if and only if `cur_state2 != new_state1` and `cur_state2 != new_state2`. + pub cur_state2_was_just_deleted: bool, } -impl<'a> RatchetUpdate<'a> { - /// Returns the final `RatchetStates` that should be the only thing saved after this update is - /// fully committed. Future calls to `ApplicationLayer::restore_by_identity` should return this struct. - pub fn to_states(&self) -> RatchetStates { - RatchetStates::new(self.state1.clone(), self.state2.cloned()) +impl<'a> CompareAndSwap<'a> { + pub(crate) fn new( + new_state1: &'a RatchetState, + new_state2: Option<&'a RatchetState>, + new_state1_was_just_added: bool, + cur_state1: &'a RatchetState, + cur_state2: Option<&'a RatchetState>, + cur_state1_was_just_deleted: bool, + cur_state2_was_just_deleted: bool, + ) -> Self { + Self { + new_state1, + new_state2, + new_state1_was_just_added, + cur_state1, + cur_state2, + cur_state1_was_just_deleted, + cur_state2_was_just_deleted, + } + } + /// Returns the final `RatchetStates` that must be swapped into storage if this update is + /// fully committed. + /// Future calls to `ApplicationLayer::restore_by_identity` should return this struct. + pub fn to_new_states(&self) -> RatchetStates { + RatchetStates::new(self.new_state1.clone(), self.new_state2.cloned()) + } + /// Returns the `RatchetStates` that is expected to currently be in storage for this peer. + /// This value must be compared with the current value in storage, and if they are equal, + /// the return value of `CompareAndSwap::to_new_states` must overwrite it. + pub fn to_cur_states(&self) -> RatchetStates { + RatchetStates::new(self.cur_state1.clone(), self.cur_state2.cloned()) } /// If this update specifies adding a brand new ratchet fingerprint, this function will return it. /// The returned ratchet fingerprint will always be the ratchet fingerprint of field `state1`. /// - /// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be true. + /// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be `true`. pub fn added_fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - if self.state1_was_just_added { - self.state1.fingerprint() - } else { - None - } + self.new_state1_was_just_added.then_some(self.new_state1.fingerprint()) } - /// If this updated specifies to delete an old ratchet fingerprint, this function will return it. - /// The returned ratchet fingerprint will always be the ratchet fingerprint of field - /// `deleted_state1`. + /// If this updated specifies to delete `cur_state1`, and `cur_state1` has a non-zero ratchet + /// fingerprint, this function will return the ratchet fingerprint. + /// + /// If `cur_state1` is not being deleted (i.e. `cur_state1 == new_state1` or + /// `cur_state1 == new_state2`), then this will return `None`. /// /// There may be a second ratchet fingerprint to be deleted, which function /// `deleted_fingerprint2` will return. pub fn deleted_fingerprint1(&self) -> Option<&[u8; RATCHET_SIZE]> { - if let Some(rs) = &self.deleted_state1 { - rs.fingerprint() - } else { - None + if self.cur_state1_was_just_deleted && !self.cur_state1.is_empty() { + return Some(self.cur_state1.fingerprint()); } + None } - /// If this updated specifies to delete two ratchet fingerprints, this function will return the - /// second one. `deleted_fingerprint1` will return the first one. - /// The returned ratchet fingerprint will always be the ratchet fingerprint of field - /// `deleted_state2`. + /// If this updated specifies to delete `cur_state2`, and `cur_state2` has a non-zero ratchet + /// fingerprint, this function will return the ratchet fingerprint. /// - /// It is exceptionally rare that there will be more than one ratchet fingerprint to be deleted. - /// Care should be taken to make sure that this update will still be correctly committed in the - /// rare event that this returns `Some`. + /// If `cur_state2` is not being deleted (i.e. `cur_state2 == new_state1` or + /// `cur_state2 == new_state2`), then this will return `None`. + /// + /// There may be a second ratchet fingerprint to be deleted, which function + /// `deleted_fingerprint1` will return. pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> { - if let Some(rs) = &self.deleted_state2 { - rs.fingerprint() - } else { - None + if self.cur_state2_was_just_deleted { + if let Some(rf) = self.cur_state2 { + if !rf.is_empty() { + return Some(rf.fingerprint()); + } + } } + None + } + /// Returns true if the currently stored ratchet states are expected to be the initial ratchet + /// states. This is the default value for a peer's ratchet states in the event they could not + /// be found in storage. + /// + /// If a peer could not be found in storage, and this function returns true, + /// then the peer should be added to storage with the new ratchet states specified by this + /// `CompareAndSwap` struct. + pub fn cur_is_initial_states(&self) -> bool { + self.cur_state1.is_empty() && self.cur_state2.is_none() + } + /// Compares the ratchet fingerprints of `cur_state1` and `cur_state2` with `rf1` and `rf2`. + /// If they are equal this function will return `true`. + /// + /// If this function returns `true`, then the implementation may proceed to swap out the ratchet + /// states these fingerprints come from with `new_state1` and `new_state2`. + pub fn compare_fingerprints(&self, rf1: &[u8; RATCHET_SIZE], rf2: Option<&[u8; RATCHET_SIZE]>) -> bool { + self.cur_state1.fingerprint_eq(rf1) & RatchetState::fingerprint_eq_nullable(self.cur_state2, rf2) + } + /// Compares `cur_state1` and `cur_state2` with `other.state1` and `other.state2`. + /// If they are equal this function will return `true`. + /// + /// If this function returns `true`, then the implementation may proceed to swap `other.state1` + /// and `other.state2` with `new_state1` and `new_state2`. + pub fn compare(&self, other: &RatchetStates) -> bool { + self.cur_state1.eq(&other.state1) & self.cur_state2.eq(&other.state2.as_ref()) } } diff --git a/reference/src/result.rs b/reference/src/result.rs index 4fa54f3..380f15d 100644 --- a/reference/src/result.rs +++ b/reference/src/result.rs @@ -166,7 +166,7 @@ pub enum ReceiveError { WriteError(std::io::Error), } -macro_rules! byzantine_fault { +macro_rules! fault { ($name:expr, $unnatural:ident) => { ReceiveError::ByzantineFault(crate::result::ByzantineFault { #[cfg(feature = "debug")] @@ -178,7 +178,7 @@ macro_rules! byzantine_fault { }) }; } -pub(crate) use byzantine_fault; +pub(crate) use fault; /// Result generated by the context packet receive function, with possible payloads. #[derive(Clone)] diff --git a/reference/src/zeta.rs b/reference/src/zeta.rs index 3c333db..776e194 100644 --- a/reference/src/zeta.rs +++ b/reference/src/zeta.rs @@ -7,13 +7,13 @@ use std::sync::{Arc, Weak}; use rand_core::RngCore; use zeroize::Zeroizing; -use crate::application::{ApplicationLayer, CryptoLayer, RatchetState, RatchetStates, RatchetUpdate}; +use crate::application::{ApplicationLayer, CompareAndSwap, CryptoLayer, RatchetState, RatchetStates}; use crate::challenge::{gen_null_response, respond_to_challenge_in_place}; use crate::context::{log, ContextInner, SessionMap}; use crate::crypto::*; use crate::fragmentation::DefragBuffer; use crate::proto::*; -use crate::result::{byzantine_fault, FaultType, OpenError, ReceiveError, SendError}; +use crate::result::{fault, FaultType, OpenError, ReceiveError, SendError}; use crate::symmetric_state::SymmetricState; #[cfg(feature = "logging")] use crate::LogEvent::*; @@ -115,7 +115,7 @@ impl SymmetricState { self.mix_key(&pub_key); e_secret } - fn read_e(&mut self, i: &mut usize, packet: &Vec) -> Option { + fn read_e(&mut self, i: &mut usize, packet: &[u8]) -> Option { let j = *i + P384_PUBLIC_KEY_SIZE; let pub_key = &packet[*i..j]; self.mix_hash(pub_key); @@ -265,12 +265,8 @@ fn create_a1_state( noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 0), i, &mut x1); // Process message pattern 1 payload. let i = x1.len(); - if let Some(rf) = ratchet_state1.fingerprint() { - x1.extend(rf.as_ref()); - } - if let Some(Some(rf)) = ratchet_state2.map(|rs| rs.fingerprint()) { - x1.extend(rf.as_ref()); - } + x1.extend(ratchet_state1.fingerprint()); + x1.extend(ratchet_state2.map_or(&[0u8; RATCHET_SIZE], |rs| rs.fingerprint())); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), i, &mut x1); let c = u64::from_be_bytes(x1[x1.len() - 8..].try_into().unwrap()); @@ -365,23 +361,23 @@ pub(crate) fn received_x1_trans>( // ... // -> e, es, e1 // <- e, ee, ekem1, psk - if !(HANDSHAKE_HELLO_MIN_SIZE..=HANDSHAKE_HELLO_MAX_SIZE).contains(&x1.len()) { - return Err(byzantine_fault!(InvalidPacket, true)); + if HANDSHAKE_HELLO_SIZE != x1.len() { + return Err(fault!(InvalidPacket, true)); } if n[AES_GCM_NONCE_SIZE - 8..] != x1[x1.len() - 8..] { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); let mut i = 0; // Noise process prologue. let j = i + KID_SIZE; noise.mix_hash(&x1[i..j]); - let kid_send = NonZeroU32::new(u32::from_be_bytes(x1[i..j].try_into().unwrap())) - .ok_or(byzantine_fault!(InvalidPacket, true))?; + let kid_send = + NonZeroU32::new(u32::from_be_bytes(x1[i..j].try_into().unwrap())).ok_or(fault!(InvalidPacket, true))?; noise.mix_hash(&ctx.s_secret.public_key_bytes()); i = j; // Process message pattern 1 e token. - let e_remote = noise.read_e(&mut i, &x1).ok_or(byzantine_fault!(FailedAuth, true))?; + let e_remote = noise.read_e(&mut i, &x1).ok_or(fault!(FailedAuth, true))?; // Process message pattern 1 es token. noise.mix_dh(&ctx.s_secret, &e_remote); // Process message pattern 1 e1 token. @@ -389,40 +385,50 @@ pub(crate) fn received_x1_trans>( let k = j + AES_GCM_TAG_SIZE; let tag = x1[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 0), &mut x1[i..j], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let e1_start = i; let e1_end = j; i = k; // Process message pattern 1 payload. - let k = x1.len(); - let j = k - AES_GCM_TAG_SIZE; + let j = i + RATCHET_SIZE + RATCHET_SIZE; + let k = j + AES_GCM_TAG_SIZE; let tag = x1[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), &mut x1[i..j], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } + debug_assert_eq!(k, x1.len()); + let rf1 = &x1[i..i + RATCHET_SIZE]; + let rf2 = &x1[i + RATCHET_SIZE..j]; let mut ratchet_state = None; - while i + RATCHET_SIZE <= j { - let rf = (&x1[i..i + RATCHET_SIZE]).try_into().unwrap(); - match app.restore_by_fingerprint(rf) { + if !secure_eq(rf1, &[0u8; RATCHET_SIZE]) { + match app.restore_by_fingerprint(rf1.try_into().unwrap()) { Ok(None) => {} Ok(Some(rs)) => { ratchet_state = Some(rs); - break; } Err(e) => return Err(ReceiveError::StorageError(e)), } - i += RATCHET_SIZE; } - let ratchet_state = if let Some(rs) = ratchet_state { - rs - } else { - if app.hello_requires_recognized_ratchet() { - return Err(byzantine_fault!(FailedAuth, true)); + if ratchet_state.is_none() { + if !secure_eq(rf2, &[0u8; RATCHET_SIZE]) { + match app.restore_by_fingerprint(rf1.try_into().unwrap()) { + Ok(None) => {} + Ok(Some(rs)) => { + ratchet_state = Some(rs); + } + Err(e) => return Err(ReceiveError::StorageError(e)), + } } - RatchetState::empty() - }; + if ratchet_state.is_none() && app.hello_requires_recognized_ratchet() { + return Err(fault!(FailedAuth, true)); + } + } + // If we get to this point and haven't found a full ratchet state, + // set it to the empty ratchet state. + let ratchet_state = ratchet_state.unwrap_or_default(); + let (hk_send, hk_recv) = noise.get_ask(LABEL_HEADER_KEY); let mut x2 = Vec::new(); @@ -437,7 +443,7 @@ pub(crate) fn received_x1_trans>( (&x1[e1_start..e1_end]).try_into().unwrap(), ) .map(|(ct, secret)| (ct, Zeroizing::new(secret))) - .ok_or(byzantine_fault!(FailedAuth, true))?; + .ok_or(fault!(FailedAuth, true))?; x2.extend(ekem1); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), i, &mut x2); noise.mix_key(ekem1_secret.as_ref()); @@ -493,14 +499,14 @@ pub(crate) fn received_x2_trans>( // <- e, ee, ekem1, psk // -> s, se if HANDSHAKE_RESPONSE_SIZE != x2.len() { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if Some(kid) != zeta.key_ref(true).recv.kid { - return Err(byzantine_fault!(UnknownLocalKeyId, true)); + return Err(fault!(UnknownLocalKeyId, true)); } let (_, c) = from_nonce(&n); if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || n[AES_GCM_NONCE_SIZE - 3..] != x2[x2.len() - 3..] { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let mut should_warn_missing_ratchet = false; let result = (|| { @@ -508,7 +514,7 @@ pub(crate) fn received_x2_trans>( let mut noise = noise.clone(); let mut i = 0; // Process message pattern 2 e token. - let e_remote = noise.read_e(&mut i, &x2).ok_or(byzantine_fault!(FailedAuth, true))?; + let e_remote = noise.read_e(&mut i, &x2).ok_or(fault!(FailedAuth, true))?; // Process message pattern 2 ee token. noise.mix_dh(e_secret, &e_remote); // Process message pattern 2 ekem1 token. @@ -516,12 +522,12 @@ pub(crate) fn received_x2_trans>( let k = j + AES_GCM_TAG_SIZE; let tag = x2[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), &mut x2[i..j], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let ekem1_secret = e1_secret .decapsulate((&x2[i..j]).try_into().unwrap()) .map(Zeroizing::new) - .ok_or(byzantine_fault!(FailedAuth, true))?; + .ok_or(fault!(FailedAuth, true))?; noise.mix_key(ekem1_secret.as_ref()); drop(ekem1_secret); i = k; @@ -568,7 +574,7 @@ pub(crate) fn received_x2_trans>( } } - let (kid_send, mut noise) = result.ok_or(byzantine_fault!(FailedAuth, true))?; + let (kid_send, mut noise) = result.ok_or(fault!(FailedAuth, true))?; let mut x3 = Vec::new(); // Process message pattern 3 s token. @@ -585,24 +591,26 @@ pub(crate) fn received_x2_trans>( let (rk, rf) = noise.get_ask(LABEL_RATCHET_STATE); let new_ratchet_state = RatchetState::new(rk, rf, chain_len + 1); - let (ratchet_to_preserve, ratchet_to_delete) = if ratchet_i == 1 { - (Some(&zeta.ratchet_state1), zeta.ratchet_state2.as_ref()) + let ratchet_to_preserve = if ratchet_i == 1 { + Some(&zeta.ratchet_state1) } else { - (zeta.ratchet_state2.as_ref(), Some(&zeta.ratchet_state1)) + zeta.ratchet_state2.as_ref() }; let result = app.save_ratchet_state( &zeta.s_remote, &zeta.session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: ratchet_to_preserve, - state1_was_just_added: true, - deleted_state1: ratchet_to_delete, - deleted_state2: None, - }, + CompareAndSwap::new( + &new_ratchet_state, + ratchet_to_preserve, + true, + &zeta.ratchet_state1, + zeta.ratchet_state2.as_ref(), + ratchet_i == 2, + ratchet_i == 1, + ), ); - if let Err(e) = result { - return Err(ReceiveError::StorageError(e)); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true)); } let (kek_recv, kek_send) = noise.get_ask(LABEL_KEX_KEY); @@ -626,7 +634,7 @@ pub(crate) fn received_x2_trans>( Ok(packet) } else { - Err(byzantine_fault!(FailedAuth, true)) + Err(fault!(FailedAuth, true)) } })(); match &result { @@ -672,10 +680,10 @@ pub(crate) fn received_x3_trans>( use FaultType::*; // -> s, se if !(HANDSHAKE_COMPLETION_MIN_SIZE..=HANDSHAKE_COMPLETION_MAX_SIZE).contains(&x3.len()) { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if kid != zeta.kid_recv { - return Err(byzantine_fault!(UnknownLocalKeyId, true)); + return Err(fault!(UnknownLocalKeyId, true)); } let mut noise = zeta.noise.clone(); @@ -685,10 +693,9 @@ pub(crate) fn received_x3_trans>( let k = j + AES_GCM_TAG_SIZE; let tag = x3[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 1), &mut x3[i..j], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } - let s_remote = - C::PublicKey::from_bytes((&x3[i..j]).try_into().unwrap()).ok_or(byzantine_fault!(FailedAuth, true))?; + let s_remote = C::PublicKey::from_bytes((&x3[i..j]).try_into().unwrap()).ok_or(fault!(FailedAuth, true))?; i = k; // Process message pattern 3 se token. noise.mix_dh(&zeta.e_secret, &s_remote); @@ -697,7 +704,7 @@ pub(crate) fn received_x3_trans>( let j = k - AES_GCM_TAG_SIZE; let tag = x3[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 0), &mut x3[i..j], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let identity_start = i; let identity_end = j; @@ -727,13 +734,13 @@ pub(crate) fn received_x3_trans>( let mut should_warn_missing_ratchet = false; if (zeta.ratchet_state != state1) & (Some(&zeta.ratchet_state) != state2.as_ref()) { - if !responder_disallows_downgrade && zeta.ratchet_state.fingerprint().is_none() { + if !responder_disallows_downgrade && zeta.ratchet_state.is_empty() { should_warn_missing_ratchet = true; } else { if !responder_silently_rejects { send(&create_reject(), Some(&zeta.hk_send)) } - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } } @@ -743,16 +750,10 @@ pub(crate) fn received_x3_trans>( let result = app.save_ratchet_state( &s_remote, &session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: None, - state1_was_just_added: true, - deleted_state1: Some(&state1), - deleted_state2: state2.as_ref(), - }, + CompareAndSwap::new(&new_ratchet_state, None, true, &state1, state2.as_ref(), true, true), ); - if let Err(e) = result { - return Err(ReceiveError::StorageError(e)); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true)); } let mut c1 = Vec::new(); @@ -772,7 +773,7 @@ pub(crate) fn received_x3_trans>( let entry = match session_map.entry(zeta.kid_recv) { // We could have issued the kid that we initially offered Alice to someone else // before Alice was able to respond. It is unlikely but possible. - Occupied(_) => return Err(byzantine_fault!(OutOfSequence, false)), + Occupied(_) => return Err(fault!(OutOfSequence, false)), Vacant(entry) => entry, }; let session = Arc::new(Session(RefCell::new(Zeta { @@ -807,7 +808,7 @@ pub(crate) fn received_x3_trans>( if !responder_silently_rejects { send(&create_reject(), Some(&zeta.hk_send)) } - Err(byzantine_fault!(FailedAuth, true)) + Err(fault!(FailedAuth, true)) } } /// Corresponds to Transition Algorithm 5 found in Section 4.3. @@ -823,7 +824,7 @@ pub(crate) fn received_c1_trans>( use FaultType::*; if c1.len() != KEY_CONFIRMATION_SIZE { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } let is_other = if Some(kid) == zeta.key_ref(true).recv.kid { true @@ -832,7 +833,7 @@ pub(crate) fn received_c1_trans>( } else { // Some key confirmation may have arrived extremely delayed. // It is unlikely but possible. - return Err(byzantine_fault!(OutOfSequence, false)); + return Err(fault!(OutOfSequence, false)); }; let specified_key = zeta @@ -840,14 +841,14 @@ pub(crate) fn received_c1_trans>( .recv .kek .as_ref() - .ok_or(byzantine_fault!(OutOfSequence, true))?; + .ok_or(fault!(OutOfSequence, true))?; let tag = c1[..].try_into().unwrap(); if !C::Aead::decrypt_in_place(specified_key, &n, None, &mut [], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); if !zeta.update_counter_window(c) { - return Err(byzantine_fault!(ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, true)); } let just_establised = is_other && matches!(&zeta.beta, ZetaAutomata::A3 { .. }); @@ -857,16 +858,19 @@ pub(crate) fn received_c1_trans>( let result = app.save_ratchet_state( &zeta.s_remote, &zeta.session_data, - RatchetUpdate { - state1: &zeta.ratchet_state1, - state2: None, - state1_was_just_added: false, - deleted_state1: zeta.ratchet_state2.as_ref(), - deleted_state2: None, - }, + CompareAndSwap::new( + &zeta.ratchet_state1, + None, + false, + &zeta.ratchet_state1, + zeta.ratchet_state2.as_ref(), + false, + true, + ), ); - if let Err(e) = result { - return Err(ReceiveError::StorageError(e)); + if !result.map_err(ReceiveError::StorageError)? { + zeta.expire(); + return Err(fault!(OutOfSequence, true)); } } @@ -880,7 +884,7 @@ pub(crate) fn received_c1_trans>( } let c2 = Vec::new(); if !send_control::(zeta, PACKET_TYPE_ACK, c2, send) { - return Err(byzantine_fault!(OutOfSequence, true)); + return Err(fault!(OutOfSequence, true)); } Ok(just_establised) @@ -898,24 +902,24 @@ pub(crate) fn received_c2_trans>( use FaultType::*; if c2.len() != ACKNOWLEDGEMENT_SIZE { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if Some(kid) != zeta.key_ref(false).recv.kid { // Some acknowledgement may have arrived extremely delayed. - return Err(byzantine_fault!(UnknownLocalKeyId, false)); + return Err(fault!(UnknownLocalKeyId, false)); } if !matches!(&zeta.beta, ZetaAutomata::S1) { // Some acknowledgement may have arrived extremely delayed. - return Err(byzantine_fault!(OutOfSequence, false)); + return Err(fault!(OutOfSequence, false)); } let tag = c2[..].try_into().unwrap(); if !C::Aead::decrypt_in_place(zeta.key_ref(false).recv.kek.as_ref().unwrap(), &n, None, &mut [], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); if !zeta.update_counter_window(c) { - return Err(byzantine_fault!(ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, true)); } let r = rng.borrow_mut().next_u64() % C::SETTINGS.rekey_time_max_jitter; @@ -935,19 +939,19 @@ pub(crate) fn received_d_trans( use FaultType::*; if d.len() != SESSION_REJECTED_SIZE { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if Some(kid) != zeta.key_ref(true).recv.kid || !matches!(&zeta.beta, ZetaAutomata::A3 { .. }) { - return Err(byzantine_fault!(OutOfSequence, true)); + return Err(fault!(OutOfSequence, true)); } let tag = d[..].try_into().unwrap(); if !C::Aead::decrypt_in_place(zeta.key_ref(true).recv.kek.as_ref().unwrap(), &n, None, &mut [], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); if !zeta.update_counter_window(c) { - return Err(byzantine_fault!(ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, true)); } zeta.expire(); @@ -1103,11 +1107,11 @@ pub(crate) fn received_k1_trans>( // -> psk, e, es, ss // <- e, ee, se if k1.len() != REKEY_SIZE { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if Some(kid) != zeta.key_ref(false).recv.kid { // Some rekey packet may have arrived extremely delayed. - return Err(byzantine_fault!(UnknownLocalKeyId, false)); + return Err(fault!(UnknownLocalKeyId, false)); } let should_rekey_as_bob = match &zeta.beta { ZetaAutomata::S2 { .. } => true, @@ -1116,7 +1120,7 @@ pub(crate) fn received_k1_trans>( }; if !should_rekey_as_bob { // Some rekey packet may have arrived extremely delayed. - return Err(byzantine_fault!(OutOfSequence, false)); + return Err(fault!(OutOfSequence, false)); } let i = k1.len() - AES_GCM_TAG_SIZE; @@ -1128,11 +1132,11 @@ pub(crate) fn received_k1_trans>( &mut k1[..i], &tag, ) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); if !zeta.update_counter_window(c) { - return Err(byzantine_fault!(ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, true)); } k1.truncate(i); @@ -1145,7 +1149,7 @@ pub(crate) fn received_k1_trans>( // Process message pattern 1 psk0 token. noise.mix_key_and_hash(zeta.ratchet_state1.key()); // Process message pattern 1 e token. - let e_remote = noise.read_e(&mut i, &k1).ok_or(byzantine_fault!(FailedAuth, true))?; + let e_remote = noise.read_e(&mut i, &k1).ok_or(fault!(FailedAuth, true))?; // Process message pattern 1 es token. noise.mix_dh(s_secret, &e_remote); // Process message pattern 1 ss token. @@ -1155,10 +1159,10 @@ pub(crate) fn received_k1_trans>( let k = j + AES_GCM_TAG_SIZE; let tag = k1[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_REKEY_INIT, 0), &mut k1[i..j], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } - let kid_send = NonZeroU32::new(u32::from_be_bytes(k1[i..j].try_into().unwrap())) - .ok_or(byzantine_fault!(FailedAuth, true))?; + let kid_send = + NonZeroU32::new(u32::from_be_bytes(k1[i..j].try_into().unwrap())).ok_or(fault!(FailedAuth, true))?; let mut k2 = Vec::new(); // Process message pattern 2 e token. @@ -1178,16 +1182,18 @@ pub(crate) fn received_k1_trans>( let result = app.save_ratchet_state( &zeta.s_remote, &zeta.session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: Some(&zeta.ratchet_state1), - state1_was_just_added: true, - deleted_state1: zeta.ratchet_state2.as_ref(), - deleted_state2: None, - }, + CompareAndSwap::new( + &new_ratchet_state, + Some(&zeta.ratchet_state1), + true, + &zeta.ratchet_state1, + zeta.ratchet_state2.as_ref(), + false, + true, + ), ); - if let Err(e) = result { - return Err(ReceiveError::StorageError(e)); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true)); } let (kek_send, kek_recv) = noise.get_ask(LABEL_KEX_KEY); let (nk_send, nk_recv) = noise.split(); @@ -1226,15 +1232,15 @@ pub(crate) fn received_k2_trans>( use FaultType::*; // <- e, ee, se if k2.len() != REKEY_SIZE { - return Err(byzantine_fault!(InvalidPacket, true)); + return Err(fault!(InvalidPacket, true)); } if Some(kid) != zeta.key_ref(false).recv.kid { // Some rekey packet may have arrived extremely delayed. - return Err(byzantine_fault!(UnknownLocalKeyId, false)); + return Err(fault!(UnknownLocalKeyId, false)); } if !matches!(&zeta.beta, ZetaAutomata::R1 { .. }) { // Some rekey packet may have arrived extremely delayed. - return Err(byzantine_fault!(OutOfSequence, false)); + return Err(fault!(OutOfSequence, false)); } let i = k2.len() - AES_GCM_TAG_SIZE; @@ -1246,11 +1252,11 @@ pub(crate) fn received_k2_trans>( &mut k2[..i], &tag, ) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); if !zeta.update_counter_window(c) { - return Err(byzantine_fault!(ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, true)); } k2.truncate(i); let result = (|| { @@ -1258,7 +1264,7 @@ pub(crate) fn received_k2_trans>( let mut noise = noise.clone(); let mut i = 0; // Process message pattern 2 e token. - let e_remote = noise.read_e(&mut i, &k2).ok_or(byzantine_fault!(FailedAuth, true))?; + let e_remote = noise.read_e(&mut i, &k2).ok_or(fault!(FailedAuth, true))?; // Process message pattern 2 ee token. noise.mix_dh(e_secret, &e_remote); // Process message pattern 2 se token. @@ -1268,26 +1274,28 @@ pub(crate) fn received_k2_trans>( let k = j + AES_GCM_TAG_SIZE; let tag = k2[j..k].try_into().unwrap(); if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_REKEY_COMPLETE, 0), &mut k2[i..j], tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } - let kid_send = NonZeroU32::new(u32::from_be_bytes(k2[i..j].try_into().unwrap())) - .ok_or(byzantine_fault!(InvalidPacket, true))?; + let kid_send = + NonZeroU32::new(u32::from_be_bytes(k2[i..j].try_into().unwrap())).ok_or(fault!(InvalidPacket, true))?; let (rk, rf) = noise.get_ask(LABEL_RATCHET_STATE); let new_ratchet_state = RatchetState::new(rk, rf, zeta.ratchet_state1.chain_len() + 1); let result = app.save_ratchet_state( &zeta.s_remote, &zeta.session_data, - RatchetUpdate { - state1: &new_ratchet_state, - state2: None, - state1_was_just_added: true, - deleted_state1: Some(&zeta.ratchet_state1), - deleted_state2: zeta.ratchet_state2.as_ref(), - }, + CompareAndSwap::new( + &new_ratchet_state, + None, + true, + &zeta.ratchet_state1, + zeta.ratchet_state2.as_ref(), + true, + true, + ), ); - if let Err(e) = result { - return Err(ReceiveError::StorageError(e)); + if !result.map_err(ReceiveError::StorageError)? { + return Err(fault!(OutOfSequence, true)); } let (kek_recv, kek_send) = noise.get_ask(LABEL_KEX_KEY); let (nk_recv, nk_send) = noise.split(); @@ -1363,7 +1371,7 @@ pub(crate) fn received_payload_in_place( use FaultType::*; if payload.len() < AES_GCM_TAG_SIZE { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let is_other = if Some(kid) == zeta.key_ref(true).recv.kid { true @@ -1372,21 +1380,21 @@ pub(crate) fn received_payload_in_place( } else { // A packet would have to be delayed by around an hour for this error to occur, but it can // occur naturally due to just out-of-order transport. - return Err(byzantine_fault!(OutOfSequence, false)); + return Err(fault!(OutOfSequence, false)); }; let i = payload.len() - AES_GCM_TAG_SIZE; let specified_key = zeta.key_ref(is_other).recv.nk.as_ref(); - let specified_key = specified_key.ok_or(byzantine_fault!(OutOfSequence, true))?; + let specified_key = specified_key.ok_or(fault!(OutOfSequence, true))?; let tag = payload[i..].try_into().unwrap(); if !C::Aead::decrypt_in_place(specified_key, &n, None, &mut payload[..i], &tag) { - return Err(byzantine_fault!(FailedAuth, true)); + return Err(fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); if !zeta.update_counter_window(c) { // This error is marked as not happening naturally, but it could occur if something about // the transport protocol is duplicating packets. - return Err(byzantine_fault!(ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, true)); } payload.truncate(i); diff --git a/whitepaper/zssp.pdf b/whitepaper/zssp.pdf index b0e7680..be71c71 100644 Binary files a/whitepaper/zssp.pdf and b/whitepaper/zssp.pdf differ diff --git a/whitepaper/zssp.tex b/whitepaper/zssp.tex index 75262e0..ebd55a0 100644 --- a/whitepaper/zssp.tex +++ b/whitepaper/zssp.tex @@ -214,13 +214,13 @@ So Alice and Bob will need to allow potentially the last two derived ratchet key To minimize the number of ratchet keys Alice and Bob have to store in memory, the final step of every key exchange in ZSSP is to delete the previous ratchet key, once both peers are certain the other peer has derived the new one. So usually Alice and Bob will only have and use the most recently derived ratchet key, but if a peer disappears during key exchange, Alice and Bob will have up to the last two ratchet keys, and will be able to coordinate using the one they are certain both peers have access to. -If this is the first time Alice and Bob are communicating, Alice will send an empty string instead of a ratchet fingerprint to signal this fact. Both Alice and Bob understand this to mean they ought to complete the key exchange with a ratchet key of all zeros. When Alice reveals their identity in the third message of Noise XK, Bob will have to check that it is indeed true that Alice has never communicated with Bob before, and they don't already have a ratchet key and fingerprint. Similarly, if Alice does use a valid ratchet fingerprint and key, Bob must check if the pair is actually associated with Alice, and isn't being stolen from a different peer. +If this is the first time Alice and Bob are communicating, Alice will send a string of all zeros instead of a ratchet fingerprint to signal this fact. Both Alice and Bob understand this to mean they ought to complete the key exchange with a ratchet key of all zeros. When Alice reveals their identity in the third message of Noise XK, Bob will have to check that it is indeed true that Alice has never communicated with Bob before, and they don't already have a ratchet key and fingerprint. Similarly, if Alice does use a valid ratchet fingerprint and key, Bob must check if the pair is actually associated with Alice, and isn't being stolen from a different peer. Since the ratchet fingerprint is included in the first message of Noise XK, with weak forward secrecy, there is some concern it could violate identity hiding. However, since the ratchet fingerprint is a constantly rotating ASK, information about the communicating peers can only be leaked by it if the same ratchet fingerprint is used twice. In normal operation, not only will the ratchet fingerprint only be used once, but it will be deleted with the ratchet key when the key exchange is completed, providing replay protection. Furthermore, if the same ratchet fingerprint is used more than once, all this will reveal is that the same pair of peers are initiating a session with each other, and only if Bob's private key is compromised. -Actually this protocol give ZSSP a lot of useful properties. For one thing Bob can require Alice to include a recognized, nonempty ratchet fingerprint in their first message. This is how ZSSP can achieve ``Silence is Golden'' despite the fact that the first Noise XK packet is usually anonymous and replayable. It also allows Bob to authenticate Alice's identity immediately, as opposed to waiting for Alice's next message. This protocol also is integral to how Alice and Bob are able to coordinate the ratchet key management described in \sectionref{sec:key_management} without any additional round trips. +Actually this protocol give ZSSP a lot of useful properties. For one thing Bob can require Alice to include a recognized, non-zero ratchet fingerprint in their first message. This is how ZSSP can achieve ``Silence is Golden'' despite the fact that the first Noise XK packet is usually anonymous and replayable. It also allows Bob to authenticate Alice's identity immediately, as opposed to waiting for Alice's next message. This protocol also is integral to how Alice and Bob are able to coordinate the ratchet key management described in \sectionref{sec:key_management} without any additional round trips. -If Alice has corrupted their storage, Alice can either send the empty string or send whatever corrupted ratchet fingerprint they still have to Bob. If Bob sees the empty string, and Bob is in opportunistic mode, Bob completes the key exchange with the zero ratchet key, making it possible for Alice to connect. If Alice sent a corrupted ratchet fingerprint, or Bob is the one who corrupted their storage, and if Bob is in opportunistic mode, Bob will ignore the unrecognized ratchet fingerprint and instead uses the zero ratchet key as the PSK. When Alice receives Bob's reply, if Alice is in opportunistic mode, Alice will decrypt Bob's reply with both the ratchet key they intended to use and the zero ratchet key, and go with the one that successfully decrypts the reply. This allows Alice and Bob to reset their ratchet keys in such a way that an attacker cannot perform a downgrade attack without at least one peer's static private key. And if Alice and Bob are in persistent mode, a downgrade attack is simply not possible. +If Alice has corrupted their storage, Alice can either send zeros or send whatever corrupted ratchet fingerprint they still have to Bob. If Bob sees zeros instead of a ratchet fingerprint, and Bob is in opportunistic mode, Bob completes the key exchange with the zero ratchet key, making it possible for Alice to connect. If Alice sent a corrupted ratchet fingerprint, or Bob is the one who corrupted their storage, and if Bob is in opportunistic mode, Bob will ignore the unrecognized ratchet fingerprint and instead uses the zero ratchet key as the PSK. When Alice receives Bob's reply, if Alice is in opportunistic mode, Alice will decrypt Bob's reply with both the ratchet key they intended to use and the zero ratchet key, and go with the one that successfully decrypts the reply. This allows Alice and Bob to reset their ratchet keys in such a way that an attacker cannot perform a downgrade attack without at least one peer's static private key. And if Alice and Bob are in persistent mode, a downgrade attack is simply not possible. \section{Zeta Key Exchange} @@ -271,8 +271,8 @@ The next section will be dedicated to describing ZKE in explicit detail. We will \wordbox[tlr]{2}{\texttt{e1} (1568 bytes)} \\ \bitbox[blr]{2}{} & \bitbox{2}{\texttt{e1\_tag} (16 bytes)} \\ \begin{rightwordgroup}{Noise XK\\Payload} - \wordbox{2}{$\texttt{rf}_1$ (optional 32 bytes)} \\ - \wordbox{2}{$\texttt{rf}_2$ (optional 32 bytes)} \\ + \wordbox{2}{$\texttt{rf}_1$ (32 bytes)} \\ + \wordbox{2}{$\texttt{rf}_2$ (32 bytes)} \\ \wordbox{1}{\texttt{rf\_tag} (16 bytes)} \end{rightwordgroup} \\ \end{bytefield} @@ -542,7 +542,9 @@ Given $(x, g^x)$ and $(y, g^y)$, two output keypairs from $\DHGEN()$, a \emph{ke \subsection{Transition Algorithms}\label{sec:trans_alg} -With the necessary background established, we can now define the final component of the Zeta state machine, the state transition algorithms. As is described in \definitionref{def:state_machine}, each state transition within $\zeta$ has a corresponding algorithm that must be computed. All of these algorithms are described in this section. They have access to a collection of global variables, defined below. +With the necessary background established, we can now define the final component of the Zeta state machine, the state transition algorithms. Anyone looking to implement the Zeta state machine must read this section in its entireity. Many paragraphs contain rules not represented within the diagrams or algorithm psuedocode. Each rule qualified with \emph{must} must be implemented. + +As is described in \definitionref{def:state_machine}, each state transition within $\zeta$ has a corresponding algorithm. These algorithms \emph{must} be computed atomically with the state transition. If the algorithm fails or aborts, then the corresponding state transition \emph{must} not occur. All of these algorithms are described in this section. They have access to a collection of global variables, defined below. \begin{itemize} \item Persistent ratchet keys $\texttt{rk}_1, \texttt{rk}_2$, to be used as a handshake PSK @@ -562,16 +564,20 @@ With the necessary background established, we can now define the final component \item Confirmed key index $\texttt{i}$, used so peers know the most recently confirmed Noise key. \end{itemize} -All of these variables are independent \emph{per instance of} $\zeta$. So each instance of $\zeta$ stores a unique and independent set of these variables, and they are never shared between separate instances of $\zeta$. The persistent state variables $\texttt{rk}$ and $\texttt{rf}$ are a partial exception to this rule, as they can be accessed by multiple instances of $\zeta$ over time. However each of these instances must be associated with a single, unique remote peer, and two instances of $\zeta$ must never access the same set of persistent state variables at the same time. +All of these variables, except $\texttt{rk}$ and $\texttt{rf}$, are independent \emph{per instance of} $\zeta$. So each instance of $\zeta$ stores a unique and independent set of these variables, and they are never shared between separate instances of $\zeta$. -What constitutes a ``single, unique remote peer'' is up to the upper protocol to decide. Usually remote peers are identified and differentiated by their static public keys ($g^u$ or $g^v$). However ZKE does not strictly enforce a one-to-one relationship between static public keys and identity specifically so the upper protocol is able to rotate the static public keys of peers. +Variables $\texttt{rk}$ and $\texttt{rf}$ are stored on a persistent storage device, and can be accessed by multiple instances of $\zeta$ over time. Implementations \emph{must} guarantee each instance of persistent variables $\texttt{rk}$ and $\texttt{rf}$ is associated with a single, unique remote peer. When implementing $\zeta$, each instance of $\zeta$ \emph{must} store a local copy of $\texttt{rf}$. Writes to $\texttt{rk}$ and $\texttt{rf}$ \emph{must} be batched together as one atomic operation. Implementations \emph{must} check that the local copy of $\texttt{rf}$ is consistent with $\texttt{rf}$ prior to every write. If they are inconsistent, then the entire write \emph{must} be cancelled, and $\zeta$ \emph{must} immediately abort the transition algorithm which triggered the write. Then, $\zeta$ \emph{must} be deleted (i.e. $\zeta$ \emph{must} perform the timeout transition, $\tau$). This policy guarantees that if multiple, concurrent instances of $\zeta$ exist for the same remote peer, only one of them will be allowed to write to $\texttt{rk}$ and $\texttt{rf}$, while the others will be deleted. + +It \emph{must} be infeasible to recover a non-zero value of $\texttt{rk}$ or $\texttt{rf}$ based on the amount of time read and write operations on $\texttt{rk}$ or $\texttt{rf}$ take. To this end, implementations must not directly operate on ratchet keys (check for equality, create a search index, etc.). Instead, these operations must be performed indirectly upon their ratchet fingerprints. This is to ensure that, in the event a side-channel attack is found, only the ratchet fingerprint can be leaked. + +What constitutes a ``single, unique remote peer'' is up to the upper protocol to decide. Usually remote peers are identified and differentiated by their static public keys ($g^u$ or $g^v$). However ZKE does not strictly enforce a one-to-one relationship between static public keys and identity specifically so the upper protocol is able to rotate the static public keys of peers. There \emph{must} be a well-defined equality relation between peers. \begin{definition}[H][Session Counter] Every instance of $\zeta$ contains a single monotonically increasing counter, that is initialized to 0. We will use the notation $\COUNTER()$ to represent incrementing this counter by 1 and returning the previous counter value. So the very first call to $\COUNTER()$ within $\zeta$ will return 0, the second call will return 1, and so on. - It is assumed that that output of $\COUNTER()$, the counter value, will be used as an AES-GCM nonce. When a remote peer decrypts an AES-GCM ciphertext that uses a counter value nonce, they must first statefully check that this counter has never been received before, and fail authentication if it has been replayed. + It is assumed that that output of $\COUNTER()$, the counter value, will be used as an AES-GCM nonce. When a remote peer decrypts an AES-GCM ciphertext that uses a counter value nonce, they \emph{must} first statefully check that this counter has never been received before, and fail authentication if it has been replayed. \end{definition} ZKE uses a 64-bit (8 byte) counter, but AES-GCM uses a 96-bit (12 byte) nonce/IV. The Noise protocol does specify that, in order to use a counter value as an AES nonce, it should be encoded as a big-endian integer with 4 bytes of zero padding to the left of it. However, for good reason, we have decided to utilize the rightmost byte of that 4 byte padding to store the packet type number, as depicted in \figureref{fig:nonce}. This accomplishes two goals. First it explicitly authenticates the packet type of a packet, which makes it infeasible for an attacker to change the packet type field of a header without triggering an authentication failure. We could have used the additional authentication data parameter of AES-GCM to accomplish this, but the Noise protocol already makes extensive use of this parameter. Second, this nonce construction reduces the risk of catastrophic nonce reuse due to implementation error, specifically during key exchanges. While a correctly implemented counter would be sufficient for security, this adds an extra layer of defence. Given a packet type number $p$, and a counter value $c$, we will use the notation $p||c$ to represent this nonce construction. @@ -587,16 +593,15 @@ ZKE uses a 64-bit (8 byte) counter, but AES-GCM uses a 96-bit (12 byte) nonce/IV \end{figure} \begin{definition}[H][Key Id Generation] - \algn{KID-Gen} is a stateful algorithm that outputs a locally unique, uniform random 32-bit string. The output of this algorithm will be used as the \emph{key id} for a session key, allowing the local peer to multiplex many keys for many remote peers simultaneously. The output of $\algn{KID-Gen}()$ must be locally unique from all other key ids currently in use. $\algn{KID-Gen}()$ may only repeat an output if that output is currently not being used as a key id. + \algn{KID-Gen} is a stateful algorithm that outputs a locally unique, uniform random 32-bit string. The output of this algorithm will be used as the \emph{key id} for a session key, allowing the local peer to multiplex many keys for many remote peers simultaneously. The output of $\algn{KID-Gen}()$ \emph{must} be locally unique from all other key ids currently in use. $\algn{KID-Gen}()$ may only repeat an output if that output is currently not being used as a key id. \end{definition} - \begin{definition}[H][Persistent State] The variables $\texttt{rk}$ and $\texttt{rf}$ are considered the \emph{persistent state} of ZKE. The values stored in these variables are not lost when a session ends, and can be \emph{restored} at any time to initiate a new session. - $\texttt{rk}$ is a tuple of up to two 256-bit \emph{ratchet keys}. Similarly, $\texttt{rf}$ is a tuple of up to two 256-bit \emph{ratchet fingerprints}. $\texttt{rk}$ is initialized to $(0^{256}, \bot)$ and $\texttt{rf}$ is initialized to $(\varepsilon, \bot)$. + $\texttt{rk}$ is a tuple of up to two 256-bit \emph{ratchet keys}. Similarly, $\texttt{rf}$ is a tuple of up to two 256-bit \emph{ratchet fingerprints}. Both $\texttt{rk}$ and $\texttt{rf}$ are initialized to $(0^{256}, \bot)$. - If two peers are in persistent mode, and wish to connect for the first time, they may use a preshared one time password, $p$, to do so. $p$ should be a randomized string, and it must not be correlated with the identity of either peer. If a one time password is being used then $r\gets \KDF(p, \texttt{"ZSSP\_OTP\_TO\_RATCHET"}, \varepsilon, 2)$ must be computed. Both peers then initialize $\texttt{rk}$ to $(r_1, \bot)$ and $\texttt{rf}$ to $(r_2, \bot)$. + If two peers are in persistent mode, and wish to connect for the first time, they may use a preshared one time password, $p$, to do so. $p$ \emph{must} be a random string with at least 256-bits of entropy. If a one time password is being used then $r\gets \KDF(p, \texttt{"ZSSP\_OTP\_TO\_RATCHET"}, \varepsilon, 2)$ \emph{must} be computed. Both peers then initialize $\texttt{rk}$ to $(r_1, \bot)$ and $\texttt{rf}$ to $(r_2, \bot)$. $p$ \emph{must} only be used once; A fresh instance of $p$ \emph{must} be generated for every new pair of peers who wish to connect. \end{definition} $\bot$ represents the ``null'' value. When a variable is set to $\bot$, it means whatever was previous stored within the variable is being permanently deleted from memory. All variables are initialized to $\bot$ unless otherwise stated. $\varepsilon$ is the empty string. @@ -666,7 +671,11 @@ Prior to the execution of any of the following algorithms, $\zeta$ will check if \State $X_1\texttt{.key\_id} \gets \texttt{kid}^1_1$ \State $X_1\texttt{.e} \gets g^x$ \State $X_1\texttt{.e1} \gets \AEAD(k_4, 0||0, h_3, e_{pub})$ - \State $X_1\texttt{.rf} \gets \AEAD(k_4, 0||1, h_5, \texttt{rf})$ + \If{$\texttt{rf}_2=\bot$} + \State $X_1\texttt{.rf} \gets \AEAD(k_4, 0||1, h_5,\,\texttt{rf}_1||0^{256})$ + \Else + \State $X_1\texttt{.rf} \gets \AEAD(k_4, 0||1, h_5,\,\texttt{rf}_1||\texttt{rf}_2)$ + \EndIf \State $\texttt{hk} \gets \KDF(h_6, \texttt{"ASKH"}, c_4, 2)$ \State $c \gets X_1\texttt{.rf\_tag}\ \text{(last 8 bytes)}$ \State $\textbf{Send } (0,\ 0||c,\ X_1)$ @@ -686,15 +695,15 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi \EndIf \State $\textbf{Decrypt } X_1$ according to Noise XK \State $\texttt{rf}' \gets X_1\texttt{.rf}_1$ - \State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$ + \State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$ (or $\texttt{rk}'\gets \bot$ if $\texttt{rf}' = 0^{256}$) \If{$\texttt{rk}' = \bot$} \State $\texttt{rf}' \gets X_1\texttt{.rf}_2$ - \State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$ + \State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$ (or $\texttt{rk}'\gets \bot$ if $\texttt{rf}' = 0^{256}$) \If{$\texttt{rk}' = \bot$} \If{$\pi_1=1$} \State \textbf{Fail authentication} \Else - \State $\texttt{rf}' \gets \varepsilon$ + \State $\texttt{rf}' \gets 0^{256}$ \State $\texttt{rk}' \gets 0^{256}$ \EndIf \EndIf @@ -719,12 +728,12 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi \If{$c \geq 2^{24} \orb c \neq X_2\texttt{.key\_id\_tag}$ (last 3 bytes)} \State \textbf{Fail authentication} \EndIf - \State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_1$ \State $i \gets 1$ - \If{$X_2$ failed authentication} - \State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_2$ + \State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_1$ + \If{$X_2$ failed to decrypt} \State $i \gets 2$ - \If{$X_2$ failed authentication} + \State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_2$ + \If{$X_2$ failed to decrypt} \If{$\pi_2=1$} \State \textbf{Fail authentication} \Else @@ -736,10 +745,10 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi \State $\texttt{kid}^1_2 \gets X_2\texttt{.key\_id}$ \State $X_3\texttt{.s} \gets \AEAD(k_{10}, 2||1, h_{11}, g^u)$ \State $X_3\texttt{.identity} \gets \AEAD(k_{13}, 2||0, h_{12}, \texttt{identity})$ - \State $\texttt{kek}^1 \gets \KDF(h_{14}, \texttt{"ASKK"}, c_{13}, 2)$ - \State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$ \State $(\texttt{rk}_2, \texttt{rf}_2) \gets (\texttt{rk}_i, \texttt{rf}_i)$ \State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{14}, \texttt{"ASKR"}, c_{13}, 2)$ + \State $\texttt{kek}^1 \gets \KDF(h_{14}, \texttt{"ASKK"}, c_{13}, 2)$ + \State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$ \State $\texttt{r} \gets 1$ \State $\texttt{i} \gets 0$ \State $\textbf{Send } (\texttt{kid}^1_2,\ 2||0,\ X_3)$ @@ -748,7 +757,7 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi Packet types $X_2$ and $K_1$ represent the second to last message of Noise XK and Noise KK. The remote peer will not have derived the Noise key yet, so if this peer attempted to encrypt with this key immediately, there is a risk its packets will arrive out of order and be undecryptable. Receiving a valid $C_1$ packet from the remote peer confirms that they have successfully derived the correct Noise key, which signals it can now be reliably used for encryption. -Similarly, the new ratchet key and fingerprint will not have been derived by the remote peer yet. So this peer will have to save these keys to persistent storage, but not yet delete the previous ratchet key and fingerprint until the remote peer sends a key confirmation. This means that for a brief period of time Alice has two sets of ratchet keys and fingerprints saved, instead of just one. We want to make sure Alice and Bob never have more than two of these keys saved at a time. So during the Noise XK handshake if Alice has two sets of keys and fingerprints, they must send both fingerprints to Bob, and delete the key and fingerprint that Bob chooses not to use as the PSK. +Similarly, the new ratchet key and fingerprint will not have been derived by the remote peer yet. So this peer will have to save these keys to persistent storage, but not yet delete the previous ratchet key and fingerprint until the remote peer sends a key confirmation. This means that for a brief period of time Alice has two sets of ratchet keys and fingerprints saved, instead of just one. We want to make sure Alice and Bob never have more than two of these keys saved at a time. So during the Noise XK handshake if Alice has two sets of keys and fingerprints, they \emph{must} send both fingerprints to Bob, and delete the key and fingerprint that Bob chooses not to use as the PSK. \begin{algorithm}[H] \caption{Transition $\delta(B_2, X_3)=S_1$ -- Bob receives the final packet of Noise XK. Input $\algn{Accept}$ is a function provided by the upper protocol that takes as input Alice's identity and outputs security flags $(\pi_3, \pi_4)$, $\pi_3$ can be $\bot$. If $\pi_3=\bot$, Bob will reject Alice's session. If $\pi_3=1$, and Bob is aware of a ratchet key Alice should have, but Alice is not connecting with it, Bob will reject Alice's session (similar to when $\pi_2=1$). If Bob rejects Alice's session and $\pi_4 = 0$, Bob will send a packet to Alice explicitly rejecting their session.}\label{alg:recv_x3} @@ -760,8 +769,8 @@ Similarly, the new ratchet key and fingerprint will not have been derived by the \State $(\pi_3, \pi_4) \gets \algn{Accept}(X_3\texttt{.s}, X_3\texttt{.identity})$ \If{$\pi_3 \neq \bot$} \State $\textbf{Restore } (\texttt{rk}, \texttt{rf}) \text{ based on } (X_3\texttt{.s}, X_3\texttt{.identity})$ - \If{$(\texttt{rf}', \texttt{rk}') \neq (\texttt{rf}_1, \texttt{rk}_1) \andb (\texttt{rf}', \texttt{rk}') \neq (\texttt{rf}_2, \texttt{rk}_2)$} - \If{$\pi_3 = 0 \andb \texttt{rf}' = \varepsilon$} + \If{$\texttt{rf}' \neq \texttt{rf}_1 \andb \texttt{rf}' \neq \texttt{rf}_2$} + \If{$\pi_3 = 0 \andb \texttt{rf}' = 0^{256}$} \State \textbf{Warn} the upper protocol that Alice does not have the ratchet key \Else \If{$\pi_4 = 0$} @@ -771,9 +780,9 @@ Similarly, the new ratchet key and fingerprint will not have been derived by the \State \textbf{Fail authentication} \EndIf \EndIf - \State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$ \State $(\texttt{rk}_2, \texttt{rf}_2) \gets \bot$ \State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{14}, \texttt{"ASKR"}, c_{13}, 2)$ + \State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$ \State $\texttt{r} \gets 2$ \State $\texttt{i} \gets 1$ \State $C_1\texttt{.kek\_tag} \gets \AEAD(\texttt{kek}^1_1, 3||c, \varepsilon, \varepsilon)$ @@ -788,12 +797,14 @@ Similarly, the new ratchet key and fingerprint will not have been derived by the \end{algorithmic} \end{algorithm} -When packet $X_3$ is received, Bob might refuse to start a session with Alice based on their static key and identity. The preferred way for Bob to do this is to simply do nothing, and ignore Alice's packets. This would follow the ``Silence is Golden" principle of security. However there are some applications where, usually for UX reasons, Alice needs to be able tell apart having a bad connection with Bob from Bob rejecting Alice's identity. If Bob always goes silent Alice has no reliable way to tell these two situations apart. Since both Bob and Alice have the key exchange key at this point in the handshake, it was a very natural and secure extension of the protocol to allow Bob to notify Alice that the session has been refused. +If, when function $\algn{Accept}$ is called, any other instance of $\zeta$ exists with the same remote peer, then $\algn{Accept}$ \emph{must} be implemented to do one of the following: Either the other instance of $\zeta$ is deleted, or $\algn{Accept}$ returns $\pi_3=\bot$ to reject the new session. Instances of $\zeta$ in state $B_2$ \emph{must} be ignored in regards to the prior rule, since the identity of the remote peer will not have been confirmed and might not be known. Keep in mind that this rule implies that if Alice attempts to open multiple sessions with Bob at once, only one of them will be accepted. + +When packet $X_3$ is received, Bob might refuse to start a session with Alice based on their static key and identity. The preferred way for Bob to do this is to simply do nothing, and ignore Alice's packets. This would follow the ``Silence is Golden" principle of security. However there are some applications where, usually for UX reasons, Alice needs to be able tell apart having a bad connection with Bob from Bob rejecting Alice's identity. If Bob always goes silent, Alice has no reliable way to tell these two situations apart. This is the reason why packet $D$ is created and sent at the bottom of \algorithmref{alg:recv_x3}. Receipt of packet $D$ implies Bob has explicitly rejected a session with Alice. Packet types $X_3$ and $K_2$ represent the final messages of Noise XK and Noise KK, respectively. The peer who receives these packets can be sure that the remote peer has derived an identical Noise key already, and so the new key can be reliably used immediately. \begin{algorithm}[H] - \caption{Transition $\delta(A_3, C_1)=S_2$ and $\delta(R_2, C_1)=S_2$ -- A key confirmation for either the Noise XK initial handshake or Noise KK rekeying has been received. This peer must send an Acknowledgement, \figureref{packet:key_conf}, to let the remote peer know that it has been received.}\label{alg:key_conf} + \caption{Transition $\delta(A_3, C_1)=S_2$ and $\delta(R_2, C_1)=S_2$ -- A key confirmation for either the Noise XK initial handshake or Noise KK rekeying has been received. This peer \emph{must} send an Acknowledgement, \figureref{packet:key_conf}, to let the remote peer know that it has been received.}\label{alg:key_conf} \begin{algorithmic} \Require $(\texttt{kid}^{\texttt{i} + 1}_{\texttt{r}},\ 3||c,\ C_1)$ \State $\textbf{Decrypt } C_1$ using $\texttt{kek}^{\texttt{i} + 1}_{\texttt{r}}$ @@ -820,7 +831,6 @@ Packet types $C_2$ and $D$ have trivial transition algorithms. They merely decry \end{algorithmic} \end{algorithm} - \begin{algorithm}[H] \caption{Transition $\delta(S_2, K_1)=R_2$ and $\delta(R_1, K_1)=R_2$ -- Bob has received Alice's request to rekey and sends a Rekey Completion packet, \figureref{packet:rekey}, in reply. Bob has now finished Noise KK and has obtained a new Noise key. A new pair of key ids are generated so Bob can tell apart packets encrypted with the new key from those encrypted with the previous key.} \begin{algorithmic} @@ -833,28 +843,27 @@ Packet types $C_2$ and $D$ have trivial transition algorithms. They merely decry \State $K_2\texttt{.key\_id} \gets \AEAD(k_{10}', 6||0, h_{8}, \texttt{kid}^{\texttt{i} + 1}_\texttt{r})$ \State $c \gets \COUNTER()$ \State $K_2 \gets \AEAD(\texttt{kek}^\texttt{i}_{3 - \texttt{r}}, 6||c, \varepsilon, K_2)$ + \State $(\texttt{rk}_2, \texttt{rf}_2) \gets (\texttt{rk}_1, \texttt{rf}_1)$ + \State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$ \State $\texttt{kek}^{\texttt{i} - 1} \gets \bot$ \State $(\texttt{kek}^{\texttt{i} + 1}_{3 - \texttt{r}}, \texttt{kek}^{\texttt{i} + 1}_\texttt{r}) \gets \KDF(h_{11}', \texttt{"ASKK"}, c_{10}', 2)$ \State $\texttt{nk}^{\texttt{i} - 1} \gets \bot$ \State $(\texttt{nk}^{\texttt{i} + 1}_{3 - \texttt{r}}, \texttt{nk}^{\texttt{i} + 1}_\texttt{r}) \gets \KDF(\varepsilon, L, c_{10}', 2)$ - \State $(\texttt{rk}_2, \texttt{rf}_2) \gets (\texttt{rk}_1, \texttt{rf}_1)$ - \State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$ \State $\textbf{Send } (\texttt{kid}^\texttt{i}_{3 - \texttt{r}},\ 6||c,\ K_2)$ \end{algorithmic} \end{algorithm} - \begin{algorithm}[H] \caption{Transition $\delta(R_1, K_2)=S_1$ -- Alice has received Bob's reply and so can also finish Noise KK. They send a Key Confirmation, \figureref{packet:key_conf}, to signal the completion of the handshake. The second to last Noise key is deleted for forward secrecy.} \begin{algorithmic} \Require $(\texttt{kid}^\texttt{i}_\texttt{r},\ 6||c,\ K_2)$ \State $\textbf{Decrypt } K_2$ using $\texttt{kek}^\texttt{i}_\texttt{r}$, and Noise KK \State $\texttt{kid}^{\texttt{i} + 1}_{3 - \texttt{r}} \gets K_2\texttt{.key\_id}$ + \State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$ \State $\texttt{kek}^{\texttt{i} - 1} \gets \bot$ \State $(\texttt{kek}^{\texttt{i} + 1}_\texttt{r}, \texttt{kek}^{\texttt{i} + 1}_{3 - \texttt{r}}) \gets \KDF(h_{11}', \texttt{"ASKK"}, c_{10}', 2)$ \State $\texttt{nk}^{\texttt{i} - 1} \gets \bot$ \State $(\texttt{nk}^{\texttt{i} + 1}_\texttt{r}, \texttt{nk}^{\texttt{i} + 1}_{3 - \texttt{r}}) \gets \KDF(\varepsilon, L, c_{10}', 2)$ - \State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$ \State $\texttt{i} \gets \texttt{i} + 1$ \State $c \gets \COUNTER()$ \State $C_1\texttt{.kek\_tag} \gets \AEAD(\texttt{kek}^\texttt{i}_{3 - \texttt{r}}, 3||c, \varepsilon, \varepsilon)$ @@ -995,7 +1004,7 @@ This protocol violates the Silence is Golden principle of security. So we recomm \section{ZeroTier Fragmentation Protocol} -ZSSP supports the partitioning of large packets into smaller fragments, that can each be sent individually between peers, and will be reassembled at the other end. Most datagram protocols have a maximum transmission unit (MTU), that all packets must be smaller than. Fragmentation makes it possible for the upper protocol to send packets much larger than the MTU. The MTU of UDP is inconsistent and can go as low as 1280 bytes. Satellite links can go even smaller, with MTU in the 500 bytes range. While many applications would work with such small MTU, they would not work efficiently, and having such low MTUs even as just a possibility often greatly hinders the design of a protocol, and what platforms it can support. To make real-time networking operate reliably and efficiently on top of links with low MTU, fragmentation is necessary. +ZSSP supports the partitioning of large packets into smaller fragments, that can each be sent individually between peers, and will be reassembled at the other end. Most datagram protocols have a maximum transmission unit (MTU), that all packets must be smaller than. Fragmentation makes it possible for the upper protocol to send packets much larger than the MTU. The MTU of UDP is inconsistent and can go as low as 1280 bytes. And if we consider non-IP networks, MTU can go as low as just a few hundred bytes. While many applications would work with small MTU, they might not work efficiently, and having such low MTUs even as just a possibility often greatly hinders the design of a protocol, and what platforms it can support. To make real-time networking operate reliably and efficiently on top of links with low MTU, fragmentation is necessary. ZFP is responsible for fragmentation within ZSSP. It sits at the very bottom of the ZSSP protocol stack, just above the datagram protocol. We have chosen to put the fragmentation protocol at the bottom of the ZSSP stack, instead of the top for a few reasons. First and foremost it allows key exchange packets to be as large as we need, larger than the MTU of the underlying datagram protocol. This allows us to use P384 and Kyber1024 keys, instead of their smaller and less secure variants. The second reason is that it allows the bandwidth overhead of fragmentation to be extremely small, just 16 bytes per fragment. If the fragmentation protocol were at the top of the stack instead, then each fragment would have required an additional 16 bytes for each MAC. This also benefits efficiency, because it reduces the number of MACs that must be computed and subsequently verified.