From 52234c44fb43284c9b1e3c022292eaba298c551c Mon Sep 17 00:00:00 2001 From: Sean OMeara Date: Wed, 18 Jan 2023 16:27:25 +0100 Subject: [PATCH 01/14] silence compiler warnings about _unused_variables (#1852) --- controller/src/postgresdatabase.rs | 10 +++++----- network-hypervisor/src/vl1/peer.rs | 32 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/controller/src/postgresdatabase.rs b/controller/src/postgresdatabase.rs index cc7198ed9..8b48ab8c5 100644 --- a/controller/src/postgresdatabase.rs +++ b/controller/src/postgresdatabase.rs @@ -193,7 +193,7 @@ impl VL1DataStorage for PostgresDatabase { Some(self.local_identity.clone()) } - fn save_node_identity(&self, id: &Valid) -> bool { + fn save_node_identity(&self, _id: &Valid) -> bool { panic!("local identity saving not supported by PostgresDatabase") } } @@ -389,7 +389,7 @@ impl Database for PostgresDatabase { })) } - async fn save_network(&self, obj: Network, generate_change_notification: bool) -> Result<(), Error> { + async fn save_network(&self, _obj: Network, _generate_change_notification: bool) -> Result<(), Error> { todo!() } @@ -405,11 +405,11 @@ impl Database for PostgresDatabase { Ok(r) } - async fn get_member(&self, network_id: NetworkId, node_id: Address) -> Result, Error> { + async fn get_member(&self, _network_id: NetworkId, _node_id: Address) -> Result, Error> { todo!() } - async fn save_member(&self, obj: Member, generate_change_notification: bool) -> Result<(), Error> { + async fn save_member(&self, _obj: Member, _generate_change_notification: bool) -> Result<(), Error> { todo!() } @@ -443,7 +443,7 @@ impl Database for PostgresDatabase { return Ok(false); } - async fn log_request(&self, obj: RequestLogItem) -> Result<(), Error> { + async fn log_request(&self, _obj: RequestLogItem) -> Result<(), Error> { todo!() } } diff --git a/network-hypervisor/src/vl1/peer.rs b/network-hypervisor/src/vl1/peer.rs index 3178b213b..e53800df6 100644 --- a/network-hypervisor/src/vl1/peer.rs +++ b/network-hypervisor/src/vl1/peer.rs @@ -784,7 +784,7 @@ impl Peer { inner: &Inner, node: &Node, time_ticks: i64, - message_id: MessageId, + _message_id: MessageId, payload: &PacketBuffer, ) -> PacketHandlerResult { if node.this_node_is_root() || inner.should_respond_to(&self.identity) { @@ -813,12 +813,12 @@ impl Peer { fn handle_incoming_rendezvous( self: &Arc, - app: &Application, + _app: &Application, node: &Node, - time_ticks: i64, - message_id: MessageId, - source_path: &Arc, - payload: &PacketBuffer, + _time_ticks: i64, + _message_id: MessageId, + _source_path: &Arc, + _payload: &PacketBuffer, ) -> PacketHandlerResult { if node.is_peer_root(self) {} return PacketHandlerResult::Ok; @@ -853,22 +853,22 @@ impl Peer { fn handle_incoming_push_direct_paths( self: &Arc, - app: &Application, - node: &Node, - time_ticks: i64, - source_path: &Arc, - payload: &PacketBuffer, + _app: &Application, + _node: &Node, + _time_ticks: i64, + _source_path: &Arc, + _payload: &PacketBuffer, ) -> PacketHandlerResult { PacketHandlerResult::Ok } fn handle_incoming_user_message( self: &Arc, - app: &Application, - node: &Node, - time_ticks: i64, - source_path: &Arc, - payload: &PacketBuffer, + _app: &Application, + _node: &Node, + _time_ticks: i64, + _source_path: &Arc, + _payload: &PacketBuffer, ) -> PacketHandlerResult { PacketHandlerResult::Ok } From 5cf99ecb1d0bd830418d58f47ff453032b568d7e Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Tue, 28 Feb 2023 17:54:08 -0500 Subject: [PATCH 02/14] Tetanus noise xk (#1881) * Noise XK work in progress. * A whole lot more Noise_XK work... exchange almost done. * Delete a bunch of commented out old Noise_IK code. * Add back in send() and a few other things to Noise_XK ZSSP. * Some p384 experiment in attic * A ton of ZSSP work, and put MPL on ZSSP. * updated kbkdf512 to use the modern nist standard * Parameterize KBKDF on resulting output key size the way NIST likes. * updated variable comment * Make the label a const parameter on kbkdf. * updated variable comment * Add MPL to utils and other stuff. * layout tweak * Some more ZSSP work and a VDF we may use. * warning removal * More ZSSP work, add benchmarks for mimcvdf. * Almost ready to test... * Build fix. * Add automatic retransmission in the earliest stages of session init. * Just about ready to test... wow. * It opens a session. * ZSSP basically works... --------- Co-authored-by: mamoniot --- crypto/benches/benchmark_crypto.rs | 28 +- crypto/src/aes.rs | 159 +- crypto/src/hash.rs | 2 + crypto/src/lib.rs | 11 + crypto/src/mimcvdf.rs | 141 ++ crypto/src/secret.rs | 5 + utils/src/arrayvec.rs | 8 +- utils/src/blob.rs | 8 +- utils/src/buffer.rs | 8 +- utils/src/defer.rs | 12 +- utils/src/dictionary.rs | 8 +- utils/src/error.rs | 8 +- utils/src/exitcode.rs | 8 +- utils/src/gate.rs | 10 +- utils/src/gatherarray.rs | 8 +- utils/src/hex.rs | 8 +- utils/src/io.rs | 8 +- utils/src/json.rs | 8 + utils/src/lib.rs | 8 +- utils/src/marshalable.rs | 8 +- utils/src/memory.rs | 8 +- utils/src/pool.rs | 8 +- utils/src/reaper.rs | 8 + utils/src/ringbuffer.rs | 8 +- utils/src/ringbuffermap.rs | 10 +- utils/src/sync.rs | 8 + utils/src/thing.rs | 8 +- utils/src/varint.rs | 8 +- zssp/Cargo.toml | 16 + zssp/src/applicationlayer.rs | 111 +- zssp/src/constants.rs | 113 -- zssp/src/error.rs | 54 +- zssp/src/lib.rs | 14 +- zssp/src/main.rs | 219 +++ zssp/src/proto.rs | 212 +++ zssp/src/sessionid.rs | 27 +- zssp/src/tests.rs | 10 + zssp/src/zssp.rs | 2620 ++++++++++++++-------------- 38 files changed, 2421 insertions(+), 1505 deletions(-) create mode 100644 crypto/src/mimcvdf.rs delete mode 100644 zssp/src/constants.rs create mode 100644 zssp/src/main.rs create mode 100644 zssp/src/proto.rs diff --git a/crypto/benches/benchmark_crypto.rs b/crypto/benches/benchmark_crypto.rs index ee2a7efb7..fd01855c9 100644 --- a/crypto/benches/benchmark_crypto.rs +++ b/crypto/benches/benchmark_crypto.rs @@ -1,34 +1,40 @@ use criterion::{criterion_group, criterion_main, Criterion}; use std::time::Duration; +use zerotier_crypto::mimcvdf; use zerotier_crypto::p384::*; -use zerotier_crypto::random; use zerotier_crypto::x25519::*; pub fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("cryptography"); + + let mut input = 1; + let mut proof = 0; + group.bench_function("mimcvdf::delay(1000)", |b| { + b.iter(|| { + input += 1; + proof = mimcvdf::delay(input, 1000); + }) + }); + group.bench_function("mimcvdf::verify(1000)", |b| { + b.iter(|| { + assert!(mimcvdf::verify(proof, input, 1000)); + }) + }); + let p384_a = P384KeyPair::generate(); let p384_b = P384KeyPair::generate(); - //let kyber_a = pqc_kyber::keypair(&mut random::SecureRandom::default()); - //let kyber_encap = pqc_kyber::encapsulate(&kyber_a.public, &mut random::SecureRandom::default()).unwrap(); - let x25519_a = X25519KeyPair::generate(); let x25519_b = X25519KeyPair::generate(); let x25519_b_pub = x25519_b.public_bytes(); - let mut group = c.benchmark_group("cryptography"); group.measurement_time(Duration::new(10, 0)); group.bench_function("ecdhp384", |b| { b.iter(|| p384_a.agree(p384_b.public_key()).expect("ecdhp384 failed")) }); group.bench_function("ecdhx25519", |b| b.iter(|| x25519_a.agree(&x25519_b_pub))); - //group.bench_function("kyber_encapsulate", |b| { - // b.iter(|| pqc_kyber::encapsulate(&kyber_a.public, &mut random::SecureRandom::default()).expect("kyber encapsulate failed")) - //}); - //group.bench_function("kyber_decapsulate", |b| { - // b.iter(|| pqc_kyber::decapsulate(&kyber_encap.0, &kyber_a.secret).expect("kyber decapsulate failed")) - //}); group.finish(); } diff --git a/crypto/src/aes.rs b/crypto/src/aes.rs index 014d241ab..5d0cd2c32 100644 --- a/crypto/src/aes.rs +++ b/crypto/src/aes.rs @@ -46,7 +46,7 @@ mod fruit_flavored { data_out_len: usize, data_out_written: *mut usize, ) -> i32; - //fn CCCryptorReset(cryptor_ref: *mut c_void, iv: *const c_void) -> i32; + fn CCCryptorReset(cryptor_ref: *mut c_void, iv: *const c_void) -> i32; fn CCCryptorRelease(cryptor_ref: *mut c_void) -> i32; fn CCCryptorGCMSetIV(cryptor_ref: *mut c_void, iv: *const c_void, iv_len: usize) -> i32; fn CCCryptorGCMAddAAD(cryptor_ref: *mut c_void, aad: *const c_void, len: usize) -> i32; @@ -183,6 +183,101 @@ mod fruit_flavored { unsafe impl Send for Aes {} unsafe impl Sync for Aes {} + pub struct AesCtr(*mut c_void); + + impl Drop for AesCtr { + #[inline(always)] + fn drop(&mut self) { + unsafe { CCCryptorRelease(self.0) }; + } + } + + impl AesCtr { + /// Construct a new AES-CTR cipher. + /// Key must be 16, 24, or 32 bytes in length or a panic will occur. + pub fn new(k: &[u8]) -> Self { + if k.len() != 32 && k.len() != 24 && k.len() != 16 { + panic!("AES supports 128, 192, or 256 bits keys"); + } + unsafe { + let mut ptr: *mut c_void = null_mut(); + let result = CCCryptorCreateWithMode( + kCCEncrypt, + kCCModeCTR, + kCCAlgorithmAES, + 0, + [0_u64; 2].as_ptr().cast(), + k.as_ptr().cast(), + k.len(), + null(), + 0, + 0, + 0, + &mut ptr, + ); + if result != 0 { + panic!("CCCryptorCreateWithMode for CTR mode returned {}", result); + } + AesCtr(ptr) + } + } + + /// Initialize AES-CTR for encryption or decryption with the given IV. + /// If it's already been used, this also resets the cipher. There is no separate reset. + pub fn reset_set_iv(&mut self, iv: &[u8]) { + unsafe { + if iv.len() == 16 { + if CCCryptorReset(self.0, iv.as_ptr().cast()) != 0 { + panic!("CCCryptorReset for CTR mode failed (old MacOS bug)"); + } + } else if iv.len() < 16 { + let mut iv2 = [0_u8; 16]; + iv2[0..iv.len()].copy_from_slice(iv); + if CCCryptorReset(self.0, iv2.as_ptr().cast()) != 0 { + panic!("CCCryptorReset for CTR mode failed (old MacOS bug)"); + } + } else { + panic!("CTR IV must be less than or equal to 16 bytes in length"); + } + } + } + + /// Encrypt or decrypt (same operation with CTR mode) + #[inline(always)] + pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) { + unsafe { + assert!(output.len() >= input.len()); + let mut data_out_written: usize = 0; + CCCryptorUpdate( + self.0, + input.as_ptr().cast(), + input.len(), + output.as_mut_ptr().cast(), + output.len(), + &mut data_out_written, + ); + } + } + + /// Encrypt or decrypt in place (same operation with CTR mode) + #[inline(always)] + pub fn crypt_in_place(&mut self, data: &mut [u8]) { + unsafe { + let mut data_out_written: usize = 0; + CCCryptorUpdate( + self.0, + data.as_ptr().cast(), + data.len(), + data.as_mut_ptr().cast(), + data.len(), + &mut data_out_written, + ); + } + } + } + + unsafe impl Send for AesCtr {} + pub struct AesGcm(*mut c_void, bool); impl Drop for AesGcm { @@ -307,6 +402,17 @@ mod openssl_aes { use std::cell::UnsafeCell; use std::mem::MaybeUninit; + fn aes_ctr_by_key_size(ks: usize) -> Cipher { + match ks { + 16 => Cipher::aes_128_ctr(), + 24 => Cipher::aes_192_ctr(), + 32 => Cipher::aes_256_ctr(), + _ => { + panic!("AES supports 128, 192, or 256 bits keys"); + } + } + } + fn aes_gcm_by_key_size(ks: usize) -> Cipher { match ks { 16 => Cipher::aes_128_gcm(), @@ -390,6 +496,53 @@ mod openssl_aes { unsafe impl Send for Aes {} unsafe impl Sync for Aes {} + pub struct AesCtr(Secret<32>, usize, Option); + + impl AesCtr { + /// Construct a new AES-CTR cipher. + /// Key must be 16, 24, or 32 bytes in length or a panic will occur. + #[inline(always)] + pub fn new(k: &[u8]) -> Self { + let mut s: Secret<32> = Secret::default(); + match k.len() { + 16 | 24 | 32 => { + s.0[..k.len()].copy_from_slice(k); + Self(s, k.len(), None) + } + _ => { + panic!("AES supports 128, 192, or 256 bits keys"); + } + } + } + + /// Initialize AES-CTR for encryption or decryption with the given IV. + /// If it's already been used, this also resets the cipher. There is no separate reset. + #[inline(always)] + pub fn reset_set_iv(&mut self, iv: &[u8]) { + let mut c = Crypter::new(aes_ctr_by_key_size(self.1), Mode::Encrypt, &self.0 .0[..self.1], Some(iv)).unwrap(); + c.pad(false); + let _ = self.2.replace(c); + } + + /// Encrypt or decrypt (same operation with CTR mode) + #[inline(always)] + pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) { + let _ = self.2.as_mut().unwrap().update(input, output); + } + + /// Encrypt or decrypt in place (same operation with CTR mode) + #[inline(always)] + pub fn crypt_in_place(&mut self, data: &mut [u8]) { + let _ = self + .2 + .as_mut() + .unwrap() + .update(unsafe { &*std::slice::from_raw_parts(data.as_ptr(), data.len()) }, data); + } + } + + unsafe impl Send for AesCtr {} + pub struct AesGcm(Secret<32>, usize, CipherCtx, bool); impl AesGcm { @@ -479,10 +632,10 @@ mod openssl_aes { } #[cfg(target_os = "macos")] -pub use fruit_flavored::{Aes, AesGcm}; +pub use fruit_flavored::{Aes, AesCtr, AesGcm}; #[cfg(not(target_os = "macos"))] -pub use openssl_aes::{Aes, AesGcm}; +pub use openssl_aes::{Aes, AesCtr, AesGcm}; #[cfg(test)] mod tests { diff --git a/crypto/src/hash.rs b/crypto/src/hash.rs index 85131f54e..d148517c2 100644 --- a/crypto/src/hash.rs +++ b/crypto/src/hash.rs @@ -7,6 +7,8 @@ use std::ptr::null; pub const SHA512_HASH_SIZE: usize = 64; pub const SHA384_HASH_SIZE: usize = 48; +pub const HMAC_SHA512_SIZE: usize = 64; +pub const HMAC_SHA384_SIZE: usize = 48; pub struct SHA512(Option); diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 94ab7ff7f..685bccde7 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -3,6 +3,7 @@ pub mod aes; pub mod aes_gmac_siv; pub mod hash; +pub mod mimcvdf; pub mod p384; pub mod poly1305; pub mod random; @@ -27,3 +28,13 @@ pub fn secure_eq + ?Sized, B: AsRef<[u8]> + ?Sized>(a: &A, b: &B) false } } + +extern "C" { + fn OPENSSL_cleanse(ptr: *mut std::ffi::c_void, len: usize); +} + +/// Destroy the contents of some memory +#[inline(always)] +pub fn burn(b: &mut [u8]) { + unsafe { OPENSSL_cleanse(b.as_mut_ptr().cast(), b.len()) }; +} diff --git a/crypto/src/mimcvdf.rs b/crypto/src/mimcvdf.rs new file mode 100644 index 000000000..bfb49921f --- /dev/null +++ b/crypto/src/mimcvdf.rs @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +/* + * MIMC is a hash function originally designed for use with STARK and SNARK proofs. It's based + * on modular multiplication and exponentiation instead of the usual bit twiddling or ARX + * operations that underpin more common hash algorithms. + * + * It's useful as a verifiable delay function because it can be computed in both directions with + * one direction taking orders of magnitude longer than the other. The "backward" direction is + * used as the delay function as it requires modular exponentiation which is inherently more + * compute intensive. The "forward" direction simply requires modular cubing which is two modular + * multiplications and is much faster. + * + * It's also nice because it's incredibly simple with a tiny code footprint. + * + * This is used for anti-DOS and anti-spamming delay functions. It's not used for anything + * really "cryptographically hard," and if it were broken cryptographically it would still be + * useful as a VDF as long as the break didn't yield a significantly faster way of computing a + * delay proof than the straightforward iterative way implemented here. + * + * Here are two references on MIMC with the first being the original paper and the second being + * a blog post describing its use as a VDF. + * + * https://eprint.iacr.org/2016/492.pdf + * https://vitalik.ca/general/2018/07/21/starks_part_3.html + */ + +// p = 2^127 - 39, the largest 127-bit prime of the form 6k + 5 +const PRIME: u128 = 170141183460469231731687303715884105689; + +// (2p - 1) / 3 +const PRIME_2P_MINUS_1_DIV_3: u128 = 113427455640312821154458202477256070459; + +// Randomly generated round constants, each modulo PRIME. +const K_COUNT_MASK: usize = 31; +const K: [u128; 32] = [ + 0x1fdd07a761b611bb1ab9419a70599a7c, + 0x23056b05d5c6b925e333d7418047650a, + 0x77a638f9b437a307f8866fbd2672c705, + 0x60213dab83bab91d1c310bd87e9da332, + 0xf56bc883301ab373179e46b098b7a7, + 0x7914a0dbd2f971344173b350c28a838, + 0x44bb64af5e446e6ebdc068d10d318f26, + 0x1bca1921fd328bb725ae0cbcbc20a263, + 0xafa963242f5216a7da1cd5328b23659, + 0x7fe17c43782b883a63ee0a790e0b2b77, + 0x23bb62abf728bf453200ee528f902c33, + 0x75ec0c055be14955db6878567e3c0465, + 0x7902bb57876e0b08b4de02a66755e5d7, + 0xe5d7094f37b615f5a1e1594b0390de8, + 0x12d4ddee90653a26f5de63ff4651f2d, + 0xce4a15bc35633b5ed8bcae2c93d739c, + 0x23f25b935e52df87255db8c608ef9ab4, + 0x611a08d7464fb984c98104d77f1609a7, + 0x7aa825876a7f6acde5efa57992da9c43, + 0x2be9686f630fa28a0a0e1081a59755b4, + 0x50060dac9ac4656ba3f8ee7592f4e28a, + 0x4113abff6f5bb303eac2ca809d4d529d, + 0x2af9d01d4e753feb5834c14ca0543397, + 0x73c2d764691ced2b823dda887e22ae85, + 0x5b53dcd4750ff888dca2497cec4dacb7, + 0x5d8984a52c2d8f3cc9bcf61ef29f8a1, + 0x588d8cc99533d649aabb5f0f552140e, + 0x4dae04985fde8c8464ba08aaa7d8761e, + 0x53f0c4740b8c3bda3fc05109b9a2b71, + 0x3e918c88a6795e3bf840e0b74d91b9d7, + 0x1dbcb30d724f11200aebb1dff87def91, + 0x6086b0af0e1e68558170239d23be9780, +]; + +fn mulmod(mut a: u128, mut b: u128) -> u128 { + let mut res: u128 = 0; + a %= M; + loop { + if (b & 1) != 0 { + res = res.wrapping_add(a) % M; + } + b = b.wrapping_shr(1); + if b != 0 { + a = a.wrapping_shl(1) % M; + } else { + return res; + } + } +} + +#[inline(always)] +fn powmod(mut base: u128, mut exp: u128) -> u128 { + let mut res: u128 = 1; + loop { + if (exp & 1) != 0 { + res = mulmod::(base, res); + } + exp = exp.wrapping_shr(1); + if exp != 0 { + base = mulmod::(base, base); + } else { + return res; + } + } +} + +/// Compute MIMC for the given number of iterations and return a proof that can be checked much more quickly. +pub fn delay(mut input: u128, rounds: usize) -> u128 { + debug_assert!(rounds > 0); + input %= PRIME; + for r in 1..(rounds + 1) { + input = powmod::(input ^ K[(rounds - r) & K_COUNT_MASK], PRIME_2P_MINUS_1_DIV_3); + } + input +} + +/// Quickly verify the result of delay() given the returned proof, original input, and original number of rounds. +pub fn verify(mut proof: u128, original_input: u128, rounds: usize) -> bool { + debug_assert!(rounds > 0); + for r in 0..rounds { + proof = mulmod::(proof, mulmod::(proof, proof)) ^ K[r & K_COUNT_MASK]; + } + proof == (original_input % PRIME) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn delay_and_verify() { + for i in 1..5 { + let input = (crate::random::xorshift64_random() as u128).wrapping_mul(crate::random::xorshift64_random() as u128); + let proof = delay(input, i * 3); + //println!("{}", proof); + assert!(verify(proof, input, i * 3)); + } + } +} diff --git a/crypto/src/secret.rs b/crypto/src/secret.rs index 666b30a7d..8705f37cf 100644 --- a/crypto/src/secret.rs +++ b/crypto/src/secret.rs @@ -37,6 +37,11 @@ impl Secret { &self.0 } + #[inline(always)] + pub fn as_bytes_mut(&mut self) -> &mut [u8; L] { + &mut self.0 + } + /// Get the first N bytes of this secret as a fixed length array. #[inline(always)] pub fn first_n(&self) -> &[u8; N] { diff --git a/utils/src/arrayvec.rs b/utils/src/arrayvec.rs index db76c5ebf..1018d6c8b 100644 --- a/utils/src/arrayvec.rs +++ b/utils/src/arrayvec.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::fmt::Debug; use std::io::Write; diff --git a/utils/src/blob.rs b/utils/src/blob.rs index 0ad7b477e..b02f7902b 100644 --- a/utils/src/blob.rs +++ b/utils/src/blob.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::fmt::Debug; diff --git a/utils/src/buffer.rs b/utils/src/buffer.rs index d7dfbf96d..cfffef323 100644 --- a/utils/src/buffer.rs +++ b/utils/src/buffer.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::error::Error; use std::fmt::{Debug, Display}; diff --git a/utils/src/defer.rs b/utils/src/defer.rs index b66b983b4..da8e224e0 100644 --- a/utils/src/defer.rs +++ b/utils/src/defer.rs @@ -1,4 +1,11 @@ -/// Defer execution of a closure until dropped. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + struct Defer(Option); impl Drop for Defer { @@ -8,6 +15,9 @@ impl Drop for Defer { } /// Defer execution of a closure until the return value is dropped. +/// +/// This mimics the defer statement in Go, allowing you to always do some cleanup at +/// the end of a function no matter where it exits. pub fn defer(f: F) -> impl Drop { Defer(Some(f)) } diff --git a/utils/src/dictionary.rs b/utils/src/dictionary.rs index d54c809c8..f9dd18b73 100644 --- a/utils/src/dictionary.rs +++ b/utils/src/dictionary.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::collections::BTreeMap; use std::io::Write; diff --git a/utils/src/error.rs b/utils/src/error.rs index 1a23ea274..a958e77cd 100644 --- a/utils/src/error.rs +++ b/utils/src/error.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::error::Error; use std::fmt::{Debug, Display}; diff --git a/utils/src/exitcode.rs b/utils/src/exitcode.rs index ad9c8e4ba..7cb96c3fa 100644 --- a/utils/src/exitcode.rs +++ b/utils/src/exitcode.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ // These were taken from BSD sysexits.h to provide some standard for process exit codes. diff --git a/utils/src/gate.rs b/utils/src/gate.rs index f869507f0..037edcc26 100644 --- a/utils/src/gate.rs +++ b/utils/src/gate.rs @@ -1,6 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. - -//use std::sync::atomic::{AtomicI64, Ordering}; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ /// Boolean rate limiter with normal (non-atomic) semantics. #[repr(transparent)] diff --git a/utils/src/gatherarray.rs b/utils/src/gatherarray.rs index 4442322b8..091a77354 100644 --- a/utils/src/gatherarray.rs +++ b/utils/src/gatherarray.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::mem::{size_of, MaybeUninit}; use std::ptr::copy_nonoverlapping; diff --git a/utils/src/hex.rs b/utils/src/hex.rs index 7c3ace132..d9438e0c9 100644 --- a/utils/src/hex.rs +++ b/utils/src/hex.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ pub const HEX_CHARS: [u8; 16] = [ b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f', diff --git a/utils/src/io.rs b/utils/src/io.rs index 89520ccda..a67f83b4a 100644 --- a/utils/src/io.rs +++ b/utils/src/io.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::fs::File; use std::io::Read; diff --git a/utils/src/json.rs b/utils/src/json.rs index 71c825290..8b76fd10e 100644 --- a/utils/src/json.rs +++ b/utils/src/json.rs @@ -1,3 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + use serde::de::DeserializeOwned; use serde::Serialize; use serde_json::ser::Formatter; diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 0ddab812b..c9dc4da25 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ pub mod arrayvec; pub mod blob; diff --git a/utils/src/marshalable.rs b/utils/src/marshalable.rs index 520ee1ec2..ffe6c8dcf 100644 --- a/utils/src/marshalable.rs +++ b/utils/src/marshalable.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::error::Error; use std::fmt::{Debug, Display}; diff --git a/utils/src/memory.rs b/utils/src/memory.rs index 281c86c12..9710a9eb8 100644 --- a/utils/src/memory.rs +++ b/utils/src/memory.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ #[allow(unused_imports)] use std::mem::{needs_drop, size_of, MaybeUninit}; diff --git a/utils/src/pool.rs b/utils/src/pool.rs index 2ed8df323..08ab98464 100644 --- a/utils/src/pool.rs +++ b/utils/src/pool.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; diff --git a/utils/src/reaper.rs b/utils/src/reaper.rs index ecfbe0b44..60624c20a 100644 --- a/utils/src/reaper.rs +++ b/utils/src/reaper.rs @@ -1,3 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + use std::collections::VecDeque; use std::sync::Arc; diff --git a/utils/src/ringbuffer.rs b/utils/src/ringbuffer.rs index 27c3d1a2d..225fbf58d 100644 --- a/utils/src/ringbuffer.rs +++ b/utils/src/ringbuffer.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::mem::MaybeUninit; diff --git a/utils/src/ringbuffermap.rs b/utils/src/ringbuffermap.rs index aa1b2482d..c438bb642 100644 --- a/utils/src/ringbuffermap.rs +++ b/utils/src/ringbuffermap.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::hash::{Hash, Hasher}; use std::mem::MaybeUninit; @@ -102,8 +108,8 @@ struct Entry { /// buckets in the hash table. The maximum for both these parameters is 65535. This could be /// increased by making the index variables larger (e.g. u32 instead of u16). pub struct RingBufferMap { - salt: u32, entries: [Entry; C], + salt: u32, buckets: [u16; B], entry_ptr: u16, } diff --git a/utils/src/sync.rs b/utils/src/sync.rs index edebd1f46..baf63e339 100644 --- a/utils/src/sync.rs +++ b/utils/src/sync.rs @@ -1,3 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; /// Variant version of lock for RwLock with automatic conversion to a write lock as needed. diff --git a/utils/src/thing.rs b/utils/src/thing.rs index 7415b96e1..ff11d0ec5 100644 --- a/utils/src/thing.rs +++ b/utils/src/thing.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::any::TypeId; use std::mem::{forget, size_of, MaybeUninit}; diff --git a/utils/src/varint.rs b/utils/src/varint.rs index 76e4750f0..2445d38f3 100644 --- a/utils/src/varint.rs +++ b/utils/src/varint.rs @@ -1,4 +1,10 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ use std::io::{Read, Write}; diff --git a/zssp/Cargo.toml b/zssp/Cargo.toml index 924691225..d273a4013 100644 --- a/zssp/Cargo.toml +++ b/zssp/Cargo.toml @@ -5,6 +5,22 @@ license = "MPL-2.0" name = "zssp" version = "0.1.0" +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = 'abort' + +[lib] +name = "zssp" +path = "src/lib.rs" +doc = true + +[[bin]] +name = "zssp_test" +path = "src/main.rs" +doc = false + [dependencies] zerotier-utils = { path = "../utils" } zerotier-crypto = { path = "../crypto" } diff --git a/zssp/src/applicationlayer.rs b/zssp/src/applicationlayer.rs index 6a366d590..145510dcc 100644 --- a/zssp/src/applicationlayer.rs +++ b/zssp/src/applicationlayer.rs @@ -1,77 +1,78 @@ -use std::ops::Deref; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ -use zerotier_crypto::{ - p384::{P384KeyPair, P384PublicKey}, - secret::Secret, -}; - -use crate::{ - sessionid::SessionId, - zssp::{ReceiveContext, Session}, -}; +use zerotier_crypto::p384::P384KeyPair; /// Trait to implement to integrate the session into an application. /// /// Templating the session on this trait lets the code here be almost entirely transport, OS, /// and use case independent. +/// +/// The constants exposed in this trait can be redefined from their defaults to change rekey +/// and negotiation timeout behavior. This is discouraged except for testing purposes when low +/// key lifetime values may be desirable to test rekeying. Also note that each side takes turns +/// initiating rekey, so if both sides don't have the same values you'll get asymmetric timing +/// behavior. This will still work as long as the key usage counter doesn't exceed the +/// EXPIRE_AFTER_USES limit. pub trait ApplicationLayer: Sized { - /// Arbitrary opaque object associated with a session, such as a connection state object. + /// Rekey after this many key uses. + /// + /// The default is 1/4 the recommended NIST limit for AES-GCM. Unless you are transferring + /// a massive amount of data REKEY_AFTER_TIME_MS is probably going to kick in first. + const REKEY_AFTER_USES: u64 = 536870912; + + /// Hard expiration after this many uses. + /// + /// Attempting to encrypt more than this many messages with a key will cause a hard error + /// and the internal erasure of ephemeral key material. You'll only ever hit this if something + /// goes wrong and rekeying fails. + const EXPIRE_AFTER_USES: u64 = 2147483648; + + /// Start attempting to rekey after a key has been in use for this many milliseconds. + /// + /// Default is two hours. + const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60 * 2; + + /// Maximum random jitter to add to rekey-after time. + /// + /// Default is ten minutes. + const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; + + /// Timeout for incoming Noise_XK session negotiation in milliseconds. + /// + /// Default is two seconds, which should be enough for even extremely slow links or links + /// over very long distances. + const INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS: i64 = 2000; + + /// Retry interval for outgoing connection initiation or rekey attempts. + /// + /// Retry attepmpts will be no more often than this, but the delay may end up being slightly more + /// in some cases depending on where in the cycle the initial attempt falls. + const RETRY_INTERVAL: i64 = 500; + + /// Type for arbitrary opaque object for use by the application that is attached to each session. type Data; - /// Arbitrary object that dereferences to the session, such as Arc>. - type SessionRef<'a>: Deref>; - - /// A buffer containing data read from the network that can be cached. + /// Data type for incoming packet buffers. /// - /// This can be e.g. a pooled buffer that automatically returns itself to the pool when dropped. - /// It can also just be a Vec or Box<[u8]> or something like that. + /// 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: AsRef<[u8]> + AsMut<[u8]>; - /// Remote physical address on whatever transport this session is using. - type RemoteAddress; - - /// Rate limit for attempts to rekey existing sessions in milliseconds (default: 2000). - const REKEY_RATE_LIMIT_MS: i64 = 2000; - /// Get a reference to this host's static public key blob. /// /// This must contain a NIST P-384 public key but can contain other information. In ZeroTier this /// is a byte serialized identity. It could just be a naked NIST P-384 key if that's all you need. fn get_local_s_public_blob(&self) -> &[u8]; - /// Get SHA384(this host's static public key blob). - /// - /// This allows us to avoid computing SHA384(public key blob) over and over again. - fn get_local_s_public_blob_hash(&self) -> &[u8; 48]; - - /// Get a reference to this hosts' static public key's NIST P-384 secret key pair. + /// Get a reference to this host's static public key's NIST P-384 secret key pair. /// /// This must return the NIST P-384 public key that is contained within the static public key blob. fn get_local_s_keypair(&self) -> &P384KeyPair; - - /// Extract the NIST P-384 ECC public key component from a static public key blob or return None on failure. - /// - /// This is called to parse the static public key blob from the other end and extract its NIST P-384 public - /// key. SECURITY NOTE: the information supplied here is from the wire so care must be taken to parse it - /// safely and fail on any error or corruption. - fn extract_s_public_from_raw(static_public: &[u8]) -> Option; - - /// Look up a local session by local session ID or return None if not found. - fn lookup_session<'a>(&self, local_session_id: SessionId) -> Option>; - - /// Rate limit and check an attempted new session (called before accept_new_session). - fn check_new_session(&self, rc: &ReceiveContext, remote_address: &Self::RemoteAddress) -> bool; - - /// Check whether a new session should be accepted. - /// - /// On success a tuple of local session ID, static secret, and associated object is returned. The - /// static secret is whatever results from agreement between the local and remote static public - /// keys. - fn accept_new_session( - &self, - receive_context: &ReceiveContext, - remote_address: &Self::RemoteAddress, - remote_static_public: &[u8], - remote_metadata: &[u8], - ) -> Option<(SessionId, Secret<64>, Self::Data)>; } diff --git a/zssp/src/constants.rs b/zssp/src/constants.rs deleted file mode 100644 index f38973cf1..000000000 --- a/zssp/src/constants.rs +++ /dev/null @@ -1,113 +0,0 @@ -/// Minimum size of a valid physical ZSSP packet or packet fragment. -pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; - -/// Minimum physical MTU for ZSSP to function. -pub const MIN_TRANSPORT_MTU: usize = 64; - -/// Minimum recommended interval between calls to service() on each session, in milliseconds. -pub const SERVICE_INTERVAL: u64 = 10000; - -/// Setting this to true enables kyber1024 post-quantum forward secrecy. -/// -/// Kyber1024 is used for data forward secrecy but not authentication. Authentication would -/// require Kyber1024 in identities, which would make them huge, and isn't needed for our -/// threat model which is data warehousing today to decrypt tomorrow. Breaking authentication -/// is only relevant today, not in some mid to far future where a QC that can break 384-bit ECC -/// exists. -/// -/// This is normally enabled but could be disabled at build time for e.g. very small devices. -/// It might not even be necessary there to disable it since it's not that big and is usually -/// faster than NIST P-384 ECDH. -pub(crate) const JEDI: bool = true; - -/// Maximum number of fragments for data packets. -pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 - -/// Maximum number of fragments for key exchange packets (can be smaller to save memory, only a few needed) -pub(crate) const KEY_EXCHANGE_MAX_FRAGMENTS: usize = 2; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. - -/// Start attempting to rekey after a key has been used to send packets this many times. -/// This is 1/4 the recommended NIST limit for AES-GCM key lifetimes under most conditions. -pub(crate) const REKEY_AFTER_USES: u64 = 536870912; - -/// Hard expiration after this many uses. -/// -/// Use of the key beyond this point is prohibited. If we reach this number of key uses -/// the key will be destroyed in memory and the session will cease to function. A hard -/// error is also generated. -pub(crate) const EXPIRE_AFTER_USES: u64 = REKEY_AFTER_USES * 2; - -/// Start attempting to rekey after a key has been in use for this many milliseconds. -pub(crate) const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour - -/// Maximum random jitter to add to rekey-after time. -pub(crate) const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 minutes - -/// Version 0: AES-256-GCM + NIST P-384 + optional Kyber1024 PQ forward secrecy -pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; - -/// Secondary key type: none, use only P-384 for forward secrecy. -pub(crate) const HYBRID_KEY_TYPE_NONE: u8 = 0; - -/// Secondary key type: Kyber1024, PQ forward secrecy enabled. -pub(crate) const HYBRID_KEY_TYPE_KYBER1024: u8 = 1; - -/// Size of packet header -pub(crate) const HEADER_SIZE: usize = 16; - -/// Start of single block AES encryption of a portion of the header (and some data). -pub(crate) const HEADER_CHECK_ENCRYPT_START: usize = 6; - -/// End of single block AES encryption of a portion of the header (and some data). -pub(crate) const HEADER_CHECK_ENCRYPT_END: usize = 22; - -/// Size of AES-GCM keys (256 bits) -pub(crate) const AES_KEY_SIZE: usize = 32; - -/// Size of AES-GCM MAC tags -pub(crate) const AES_GCM_TAG_SIZE: usize = 16; - -/// Size of HMAC-SHA384 MAC tags -pub(crate) const HMAC_SIZE: usize = 48; - -/// Size of a session ID, which behaves a bit like a TCP port number. -/// -/// This is large since some ZeroTier nodes handle huge numbers of links, like roots and controllers. -pub(crate) const SESSION_ID_SIZE: usize = 6; - -/// Maximum difference between out-of-order incoming packet counters, and size of deduplication buffer. -pub(crate) const COUNTER_WINDOW_MAX_OUT_OF_ORDER: usize = 16; - -/// Maximum skip-ahead for counter. -/// -/// This is huge (2^24) because its real purpose is to filter out bad packets where decryption of -/// the counter yields an invalid value. -pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 16777216; - -// Packet types can range from 0 to 15 (4 bits) -- 0-3 are defined and 4-15 are reserved for future use -pub(crate) const PACKET_TYPE_DATA: u8 = 0; -pub(crate) const PACKET_TYPE_INITIAL_KEY_OFFER: u8 = 1; // "alice" -pub(crate) const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 2; // "bob" - -// Key usage labels for sub-key derivation using NIST-style KBKDF (basically just HMAC KDF). -pub(crate) const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'M'; // HMAC-SHA384 authentication for key exchanges -pub(crate) const KBKDF_KEY_USAGE_LABEL_HEADER_CHECK: u8 = b'H'; // AES-based header check code generation -pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction -pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction -pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHETING: u8 = b'R'; // Key input for next ephemeral ratcheting - -// AES key size for header check code generation -pub(crate) const HEADER_CHECK_AES_KEY_SIZE: usize = 16; - -/// Aribitrary starting value for master key derivation. -/// -/// It doesn't matter very much what this is but it's good for it to be unique. It should -/// be changed if this code is changed in any cryptographically meaningful way like changing -/// the primary algorithm from NIST P-384 or the transport cipher from AES-GCM. -pub(crate) const INITIAL_KEY: [u8; 64] = [ - // macOS command line to generate: - // echo -n 'ZSSP_Noise_IKpsk2_NISTP384_?KYBER1024_AESGCM_SHA512' | shasum -a 512 | cut -d ' ' -f 1 | xxd -r -p | xxd -i - 0x35, 0x6a, 0x75, 0xc0, 0xbf, 0xbe, 0xc3, 0x59, 0x70, 0x94, 0x50, 0x69, 0x4c, 0xa2, 0x08, 0x40, 0xc7, 0xdf, 0x67, 0xa8, 0x68, 0x52, - 0x6e, 0xd5, 0xdd, 0x77, 0xec, 0x59, 0x6f, 0x8e, 0xa1, 0x99, 0xb4, 0x32, 0x85, 0xaf, 0x7f, 0x0d, 0xa9, 0x6c, 0x01, 0xfb, 0x72, 0x46, - 0xc0, 0x09, 0x58, 0xb8, 0xe0, 0xa8, 0xcf, 0xb1, 0x58, 0x04, 0x6e, 0x32, 0xba, 0xa8, 0xb8, 0xf9, 0x0a, 0xa4, 0xbf, 0x36, -]; diff --git a/zssp/src/error.rs b/zssp/src/error.rs index 20ab254cc..e416a8954 100644 --- a/zssp/src/error.rs +++ b/zssp/src/error.rs @@ -1,8 +1,14 @@ -use crate::sessionid::SessionId; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ pub enum Error { /// The packet was addressed to an unrecognized local session (should usually be ignored) - UnknownLocalSessionId(SessionId), + UnknownLocalSessionId, /// Packet was not well formed InvalidPacket, @@ -15,18 +21,12 @@ pub enum Error { /// There is a safe way to reply if absolutely necessary, by sending the reply back after a constant amount of time, but this is difficult to get correct. FailedAuthentication, - /// New session was rejected by the application layer. - NewSessionRejected, - /// Rekeying failed and session secret has reached its hard usage count limit MaxKeyLifetimeExceeded, /// Attempt to send using session without established key SessionNotEstablished, - /// Packet ignored by rate limiter. - RateLimited, - /// The other peer specified an unrecognized protocol version UnknownProtocolVersion, @@ -36,6 +36,9 @@ pub enum Error { /// Data object is too large to send, even with fragmentation DataTooLarge, + /// Packet counter was outside window or packet arrived with session in an unexpected state. + OutOfSequence, + /// An unexpected buffer overrun occured while attempting to encode or decode a packet. /// /// This can only ever happen if exceptionally large key blobs or metadata are being used, @@ -43,22 +46,29 @@ pub enum Error { UnexpectedBufferOverrun, } +// An I/O error in the parser means an invalid packet. +impl From for Error { + #[inline(always)] + fn from(_: std::io::Error) -> Self { + Self::InvalidPacket + } +} + impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::UnknownLocalSessionId(id) => f.write_str(format!("UnknownLocalSessionId({})", id).as_str()), - Self::InvalidPacket => f.write_str("InvalidPacket"), - Self::InvalidParameter => f.write_str("InvalidParameter"), - Self::FailedAuthentication => f.write_str("FailedAuthentication"), - Self::NewSessionRejected => f.write_str("NewSessionRejected"), - Self::MaxKeyLifetimeExceeded => f.write_str("MaxKeyLifetimeExceeded"), - Self::SessionNotEstablished => f.write_str("SessionNotEstablished"), - Self::RateLimited => f.write_str("RateLimited"), - Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"), - Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"), - Self::DataTooLarge => f.write_str("DataTooLarge"), - Self::UnexpectedBufferOverrun => f.write_str("UnexpectedBufferOverrun"), - } + f.write_str(match self { + Self::UnknownLocalSessionId => "UnknownLocalSessionId", + Self::InvalidPacket => "InvalidPacket", + Self::InvalidParameter => "InvalidParameter", + Self::FailedAuthentication => "FailedAuthentication", + Self::MaxKeyLifetimeExceeded => "MaxKeyLifetimeExceeded", + Self::SessionNotEstablished => "SessionNotEstablished", + Self::UnknownProtocolVersion => "UnknownProtocolVersion", + Self::DataBufferTooSmall => "DataBufferTooSmall", + Self::DataTooLarge => "DataTooLarge", + Self::OutOfSequence => "OutOfSequence", + Self::UnexpectedBufferOverrun => "UnexpectedBufferOverrun", + }) } } diff --git a/zssp/src/lib.rs b/zssp/src/lib.rs index 0c05db40f..256a6909a 100644 --- a/zssp/src/lib.rs +++ b/zssp/src/lib.rs @@ -1,12 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + mod applicationlayer; mod error; +mod proto; mod sessionid; mod tests; mod zssp; -pub mod constants; - pub use crate::applicationlayer::ApplicationLayer; pub use crate::error::Error; +pub use crate::proto::{MAX_INIT_PAYLOAD_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU}; pub use crate::sessionid::SessionId; -pub use crate::zssp::{ReceiveContext, ReceiveResult, Role, Session}; +pub use crate::zssp::{Context, ReceiveResult, Session}; diff --git a/zssp/src/main.rs b/zssp/src/main.rs new file mode 100644 index 000000000..f22f9c3a1 --- /dev/null +++ b/zssp/src/main.rs @@ -0,0 +1,219 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; +use zerotier_crypto::secret::Secret; +use zerotier_utils::ms_monotonic; + +const TEST_MTU: usize = 1500; + +struct TestApplication { + identity_key: P384KeyPair, +} + +impl zssp::ApplicationLayer for TestApplication { + type Data = (); + + type IncomingPacketBuffer = Vec; + + fn get_local_s_public_blob(&self) -> &[u8] { + self.identity_key.public_key_bytes() + } + + fn get_local_s_keypair(&self) -> &zerotier_crypto::p384::P384KeyPair { + &self.identity_key + } +} + +fn alice_main( + run: &AtomicBool, + alice_app: &TestApplication, + bob_app: &TestApplication, + alice_out: mpsc::SyncSender>, + alice_in: mpsc::Receiver>, +) { + let context = zssp::Context::::new(16); + let mut data_buf = [0u8; 65536]; + let mut next_service = ms_monotonic() + 500; + + let alice_session = context + .open( + alice_app, + |b| { + let _ = alice_out.send(b.to_vec()); + }, + TEST_MTU, + bob_app.identity_key.public_key(), + Secret::default(), + None, + (), + ms_monotonic(), + ) + .unwrap(); + + println!("[alice] opening session {}", alice_session.id.to_string()); + + let test_data = [1u8; 10000]; + let mut up = false; + + while run.load(Ordering::Relaxed) { + let pkt = alice_in.try_recv(); + let current_time = ms_monotonic(); + + if let Ok(pkt) = pkt { + //println!("bob >> alice {}", pkt.len()); + match context.receive( + alice_app, + || true, + |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), + |_, b| { + let _ = alice_out.send(b.to_vec()); + }, + &mut data_buf, + pkt, + TEST_MTU, + current_time, + ) { + Ok(zssp::ReceiveResult::Ok) => { + //println!("[alice] ok"); + } + Ok(zssp::ReceiveResult::OkData(_, _)) => { + //println!("[alice] received {}", data.len()); + } + Ok(zssp::ReceiveResult::OkNewSession(s)) => { + println!("[alice] new session {}", s.id.to_string()); + } + Ok(zssp::ReceiveResult::Rejected) => {} + Err(e) => { + println!("[alice] ERROR {}", e.to_string()); + } + } + } + + if up { + assert!(alice_session + .send( + |b| { + let _ = alice_out.send(b.to_vec()); + }, + &mut data_buf[..TEST_MTU], + &test_data[..2048 + ((zerotier_crypto::random::xorshift64_random() as usize) % (test_data.len() - 2048))], + ) + .is_ok()); + } else { + if alice_session.established() { + up = true; + } + } + + if current_time >= next_service { + next_service = current_time + + context.service( + |_, b| { + let _ = alice_out.send(b.to_vec()); + }, + TEST_MTU, + current_time, + ); + } + } +} + +fn bob_main( + run: &AtomicBool, + _alice_app: &TestApplication, + bob_app: &TestApplication, + bob_out: mpsc::SyncSender>, + bob_in: mpsc::Receiver>, +) { + let context = zssp::Context::::new(16); + let mut data_buf = [0u8; 65536]; + let mut last_speed_metric = ms_monotonic(); + let mut next_service = last_speed_metric + 500; + let mut transferred = 0u64; + + let mut bob_session = None; + + while run.load(Ordering::Relaxed) { + let pkt = bob_in.recv_timeout(Duration::from_millis(10)); + let current_time = ms_monotonic(); + + if let Ok(pkt) = pkt { + //println!("alice >> bob {}", pkt.len()); + match context.receive( + bob_app, + || true, + |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), + |_, b| { + let _ = bob_out.send(b.to_vec()); + }, + &mut data_buf, + pkt, + TEST_MTU, + current_time, + ) { + Ok(zssp::ReceiveResult::Ok) => { + //println!("[bob] ok"); + } + Ok(zssp::ReceiveResult::OkData(_, data)) => { + //println!("[bob] received {}", data.len()); + transferred += data.len() as u64; + } + Ok(zssp::ReceiveResult::OkNewSession(s)) => { + println!("[bob] new session {}", s.id.to_string()); + let _ = bob_session.replace(s); + } + Ok(zssp::ReceiveResult::Rejected) => {} + Err(e) => { + println!("[bob] ERROR {}", e.to_string()); + } + } + } + + let speed_metric_elapsed = current_time - last_speed_metric; + if speed_metric_elapsed >= 1000 { + last_speed_metric = current_time; + println!( + "[bob] RX speed {} MiB/sec", + ((transferred as f64) / 1048576.0) / ((speed_metric_elapsed as f64) / 1000.0) + ); + transferred = 0; + } + + if current_time >= next_service { + next_service = current_time + + context.service( + |_, b| { + let _ = bob_out.send(b.to_vec()); + }, + TEST_MTU, + current_time, + ); + } + } +} + +fn main() { + let run = AtomicBool::new(true); + + let alice_app = TestApplication { identity_key: P384KeyPair::generate() }; + let bob_app = TestApplication { identity_key: P384KeyPair::generate() }; + + let (alice_out, bob_in) = mpsc::sync_channel::>(128); + let (bob_out, alice_in) = mpsc::sync_channel::>(128); + + thread::scope(|ts| { + let alice_thread = ts.spawn(|| alice_main(&run, &alice_app, &bob_app, alice_out, alice_in)); + let bob_thread = ts.spawn(|| bob_main(&run, &alice_app, &bob_app, bob_out, bob_in)); + + thread::sleep(Duration::from_secs(60 * 10)); + + run.store(false, Ordering::SeqCst); + let _ = alice_thread.join(); + let _ = bob_thread.join(); + }); + + std::process::exit(0); +} diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs new file mode 100644 index 000000000..210107efd --- /dev/null +++ b/zssp/src/proto.rs @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +use std::mem::size_of; + +use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_PUBLICKEYBYTES}; +use zerotier_crypto::hash::{HMAC_SHA384_SIZE, SHA384_HASH_SIZE}; +use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE; + +use crate::error::Error; +use crate::sessionid::SessionId; + +/// Minimum size of a valid physical ZSSP packet of any type. Anything smaller is discarded. +pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; + +/// Minimum physical MTU for ZSSP to function. +pub const MIN_TRANSPORT_MTU: usize = 128; + +/// Maximum combined size of static public blob and metadata. +pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_XK_ACK_MIN_SIZE; + +pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; + +pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 16; +pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 16777216; + +pub(crate) const PACKET_TYPE_DATA: u8 = 0; +pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_INIT: u8 = 1; +pub(crate) const PACKET_TYPE_BOB_NOISE_XK_ACK: u8 = 2; +pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_ACK: u8 = 3; +pub(crate) const PACKET_TYPE_ALICE_REKEY_INIT: u8 = 4; +pub(crate) const PACKET_TYPE_BOB_REKEY_ACK: u8 = 5; + +pub(crate) const HEADER_SIZE: usize = 16; +pub(crate) const HEADER_PROTECT_ENCRYPT_START: usize = 6; +pub(crate) const HEADER_PROTECT_ENCRYPT_END: usize = 22; + +pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION: u8 = b'X'; // intermediate keys used in key exchanges +pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION: u8 = b'x'; // intermediate keys used in key exchanges +pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction +pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction +pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHET: u8 = b'R'; // Key used in derivatin of next session key + +pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 +pub(crate) const MAX_NOISE_HANDSHAKE_FRAGMENTS: usize = 16; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. +pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS * MIN_TRANSPORT_MTU; + +pub(crate) const BASE_KEY_SIZE: usize = 64; + +pub(crate) const AES_256_KEY_SIZE: usize = 32; +pub(crate) const AES_HEADER_PROTECTION_KEY_SIZE: usize = 16; +pub(crate) const AES_GCM_TAG_SIZE: usize = 16; +pub(crate) const AES_GCM_NONCE_SIZE: usize = 12; + +/// The first packet in Noise_XK exchange containing Alice's ephemeral keys, session ID, and a random +/// symmetric key to protect header fragmentation fields for this session. +#[allow(unused)] +#[repr(C, packed)] +pub(crate) struct AliceNoiseXKInit { + pub header: [u8; HEADER_SIZE], + pub session_protocol_version: u8, + pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE], + // -- start AES-CTR(es) encrypted section + pub alice_session_id: [u8; SessionId::SIZE], + pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES], + pub header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE], + // -- end encrypted section + pub hmac_es: [u8; HMAC_SHA384_SIZE], +} + +impl AliceNoiseXKInit { + pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; + pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_PROTECTION_KEY_SIZE; + pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; +} + +/// The response to AliceNoiceXKInit containing Bob's ephemeral keys. +#[allow(unused)] +#[repr(C, packed)] +pub(crate) struct BobNoiseXKAck { + pub header: [u8; HEADER_SIZE], + pub session_protocol_version: u8, + pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE], + // -- start AES-CTR(es_ee) encrypted section + pub bob_session_id: [u8; SessionId::SIZE], + pub bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES], + // -- end encrypted sectiion + pub hmac_es_ee: [u8; HMAC_SHA384_SIZE], +} + +impl BobNoiseXKAck { + pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; + pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_CIPHERTEXTBYTES; + pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; +} + +/// Alice's final response containing her identity (she already knows Bob's) and meta-data. +/* +#[allow(unused)] +#[repr(C, packed)] +pub(crate) struct AliceNoiseXKAck { + pub header: [u8; HEADER_SIZE], + pub session_protocol_version: u8, + // -- start AES-CTR(es_ee_hk) encrypted section + pub alice_static_blob_length: [u8; 2], + pub alice_static_blob: [u8; ???], + pub alice_metadata_length: [u8; 2], + pub alice_metadata: [u8; ???], + // -- end encrypted section + pub hmac_es_ee: [u8; HMAC_SHA384_SIZE], + pub hmac_es_ee_se_hk_psk: [u8; HMAC_SHA384_SIZE], +} +*/ + +pub(crate) const ALICE_NOISE_XK_ACK_ENC_START: usize = HEADER_SIZE + 1; +pub(crate) const ALICE_NOISE_XK_ACK_AUTH_SIZE: usize = HMAC_SHA384_SIZE + HMAC_SHA384_SIZE; +pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = ALICE_NOISE_XK_ACK_ENC_START + 2 + 2 + ALICE_NOISE_XK_ACK_AUTH_SIZE; + +#[allow(unused)] +#[repr(C, packed)] +pub(crate) struct AliceRekeyInit { + pub header: [u8; HEADER_SIZE], + // -- start AES-GCM encrypted portion (using current key) + pub alice_e: [u8; P384_PUBLIC_KEY_SIZE], + // -- end AES-GCM encrypted portion + pub gcm_mac: [u8; AES_GCM_TAG_SIZE], +} + +impl AliceRekeyInit { + pub const ENC_START: usize = HEADER_SIZE; + pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE; + pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; +} + +#[allow(unused)] +#[repr(C, packed)] +pub(crate) struct BobRekeyAck { + pub header: [u8; HEADER_SIZE], + // -- start AES-GCM encrypted portion (using current key) + pub bob_e: [u8; P384_PUBLIC_KEY_SIZE], + pub next_key_fingerprint: [u8; SHA384_HASH_SIZE], + // -- end AES-GCM encrypted portion + pub gcm_mac: [u8; AES_GCM_TAG_SIZE], +} + +impl BobRekeyAck { + pub const ENC_START: usize = HEADER_SIZE; + pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE + SHA384_HASH_SIZE; + pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; +} + +// Annotate only these structs as being compatible with packet_buffer_as_bytes(). These structs +// are packed flat buffers containing only byte or byte array fields, making them safe to treat +// this way even on architectures that require type size aligned access. +pub(crate) trait ProtocolFlatBuffer {} +impl ProtocolFlatBuffer for AliceNoiseXKInit {} +impl ProtocolFlatBuffer for BobNoiseXKAck {} +//impl ProtocolFlatBuffer for NoiseXKAliceStaticAck {} +impl ProtocolFlatBuffer for AliceRekeyInit {} +impl ProtocolFlatBuffer for BobRekeyAck {} + +#[derive(Clone, Copy)] +#[repr(C, packed)] +struct MessageNonceCreateBuffer(u64, u32); + +/// Create a 96-bit AES-GCM nonce. +/// +/// The primary information that we want to be contained here is the counter and the +/// packet type. The former makes this unique and the latter's inclusion authenticates +/// it as effectively AAD. Other elements of the header are either not authenticated, +/// like fragmentation info, or their authentication is implied via key exchange like +/// the session ID. +/// +/// This is also used as part of HMAC authentication for key exchange packets. +#[inline(always)] +pub(crate) fn create_message_nonce(packet_type: u8, counter: u64) -> [u8; AES_GCM_NONCE_SIZE] { + unsafe { std::mem::transmute(MessageNonceCreateBuffer(counter.to_le(), (packet_type as u32).to_le())) } +} + +#[inline(always)] +pub(crate) fn byte_array_as_proto_buffer(b: &[u8]) -> Result<&B, Error> { + if b.len() >= size_of::() { + Ok(unsafe { &*b.as_ptr().cast() }) + } else { + Err(Error::InvalidPacket) + } +} + +#[inline(always)] +pub(crate) fn byte_array_as_proto_buffer_mut(b: &mut [u8]) -> Result<&mut B, Error> { + if b.len() >= size_of::() { + Ok(unsafe { &mut *b.as_mut_ptr().cast() }) + } else { + Err(Error::InvalidPacket) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_packed_struct_sizing() { + assert_eq!(size_of::(), AliceNoiseXKInit::SIZE); + assert_eq!(size_of::(), BobNoiseXKAck::SIZE); + } +} diff --git a/zssp/src/sessionid.rs b/zssp/src/sessionid.rs index 09f32bd63..be272034c 100644 --- a/zssp/src/sessionid.rs +++ b/zssp/src/sessionid.rs @@ -1,17 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + use std::fmt::Display; use std::num::NonZeroU64; use zerotier_crypto::random; use zerotier_utils::memory::{array_range, as_byte_array}; -use crate::constants::SESSION_ID_SIZE; - /// 48-bit session ID (most significant 16 bits of u64 are unused) #[derive(Copy, Clone, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct SessionId(NonZeroU64); // stored little endian internally +const SESSION_ID_SIZE_BYTES: usize = 6; + impl SessionId { + pub const SIZE: usize = SESSION_ID_SIZE_BYTES; + pub const NONE: u64 = 0; pub const MAX: u64 = 0xffffffffffff; /// Create a new session ID, panicing if 'i' is zero or exceeds MAX. @@ -20,11 +30,18 @@ impl SessionId { Self(NonZeroU64::new(i.to_le()).unwrap()) } - /// Create a new random session ID (non-cryptographic PRNG) + /// Create a new random (non-zero) session ID (non-cryptographic PRNG) pub fn random() -> Self { Self(NonZeroU64::new(((random::xorshift64_random() % (Self::MAX - 1)) + 1).to_le()).unwrap()) } + pub(crate) fn new_from_bytes(b: &[u8; Self::SIZE]) -> Option { + let mut tmp = [0u8; 8]; + tmp[..SESSION_ID_SIZE_BYTES].copy_from_slice(b); + Self::new_from_u64_le(u64::from_ne_bytes(tmp)) + } + + /// Create from a u64 that is already in little-endian byte order. #[inline(always)] pub(crate) fn new_from_u64_le(i: u64) -> Option { NonZeroU64::new(i & Self::MAX.to_le()).map(|i| Self(i)) @@ -32,8 +49,8 @@ impl SessionId { /// Get this session ID as a little-endian byte array. #[inline(always)] - pub(crate) fn as_bytes(&self) -> &[u8; SESSION_ID_SIZE] { - array_range::(as_byte_array(&self.0)) + pub(crate) fn as_bytes(&self) -> &[u8; Self::SIZE] { + array_range::(as_byte_array(&self.0)) } } diff --git a/zssp/src/tests.rs b/zssp/src/tests.rs index febf1616e..fd06c4e38 100644 --- a/zssp/src/tests.rs +++ b/zssp/src/tests.rs @@ -1,3 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +/* #[allow(unused_imports)] #[cfg(test)] mod tests { @@ -214,3 +223,4 @@ mod tests { } } } +*/ diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index ffef800b1..3142ab72b 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -1,59 +1,74 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ // ZSSP: ZeroTier Secure Session Protocol -// FIPS compliant Noise_IK with Jedi powers and built-in attack-resistant large payload (fragmentation) support. +// FIPS compliant Noise_XK with Jedi powers and built-in attack-resistant large payload (fragmentation) support. -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Mutex, RwLock}; +use std::collections::HashMap; +use std::num::NonZeroU64; +use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, RwLock, Weak}; -use zerotier_crypto::aes::{Aes, AesGcm}; -use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, SHA384}; -use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE}; -use zerotier_crypto::random; +use zerotier_crypto::aes::{Aes, AesCtr, AesGcm}; +use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384, SHA384_HASH_SIZE}; +use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE, P384_PUBLIC_KEY_SIZE}; use zerotier_crypto::secret::Secret; -use zerotier_crypto::secure_eq; +use zerotier_crypto::{random, secure_eq}; +use zerotier_utils::arrayvec::ArrayVec; use zerotier_utils::gatherarray::GatherArray; use zerotier_utils::memory; use zerotier_utils::ringbuffermap::RingBufferMap; -use zerotier_utils::unlikely_branch; -use zerotier_utils::varint; + +use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_SECRETKEYBYTES, KYBER_SSBYTES}; use crate::applicationlayer::ApplicationLayer; -use crate::constants::*; use crate::error::Error; +use crate::proto::*; use crate::sessionid::SessionId; -/// Result generated by the packet receive function, with possible payloads. -pub enum ReceiveResult<'a, H: ApplicationLayer> { - /// Packet is valid, no action needs to be taken. +/// Session context for local application. +/// +/// Each application using ZSSP must create an instance of this to own sessions and +/// defragment incoming packets that are not yet associated with a session. +pub struct Context { + max_incomplete_session_queue_size: usize, + defrag: Mutex, 256, 256>>, + sessions: RwLock>, +} + +/// Lookup maps for sessions within a session context. +struct SessionsById { + // Active sessions, automatically closed if the application no longer holds their Arc<>. + active: HashMap>>, + + // Incomplete sessions in the middle of three-phase Noise_XK negotiation, expired after timeout. + incoming: HashMap>, +} + +/// Result generated by the context packet receive function, with possible payloads. +pub enum ReceiveResult<'b, Application: ApplicationLayer> { + /// Packet was valid, but no action needs to be taken. Ok, - /// Packet is valid and a data payload was decoded and authenticated. - /// - /// The returned reference is to the filled parts of the data buffer supplied to receive. - OkData(&'a mut [u8]), + /// Packet was valid and a data payload was decoded and authenticated. + OkData(Arc>, &'b mut [u8]), - /// Packet is valid and a new session was created. - /// - /// The session will have already been gated by the accept_new_session() method in ApplicationLayer. - OkNewSession(Session), + /// Packet was valid and a new session was created. + OkNewSession(Arc>), - /// Packet appears valid but was ignored e.g. as a duplicate. - Ignored, + /// Packet appears valid but was rejected by the application layer, e.g. a rejected new session attempt. + Rejected, } -/// State information to associate with receiving contexts such as sockets or remote paths/endpoints. +/// ZeroTier Secure Session Protocol (ZSSP) Session /// -/// This holds the data structures used to defragment incoming packets that are not associated with an -/// existing session, which would be new attempts to create sessions. Typically one of these is associated -/// with a single listen socket, local bound port, or other inbound endpoint. -pub struct ReceiveContext { - initial_offer_defrag: Mutex, 1024, 128>>, - incoming_init_header_check_cipher: Aes, -} - -/// A FIPS compliant variant of Noise_IK with hybrid Kyber1024 PQ data forward secrecy. +/// A FIPS/NIST compliant variant of Noise_XK with hybrid Kyber1024 PQ data forward secrecy. pub struct Session { /// This side's locally unique session ID pub id: SessionId, @@ -61,136 +76,1157 @@ pub struct Session { /// An arbitrary application defined object associated with each session pub application_data: Application::Data, - send_counter: AtomicU64, // Outgoing packet counter and nonce state - receive_window: [AtomicU64; COUNTER_WINDOW_MAX_OUT_OF_ORDER], // Receive window for anti-replay and deduplication - psk: Secret<64>, // Arbitrary PSK provided by external code - noise_ss: Secret<48>, // Static raw shared ECDH NIST P-384 key - header_check_cipher: Aes, // Cipher used for header check codes (not Noise related) - state: RwLock, // Mutable parts of state (other than defrag buffers) - remote_s_public_blob_hash: [u8; 48], // SHA384(remote static public key blob) - remote_s_public_p384_bytes: [u8; P384_PUBLIC_KEY_SIZE], // Remote NIST P-384 static public key - - defrag: Mutex, 8, 8>>, + psk: Secret, + send_counter: AtomicU64, + receive_window: [AtomicU64; COUNTER_WINDOW_MAX_OOO], + header_protection_cipher: Aes, + state: RwLock, + defrag: Mutex, 16, 16>>, } -struct SessionMutableState { - remote_session_id: Option, // The other side's 48-bit session ID - session_keys: [Option; 2], // Buffers to store last and latest key by 1-bit key index - cur_session_key_idx: usize, // Pointer to latest session key other side is confirmed to have - offer: Option, // Most recent ephemeral offer sent to remote - last_remote_offer: i64, // Time of most recent ephemeral offer (ms) +/// Most of the mutable parts of a session state. +struct State { + remote_session_id: Option, + keys: [Option; 2], + current_key: usize, + current_offer: Offer, } -/// A shared symmetric session key. +/// State related to an incoming session not yet fully established. +struct IncomingIncompleteSession { + timestamp: i64, + request_hash: [u8; SHA384_HASH_SIZE], + alice_session_id: SessionId, + bob_session_id: SessionId, + noise_es_ee: Secret, + bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES], + hk: Secret, + header_protection_key: Secret, + bob_noise_e_secret: P384KeyPair, +} + +/// State related to an outgoing session attempt. +struct OutgoingSessionInit { + last_retry_time: AtomicI64, + alice_noise_e_secret: P384KeyPair, + noise_es: Secret, + alice_hk_secret: Secret, + metadata: Option>, + init_packet: [u8; AliceNoiseXKInit::SIZE], +} + +/// Latest outgoing offer, either an outgoing attempt or a rekey attempt. +enum Offer { + None, + NoiseXKInit(Box), + RekeyInit(P384KeyPair, [u8; AliceRekeyInit::SIZE], AtomicI64), +} + +/// An ephemeral session key with expiration info. struct SessionKey { - ratchet_count: u64, // Number of preceding session keys in ratchet - rekey_at_time: i64, // Rekey at or after this time (ticks) - rekey_at_counter: u64, // Rekey at or after this counter - expire_at_counter: u64, // Hard error when this counter value is reached or exceeded - secret_fingerprint: [u8; 16], // First 128 bits of a SHA384 computed from the secret - ratchet_key: Secret<64>, // Ratchet key for deriving the next session key - receive_key: Secret, // Receive side AES-GCM key - send_key: Secret, // Send side AES-GCM key + ratchet_key: Secret, // Key used in derivation of the next session key + receive_key: Secret, // Receive side AES-GCM key + send_key: Secret, // Send side AES-GCM key receive_cipher_pool: Mutex>>, // Pool of reusable sending ciphers send_cipher_pool: Mutex>>, // Pool of reusable receiving ciphers - role: Role, // Was this side Alice or Bob? - confirmed: bool, // We have confirmed that the other side has this key - jedi: bool, // True if Kyber1024 was used (both sides enabled) + rekey_at_time: i64, // Rekey at or after this time (ticks) + created_at_counter: u64, // Counter at which session was created + rekey_at_counter: u64, // Rekey at or after this counter + expire_at_counter: u64, // Hard error when this counter value is reached or exceeded + bob: bool, // Was this side "Bob" in this exchange? } -/// Alice's KEY_OFFER, remembered so Noise agreement process can resume on KEY_COUNTER_OFFER. -struct EphemeralOffer { - id: [u8; 16], // Arbitrary random offer ID - creation_time: i64, // Local time when offer was created - ratchet_count: u64, // Ratchet count (starting at zero) for initial offer - ratchet_key: Option>, // Ratchet key from previous offer or None if first offer - ss_key: Secret<64>, // Noise session key "under construction" at state after offer sent - alice_e_keypair: P384KeyPair, // NIST P-384 key pair (Noise ephemeral key for Alice) - alice_hk_keypair: Option, // Kyber1024 key pair (PQ hybrid ephemeral key for Alice) -} +impl Context { + /// Create a new session context. + pub fn new(max_incomplete_session_queue_size: usize) -> Self { + Self { + max_incomplete_session_queue_size, + defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())), + sessions: RwLock::new(SessionsById { + active: HashMap::with_capacity(64), + incoming: HashMap::with_capacity(64), + }), + } + } -/// Was this side the one who sent the first offer (Alice) or countered (Bob). -/// -/// Note that the role can switch through the course of a session. It's the side that most recently -/// initiated a session or a rekey event. Initiator is Alice, responder is Bob. -#[derive(Clone, Copy)] -pub enum Role { - Alice, - Bob, -} - -impl Session { - /// Create a new session and send an initial key offer message to the other end. + /// Perform periodic background service and cleanup tasks. /// - /// * `app` - Interface to application using ZSSP - /// * `local_session_id` - ID for this side (Alice) of the session, must be locally unique - /// * `remote_s_public_blob` - Remote side's (Bob's) public key/identity - /// * `offer_metadata` - Arbitrary meta-data to send with key offer (empty if none) - /// * `psk` - Arbitrary pre-shared key to include as initial key material (use all zeroes if none) - /// * `application_data` - Arbitrary object to put into session - /// * `mtu` - Physical wire maximum transmission unit (current value, can change through the course of a session) - /// * `current_time` - Current monotonic time in milliseconds since an arbitrary time in the past - pub fn start_new( - app: &Application, + /// This returns the number of milliseconds until it should be called again. + /// + /// * `send` - Function to send packets to remote sessions + /// * `mtu` - Physical MTU + /// * `current_time` - Current monotonic time in milliseconds + pub fn service>, &mut [u8])>( + &self, mut send: SendFunction, - local_session_id: SessionId, - remote_s_public_blob: &[u8], - offer_metadata: &[u8], - psk: &Secret<64>, - application_data: Application::Data, mtu: usize, current_time: i64, - ) -> Result { - let bob_s_public_blob = remote_s_public_blob; - if let Some(bob_s_public) = Application::extract_s_public_from_raw(bob_s_public_blob) { - if let Some(noise_ss) = app.get_local_s_keypair().agree(&bob_s_public) { - let bob_s_public_blob_hash = SHA384::hash(bob_s_public_blob); - let header_check_cipher = - Aes::new(kbkdf512(noise_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::()); - let mut offer = None; - if send_ephemeral_offer( - &mut send, - 1, - local_session_id, - None, - app.get_local_s_public_blob(), - offer_metadata, - &bob_s_public, - &bob_s_public_blob_hash, - &noise_ss, - None, - None, - mtu, - current_time, - &mut offer, - ) - .is_ok() - { - return Ok(Self { - id: local_session_id, - application_data, - send_counter: AtomicU64::new(2), // 1 was used above - receive_window: std::array::from_fn(|_| AtomicU64::new(0)), - psk: psk.clone(), - noise_ss, - header_check_cipher, - state: RwLock::new(SessionMutableState { - remote_session_id: None, - session_keys: [None, None], - cur_session_key_idx: 0, - offer, - last_remote_offer: i64::MIN, - }), - remote_s_public_blob_hash: bob_s_public_blob_hash, - remote_s_public_p384_bytes: bob_s_public.as_bytes().clone(), - defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), - }); + ) -> i64 { + let mut dead_active = Vec::new(); + let mut dead_pending = Vec::new(); + let retry_cutoff = current_time - Application::RETRY_INTERVAL; + let negotiation_timeout_cutoff = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; + + { + let sessions = self.sessions.read().unwrap(); + + for (id, s) in sessions.active.iter() { + if let Some(session) = s.upgrade() { + let state = session.state.read().unwrap(); + match &state.current_offer { + Offer::None => { + if let Some(key) = state.keys[state.current_key].as_ref() { + if key.bob + && (current_time >= key.rekey_at_time + || session.send_counter.load(Ordering::Relaxed) >= key.rekey_at_counter) + { + session.initiate_rekey(|b| send(&session, b), current_time); + } + } + } + Offer::NoiseXKInit(offer) => { + if offer.last_retry_time.load(Ordering::Relaxed) < retry_cutoff { + offer.last_retry_time.store(current_time, Ordering::Relaxed); + let _ = send_with_fragmentation( + |b| send(&session, b), + &mut (offer.init_packet.clone()), + mtu, + PACKET_TYPE_ALICE_NOISE_XK_INIT, + None, + 0, + 1, + None, + ); + } + } + Offer::RekeyInit(_, rekey_packet, last_retry_time) => { + if last_retry_time.load(Ordering::Relaxed) < retry_cutoff { + last_retry_time.store(current_time, Ordering::Relaxed); + send(&session, &mut (rekey_packet.clone())); + } + } + } + } else { + dead_active.push(*id); + } + } + + for (id, incoming) in sessions.incoming.iter() { + if incoming.timestamp < negotiation_timeout_cutoff { + dead_pending.push(*id); } } } - return Err(Error::InvalidParameter); + + if !dead_active.is_empty() || !dead_pending.is_empty() { + let mut sessions = self.sessions.write().unwrap(); + for id in dead_active.iter() { + sessions.active.remove(id); + } + for id in dead_pending.iter() { + sessions.incoming.remove(id); + } + } + + Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS.min(Application::RETRY_INTERVAL) } + /// Create a new session and send initial packet(s) to other side. + /// + /// This will return Error::DataTooLarge if the combined size of the metadata and the local static public + /// blob (as retrieved from the application layer) exceed MAX_INIT_PAYLOAD_SIZE. + /// + /// * `app` - Application layer instance + /// * `send` - User-supplied packet sending function + /// * `mtu` - Physical MTU for calls to send() + /// * `remote_s_public_p384` - Remote side's static public NIST P-384 key + /// * `psk` - Pre-shared key (use all zero if none) + /// * `metadata` - Optional metadata to be included in initial handshake + /// * `application_data` - Arbitrary opaque data to include with session object + /// * `current_time` - Current monotonic time in milliseconds + pub fn open( + &self, + app: &Application, + mut send: SendFunction, + mtu: usize, + remote_s_public_p384: &P384PublicKey, + psk: Secret, + metadata: Option<&[u8]>, + application_data: Application::Data, + current_time: i64, + ) -> Result>, Error> { + if (metadata.map(|md| md.len()).unwrap_or(0) + app.get_local_s_public_blob().len()) > MAX_INIT_PAYLOAD_SIZE { + return Err(Error::DataTooLarge); + } + + let alice_noise_e_secret = P384KeyPair::generate(); + let alice_noise_e = alice_noise_e_secret.public_key_bytes().clone(); + let noise_es = alice_noise_e_secret.agree(&remote_s_public_p384).ok_or(Error::InvalidParameter)?; + let alice_hk_secret = pqc_kyber::keypair(&mut random::SecureRandom::default()); + let header_protection_key: Secret = Secret(random::get_bytes_secure()); + + let (local_session_id, session) = { + let mut sessions = self.sessions.write().unwrap(); + + let mut local_session_id; + loop { + local_session_id = SessionId::random(); + if !sessions.active.contains_key(&local_session_id) && !sessions.incoming.contains_key(&local_session_id) { + break; + } + } + + let session = Arc::new(Session { + id: local_session_id, + application_data, + psk, + send_counter: AtomicU64::new(3), // 1 and 2 are reserved for init and final ack + receive_window: std::array::from_fn(|_| AtomicU64::new(0)), + header_protection_cipher: Aes::new(header_protection_key.as_bytes()), + state: RwLock::new(State { + remote_session_id: None, + keys: [None, None], + current_key: 0, + current_offer: Offer::NoiseXKInit(Box::new(OutgoingSessionInit { + last_retry_time: AtomicI64::new(current_time), + alice_noise_e_secret, + noise_es: noise_es.clone(), + alice_hk_secret: Secret(alice_hk_secret.secret), + metadata: metadata.map(|md| ArrayVec::try_from(md).unwrap()), + init_packet: [0u8; AliceNoiseXKInit::SIZE], + })), + }), + defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), + }); + + sessions.active.insert(local_session_id, Arc::downgrade(&session)); + + (local_session_id, session) + }; + + { + let mut state = session.state.write().unwrap(); + let init_packet = if let Offer::NoiseXKInit(offer) = &mut state.current_offer { + &mut offer.init_packet + } else { + panic!(); + }; + + let init: &mut AliceNoiseXKInit = byte_array_as_proto_buffer_mut(init_packet).unwrap(); + init.session_protocol_version = SESSION_PROTOCOL_VERSION; + init.alice_noise_e = alice_noise_e; + init.alice_session_id = *local_session_id.as_bytes(); + init.alice_hk_public = alice_hk_secret.public; + init.header_protection_key = header_protection_key.0; + + aes_ctr_crypt_one_time_use_key( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START], + ); + + let hmac = hmac_sha384_2( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_INIT, 1), + &init_packet[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], + ); + init_packet[AliceNoiseXKInit::AUTH_START..AliceNoiseXKInit::AUTH_START + HMAC_SHA384_SIZE].copy_from_slice(&hmac); + + send_with_fragmentation( + &mut send, + &mut (init_packet.clone()), + mtu, + PACKET_TYPE_ALICE_NOISE_XK_INIT, + None, + 0, + 1, + None, + )?; + } + + return Ok(session); + } + + /// Receive, authenticate, decrypt, and process a physical wire packet. + /// + /// The send function may be called one or more times to send packets. If the packet is associated + /// wtth an active session this session is supplied, otherwise this parameter is None. The size + /// of packets to be sent will not exceed the supplied mtu. + /// + /// The check_allow_incoming_session function is called when an initial Noise_XK init message is + /// received. This is before anything is known about the caller. A return value of true proceeds + /// with negotiation. False drops the packet. + /// + /// The check_accept_session function is called at the end of negotiation for an incoming session + /// with the caller's static public blob and meta-data if any. It must return the P-384 static public + /// key extracted from the supplied blob, a PSK (or all zeroes if none), and application data to + /// associate with the new session. A return of None abandons the session. + /// + /// Note that if check_accept_session accepts and returns Some() the session could still fail with + /// receive() returning an error. A Some() return from check_accept_sesion doesn't guarantee + /// successful new session init. + /// + /// * `app` - Interface to application using ZSSP + /// * `check_allow_incoming_session` - Function to call to check whether an unidentified new session should be accepted + /// * `check_accept_session` - Function to accept sessions after final negotiation, or returns None if rejected + /// * `send` - Function to call to send packets + /// * `data_buf` - Buffer to receive decrypted and authenticated object data (an error is returned if too small) + /// * `incoming_packet_buf` - Buffer containing incoming wire packet (receive() takes ownership) + /// * `mtu` - Physical wire MTU for sending packets + /// * `current_time` - Current monotonic time in milliseconds + pub fn receive< + 'b, + SendFunction: FnMut(Option<&Arc>>, &mut [u8]), + CheckAllowIncomingSession: FnMut() -> bool, + CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>, + >( + &self, + app: &Application, + mut check_allow_incoming_session: CheckAllowIncomingSession, + mut check_accept_session: CheckAcceptSession, + mut send: SendFunction, + data_buf: &'b mut [u8], + mut incoming_packet_buf: Application::IncomingPacketBuffer, + mtu: usize, + current_time: i64, + ) -> Result, Error> { + let incoming_packet: &mut [u8] = incoming_packet_buf.as_mut(); + if incoming_packet.len() < MIN_PACKET_SIZE { + return Err(Error::InvalidPacket); + } + + let mut incoming = None; + if let Some(local_session_id) = SessionId::new_from_u64_le(memory::load_raw(incoming_packet)) { + if let Some(session) = self + .sessions + .read() + .unwrap() + .active + .get(&local_session_id) + .and_then(|s| s.upgrade()) + { + debug_assert!(!self.sessions.read().unwrap().incoming.contains_key(&local_session_id)); + + session + .header_protection_cipher + .decrypt_block_in_place(&mut incoming_packet[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); + let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_packet); + + if session.check_receive_window(incoming_counter) { + if fragment_count > 1 { + if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count { + let mut defrag = session.defrag.lock().unwrap(); + let fragment_gather_array = defrag.get_or_create_mut(&incoming_counter, || GatherArray::new(fragment_count)); + if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) { + drop(defrag); // release lock + return self.process_complete_incoming_packet( + app, + &mut send, + &mut check_allow_incoming_session, + &mut check_accept_session, + data_buf, + incoming_counter, + assembled_packet.as_ref(), + packet_type, + Some(session), + None, + key_index, + mtu, + current_time, + ); + } else { + return Ok(ReceiveResult::Ok); + } + } else { + return Err(Error::InvalidPacket); + } + } else { + return self.process_complete_incoming_packet( + app, + &mut send, + &mut check_allow_incoming_session, + &mut check_accept_session, + data_buf, + incoming_counter, + &[incoming_packet_buf], + packet_type, + Some(session), + None, + key_index, + mtu, + current_time, + ); + } + } else { + return Err(Error::OutOfSequence); + } + } else { + if let Some(i) = self.sessions.read().unwrap().incoming.get(&local_session_id).cloned() { + Aes::new(i.header_protection_key.as_bytes()) + .decrypt_block_in_place(&mut incoming_packet[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); + incoming = Some(i); + } else { + return Err(Error::UnknownLocalSessionId); + } + } + } + + // If we make it here the packet is not associated with a session or is associated with an + // incoming session (Noise_XK mid-negotiation). + + let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_packet); + if fragment_count > 1 { + let mut defrag = self.defrag.lock().unwrap(); + let fragment_gather_array = defrag.get_or_create_mut(&incoming_counter, || GatherArray::new(fragment_count)); + if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) { + drop(defrag); // release lock + return self.process_complete_incoming_packet( + app, + &mut send, + &mut check_allow_incoming_session, + &mut check_accept_session, + data_buf, + incoming_counter, + assembled_packet.as_ref(), + packet_type, + None, + incoming, + key_index, + mtu, + current_time, + ); + } + } else { + return self.process_complete_incoming_packet( + app, + &mut send, + &mut check_allow_incoming_session, + &mut check_accept_session, + data_buf, + incoming_counter, + &[incoming_packet_buf], + packet_type, + None, + incoming, + key_index, + mtu, + current_time, + ); + } + + return Ok(ReceiveResult::Ok); + } + + fn process_complete_incoming_packet< + 'b, + SendFunction: FnMut(Option<&Arc>>, &mut [u8]), + CheckAllowIncomingSession: FnMut() -> bool, + CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>, + >( + &self, + app: &Application, + send: &mut SendFunction, + check_allow_incoming_session: &mut CheckAllowIncomingSession, + check_accept_session: &mut CheckAcceptSession, + data_buf: &'b mut [u8], + incoming_counter: u64, + fragments: &[Application::IncomingPacketBuffer], + packet_type: u8, + session: Option>>, + incoming: Option>, + key_index: usize, + mtu: usize, + current_time: i64, + ) -> Result, Error> { + debug_assert!(fragments.len() >= 1); + + // Generate incoming message nonce for decryption and authentication. + let incoming_message_nonce = create_message_nonce(packet_type, incoming_counter); + + if packet_type == PACKET_TYPE_DATA { + if let Some(session) = session { + let state = session.state.read().unwrap(); + if let Some(key) = state.keys[key_index].as_ref() { + let mut c = key.get_receive_cipher(); + c.reset_init_gcm(&incoming_message_nonce); + + let mut data_len = 0; + + // Decrypt fragments 0..N-1 where N is the number of fragments. + for f in fragments[..(fragments.len() - 1)].iter() { + let f: &[u8] = f.as_ref(); + debug_assert!(f.len() >= HEADER_SIZE); + let current_frag_data_start = data_len; + data_len += f.len() - HEADER_SIZE; + if data_len > data_buf.len() { + key.return_receive_cipher(c); + return Err(Error::DataBufferTooSmall); + } + c.crypt(&f[HEADER_SIZE..], &mut data_buf[current_frag_data_start..data_len]); + } + + // Decrypt final fragment (or only fragment if not fragmented) + let current_frag_data_start = data_len; + let last_fragment = fragments.last().unwrap().as_ref(); + if last_fragment.len() < (HEADER_SIZE + AES_GCM_TAG_SIZE) { + return Err(Error::InvalidPacket); + } + data_len += last_fragment.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE); + if data_len > data_buf.len() { + key.return_receive_cipher(c); + return Err(Error::DataBufferTooSmall); + } + let payload_end = last_fragment.len() - AES_GCM_TAG_SIZE; + c.crypt( + &last_fragment[HEADER_SIZE..payload_end], + &mut data_buf[current_frag_data_start..data_len], + ); + + let aead_authentication_ok = c.finish_decrypt(&last_fragment[payload_end..]); + key.return_receive_cipher(c); + + if aead_authentication_ok { + if session.update_receive_window(incoming_counter) { + // Update the current key to point to this key if it's newer, since having received + // a packet encrypted with it proves that the other side has successfully derived it + // as well. + if state.current_key == key_index { + drop(state); + } else { + let key_created_at_counter = key.created_at_counter; + drop(state); + let mut state = session.state.write().unwrap(); + if let Some(other_session_key) = state.keys[state.current_key].as_ref() { + if other_session_key.created_at_counter < key_created_at_counter { + state.current_key = key_index; + } + } else { + state.current_key = key_index; + } + } + + return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); + } else { + return Err(Error::OutOfSequence); + } + } + } + + return Err(Error::FailedAuthentication); + } else { + return Err(Error::UnknownLocalSessionId); + } + } else { + // For Noise setup/KEX packets go ahead and pre-assemble all fragments to simplify the code below. + let mut pkt_assembly_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; + let pkt_assembled_size = assemble_fragments_into::(fragments, &mut pkt_assembly_buffer)?; + if pkt_assembled_size < MIN_PACKET_SIZE { + return Err(Error::InvalidPacket); + } + let pkt_assembled = &mut pkt_assembly_buffer[..pkt_assembled_size]; + if pkt_assembled[HEADER_SIZE] != SESSION_PROTOCOL_VERSION { + return Err(Error::UnknownProtocolVersion); + } + + match packet_type { + PACKET_TYPE_ALICE_NOISE_XK_INIT => { + // Alice (remote) --> Bob (local) + + /* + * This is the first message Bob receives from Alice, the initiator. It contains + * Alice's ephemeral keys but not her identity. Alice will not reveal her identity + * until forward secrecy is established and she's authenticated Bob. + * + * Bob authenticates the message and confirms that Alice indeed knows Bob's + * identity, then responds with his ephemeral keys. + * + * Bob also sends an opaque sealed object called Bob's "note to self." It contains + * Bob's state for the connection as of this first exchange, allowing Bob to be + * stateless until he knows and has confirmed Alice's identity. It's encrypted, + * authenticated, subject to a short TTL, and contains only information relevant + * to the current exchange. + */ + + if incoming_counter != 1 || session.is_some() { + return Err(Error::OutOfSequence); + } + + // Hash the init packet so we can check to see if it's just being retransmitted. Alice may + // attempt to retransmit this packet until she receives a response. + let request_hash = SHA384::hash(&pkt_assembled); + + let (alice_session_id, mut bob_session_id, noise_es_ee, bob_hk_ciphertext, header_protection_key, bob_noise_e); + if let Some(incoming) = incoming { + // If we've already seen this exact packet before, just recall the same state so we send the + // same response. + if secure_eq(&request_hash, &incoming.request_hash) { + alice_session_id = incoming.alice_session_id; + bob_session_id = incoming.bob_session_id; + noise_es_ee = incoming.noise_es_ee.clone(); + bob_hk_ciphertext = incoming.bob_hk_ciphertext; + header_protection_key = incoming.header_protection_key.clone(); + bob_noise_e = *incoming.bob_noise_e_secret.public_key_bytes(); + } else { + return Err(Error::FailedAuthentication); + } + } else { + // Otherwise parse the packet, authenticate, generate keys, etc. and record state in an + // incoming state object until this phase of the negotiation is done. + let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; + let alice_noise_e = P384PublicKey::from_bytes(&pkt.alice_noise_e).ok_or(Error::FailedAuthentication)?; + let noise_es = app.get_local_s_keypair().agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?; + + // Authenticate packet and also prove that Alice knows our static public key. + if !secure_eq( + &pkt.hmac_es, + &hmac_sha384_2( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &incoming_message_nonce, + &pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], + ), + ) { + return Err(Error::FailedAuthentication); + } + + // Let application filter incoming connection attempt by whatever criteria it wants. + if !check_allow_incoming_session() { + return Ok(ReceiveResult::Rejected); + } + + // Decrypt encrypted part of payload. + aes_ctr_crypt_one_time_use_key( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START], + ); + + let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; + alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?; + header_protection_key = Secret(pkt.header_protection_key); + + // Create Bob's ephemeral keys and derive noise_es_ee by agreeing with Alice's. Also create + // a Kyber ciphertext to send back to Alice. + let bob_noise_e_secret = P384KeyPair::generate(); + bob_noise_e = bob_noise_e_secret.public_key_bytes().clone(); + noise_es_ee = Secret(hmac_sha512( + noise_es.as_bytes(), + bob_noise_e_secret + .agree(&alice_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + )); + let (hk_ct, hk) = pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default()) + .map_err(|_| Error::FailedAuthentication) + .map(|(ct, hk)| (ct, Secret(hk)))?; + bob_hk_ciphertext = hk_ct; + + let mut sessions = self.sessions.write().unwrap(); + + loop { + bob_session_id = SessionId::random(); + if !sessions.active.contains_key(&bob_session_id) && !sessions.incoming.contains_key(&bob_session_id) { + break; + } + } + + if sessions.incoming.len() >= self.max_incomplete_session_queue_size { + // If this queue is too big, we remove the latest entry and replace it. The latest + // is used because under flood conditions this is most likely to be another bogus + // entry. If we find one that is actually timed out, that one is replaced instead. + let mut newest = i64::MIN; + let mut replace_id = None; + let cutoff_time = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; + for (id, s) in sessions.incoming.iter() { + if s.timestamp <= cutoff_time { + replace_id = Some(*id); + break; + } else if s.timestamp >= newest { + newest = s.timestamp; + replace_id = Some(*id); + } + } + let _ = sessions.incoming.remove(replace_id.as_ref().unwrap()); + } + + // Reserve session ID on this side and record incomplete session state. + sessions.incoming.insert( + bob_session_id, + Arc::new(IncomingIncompleteSession { + timestamp: current_time, + request_hash, + alice_session_id, + bob_session_id, + noise_es_ee: noise_es_ee.clone(), + bob_hk_ciphertext, + hk, + bob_noise_e_secret, + header_protection_key: Secret(pkt.header_protection_key), + }), + ); + } + + // Create Bob's ephemeral counter-offer reply. + let mut ack_packet = [0u8; BobNoiseXKAck::SIZE]; + let ack: &mut BobNoiseXKAck = byte_array_as_proto_buffer_mut(&mut ack_packet)?; + ack.session_protocol_version = SESSION_PROTOCOL_VERSION; + ack.bob_noise_e = bob_noise_e; + ack.bob_session_id = *bob_session_id.as_bytes(); + ack.bob_hk_ciphertext = bob_hk_ciphertext; + + // Encrypt main section of reply. + aes_ctr_crypt_one_time_use_key( + kbkdf::(noise_es_ee.as_bytes()).as_bytes(), + &mut ack_packet[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START], + ); + + // Add HMAC-SHA384 to reply packet. + let reply_hmac = hmac_sha384_2( + kbkdf::(noise_es_ee.as_bytes()).as_bytes(), + &create_message_nonce(PACKET_TYPE_BOB_NOISE_XK_ACK, 1), + &ack_packet[HEADER_SIZE..BobNoiseXKAck::AUTH_START], + ); + ack_packet[BobNoiseXKAck::AUTH_START..].copy_from_slice(&reply_hmac); + + send_with_fragmentation( + |b| send(None, b), + &mut ack_packet, + mtu, + PACKET_TYPE_BOB_NOISE_XK_ACK, + Some(alice_session_id), + 0, + 1, + Some(&Aes::new(header_protection_key.as_bytes())), + )?; + + return Ok(ReceiveResult::Ok); + } + + PACKET_TYPE_BOB_NOISE_XK_ACK => { + // Bob (remote) --> Alice (local) + + /* + * This is Bob's reply to Alice's first message, allowing Alice to verify Bob's + * identity. Once this is done Alice can send her identity (encrypted) to complete + * the negotiation. + */ + + if incoming_counter != 1 || incoming.is_some() { + return Err(Error::OutOfSequence); + } + + if let Some(session) = session { + let state = session.state.read().unwrap(); + if let Offer::NoiseXKInit(outgoing_offer) = &state.current_offer { + let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; + + // Derive noise_es_ee from Bob's ephemeral public key. + let bob_noise_e = P384PublicKey::from_bytes(&pkt.bob_noise_e).ok_or(Error::FailedAuthentication)?; + let noise_es_ee = Secret(hmac_sha512( + outgoing_offer.noise_es.as_bytes(), + outgoing_offer + .alice_noise_e_secret + .agree(&bob_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + )); + + let noise_es_ee_kex_hmac_key = + kbkdf::(noise_es_ee.as_bytes()); + + // Authenticate Bob's reply and the validity of bob_noise_e. + if !secure_eq( + &pkt.hmac_es_ee, + &hmac_sha384_2( + noise_es_ee_kex_hmac_key.as_bytes(), + &incoming_message_nonce, + &pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START], + ), + ) { + return Err(Error::FailedAuthentication); + } + + // Decrypt encrypted portion of message. + aes_ctr_crypt_one_time_use_key( + kbkdf::(noise_es_ee.as_bytes()).as_bytes(), + &mut pkt_assembled[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START], + ); + let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; + + if let Some(bob_session_id) = SessionId::new_from_bytes(&pkt.bob_session_id) { + // Complete Noise_XKpsk3 by mixing in noise_se followed by the PSK. The PSK as far as + // the Noise pattern is concerned is the result of mixing the externally supplied PSK + // with the Kyber1024 shared secret (hk). Kyber is treated as part of the PSK because + // it's an external add-on beyond the Noise spec. + let hk = pqc_kyber::decapsulate(&pkt.bob_hk_ciphertext, outgoing_offer.alice_hk_secret.as_bytes()) + .map_err(|_| Error::FailedAuthentication) + .map(|k| Secret(k))?; + let noise_es_ee_se_hk_psk = Secret(hmac_sha512( + &hmac_sha512( + noise_es_ee.as_bytes(), + app.get_local_s_keypair() + .agree(&bob_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + ), + &hmac_sha512(session.psk.as_bytes(), hk.as_bytes()), + )); + + let reply_message_nonce = create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_ACK, 2); + + // Create reply informing Bob of our static identity now that we've verified Bob and set + // up forward secrecy. Also return Bob's opaque note. + let mut reply_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; + reply_buffer[HEADER_SIZE] = SESSION_PROTOCOL_VERSION; + let mut reply_len = HEADER_SIZE + 1; + let mut reply_buffer_append = |b: &[u8]| { + let reply_len_new = reply_len + b.len(); + debug_assert!(reply_len_new <= MAX_NOISE_HANDSHAKE_SIZE); + reply_buffer[reply_len..reply_len_new].copy_from_slice(b); + reply_len = reply_len_new; + }; + let alice_s_public_blob = app.get_local_s_public_blob(); + assert!(alice_s_public_blob.len() <= (u16::MAX as usize)); + reply_buffer_append(&(alice_s_public_blob.len() as u16).to_le_bytes()); + reply_buffer_append(alice_s_public_blob); + if let Some(md) = outgoing_offer.metadata.as_ref() { + reply_buffer_append(&(md.len() as u16).to_le_bytes()); + reply_buffer_append(md.as_ref()); + } else { + reply_buffer_append(&[0u8, 0u8]); // no meta-data + } + + // Encrypt Alice's static identity and other inner payload items. The key used here is + // mixed with 'hk' to make identity secrecy PQ forward secure. + aes_ctr_crypt_one_time_use_key( + &hmac_sha512(noise_es_ee.as_bytes(), hk.as_bytes())[..AES_256_KEY_SIZE], + &mut reply_buffer[HEADER_SIZE + 1..reply_len], + ); + + // First attach HMAC allowing Bob to verify that this is from the same Alice and to + // verify the authenticity of encrypted data. + let hmac_es_ee = hmac_sha384_2( + noise_es_ee_kex_hmac_key.as_bytes(), + &reply_message_nonce, + &reply_buffer[HEADER_SIZE..reply_len], + ); + reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee); + reply_len += HMAC_SHA384_SIZE; + + // Then attach the final HMAC permitting Bob to verify the authenticity of the whole + // key exchange. Bob won't be able to do this until he decrypts and parses Alice's + // identity, so the first HMAC is to let him authenticate that first. + let hmac_es_ee_se_hk_psk = hmac_sha384_2( + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) + .as_bytes(), + &reply_message_nonce, + &reply_buffer[HEADER_SIZE..reply_len], + ); + reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee_se_hk_psk); + reply_len += HMAC_SHA384_SIZE; + + drop(state); + { + let mut state = session.state.write().unwrap(); + let _ = state.remote_session_id.insert(bob_session_id); + let _ = + state.keys[0].insert(SessionKey::new::(noise_es_ee_se_hk_psk, current_time, 2, false)); + state.current_key = 0; + state.current_offer = Offer::None; + } + + send_with_fragmentation( + |b| send(Some(&session), b), + &mut reply_buffer[..reply_len], + mtu, + PACKET_TYPE_ALICE_NOISE_XK_ACK, + Some(bob_session_id), + 0, + 2, + Some(&session.header_protection_cipher), + )?; + + return Ok(ReceiveResult::Ok); + } else { + return Err(Error::InvalidPacket); + } + } else { + return Err(Error::OutOfSequence); + } + } else { + return Err(Error::UnknownLocalSessionId); + } + } + + PACKET_TYPE_ALICE_NOISE_XK_ACK => { + // Alice (remote) --> Bob (local) + + /* + * After negotiating a keyed session and Alice has had the opportunity to + * verify Bob, this is when Bob gets to learn who Alice is. At this point + * Bob can make a final decision about whether to keep talking to Alice + * and can create an actual session using the state memo-ized in the memo + * that Alice must return. + */ + + if incoming_counter != 2 || session.is_some() { + return Err(Error::OutOfSequence); + } + if pkt_assembled.len() < ALICE_NOISE_XK_ACK_MIN_SIZE { + return Err(Error::InvalidPacket); + } + + if let Some(incoming) = incoming { + // Check timeout, negotiations aren't allowed to take longer than this. + if (current_time - incoming.timestamp) > Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS { + return Err(Error::UnknownLocalSessionId); + } + + // Check the first HMAC to verify against the currently known noise_es_ee key, which verifies + // that this reply is part of this session. + let auth_start = pkt_assembled.len() - ALICE_NOISE_XK_ACK_AUTH_SIZE; + if !secure_eq( + &pkt_assembled[auth_start..pkt_assembled.len() - HMAC_SHA384_SIZE], + &hmac_sha384_2( + kbkdf::(incoming.noise_es_ee.as_bytes()) + .as_bytes(), + &incoming_message_nonce, + &pkt_assembled[HEADER_SIZE..auth_start], + ), + ) { + return Err(Error::FailedAuthentication); + } + + // Make a copy of pkt_assembled so we can check the second HMAC against original ciphertext later. + let mut pkt_assembly_buffer_copy = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; + pkt_assembly_buffer_copy[..pkt_assembled.len()].copy_from_slice(pkt_assembled); + + // Decrypt encrypted section so we can finally learn Alice's static identity. + aes_ctr_crypt_one_time_use_key( + &hmac_sha512(incoming.noise_es_ee.as_bytes(), incoming.hk.as_bytes())[..AES_256_KEY_SIZE], + &mut pkt_assembled[ALICE_NOISE_XK_ACK_ENC_START..auth_start], + ); + + // Read the static public blob and optional meta-data. + let mut pkt_assembled_ptr = HEADER_SIZE + 1; + let mut pkt_assembled_field_end = pkt_assembled_ptr + 2; + if pkt_assembled_field_end >= pkt_assembled.len() { + return Err(Error::InvalidPacket); + } + let alice_static_public_blob_size = + u16::from_le(memory::load_raw::(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end])) as usize; + pkt_assembled_ptr = pkt_assembled_field_end; + pkt_assembled_field_end = pkt_assembled_ptr + alice_static_public_blob_size; + if pkt_assembled_field_end >= pkt_assembled.len() { + return Err(Error::InvalidPacket); + } + let alice_static_public_blob = &pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end]; + pkt_assembled_ptr = pkt_assembled_field_end; + pkt_assembled_field_end = pkt_assembled_ptr + 2; + if pkt_assembled_field_end >= pkt_assembled.len() { + return Err(Error::InvalidPacket); + } + let alice_meta_data_size = + u16::from_le(memory::load_raw::(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end])) as usize; + pkt_assembled_ptr = pkt_assembled_field_end; + pkt_assembled_field_end = pkt_assembled_ptr + alice_meta_data_size; + let alice_meta_data = if alice_meta_data_size > 0 { + Some(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end]) + } else { + None + }; + + // Check session acceptance and fish Alice's NIST P-384 static public key out of + // her static public blob. + let check_result = check_accept_session(alice_static_public_blob, alice_meta_data); + if check_result.is_none() { + self.sessions.write().unwrap().incoming.remove(&incoming.bob_session_id); + return Ok(ReceiveResult::Rejected); + } + let (alice_noise_s, psk, application_data) = check_result.unwrap(); + + // Complete Noise_XKpsk3 on Bob's side. + let noise_es_ee_se_hk_psk = Secret(hmac_sha512( + &hmac_sha512( + incoming.noise_es_ee.as_bytes(), + incoming + .bob_noise_e_secret + .agree(&alice_noise_s) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + ), + &hmac_sha512(psk.as_bytes(), incoming.hk.as_bytes()), + )); + + // Verify the packet using the final key to verify the whole key exchange. + if !secure_eq( + &pkt_assembly_buffer_copy[auth_start + HMAC_SHA384_SIZE..pkt_assembled.len()], + &hmac_sha384_2( + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) + .as_bytes(), + &incoming_message_nonce, + &pkt_assembly_buffer_copy[HEADER_SIZE..auth_start + HMAC_SHA384_SIZE], + ), + ) { + return Err(Error::FailedAuthentication); + } + + let session = Arc::new(Session { + id: incoming.bob_session_id, + application_data, + psk, + send_counter: AtomicU64::new(2), // 1 was already used during negotiation + receive_window: std::array::from_fn(|_| AtomicU64::new(0)), + header_protection_cipher: Aes::new(incoming.header_protection_key.as_bytes()), + state: RwLock::new(State { + remote_session_id: Some(incoming.alice_session_id), + keys: [ + Some(SessionKey::new::(noise_es_ee_se_hk_psk, current_time, 2, true)), + None, + ], + current_key: 0, + current_offer: Offer::None, + }), + defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), + }); + + // Promote this from an incomplete session to an established session. + { + let mut sessions = self.sessions.write().unwrap(); + sessions.incoming.remove(&incoming.bob_session_id); + sessions.active.insert(incoming.bob_session_id, Arc::downgrade(&session)); + } + + return Ok(ReceiveResult::OkNewSession(session)); + } else { + return Err(Error::UnknownLocalSessionId); + } + } + + PACKET_TYPE_ALICE_REKEY_INIT => { + if pkt_assembled.len() != AliceRekeyInit::SIZE { + return Err(Error::InvalidPacket); + } + if incoming.is_some() { + return Err(Error::OutOfSequence); + } + + if let Some(session) = session { + let state = session.state.read().unwrap(); + if let Some(key) = state.keys[key_index].as_ref() { + // Only the current "Alice" accepts rekeys initiated by the current "Bob." + if !key.bob { + let mut c = key.get_receive_cipher(); + c.reset_init_gcm(&incoming_message_nonce); + c.crypt_in_place(&mut pkt_assembled[AliceRekeyInit::ENC_START..AliceRekeyInit::AUTH_START]); + let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[AliceRekeyInit::AUTH_START..]); + key.return_receive_cipher(c); + + if aead_authentication_ok { + let pkt: &AliceRekeyInit = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); + if let Some(alice_e) = P384PublicKey::from_bytes(&pkt.alice_e) { + let bob_e_secret = P384KeyPair::generate(); + let next_session_key = Secret(hmac_sha512( + key.ratchet_key.as_bytes(), + bob_e_secret.agree(&alice_e).ok_or(Error::FailedAuthentication)?.as_bytes(), + )); + + let mut reply_buf = [0u8; BobRekeyAck::SIZE]; + let reply: &mut BobRekeyAck = byte_array_as_proto_buffer_mut(&mut reply_buf).unwrap(); + reply.bob_e = *bob_e_secret.public_key_bytes(); + reply.next_key_fingerprint = SHA384::hash(next_session_key.as_bytes()); + + let counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; + let mut c = key.get_send_cipher(counter.get())?; + c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_BOB_REKEY_ACK, counter.get())); + c.crypt_in_place(&mut reply_buf[BobRekeyAck::ENC_START..BobRekeyAck::AUTH_START]); + reply_buf[BobRekeyAck::AUTH_START..].copy_from_slice(&c.finish_encrypt()); + key.return_send_cipher(c); + + send(Some(&session), &mut reply_buf); + + drop(state); + let mut state = session.state.write().unwrap(); + let _ = state.keys[key_index ^ 1].replace(SessionKey::new::( + next_session_key, + current_time, + counter.get(), + false, + )); + + return Ok(ReceiveResult::Ok); + } + } + return Err(Error::FailedAuthentication); + } + } + return Err(Error::OutOfSequence); + } else { + return Err(Error::UnknownLocalSessionId); + } + } + + PACKET_TYPE_BOB_REKEY_ACK => { + if pkt_assembled.len() != BobRekeyAck::SIZE { + return Err(Error::InvalidPacket); + } + if incoming.is_some() { + return Err(Error::OutOfSequence); + } + + if let Some(session) = session { + let state = session.state.read().unwrap(); + if let Offer::RekeyInit(alice_e_secret, _, _) = &state.current_offer { + if let Some(key) = state.keys[key_index].as_ref() { + // Only the current "Bob" initiates rekeys and expects this ACK. + if key.bob { + let mut c = key.get_receive_cipher(); + c.reset_init_gcm(&incoming_message_nonce); + c.crypt_in_place(&mut pkt_assembled[BobRekeyAck::ENC_START..BobRekeyAck::AUTH_START]); + let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[BobRekeyAck::AUTH_START..]); + key.return_receive_cipher(c); + + if aead_authentication_ok { + let pkt: &BobRekeyAck = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); + if let Some(bob_e) = P384PublicKey::from_bytes(&pkt.bob_e) { + let next_session_key = Secret(hmac_sha512( + key.ratchet_key.as_bytes(), + alice_e_secret.agree(&bob_e).ok_or(Error::FailedAuthentication)?.as_bytes(), + )); + + if secure_eq(&pkt.next_key_fingerprint, &SHA384::hash(next_session_key.as_bytes())) { + drop(state); + let next_key_index = key_index ^ 1; + let mut state = session.state.write().unwrap(); + let _ = state.keys[next_key_index].replace(SessionKey::new::( + next_session_key, + current_time, + session.send_counter.load(Ordering::Acquire), + true, + )); + state.current_key = next_key_index; // this is an ACK so it's confirmed + state.current_offer = Offer::None; + + return Ok(ReceiveResult::Ok); + } + } + } + return Err(Error::FailedAuthentication); + } + } + } + return Err(Error::OutOfSequence); + } else { + return Err(Error::UnknownLocalSessionId); + } + } + + _ => { + return Err(Error::InvalidPacket); + } + } + } + } +} + +impl Session { /// Send data over the session. /// /// * `send` - Function to call to send physical packet(s) @@ -206,8 +1242,8 @@ impl Session { debug_assert!(mtu_sized_buffer.len() >= MIN_TRANSPORT_MTU); let state = self.state.read().unwrap(); if let Some(remote_session_id) = state.remote_session_id { - if let Some(session_key) = state.session_keys[state.cur_session_key_idx].as_ref() { - let counter = self.send_counter.fetch_add(1, Ordering::SeqCst); + if let Some(session_key) = state.keys[state.current_key].as_ref() { + let counter = self.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?.get(); let mut c = session_key.get_send_cipher(counter)?; c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_DATA, counter)); @@ -216,28 +1252,33 @@ impl Session { (((data.len() + AES_GCM_TAG_SIZE) as f32) / (mtu_sized_buffer.len() - HEADER_SIZE) as f32).ceil() as usize; let fragment_max_chunk_size = mtu_sized_buffer.len() - HEADER_SIZE; let last_fragment_no = fragment_count - 1; + for fragment_no in 0..fragment_count { let chunk_size = fragment_max_chunk_size.min(data.len()); let mut fragment_size = chunk_size + HEADER_SIZE; + set_packet_header( mtu_sized_buffer, fragment_count, fragment_no, PACKET_TYPE_DATA, u64::from(remote_session_id), - session_key.ratchet_count, + state.current_key, counter, - )?; + ); + c.crypt(&data[..chunk_size], &mut mtu_sized_buffer[HEADER_SIZE..fragment_size]); data = &data[chunk_size..]; + if fragment_no == last_fragment_no { debug_assert!(data.is_empty()); let tagged_fragment_size = fragment_size + AES_GCM_TAG_SIZE; mtu_sized_buffer[fragment_size..tagged_fragment_size].copy_from_slice(&c.finish_encrypt()); fragment_size = tagged_fragment_size; } - self.header_check_cipher - .encrypt_block_in_place(&mut mtu_sized_buffer[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); + + self.header_protection_cipher + .encrypt_block_in_place(&mut mtu_sized_buffer[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); send(&mut mtu_sized_buffer[..fragment_size]); } debug_assert!(data.is_empty()); @@ -245,11 +1286,7 @@ impl Session { session_key.return_send_cipher(c); return Ok(()); - } else { - unlikely_branch(); } - } else { - unlikely_branch(); } return Err(Error::SessionNotEstablished); } @@ -257,1049 +1294,136 @@ impl Session { /// Check whether this session is established. pub fn established(&self) -> bool { let state = self.state.read().unwrap(); - state.remote_session_id.is_some() && state.session_keys[state.cur_session_key_idx].is_some() + state.keys[state.current_key].is_some() } - /// Get the shared key fingerprint, ratchet count, and whether Kyber was used, or None if not yet established. - pub fn status(&self) -> Option<([u8; 16], u64, Role, bool)> { - let state = self.state.read().unwrap(); - state.session_keys[state.cur_session_key_idx] - .as_ref() - .map(|k| (k.secret_fingerprint, k.ratchet_count, k.role, k.jedi)) - } - - /// This function needs to be called on each session at least every SERVICE_INTERVAL milliseconds. + /// Send a rekey init message. /// - /// * `app` - Interface to application using ZSSP - /// * `send` - Function to call to send physical packet(s) - /// * `offer_metadata' - Any meta-data to include with initial key offers sent. - /// * `mtu` - Current physical transport MTU - /// * `current_time` - Current monotonic time in milliseconds - /// * `force_expire` - Re-key now regardless of key aging (if it is our turn!) - pub fn service( - &self, - app: &Application, - mut send: SendFunction, - offer_metadata: &[u8], - mtu: usize, - current_time: i64, - force_expire: bool, - ) { + /// This is called from the session context's service() method when it's time to rekey. + /// It should only be called when the current key was established in the 'bob' role. This + /// is checked when rekey time is checked. + fn initiate_rekey(&self, mut send: SendFunction, current_time: i64) { + let rekey_e = P384KeyPair::generate(); + + let mut rekey_buf = [0u8; AliceRekeyInit::SIZE]; + let pkt: &mut AliceRekeyInit = byte_array_as_proto_buffer_mut(&mut rekey_buf).unwrap(); + pkt.alice_e = *rekey_e.public_key_bytes(); + let state = self.state.read().unwrap(); - if state.session_keys[state.cur_session_key_idx].as_ref().map_or(true, |k| { - matches!(k.role, Role::Bob) - && (force_expire || self.send_counter.load(Ordering::Relaxed) >= k.rekey_at_counter || current_time >= k.rekey_at_time) - }) && state - .offer - .as_ref() - .map_or(true, |o| (current_time - o.creation_time) > Application::REKEY_RATE_LIMIT_MS) - { - if let Some(remote_s_public) = P384PublicKey::from_bytes(&self.remote_s_public_p384_bytes) { - let mut offer = None; - if send_ephemeral_offer( - &mut send, - self.send_counter.fetch_add(1, Ordering::SeqCst), - self.id, - state.remote_session_id, - app.get_local_s_public_blob(), - offer_metadata, - &remote_s_public, - &self.remote_s_public_blob_hash, - &self.noise_ss, - state.session_keys[state.cur_session_key_idx].as_ref(), - if state.remote_session_id.is_some() { - Some(&self.header_check_cipher) - } else { - None - }, - mtu, - current_time, - &mut offer, - ) - .is_ok() - { - drop(state); - let _ = self.state.write().unwrap().offer.replace(offer.unwrap()); + if let Some(remote_session_id) = state.remote_session_id { + if let Some(key) = state.keys[state.current_key].as_ref() { + if let Some(counter) = self.get_next_outgoing_counter() { + if let Ok(mut gcm) = key.get_send_cipher(counter.get()) { + gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_ALICE_REKEY_INIT, counter.get())); + gcm.crypt_in_place(&mut rekey_buf[AliceRekeyInit::ENC_START..AliceRekeyInit::AUTH_START]); + rekey_buf[AliceRekeyInit::AUTH_START..].copy_from_slice(&gcm.finish_encrypt()); + key.return_send_cipher(gcm); + + debug_assert!(rekey_buf.len() <= MIN_TRANSPORT_MTU); + set_packet_header( + &mut rekey_buf, + 1, + 0, + PACKET_TYPE_ALICE_REKEY_INIT, + u64::from(remote_session_id), + state.current_key, + counter.get(), + ); + + send(&mut rekey_buf); + + drop(state); + self.state.write().unwrap().current_offer = Offer::RekeyInit(rekey_e, rekey_buf, AtomicI64::new(current_time)); + } } } } } + /// Get the next outgoing counter value. + #[inline(always)] + fn get_next_outgoing_counter(&self) -> Option { + NonZeroU64::new(self.send_counter.fetch_add(1, Ordering::SeqCst)) + } + /// Check the receive window without mutating state. #[inline(always)] fn check_receive_window(&self, counter: u64) -> bool { - let c = self.receive_window[(counter as usize) % COUNTER_WINDOW_MAX_OUT_OF_ORDER].load(Ordering::Acquire); - c < counter && counter.wrapping_sub(c) < COUNTER_WINDOW_MAX_SKIP_AHEAD + let prev_counter = self.receive_window[(counter as usize) % COUNTER_WINDOW_MAX_OOO].load(Ordering::Acquire); + prev_counter < counter && counter.wrapping_sub(prev_counter) < COUNTER_WINDOW_MAX_SKIP_AHEAD } /// Update the receive window, returning true if the packet is still valid. /// This should only be called after the packet is authenticated. #[inline(always)] fn update_receive_window(&self, counter: u64) -> bool { - let c = self.receive_window[(counter as usize) % COUNTER_WINDOW_MAX_OUT_OF_ORDER].fetch_max(counter, Ordering::AcqRel); - c < counter && counter.wrapping_sub(c) < COUNTER_WINDOW_MAX_SKIP_AHEAD + let prev_counter = self.receive_window[(counter as usize) % COUNTER_WINDOW_MAX_OOO].fetch_max(counter, Ordering::AcqRel); + prev_counter < counter && counter.wrapping_sub(prev_counter) < COUNTER_WINDOW_MAX_SKIP_AHEAD } } -impl ReceiveContext { - pub fn new(app: &Application) -> Self { - Self { - initial_offer_defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())), - incoming_init_header_check_cipher: Aes::new( - kbkdf512(app.get_local_s_public_blob_hash(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), - ), - } - } - - /// Receive, authenticate, decrypt, and process a physical wire packet. - /// - /// * `app` - Interface to application using ZSSP - /// * `remote_address` - Remote physical address of source endpoint - /// * `data_buf` - Buffer to receive decrypted and authenticated object data (an error is returned if too small) - /// * `incoming_packet_buf` - Buffer containing incoming wire packet (receive() takes ownership) - /// * `mtu` - Physical wire MTU for sending packets - /// * `current_time` - Current monotonic time in milliseconds - #[inline] - pub fn receive<'a, SendFunction: FnMut(&mut [u8])>( - &self, - app: &Application, - remote_address: &Application::RemoteAddress, - mut send: SendFunction, - data_buf: &'a mut [u8], - mut incoming_packet_buf: Application::IncomingPacketBuffer, - mtu: usize, - current_time: i64, - ) -> Result, Error> { - let incoming_packet: &mut [u8] = incoming_packet_buf.as_mut(); - if incoming_packet.len() < MIN_PACKET_SIZE { - unlikely_branch(); - return Err(Error::InvalidPacket); - } - - if let Some(local_session_id) = SessionId::new_from_u64_le(memory::load_raw(incoming_packet)) { - if let Some(session) = app.lookup_session(local_session_id) { - session - .header_check_cipher - .decrypt_block_in_place(&mut incoming_packet[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); - let raw_header_a = u16::from_le(memory::load_raw(&incoming_packet[6..])); - let key_index = (raw_header_a & 1) as usize; - let packet_type = (raw_header_a.wrapping_shr(1) & 7) as u8; - let fragment_count = ((raw_header_a.wrapping_shr(4) & 63) + 1) as u8; - let fragment_no = raw_header_a.wrapping_shr(10) as u8; - let counter = u64::from_le(memory::load_raw(&incoming_packet[8..])); - - if session.check_receive_window(counter) { - if fragment_count > 1 { - if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count { - let mut defrag = session.defrag.lock().unwrap(); - let fragment_gather_array = defrag.get_or_create_mut(&counter, || GatherArray::new(fragment_count)); - if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) { - drop(defrag); // release lock - return self.receive_complete( - app, - remote_address, - &mut send, - data_buf, - counter, - assembled_packet.as_ref(), - packet_type, - Some(session), - key_index, - mtu, - current_time, - ); - } - } else { - unlikely_branch(); - return Err(Error::InvalidPacket); - } - } else { - return self.receive_complete( - app, - remote_address, - &mut send, - data_buf, - counter, - &[incoming_packet_buf], - packet_type, - Some(session), - key_index, - mtu, - current_time, - ); - } - } else { - unlikely_branch(); - return Ok(ReceiveResult::Ignored); - } - } else { - unlikely_branch(); - return Err(Error::UnknownLocalSessionId(local_session_id)); - } - } else { - unlikely_branch(); // we want data receive to be the priority branch, this is only occasionally used - - self.incoming_init_header_check_cipher - .decrypt_block_in_place(&mut incoming_packet[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); - let raw_header_a = u16::from_le(memory::load_raw(&incoming_packet[6..])); - let key_index = (raw_header_a & 1) as usize; - let packet_type = (raw_header_a.wrapping_shr(1) & 7) as u8; - let fragment_count = ((raw_header_a.wrapping_shr(4) & 63) + 1) as u8; - let fragment_no = raw_header_a.wrapping_shr(10) as u8; - let counter = u64::from_le(memory::load_raw(&incoming_packet[8..])); - - if fragment_count > 1 { - let mut defrag = self.initial_offer_defrag.lock().unwrap(); - let fragment_gather_array = defrag.get_or_create_mut(&counter, || GatherArray::new(fragment_count)); - if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) { - drop(defrag); // release lock - return self.receive_complete( - app, - remote_address, - &mut send, - data_buf, - counter, - assembled_packet.as_ref(), - packet_type, - None, - key_index, - mtu, - current_time, - ); - } - } else { - return self.receive_complete( - app, - remote_address, - &mut send, - data_buf, - counter, - &[incoming_packet_buf], - packet_type, - None, - key_index, - mtu, - current_time, - ); - } - }; - - return Ok(ReceiveResult::Ok); - } - - /// Called internally when all fragments of a packet are received. - /// - /// NOTE: header check codes will already have been validated on receipt of each fragment. AEAD authentication - /// and decryption has NOT yet been performed, and is done here. - fn receive_complete<'a, SendFunction: FnMut(&mut [u8])>( - &self, - app: &Application, - remote_address: &Application::RemoteAddress, - send: &mut SendFunction, - data_buf: &'a mut [u8], - counter: u64, - fragments: &[Application::IncomingPacketBuffer], - packet_type: u8, - session: Option>, - key_index: usize, - mtu: usize, - current_time: i64, - ) -> Result, Error> { - debug_assert!(fragments.len() >= 1); - - let message_nonce = create_message_nonce(packet_type, counter); - if packet_type == PACKET_TYPE_DATA { - if let Some(session) = session { - let state = session.state.read().unwrap(); - if let Some(session_key) = state.session_keys[key_index].as_ref() { - let mut c = session_key.get_receive_cipher(); - c.reset_init_gcm(&message_nonce); - - let mut data_len = 0; - - // Decrypt fragments 0..N-1 where N is the number of fragments. - for f in fragments[..(fragments.len() - 1)].iter() { - let f = f.as_ref(); - debug_assert!(f.len() >= HEADER_SIZE); - let current_frag_data_start = data_len; - data_len += f.len() - HEADER_SIZE; - if data_len > data_buf.len() { - unlikely_branch(); - session_key.return_receive_cipher(c); - return Err(Error::DataBufferTooSmall); - } - c.crypt(&f[HEADER_SIZE..], &mut data_buf[current_frag_data_start..data_len]); - } - - // Decrypt final fragment (or only fragment if not fragmented) - let current_frag_data_start = data_len; - let last_fragment = fragments.last().unwrap().as_ref(); - if last_fragment.len() < (HEADER_SIZE + AES_GCM_TAG_SIZE) { - unlikely_branch(); - return Err(Error::InvalidPacket); - } - data_len += last_fragment.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE); - if data_len > data_buf.len() { - unlikely_branch(); - session_key.return_receive_cipher(c); - return Err(Error::DataBufferTooSmall); - } - let payload_end = last_fragment.len() - AES_GCM_TAG_SIZE; - c.crypt( - &last_fragment[HEADER_SIZE..payload_end], - &mut data_buf[current_frag_data_start..data_len], - ); - - let gcm_tag = &last_fragment[payload_end..]; - let aead_authentication_ok = c.finish_decrypt(gcm_tag); - session_key.return_receive_cipher(c); - - if aead_authentication_ok { - if session.update_receive_window(counter) { - // If the packet authenticated, this confirms that the other side indeed - // knows this session key. In that case mark the session key as confirmed - // and if the current active key is older switch it to point to this one. - if !session_key.confirmed { - unlikely_branch(); - let this_ratchet_count = session_key.ratchet_count; - drop(state); - let mut state = session.state.write().unwrap(); - - state.session_keys[key_index].as_mut().unwrap().confirmed = true; - if state.cur_session_key_idx != key_index { - if let Some(other_session_key) = state.session_keys[state.cur_session_key_idx].as_ref() { - if other_session_key.ratchet_count < this_ratchet_count { - state.cur_session_key_idx = key_index; - } - } else { - state.cur_session_key_idx = key_index; - } - } - } - - return Ok(ReceiveResult::OkData(&mut data_buf[..data_len])); - } else { - unlikely_branch(); - return Ok(ReceiveResult::Ignored); - } - } - } - return Err(Error::FailedAuthentication); - } else { - unlikely_branch(); - return Err(Error::SessionNotEstablished); - } - } else { - unlikely_branch(); - - // To greatly simplify logic handling key exchange packets, assemble these first. - // Handling KEX packets isn't the fast path so the extra copying isn't significant. - const KEX_BUF_LEN: usize = 4096; - let mut kex_packet = [0_u8; KEX_BUF_LEN]; - let mut kex_packet_len = 0; - for i in 0..fragments.len() { - let mut ff = fragments[i].as_ref(); - if ff.len() < MIN_PACKET_SIZE { - return Err(Error::InvalidPacket); - } - if i > 0 { - ff = &ff[HEADER_SIZE..]; - } - let j = kex_packet_len + ff.len(); - if j > KEX_BUF_LEN { - return Err(Error::InvalidPacket); - } - kex_packet[kex_packet_len..j].copy_from_slice(ff); - kex_packet_len = j; - } - let kex_packet_saved_ciphertext = kex_packet.clone(); // save for HMAC check later - - // Key exchange packets begin (after header) with the session protocol version. This could be - // changed in the future to support a different cipher suite. - if kex_packet[HEADER_SIZE] != SESSION_PROTOCOL_VERSION { - return Err(Error::UnknownProtocolVersion); - } - - match packet_type { - PACKET_TYPE_INITIAL_KEY_OFFER => { - // alice (remote) -> bob (local) - - //////////////////////////////////////////////////////////////// - // packet decoding for noise initial key offer - // -> e, es, s, ss - //////////////////////////////////////////////////////////////// - - if kex_packet_len < (HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + HMAC_SIZE + HMAC_SIZE) { - return Err(Error::InvalidPacket); - } - - let plaintext_end = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; - let payload_end = kex_packet_len - (AES_GCM_TAG_SIZE + HMAC_SIZE + HMAC_SIZE); - let aes_gcm_tag_end = kex_packet_len - (HMAC_SIZE + HMAC_SIZE); - let hmac1_end = kex_packet_len - HMAC_SIZE; - - // Check the secondary HMAC first, which proves that the sender knows the recipient's full static identity. - if !secure_eq( - &hmac_sha384_2( - app.get_local_s_public_blob_hash(), - &message_nonce, - &kex_packet[HEADER_SIZE..hmac1_end], - ), - &kex_packet[hmac1_end..kex_packet_len], - ) { - return Err(Error::FailedAuthentication); - } - - // Check rate limits. - if let Some(session) = session.as_ref() { - if (session.state.read().unwrap().last_remote_offer + Application::REKEY_RATE_LIMIT_MS) > current_time { - return Err(Error::RateLimited); - } - } else { - if !app.check_new_session(self, remote_address) { - return Err(Error::RateLimited); - } - } - - // Key agreement: alice (remote) ephemeral NIST P-384 <> local static NIST P-384 - let alice_e_public = - P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..plaintext_end]).ok_or(Error::FailedAuthentication)?; - let noise_es = app - .get_local_s_keypair() - .agree(&alice_e_public) - .ok_or(Error::FailedAuthentication)?; - - // Initial key derivation from starting point, mixing in alice's ephemeral public and the es. - let noise_ik_incomplete_es = Secret(hmac_sha512( - &hmac_sha512(&INITIAL_KEY, alice_e_public.as_bytes()), - noise_es.as_bytes(), - )); - - // Decrypt the encrypted part of the packet payload and authenticate the above key exchange via AES-GCM auth. - let mut c = AesGcm::new( - kbkdf512(noise_ik_incomplete_es.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::(), - false, - ); - c.reset_init_gcm(&message_nonce); - c.crypt_in_place(&mut kex_packet[plaintext_end..payload_end]); - let gcm_tag = &kex_packet[payload_end..aes_gcm_tag_end]; - if !c.finish_decrypt(gcm_tag) { - return Err(Error::FailedAuthentication); - } - - // Parse payload and get alice's session ID, alice's public blob, metadata, and (if present) Alice's Kyber1024 public. - let ( - offer_id, - alice_session_id, - alice_s_public_blob, - alice_metadata, - alice_hk_public_raw, - alice_ratchet_key_fingerprint, - ) = parse_dec_key_offer_after_header(&kex_packet[plaintext_end..kex_packet_len], packet_type)?; - - // We either have a session, in which case they should have supplied a ratchet key fingerprint, or - // we don't and they should not have supplied one. - if session.is_some() != alice_ratchet_key_fingerprint.is_some() { - return Err(Error::FailedAuthentication); - } - - // Extract alice's static NIST P-384 public key from her public blob. - let alice_s_public = Application::extract_s_public_from_raw(alice_s_public_blob).ok_or(Error::InvalidPacket)?; - - // Key agreement: both sides' static P-384 keys. - let noise_ss = app - .get_local_s_keypair() - .agree(&alice_s_public) - .ok_or(Error::FailedAuthentication)?; - - // Mix result of 'ss' agreement into master key. - let noise_ik_incomplete_es_ss = Secret(hmac_sha512(noise_ik_incomplete_es.as_bytes(), noise_ss.as_bytes())); - drop(noise_ik_incomplete_es); - - // Authenticate entire packet with HMAC-SHA384, verifying alice's identity via 'ss' secret that was - // just mixed into the key. - if !secure_eq( - &hmac_sha384_2( - kbkdf512(noise_ik_incomplete_es_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), - &message_nonce, - &kex_packet_saved_ciphertext[HEADER_SIZE..aes_gcm_tag_end], - ), - &kex_packet[aes_gcm_tag_end..hmac1_end], - ) { - return Err(Error::FailedAuthentication); - } - - // Alice's offer has been verified and her current key state reconstructed. - - // Perform checks and match ratchet key if there's an existing session, or gate (via host) and - // then create new sessions. - let (new_session, ratchet_key, last_ratchet_count) = if let Some(session) = session.as_ref() { - // Existing session identity must match the one in this offer. - if !secure_eq(&session.remote_s_public_blob_hash, &SHA384::hash(&alice_s_public_blob)) { - return Err(Error::FailedAuthentication); - } - - // Match ratchet key fingerprint and fail if no match, which likely indicates an old offer packet. - let alice_ratchet_key_fingerprint = alice_ratchet_key_fingerprint.unwrap(); - let mut ratchet_key = None; - let mut last_ratchet_count = 0; - let state = session.state.read().unwrap(); - for k in state.session_keys.iter() { - if let Some(k) = k.as_ref() { - if public_fingerprint_of_secret(k.ratchet_key.as_bytes())[..16].eq(alice_ratchet_key_fingerprint) { - ratchet_key = Some(k.ratchet_key.clone()); - last_ratchet_count = k.ratchet_count; - break; - } - } - } - if ratchet_key.is_none() { - return Ok(ReceiveResult::Ignored); // old packet? - } - - (None, ratchet_key, last_ratchet_count) - } else { - if let Some((new_session_id, psk, associated_object)) = - app.accept_new_session(self, remote_address, alice_s_public_blob, alice_metadata) - { - let header_check_cipher = Aes::new( - kbkdf512(noise_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), - ); - ( - Some(Session:: { - id: new_session_id, - application_data: associated_object, - receive_window: std::array::from_fn(|_| AtomicU64::new(0)), - send_counter: AtomicU64::new(1), - psk, - noise_ss, - header_check_cipher, - state: RwLock::new(SessionMutableState { - remote_session_id: Some(alice_session_id), - session_keys: [None, None], - cur_session_key_idx: 0, - offer: None, - last_remote_offer: current_time, - }), - remote_s_public_blob_hash: SHA384::hash(&alice_s_public_blob), - remote_s_public_p384_bytes: alice_s_public.as_bytes().clone(), - defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), - }), - None, - 0, - ) - } else { - return Err(Error::NewSessionRejected); - } - }; - - // Set 'session' to a reference to either the existing or the new session. - let existing_session = session; - let session = existing_session.as_ref().map_or_else(|| new_session.as_ref().unwrap(), |s| &*s); - - if !session.update_receive_window(counter) { - return Ok(ReceiveResult::Ignored); - } - - // Generate our ephemeral NIST P-384 key pair. - let bob_e_keypair = P384KeyPair::generate(); - - // Key agreement: both sides' ephemeral P-384 public keys. - let noise_ee = bob_e_keypair.agree(&alice_e_public).ok_or(Error::FailedAuthentication)?; - - // Key agreement: bob (local) static NIST P-384, alice (remote) ephemeral P-384. - let noise_se = bob_e_keypair.agree(&alice_s_public).ok_or(Error::FailedAuthentication)?; - - // Mix in the psk, the key to this point, our ephemeral public, ee, and se, completing Noise_IK. - // - // FIPS note: the order of HMAC parameters are flipped here from the usual Noise HMAC(key, X). That's because - // NIST/FIPS allows HKDF with HMAC(salt, key) and salt is allowed to be anything. This way if the PSK is not - // FIPS compliant the compliance of the entire key derivation is not invalidated. Both inputs are secrets of - // fixed size so this shouldn't matter cryptographically. - let noise_ik_complete = Secret(hmac_sha512( - session.psk.as_bytes(), - &hmac_sha512( - &hmac_sha512( - &hmac_sha512(noise_ik_incomplete_es_ss.as_bytes(), bob_e_keypair.public_key_bytes()), - noise_ee.as_bytes(), - ), - noise_se.as_bytes(), - ), - )); - drop(noise_ik_incomplete_es_ss); - drop(noise_ee); - drop(noise_se); - - // At this point we've completed Noise_IK key derivation with NIST P-384 ECDH, but now for hybrid and ratcheting... - - // Generate a Kyber encapsulated ciphertext if Kyber is enabled and the other side sent us a public key. - let (bob_hk_public, hybrid_kk) = if JEDI && alice_hk_public_raw.len() > 0 { - if let Ok((bob_hk_public, hybrid_kk)) = - pqc_kyber::encapsulate(alice_hk_public_raw, &mut random::SecureRandom::default()) - { - (Some(bob_hk_public), Some(Secret(hybrid_kk))) - } else { - return Err(Error::FailedAuthentication); - } - } else { - (None, None) - }; - - //////////////////////////////////////////////////////////////// - // packet encoding for noise key counter offer - // <- e, ee, se - //////////////////////////////////////////////////////////////// - - let next_ratchet_count = last_ratchet_count + 1; - - let mut reply_buf = [0_u8; KEX_BUF_LEN]; - let reply_counter = session.send_counter.fetch_add(1, Ordering::SeqCst); - let mut idx = HEADER_SIZE; - - idx = safe_write_all(&mut reply_buf, idx, &[SESSION_PROTOCOL_VERSION])?; - idx = safe_write_all(&mut reply_buf, idx, bob_e_keypair.public_key_bytes())?; - let plaintext_end = idx; - - idx = safe_write_all(&mut reply_buf, idx, offer_id)?; - idx = safe_write_all(&mut reply_buf, idx, session.id.as_bytes())?; - idx = varint_safe_write(&mut reply_buf, idx, 0)?; // they don't need our static public; they have it - idx = varint_safe_write(&mut reply_buf, idx, 0)?; // no meta-data in counter-offers (could be used in the future) - if let Some(bob_hk_public) = bob_hk_public.as_ref() { - idx = safe_write_all(&mut reply_buf, idx, &[HYBRID_KEY_TYPE_KYBER1024])?; - idx = safe_write_all(&mut reply_buf, idx, bob_hk_public)?; - } else { - idx = safe_write_all(&mut reply_buf, idx, &[HYBRID_KEY_TYPE_NONE])?; - } - if ratchet_key.is_some() { - idx = safe_write_all(&mut reply_buf, idx, &[0x01])?; - idx = safe_write_all(&mut reply_buf, idx, alice_ratchet_key_fingerprint.unwrap())?; - } else { - idx = safe_write_all(&mut reply_buf, idx, &[0x00])?; - } - let payload_end = idx; - - let reply_message_nonce = create_message_nonce(PACKET_TYPE_KEY_COUNTER_OFFER, reply_counter); - - // Encrypt reply packet using final Noise_IK key BEFORE mixing hybrid or ratcheting, since the other side - // must decrypt before doing these things. - let mut c = AesGcm::new( - kbkdf512(noise_ik_complete.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::(), - true, - ); - c.reset_init_gcm(&reply_message_nonce); - c.crypt_in_place(&mut reply_buf[plaintext_end..payload_end]); - let gcm_tag = c.finish_encrypt(); - - idx = safe_write_all(&mut reply_buf, idx, &gcm_tag)?; - let aes_gcm_tag_end = idx; - - // Mix ratchet key from previous session key (if any) and Kyber1024 hybrid shared key (if any). - let mut session_key = noise_ik_complete; - if let Some(ratchet_key) = ratchet_key { - session_key = Secret(hmac_sha512(ratchet_key.as_bytes(), session_key.as_bytes())); - } - if let Some(hybrid_kk) = hybrid_kk.as_ref() { - session_key = Secret(hmac_sha512(hybrid_kk.as_bytes(), session_key.as_bytes())); - } - - // Authenticate packet using HMAC-SHA384 with final key. Note that while the final key now has the Kyber secret - // mixed in, this doesn't constitute session authentication with Kyber because there's no static Kyber key - // associated with the remote identity. An attacker who can break NIST P-384 (and has the psk) could MITM the - // Kyber exchange, but you'd need a not-yet-existing quantum computer for that. - let hmac = hmac_sha384_2( - kbkdf512(session_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), - &reply_message_nonce, - &reply_buf[HEADER_SIZE..aes_gcm_tag_end], - ); - idx = safe_write_all(&mut reply_buf, idx, &hmac)?; - let packet_end = idx; - - let session_key = SessionKey::new( - session_key, - Role::Bob, - current_time, - reply_counter, - next_ratchet_count, - false, // Bob can't know yet if Alice got the counter offer - hybrid_kk.is_some(), - ); - - let next_key_index = (next_ratchet_count as usize) & 1; - - let mut state = session.state.write().unwrap(); - let _ = state.remote_session_id.replace(alice_session_id); - let _ = state.session_keys[next_key_index].replace(session_key); - state.last_remote_offer = current_time; - drop(state); - - // Bob now has final key state for this exchange. Yay! Now reply to Alice so she can construct it. - - send_with_fragmentation( - send, - &mut reply_buf[..packet_end], - mtu, - PACKET_TYPE_KEY_COUNTER_OFFER, - u64::from(alice_session_id), - next_ratchet_count, - reply_counter, - &session.header_check_cipher, - )?; - - if new_session.is_some() { - return Ok(ReceiveResult::OkNewSession(new_session.unwrap())); - } else { - return Ok(ReceiveResult::Ok); - } - } - - PACKET_TYPE_KEY_COUNTER_OFFER => { - // bob (remote) -> alice (local) - - //////////////////////////////////////////////////////////////// - // packet decoding for noise key counter offer - // <- e, ee, se - //////////////////////////////////////////////////////////////// - - if kex_packet_len < (HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + HMAC_SIZE) { - return Err(Error::InvalidPacket); - } - let plaintext_end = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; - let payload_end = kex_packet_len - (AES_GCM_TAG_SIZE + HMAC_SIZE); - let aes_gcm_tag_end = kex_packet_len - HMAC_SIZE; - - if let Some(session) = session { - let state = session.state.read().unwrap(); - if let Some(offer) = state.offer.as_ref() { - let bob_e_public = P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..plaintext_end]) - .ok_or(Error::FailedAuthentication)?; - let noise_ee = offer.alice_e_keypair.agree(&bob_e_public).ok_or(Error::FailedAuthentication)?; - let noise_se = app.get_local_s_keypair().agree(&bob_e_public).ok_or(Error::FailedAuthentication)?; - - let noise_ik_complete = Secret(hmac_sha512( - session.psk.as_bytes(), - &hmac_sha512( - &hmac_sha512(&hmac_sha512(offer.ss_key.as_bytes(), bob_e_public.as_bytes()), noise_ee.as_bytes()), - noise_se.as_bytes(), - ), - )); - drop(noise_ee); - drop(noise_se); - - let mut c = AesGcm::new( - kbkdf512(noise_ik_complete.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE) - .first_n::(), - false, - ); - c.reset_init_gcm(&message_nonce); - c.crypt_in_place(&mut kex_packet[plaintext_end..payload_end]); - let gcm_tag = &kex_packet[payload_end..aes_gcm_tag_end]; - if !c.finish_decrypt(gcm_tag) { - return Err(Error::FailedAuthentication); - } - - // Alice has now completed Noise_IK with NIST P-384 and verified with GCM auth, but now for hybrid... - - let (offer_id, bob_session_id, _, _, bob_hk_public_raw, bob_ratchet_key_id) = - parse_dec_key_offer_after_header(&kex_packet[plaintext_end..kex_packet_len], packet_type)?; - - // Check that this is a counter offer to the original offer we sent. - if !offer.id.eq(offer_id) { - return Ok(ReceiveResult::Ignored); - } - - // Kyber1024 key agreement if enabled. - let hybrid_kk = if JEDI && bob_hk_public_raw.len() > 0 && offer.alice_hk_keypair.is_some() { - if let Ok(hybrid_kk) = - pqc_kyber::decapsulate(bob_hk_public_raw, &offer.alice_hk_keypair.as_ref().unwrap().secret) - { - Some(Secret(hybrid_kk)) - } else { - return Err(Error::FailedAuthentication); - } - } else { - None - }; - - // The session key starts with the final noise_ik key and may have other things mixed into it below. - let mut session_key = noise_ik_complete; - - // Mix ratchet key from previous session key (if any) and Kyber1024 hybrid shared key (if any). - let last_ratchet_count = if bob_ratchet_key_id.is_some() && offer.ratchet_key.is_some() { - session_key = Secret(hmac_sha512(offer.ratchet_key.as_ref().unwrap().as_bytes(), session_key.as_bytes())); - offer.ratchet_count - } else { - 0 - }; - if let Some(hybrid_kk) = hybrid_kk.as_ref() { - session_key = Secret(hmac_sha512(hybrid_kk.as_bytes(), session_key.as_bytes())); - } - - // Check main packet HMAC for full validation of session key. - if !secure_eq( - &hmac_sha384_2( - kbkdf512(session_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), - &message_nonce, - &kex_packet_saved_ciphertext[HEADER_SIZE..aes_gcm_tag_end], - ), - &kex_packet[aes_gcm_tag_end..kex_packet_len], - ) { - return Err(Error::FailedAuthentication); - } - - // Alice has now completed and validated the full hybrid exchange. - - let reply_counter = session.send_counter.fetch_add(1, Ordering::SeqCst); - let next_ratchet_count = last_ratchet_count + 1; - - let session_key = SessionKey::new( - session_key, - Role::Alice, - current_time, - reply_counter, - next_ratchet_count, - true, // Alice knows Bob got the offer - hybrid_kk.is_some(), - ); - - drop(state); - let mut state = session.state.write().unwrap(); - - let _ = state.remote_session_id.replace(bob_session_id); - let next_key_index = (next_ratchet_count as usize) & 1; - let _ = state.session_keys[next_key_index].replace(session_key); - state.cur_session_key_idx = next_key_index; - let _ = state.offer.take(); - - return Ok(ReceiveResult::Ok); - } - } - - // Just ignore counter-offers that are out of place. They probably indicate that this side - // restarted and needs to establish a new session. - return Ok(ReceiveResult::Ignored); - } - - _ => return Err(Error::InvalidPacket), - } - } - } -} - -/// Create an send an ephemeral offer, populating ret_ephemeral_offer on success. -fn send_ephemeral_offer( - send: &mut SendFunction, - counter: u64, - alice_session_id: SessionId, - bob_session_id: Option, - alice_s_public_blob: &[u8], - alice_metadata: &[u8], - bob_s_public: &P384PublicKey, - bob_s_public_blob_hash: &[u8], - noise_ss: &Secret<48>, - current_key: Option<&SessionKey>, - header_check_cipher: Option<&Aes>, // None to use one based on the recipient's public key for initial contact - mtu: usize, - current_time: i64, - ret_ephemeral_offer: &mut Option, // We want to prevent copying the EphemeralOffer up the stack because it's very big. ret_ephemeral_offer will be overwritten with the returned EphemeralOffer when the call completes. -) -> Result<(), Error> { - // Generate a NIST P-384 pair. - let alice_e_keypair = P384KeyPair::generate(); - - // Perform key agreement with the other side's static P-384 public key. - let noise_es = alice_e_keypair.agree(bob_s_public).ok_or(Error::InvalidPacket)?; - - // Generate a Kyber1024 (hybrid PQ crypto) pair if enabled. - let alice_hk_keypair = if JEDI { - Some(pqc_kyber::keypair(&mut random::SecureRandom::get())) - } else { - None - }; - - // Get ratchet key for current key if one exists. - let (ratchet_key, ratchet_count) = if let Some(current_key) = current_key { - (Some(current_key.ratchet_key.clone()), current_key.ratchet_count) - } else { - (None, 0) - }; - - // Random ephemeral offer ID - let id: [u8; 16] = random::get_bytes_secure(); - - //////////////////////////////////////////////////////////////// - // packet encoding for noise initial key offer and for noise rekeying - // -> e, es, s, ss - //////////////////////////////////////////////////////////////// - - // Create ephemeral offer packet (not fragmented yet). - let mut packet_buf = [0_u8; 4096]; - let mut idx = HEADER_SIZE; - - idx = safe_write_all(&mut packet_buf, idx, &[SESSION_PROTOCOL_VERSION])?; - //TODO: check this, the below line is supposed to be the blob, not just the key, right? - idx = safe_write_all(&mut packet_buf, idx, alice_e_keypair.public_key_bytes())?; - let plaintext_end = idx; - - idx = safe_write_all(&mut packet_buf, idx, &id)?; - idx = safe_write_all(&mut packet_buf, idx, alice_session_id.as_bytes())?; - idx = varint_safe_write(&mut packet_buf, idx, alice_s_public_blob.len() as u64)?; - idx = safe_write_all(&mut packet_buf, idx, alice_s_public_blob)?; - idx = varint_safe_write(&mut packet_buf, idx, alice_metadata.len() as u64)?; - idx = safe_write_all(&mut packet_buf, idx, alice_metadata)?; - if let Some(hkp) = alice_hk_keypair { - idx = safe_write_all(&mut packet_buf, idx, &[HYBRID_KEY_TYPE_KYBER1024])?; - idx = safe_write_all(&mut packet_buf, idx, &hkp.public)?; - } else { - idx = safe_write_all(&mut packet_buf, idx, &[HYBRID_KEY_TYPE_NONE])?; - } - if let Some(ratchet_key) = ratchet_key.as_ref() { - idx = safe_write_all(&mut packet_buf, idx, &[0x01])?; - idx = safe_write_all(&mut packet_buf, idx, &public_fingerprint_of_secret(ratchet_key.as_bytes())[..16])?; - } else { - idx = safe_write_all(&mut packet_buf, idx, &[0x00])?; - } - let payload_end = idx; - - // Create ephemeral agreement secret. - let es_key = Secret(hmac_sha512( - &hmac_sha512(&INITIAL_KEY, alice_e_keypair.public_key_bytes()), - noise_es.as_bytes(), - )); - - let bob_session_id = bob_session_id.map_or(0u64, |i| u64::from(i)); - - let message_nonce = create_message_nonce(PACKET_TYPE_INITIAL_KEY_OFFER, counter); - - // Encrypt packet and attach AES-GCM tag. - let gcm_tag = { - let mut c = AesGcm::new( - kbkdf512(es_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::(), - true, - ); - c.reset_init_gcm(&message_nonce); - c.crypt_in_place(&mut packet_buf[plaintext_end..payload_end]); - c.finish_encrypt() - }; - - idx = safe_write_all(&mut packet_buf, idx, &gcm_tag)?; - let aes_gcm_tag_end = idx; - - // Mix in static secret. - let ss_key = Secret(hmac_sha512(es_key.as_bytes(), noise_ss.as_bytes())); - drop(es_key); - - // HMAC packet using static + ephemeral key. - let hmac1 = hmac_sha384_2( - kbkdf512(ss_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), - &message_nonce, - &packet_buf[HEADER_SIZE..aes_gcm_tag_end], - ); - idx = safe_write_all(&mut packet_buf, idx, &hmac1)?; - let hmac1_end = idx; - - // Add secondary HMAC to verify that the caller knows the recipient's full static public identity. - let hmac2 = hmac_sha384_2(bob_s_public_blob_hash, &message_nonce, &packet_buf[HEADER_SIZE..hmac1_end]); - idx = safe_write_all(&mut packet_buf, idx, &hmac2)?; - let packet_end = idx; - - let mut init_header_check_cipher_tmp = None; - send_with_fragmentation( - send, - &mut packet_buf[..packet_end], - mtu, - PACKET_TYPE_INITIAL_KEY_OFFER, - bob_session_id, - ratchet_count, - counter, - header_check_cipher.unwrap_or_else(|| { - init_header_check_cipher_tmp = Some(Aes::new( - kbkdf512(&bob_s_public_blob_hash, KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), - )); - init_header_check_cipher_tmp.as_ref().unwrap() - }), - )?; - - *ret_ephemeral_offer = Some(EphemeralOffer { - id, - creation_time: current_time, - ratchet_count, - ratchet_key, - ss_key, - alice_e_keypair, - alice_hk_keypair, - }); - - Ok(()) -} - fn set_packet_header( packet: &mut [u8], fragment_count: usize, fragment_no: usize, packet_type: u8, - recipient_session_id: u64, - ratchet_count: u64, + remote_session_id: u64, + key_index: usize, counter: u64, -) -> Result<(), Error> { +) { debug_assert!(packet.len() >= MIN_PACKET_SIZE); debug_assert!(fragment_count > 0); + debug_assert!(fragment_count <= MAX_FRAGMENTS); debug_assert!(fragment_no < MAX_FRAGMENTS); debug_assert!(packet_type <= 0x0f); // packet type is 4 bits - if fragment_count <= MAX_FRAGMENTS { - // [0-47] recipient session ID - // -- start of header check cipher single block encrypt -- - // [48-48] key index (least significant bit of ratchet count) - // [49-51] packet type (0-15) - // [52-57] fragment count (1..64 - 1, so 0 means 1 fragment) - // [58-63] fragment number (0..63) - // [64-127] 64-bit counter - memory::store_raw( - (u64::from(recipient_session_id) - | (ratchet_count & 1).wrapping_shl(48) - | (packet_type as u64).wrapping_shl(49) - | ((fragment_count - 1) as u64).wrapping_shl(52) - | (fragment_no as u64).wrapping_shl(58)) - .to_le(), - packet, - ); - memory::store_raw(counter.to_le(), &mut packet[8..]); - Ok(()) - } else { - unlikely_branch(); - Err(Error::DataTooLarge) - } + + // [0-47] recipient session ID + // -- start of header check cipher single block encrypt -- + // [48-48] key index (least significant bit) + // [49-51] packet type (0-15) + // [52-57] fragment count (1..64 - 1, so 0 means 1 fragment) + // [58-63] fragment number (0..63) + // [64-127] 64-bit counter + memory::store_raw( + (u64::from(remote_session_id) + | ((key_index & 1) as u64).wrapping_shl(48) + | (packet_type as u64).wrapping_shl(49) + | ((fragment_count - 1) as u64).wrapping_shl(52) + | (fragment_no as u64).wrapping_shl(58)) + .to_le(), + packet, + ); + memory::store_raw(counter.to_le(), &mut packet[8..]); } -#[derive(Clone, Copy)] -#[repr(C, packed)] -struct MessageNonce(u64, u32); - -/// Create a 12-bit AES-GCM nonce. -/// -/// The primary information that we want to be contained here is the counter and the -/// packet type. The former makes this unique and the latter's inclusion authenticates -/// it as effectively AAD. Other elements of the header are either not authenticated, -/// like fragmentation info, or their authentication is implied via key exchange like -/// the session ID. -/// -/// This is also used as part of HMAC authentication for key exchange packets. #[inline(always)] -fn create_message_nonce(packet_type: u8, counter: u64) -> [u8; 12] { - memory::to_byte_array(MessageNonce(counter.to_le(), (packet_type as u32).to_le())) +fn parse_packet_header(incoming_packet: &[u8]) -> (usize, u8, u8, u8, u64) { + let raw_header_a = u16::from_le(memory::load_raw(&incoming_packet[6..])); + let key_index = (raw_header_a & 1) as usize; + let packet_type = (raw_header_a.wrapping_shr(1) & 7) as u8; + let fragment_count = ((raw_header_a.wrapping_shr(4) & 63) + 1) as u8; + let fragment_no = raw_header_a.wrapping_shr(10) as u8; + let counter = u64::from_le(memory::load_raw(&incoming_packet[8..])); + (key_index, packet_type, fragment_count, fragment_no, counter) } /// Break a packet into fragments and send them all. +/// /// The contents of packet[] are mangled during this operation, so it should be discarded after. +/// This is only used for key exchange and control packets. For data packets this is done inline +/// for better performance with encryption and fragmentation happening at the same time. fn send_with_fragmentation( - send: &mut SendFunction, + mut send: SendFunction, packet: &mut [u8], mtu: usize, packet_type: u8, - recipient_session_id: u64, - ratchet_count: u64, + remote_session_id: Option, + key_index: usize, counter: u64, - header_check_cipher: &Aes, + header_protect_cipher: Option<&Aes>, ) -> Result<(), Error> { let packet_len = packet.len(); + let recipient_session_id = remote_session_id.map_or(SessionId::NONE, |s| u64::from(s)); let fragment_count = ((packet_len as f32) / (mtu as f32)).ceil() as usize; let mut fragment_start = 0; let mut fragment_end = packet_len.min(mtu); @@ -1311,10 +1435,12 @@ fn send_with_fragmentation( fragment_no, packet_type, recipient_session_id, - ratchet_count, + key_index, counter, - )?; - header_check_cipher.encrypt_block_in_place(&mut fragment[6..22]); + ); + if let Some(hcc) = header_protect_cipher { + hcc.encrypt_block_in_place(&mut fragment[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); + } send(fragment); fragment_start = fragment_end - HEADER_SIZE; fragment_end = (fragment_start + mtu).min(packet_len); @@ -1322,79 +1448,55 @@ fn send_with_fragmentation( Ok(()) } -/// Parse KEY_OFFER and KEY_COUNTER_OFFER starting after the unencrypted public key part. -fn parse_dec_key_offer_after_header( - incoming_packet: &[u8], - packet_type: u8, -) -> Result<(&[u8], SessionId, &[u8], &[u8], &[u8], Option<&[u8]>), Error> { - let mut p = &incoming_packet[..]; - let offer_id = safe_read_exact(&mut p, 16)?; - - let mut session_id_buf = 0_u64.to_ne_bytes(); - session_id_buf[..SESSION_ID_SIZE].copy_from_slice(safe_read_exact(&mut p, SESSION_ID_SIZE)?); - let alice_session_id = SessionId::new_from_u64_le(u64::from_ne_bytes(session_id_buf)).ok_or(Error::InvalidPacket)?; - - let alice_s_public_blob_len = varint_safe_read(&mut p)?; - let alice_s_public_blob = safe_read_exact(&mut p, alice_s_public_blob_len as usize)?; - - let alice_metadata_len = varint_safe_read(&mut p)?; - let alice_metadata = safe_read_exact(&mut p, alice_metadata_len as usize)?; - - let alice_hk_public_raw = match safe_read_exact(&mut p, 1)?[0] { - HYBRID_KEY_TYPE_KYBER1024 => { - if packet_type == PACKET_TYPE_INITIAL_KEY_OFFER { - safe_read_exact(&mut p, pqc_kyber::KYBER_PUBLICKEYBYTES)? - } else { - safe_read_exact(&mut p, pqc_kyber::KYBER_CIPHERTEXTBYTES)? - } +/// Assemble a series of fragments into a buffer and return the length of the assembled packet in bytes. +/// +/// This is also only used for key exchange and control packets. For data packets decryption and assembly +/// happen in one pass for better performance. +fn assemble_fragments_into(fragments: &[A::IncomingPacketBuffer], d: &mut [u8]) -> Result { + let mut l = 0; + for i in 0..fragments.len() { + let mut ff = fragments[i].as_ref(); + if ff.len() <= MIN_PACKET_SIZE { + return Err(Error::InvalidPacket); } - _ => &[], - }; - - if p.is_empty() { - return Err(Error::InvalidPacket); + if i > 0 { + ff = &ff[HEADER_SIZE..]; + } + let j = l + ff.len(); + if j > d.len() { + return Err(Error::InvalidPacket); + } + d[l..j].copy_from_slice(ff); + l = j; } - let alice_ratchet_key_fingerprint = if safe_read_exact(&mut p, 1)?[0] == 0x01 { - Some(safe_read_exact(&mut p, 16)?) - } else { - None - }; - - Ok(( - offer_id, //always 16 bytes - alice_session_id, - alice_s_public_blob, - alice_metadata, - alice_hk_public_raw, - alice_ratchet_key_fingerprint, //always 16 bytes - )) + return Ok(l); } impl SessionKey { - /// Create a new symmetric shared session key and set its key expiration times, etc. - fn new(key: Secret<64>, role: Role, current_time: i64, current_counter: u64, ratchet_count: u64, confirmed: bool, jedi: bool) -> Self { - let a2b: Secret = kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n_clone(); - let b2a: Secret = kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n_clone(); - let (receive_key, send_key) = match role { - Role::Alice => (b2a, a2b), - Role::Bob => (a2b, b2a), + fn new(key: Secret, current_time: i64, current_counter: u64, role_is_bob: bool) -> Self { + let a2b = kbkdf::(key.as_bytes()); + let b2a = kbkdf::(key.as_bytes()); + let (receive_key, send_key) = if role_is_bob { + (a2b, b2a) + } else { + (b2a, a2b) }; Self { - ratchet_count, - rekey_at_time: current_time - .checked_add(REKEY_AFTER_TIME_MS + ((random::xorshift64_random() as u32) % REKEY_AFTER_TIME_MS_MAX_JITTER) as i64) - .unwrap(), - rekey_at_counter: current_counter.checked_add(REKEY_AFTER_USES).unwrap(), - expire_at_counter: current_counter.checked_add(EXPIRE_AFTER_USES).unwrap(), - secret_fingerprint: public_fingerprint_of_secret(key.as_bytes())[..16].try_into().unwrap(), - ratchet_key: kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_RATCHETING), + ratchet_key: kbkdf::(key.as_bytes()), receive_key, send_key, receive_cipher_pool: Mutex::new(Vec::with_capacity(2)), send_cipher_pool: Mutex::new(Vec::with_capacity(2)), - role, - confirmed, - jedi, + rekey_at_time: current_time + .checked_add( + Application::REKEY_AFTER_TIME_MS + + ((random::xorshift64_random() as u32) % Application::REKEY_AFTER_TIME_MS_MAX_JITTER) as i64, + ) + .unwrap(), + created_at_counter: current_counter, + rekey_at_counter: current_counter.checked_add(Application::REKEY_AFTER_USES).unwrap(), + expire_at_counter: current_counter.checked_add(Application::EXPIRE_AFTER_USES).unwrap(), + bob: role_is_bob, } } @@ -1407,8 +1509,6 @@ impl SessionKey { .pop() .unwrap_or_else(|| Box::new(AesGcm::new(self.send_key.as_bytes(), true)))) } else { - unlikely_branch(); - // Not only do we return an error, but we also destroy the key. let mut scp = self.send_cipher_pool.lock().unwrap(); scp.clear(); @@ -1435,46 +1535,6 @@ impl SessionKey { } } -/// Write src into buffer starting at the index idx. If buffer cannot fit src at that location, nothing at all is written and Error::UnexpectedBufferOverrun is returned. No other errors can be returned by this function. An idx incremented by the amount written is returned. -fn safe_write_all(buffer: &mut [u8], idx: usize, src: &[u8]) -> Result { - let dest = &mut buffer[idx..]; - let amt = src.len(); - if dest.len() >= amt { - dest[..amt].copy_from_slice(src); - Ok(idx + amt) - } else { - unlikely_branch(); - Err(Error::UnexpectedBufferOverrun) - } -} - -/// Write a variable length integer, which can consume up to 10 bytes. Uses safe_write_all to do so. -fn varint_safe_write(buffer: &mut [u8], idx: usize, v: u64) -> Result { - let mut b = [0_u8; varint::VARINT_MAX_SIZE_BYTES]; - let i = varint::encode(&mut b, v); - safe_write_all(buffer, idx, &b[0..i]) -} - -/// Read exactly amt bytes from src and return the slice those bytes reside in. If src is smaller than amt, Error::InvalidPacket is returned. if the read was successful src is incremented to start at the first unread byte. -fn safe_read_exact<'a>(src: &mut &'a [u8], amt: usize) -> Result<&'a [u8], Error> { - if src.len() >= amt { - let (a, b) = src.split_at(amt); - *src = b; - Ok(a) - } else { - unlikely_branch(); - Err(Error::InvalidPacket) - } -} - -/// Read a variable length integer, which can consume up to 10 bytes. Uses varint_safe_read to do so. -fn varint_safe_read(src: &mut &[u8]) -> Result { - let (v, amt) = varint::decode(*src).ok_or(Error::InvalidPacket)?; - let (_, b) = src.split_at(amt); - *src = b; - Ok(v) -} - /// Shortcut to HMAC data split into two slices. fn hmac_sha384_2(key: &[u8], a: &[u8], b: &[u8]) -> [u8; 48] { let mut hmac = HMACSHA384::new(key); @@ -1483,16 +1543,34 @@ fn hmac_sha384_2(key: &[u8], a: &[u8], b: &[u8]) -> [u8; 48] { hmac.finish() } -/// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 12) -/// Cryptographically this isn't meaningfully different from HMAC(key, [label]) but this is how NIST rolls. -fn kbkdf512(key: &[u8], label: u8) -> Secret<64> { - Secret(hmac_sha512(key, &[0, 0, 0, 0, b'Z', b'T', label, 0, 0, 0, 0, 0x02, 0x00])) +/// Shortcut to AES-CTR encrypt or decrypt with a zero IV. +/// +/// This is used during Noise_XK handshaking. Each stage uses a different key to encrypt the +/// payload that is used only once per handshake and per session. +fn aes_ctr_crypt_one_time_use_key(key: &[u8], data: &mut [u8]) { + let mut ctr = AesCtr::new(key); + ctr.reset_set_iv(&[0u8; 12]); + ctr.crypt_in_place(data); } -/// Get a hash of a secret that can be used as a public key fingerprint to check ratcheting during key exchange. -fn public_fingerprint_of_secret(key: &[u8]) -> [u8; 48] { - let mut tmp = SHA384::new(); - tmp.update(&[0xf0, 0x0d]); // arbitrary salt - tmp.update(key); - tmp.finish() +/// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 7) +/// Cryptographically this isn't meaningfully different from HMAC(key, [label]) but this is how NIST rolls. +fn kbkdf(key: &[u8]) -> Secret { + //These are the values we have assigned to the 5 variables involved in https://csrc.nist.gov/publications/detail/sp/800-108/final: + // K_in = key, i = 0x01, Label = 'Z'||'T'||LABEL, Context = 0x00, L = (OUTPUT_BYTES * 8) + Secret::::from_bytes( + &hmac_sha512( + key, + &[ + 1, + b'Z', + b'T', + LABEL, + 0x00, + 0, + (((OUTPUT_BYTES * 8) >> 8) & 0xff) as u8, + ((OUTPUT_BYTES * 8) & 0xff) as u8, + ], + )[..OUTPUT_BYTES], + ) } From aca394946c74c9ee97b4c10a3c2e473cf0d9f1c3 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Tue, 28 Feb 2023 18:00:44 -0500 Subject: [PATCH 03/14] Warning removal. --- zssp/src/zssp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 3142ab72b..20d7d58bd 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -16,7 +16,7 @@ use std::sync::{Arc, Mutex, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesCtr, AesGcm}; use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384, SHA384_HASH_SIZE}; -use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE, P384_PUBLIC_KEY_SIZE}; +use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE}; use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; From 97fc6b8ff256cddc2064d9d67db362b9b087092a Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Tue, 28 Feb 2023 18:01:26 -0500 Subject: [PATCH 04/14] remove old docs --- zssp/README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/zssp/README.md b/zssp/README.md index 437f92059..a533cd104 100644 --- a/zssp/README.md +++ b/zssp/README.md @@ -1,8 +1,2 @@ ZeroTier Secure Socket Protocol ====== - -**NOTE: this protocol and code have not yet been formally audited and should not be used in anything production.** - -ZSSP (ZeroTier Secure Socket Protocol) is an implementation of the Noise_IK pattern using FIPS/NIST compliant primitives. After Noise_IK negotiation is complete ZSSP also adds key ratcheting and optional (enabled by default) support for quantum data forward secrecy with Kyber1024. - -It's general purpose and could be used with any system but contains a few specific design choices to make it optimal for ZeroTier and easy to distinguish from legacy ZeroTier V1 traffic for backward compatibility. From 8b6088f335b9fdd7ec766b0d766bbc9198d2c8b2 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Tue, 28 Feb 2023 18:06:47 -0500 Subject: [PATCH 05/14] Remove old tests from ZSSP, new test in main() can also be made into a unit test in the future. --- zssp/src/lib.rs | 1 - zssp/src/tests.rs | 226 ---------------------------------------------- 2 files changed, 227 deletions(-) delete mode 100644 zssp/src/tests.rs diff --git a/zssp/src/lib.rs b/zssp/src/lib.rs index 256a6909a..718e5adb4 100644 --- a/zssp/src/lib.rs +++ b/zssp/src/lib.rs @@ -10,7 +10,6 @@ mod applicationlayer; mod error; mod proto; mod sessionid; -mod tests; mod zssp; pub use crate::applicationlayer::ApplicationLayer; diff --git a/zssp/src/tests.rs b/zssp/src/tests.rs deleted file mode 100644 index fd06c4e38..000000000 --- a/zssp/src/tests.rs +++ /dev/null @@ -1,226 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - -/* -#[allow(unused_imports)] -#[cfg(test)] -mod tests { - use std::collections::LinkedList; - use std::sync::{Arc, Mutex}; - use zerotier_crypto::hash::SHA384; - use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; - use zerotier_crypto::random; - use zerotier_crypto::secret::Secret; - use zerotier_utils::hex; - - use crate::*; - use constants::*; - - struct TestHost { - local_s: P384KeyPair, - local_s_hash: [u8; 48], - psk: Secret<64>, - session: Mutex>>>>, - session_id_counter: Mutex, - queue: Mutex>>, - key_id: Mutex<[u8; 16]>, - this_name: &'static str, - other_name: &'static str, - } - - impl TestHost { - fn new(psk: Secret<64>, this_name: &'static str, other_name: &'static str) -> Self { - let local_s = P384KeyPair::generate(); - let local_s_hash = SHA384::hash(local_s.public_key_bytes()); - Self { - local_s, - local_s_hash, - psk, - session: Mutex::new(None), - session_id_counter: Mutex::new(1), - queue: Mutex::new(LinkedList::new()), - key_id: Mutex::new([0; 16]), - this_name, - other_name, - } - } - } - - impl ApplicationLayer for Box { - type Data = u32; - type SessionRef<'a> = Arc>>; - type IncomingPacketBuffer = Vec; - type RemoteAddress = u32; - - const REKEY_RATE_LIMIT_MS: i64 = 0; - - fn get_local_s_public_blob(&self) -> &[u8] { - self.local_s.public_key_bytes() - } - - fn get_local_s_public_blob_hash(&self) -> &[u8; 48] { - &self.local_s_hash - } - - fn get_local_s_keypair(&self) -> &P384KeyPair { - &self.local_s - } - - fn extract_s_public_from_raw(static_public: &[u8]) -> Option { - P384PublicKey::from_bytes(static_public) - } - - fn lookup_session<'a>(&self, local_session_id: SessionId) -> Option> { - self.session.lock().unwrap().as_ref().and_then(|s| { - if s.id == local_session_id { - Some(s.clone()) - } else { - None - } - }) - } - - fn check_new_session(&self, _: &ReceiveContext, _: &Self::RemoteAddress) -> bool { - true - } - - fn accept_new_session(&self, _: &ReceiveContext, _: &u32, _: &[u8], _: &[u8]) -> Option<(SessionId, Secret<64>, Self::Data)> { - loop { - let mut new_id = self.session_id_counter.lock().unwrap(); - *new_id += 1; - return Some((SessionId::new_from_u64_le((*new_id).to_le()).unwrap(), self.psk.clone(), 0)); - } - } - } - - #[allow(unused_variables)] - #[test] - fn establish_session() { - let mut data_buf = [0_u8; (1280 - 32) * MAX_FRAGMENTS]; - let mut mtu_buffer = [0_u8; 1280]; - let mut psk: Secret<64> = Secret::default(); - random::fill_bytes_secure(&mut psk.0); - - let alice_host = Box::new(TestHost::new(psk.clone(), "alice", "bob")); - let bob_host = Box::new(TestHost::new(psk.clone(), "bob", "alice")); - let alice_rc: Box>> = Box::new(ReceiveContext::new(&alice_host)); - let bob_rc: Box>> = Box::new(ReceiveContext::new(&bob_host)); - - //println!("zssp: size of session (bytes): {}", std::mem::size_of::>>()); - - let _ = alice_host.session.lock().unwrap().insert(Arc::new( - Session::start_new( - &alice_host, - |data| bob_host.queue.lock().unwrap().push_front(data.to_vec()), - SessionId::random(), - bob_host.local_s.public_key_bytes(), - &[], - &psk, - 1, - mtu_buffer.len(), - 1, - ) - .unwrap(), - )); - - for test_loop in 0..256 { - let time_ticks = (test_loop * 10000) as i64; - for host in [&alice_host, &bob_host] { - let send_to_other = |data: &mut [u8]| { - if std::ptr::eq(host, &alice_host) { - bob_host.queue.lock().unwrap().push_front(data.to_vec()); - } else { - alice_host.queue.lock().unwrap().push_front(data.to_vec()); - } - }; - - let rc = if std::ptr::eq(host, &alice_host) { - &alice_rc - } else { - &bob_rc - }; - - loop { - if let Some(qi) = host.queue.lock().unwrap().pop_back() { - let qi_len = qi.len(); - let r = rc.receive(host, &0, send_to_other, &mut data_buf, qi, mtu_buffer.len(), time_ticks); - if r.is_ok() { - let r = r.unwrap(); - match r { - ReceiveResult::Ok => { - //println!("zssp: {} => {} ({}): Ok", host.other_name, host.this_name, qi_len); - } - ReceiveResult::OkData(data) => { - //println!("zssp: {} => {} ({}): OkData length=={}", host.other_name, host.this_name, qi_len, data.len()); - assert!(!data.iter().any(|x| *x != 0x12)); - } - ReceiveResult::OkNewSession(new_session) => { - println!("zssp: new session at {} ({})", host.this_name, u64::from(new_session.id)); - let mut hs = host.session.lock().unwrap(); - assert!(hs.is_none()); - let _ = hs.insert(Arc::new(new_session)); - } - ReceiveResult::Ignored => { - println!("zssp: {} => {} ({}): Ignored", host.other_name, host.this_name, qi_len); - } - } - } else { - println!( - "zssp: {} => {} ({}): error: {}", - host.other_name, - host.this_name, - qi_len, - r.err().unwrap().to_string() - ); - panic!(); - } - } else { - break; - } - } - - data_buf.fill(0x12); - if let Some(session) = host.session.lock().unwrap().as_ref().cloned() { - if session.established() { - { - let mut key_id = host.key_id.lock().unwrap(); - let security_info = session.status().unwrap(); - if !security_info.0.eq(key_id.as_ref()) { - *key_id = security_info.0; - println!( - "zssp: new key at {}: fingerprint {} ratchet {} kyber {} latest role {}", - host.this_name, - hex::to_string(key_id.as_ref()), - security_info.1, - security_info.3, - match security_info.2 { - Role::Alice => "A", - Role::Bob => "B", - } - ); - } - } - for _ in 0..4 { - assert!(session - .send( - send_to_other, - &mut mtu_buffer, - &data_buf[..((random::xorshift64_random() as usize) % data_buf.len())] - ) - .is_ok()); - } - if (test_loop % 13) == 0 && test_loop > 0 { - session.service(host, send_to_other, &[], mtu_buffer.len(), time_ticks, true); - } - } - } - } - } - } -} -*/ From 652f7360f0f3e344a1a071a95316c5a50f667b54 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 1 Mar 2023 09:22:10 -0500 Subject: [PATCH 06/14] Add key_info() to get key information. --- zssp/src/main.rs | 7 +++++++ zssp/src/zssp.rs | 45 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/zssp/src/main.rs b/zssp/src/main.rs index f22f9c3a1..51a98e820 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -14,6 +14,13 @@ struct TestApplication { } impl zssp::ApplicationLayer for TestApplication { + const REKEY_AFTER_USES: u64 = 131072; + const EXPIRE_AFTER_USES: u64 = 2147483648; + const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60 * 2; + const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; + const INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS: i64 = 2000; + const RETRY_INTERVAL: i64 = 500; + type Data = (); type IncomingPacketBuffer = Vec; diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 20d7d58bd..4419bd0bc 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -133,6 +133,7 @@ struct SessionKey { created_at_counter: u64, // Counter at which session was created rekey_at_counter: u64, // Rekey at or after this counter expire_at_counter: u64, // Hard error when this counter value is reached or exceeded + ratchet_count: u64, // Number of rekey events bob: bool, // Was this side "Bob" in this exchange? } @@ -926,8 +927,13 @@ impl Context { { let mut state = session.state.write().unwrap(); let _ = state.remote_session_id.insert(bob_session_id); - let _ = - state.keys[0].insert(SessionKey::new::(noise_es_ee_se_hk_psk, current_time, 2, false)); + let _ = state.keys[0].insert(SessionKey::new::( + noise_es_ee_se_hk_psk, + 1, + current_time, + 2, + false, + )); state.current_key = 0; state.current_offer = Offer::None; } @@ -1078,7 +1084,7 @@ impl Context { state: RwLock::new(State { remote_session_id: Some(incoming.alice_session_id), keys: [ - Some(SessionKey::new::(noise_es_ee_se_hk_psk, current_time, 2, true)), + Some(SessionKey::new::(noise_es_ee_se_hk_psk, 1, current_time, 2, true)), None, ], current_key: 0, @@ -1111,7 +1117,8 @@ impl Context { if let Some(session) = session { let state = session.state.read().unwrap(); if let Some(key) = state.keys[key_index].as_ref() { - // Only the current "Alice" accepts rekeys initiated by the current "Bob." + // Only the current "Alice" accepts rekeys initiated by the current "Bob." These roles + // flip with each rekey event. if !key.bob { let mut c = key.get_receive_cipher(); c.reset_init_gcm(&incoming_message_nonce); @@ -1142,10 +1149,16 @@ impl Context { send(Some(&session), &mut reply_buf); + // The new "Bob" doesn't know yet if Alice has received the new key, so the + // new key is recorded as the "alt" (key_index ^ 1) but the current key is + // not advanced yet. This happens automatically the first time we receive a + // valid packet with the new key. + let next_ratchet_count = key.ratchet_count + 1; drop(state); let mut state = session.state.write().unwrap(); let _ = state.keys[key_index ^ 1].replace(SessionKey::new::( next_session_key, + next_ratchet_count, current_time, counter.get(), false, @@ -1192,11 +1205,16 @@ impl Context { )); if secure_eq(&pkt.next_key_fingerprint, &SHA384::hash(next_session_key.as_bytes())) { + // The new "Alice" knows Bob has the key since this is an ACK, so she can go + // ahead and set current_key to the new key. Then when she sends something + // to Bob the other side will automatically advance to the new key as well. + let next_ratchet_count = key.ratchet_count + 1; drop(state); let next_key_index = key_index ^ 1; let mut state = session.state.write().unwrap(); let _ = state.keys[next_key_index].replace(SessionKey::new::( next_session_key, + next_ratchet_count, current_time, session.send_counter.load(Ordering::Acquire), true, @@ -1297,6 +1315,16 @@ impl Session { state.keys[state.current_key].is_some() } + /// Get the ratchet count and a hash fingerprint of the current active key. + pub fn key_info(&self) -> Option<(u64, [u8; 48])> { + let state = self.state.read().unwrap(); + if let Some(key) = state.keys[state.current_key].as_ref() { + Some((key.ratchet_count, SHA384::hash(key.ratchet_key.as_bytes()))) + } else { + None + } + } + /// Send a rekey init message. /// /// This is called from the session context's service() method when it's time to rekey. @@ -1473,7 +1501,13 @@ fn assemble_fragments_into(fragments: &[A::IncomingPacketBu } impl SessionKey { - fn new(key: Secret, current_time: i64, current_counter: u64, role_is_bob: bool) -> Self { + fn new( + key: Secret, + ratchet_count: u64, + current_time: i64, + current_counter: u64, + role_is_bob: bool, + ) -> Self { let a2b = kbkdf::(key.as_bytes()); let b2a = kbkdf::(key.as_bytes()); let (receive_key, send_key) = if role_is_bob { @@ -1496,6 +1530,7 @@ impl SessionKey { created_at_counter: current_counter, rekey_at_counter: current_counter.checked_add(Application::REKEY_AFTER_USES).unwrap(), expire_at_counter: current_counter.checked_add(Application::EXPIRE_AFTER_USES).unwrap(), + ratchet_count, bob: role_is_bob, } } From 8b6c9051fbfb339b2c1ec85f2d738d8957e5b6fd Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 1 Mar 2023 14:01:30 -0500 Subject: [PATCH 07/14] Rekeying is now tested and works. --- zssp/src/applicationlayer.rs | 2 +- zssp/src/main.rs | 102 +++++++++++++++---------- zssp/src/proto.rs | 22 +++--- zssp/src/zssp.rs | 139 ++++++++++++++++++++--------------- 4 files changed, 158 insertions(+), 107 deletions(-) diff --git a/zssp/src/applicationlayer.rs b/zssp/src/applicationlayer.rs index 145510dcc..b9a1f1723 100644 --- a/zssp/src/applicationlayer.rs +++ b/zssp/src/applicationlayer.rs @@ -31,7 +31,7 @@ pub trait ApplicationLayer: Sized { /// Attempting to encrypt more than this many messages with a key will cause a hard error /// and the internal erasure of ephemeral key material. You'll only ever hit this if something /// goes wrong and rekeying fails. - const EXPIRE_AFTER_USES: u64 = 2147483648; + const EXPIRE_AFTER_USES: u64 = 2147483647; /// Start attempting to rekey after a key has been in use for this many milliseconds. /// diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 51a98e820..26acbce93 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -14,7 +14,7 @@ struct TestApplication { } impl zssp::ApplicationLayer for TestApplication { - const REKEY_AFTER_USES: u64 = 131072; + const REKEY_AFTER_USES: u64 = 100000; const EXPIRE_AFTER_USES: u64 = 2147483648; const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60 * 2; const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; @@ -44,6 +44,9 @@ fn alice_main( let context = zssp::Context::::new(16); let mut data_buf = [0u8; 65536]; let mut next_service = ms_monotonic() + 500; + let mut last_ratchet_count = 0; + let test_data = [1u8; 10000]; + let mut up = false; let alice_session = context .open( @@ -62,51 +65,57 @@ fn alice_main( println!("[alice] opening session {}", alice_session.id.to_string()); - let test_data = [1u8; 10000]; - let mut up = false; - while run.load(Ordering::Relaxed) { - let pkt = alice_in.try_recv(); let current_time = ms_monotonic(); - - if let Ok(pkt) = pkt { - //println!("bob >> alice {}", pkt.len()); - match context.receive( - alice_app, - || true, - |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), - |_, b| { - let _ = alice_out.send(b.to_vec()); - }, - &mut data_buf, - pkt, - TEST_MTU, - current_time, - ) { - Ok(zssp::ReceiveResult::Ok) => { - //println!("[alice] ok"); - } - Ok(zssp::ReceiveResult::OkData(_, _)) => { - //println!("[alice] received {}", data.len()); - } - Ok(zssp::ReceiveResult::OkNewSession(s)) => { - println!("[alice] new session {}", s.id.to_string()); - } - Ok(zssp::ReceiveResult::Rejected) => {} - Err(e) => { - println!("[alice] ERROR {}", e.to_string()); + loop { + let pkt = alice_in.try_recv(); + if let Ok(pkt) = pkt { + //println!("bob >> alice {}", pkt.len()); + match context.receive( + alice_app, + || true, + |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), + |_, b| { + let _ = alice_out.send(b.to_vec()); + }, + &mut data_buf, + pkt, + TEST_MTU, + current_time, + ) { + Ok(zssp::ReceiveResult::Ok) => { + //println!("[alice] ok"); + } + Ok(zssp::ReceiveResult::OkData(_, data)) => { + //println!("[alice] received {}", data.len()); + } + Ok(zssp::ReceiveResult::OkNewSession(s)) => { + println!("[alice] new session {}", s.id.to_string()); + } + Ok(zssp::ReceiveResult::Rejected) => {} + Err(e) => { + println!("[alice] ERROR {}", e.to_string()); + } } + } else { + break; } } if up { + let ratchet_count = alice_session.key_info().unwrap().0; + if ratchet_count > last_ratchet_count { + last_ratchet_count = ratchet_count; + println!("[alice] new key! ratchet count {}", ratchet_count); + } + assert!(alice_session .send( |b| { let _ = alice_out.send(b.to_vec()); }, &mut data_buf[..TEST_MTU], - &test_data[..2048 + ((zerotier_crypto::random::xorshift64_random() as usize) % (test_data.len() - 2048))], + &test_data[..1000 + ((zerotier_crypto::random::xorshift64_random() as usize) % (test_data.len() - 1000))], ) .is_ok()); } else { @@ -137,6 +146,8 @@ fn bob_main( ) { let context = zssp::Context::::new(16); let mut data_buf = [0u8; 65536]; + let mut data_buf_2 = [0u8; TEST_MTU]; + let mut last_ratchet_count = 0; let mut last_speed_metric = ms_monotonic(); let mut next_service = last_speed_metric + 500; let mut transferred = 0u64; @@ -144,7 +155,7 @@ fn bob_main( let mut bob_session = None; while run.load(Ordering::Relaxed) { - let pkt = bob_in.recv_timeout(Duration::from_millis(10)); + let pkt = bob_in.recv_timeout(Duration::from_millis(100)); let current_time = ms_monotonic(); if let Ok(pkt) = pkt { @@ -164,9 +175,18 @@ fn bob_main( Ok(zssp::ReceiveResult::Ok) => { //println!("[bob] ok"); } - Ok(zssp::ReceiveResult::OkData(_, data)) => { + Ok(zssp::ReceiveResult::OkData(s, data)) => { //println!("[bob] received {}", data.len()); transferred += data.len() as u64; + assert!(s + .send( + |b| { + let _ = bob_out.send(b.to_vec()); + }, + &mut data_buf_2, + data.as_mut(), + ) + .is_ok()); } Ok(zssp::ReceiveResult::OkNewSession(s)) => { println!("[bob] new session {}", s.id.to_string()); @@ -179,6 +199,14 @@ fn bob_main( } } + if let Some(bob_session) = bob_session.as_ref() { + let ratchet_count = bob_session.key_info().unwrap().0; + if ratchet_count > last_ratchet_count { + last_ratchet_count = ratchet_count; + println!("[bob] new key! ratchet count {}", ratchet_count); + } + } + let speed_metric_elapsed = current_time - last_speed_metric; if speed_metric_elapsed >= 1000 { last_speed_metric = current_time; @@ -208,8 +236,8 @@ fn main() { let alice_app = TestApplication { identity_key: P384KeyPair::generate() }; let bob_app = TestApplication { identity_key: P384KeyPair::generate() }; - let (alice_out, bob_in) = mpsc::sync_channel::>(128); - let (bob_out, alice_in) = mpsc::sync_channel::>(128); + let (alice_out, bob_in) = mpsc::sync_channel::>(1024); + let (bob_out, alice_in) = mpsc::sync_channel::>(1024); thread::scope(|ts| { let alice_thread = ts.spawn(|| alice_main(&run, &alice_app, &bob_app, alice_out, alice_in)); diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index 210107efd..32510bd44 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -33,8 +33,8 @@ pub(crate) const PACKET_TYPE_DATA: u8 = 0; pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_INIT: u8 = 1; pub(crate) const PACKET_TYPE_BOB_NOISE_XK_ACK: u8 = 2; pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_ACK: u8 = 3; -pub(crate) const PACKET_TYPE_ALICE_REKEY_INIT: u8 = 4; -pub(crate) const PACKET_TYPE_BOB_REKEY_ACK: u8 = 5; +pub(crate) const PACKET_TYPE_REKEY_INIT: u8 = 4; +pub(crate) const PACKET_TYPE_REKEY_ACK: u8 = 5; pub(crate) const HEADER_SIZE: usize = 16; pub(crate) const HEADER_PROTECT_ENCRYPT_START: usize = 6; @@ -123,24 +123,26 @@ pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = ALICE_NOISE_XK_ACK_ENC_STA #[allow(unused)] #[repr(C, packed)] -pub(crate) struct AliceRekeyInit { +pub(crate) struct RekeyInit { pub header: [u8; HEADER_SIZE], + pub session_protocol_version: u8, // -- start AES-GCM encrypted portion (using current key) pub alice_e: [u8; P384_PUBLIC_KEY_SIZE], // -- end AES-GCM encrypted portion pub gcm_mac: [u8; AES_GCM_TAG_SIZE], } -impl AliceRekeyInit { - pub const ENC_START: usize = HEADER_SIZE; +impl RekeyInit { + pub const ENC_START: usize = HEADER_SIZE + 1; pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE; pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; } #[allow(unused)] #[repr(C, packed)] -pub(crate) struct BobRekeyAck { +pub(crate) struct RekeyAck { pub header: [u8; HEADER_SIZE], + pub session_protocol_version: u8, // -- start AES-GCM encrypted portion (using current key) pub bob_e: [u8; P384_PUBLIC_KEY_SIZE], pub next_key_fingerprint: [u8; SHA384_HASH_SIZE], @@ -148,8 +150,8 @@ pub(crate) struct BobRekeyAck { pub gcm_mac: [u8; AES_GCM_TAG_SIZE], } -impl BobRekeyAck { - pub const ENC_START: usize = HEADER_SIZE; +impl RekeyAck { + pub const ENC_START: usize = HEADER_SIZE + 1; pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE + SHA384_HASH_SIZE; pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; } @@ -161,8 +163,8 @@ pub(crate) trait ProtocolFlatBuffer {} impl ProtocolFlatBuffer for AliceNoiseXKInit {} impl ProtocolFlatBuffer for BobNoiseXKAck {} //impl ProtocolFlatBuffer for NoiseXKAliceStaticAck {} -impl ProtocolFlatBuffer for AliceRekeyInit {} -impl ProtocolFlatBuffer for BobRekeyAck {} +impl ProtocolFlatBuffer for RekeyInit {} +impl ProtocolFlatBuffer for RekeyAck {} #[derive(Clone, Copy)] #[repr(C, packed)] diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 4419bd0bc..76e07b830 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -119,7 +119,7 @@ struct OutgoingSessionInit { enum Offer { None, NoiseXKInit(Box), - RekeyInit(P384KeyPair, [u8; AliceRekeyInit::SIZE], AtomicI64), + RekeyInit(P384KeyPair, [u8; RekeyInit::SIZE], AtomicI64), } /// An ephemeral session key with expiration info. @@ -181,6 +181,7 @@ impl Context { && (current_time >= key.rekey_at_time || session.send_counter.load(Ordering::Relaxed) >= key.rekey_at_counter) { + drop(state); session.initiate_rekey(|b| send(&session, b), current_time); } } @@ -1106,8 +1107,8 @@ impl Context { } } - PACKET_TYPE_ALICE_REKEY_INIT => { - if pkt_assembled.len() != AliceRekeyInit::SIZE { + PACKET_TYPE_REKEY_INIT => { + if pkt_assembled.len() != RekeyInit::SIZE { return Err(Error::InvalidPacket); } if incoming.is_some() { @@ -1116,58 +1117,74 @@ impl Context { if let Some(session) = session { let state = session.state.read().unwrap(); - if let Some(key) = state.keys[key_index].as_ref() { - // Only the current "Alice" accepts rekeys initiated by the current "Bob." These roles - // flip with each rekey event. - if !key.bob { - let mut c = key.get_receive_cipher(); - c.reset_init_gcm(&incoming_message_nonce); - c.crypt_in_place(&mut pkt_assembled[AliceRekeyInit::ENC_START..AliceRekeyInit::AUTH_START]); - let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[AliceRekeyInit::AUTH_START..]); - key.return_receive_cipher(c); + if let Some(remote_session_id) = state.remote_session_id { + if let Some(key) = state.keys[key_index].as_ref() { + // Only the current "Alice" accepts rekeys initiated by the current "Bob." These roles + // flip with each rekey event. + if !key.bob { + let mut c = key.get_receive_cipher(); + c.reset_init_gcm(&incoming_message_nonce); + c.crypt_in_place(&mut pkt_assembled[RekeyInit::ENC_START..RekeyInit::AUTH_START]); + let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[RekeyInit::AUTH_START..]); + key.return_receive_cipher(c); - if aead_authentication_ok { - let pkt: &AliceRekeyInit = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); - if let Some(alice_e) = P384PublicKey::from_bytes(&pkt.alice_e) { - let bob_e_secret = P384KeyPair::generate(); - let next_session_key = Secret(hmac_sha512( - key.ratchet_key.as_bytes(), - bob_e_secret.agree(&alice_e).ok_or(Error::FailedAuthentication)?.as_bytes(), - )); + if aead_authentication_ok { + let pkt: &RekeyInit = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); + if let Some(alice_e) = P384PublicKey::from_bytes(&pkt.alice_e) { + let bob_e_secret = P384KeyPair::generate(); + let next_session_key = Secret(hmac_sha512( + key.ratchet_key.as_bytes(), + bob_e_secret.agree(&alice_e).ok_or(Error::FailedAuthentication)?.as_bytes(), + )); - let mut reply_buf = [0u8; BobRekeyAck::SIZE]; - let reply: &mut BobRekeyAck = byte_array_as_proto_buffer_mut(&mut reply_buf).unwrap(); - reply.bob_e = *bob_e_secret.public_key_bytes(); - reply.next_key_fingerprint = SHA384::hash(next_session_key.as_bytes()); + let mut reply_buf = [0u8; RekeyAck::SIZE]; + let reply: &mut RekeyAck = byte_array_as_proto_buffer_mut(&mut reply_buf).unwrap(); + reply.session_protocol_version = SESSION_PROTOCOL_VERSION; + reply.bob_e = *bob_e_secret.public_key_bytes(); + reply.next_key_fingerprint = SHA384::hash(next_session_key.as_bytes()); - let counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; - let mut c = key.get_send_cipher(counter.get())?; - c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_BOB_REKEY_ACK, counter.get())); - c.crypt_in_place(&mut reply_buf[BobRekeyAck::ENC_START..BobRekeyAck::AUTH_START]); - reply_buf[BobRekeyAck::AUTH_START..].copy_from_slice(&c.finish_encrypt()); - key.return_send_cipher(c); + let counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?.get(); + set_packet_header( + &mut reply_buf, + 1, + 0, + PACKET_TYPE_REKEY_ACK, + u64::from(remote_session_id), + state.current_key, + counter, + ); - send(Some(&session), &mut reply_buf); + let mut c = key.get_send_cipher(counter)?; + c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_REKEY_ACK, counter)); + c.crypt_in_place(&mut reply_buf[RekeyAck::ENC_START..RekeyAck::AUTH_START]); + reply_buf[RekeyAck::AUTH_START..].copy_from_slice(&c.finish_encrypt()); + key.return_send_cipher(c); - // The new "Bob" doesn't know yet if Alice has received the new key, so the - // new key is recorded as the "alt" (key_index ^ 1) but the current key is - // not advanced yet. This happens automatically the first time we receive a - // valid packet with the new key. - let next_ratchet_count = key.ratchet_count + 1; - drop(state); - let mut state = session.state.write().unwrap(); - let _ = state.keys[key_index ^ 1].replace(SessionKey::new::( - next_session_key, - next_ratchet_count, - current_time, - counter.get(), - false, - )); + session.header_protection_cipher.encrypt_block_in_place( + &mut reply_buf[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END], + ); + send(Some(&session), &mut reply_buf); - return Ok(ReceiveResult::Ok); + // The new "Bob" doesn't know yet if Alice has received the new key, so the + // new key is recorded as the "alt" (key_index ^ 1) but the current key is + // not advanced yet. This happens automatically the first time we receive a + // valid packet with the new key. + let next_ratchet_count = key.ratchet_count + 1; + drop(state); + let mut state = session.state.write().unwrap(); + let _ = state.keys[key_index ^ 1].replace(SessionKey::new::( + next_session_key, + next_ratchet_count, + current_time, + counter, + false, + )); + + return Ok(ReceiveResult::Ok); + } } + return Err(Error::FailedAuthentication); } - return Err(Error::FailedAuthentication); } } return Err(Error::OutOfSequence); @@ -1176,8 +1193,8 @@ impl Context { } } - PACKET_TYPE_BOB_REKEY_ACK => { - if pkt_assembled.len() != BobRekeyAck::SIZE { + PACKET_TYPE_REKEY_ACK => { + if pkt_assembled.len() != RekeyAck::SIZE { return Err(Error::InvalidPacket); } if incoming.is_some() { @@ -1192,12 +1209,12 @@ impl Context { if key.bob { let mut c = key.get_receive_cipher(); c.reset_init_gcm(&incoming_message_nonce); - c.crypt_in_place(&mut pkt_assembled[BobRekeyAck::ENC_START..BobRekeyAck::AUTH_START]); - let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[BobRekeyAck::AUTH_START..]); + c.crypt_in_place(&mut pkt_assembled[RekeyAck::ENC_START..RekeyAck::AUTH_START]); + let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[RekeyAck::AUTH_START..]); key.return_receive_cipher(c); if aead_authentication_ok { - let pkt: &BobRekeyAck = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); + let pkt: &RekeyAck = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); if let Some(bob_e) = P384PublicKey::from_bytes(&pkt.bob_e) { let next_session_key = Secret(hmac_sha512( key.ratchet_key.as_bytes(), @@ -1333,8 +1350,9 @@ impl Session { fn initiate_rekey(&self, mut send: SendFunction, current_time: i64) { let rekey_e = P384KeyPair::generate(); - let mut rekey_buf = [0u8; AliceRekeyInit::SIZE]; - let pkt: &mut AliceRekeyInit = byte_array_as_proto_buffer_mut(&mut rekey_buf).unwrap(); + let mut rekey_buf = [0u8; RekeyInit::SIZE]; + let pkt: &mut RekeyInit = byte_array_as_proto_buffer_mut(&mut rekey_buf).unwrap(); + pkt.session_protocol_version = SESSION_PROTOCOL_VERSION; pkt.alice_e = *rekey_e.public_key_bytes(); let state = self.state.read().unwrap(); @@ -1342,9 +1360,9 @@ impl Session { if let Some(key) = state.keys[state.current_key].as_ref() { if let Some(counter) = self.get_next_outgoing_counter() { if let Ok(mut gcm) = key.get_send_cipher(counter.get()) { - gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_ALICE_REKEY_INIT, counter.get())); - gcm.crypt_in_place(&mut rekey_buf[AliceRekeyInit::ENC_START..AliceRekeyInit::AUTH_START]); - rekey_buf[AliceRekeyInit::AUTH_START..].copy_from_slice(&gcm.finish_encrypt()); + gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_REKEY_INIT, counter.get())); + gcm.crypt_in_place(&mut rekey_buf[RekeyInit::ENC_START..RekeyInit::AUTH_START]); + rekey_buf[RekeyInit::AUTH_START..].copy_from_slice(&gcm.finish_encrypt()); key.return_send_cipher(gcm); debug_assert!(rekey_buf.len() <= MIN_TRANSPORT_MTU); @@ -1352,15 +1370,18 @@ impl Session { &mut rekey_buf, 1, 0, - PACKET_TYPE_ALICE_REKEY_INIT, + PACKET_TYPE_REKEY_INIT, u64::from(remote_session_id), state.current_key, counter.get(), ); + drop(state); + + self.header_protection_cipher + .encrypt_block_in_place(&mut rekey_buf[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); send(&mut rekey_buf); - drop(state); self.state.write().unwrap().current_offer = Offer::RekeyInit(rekey_e, rekey_buf, AtomicI64::new(current_time)); } } From d00cf1d53456c9e0447694dfc457e15b68f71945 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 1 Mar 2023 14:32:20 -0500 Subject: [PATCH 08/14] Show key fingerprint. --- zssp/src/main.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 26acbce93..a44936a09 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -5,6 +5,7 @@ use std::time::Duration; use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; use zerotier_crypto::secret::Secret; +use zerotier_utils::hex; use zerotier_utils::ms_monotonic; const TEST_MTU: usize = 1500; @@ -14,7 +15,7 @@ struct TestApplication { } impl zssp::ApplicationLayer for TestApplication { - const REKEY_AFTER_USES: u64 = 100000; + const REKEY_AFTER_USES: u64 = 350000; const EXPIRE_AFTER_USES: u64 = 2147483648; const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60 * 2; const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; @@ -103,10 +104,10 @@ fn alice_main( } if up { - let ratchet_count = alice_session.key_info().unwrap().0; - if ratchet_count > last_ratchet_count { - last_ratchet_count = ratchet_count; - println!("[alice] new key! ratchet count {}", ratchet_count); + let ki = alice_session.key_info().unwrap(); + if ki.0 > last_ratchet_count { + last_ratchet_count = ki.0; + println!("[alice] new key! ratchet count {} fp {}", ki.0, hex::to_string(&ki.1[..16])); } assert!(alice_session @@ -115,7 +116,7 @@ fn alice_main( let _ = alice_out.send(b.to_vec()); }, &mut data_buf[..TEST_MTU], - &test_data[..1000 + ((zerotier_crypto::random::xorshift64_random() as usize) % (test_data.len() - 1000))], + &test_data[..1400 + ((zerotier_crypto::random::xorshift64_random() as usize) % (test_data.len() - 1400))], ) .is_ok()); } else { @@ -177,7 +178,6 @@ fn bob_main( } Ok(zssp::ReceiveResult::OkData(s, data)) => { //println!("[bob] received {}", data.len()); - transferred += data.len() as u64; assert!(s .send( |b| { @@ -187,6 +187,7 @@ fn bob_main( data.as_mut(), ) .is_ok()); + transferred += data.len() as u64 * 2; // *2 because we are also sending this many bytes back } Ok(zssp::ReceiveResult::OkNewSession(s)) => { println!("[bob] new session {}", s.id.to_string()); @@ -200,10 +201,10 @@ fn bob_main( } if let Some(bob_session) = bob_session.as_ref() { - let ratchet_count = bob_session.key_info().unwrap().0; - if ratchet_count > last_ratchet_count { - last_ratchet_count = ratchet_count; - println!("[bob] new key! ratchet count {}", ratchet_count); + let ki = bob_session.key_info().unwrap(); + if ki.0 > last_ratchet_count { + last_ratchet_count = ki.0; + println!("[bob] new key! ratchet count {} fp {}", ki.0, hex::to_string(&ki.1[..16])); } } @@ -211,7 +212,7 @@ fn bob_main( if speed_metric_elapsed >= 1000 { last_speed_metric = current_time; println!( - "[bob] RX speed {} MiB/sec", + "[bob] throughput: {} MiB/sec (combined input and output)", ((transferred as f64) / 1048576.0) / ((speed_metric_elapsed as f64) / 1000.0) ); transferred = 0; From 87989ac008baa967dafd4d61456aebdd060b4ee5 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 1 Mar 2023 16:51:03 -0500 Subject: [PATCH 09/14] Factor out memory:: stuff, does not appear to have any real performance benefit. --- zssp/src/main.rs | 2 +- zssp/src/zssp.rs | 29 +++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/zssp/src/main.rs b/zssp/src/main.rs index a44936a09..0567541db 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -87,7 +87,7 @@ fn alice_main( Ok(zssp::ReceiveResult::Ok) => { //println!("[alice] ok"); } - Ok(zssp::ReceiveResult::OkData(_, data)) => { + Ok(zssp::ReceiveResult::OkData(_, _)) => { //println!("[alice] received {}", data.len()); } Ok(zssp::ReceiveResult::OkNewSession(s)) => { diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 76e07b830..423d48828 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -22,7 +22,6 @@ use zerotier_crypto::{random, secure_eq}; use zerotier_utils::arrayvec::ArrayVec; use zerotier_utils::gatherarray::GatherArray; -use zerotier_utils::memory; use zerotier_utils::ringbuffermap::RingBufferMap; use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_SECRETKEYBYTES, KYBER_SSBYTES}; @@ -397,7 +396,7 @@ impl Context { } let mut incoming = None; - if let Some(local_session_id) = SessionId::new_from_u64_le(memory::load_raw(incoming_packet)) { + if let Some(local_session_id) = SessionId::new_from_u64_le(u64::from_le_bytes(incoming_packet[0..8].try_into().unwrap())) { if let Some(session) = self .sessions .read() @@ -1018,7 +1017,7 @@ impl Context { return Err(Error::InvalidPacket); } let alice_static_public_blob_size = - u16::from_le(memory::load_raw::(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end])) as usize; + u16::from_le_bytes(pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end].try_into().unwrap()) as usize; pkt_assembled_ptr = pkt_assembled_field_end; pkt_assembled_field_end = pkt_assembled_ptr + alice_static_public_blob_size; if pkt_assembled_field_end >= pkt_assembled.len() { @@ -1031,7 +1030,7 @@ impl Context { return Err(Error::InvalidPacket); } let alice_meta_data_size = - u16::from_le(memory::load_raw::(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end])) as usize; + u16::from_le_bytes(pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end].try_into().unwrap()) as usize; pkt_assembled_ptr = pkt_assembled_field_end; pkt_assembled_field_end = pkt_assembled_ptr + alice_meta_data_size; let alice_meta_data = if alice_meta_data_size > 0 { @@ -1411,6 +1410,7 @@ impl Session { } } +#[inline(always)] fn set_packet_header( packet: &mut [u8], fragment_count: usize, @@ -1433,27 +1433,32 @@ fn set_packet_header( // [52-57] fragment count (1..64 - 1, so 0 means 1 fragment) // [58-63] fragment number (0..63) // [64-127] 64-bit counter - memory::store_raw( - (u64::from(remote_session_id) + assert!(packet.len() >= 16); + packet[0..8].copy_from_slice( + &(remote_session_id | ((key_index & 1) as u64).wrapping_shl(48) | (packet_type as u64).wrapping_shl(49) | ((fragment_count - 1) as u64).wrapping_shl(52) | (fragment_no as u64).wrapping_shl(58)) - .to_le(), - packet, + .to_le_bytes(), ); - memory::store_raw(counter.to_le(), &mut packet[8..]); + packet[8..16].copy_from_slice(&counter.to_le_bytes()); } #[inline(always)] fn parse_packet_header(incoming_packet: &[u8]) -> (usize, u8, u8, u8, u64) { - let raw_header_a = u16::from_le(memory::load_raw(&incoming_packet[6..])); + let raw_header_a = u16::from_le_bytes(incoming_packet[6..8].try_into().unwrap()); let key_index = (raw_header_a & 1) as usize; let packet_type = (raw_header_a.wrapping_shr(1) & 7) as u8; let fragment_count = ((raw_header_a.wrapping_shr(4) & 63) + 1) as u8; let fragment_no = raw_header_a.wrapping_shr(10) as u8; - let counter = u64::from_le(memory::load_raw(&incoming_packet[8..])); - (key_index, packet_type, fragment_count, fragment_no, counter) + ( + key_index, + packet_type, + fragment_count, + fragment_no, + u64::from_le_bytes(incoming_packet[8..16].try_into().unwrap()), + ) } /// Break a packet into fragments and send them all. From 40945cf6c92bc0cb07aee25d716b3c4255481a6c Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Thu, 2 Mar 2023 19:09:31 -0500 Subject: [PATCH 10/14] Rework defragmentation, and it now tolerates very poor link quality pretty well. --- utils/src/gatherarray.rs | 6 +- zssp/src/applicationlayer.rs | 8 + zssp/src/fragged.rs | 103 ++++++++ zssp/src/lib.rs | 1 + zssp/src/main.rs | 164 +++++++----- zssp/src/proto.rs | 25 +- zssp/src/zssp.rs | 500 ++++++++++++++++++++--------------- 7 files changed, 514 insertions(+), 293 deletions(-) create mode 100644 zssp/src/fragged.rs diff --git a/utils/src/gatherarray.rs b/utils/src/gatherarray.rs index 091a77354..b0b0a3539 100644 --- a/utils/src/gatherarray.rs +++ b/utils/src/gatherarray.rs @@ -38,7 +38,7 @@ impl GatherArray { /// Add an item to the array if we don't have this index anymore, returning complete array if all parts are here. #[inline(always)] - pub fn add(&mut self, index: u8, value: T) -> Option> { + pub fn add_return_when_satisfied(&mut self, index: u8, value: T) -> Option> { if index < self.goal { let mut have = self.have_bits; let got = 1u64.wrapping_shl(index as u32); @@ -91,9 +91,9 @@ mod tests { for goal in 2u8..64u8 { let mut m = GatherArray::::new(goal); for x in 0..(goal - 1) { - assert!(m.add(x, x).is_none()); + assert!(m.add_return_when_satisfied(x, x).is_none()); } - let r = m.add(goal - 1, goal - 1).unwrap(); + let r = m.add_return_when_satisfied(goal - 1, goal - 1).unwrap(); for x in 0..goal { assert_eq!(r.as_ref()[x as usize], x); } diff --git a/zssp/src/applicationlayer.rs b/zssp/src/applicationlayer.rs index b9a1f1723..1c89ff72f 100644 --- a/zssp/src/applicationlayer.rs +++ b/zssp/src/applicationlayer.rs @@ -6,6 +6,8 @@ * https://www.zerotier.com/ */ +use std::hash::Hash; + use zerotier_crypto::p384::P384KeyPair; /// Trait to implement to integrate the session into an application. @@ -65,6 +67,12 @@ pub trait ApplicationLayer: Sized { /// for a short period of time when assembling fragmented packets on the receive path. type IncomingPacketBuffer: AsRef<[u8]> + AsMut<[u8]>; + /// Opaque type for whatever constitutes a physical path to the application. + /// + /// A physical path could be an IP address or IP plus device in the case of UDP, a socket in the + /// case of TCP, etc. + type PhysicalPath: PartialEq + Eq + Hash + Clone; + /// Get a reference to this host's static public key blob. /// /// This must contain a NIST P-384 public key but can contain other information. In ZeroTier this diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs new file mode 100644 index 000000000..e1b0629e6 --- /dev/null +++ b/zssp/src/fragged.rs @@ -0,0 +1,103 @@ +use std::mem::{needs_drop, size_of, zeroed, MaybeUninit}; +use std::ptr::slice_from_raw_parts; + +/// Fast packet defragmenter +pub struct Fragged { + have: u64, + counter: u64, + frags: [MaybeUninit; MAX_FRAGMENTS], +} + +pub struct Assembled([MaybeUninit; MAX_FRAGMENTS], usize); + +impl AsRef<[Fragment]> for Assembled { + #[inline(always)] + fn as_ref(&self) -> &[Fragment] { + unsafe { &*slice_from_raw_parts(self.0.as_ptr().cast::(), self.1) } + } +} + +impl Drop for Assembled { + #[inline(always)] + fn drop(&mut self) { + for i in 0..self.1 { + unsafe { + self.0.get_unchecked_mut(i).assume_init_drop(); + } + } + } +} + +impl Fragged { + pub fn new() -> Self { + debug_assert!(MAX_FRAGMENTS <= 64); + debug_assert_eq!(size_of::>(), size_of::()); + debug_assert_eq!( + size_of::<[MaybeUninit; MAX_FRAGMENTS]>(), + size_of::<[Fragment; MAX_FRAGMENTS]>() + ); + unsafe { zeroed() } + } + + pub fn assemble( + &mut self, + counter: u64, + fragment: Fragment, + fragment_no: u8, + fragment_count: u8, + ) -> Option> { + if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { + debug_assert!((fragment_count as usize) <= MAX_FRAGMENTS); + debug_assert!((fragment_no as usize) < MAX_FRAGMENTS); + + let mut have = self.have; + if counter != self.counter { + self.counter = counter; + if needs_drop::() { + let mut i = 0; + while have != 0 { + if (have & 1) != 0 { + debug_assert!(i < MAX_FRAGMENTS); + unsafe { self.frags.get_unchecked_mut(i).assume_init_drop() }; + } + have = have.wrapping_shr(1); + i += 1; + } + } else { + have = 0; + } + } + + unsafe { + self.frags.get_unchecked_mut(fragment_no as usize).write(fragment); + } + + let want = 0xffffffffffffffffu64.wrapping_shr((64 - fragment_count) as u32); + have |= 1u64.wrapping_shl(fragment_no as u32); + if (have & want) == want { + self.have = 0; + return Some(Assembled(unsafe { std::mem::transmute_copy(&self.frags) }, fragment_count as usize)); + } else { + self.have = have; + } + } + return None; + } +} + +impl Drop for Fragged { + fn drop(&mut self) { + if needs_drop::() { + let mut have = self.have; + let mut i = 0; + while have != 0 { + if (have & 1) != 0 { + debug_assert!(i < MAX_FRAGMENTS); + unsafe { self.frags.get_unchecked_mut(i).assume_init_drop() }; + } + have = have.wrapping_shr(1); + i += 1; + } + } + } +} diff --git a/zssp/src/lib.rs b/zssp/src/lib.rs index 718e5adb4..d4b41ca62 100644 --- a/zssp/src/lib.rs +++ b/zssp/src/lib.rs @@ -8,6 +8,7 @@ mod applicationlayer; mod error; +mod fragged; mod proto; mod sessionid; mod zssp; diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 0567541db..02b86b6ae 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -1,9 +1,12 @@ +use std::iter::ExactSizeIterator; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; use std::thread; use std::time::Duration; use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; +use zerotier_crypto::random; use zerotier_crypto::secret::Secret; use zerotier_utils::hex; use zerotier_utils::ms_monotonic; @@ -23,8 +26,8 @@ impl zssp::ApplicationLayer for TestApplication { const RETRY_INTERVAL: i64 = 500; type Data = (); - type IncomingPacketBuffer = Vec; + type PhysicalPath = usize; fn get_local_s_public_blob(&self) -> &[u8] { self.identity_key.public_key_bytes() @@ -37,6 +40,7 @@ impl zssp::ApplicationLayer for TestApplication { fn alice_main( run: &AtomicBool, + packet_success_rate: u32, alice_app: &TestApplication, bob_app: &TestApplication, alice_out: mpsc::SyncSender>, @@ -46,7 +50,7 @@ fn alice_main( let mut data_buf = [0u8; 65536]; let mut next_service = ms_monotonic() + 500; let mut last_ratchet_count = 0; - let test_data = [1u8; 10000]; + let test_data = [1u8; TEST_MTU * 10]; let mut up = false; let alice_session = context @@ -71,31 +75,35 @@ fn alice_main( loop { let pkt = alice_in.try_recv(); if let Ok(pkt) = pkt { - //println!("bob >> alice {}", pkt.len()); - match context.receive( - alice_app, - || true, - |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), - |_, b| { - let _ = alice_out.send(b.to_vec()); - }, - &mut data_buf, - pkt, - TEST_MTU, - current_time, - ) { - Ok(zssp::ReceiveResult::Ok) => { - //println!("[alice] ok"); - } - Ok(zssp::ReceiveResult::OkData(_, _)) => { - //println!("[alice] received {}", data.len()); - } - Ok(zssp::ReceiveResult::OkNewSession(s)) => { - println!("[alice] new session {}", s.id.to_string()); - } - Ok(zssp::ReceiveResult::Rejected) => {} - Err(e) => { - println!("[alice] ERROR {}", e.to_string()); + if (random::xorshift64_random() as u32) <= packet_success_rate { + match context.receive( + alice_app, + || true, + |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), + |_, b| { + let _ = alice_out.send(b.to_vec()); + }, + &0, + &mut data_buf, + pkt, + TEST_MTU, + current_time, + ) { + Ok(zssp::ReceiveResult::Ok) => { + //println!("[alice] ok"); + } + Ok(zssp::ReceiveResult::OkData(_, _)) => { + //println!("[alice] received {}", data.len()); + } + Ok(zssp::ReceiveResult::OkNewSession(s)) => { + println!("[alice] new session {}", s.id.to_string()); + } + Ok(zssp::ReceiveResult::Rejected) => {} + Err(e) => { + println!("[alice] ERROR {}", e.to_string()); + //run.store(false, Ordering::SeqCst); + //break; + } } } } else { @@ -116,12 +124,14 @@ fn alice_main( let _ = alice_out.send(b.to_vec()); }, &mut data_buf[..TEST_MTU], - &test_data[..1400 + ((zerotier_crypto::random::xorshift64_random() as usize) % (test_data.len() - 1400))], + &test_data[..1400 + ((random::xorshift64_random() as usize) % (test_data.len() - 1400))], ) .is_ok()); } else { if alice_session.established() { up = true; + } else { + thread::sleep(Duration::from_millis(10)); } } @@ -140,6 +150,7 @@ fn alice_main( fn bob_main( run: &AtomicBool, + packet_success_rate: u32, _alice_app: &TestApplication, bob_app: &TestApplication, bob_out: mpsc::SyncSender>, @@ -160,42 +171,46 @@ fn bob_main( let current_time = ms_monotonic(); if let Ok(pkt) = pkt { - //println!("alice >> bob {}", pkt.len()); - match context.receive( - bob_app, - || true, - |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), - |_, b| { - let _ = bob_out.send(b.to_vec()); - }, - &mut data_buf, - pkt, - TEST_MTU, - current_time, - ) { - Ok(zssp::ReceiveResult::Ok) => { - //println!("[bob] ok"); - } - Ok(zssp::ReceiveResult::OkData(s, data)) => { - //println!("[bob] received {}", data.len()); - assert!(s - .send( - |b| { - let _ = bob_out.send(b.to_vec()); - }, - &mut data_buf_2, - data.as_mut(), - ) - .is_ok()); - transferred += data.len() as u64 * 2; // *2 because we are also sending this many bytes back - } - Ok(zssp::ReceiveResult::OkNewSession(s)) => { - println!("[bob] new session {}", s.id.to_string()); - let _ = bob_session.replace(s); - } - Ok(zssp::ReceiveResult::Rejected) => {} - Err(e) => { - println!("[bob] ERROR {}", e.to_string()); + if (random::xorshift64_random() as u32) <= packet_success_rate { + match context.receive( + bob_app, + || true, + |s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), + |_, b| { + let _ = bob_out.send(b.to_vec()); + }, + &0, + &mut data_buf, + pkt, + TEST_MTU, + current_time, + ) { + Ok(zssp::ReceiveResult::Ok) => { + //println!("[bob] ok"); + } + Ok(zssp::ReceiveResult::OkData(s, data)) => { + //println!("[bob] received {}", data.len()); + assert!(s + .send( + |b| { + let _ = bob_out.send(b.to_vec()); + }, + &mut data_buf_2, + data.as_mut(), + ) + .is_ok()); + transferred += data.len() as u64 * 2; // *2 because we are also sending this many bytes back + } + Ok(zssp::ReceiveResult::OkNewSession(s)) => { + println!("[bob] new session {}", s.id.to_string()); + let _ = bob_session.replace(s); + } + Ok(zssp::ReceiveResult::Rejected) => {} + Err(e) => { + println!("[bob] ERROR {}", e.to_string()); + //run.store(false, Ordering::SeqCst); + //break; + } } } } @@ -211,10 +226,12 @@ fn bob_main( let speed_metric_elapsed = current_time - last_speed_metric; if speed_metric_elapsed >= 1000 { last_speed_metric = current_time; - println!( - "[bob] throughput: {} MiB/sec (combined input and output)", - ((transferred as f64) / 1048576.0) / ((speed_metric_elapsed as f64) / 1000.0) - ); + if transferred > 0 { + println!( + "[bob] throughput: {} MiB/sec (combined input and output)", + ((transferred as f64) / 1048576.0) / ((speed_metric_elapsed as f64) / 1000.0) + ); + } transferred = 0; } @@ -240,9 +257,16 @@ fn main() { let (alice_out, bob_in) = mpsc::sync_channel::>(1024); let (bob_out, alice_in) = mpsc::sync_channel::>(1024); + let args = std::env::args(); + let packet_success_rate = if args.len() <= 1 { + u32::MAX + } else { + ((u32::MAX as f64) * f64::from_str(args.last().unwrap().as_str()).unwrap()) as u32 + }; + thread::scope(|ts| { - let alice_thread = ts.spawn(|| alice_main(&run, &alice_app, &bob_app, alice_out, alice_in)); - let bob_thread = ts.spawn(|| bob_main(&run, &alice_app, &bob_app, bob_out, bob_in)); + let alice_thread = ts.spawn(|| alice_main(&run, packet_success_rate, &alice_app, &bob_app, alice_out, alice_in)); + let bob_thread = ts.spawn(|| bob_main(&run, packet_success_rate, &alice_app, &bob_app, bob_out, bob_in)); thread::sleep(Duration::from_secs(60 * 10)); diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index 32510bd44..ec8ce9991 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -24,24 +24,29 @@ pub const MIN_TRANSPORT_MTU: usize = 128; /// Maximum combined size of static public blob and metadata. pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_XK_ACK_MIN_SIZE; +/// Version 0: Noise_XK with NIST P-384 plus Kyber1024 hybrid exchange on session init. pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; -pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 16; +/// Maximum window over which packets may be reordered. +pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 32; + +/// Maximum number of counter steps that the counter is allowed to skip ahead. pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 16777216; -pub(crate) const PACKET_TYPE_DATA: u8 = 0; -pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_INIT: u8 = 1; -pub(crate) const PACKET_TYPE_BOB_NOISE_XK_ACK: u8 = 2; -pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_ACK: u8 = 3; -pub(crate) const PACKET_TYPE_REKEY_INIT: u8 = 4; -pub(crate) const PACKET_TYPE_REKEY_ACK: u8 = 5; +pub(crate) const PACKET_TYPE_NOP: u8 = 0; +pub(crate) const PACKET_TYPE_DATA: u8 = 1; +pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_INIT: u8 = 2; +pub(crate) const PACKET_TYPE_BOB_NOISE_XK_ACK: u8 = 3; +pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_ACK: u8 = 4; +pub(crate) const PACKET_TYPE_REKEY_INIT: u8 = 5; +pub(crate) const PACKET_TYPE_REKEY_ACK: u8 = 6; pub(crate) const HEADER_SIZE: usize = 16; pub(crate) const HEADER_PROTECT_ENCRYPT_START: usize = 6; pub(crate) const HEADER_PROTECT_ENCRYPT_END: usize = 22; -pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION: u8 = b'X'; // intermediate keys used in key exchanges -pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION: u8 = b'x'; // intermediate keys used in key exchanges +pub(crate) const KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION: u8 = b'x'; // AES-CTR encryption during initial setup +pub(crate) const KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION: u8 = b'X'; // HMAC-SHA384 during initial setup pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHET: u8 = b'R'; // Key used in derivatin of next session key @@ -50,6 +55,7 @@ pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 pub(crate) const MAX_NOISE_HANDSHAKE_FRAGMENTS: usize = 16; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS * MIN_TRANSPORT_MTU; +/// Size of keys used during derivation, mixing, etc. process. pub(crate) const BASE_KEY_SIZE: usize = 64; pub(crate) const AES_256_KEY_SIZE: usize = 32; @@ -162,7 +168,6 @@ impl RekeyAck { pub(crate) trait ProtocolFlatBuffer {} impl ProtocolFlatBuffer for AliceNoiseXKInit {} impl ProtocolFlatBuffer for BobNoiseXKAck {} -//impl ProtocolFlatBuffer for NoiseXKAliceStaticAck {} impl ProtocolFlatBuffer for RekeyInit {} impl ProtocolFlatBuffer for RekeyAck {} diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 423d48828..b510cdba0 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -15,19 +15,18 @@ use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesCtr, AesGcm}; -use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384, SHA384_HASH_SIZE}; +use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384}; use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE}; use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; use zerotier_utils::arrayvec::ArrayVec; -use zerotier_utils::gatherarray::GatherArray; -use zerotier_utils::ringbuffermap::RingBufferMap; -use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_SECRETKEYBYTES, KYBER_SSBYTES}; +use pqc_kyber::{KYBER_SECRETKEYBYTES, KYBER_SSBYTES}; use crate::applicationlayer::ApplicationLayer; use crate::error::Error; +use crate::fragged::Fragged; use crate::proto::*; use crate::sessionid::SessionId; @@ -37,7 +36,12 @@ use crate::sessionid::SessionId; /// defragment incoming packets that are not yet associated with a session. pub struct Context { max_incomplete_session_queue_size: usize, - defrag: Mutex, 256, 256>>, + defrag: Mutex< + HashMap< + (Application::PhysicalPath, u64), + Arc, i64)>>, + >, + >, sessions: RwLock>, } @@ -80,7 +84,7 @@ pub struct Session { receive_window: [AtomicU64; COUNTER_WINDOW_MAX_OOO], header_protection_cipher: Aes, state: RwLock, - defrag: Mutex, 16, 16>>, + defrag: [Mutex>; COUNTER_WINDOW_MAX_OOO], } /// Most of the mutable parts of a session state. @@ -91,20 +95,16 @@ struct State { current_offer: Offer, } -/// State related to an incoming session not yet fully established. struct IncomingIncompleteSession { timestamp: i64, - request_hash: [u8; SHA384_HASH_SIZE], alice_session_id: SessionId, bob_session_id: SessionId, noise_es_ee: Secret, - bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES], hk: Secret, header_protection_key: Secret, bob_noise_e_secret: P384KeyPair, } -/// State related to an outgoing session attempt. struct OutgoingSessionInit { last_retry_time: AtomicI64, alice_noise_e_secret: P384KeyPair, @@ -114,14 +114,19 @@ struct OutgoingSessionInit { init_packet: [u8; AliceNoiseXKInit::SIZE], } -/// Latest outgoing offer, either an outgoing attempt or a rekey attempt. +struct OutgoingSessionAck { + last_retry_time: AtomicI64, + ack: [u8; MAX_NOISE_HANDSHAKE_SIZE], + ack_size: usize, +} + enum Offer { None, NoiseXKInit(Box), - RekeyInit(P384KeyPair, [u8; RekeyInit::SIZE], AtomicI64), + NoiseXKAck(Box), + RekeyInit(P384KeyPair, i64), } -/// An ephemeral session key with expiration info. struct SessionKey { ratchet_key: Secret, // Key used in derivation of the next session key receive_key: Secret, // Receive side AES-GCM key @@ -134,6 +139,7 @@ struct SessionKey { expire_at_counter: u64, // Hard error when this counter value is reached or exceeded ratchet_count: u64, // Number of rekey events bob: bool, // Was this side "Bob" in this exchange? + confirmed: bool, // Is this key confirmed by the other side? } impl Context { @@ -141,7 +147,7 @@ impl Context { pub fn new(max_incomplete_session_queue_size: usize) -> Self { Self { max_incomplete_session_queue_size, - defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())), + defrag: Mutex::new(HashMap::new()), sessions: RwLock::new(SessionsById { active: HashMap::with_capacity(64), incoming: HashMap::with_capacity(64), @@ -173,19 +179,14 @@ impl Context { for (id, s) in sessions.active.iter() { if let Some(session) = s.upgrade() { let state = session.state.read().unwrap(); - match &state.current_offer { - Offer::None => { - if let Some(key) = state.keys[state.current_key].as_ref() { - if key.bob - && (current_time >= key.rekey_at_time - || session.send_counter.load(Ordering::Relaxed) >= key.rekey_at_counter) - { - drop(state); - session.initiate_rekey(|b| send(&session, b), current_time); - } - } - } + if match &state.current_offer { + Offer::None => true, Offer::NoiseXKInit(offer) => { + // If there's an outstanding attempt to open a session, retransmit this periodically + // in case the initial packet doesn't make it. Note that we currently don't have + // retransmission for the intermediate steps, so a new session may still fail if the + // packet loss rate is huge. The application layer has its own logic to keep trying + // under those conditions. if offer.last_retry_time.load(Ordering::Relaxed) < retry_cutoff { offer.last_retry_time.store(current_time, Ordering::Relaxed); let _ = send_with_fragmentation( @@ -199,11 +200,37 @@ impl Context { None, ); } + false } - Offer::RekeyInit(_, rekey_packet, last_retry_time) => { - if last_retry_time.load(Ordering::Relaxed) < retry_cutoff { - last_retry_time.store(current_time, Ordering::Relaxed); - send(&session, &mut (rekey_packet.clone())); + Offer::NoiseXKAck(ack) => { + // We also keep retransmitting the final ACK until we get a valid DATA or NOP packet + // from Bob, otherwise we could get a half open session. + if ack.last_retry_time.load(Ordering::Relaxed) < retry_cutoff { + ack.last_retry_time.store(current_time, Ordering::Relaxed); + let _ = send_with_fragmentation( + |b| send(&session, b), + &mut (ack.ack.clone())[..ack.ack_size], + mtu, + PACKET_TYPE_ALICE_NOISE_XK_ACK, + state.remote_session_id, + 0, + 2, + Some(&session.header_protection_cipher), + ); + } + false + } + Offer::RekeyInit(_, last_rekey_attempt_time) => *last_rekey_attempt_time < retry_cutoff, + } { + // Check whether we need to rekey if there is no pending offer or if the last rekey + // offer was before retry_cutoff (checked in the 'match' above). + if let Some(key) = state.keys[state.current_key].as_ref() { + if key.bob + && (current_time >= key.rekey_at_time + || session.send_counter.load(Ordering::Relaxed) >= key.rekey_at_counter) + { + drop(state); + session.initiate_rekey(|b| send(&session, b), current_time); } } } @@ -297,7 +324,7 @@ impl Context { init_packet: [0u8; AliceNoiseXKInit::SIZE], })), }), - defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), + defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), }); sessions.active.insert(local_session_id, Arc::downgrade(&session)); @@ -310,7 +337,7 @@ impl Context { let init_packet = if let Offer::NoiseXKInit(offer) = &mut state.current_offer { &mut offer.init_packet } else { - panic!(); + panic!(); // should be impossible }; let init: &mut AliceNoiseXKInit = byte_array_as_proto_buffer_mut(init_packet).unwrap(); @@ -321,12 +348,12 @@ impl Context { init.header_protection_key = header_protection_key.0; aes_ctr_crypt_one_time_use_key( - kbkdf::(noise_es.as_bytes()).as_bytes(), + kbkdf::(noise_es.as_bytes()).as_bytes(), &mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START], ); let hmac = hmac_sha384_2( - kbkdf::(noise_es.as_bytes()).as_bytes(), + kbkdf::(noise_es.as_bytes()).as_bytes(), &create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_INIT, 1), &init_packet[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], ); @@ -385,6 +412,7 @@ impl Context { mut check_allow_incoming_session: CheckAllowIncomingSession, mut check_accept_session: CheckAcceptSession, mut send: SendFunction, + source: &Application::PhysicalPath, data_buf: &'b mut [u8], mut incoming_packet_buf: Application::IncomingPacketBuffer, mtu: usize, @@ -414,31 +442,27 @@ impl Context { if session.check_receive_window(incoming_counter) { if fragment_count > 1 { - if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count { - let mut defrag = session.defrag.lock().unwrap(); - let fragment_gather_array = defrag.get_or_create_mut(&incoming_counter, || GatherArray::new(fragment_count)); - if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) { - drop(defrag); // release lock - return self.process_complete_incoming_packet( - app, - &mut send, - &mut check_allow_incoming_session, - &mut check_accept_session, - data_buf, - incoming_counter, - assembled_packet.as_ref(), - packet_type, - Some(session), - None, - key_index, - mtu, - current_time, - ); - } else { - return Ok(ReceiveResult::Ok); - } + let mut fragged = session.defrag[(incoming_counter as usize) % COUNTER_WINDOW_MAX_OOO].lock().unwrap(); + if let Some(assembled_packet) = fragged.assemble(incoming_counter, incoming_packet_buf, fragment_no, fragment_count) + { + drop(fragged); + return self.process_complete_incoming_packet( + app, + &mut send, + &mut check_allow_incoming_session, + &mut check_accept_session, + data_buf, + incoming_counter, + assembled_packet.as_ref(), + packet_type, + Some(session), + None, + key_index, + mtu, + current_time, + ); } else { - return Err(Error::InvalidPacket); + return Ok(ReceiveResult::Ok); } } else { return self.process_complete_incoming_packet( @@ -476,10 +500,19 @@ impl Context { let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_packet); if fragment_count > 1 { - let mut defrag = self.defrag.lock().unwrap(); - let fragment_gather_array = defrag.get_or_create_mut(&incoming_counter, || GatherArray::new(fragment_count)); - if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) { - drop(defrag); // release lock + let fragged_m = { + let mut defrag = self.defrag.lock().unwrap(); + defrag + .entry((source.clone(), incoming_counter)) + .or_insert_with(|| Arc::new(Mutex::new((Fragged::new(), current_time)))) + .clone() + }; + let mut fragged = fragged_m.lock().unwrap(); + if let Some(assembled_packet) = fragged + .0 + .assemble(incoming_counter, incoming_packet_buf, fragment_no, fragment_count) + { + self.defrag.lock().unwrap().remove(&(source.clone(), incoming_counter)); return self.process_complete_incoming_packet( app, &mut send, @@ -543,7 +576,7 @@ impl Context { // Generate incoming message nonce for decryption and authentication. let incoming_message_nonce = create_message_nonce(packet_type, incoming_counter); - if packet_type == PACKET_TYPE_DATA { + if packet_type <= PACKET_TYPE_DATA { if let Some(session) = session { let state = session.state.read().unwrap(); if let Some(key) = state.keys[key_index].as_ref() { @@ -590,22 +623,41 @@ impl Context { // Update the current key to point to this key if it's newer, since having received // a packet encrypted with it proves that the other side has successfully derived it // as well. - if state.current_key == key_index { + if state.current_key == key_index && key.confirmed { drop(state); } else { - let key_created_at_counter = key.created_at_counter; + let current_key_created_at_counter = key.created_at_counter; + drop(state); let mut state = session.state.write().unwrap(); - if let Some(other_session_key) = state.keys[state.current_key].as_ref() { - if other_session_key.created_at_counter < key_created_at_counter { + + if state.current_key != key_index { + if let Some(other_session_key) = state.keys[state.current_key].as_ref() { + if other_session_key.created_at_counter < current_key_created_at_counter { + state.current_key = key_index; + } + } else { state.current_key = key_index; } } else { - state.current_key = key_index; + state.keys[key_index].as_mut().unwrap().confirmed = true; + } + + // If we got a valid data packet from Bob, this means we can cancel any offers + // that are still oustanding for initialization. + match &state.current_offer { + Offer::NoiseXKInit(_) | Offer::NoiseXKAck(_) => { + state.current_offer = Offer::None; + } + _ => {} } } - return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); + if packet_type == PACKET_TYPE_DATA { + return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); + } else { + println!("nop"); + } } else { return Err(Error::OutOfSequence); } @@ -650,120 +702,104 @@ impl Context { if incoming_counter != 1 || session.is_some() { return Err(Error::OutOfSequence); } - - // Hash the init packet so we can check to see if it's just being retransmitted. Alice may - // attempt to retransmit this packet until she receives a response. - let request_hash = SHA384::hash(&pkt_assembled); - - let (alice_session_id, mut bob_session_id, noise_es_ee, bob_hk_ciphertext, header_protection_key, bob_noise_e); - if let Some(incoming) = incoming { - // If we've already seen this exact packet before, just recall the same state so we send the - // same response. - if secure_eq(&request_hash, &incoming.request_hash) { - alice_session_id = incoming.alice_session_id; - bob_session_id = incoming.bob_session_id; - noise_es_ee = incoming.noise_es_ee.clone(); - bob_hk_ciphertext = incoming.bob_hk_ciphertext; - header_protection_key = incoming.header_protection_key.clone(); - bob_noise_e = *incoming.bob_noise_e_secret.public_key_bytes(); - } else { - return Err(Error::FailedAuthentication); - } - } else { - // Otherwise parse the packet, authenticate, generate keys, etc. and record state in an - // incoming state object until this phase of the negotiation is done. - let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; - let alice_noise_e = P384PublicKey::from_bytes(&pkt.alice_noise_e).ok_or(Error::FailedAuthentication)?; - let noise_es = app.get_local_s_keypair().agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?; - - // Authenticate packet and also prove that Alice knows our static public key. - if !secure_eq( - &pkt.hmac_es, - &hmac_sha384_2( - kbkdf::(noise_es.as_bytes()).as_bytes(), - &incoming_message_nonce, - &pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], - ), - ) { - return Err(Error::FailedAuthentication); - } - - // Let application filter incoming connection attempt by whatever criteria it wants. - if !check_allow_incoming_session() { - return Ok(ReceiveResult::Rejected); - } - - // Decrypt encrypted part of payload. - aes_ctr_crypt_one_time_use_key( - kbkdf::(noise_es.as_bytes()).as_bytes(), - &mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START], - ); - - let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; - alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?; - header_protection_key = Secret(pkt.header_protection_key); - - // Create Bob's ephemeral keys and derive noise_es_ee by agreeing with Alice's. Also create - // a Kyber ciphertext to send back to Alice. - let bob_noise_e_secret = P384KeyPair::generate(); - bob_noise_e = bob_noise_e_secret.public_key_bytes().clone(); - noise_es_ee = Secret(hmac_sha512( - noise_es.as_bytes(), - bob_noise_e_secret - .agree(&alice_noise_e) - .ok_or(Error::FailedAuthentication)? - .as_bytes(), - )); - let (hk_ct, hk) = pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default()) - .map_err(|_| Error::FailedAuthentication) - .map(|(ct, hk)| (ct, Secret(hk)))?; - bob_hk_ciphertext = hk_ct; - - let mut sessions = self.sessions.write().unwrap(); - - loop { - bob_session_id = SessionId::random(); - if !sessions.active.contains_key(&bob_session_id) && !sessions.incoming.contains_key(&bob_session_id) { - break; - } - } - - if sessions.incoming.len() >= self.max_incomplete_session_queue_size { - // If this queue is too big, we remove the latest entry and replace it. The latest - // is used because under flood conditions this is most likely to be another bogus - // entry. If we find one that is actually timed out, that one is replaced instead. - let mut newest = i64::MIN; - let mut replace_id = None; - let cutoff_time = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; - for (id, s) in sessions.incoming.iter() { - if s.timestamp <= cutoff_time { - replace_id = Some(*id); - break; - } else if s.timestamp >= newest { - newest = s.timestamp; - replace_id = Some(*id); - } - } - let _ = sessions.incoming.remove(replace_id.as_ref().unwrap()); - } - - // Reserve session ID on this side and record incomplete session state. - sessions.incoming.insert( - bob_session_id, - Arc::new(IncomingIncompleteSession { - timestamp: current_time, - request_hash, - alice_session_id, - bob_session_id, - noise_es_ee: noise_es_ee.clone(), - bob_hk_ciphertext, - hk, - bob_noise_e_secret, - header_protection_key: Secret(pkt.header_protection_key), - }), - ); + if pkt_assembled.len() != AliceNoiseXKInit::SIZE { + return Err(Error::InvalidPacket); } + // Otherwise parse the packet, authenticate, generate keys, etc. and record state in an + // incoming state object until this phase of the negotiation is done. + let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; + let alice_noise_e = P384PublicKey::from_bytes(&pkt.alice_noise_e).ok_or(Error::FailedAuthentication)?; + let noise_es = app.get_local_s_keypair().agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?; + + // Authenticate packet and also prove that Alice knows our static public key. + if !secure_eq( + &pkt.hmac_es, + &hmac_sha384_2( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &incoming_message_nonce, + &pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], + ), + ) { + return Err(Error::FailedAuthentication); + } + + // Let application filter incoming connection attempt by whatever criteria it wants. + if !check_allow_incoming_session() { + return Ok(ReceiveResult::Rejected); + } + + // Decrypt encrypted part of payload. + aes_ctr_crypt_one_time_use_key( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START], + ); + + let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; + let alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?; + let header_protection_key = Secret(pkt.header_protection_key); + + // Create Bob's ephemeral keys and derive noise_es_ee by agreeing with Alice's. Also create + // a Kyber ciphertext to send back to Alice. + let bob_noise_e_secret = P384KeyPair::generate(); + let bob_noise_e = bob_noise_e_secret.public_key_bytes().clone(); + let noise_es_ee = Secret(hmac_sha512( + noise_es.as_bytes(), + bob_noise_e_secret + .agree(&alice_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + )); + let (bob_hk_ciphertext, hk) = pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default()) + .map_err(|_| Error::FailedAuthentication) + .map(|(ct, hk)| (ct, Secret(hk)))?; + + let mut sessions = self.sessions.write().unwrap(); + + let mut bob_session_id; + loop { + bob_session_id = SessionId::random(); + if !sessions.active.contains_key(&bob_session_id) && !sessions.incoming.contains_key(&bob_session_id) { + break; + } + } + + if sessions.incoming.len() >= self.max_incomplete_session_queue_size { + // If this queue is too big, we remove the latest entry and replace it. The latest + // is used because under flood conditions this is most likely to be another bogus + // entry. If we find one that is actually timed out, that one is replaced instead. + let mut newest = i64::MIN; + let mut replace_id = None; + let cutoff_time = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; + for (id, s) in sessions.incoming.iter() { + if s.timestamp <= cutoff_time { + replace_id = Some(*id); + break; + } else if s.timestamp >= newest { + newest = s.timestamp; + replace_id = Some(*id); + } + } + let _ = sessions.incoming.remove(replace_id.as_ref().unwrap()); + } + + // Reserve session ID on this side and record incomplete session state. + sessions.incoming.insert( + bob_session_id, + Arc::new(IncomingIncompleteSession { + timestamp: current_time, + alice_session_id, + bob_session_id, + noise_es_ee: noise_es_ee.clone(), + hk, + bob_noise_e_secret, + header_protection_key: Secret(pkt.header_protection_key), + }), + ); + debug_assert!(!sessions.active.contains_key(&bob_session_id)); + + drop(sessions); + // Create Bob's ephemeral counter-offer reply. let mut ack_packet = [0u8; BobNoiseXKAck::SIZE]; let ack: &mut BobNoiseXKAck = byte_array_as_proto_buffer_mut(&mut ack_packet)?; @@ -774,13 +810,13 @@ impl Context { // Encrypt main section of reply. aes_ctr_crypt_one_time_use_key( - kbkdf::(noise_es_ee.as_bytes()).as_bytes(), + kbkdf::(noise_es_ee.as_bytes()).as_bytes(), &mut ack_packet[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START], ); // Add HMAC-SHA384 to reply packet. let reply_hmac = hmac_sha384_2( - kbkdf::(noise_es_ee.as_bytes()).as_bytes(), + kbkdf::(noise_es_ee.as_bytes()).as_bytes(), &create_message_nonce(PACKET_TYPE_BOB_NOISE_XK_ACK, 1), &ack_packet[HEADER_SIZE..BobNoiseXKAck::AUTH_START], ); @@ -812,9 +848,18 @@ impl Context { if incoming_counter != 1 || incoming.is_some() { return Err(Error::OutOfSequence); } + if pkt_assembled.len() != BobNoiseXKAck::SIZE { + return Err(Error::InvalidPacket); + } if let Some(session) = session { let state = session.state.read().unwrap(); + + // This doesn't make sense if the session is up. + if state.keys[state.current_key].is_some() { + return Err(Error::OutOfSequence); + } + if let Offer::NoiseXKInit(outgoing_offer) = &state.current_offer { let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; @@ -830,7 +875,7 @@ impl Context { )); let noise_es_ee_kex_hmac_key = - kbkdf::(noise_es_ee.as_bytes()); + kbkdf::(noise_es_ee.as_bytes()); // Authenticate Bob's reply and the validity of bob_noise_e. if !secure_eq( @@ -846,7 +891,7 @@ impl Context { // Decrypt encrypted portion of message. aes_ctr_crypt_one_time_use_key( - kbkdf::(noise_es_ee.as_bytes()).as_bytes(), + kbkdf::(noise_es_ee.as_bytes()).as_bytes(), &mut pkt_assembled[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START], ); let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; @@ -915,7 +960,7 @@ impl Context { // key exchange. Bob won't be able to do this until he decrypts and parses Alice's // identity, so the first HMAC is to let him authenticate that first. let hmac_es_ee_se_hk_psk = hmac_sha384_2( - kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) .as_bytes(), &reply_message_nonce, &reply_buffer[HEADER_SIZE..reply_len], @@ -933,9 +978,15 @@ impl Context { current_time, 2, false, + false, )); + debug_assert!(state.keys[1].is_none()); state.current_key = 0; - state.current_offer = Offer::None; + state.current_offer = Offer::NoiseXKAck(Box::new(OutgoingSessionAck { + last_retry_time: AtomicI64::new(current_time), + ack: reply_buffer, + ack_size: reply_len, + })); } send_with_fragmentation( @@ -980,18 +1031,13 @@ impl Context { } if let Some(incoming) = incoming { - // Check timeout, negotiations aren't allowed to take longer than this. - if (current_time - incoming.timestamp) > Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS { - return Err(Error::UnknownLocalSessionId); - } - // Check the first HMAC to verify against the currently known noise_es_ee key, which verifies // that this reply is part of this session. let auth_start = pkt_assembled.len() - ALICE_NOISE_XK_ACK_AUTH_SIZE; if !secure_eq( &pkt_assembled[auth_start..pkt_assembled.len() - HMAC_SHA384_SIZE], &hmac_sha384_2( - kbkdf::(incoming.noise_es_ee.as_bytes()) + kbkdf::(incoming.noise_es_ee.as_bytes()) .as_bytes(), &incoming_message_nonce, &pkt_assembled[HEADER_SIZE..auth_start], @@ -1065,7 +1111,7 @@ impl Context { if !secure_eq( &pkt_assembly_buffer_copy[auth_start + HMAC_SHA384_SIZE..pkt_assembled.len()], &hmac_sha384_2( - kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) .as_bytes(), &incoming_message_nonce, &pkt_assembly_buffer_copy[HEADER_SIZE..auth_start + HMAC_SHA384_SIZE], @@ -1084,22 +1130,30 @@ impl Context { state: RwLock::new(State { remote_session_id: Some(incoming.alice_session_id), keys: [ - Some(SessionKey::new::(noise_es_ee_se_hk_psk, 1, current_time, 2, true)), + Some(SessionKey::new::( + noise_es_ee_se_hk_psk, + 1, + current_time, + 2, + true, + true, + )), None, ], current_key: 0, current_offer: Offer::None, }), - defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), + defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), }); - // Promote this from an incomplete session to an established session. { let mut sessions = self.sessions.write().unwrap(); sessions.incoming.remove(&incoming.bob_session_id); sessions.active.insert(incoming.bob_session_id, Arc::downgrade(&session)); } + let _ = session.send_nop(|b| send(Some(&session), b)); + return Ok(ReceiveResult::OkNewSession(session)); } else { return Err(Error::UnknownLocalSessionId); @@ -1177,6 +1231,7 @@ impl Context { current_time, counter, false, + false, )); return Ok(ReceiveResult::Ok); @@ -1202,7 +1257,7 @@ impl Context { if let Some(session) = session { let state = session.state.read().unwrap(); - if let Offer::RekeyInit(alice_e_secret, _, _) = &state.current_offer { + if let Offer::RekeyInit(alice_e_secret, _) = &state.current_offer { if let Some(key) = state.keys[key_index].as_ref() { // Only the current "Bob" initiates rekeys and expects this ACK. if key.bob { @@ -1234,6 +1289,7 @@ impl Context { current_time, session.send_counter.load(Ordering::Acquire), true, + true, )); state.current_key = next_key_index; // this is an ACK so it's confirmed state.current_offer = Offer::None; @@ -1325,6 +1381,32 @@ impl Session { return Err(Error::SessionNotEstablished); } + /// Send a NOP to the other side (e.g. for keep alive). + pub fn send_nop(&self, mut send: SendFunction) -> Result<(), Error> { + let state = self.state.read().unwrap(); + if let Some(remote_session_id) = state.remote_session_id { + if let Some(session_key) = state.keys[state.current_key].as_ref() { + let counter = self.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?.get(); + let mut nop = [0u8; HEADER_SIZE + AES_GCM_TAG_SIZE]; + let mut c = session_key.get_send_cipher(counter)?; + c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_NOP, counter)); + nop[HEADER_SIZE..].copy_from_slice(&c.finish_encrypt()); + session_key.return_send_cipher(c); + set_packet_header( + &mut nop, + 1, + 0, + PACKET_TYPE_NOP, + u64::from(remote_session_id), + state.current_key, + counter, + ); + send(&mut nop); + } + } + return Err(Error::SessionNotEstablished); + } + /// Check whether this session is established. pub fn established(&self) -> bool { let state = self.state.read().unwrap(); @@ -1381,7 +1463,7 @@ impl Session { .encrypt_block_in_place(&mut rekey_buf[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); send(&mut rekey_buf); - self.state.write().unwrap().current_offer = Offer::RekeyInit(rekey_e, rekey_buf, AtomicI64::new(current_time)); + self.state.write().unwrap().current_offer = Offer::RekeyInit(rekey_e, current_time); } } } @@ -1448,15 +1530,11 @@ fn set_packet_header( #[inline(always)] fn parse_packet_header(incoming_packet: &[u8]) -> (usize, u8, u8, u8, u64) { let raw_header_a = u16::from_le_bytes(incoming_packet[6..8].try_into().unwrap()); - let key_index = (raw_header_a & 1) as usize; - let packet_type = (raw_header_a.wrapping_shr(1) & 7) as u8; - let fragment_count = ((raw_header_a.wrapping_shr(4) & 63) + 1) as u8; - let fragment_no = raw_header_a.wrapping_shr(10) as u8; ( - key_index, - packet_type, - fragment_count, - fragment_no, + (raw_header_a & 1) as usize, + (raw_header_a.wrapping_shr(1) & 7) as u8, + ((raw_header_a.wrapping_shr(4) & 63) + 1) as u8, + raw_header_a.wrapping_shr(10) as u8, u64::from_le_bytes(incoming_packet[8..16].try_into().unwrap()), ) } @@ -1532,11 +1610,12 @@ impl SessionKey { ratchet_count: u64, current_time: i64, current_counter: u64, - role_is_bob: bool, + bob: bool, + confirmed: bool, ) -> Self { let a2b = kbkdf::(key.as_bytes()); let b2a = kbkdf::(key.as_bytes()); - let (receive_key, send_key) = if role_is_bob { + let (receive_key, send_key) = if bob { (a2b, b2a) } else { (b2a, a2b) @@ -1557,7 +1636,8 @@ impl SessionKey { rekey_at_counter: current_counter.checked_add(Application::REKEY_AFTER_USES).unwrap(), expire_at_counter: current_counter.checked_add(Application::EXPIRE_AFTER_USES).unwrap(), ratchet_count, - bob: role_is_bob, + bob, + confirmed, } } From bbeff32bce6f9e03e3a849e498081721944dd118 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 3 Mar 2023 09:46:23 -0500 Subject: [PATCH 11/14] Circuit breaker for incoming defrag queue, and ZSSP now works very well even under very poor network conditions. --- crypto/src/mimcvdf.rs | 5 +- rustfmt.toml | 2 +- zssp/Cargo.toml | 2 +- zssp/src/error.rs | 1 + zssp/src/main.rs | 16 ++-- zssp/src/zssp.rs | 174 +++++++++++++++++++----------------------- 6 files changed, 95 insertions(+), 105 deletions(-) diff --git a/crypto/src/mimcvdf.rs b/crypto/src/mimcvdf.rs index bfb49921f..030dbff69 100644 --- a/crypto/src/mimcvdf.rs +++ b/crypto/src/mimcvdf.rs @@ -7,7 +7,7 @@ */ /* - * MIMC is a hash function originally designed for use with STARK and SNARK proofs. It's based + * MIMC is a cipher originally designed for use with STARK and SNARK proofs. It's based * on modular multiplication and exponentiation instead of the usual bit twiddling or ARX * operations that underpin more common hash algorithms. * @@ -17,7 +17,8 @@ * compute intensive. The "forward" direction simply requires modular cubing which is two modular * multiplications and is much faster. * - * It's also nice because it's incredibly simple with a tiny code footprint. + * It's a nice VDF because it's incredibly simple with a tiny code footprint. Most other VDFs + * involve RSA group operations or zero knowledge proofs. * * This is used for anti-DOS and anti-spamming delay functions. It's not used for anything * really "cryptographically hard," and if it were broken cryptographically it would still be diff --git a/rustfmt.toml b/rustfmt.toml index 554a221fb..c532918b9 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,5 @@ #unstable_features = true -max_width = 140 +max_width = 150 #use_small_heuristics = "Max" edition = "2021" #empty_item_single_line = true diff --git a/zssp/Cargo.toml b/zssp/Cargo.toml index d273a4013..29cc28958 100644 --- a/zssp/Cargo.toml +++ b/zssp/Cargo.toml @@ -24,4 +24,4 @@ doc = false [dependencies] zerotier-utils = { path = "../utils" } zerotier-crypto = { path = "../crypto" } -pqc_kyber = { git = "https://github.com/Argyle-Software/kyber", rev = "8c7927e00f4e3508769bf69afd55b2be1c22884d", features = ["kyber1024", "std"], default-features = false } +pqc_kyber = { version = "0.4.0", default-features = false, features = ["kyber1024", "std"] } diff --git a/zssp/src/error.rs b/zssp/src/error.rs index e416a8954..5fd35c9b4 100644 --- a/zssp/src/error.rs +++ b/zssp/src/error.rs @@ -6,6 +6,7 @@ * https://www.zerotier.com/ */ +#[derive(PartialEq, Eq)] pub enum Error { /// The packet was addressed to an unrecognized local session (should usually be ignored) UnknownLocalSessionId, diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 02b86b6ae..52f84824b 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -52,6 +52,7 @@ fn alice_main( let mut last_ratchet_count = 0; let test_data = [1u8; TEST_MTU * 10]; let mut up = false; + let mut last_error = zssp::Error::UnknownProtocolVersion; let alice_session = context .open( @@ -100,9 +101,10 @@ fn alice_main( } Ok(zssp::ReceiveResult::Rejected) => {} Err(e) => { - println!("[alice] ERROR {}", e.to_string()); - //run.store(false, Ordering::SeqCst); - //break; + if e != last_error { + println!("[alice] ERROR {}", e.to_string()); + last_error = e; + } } } } @@ -163,6 +165,7 @@ fn bob_main( let mut last_speed_metric = ms_monotonic(); let mut next_service = last_speed_metric + 500; let mut transferred = 0u64; + let mut last_error = zssp::Error::UnknownProtocolVersion; let mut bob_session = None; @@ -207,9 +210,10 @@ fn bob_main( } Ok(zssp::ReceiveResult::Rejected) => {} Err(e) => { - println!("[bob] ERROR {}", e.to_string()); - //run.store(false, Ordering::SeqCst); - //break; + if e != last_error { + println!("[bob] ERROR {}", e.to_string()); + last_error = e; + } } } } diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index b510cdba0..01985c22b 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -7,9 +7,9 @@ */ // ZSSP: ZeroTier Secure Session Protocol -// FIPS compliant Noise_XK with Jedi powers and built-in attack-resistant large payload (fragmentation) support. +// FIPS compliant Noise_XK with Jedi powers (Kyber1024) and built-in attack-resistant large payload (fragmentation) support. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::num::NonZeroU64; use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock, Weak}; @@ -20,8 +20,6 @@ use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_ use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; -use zerotier_utils::arrayvec::ArrayVec; - use pqc_kyber::{KYBER_SECRETKEYBYTES, KYBER_SSBYTES}; use crate::applicationlayer::ApplicationLayer; @@ -39,7 +37,10 @@ pub struct Context { defrag: Mutex< HashMap< (Application::PhysicalPath, u64), - Arc, i64)>>, + Arc<( + Mutex>, + i64, // creation timestamp + )>, >, >, sessions: RwLock>, @@ -110,7 +111,7 @@ struct OutgoingSessionInit { alice_noise_e_secret: P384KeyPair, noise_es: Secret, alice_hk_secret: Secret, - metadata: Option>, + metadata: Option>, init_packet: [u8; AliceNoiseXKInit::SIZE], } @@ -144,6 +145,8 @@ struct SessionKey { impl Context { /// Create a new session context. + /// + /// * `max_incomplete_session_queue_size` - Maximum number of incomplete sessions in negotiation phase pub fn new(max_incomplete_session_queue_size: usize) -> Self { Self { max_incomplete_session_queue_size, @@ -162,12 +165,7 @@ impl Context { /// * `send` - Function to send packets to remote sessions /// * `mtu` - Physical MTU /// * `current_time` - Current monotonic time in milliseconds - pub fn service>, &mut [u8])>( - &self, - mut send: SendFunction, - mtu: usize, - current_time: i64, - ) -> i64 { + pub fn service>, &mut [u8])>(&self, mut send: SendFunction, mtu: usize, current_time: i64) -> i64 { let mut dead_active = Vec::new(); let mut dead_pending = Vec::new(); let retry_cutoff = current_time - Application::RETRY_INTERVAL; @@ -225,9 +223,7 @@ impl Context { // Check whether we need to rekey if there is no pending offer or if the last rekey // offer was before retry_cutoff (checked in the 'match' above). if let Some(key) = state.keys[state.current_key].as_ref() { - if key.bob - && (current_time >= key.rekey_at_time - || session.send_counter.load(Ordering::Relaxed) >= key.rekey_at_counter) + if key.bob && (current_time >= key.rekey_at_time || session.send_counter.load(Ordering::Relaxed) >= key.rekey_at_counter) { drop(state); session.initiate_rekey(|b| send(&session, b), current_time); @@ -240,7 +236,7 @@ impl Context { } for (id, incoming) in sessions.incoming.iter() { - if incoming.timestamp < negotiation_timeout_cutoff { + if incoming.timestamp <= negotiation_timeout_cutoff { dead_pending.push(*id); } } @@ -256,6 +252,8 @@ impl Context { } } + self.defrag.lock().unwrap().retain(|_, fragged| fragged.1 > negotiation_timeout_cutoff); + Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS.min(Application::RETRY_INTERVAL) } @@ -279,11 +277,11 @@ impl Context { mtu: usize, remote_s_public_p384: &P384PublicKey, psk: Secret, - metadata: Option<&[u8]>, + metadata: Option>, application_data: Application::Data, current_time: i64, ) -> Result>, Error> { - if (metadata.map(|md| md.len()).unwrap_or(0) + app.get_local_s_public_blob().len()) > MAX_INIT_PAYLOAD_SIZE { + if (metadata.as_ref().map(|md| md.len()).unwrap_or(0) + app.get_local_s_public_blob().len()) > MAX_INIT_PAYLOAD_SIZE { return Err(Error::DataTooLarge); } @@ -320,7 +318,7 @@ impl Context { alice_noise_e_secret, noise_es: noise_es.clone(), alice_hk_secret: Secret(alice_hk_secret.secret), - metadata: metadata.map(|md| ArrayVec::try_from(md).unwrap()), + metadata, init_packet: [0u8; AliceNoiseXKInit::SIZE], })), }), @@ -377,8 +375,9 @@ impl Context { /// Receive, authenticate, decrypt, and process a physical wire packet. /// /// The send function may be called one or more times to send packets. If the packet is associated - /// wtth an active session this session is supplied, otherwise this parameter is None. The size - /// of packets to be sent will not exceed the supplied mtu. + /// wtth an active session this session is supplied, otherwise this parameter is None and the packet + /// should be a reply to the current incoming packet. The size of packets to be sent will not exceed + /// the supplied mtu. /// /// The check_allow_incoming_session function is called when an initial Noise_XK init message is /// received. This is before anything is known about the caller. A return value of true proceeds @@ -425,14 +424,7 @@ impl Context { let mut incoming = None; if let Some(local_session_id) = SessionId::new_from_u64_le(u64::from_le_bytes(incoming_packet[0..8].try_into().unwrap())) { - if let Some(session) = self - .sessions - .read() - .unwrap() - .active - .get(&local_session_id) - .and_then(|s| s.upgrade()) - { + if let Some(session) = self.sessions.read().unwrap().active.get(&local_session_id).and_then(|s| s.upgrade()) { debug_assert!(!self.sessions.read().unwrap().incoming.contains_key(&local_session_id)); session @@ -443,8 +435,7 @@ impl Context { if session.check_receive_window(incoming_counter) { if fragment_count > 1 { let mut fragged = session.defrag[(incoming_counter as usize) % COUNTER_WINDOW_MAX_OOO].lock().unwrap(); - if let Some(assembled_packet) = fragged.assemble(incoming_counter, incoming_packet_buf, fragment_no, fragment_count) - { + if let Some(assembled_packet) = fragged.assemble(incoming_counter, incoming_packet_buf, fragment_no, fragment_count) { drop(fragged); return self.process_complete_incoming_packet( app, @@ -500,18 +491,36 @@ impl Context { let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_packet); if fragment_count > 1 { - let fragged_m = { + let f = { let mut defrag = self.defrag.lock().unwrap(); - defrag + let f = defrag .entry((source.clone(), incoming_counter)) - .or_insert_with(|| Arc::new(Mutex::new((Fragged::new(), current_time)))) - .clone() + .or_insert_with(|| Arc::new((Mutex::new(Fragged::new()), current_time))) + .clone(); + + // Anti-DOS emergency cleaning of the incoming defragmentation queue for packets not + // associated with known sessions. + if defrag.len() >= self.max_incomplete_session_queue_size { + // First, drop all entries that are timed out or whose physical source duplicates another entry. + let mut sources = HashSet::with_capacity(defrag.len()); + let negotiation_timeout_cutoff = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; + defrag.retain(|k, fragged| (fragged.1 > negotiation_timeout_cutoff && sources.insert(k.0.clone())) || Arc::ptr_eq(fragged, &f)); + + // Then, if we are still at or over the limit, drop 10% of remaining entries at random. + if defrag.len() >= self.max_incomplete_session_queue_size { + let mut rn = random::next_u32_secure(); + defrag.retain(|_, fragged| { + rn = prng32(rn); + rn > (u32::MAX / 10) || Arc::ptr_eq(fragged, &f) + }); + } + } + + f }; - let mut fragged = fragged_m.lock().unwrap(); - if let Some(assembled_packet) = fragged - .0 - .assemble(incoming_counter, incoming_packet_buf, fragment_no, fragment_count) - { + let mut fragged = f.0.lock().unwrap(); + + if let Some(assembled_packet) = fragged.assemble(incoming_counter, incoming_packet_buf, fragment_no, fragment_count) { self.defrag.lock().unwrap().remove(&(source.clone(), incoming_counter)); return self.process_complete_incoming_packet( app, @@ -610,10 +619,7 @@ impl Context { return Err(Error::DataBufferTooSmall); } let payload_end = last_fragment.len() - AES_GCM_TAG_SIZE; - c.crypt( - &last_fragment[HEADER_SIZE..payload_end], - &mut data_buf[current_frag_data_start..data_len], - ); + c.crypt(&last_fragment[HEADER_SIZE..payload_end], &mut data_buf[current_frag_data_start..data_len]); let aead_authentication_ok = c.finish_decrypt(&last_fragment[payload_end..]); key.return_receive_cipher(c); @@ -639,9 +645,8 @@ impl Context { } else { state.current_key = key_index; } - } else { - state.keys[key_index].as_mut().unwrap().confirmed = true; } + state.keys[key_index].as_mut().unwrap().confirmed = true; // If we got a valid data packet from Bob, this means we can cancel any offers // that are still oustanding for initialization. @@ -656,7 +661,7 @@ impl Context { if packet_type == PACKET_TYPE_DATA { return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); } else { - println!("nop"); + return Ok(ReceiveResult::Ok); } } else { return Err(Error::OutOfSequence); @@ -745,10 +750,7 @@ impl Context { let bob_noise_e = bob_noise_e_secret.public_key_bytes().clone(); let noise_es_ee = Secret(hmac_sha512( noise_es.as_bytes(), - bob_noise_e_secret - .agree(&alice_noise_e) - .ok_or(Error::FailedAuthentication)? - .as_bytes(), + bob_noise_e_secret.agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?.as_bytes(), )); let (bob_hk_ciphertext, hk) = pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default()) .map_err(|_| Error::FailedAuthentication) @@ -960,8 +962,7 @@ impl Context { // key exchange. Bob won't be able to do this until he decrypts and parses Alice's // identity, so the first HMAC is to let him authenticate that first. let hmac_es_ee_se_hk_psk = hmac_sha384_2( - kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) - .as_bytes(), + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()).as_bytes(), &reply_message_nonce, &reply_buffer[HEADER_SIZE..reply_len], ); @@ -972,14 +973,8 @@ impl Context { { let mut state = session.state.write().unwrap(); let _ = state.remote_session_id.insert(bob_session_id); - let _ = state.keys[0].insert(SessionKey::new::( - noise_es_ee_se_hk_psk, - 1, - current_time, - 2, - false, - false, - )); + let _ = + state.keys[0].insert(SessionKey::new::(noise_es_ee_se_hk_psk, 1, current_time, 2, false, false)); debug_assert!(state.keys[1].is_none()); state.current_key = 0; state.current_offer = Offer::NoiseXKAck(Box::new(OutgoingSessionAck { @@ -1037,8 +1032,7 @@ impl Context { if !secure_eq( &pkt_assembled[auth_start..pkt_assembled.len() - HMAC_SHA384_SIZE], &hmac_sha384_2( - kbkdf::(incoming.noise_es_ee.as_bytes()) - .as_bytes(), + kbkdf::(incoming.noise_es_ee.as_bytes()).as_bytes(), &incoming_message_nonce, &pkt_assembled[HEADER_SIZE..auth_start], ), @@ -1111,8 +1105,7 @@ impl Context { if !secure_eq( &pkt_assembly_buffer_copy[auth_start + HMAC_SHA384_SIZE..pkt_assembled.len()], &hmac_sha384_2( - kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) - .as_bytes(), + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()).as_bytes(), &incoming_message_nonce, &pkt_assembly_buffer_copy[HEADER_SIZE..auth_start + HMAC_SHA384_SIZE], ), @@ -1130,14 +1123,7 @@ impl Context { state: RwLock::new(State { remote_session_id: Some(incoming.alice_session_id), keys: [ - Some(SessionKey::new::( - noise_es_ee_se_hk_psk, - 1, - current_time, - 2, - true, - true, - )), + Some(SessionKey::new::(noise_es_ee_se_hk_psk, 1, current_time, 2, true, true)), None, ], current_key: 0, @@ -1213,9 +1199,9 @@ impl Context { reply_buf[RekeyAck::AUTH_START..].copy_from_slice(&c.finish_encrypt()); key.return_send_cipher(c); - session.header_protection_cipher.encrypt_block_in_place( - &mut reply_buf[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END], - ); + session + .header_protection_cipher + .encrypt_block_in_place(&mut reply_buf[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); send(Some(&session), &mut reply_buf); // The new "Bob" doesn't know yet if Alice has received the new key, so the @@ -1323,12 +1309,7 @@ impl Session { /// * `mtu_sized_buffer` - A writable work buffer whose size also specifies the physical MTU /// * `data` - Data to send #[inline] - pub fn send( - &self, - mut send: SendFunction, - mtu_sized_buffer: &mut [u8], - mut data: &[u8], - ) -> Result<(), Error> { + pub fn send(&self, mut send: SendFunction, mtu_sized_buffer: &mut [u8], mut data: &[u8]) -> Result<(), Error> { debug_assert!(mtu_sized_buffer.len() >= MIN_TRANSPORT_MTU); let state = self.state.read().unwrap(); if let Some(remote_session_id) = state.remote_session_id { @@ -1338,8 +1319,7 @@ impl Session { let mut c = session_key.get_send_cipher(counter)?; c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_DATA, counter)); - let fragment_count = - (((data.len() + AES_GCM_TAG_SIZE) as f32) / (mtu_sized_buffer.len() - HEADER_SIZE) as f32).ceil() as usize; + let fragment_count = (((data.len() + AES_GCM_TAG_SIZE) as f32) / (mtu_sized_buffer.len() - HEADER_SIZE) as f32).ceil() as usize; let fragment_max_chunk_size = mtu_sized_buffer.len() - HEADER_SIZE; let last_fragment_no = fragment_count - 1; @@ -1392,15 +1372,9 @@ impl Session { c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_NOP, counter)); nop[HEADER_SIZE..].copy_from_slice(&c.finish_encrypt()); session_key.return_send_cipher(c); - set_packet_header( - &mut nop, - 1, - 0, - PACKET_TYPE_NOP, - u64::from(remote_session_id), - state.current_key, - counter, - ); + set_packet_header(&mut nop, 1, 0, PACKET_TYPE_NOP, u64::from(remote_session_id), state.current_key, counter); + self.header_protection_cipher + .encrypt_block_in_place(&mut nop[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); send(&mut nop); } } @@ -1628,8 +1602,7 @@ impl SessionKey { send_cipher_pool: Mutex::new(Vec::with_capacity(2)), rekey_at_time: current_time .checked_add( - Application::REKEY_AFTER_TIME_MS - + ((random::xorshift64_random() as u32) % Application::REKEY_AFTER_TIME_MS_MAX_JITTER) as i64, + Application::REKEY_AFTER_TIME_MS + ((random::xorshift64_random() as u32) % Application::REKEY_AFTER_TIME_MS_MAX_JITTER) as i64, ) .unwrap(), created_at_counter: current_counter, @@ -1715,3 +1688,14 @@ fn kbkdf(key: &[u8]) -> Secret u32 { + // based on lowbias32 from https://nullprogram.com/blog/2018/07/31/ + x = x.wrapping_add(1); // don't get stuck on 0 + x ^= x.wrapping_shr(16); + x = x.wrapping_mul(0x7feb352d); + x ^= x.wrapping_shr(15); + x = x.wrapping_mul(0x846ca68b); + x ^= x.wrapping_shr(16); + x +} From 781b5eb2709bf62de626ce4e1d2b1154506b9dfc Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 3 Mar 2023 09:49:25 -0500 Subject: [PATCH 12/14] Format tweak. --- controller/src/controller.rs | 10 +-- controller/src/filedatabase.rs | 19 ++--- controller/src/postgresdatabase.rs | 27 ++----- crypto/benches/benchmark_crypto.rs | 4 +- crypto/src/aes.rs | 28 ++------ crypto/src/aes_gmac_siv/impl_openssl.rs | 24 ++----- crypto/src/p384.rs | 64 +++++------------ crypto/src/poly1305.rs | 12 ++-- crypto/src/salsa.rs | 10 +-- network-hypervisor/src/vl1/endpoint.rs | 20 ++---- network-hypervisor/src/vl1/identity.rs | 78 ++++++--------------- network-hypervisor/src/vl1/inetaddress.rs | 11 +-- network-hypervisor/src/vl1/mac.rs | 12 +--- network-hypervisor/src/vl1/node.rs | 51 +++----------- network-hypervisor/src/vl1/peer.rs | 33 +++------ network-hypervisor/src/vl2/networkconfig.rs | 23 ++---- service/src/main.rs | 9 +-- utils/src/dictionary.rs | 6 +- utils/src/json.rs | 6 +- utils/src/lib.rs | 5 +- vl1-service/src/constants.rs | 22 +++--- vl1-service/src/sys/udp.rs | 14 +--- vl1-service/src/vl1service.rs | 3 +- vl1-service/src/vl1settings.rs | 3 +- zssp/src/fragged.rs | 8 +-- 25 files changed, 129 insertions(+), 373 deletions(-) diff --git a/controller/src/controller.rs b/controller/src/controller.rs index 32ef810d0..39556d07e 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -67,9 +67,7 @@ impl Controller { recently_authorized: RwLock::new(HashMap::new()), })) } else { - Err(Box::new(InvalidParameterError( - "local controller's identity not readable by database", - ))) + Err(Box::new(InvalidParameterError("local controller's identity not readable by database"))) } } @@ -238,8 +236,7 @@ impl Controller { if member.node_id != *m { if let Some(peer) = self.service.read().unwrap().upgrade().and_then(|s| s.node().peer(*m)) { revocations.clear(); - Revocation::new(member.network_id, time_clock, member.node_id, *m, &self.local_identity, false) - .map(|r| revocations.push(r)); + Revocation::new(member.network_id, time_clock, member.node_id, *m, &self.local_identity, false).map(|r| revocations.push(r)); self.send_revocations(&peer, &mut revocations); } } @@ -565,8 +562,7 @@ impl InnerProtocolLayer for Controller { }; // Launch handler as an async background task. - let (self2, source, source_remote_endpoint) = - (self.self_ref.upgrade().unwrap(), source.clone(), source_path.endpoint.clone()); + let (self2, source, source_remote_endpoint) = (self.self_ref.upgrade().unwrap(), source.clone(), source_path.endpoint.clone()); self.reaper.add( self.runtime.spawn(async move { let node_id = source.identity.address; diff --git a/controller/src/filedatabase.rs b/controller/src/filedatabase.rs index 6fb5d9e68..f4b5aeecc 100644 --- a/controller/src/filedatabase.rs +++ b/controller/src/filedatabase.rs @@ -50,8 +50,8 @@ impl FileDatabase { let db_weak = db_weak_tmp.clone(); let runtime2 = runtime.clone(); - let local_identity = load_node_identity(base_path.as_path()) - .ok_or(std::io::Error::new(std::io::ErrorKind::NotFound, "identity.secret not found"))?; + let local_identity = + load_node_identity(base_path.as_path()).ok_or(std::io::Error::new(std::io::ErrorKind::NotFound, "identity.secret not found"))?; let controller_address = local_identity.address; let db = Arc::new(Self { @@ -144,8 +144,7 @@ impl FileDatabase { .send(Change::NetworkChanged(old_network, new_network)); } (true, None) => { - let _ = - db.change_sender.send(Change::NetworkCreated(new_network)); + let _ = db.change_sender.send(Change::NetworkCreated(new_network)); } _ => {} } @@ -155,13 +154,11 @@ impl FileDatabase { if let Ok(Some(new_member)) = Self::load_object::(changed).await { match db.cache.on_member_updated(new_member.clone()) { (true, Some(old_member)) => { - let _ = db - .change_sender - .send(Change::MemberChanged(old_member, new_member)); + let _ = + db.change_sender.send(Change::MemberChanged(old_member, new_member)); } (true, None) => { - let _ = - db.change_sender.send(Change::MemberCreated(new_member)); + let _ = db.change_sender.send(Change::MemberCreated(new_member)); } _ => {} } @@ -315,9 +312,7 @@ impl Database for FileDatabase { if ent.file_type().await.map_or(false, |t| t.is_file() || t.is_symlink()) { let osname = ent.file_name(); let name = osname.to_string_lossy(); - if name.len() == (zerotier_network_hypervisor::protocol::ADDRESS_SIZE_STRING + 6) - && name.starts_with("M") - && name.ends_with(".yaml") + if name.len() == (zerotier_network_hypervisor::protocol::ADDRESS_SIZE_STRING + 6) && name.starts_with("M") && name.ends_with(".yaml") { if let Ok(member_address) = u64::from_str_radix(&name[1..11], 16) { if let Some(member_address) = Address::from_u64(member_address) { diff --git a/controller/src/postgresdatabase.rs b/controller/src/postgresdatabase.rs index 8b48ab8c5..379b44a6e 100644 --- a/controller/src/postgresdatabase.rs +++ b/controller/src/postgresdatabase.rs @@ -88,18 +88,13 @@ impl PostgresConnection { let (client, connection) = tokio_postgres::connect(postgres_path, tokio_postgres::NoTls).await?; Ok(Box::new(Self { s_list_networks: client - .prepare_typed( - "SELECT id FROM ztc_network WHERE controller_id = $1 AND deleted = false", - &[Type::TEXT], - ) + .prepare_typed("SELECT id FROM ztc_network WHERE controller_id = $1 AND deleted = false", &[Type::TEXT]) .await?, s_list_members: client .prepare_typed("SELECT id FROM ztc_member WHERE network_id = $1 AND deleted = false", &[Type::TEXT]) .await?, s_get_network: client.prepare_typed(GET_NETWORK_SQL, &[Type::TEXT]).await?, - s_get_network_members_with_capabilities: client - .prepare_typed(GET_NETWORK_MEMBERS_WITH_CAPABILITIES_SQL, &[Type::TEXT]) - .await?, + s_get_network_members_with_capabilities: client.prepare_typed(GET_NETWORK_MEMBERS_WITH_CAPABILITIES_SQL, &[Type::TEXT]).await?, client, connection_task: runtime.spawn(async move { if let Err(e) = connection.await { @@ -144,12 +139,7 @@ pub struct PostgresDatabase { } impl PostgresDatabase { - pub async fn new( - runtime: Handle, - postgres_path: String, - num_connections: usize, - local_identity: Valid, - ) -> Result, Error> { + pub async fn new(runtime: Handle, postgres_path: String, num_connections: usize, local_identity: Valid) -> Result, Error> { assert!(num_connections > 0); let (sender, _) = channel(4096); let mut connections = Vec::with_capacity(num_connections); @@ -217,11 +207,7 @@ impl Database for PostgresDatabase { let c = self.get_connection().await?; let network_id_string = id.to_string(); if let Some(r) = c.client.query_opt(&c.s_get_network, &[&network_id_string]).await? { - if let Ok(with_caps) = c - .client - .query(&c.s_get_network_members_with_capabilities, &[&network_id_string]) - .await - { + if let Ok(with_caps) = c.client.query(&c.s_get_network_members_with_capabilities, &[&network_id_string]).await { (r, with_caps) } else { (r, Vec::new()) @@ -341,10 +327,7 @@ impl Database for PostgresDatabase { if let Ok(member_id) = Address::from_str(wc.get(0)) { if let Ok(cap_ids) = serde_json::from_str::>(wc.get(1)) { for cap_id in cap_ids.iter() { - members_by_cap - .entry(*cap_id) - .or_insert_with(|| Vec::with_capacity(4)) - .push(member_id); + members_by_cap.entry(*cap_id).or_insert_with(|| Vec::with_capacity(4)).push(member_id); } } } diff --git a/crypto/benches/benchmark_crypto.rs b/crypto/benches/benchmark_crypto.rs index fd01855c9..2157322aa 100644 --- a/crypto/benches/benchmark_crypto.rs +++ b/crypto/benches/benchmark_crypto.rs @@ -31,9 +31,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { group.measurement_time(Duration::new(10, 0)); - group.bench_function("ecdhp384", |b| { - b.iter(|| p384_a.agree(p384_b.public_key()).expect("ecdhp384 failed")) - }); + group.bench_function("ecdhp384", |b| b.iter(|| p384_a.agree(p384_b.public_key()).expect("ecdhp384 failed"))); group.bench_function("ecdhx25519", |b| b.iter(|| x25519_a.agree(&x25519_b_pub))); group.finish(); diff --git a/crypto/src/aes.rs b/crypto/src/aes.rs index 5d0cd2c32..54be9e016 100644 --- a/crypto/src/aes.rs +++ b/crypto/src/aes.rs @@ -135,14 +135,7 @@ mod fruit_flavored { assert_eq!(data.len(), 16); unsafe { let mut data_out_written = 0; - CCCryptorUpdate( - self.0, - data.as_ptr().cast(), - 16, - data.as_mut_ptr().cast(), - 16, - &mut data_out_written, - ); + CCCryptorUpdate(self.0, data.as_ptr().cast(), 16, data.as_mut_ptr().cast(), 16, &mut data_out_written); } } @@ -168,14 +161,7 @@ mod fruit_flavored { assert_eq!(data.len(), 16); unsafe { let mut data_out_written = 0; - CCCryptorUpdate( - self.1, - data.as_ptr().cast(), - 16, - data.as_mut_ptr().cast(), - 16, - &mut data_out_written, - ); + CCCryptorUpdate(self.1, data.as_ptr().cast(), 16, data.as_mut_ptr().cast(), 16, &mut data_out_written); } } } @@ -357,15 +343,9 @@ mod fruit_flavored { pub fn crypt_in_place(&mut self, data: &mut [u8]) { unsafe { if self.1 { - assert_eq!( - CCCryptorGCMEncrypt(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast()), - 0 - ); + assert_eq!(CCCryptorGCMEncrypt(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast()), 0); } else { - assert_eq!( - CCCryptorGCMDecrypt(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast()), - 0 - ); + assert_eq!(CCCryptorGCMDecrypt(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast()), 0); } } } diff --git a/crypto/src/aes_gmac_siv/impl_openssl.rs b/crypto/src/aes_gmac_siv/impl_openssl.rs index edd856fd9..7615aafd7 100644 --- a/crypto/src/aes_gmac_siv/impl_openssl.rs +++ b/crypto/src/aes_gmac_siv/impl_openssl.rs @@ -178,15 +178,9 @@ impl AesGmacSiv { self.tmp.copy_from_slice(&tag_tmp[0..16]); self.tmp[12] &= 0x7f; - let _ = self.ctr.replace( - Crypter::new( - aes_ctr_by_key_size(self.k1.len()), - Mode::Encrypt, - self.k1.as_slice(), - Some(&self.tmp), - ) - .unwrap(), - ); + let _ = self + .ctr + .replace(Crypter::new(aes_ctr_by_key_size(self.k1.len()), Mode::Encrypt, self.k1.as_slice(), Some(&self.tmp)).unwrap()); } /// Feed plaintext for second pass and write ciphertext to supplied buffer. @@ -219,15 +213,9 @@ impl AesGmacSiv { pub fn decrypt_init(&mut self, tag: &[u8]) { self.tmp.copy_from_slice(tag); self.tmp[12] &= 0x7f; - let _ = self.ctr.replace( - Crypter::new( - aes_ctr_by_key_size(self.k1.len()), - Mode::Decrypt, - self.k1.as_slice(), - Some(&self.tmp), - ) - .unwrap(), - ); + let _ = self + .ctr + .replace(Crypter::new(aes_ctr_by_key_size(self.k1.len()), Mode::Decrypt, self.k1.as_slice(), Some(&self.tmp)).unwrap()); let mut tag_tmp = [0_u8; 32]; let mut ecb = Crypter::new(aes_ecb_by_key_size(self.k1.len()), Mode::Decrypt, self.k1.as_slice(), None).unwrap(); diff --git a/crypto/src/p384.rs b/crypto/src/p384.rs index 4baea80d9..28f4a0066 100644 --- a/crypto/src/p384.rs +++ b/crypto/src/p384.rs @@ -55,9 +55,7 @@ mod openssl_based { impl P384PublicKey { fn new_from_point(key: &EcPointRef) -> Self { let mut bnc = BigNumContext::new().unwrap(); - let kb = key - .to_bytes(GROUP_P384.as_ref(), PointConversionForm::COMPRESSED, &mut bnc) - .unwrap(); + let kb = key.to_bytes(GROUP_P384.as_ref(), PointConversionForm::COMPRESSED, &mut bnc).unwrap(); let mut bytes = [0_u8; 49]; bytes[(49 - kb.len())..].copy_from_slice(kb.as_slice()); Self { @@ -599,17 +597,12 @@ mod builtin { let mut i: uint = 0; /* p = c0 */ vli_clear(l_tmp.as_mut_ptr()); vli_clear(l_tmp.as_mut_ptr().offset((48 as libc::c_int / 8 as libc::c_int) as isize)); - omega_mult( - l_tmp.as_mut_ptr(), - p_product.offset((48 as libc::c_int / 8 as libc::c_int) as isize), - ); + omega_mult(l_tmp.as_mut_ptr(), p_product.offset((48 as libc::c_int / 8 as libc::c_int) as isize)); vli_clear(p_product.offset((48 as libc::c_int / 8 as libc::c_int) as isize)); /* (c1, c0) = c0 + w * c1 */ i = 0 as libc::c_int as uint; while i < (48 as libc::c_int / 8 as libc::c_int + 3 as libc::c_int) as libc::c_uint { - let mut l_sum: uint64_t = (*p_product.offset(i as isize)) - .wrapping_add(l_tmp[i as usize]) - .wrapping_add(l_carry); + let mut l_sum: uint64_t = (*p_product.offset(i as isize)).wrapping_add(l_tmp[i as usize]).wrapping_add(l_carry); if l_sum != *p_product.offset(i as isize) { l_carry = (l_sum < *p_product.offset(i as isize)) as libc::c_int as uint64_t } @@ -855,12 +848,7 @@ mod builtin { vli_set(X1, t7.as_mut_ptr()); } - unsafe fn EccPoint_mult( - mut p_result: *mut EccPoint, - mut p_point: *mut EccPoint, - mut p_scalar: *mut uint64_t, - mut p_initialZ: *mut uint64_t, - ) { + unsafe fn EccPoint_mult(mut p_result: *mut EccPoint, mut p_point: *mut EccPoint, mut p_scalar: *mut uint64_t, mut p_initialZ: *mut uint64_t) { /* R0 and R1 */ let mut Rx: [[uint64_t; 6]; 2] = std::mem::MaybeUninit::uninit().assume_init(); let mut Ry: [[uint64_t; 6]; 2] = std::mem::MaybeUninit::uninit().assume_init(); @@ -1039,20 +1027,12 @@ mod builtin { } } ecc_native2bytes(p_privateKey, l_private.as_mut_ptr() as *const uint64_t); - ecc_native2bytes( - p_publicKey.offset(1 as libc::c_int as isize), - l_public.x.as_mut_ptr() as *const uint64_t, - ); - *p_publicKey.offset(0 as libc::c_int as isize) = (2 as libc::c_int as libc::c_ulong) - .wrapping_add(l_public.y[0 as libc::c_int as usize] & 0x1 as libc::c_int as libc::c_ulong) - as uint8_t; + ecc_native2bytes(p_publicKey.offset(1 as libc::c_int as isize), l_public.x.as_mut_ptr() as *const uint64_t); + *p_publicKey.offset(0 as libc::c_int as isize) = + (2 as libc::c_int as libc::c_ulong).wrapping_add(l_public.y[0 as libc::c_int as usize] & 0x1 as libc::c_int as libc::c_ulong) as uint8_t; return 1 as libc::c_int; } - pub unsafe fn ecdh_shared_secret( - mut p_publicKey: *const uint8_t, - mut p_privateKey: *const uint8_t, - mut p_secret: *mut uint8_t, - ) -> libc::c_int { + pub unsafe fn ecdh_shared_secret(mut p_publicKey: *const uint8_t, mut p_privateKey: *const uint8_t, mut p_secret: *mut uint8_t) -> libc::c_int { let mut l_public: EccPoint = std::mem::MaybeUninit::uninit().assume_init(); let mut l_private: [uint64_t; 6] = std::mem::MaybeUninit::uninit().assume_init(); let mut l_random: [uint64_t; 6] = std::mem::MaybeUninit::uninit().assume_init(); @@ -1079,8 +1059,7 @@ mod builtin { vli_mult(l_product.as_mut_ptr(), p_left, p_right); l_productBits = vli_numBits(l_product.as_mut_ptr().offset((48 as libc::c_int / 8 as libc::c_int) as isize)); if l_productBits != 0 { - l_productBits = (l_productBits as libc::c_uint) - .wrapping_add((48 as libc::c_int / 8 as libc::c_int * 64 as libc::c_int) as libc::c_uint) + l_productBits = (l_productBits as libc::c_uint).wrapping_add((48 as libc::c_int / 8 as libc::c_int * 64 as libc::c_int) as libc::c_uint) as uint as uint } else { l_productBits = vli_numBits(l_product.as_mut_ptr()) @@ -1094,12 +1073,8 @@ mod builtin { power of two possible while still resulting in a number less than p_left. */ vli_clear(l_modMultiple.as_mut_ptr()); vli_clear(l_modMultiple.as_mut_ptr().offset((48 as libc::c_int / 8 as libc::c_int) as isize)); - l_digitShift = l_productBits - .wrapping_sub(l_modBits) - .wrapping_div(64 as libc::c_int as libc::c_uint); - l_bitShift = l_productBits - .wrapping_sub(l_modBits) - .wrapping_rem(64 as libc::c_int as libc::c_uint); + l_digitShift = l_productBits.wrapping_sub(l_modBits).wrapping_div(64 as libc::c_int as libc::c_uint); + l_bitShift = l_productBits.wrapping_sub(l_modBits).wrapping_rem(64 as libc::c_int as libc::c_uint); if l_bitShift != 0 { l_modMultiple[l_digitShift.wrapping_add((48 as libc::c_int / 8 as libc::c_int) as libc::c_uint) as usize] = vli_lshift(l_modMultiple.as_mut_ptr().offset(l_digitShift as isize), p_mod, l_bitShift) @@ -1186,11 +1161,7 @@ mod builtin { ecc_native2bytes(p_signature.offset(48 as libc::c_int as isize), l_s.as_mut_ptr() as *const uint64_t); return 1 as libc::c_int; } - pub unsafe fn ecdsa_verify( - mut p_publicKey: *const uint8_t, - mut p_hash: *const uint8_t, - mut p_signature: *const uint8_t, - ) -> libc::c_int { + pub unsafe fn ecdsa_verify(mut p_publicKey: *const uint8_t, mut p_hash: *const uint8_t, mut p_signature: *const uint8_t) -> libc::c_int { let mut u1: [uint64_t; 6] = std::mem::MaybeUninit::uninit().assume_init(); let mut u2: [uint64_t; 6] = std::mem::MaybeUninit::uninit().assume_init(); let mut z: [uint64_t; 6] = std::mem::MaybeUninit::uninit().assume_init(); @@ -1210,8 +1181,7 @@ mod builtin { /* r, s must not be 0. */ return 0 as libc::c_int; } - if vli_cmp(curve_n.as_mut_ptr(), l_r.as_mut_ptr()) != 1 as libc::c_int - || vli_cmp(curve_n.as_mut_ptr(), l_s.as_mut_ptr()) != 1 as libc::c_int + if vli_cmp(curve_n.as_mut_ptr(), l_r.as_mut_ptr()) != 1 as libc::c_int || vli_cmp(curve_n.as_mut_ptr(), l_s.as_mut_ptr()) != 1 as libc::c_int { /* r, s must be < n. */ return 0 as libc::c_int; @@ -1233,10 +1203,10 @@ mod builtin { /* Use Shamir's trick to calculate u1*G + u2*Q */ let mut l_points: [*mut EccPoint; 4] = [0 as *mut EccPoint, &mut curve_G, &mut l_public, &mut l_sum]; /* Z = x2 - x1 */ let mut l_numBits: uint = umax(vli_numBits(u1.as_mut_ptr()), vli_numBits(u2.as_mut_ptr())); /* Z = 1/Z */ - let mut l_point: *mut EccPoint = l_points[((vli_testBit(u1.as_mut_ptr(), l_numBits.wrapping_sub(1 as libc::c_int as libc::c_uint)) - != 0) as libc::c_int - | ((vli_testBit(u2.as_mut_ptr(), l_numBits.wrapping_sub(1 as libc::c_int as libc::c_uint)) != 0) as libc::c_int) - << 1 as libc::c_int) as usize]; + let mut l_point: *mut EccPoint = l_points[((vli_testBit(u1.as_mut_ptr(), l_numBits.wrapping_sub(1 as libc::c_int as libc::c_uint)) != 0) + as libc::c_int + | ((vli_testBit(u2.as_mut_ptr(), l_numBits.wrapping_sub(1 as libc::c_int as libc::c_uint)) != 0) as libc::c_int) << 1 as libc::c_int) + as usize]; vli_set(rx.as_mut_ptr(), (*l_point).x.as_mut_ptr()); vli_set(ry.as_mut_ptr(), (*l_point).y.as_mut_ptr()); vli_clear(z.as_mut_ptr()); diff --git a/crypto/src/poly1305.rs b/crypto/src/poly1305.rs index b34da874c..b49183ab0 100644 --- a/crypto/src/poly1305.rs +++ b/crypto/src/poly1305.rs @@ -20,12 +20,12 @@ mod tests { use crate::poly1305::*; const TV0_INPUT: [u8; 32] = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; const TV0_KEY: [u8; 32] = [ - 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x33, 0x32, 0x2d, 0x62, 0x79, 0x74, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x66, 0x6f, - 0x72, 0x20, 0x50, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x33, 0x32, 0x2d, 0x62, 0x79, 0x74, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x66, 0x6f, 0x72, + 0x20, 0x50, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, ]; const TV0_TAG: [u8; 16] = [ 0x49, 0xec, 0x78, 0x09, 0x0e, 0x48, 0x1e, 0xc6, 0xc2, 0x6b, 0x33, 0xb9, 0x1c, 0xcc, 0x03, 0x07, @@ -33,8 +33,8 @@ mod tests { const TV1_INPUT: [u8; 12] = [0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21]; const TV1_KEY: [u8; 32] = [ - 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x33, 0x32, 0x2d, 0x62, 0x79, 0x74, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x66, 0x6f, - 0x72, 0x20, 0x50, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x33, 0x32, 0x2d, 0x62, 0x79, 0x74, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x66, 0x6f, 0x72, + 0x20, 0x50, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, ]; const TV1_TAG: [u8; 16] = [ 0xa6, 0xf7, 0x45, 0x00, 0x8f, 0x81, 0xc9, 0x16, 0xa2, 0x0d, 0xcc, 0x74, 0xee, 0xf2, 0xb2, 0xf0, diff --git a/crypto/src/salsa.rs b/crypto/src/salsa.rs index 0711ba07c..7c1f79b30 100644 --- a/crypto/src/salsa.rs +++ b/crypto/src/salsa.rs @@ -242,14 +242,14 @@ mod tests { use crate::salsa::*; const SALSA_20_TV0_KEY: [u8; 32] = [ - 0x0f, 0x62, 0xb5, 0x08, 0x5b, 0xae, 0x01, 0x54, 0xa7, 0xfa, 0x4d, 0xa0, 0xf3, 0x46, 0x99, 0xec, 0x3f, 0x92, 0xe5, 0x38, 0x8b, 0xde, - 0x31, 0x84, 0xd7, 0x2a, 0x7d, 0xd0, 0x23, 0x76, 0xc9, 0x1c, + 0x0f, 0x62, 0xb5, 0x08, 0x5b, 0xae, 0x01, 0x54, 0xa7, 0xfa, 0x4d, 0xa0, 0xf3, 0x46, 0x99, 0xec, 0x3f, 0x92, 0xe5, 0x38, 0x8b, 0xde, 0x31, + 0x84, 0xd7, 0x2a, 0x7d, 0xd0, 0x23, 0x76, 0xc9, 0x1c, ]; const SALSA_20_TV0_IV: [u8; 8] = [0x28, 0x8f, 0xf6, 0x5d, 0xc4, 0x2b, 0x92, 0xf9]; const SALSA_20_TV0_KS: [u8; 64] = [ - 0x5e, 0x5e, 0x71, 0xf9, 0x01, 0x99, 0x34, 0x03, 0x04, 0xab, 0xb2, 0x2a, 0x37, 0xb6, 0x62, 0x5b, 0xf8, 0x83, 0xfb, 0x89, 0xce, 0x3b, - 0x21, 0xf5, 0x4a, 0x10, 0xb8, 0x10, 0x66, 0xef, 0x87, 0xda, 0x30, 0xb7, 0x76, 0x99, 0xaa, 0x73, 0x79, 0xda, 0x59, 0x5c, 0x77, 0xdd, - 0x59, 0x54, 0x2d, 0xa2, 0x08, 0xe5, 0x95, 0x4f, 0x89, 0xe4, 0x0e, 0xb7, 0xaa, 0x80, 0xa8, 0x4a, 0x61, 0x76, 0x66, 0x3f, + 0x5e, 0x5e, 0x71, 0xf9, 0x01, 0x99, 0x34, 0x03, 0x04, 0xab, 0xb2, 0x2a, 0x37, 0xb6, 0x62, 0x5b, 0xf8, 0x83, 0xfb, 0x89, 0xce, 0x3b, 0x21, + 0xf5, 0x4a, 0x10, 0xb8, 0x10, 0x66, 0xef, 0x87, 0xda, 0x30, 0xb7, 0x76, 0x99, 0xaa, 0x73, 0x79, 0xda, 0x59, 0x5c, 0x77, 0xdd, 0x59, 0x54, + 0x2d, 0xa2, 0x08, 0xe5, 0x95, 0x4f, 0x89, 0xe4, 0x0e, 0xb7, 0xaa, 0x80, 0xa8, 0x4a, 0x61, 0x76, 0x66, 0x3f, ]; #[test] diff --git a/network-hypervisor/src/vl1/endpoint.rs b/network-hypervisor/src/vl1/endpoint.rs index ed43805f8..21a612f65 100644 --- a/network-hypervisor/src/vl1/endpoint.rs +++ b/network-hypervisor/src/vl1/endpoint.rs @@ -216,10 +216,7 @@ impl Marshalable for Endpoint { TYPE_NIL => Ok(Endpoint::Nil), TYPE_ZEROTIER => { let zt = Address::from_bytes_fixed(buf.read_bytes_fixed(cursor)?).ok_or(UnmarshalError::InvalidData)?; - Ok(Endpoint::ZeroTier( - zt, - buf.read_bytes_fixed::(cursor)?.clone(), - )) + Ok(Endpoint::ZeroTier(zt, buf.read_bytes_fixed::(cursor)?.clone())) } TYPE_ETHERNET => Ok(Endpoint::Ethernet(MAC::unmarshal(buf, cursor)?)), TYPE_WIFIDIRECT => Ok(Endpoint::WifiDirect(MAC::unmarshal(buf, cursor)?)), @@ -230,9 +227,7 @@ impl Marshalable for Endpoint { TYPE_HTTP => Ok(Endpoint::Http( String::from_utf8_lossy(buf.read_bytes(buf.read_varint(cursor)? as usize, cursor)?).to_string(), )), - TYPE_WEBRTC => Ok(Endpoint::WebRTC( - buf.read_bytes(buf.read_varint(cursor)? as usize, cursor)?.to_vec(), - )), + TYPE_WEBRTC => Ok(Endpoint::WebRTC(buf.read_bytes(buf.read_varint(cursor)? as usize, cursor)?.to_vec())), TYPE_ZEROTIER_ENCAP => { let zt = Address::from_bytes_fixed(buf.read_bytes_fixed(cursor)?).ok_or(UnmarshalError::InvalidData)?; Ok(Endpoint::ZeroTierEncap(zt, buf.read_bytes_fixed(cursor)?.clone())) @@ -361,10 +356,7 @@ impl FromStr for Endpoint { if endpoint_type == "zt" { return Ok(Endpoint::ZeroTier(Address::from_str(address)?, hash.as_slice().try_into().unwrap())); } else { - return Ok(Endpoint::ZeroTierEncap( - Address::from_str(address)?, - hash.as_slice().try_into().unwrap(), - )); + return Ok(Endpoint::ZeroTierEncap(Address::from_str(address)?, hash.as_slice().try_into().unwrap())); } } } @@ -587,11 +579,7 @@ mod tests { let inet = crate::vl1::InetAddress::from_ip_port(&v, 1234); - for e in [ - Endpoint::Icmp(inet.clone()), - Endpoint::IpTcp(inet.clone()), - Endpoint::IpUdp(inet.clone()), - ] { + for e in [Endpoint::Icmp(inet.clone()), Endpoint::IpTcp(inet.clone()), Endpoint::IpUdp(inet.clone())] { let mut buf = Buffer::<20>::new(); let res = e.marshal(&mut buf); diff --git a/network-hypervisor/src/vl1/identity.rs b/network-hypervisor/src/vl1/identity.rs index ac3793ce5..9b1a33da2 100644 --- a/network-hypervisor/src/vl1/identity.rs +++ b/network-hypervisor/src/vl1/identity.rs @@ -222,9 +222,7 @@ impl Identity { /// It would be possible to change this in the future, with care. pub fn upgrade(&mut self) -> Result { if self.secret.is_none() { - return Err(InvalidParameterError( - "an identity can only be upgraded if it includes its private key", - )); + return Err(InvalidParameterError("an identity can only be upgraded if it includes its private key")); } if self.p384.is_none() { let p384_ecdh = P384KeyPair::generate(); @@ -292,9 +290,8 @@ impl Identity { /// This is somewhat time consuming due to the memory-intensive work algorithm. pub fn validate(self) -> Option> { if let Some(p384) = self.p384.as_ref() { - let mut self_sign_buf: Vec = Vec::with_capacity( - ADDRESS_SIZE + 4 + C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE, - ); + let mut self_sign_buf: Vec = + Vec::with_capacity(ADDRESS_SIZE + 4 + C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE); let _ = self_sign_buf.write_all(&self.address.to_bytes()); let _ = self_sign_buf.write_all(&self.x25519); let _ = self_sign_buf.write_all(&self.ed25519); @@ -331,11 +328,7 @@ impl Identity { /// /// This does NOT validate either identity. Ensure that validation has been performed. pub fn is_upgraded_from(&self, other: &Identity) -> bool { - self.address == other.address - && self.x25519 == other.x25519 - && self.ed25519 == other.ed25519 - && self.p384.is_some() - && other.p384.is_none() + self.address == other.address && self.x25519 == other.x25519 && self.ed25519 == other.ed25519 && self.p384.is_some() && other.p384.is_none() } /// Perform ECDH key agreement, returning a shared secret or None on error. @@ -493,13 +486,12 @@ impl Identity { s.push(':'); } s.push_str(":2:"); // 2 == IDENTITY_ALGORITHM_EC_NIST_P384 - let p384_joined: [u8; P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE] = - concat_arrays_4( - p384.ecdh.as_bytes(), - p384.ecdsa.as_bytes(), - &p384.ecdsa_self_signature, - &p384.ed25519_self_signature, - ); + let p384_joined: [u8; P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE] = concat_arrays_4( + p384.ecdh.as_bytes(), + p384.ecdsa.as_bytes(), + &p384.ecdsa_self_signature, + &p384.ed25519_self_signature, + ); s.push_str(base64_encode_url_nopad(&p384_joined).as_str()); if self.secret.is_some() && include_private { let secret = self.secret.as_ref().unwrap(); @@ -600,9 +592,7 @@ impl FromStr for Identity { if keys[0].len() != C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE { return Err(InvalidFormatError); } - if !keys[2].is_empty() - && keys[2].len() != P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE - { + if !keys[2].is_empty() && keys[2].len() != P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE { return Err(InvalidFormatError); } if !keys[3].is_empty() && keys[3].len() != P384_SECRET_KEY_SIZE + P384_SECRET_KEY_SIZE { @@ -632,8 +622,7 @@ impl FromStr for Identity { Some(IdentityP384Public { ecdh: ecdh.unwrap(), ecdsa: ecdsa.unwrap(), - ecdsa_self_signature: keys[2].as_slice() - [(P384_PUBLIC_KEY_SIZE * 2)..((P384_PUBLIC_KEY_SIZE * 2) + P384_ECDSA_SIGNATURE_SIZE)] + ecdsa_self_signature: keys[2].as_slice()[(P384_PUBLIC_KEY_SIZE * 2)..((P384_PUBLIC_KEY_SIZE * 2) + P384_ECDSA_SIGNATURE_SIZE)] .try_into() .unwrap(), ed25519_self_signature: keys[2].as_slice()[((P384_PUBLIC_KEY_SIZE * 2) + P384_ECDSA_SIGNATURE_SIZE)..] @@ -667,10 +656,8 @@ impl FromStr for Identity { } else { Some(IdentityP384Secret { ecdh: { - let tmp = P384KeyPair::from_bytes( - &keys[2].as_slice()[..P384_PUBLIC_KEY_SIZE], - &keys[3].as_slice()[..P384_SECRET_KEY_SIZE], - ); + let tmp = + P384KeyPair::from_bytes(&keys[2].as_slice()[..P384_PUBLIC_KEY_SIZE], &keys[3].as_slice()[..P384_SECRET_KEY_SIZE]); if tmp.is_none() { return Err(InvalidFormatError); } @@ -711,16 +698,8 @@ impl Marshalable for Identity { let x25519 = buf.read_bytes_fixed::(cursor)?; let ed25519 = buf.read_bytes_fixed::(cursor)?; - let ( - mut ecdh, - mut ecdsa, - mut ecdsa_self_signature, - mut ed25519_self_signature, - mut x25519_s, - mut ed25519_s, - mut ecdh_s, - mut ecdsa_s, - ) = (None, None, None, None, None, None, None, None); + let (mut ecdh, mut ecdsa, mut ecdsa_self_signature, mut ed25519_self_signature, mut x25519_s, mut ed25519_s, mut ecdh_s, mut ecdsa_s) = + (None, None, None, None, None, None, None, None); if type_flags == 0 { const C25519_SECRETS_SIZE: u8 = (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8; @@ -736,9 +715,7 @@ impl Marshalable for Identity { _ => return Err(UnmarshalError::InvalidData), } } else { - if (type_flags & (Self::ALGORITHM_X25519 | Self::FLAG_INCLUDES_SECRETS)) - == (Self::ALGORITHM_X25519 | Self::FLAG_INCLUDES_SECRETS) - { + if (type_flags & (Self::ALGORITHM_X25519 | Self::FLAG_INCLUDES_SECRETS)) == (Self::ALGORITHM_X25519 | Self::FLAG_INCLUDES_SECRETS) { x25519_s = Some(buf.read_bytes_fixed::(cursor)?); ed25519_s = Some(buf.read_bytes_fixed::(cursor)?); } @@ -772,17 +749,12 @@ impl Marshalable for Identity { secret: if let Some(x25519_s) = x25519_s { Some(IdentitySecret { x25519: X25519KeyPair::from_bytes(x25519, x25519_s).ok_or(UnmarshalError::InvalidData)?, - ed25519: Ed25519KeyPair::from_bytes(ed25519, ed25519_s.ok_or(UnmarshalError::InvalidData)?) - .ok_or(UnmarshalError::InvalidData)?, + ed25519: Ed25519KeyPair::from_bytes(ed25519, ed25519_s.ok_or(UnmarshalError::InvalidData)?).ok_or(UnmarshalError::InvalidData)?, p384: if let Some(ecdh_s) = ecdh_s { Some(IdentityP384Secret { - ecdh: P384KeyPair::from_bytes(ecdh.ok_or(UnmarshalError::InvalidData)?, ecdh_s) + ecdh: P384KeyPair::from_bytes(ecdh.ok_or(UnmarshalError::InvalidData)?, ecdh_s).ok_or(UnmarshalError::InvalidData)?, + ecdsa: P384KeyPair::from_bytes(ecdsa.ok_or(UnmarshalError::InvalidData)?, ecdsa_s.ok_or(UnmarshalError::InvalidData)?) .ok_or(UnmarshalError::InvalidData)?, - ecdsa: P384KeyPair::from_bytes( - ecdsa.ok_or(UnmarshalError::InvalidData)?, - ecdsa_s.ok_or(UnmarshalError::InvalidData)?, - ) - .ok_or(UnmarshalError::InvalidData)?, }) } else { None @@ -810,9 +782,7 @@ impl Eq for Identity {} impl Ord for Identity { #[inline(always)] fn cmp(&self, other: &Self) -> Ordering { - self.address - .cmp(&other.address) - .then_with(|| self.fingerprint.cmp(&other.fingerprint)) + self.address.cmp(&other.address).then_with(|| self.fingerprint.cmp(&other.fingerprint)) } } @@ -941,11 +911,7 @@ mod tests { let gen_unmarshaled = Identity::from_bytes(bytes.as_bytes()).unwrap(); assert!(gen_unmarshaled.secret.is_some()); if !gen_unmarshaled.eq(&gen) { - println!( - "{} != {}", - hex::to_string(&gen_unmarshaled.fingerprint), - hex::to_string(&gen.fingerprint) - ); + println!("{} != {}", hex::to_string(&gen_unmarshaled.fingerprint), hex::to_string(&gen.fingerprint)); } assert!(Identity::from_str(string.as_str()).unwrap().secret.is_some()); diff --git a/network-hypervisor/src/vl1/inetaddress.rs b/network-hypervisor/src/vl1/inetaddress.rs index c95de313e..bbfed8651 100644 --- a/network-hypervisor/src/vl1/inetaddress.rs +++ b/network-hypervisor/src/vl1/inetaddress.rs @@ -892,11 +892,7 @@ impl Marshalable for InetAddress { AF_INET6 => { let b = buf.append_bytes_fixed_get_mut::<19>()?; b[0] = 6; - copy_nonoverlapping( - (&(self.sin6.sin6_addr) as *const in6_addr).cast::(), - b.as_mut_ptr().offset(1), - 16, - ); + copy_nonoverlapping((&(self.sin6.sin6_addr) as *const in6_addr).cast::(), b.as_mut_ptr().offset(1), 16); b[17] = *(&self.sin6.sin6_port as *const u16).cast::(); b[18] = *(&self.sin6.sin6_port as *const u16).cast::().offset(1); } @@ -913,10 +909,7 @@ impl Marshalable for InetAddress { Ok(InetAddress::from_ip_port(&b[0..4], u16::from_be_bytes(b[4..6].try_into().unwrap()))) } else if t == 6 { let b: &[u8; 18] = buf.read_bytes_fixed(cursor)?; - Ok(InetAddress::from_ip_port( - &b[0..16], - u16::from_be_bytes(b[16..18].try_into().unwrap()), - )) + Ok(InetAddress::from_ip_port(&b[0..16], u16::from_be_bytes(b[16..18].try_into().unwrap()))) } else { Ok(InetAddress::new()) } diff --git a/network-hypervisor/src/vl1/mac.rs b/network-hypervisor/src/vl1/mac.rs index 820d7939f..81c709929 100644 --- a/network-hypervisor/src/vl1/mac.rs +++ b/network-hypervisor/src/vl1/mac.rs @@ -27,12 +27,7 @@ impl MAC { pub fn from_bytes(b: &[u8]) -> Option { if b.len() >= 6 { NonZeroU64::new( - (b[0] as u64) << 40 - | (b[1] as u64) << 32 - | (b[2] as u64) << 24 - | (b[3] as u64) << 16 as u64 - | (b[4] as u64) << 8 - | b[5] as u64, + (b[0] as u64) << 40 | (b[1] as u64) << 32 | (b[2] as u64) << 24 | (b[3] as u64) << 16 as u64 | (b[4] as u64) << 8 | b[5] as u64, ) .map(|i| MAC(i)) } else { @@ -94,10 +89,7 @@ impl Marshalable for MAC { impl ToString for MAC { fn to_string(&self) -> String { let b: [u8; 6] = self.to_bytes(); - format!( - "{:0>2x}:{:0>2x}:{:0>2x}:{:0>2x}:{:0>2x}:{:0>2x}", - b[0], b[1], b[2], b[3], b[4], b[5] - ) + format!("{:0>2x}:{:0>2x}:{:0>2x}:{:0>2x}:{:0>2x}:{:0>2x}", b[0], b[1], b[2], b[3], b[4], b[5]) } } diff --git a/network-hypervisor/src/vl1/node.rs b/network-hypervisor/src/vl1/node.rs index 98a63bf89..4db0ec531 100644 --- a/network-hypervisor/src/vl1/node.rs +++ b/network-hypervisor/src/vl1/node.rs @@ -388,14 +388,7 @@ impl Node { /// Get the root sets that this node trusts. #[inline] pub fn root_sets(&self) -> Vec { - self.roots - .read() - .unwrap() - .sets - .values() - .cloned() - .map(|s| s.remove_typestate()) - .collect() + self.roots.read().unwrap().sets.values().cloned().map(|s| s.remove_typestate()).collect() } pub fn do_background_tasks(&self, app: &Application) -> Duration { @@ -463,8 +456,7 @@ impl Node { for (_, rs) in roots.sets.iter() { for m in rs.members.iter() { - if m.endpoints.is_some() && !address_collisions.contains(&m.identity.address) && !m.identity.eq(&self.identity) - { + if m.endpoints.is_some() && !address_collisions.contains(&m.identity.address) && !m.identity.eq(&self.identity) { debug_event!( app, "[vl1] examining root {} with {} endpoints", @@ -551,11 +543,7 @@ impl Node { ); *best_root = best.clone(); } else { - debug_event!( - app, - "[vl1] selected new best root: {} (was empty)", - best.identity.address.to_string() - ); + debug_event!(app, "[vl1] selected new best root: {} (was empty)", best.identity.address.to_string()); let _ = best_root.insert(best.clone()); } } @@ -648,11 +636,7 @@ impl Node { // Lock in write mode and remove dead paths, doing so piecemeal to again avoid blocking. for dp in dead_paths.iter() { - self.paths - .write() - .unwrap() - .get_mut::>() - .remove(dp); + self.paths.write().unwrap().get_mut::>().remove(dp); } // Finally run keepalive sends as a batch. @@ -704,15 +688,9 @@ impl Node { app, "[vl1] {} -> #{} {}->{} length {} (on socket {}@{})", source_endpoint.to_string(), - packet - .bytes_fixed_at::<8>(0) - .map_or("????????????????".into(), |pid| hex::to_string(pid)), - packet - .bytes_fixed_at::<5>(13) - .map_or("??????????".into(), |src| hex::to_string(src)), - packet - .bytes_fixed_at::<5>(8) - .map_or("??????????".into(), |dest| hex::to_string(dest)), + packet.bytes_fixed_at::<8>(0).map_or("????????????????".into(), |pid| hex::to_string(pid)), + packet.bytes_fixed_at::<5>(13).map_or("??????????".into(), |src| hex::to_string(src)), + packet.bytes_fixed_at::<5>(8).map_or("??????????".into(), |dest| hex::to_string(dest)), packet.len(), source_local_socket.to_string(), source_local_interface.to_string() @@ -780,8 +758,7 @@ impl Node { for i in 1..assembled_packet.have { if let Some(f) = assembled_packet.frags[i as usize].as_ref() { if f.len() > v1::FRAGMENT_HEADER_SIZE { - ok |= - combined_packet.append_bytes(&f.as_bytes()[v1::FRAGMENT_HEADER_SIZE..]).is_ok(); + ok |= combined_packet.append_bytes(&f.as_bytes()[v1::FRAGMENT_HEADER_SIZE..]).is_ok(); } } } @@ -831,12 +808,7 @@ impl Node { #[cfg(debug_assertions)] { debug_packet_id = u64::from_be_bytes(packet_header.id); - debug_event!( - app, - "[vl1] [v1] #{:0>16x} forwarding packet to {}", - debug_packet_id, - dest.to_string() - ); + debug_event!(app, "[vl1] [v1] #{:0>16x} forwarding packet to {}", debug_packet_id, dest.to_string()); } if packet_header.increment_hops() > v1::FORWARD_MAX_HOPS { #[cfg(debug_assertions)] @@ -993,10 +965,7 @@ impl Node { time_ticks: i64, ) -> Arc { let paths = self.paths.read().unwrap(); - if let Some(path) = paths - .get::>() - .get(&PathKey::Ref(ep, local_socket)) - { + if let Some(path) = paths.get::>().get(&PathKey::Ref(ep, local_socket)) { path.clone() } else { drop(paths); diff --git a/network-hypervisor/src/vl1/peer.rs b/network-hypervisor/src/vl1/peer.rs index e53800df6..1b2bc3e86 100644 --- a/network-hypervisor/src/vl1/peer.rs +++ b/network-hypervisor/src/vl1/peer.rs @@ -324,11 +324,7 @@ impl Peer { let mut aes_gmac_siv = self.v1_proto_static_secret.aes_gmac_siv.get(); aes_gmac_siv.encrypt_init(&self.v1_proto_next_message_id().to_be_bytes()); - aes_gmac_siv.encrypt_set_aad(&v1::get_packet_aad_bytes( - self.identity.address, - node.identity.address, - flags_cipher_hops, - )); + aes_gmac_siv.encrypt_set_aad(&v1::get_packet_aad_bytes(self.identity.address, node.identity.address, flags_cipher_hops)); let payload = packet.as_bytes_starting_at_mut(v1::HEADER_SIZE).unwrap(); aes_gmac_siv.encrypt_first_pass(payload); aes_gmac_siv.encrypt_first_pass_finish(); @@ -419,8 +415,7 @@ impl Peer { let message_id = self.v1_proto_next_message_id(); { - let f: &mut (v1::PacketHeader, v1::message_component_structs::HelloFixedHeaderFields) = - packet.append_struct_get_mut().unwrap(); + let f: &mut (v1::PacketHeader, v1::message_component_structs::HelloFixedHeaderFields) = packet.append_struct_get_mut().unwrap(); f.0.id = message_id.to_ne_bytes(); f.0.dest = self.identity.address.to_bytes(); f.0.src = node.identity.address.to_bytes(); @@ -545,16 +540,9 @@ impl Peer { return match verb { message_type::VL1_NOP => PacketHandlerResult::Ok, message_type::VL1_HELLO => self.handle_incoming_hello(app, inner, node, time_ticks, message_id, source_path, &payload), - message_type::VL1_ERROR => self.handle_incoming_error( - app, - inner, - node, - time_ticks, - source_path, - packet_header.hops(), - message_id, - &payload, - ), + message_type::VL1_ERROR => { + self.handle_incoming_error(app, inner, node, time_ticks, source_path, packet_header.hops(), message_id, &payload) + } message_type::VL1_OK => self.handle_incoming_ok( app, inner, @@ -567,13 +555,9 @@ impl Peer { &payload, ), message_type::VL1_WHOIS => self.handle_incoming_whois(app, inner, node, time_ticks, message_id, &payload), - message_type::VL1_RENDEZVOUS => { - self.handle_incoming_rendezvous(app, node, time_ticks, message_id, source_path, &payload) - } + message_type::VL1_RENDEZVOUS => self.handle_incoming_rendezvous(app, node, time_ticks, message_id, source_path, &payload), message_type::VL1_ECHO => self.handle_incoming_echo(app, inner, node, time_ticks, message_id, &payload), - message_type::VL1_PUSH_DIRECT_PATHS => { - self.handle_incoming_push_direct_paths(app, node, time_ticks, source_path, &payload) - } + message_type::VL1_PUSH_DIRECT_PATHS => self.handle_incoming_push_direct_paths(app, node, time_ticks, source_path, &payload), message_type::VL1_USER_MESSAGE => self.handle_incoming_user_message(app, node, time_ticks, source_path, &payload), _ => inner.handle_packet(app, node, self, &source_path, packet_header.hops(), message_id, verb, &payload, 1), }; @@ -617,8 +601,7 @@ impl Peer { } self.send(app, node, Some(source_path), time_ticks, |packet| -> Result<(), Infallible> { - let f: &mut (OkHeader, v1::message_component_structs::OkHelloFixedHeaderFields) = - packet.append_struct_get_mut().unwrap(); + let f: &mut (OkHeader, v1::message_component_structs::OkHelloFixedHeaderFields) = packet.append_struct_get_mut().unwrap(); f.0.verb = message_type::VL1_OK; f.0.in_re_verb = message_type::VL1_HELLO; f.0.in_re_message_id = message_id.to_ne_bytes(); diff --git a/network-hypervisor/src/vl2/networkconfig.rs b/network-hypervisor/src/vl2/networkconfig.rs index bf4f1d953..04c1d6435 100644 --- a/network-hypervisor/src/vl2/networkconfig.rs +++ b/network-hypervisor/src/vl2/networkconfig.rs @@ -116,10 +116,7 @@ impl NetworkConfig { d.set_u64(proto_v1_field_name::network_config::VERSION, 6); - d.set_str( - proto_v1_field_name::network_config::NETWORK_ID, - self.network_id.to_string().as_str(), - ); + d.set_str(proto_v1_field_name::network_config::NETWORK_ID, self.network_id.to_string().as_str()); if !self.name.is_empty() { d.set_str(proto_v1_field_name::network_config::NAME, self.name.as_str()); } @@ -253,8 +250,7 @@ impl NetworkConfig { let mut nc = Self::new(nwid, issued_to_address); - d.get_str(proto_v1_field_name::network_config::NAME) - .map(|x| nc.name = x.to_string()); + d.get_str(proto_v1_field_name::network_config::NAME).map(|x| nc.name = x.to_string()); nc.private = d.get_str(proto_v1_field_name::network_config::TYPE).map_or(true, |x| x == "1"); nc.timestamp = d .get_i64(proto_v1_field_name::network_config::TIMESTAMP) @@ -358,16 +354,10 @@ impl NetworkConfig { authentication_expiry_time: d .get_i64(proto_v1_field_name::network_config::SSO_AUTHENTICATION_EXPIRY_TIME) .unwrap_or(0), - issuer_url: d - .get_str(proto_v1_field_name::network_config::SSO_ISSUER_URL) - .unwrap_or("") - .to_string(), + issuer_url: d.get_str(proto_v1_field_name::network_config::SSO_ISSUER_URL).unwrap_or("").to_string(), nonce: d.get_str(proto_v1_field_name::network_config::SSO_NONCE).unwrap_or("").to_string(), state: d.get_str(proto_v1_field_name::network_config::SSO_STATE).unwrap_or("").to_string(), - client_id: d - .get_str(proto_v1_field_name::network_config::SSO_CLIENT_ID) - .unwrap_or("") - .to_string(), + client_id: d.get_str(proto_v1_field_name::network_config::SSO_CLIENT_ID).unwrap_or("").to_string(), }) } @@ -464,10 +454,7 @@ pub struct IpRoute { impl Marshalable for IpRoute { const MAX_MARSHAL_SIZE: usize = (InetAddress::MAX_MARSHAL_SIZE * 2) + 2 + 2; - fn marshal( - &self, - buf: &mut zerotier_utils::buffer::Buffer, - ) -> Result<(), zerotier_utils::marshalable::UnmarshalError> { + fn marshal(&self, buf: &mut zerotier_utils::buffer::Buffer) -> Result<(), zerotier_utils::marshalable::UnmarshalError> { self.target.marshal(buf)?; if let Some(via) = self.via.as_ref() { via.marshal(buf)?; diff --git a/service/src/main.rs b/service/src/main.rs index 52fe57242..5927891cb 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -171,10 +171,7 @@ fn main() { if suggested.is_empty() { eprintln!("Unrecognized option '{}'. Use 'help' for help.", invalid); } else { - eprintln!( - "Unrecognized option '{}', did you mean {}? Use 'help' for help.", - invalid, suggested - ); + eprintln!("Unrecognized option '{}', did you mean {}? Use 'help' for help.", invalid, suggested); } } std::process::exit(exitcode::ERR_USAGE); @@ -184,9 +181,7 @@ fn main() { let flags = Flags { json_output: global_args.is_present("json"), - base_path: global_args - .value_of("path") - .map_or_else(platform_default_home_path, |p| p.to_string()), + base_path: global_args.value_of("path").map_or_else(platform_default_home_path, |p| p.to_string()), auth_token_path_override: global_args.value_of("token_path").map(|p| p.to_string()), auth_token_override: global_args.value_of("token").map(|t| t.to_string()), }; diff --git a/utils/src/dictionary.rs b/utils/src/dictionary.rs index f9dd18b73..db47b3d86 100644 --- a/utils/src/dictionary.rs +++ b/utils/src/dictionary.rs @@ -93,13 +93,11 @@ impl Dictionary { } pub fn get_u64(&self, k: &str) -> Option { - self.get_str(k) - .map_or(None, |s| u64::from_str_radix(s, 16).map_or(None, |i| Some(i))) + self.get_str(k).map_or(None, |s| u64::from_str_radix(s, 16).map_or(None, |i| Some(i))) } pub fn get_i64(&self, k: &str) -> Option { - self.get_str(k) - .map_or(None, |s| i64::from_str_radix(s, 16).map_or(None, |i| Some(i))) + self.get_str(k).map_or(None, |s| i64::from_str_radix(s, 16).map_or(None, |i| Some(i))) } pub fn get_bool(&self, k: &str) -> Option { diff --git a/utils/src/json.rs b/utils/src/json.rs index 8b76fd10e..4d83e6941 100644 --- a/utils/src/json.rs +++ b/utils/src/json.rs @@ -44,11 +44,7 @@ pub fn json_patch(target: &mut serde_json::value::Value, source: &serde_json::va /// /// If there are no changes, None is returned. The depth limit is passed through to json_patch and /// should be set to a sanity check value to prevent overflows. -pub fn json_patch_object( - obj: O, - patch: &str, - depth_limit: usize, -) -> Result, serde_json::Error> { +pub fn json_patch_object(obj: O, patch: &str, depth_limit: usize) -> Result, serde_json::Error> { serde_json::from_str::(patch).map_or_else( |e| Err(e), |patch| { diff --git a/utils/src/lib.rs b/utils/src/lib.rs index c9dc4da25..8f211d91e 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -55,10 +55,7 @@ pub fn base64_decode_url_nopad(b64: &str) -> Option> { /// Get milliseconds since unix epoch. #[inline] pub fn ms_since_epoch() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() as i64 + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64 } /// Get milliseconds since an arbitrary time in the past, guaranteed to monotonically increase. diff --git a/vl1-service/src/constants.rs b/vl1-service/src/constants.rs index 436708eee..070e111ac 100644 --- a/vl1-service/src/constants.rs +++ b/vl1-service/src/constants.rs @@ -2,15 +2,15 @@ /// A list of unassigned or obsolete ports under 1024 that could possibly be squatted. pub const UNASSIGNED_PRIVILEGED_PORTS: [u16; 299] = [ - 4, 6, 8, 10, 12, 14, 15, 16, 26, 28, 30, 32, 34, 36, 40, 60, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 285, 288, 289, 290, - 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, - 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 703, 708, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, - 728, 732, 733, 734, 735, 736, 737, 738, 739, 740, 743, 745, 746, 755, 756, 766, 768, 778, 779, 781, 782, 783, 784, 785, 786, 787, 788, - 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 802, 803, 804, 805, 806, 807, 808, 809, 811, 812, 813, 814, 815, 816, 817, 818, - 819, 820, 821, 822, 823, 824, 825, 826, 827, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 849, 850, 851, 852, 853, - 854, 855, 856, 857, 858, 859, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, - 884, 885, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 904, 905, 906, 907, 908, 909, 910, 911, 914, 915, 916, 917, 918, 919, - 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, - 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, - 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1023, + 4, 6, 8, 10, 12, 14, 15, 16, 26, 28, 30, 32, 34, 36, 40, 60, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 285, 288, 289, 290, 291, 292, + 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 334, 335, 336, 337, + 338, 339, 340, 341, 342, 343, 703, 708, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 732, 733, 734, 735, 736, + 737, 738, 739, 740, 743, 745, 746, 755, 756, 766, 768, 778, 779, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, + 797, 798, 799, 802, 803, 804, 805, 806, 807, 808, 809, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 834, + 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 862, 863, 864, 865, 866, 867, + 868, 869, 870, 871, 872, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 904, + 905, 906, 907, 908, 909, 910, 911, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, + 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, + 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 1001, 1002, 1003, 1004, + 1005, 1006, 1007, 1008, 1009, 1023, ]; diff --git a/vl1-service/src/sys/udp.rs b/vl1-service/src/sys/udp.rs index ba8d3a949..19a950e46 100644 --- a/vl1-service/src/sys/udp.rs +++ b/vl1-service/src/sys/udp.rs @@ -31,11 +31,7 @@ fn socket_read_concurrency() -> usize { unsafe { let mut t = THREADS_PER_SOCKET; if t == 0 { - t = std::thread::available_parallelism() - .unwrap() - .get() - .max(1) - .min(MAX_PER_SOCKET_CONCURRENCY); + t = std::thread::available_parallelism().unwrap().get().max(1).min(MAX_PER_SOCKET_CONCURRENCY); THREADS_PER_SOCKET = t; } t @@ -43,13 +39,7 @@ fn socket_read_concurrency() -> usize { } pub trait UdpPacketHandler: Send + Sync { - fn incoming_udp_packet( - self: &Arc, - time_ticks: i64, - socket: &Arc, - source_address: &InetAddress, - packet: PooledPacketBuffer, - ); + fn incoming_udp_packet(self: &Arc, time_ticks: i64, socket: &Arc, source_address: &InetAddress, packet: PooledPacketBuffer); } /// A local port to which one or more UDP sockets is bound. diff --git a/vl1-service/src/vl1service.rs b/vl1-service/src/vl1service.rs index e6ed37682..f7f8c6669 100644 --- a/vl1-service/src/vl1service.rs +++ b/vl1-service/src/vl1service.rs @@ -113,8 +113,7 @@ impl VL1Service { if !state.settings.fixed_ports.contains(p) { let (total_smart_ptr_handles, most_recent_receive) = s.read().unwrap().liveness(); if total_smart_ptr_handles < most_stale_binding_liveness.0 - || (total_smart_ptr_handles == most_stale_binding_liveness.0 - && most_recent_receive <= most_stale_binding_liveness.1) + || (total_smart_ptr_handles == most_stale_binding_liveness.0 && most_recent_receive <= most_stale_binding_liveness.1) { most_stale_binding_liveness.0 = total_smart_ptr_handles; most_stale_binding_liveness.1 = most_recent_receive; diff --git a/vl1-service/src/vl1settings.rs b/vl1-service/src/vl1settings.rs index 1e928ea14..56bc4bf37 100644 --- a/vl1-service/src/vl1settings.rs +++ b/vl1-service/src/vl1settings.rs @@ -27,8 +27,7 @@ pub struct VL1Settings { impl VL1Settings { #[cfg(target_os = "macos")] - pub const DEFAULT_PREFIX_BLACKLIST: [&'static str; 11] = - ["lo", "utun", "gif", "stf", "iptap", "pktap", "feth", "zt", "llw", "anpi", "bridge"]; + pub const DEFAULT_PREFIX_BLACKLIST: [&'static str; 11] = ["lo", "utun", "gif", "stf", "iptap", "pktap", "feth", "zt", "llw", "anpi", "bridge"]; #[cfg(target_os = "linux")] pub const DEFAULT_PREFIX_BLACKLIST: [&'static str; 5] = ["lo", "tun", "tap", "ipsec", "zt"]; diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs index e1b0629e6..77ac96d44 100644 --- a/zssp/src/fragged.rs +++ b/zssp/src/fragged.rs @@ -39,13 +39,7 @@ impl Fragged { unsafe { zeroed() } } - pub fn assemble( - &mut self, - counter: u64, - fragment: Fragment, - fragment_no: u8, - fragment_count: u8, - ) -> Option> { + pub fn assemble(&mut self, counter: u64, fragment: Fragment, fragment_no: u8, fragment_count: u8) -> Option> { if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { debug_assert!((fragment_count as usize) <= MAX_FRAGMENTS); debug_assert!((fragment_no as usize) < MAX_FRAGMENTS); From 4360e0b48756ef6614c0e54af551fd06444b021e Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 3 Mar 2023 11:08:12 -0500 Subject: [PATCH 13/14] ZSSP API updates. --- zssp/src/main.rs | 6 +++--- zssp/src/zssp.rs | 28 +++++++++++++++++----------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 52f84824b..3d223c5a1 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -18,7 +18,7 @@ struct TestApplication { } impl zssp::ApplicationLayer for TestApplication { - const REKEY_AFTER_USES: u64 = 350000; + const REKEY_AFTER_USES: u64 = 300000; const EXPIRE_AFTER_USES: u64 = 2147483648; const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60 * 2; const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; @@ -90,7 +90,7 @@ fn alice_main( TEST_MTU, current_time, ) { - Ok(zssp::ReceiveResult::Ok) => { + Ok(zssp::ReceiveResult::Ok(_)) => { //println!("[alice] ok"); } Ok(zssp::ReceiveResult::OkData(_, _)) => { @@ -188,7 +188,7 @@ fn bob_main( TEST_MTU, current_time, ) { - Ok(zssp::ReceiveResult::Ok) => { + Ok(zssp::ReceiveResult::Ok(_)) => { //println!("[bob] ok"); } Ok(zssp::ReceiveResult::OkData(s, data)) => { diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 01985c22b..406cdaba4 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -57,8 +57,8 @@ struct SessionsById { /// Result generated by the context packet receive function, with possible payloads. pub enum ReceiveResult<'b, Application: ApplicationLayer> { - /// Packet was valid, but no action needs to be taken. - Ok, + /// Packet was valid, but no action needs to be taken and no payload was delivered. + Ok(Option>>), /// Packet was valid and a data payload was decoded and authenticated. OkData(Arc>, &'b mut [u8]), @@ -173,7 +173,6 @@ impl Context { { let sessions = self.sessions.read().unwrap(); - for (id, s) in sessions.active.iter() { if let Some(session) = s.upgrade() { let state = session.state.read().unwrap(); @@ -390,7 +389,11 @@ impl Context { /// /// Note that if check_accept_session accepts and returns Some() the session could still fail with /// receive() returning an error. A Some() return from check_accept_sesion doesn't guarantee - /// successful new session init. + /// successful new session init, only that the application has authorized it. + /// + /// Finally, note that the check_X() functions can end up getting called more than once for a given + /// incoming attempt from a given node if the network quality is poor. That's because the caller may + /// have to retransmit init packets causing repetition of parts of the exchange. /// /// * `app` - Interface to application using ZSSP /// * `check_allow_incoming_session` - Function to call to check whether an unidentified new session should be accepted @@ -453,7 +456,8 @@ impl Context { current_time, ); } else { - return Ok(ReceiveResult::Ok); + drop(fragged); + return Ok(ReceiveResult::Ok(Some(session))); } } else { return self.process_complete_incoming_packet( @@ -556,7 +560,7 @@ impl Context { ); } - return Ok(ReceiveResult::Ok); + return Ok(ReceiveResult::Ok(None)); } fn process_complete_incoming_packet< @@ -661,7 +665,7 @@ impl Context { if packet_type == PACKET_TYPE_DATA { return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); } else { - return Ok(ReceiveResult::Ok); + return Ok(ReceiveResult::Ok(Some(session))); } } else { return Err(Error::OutOfSequence); @@ -835,7 +839,7 @@ impl Context { Some(&Aes::new(header_protection_key.as_bytes())), )?; - return Ok(ReceiveResult::Ok); + return Ok(ReceiveResult::Ok(session)); } PACKET_TYPE_BOB_NOISE_XK_ACK => { @@ -995,7 +999,7 @@ impl Context { Some(&session.header_protection_cipher), )?; - return Ok(ReceiveResult::Ok); + return Ok(ReceiveResult::Ok(Some(session))); } else { return Err(Error::InvalidPacket); } @@ -1220,7 +1224,8 @@ impl Context { false, )); - return Ok(ReceiveResult::Ok); + drop(state); + return Ok(ReceiveResult::Ok(Some(session))); } } return Err(Error::FailedAuthentication); @@ -1280,7 +1285,8 @@ impl Context { state.current_key = next_key_index; // this is an ACK so it's confirmed state.current_offer = Offer::None; - return Ok(ReceiveResult::Ok); + drop(state); + return Ok(ReceiveResult::Ok(Some(session))); } } } From 3f6c7f27a1a7e725bd11aac82a55ba045ba4e554 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 3 Mar 2023 13:46:58 -0500 Subject: [PATCH 14/14] Just a bit of final ZSSP cleanup before moving to another thing. --- zssp/Cargo.toml | 10 +++++----- zssp/src/fragged.rs | 10 ++++++++++ zssp/src/main.rs | 2 +- zssp/src/zssp.rs | 21 ++++++++------------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/zssp/Cargo.toml b/zssp/Cargo.toml index 29cc28958..d4888b865 100644 --- a/zssp/Cargo.toml +++ b/zssp/Cargo.toml @@ -5,11 +5,11 @@ license = "MPL-2.0" name = "zssp" version = "0.1.0" -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -panic = 'abort' +#[profile.release] +#opt-level = 3 +#lto = true +#codegen-units = 1 +#panic = 'abort' [lib] name = "zssp" diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs index 77ac96d44..92ab6cb83 100644 --- a/zssp/src/fragged.rs +++ b/zssp/src/fragged.rs @@ -29,6 +29,7 @@ impl Drop for Assembled Fragged { + #[inline(always)] pub fn new() -> Self { debug_assert!(MAX_FRAGMENTS <= 64); debug_assert_eq!(size_of::>(), size_of::()); @@ -39,6 +40,11 @@ impl Fragged { unsafe { zeroed() } } + /// Add a fragment and return an assembled packet container if all fragments have been received. + /// + /// When a fully assembled packet is returned the internal state is reset and this object can + /// be reused to assemble another packet. + #[inline(always)] pub fn assemble(&mut self, counter: u64, fragment: Fragment, fragment_no: u8, fragment_count: u8) -> Option> { if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { debug_assert!((fragment_count as usize) <= MAX_FRAGMENTS); @@ -70,6 +76,9 @@ impl Fragged { have |= 1u64.wrapping_shl(fragment_no as u32); if (have & want) == want { self.have = 0; + // Setting 'have' to 0 resets the state of this object, and the fragments + // are effectively moved into the Assembled<> container and returned. That + // container will drop them when it is dropped. return Some(Assembled(unsafe { std::mem::transmute_copy(&self.frags) }, fragment_count as usize)); } else { self.have = have; @@ -80,6 +89,7 @@ impl Fragged { } impl Drop for Fragged { + #[inline(always)] fn drop(&mut self) { if needs_drop::() { let mut have = self.have; diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 3d223c5a1..f9db5a977 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -272,7 +272,7 @@ fn main() { let alice_thread = ts.spawn(|| alice_main(&run, packet_success_rate, &alice_app, &bob_app, alice_out, alice_in)); let bob_thread = ts.spawn(|| bob_main(&run, packet_success_rate, &alice_app, &bob_app, bob_out, bob_in)); - thread::sleep(Duration::from_secs(60 * 10)); + thread::sleep(Duration::from_secs(60 * 60)); run.store(false, Ordering::SeqCst); let _ = alice_thread.join(); diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 406cdaba4..a8a8f76b0 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -171,6 +171,7 @@ impl Context { let retry_cutoff = current_time - Application::RETRY_INTERVAL; let negotiation_timeout_cutoff = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; + // Scan sessions in read lock mode, then lock more briefly in write mode to delete any dead entries that we found. { let sessions = self.sessions.read().unwrap(); for (id, s) in sessions.active.iter() { @@ -251,6 +252,7 @@ impl Context { } } + // Delete any expired defragmentation queue items not associated with a session. self.defrag.lock().unwrap().retain(|_, fragged| fragged.1 > negotiation_timeout_cutoff); Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS.min(Application::RETRY_INTERVAL) @@ -502,8 +504,7 @@ impl Context { .or_insert_with(|| Arc::new((Mutex::new(Fragged::new()), current_time))) .clone(); - // Anti-DOS emergency cleaning of the incoming defragmentation queue for packets not - // associated with known sessions. + // Anti-DOS overflow purge of the incoming defragmentation queue for packets not associated with known sessions. if defrag.len() >= self.max_incomplete_session_queue_size { // First, drop all entries that are timed out or whose physical source duplicates another entry. let mut sources = HashSet::with_capacity(defrag.len()); @@ -700,15 +701,9 @@ impl Context { * * Bob authenticates the message and confirms that Alice indeed knows Bob's * identity, then responds with his ephemeral keys. - * - * Bob also sends an opaque sealed object called Bob's "note to self." It contains - * Bob's state for the connection as of this first exchange, allowing Bob to be - * stateless until he knows and has confirmed Alice's identity. It's encrypted, - * authenticated, subject to a short TTL, and contains only information relevant - * to the current exchange. */ - if incoming_counter != 1 || session.is_some() { + if incoming_counter != 1 || session.is_some() || incoming.is_some() { return Err(Error::OutOfSequence); } if pkt_assembled.len() != AliceNoiseXKInit::SIZE { @@ -770,10 +765,10 @@ impl Context { } } + // If this queue is too big, we remove the latest entry and replace it. The latest + // is used because under flood conditions this is most likely to be another bogus + // entry. If we find one that is actually timed out, that one is replaced instead. if sessions.incoming.len() >= self.max_incomplete_session_queue_size { - // If this queue is too big, we remove the latest entry and replace it. The latest - // is used because under flood conditions this is most likely to be another bogus - // entry. If we find one that is actually timed out, that one is replaced instead. let mut newest = i64::MIN; let mut replace_id = None; let cutoff_time = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; @@ -1390,7 +1385,7 @@ impl Session { /// Check whether this session is established. pub fn established(&self) -> bool { let state = self.state.read().unwrap(); - state.keys[state.current_key].is_some() + state.keys[state.current_key].as_ref().map_or(false, |k| k.confirmed) } /// Get the ratchet count and a hash fingerprint of the current active key.