From 5cf99ecb1d0bd830418d58f47ff453032b568d7e Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Tue, 28 Feb 2023 17:54:08 -0500 Subject: [PATCH] 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], + ) }