diff --git a/ZSSP.md b/ZSSP.md new file mode 100644 index 000000000..4c99c902d --- /dev/null +++ b/ZSSP.md @@ -0,0 +1,99 @@ +# ZeroTier Secure Session Protocol + +ZeroTier V2 uses a secure session protocol providing forward security, identity anonymity, and opaqueness on the wire. To this it also adds obfuscation to make ZeroTier indistinguishable from other protocols on the wire by a naive observer who doesn't know participant identities. + +## Design Goals and Principles + + - Strong forward secrecy + - Privacy (e.g. identities are not visible in the clear) + - Indistinguishability on the wire + - Simplicity (both state machine and implementation) + - Highest practical performance on common hardware (x86_64, ARM64, network accelerators) + - NIST, FIPS, and NSA CNSA (formerly Suite B) compliant + - Session init is trivially distinguishable from legacy ZeroTier traffic (by the correct recipient node at least) to allow efficient legacy support + - General enough to be useful outside ZeroTier (perhaps with minor modifications) + +## Packet Format + +All packets have the following basic layout: + + -- begin common header + [1] KKK_UTTT: K == key index, _ == reserved, U == first field is unencrypted, T == packet type + [4] 32-bit counter (little-endian) + [6] 48-bit recipient session ID (all zero in INIT) + -- end common header (11 bytes) + [...] unencrypted payload (packet type specific, usually absent) + -- begin encrypted payload + [...] encrypted payload + -- end encrypted payload + [...] cipher-specific authentication tag (16 or 48 bytes) + +## Packet Types and Payload + + 0: DATA Session data + 1: INIT Alice: start new session, send A's key(s) + 2: INIT_ACK Bob: confirm INIT, send B's key(s) + 3: DATAGRAM Sessionless datagram + 4: REKEY New ephemeral key(s) + 5: REKEY_ACK Acknowledge new ephemeral key(s) + 6-7: reserved + +DATA carries an arbitrary payload. + +All other types carry a payload consisting of pair(s) of single byte field types followed by field data. Field order is not significant except for INIT and DATAGRAM, which place on ephemeral key in an unencrypted payload section before encryption starts. The unencrypted payload section is empty for all other packet types. + +## Symmetric Cipher Modes and Key Management + +Encryption and authentication are achieved using one of two available cipher modes: AES-256-CTR+HMAC-SHA384 or AES-256-GCM. The packet type determines which mode must be used. DATA packets use AES-256-GCM while all other packet types use AES-256-CTR+HMAC-SHA384. These two modes differ only by which MAC is used, since GCM is just CTR+GMAC. + +HMAC authentication is computed over the entire packet after encryption but before obfuscation (see below). GCM authentication covers the encrypted payload only (there is no unencrypted payload in DATA), but since the header forms most of the nonce/IV it is also effectively included (without having to be added as AAD). + +HMAC-based key derivation is used to derive sub-keys for each mode to avoid using the same key for multiple purposes or algorithms. The KDF used in ZeroTier is as follows: + + HMAC-SHA384(first 48 bytes of master key, [0, 0, 0, 0, 'Z', 'T', label, 0, 0, 0, 0, 0x01, 0x80]) + +This construction is based on [section 5.1 page 12 of NIST SP 800-108](https://csrc.nist.gov/publications/detail/sp/800-108/final) with the label prefixed by the ASCII characters "ZT" and the counter always being set to zero. + +The following single byte labels are used: + + 'c' - AES-256-CTR (first 32 bytes of derived key) + 'm' - HMAC-SHA384 (48 bytes, used in combination with AES-CTR but derive a different key) + 'g' - AES-256-GCM (first 32 bytes of derived key) + +Both CTR and GCM take a 12-byte nonce that is constructed from the 11-byte common header and a single byte indicating whether this is "alice" (INIT sender) or "bob" (responder) to prevent nonce reuse when alice and bob share the same session key. + + Nonce bytes: TCCCCSSSSSSR + T: Packed KKCCTTTT byte from header + C: Counter (little-endian) + S: Recipient session ID + R: Role: 0 for Alice (INIT sender), 1 for Bob (INIT recipient) + +The counter may be initialized to any value at session start and MUST be incremented for each packet sent. + +Re-keying is initiated (by either side) after 1,073,741,824 (2^30) encryptions or one hour, whichever comes first. Communication may continue until re-keying is successful unless the key usage counter reaches 2^32. This is a hard error and causes abandonment of the session. Re-keying at 1/4 this number should allow plenty of time for re-key success before this limit is reached. See below for more details on re-keying. + +## Obfuscation + +The unencrypted header and any unencrypted payload (such as the first ephemeral setup key) are obfuscated. + +Obfuscation is performed by applying AES-256-CTR using the last 12 bytes of the packet (part of the HMAC or GCM tag) as a nonce and the first 32 bytes of the recipient node's static identity fingerprint as a key. + +Deobfuscation therefore requires knowledge of the full identity of the recipient. An observer who doesn't know this identity only sees noise and can't distinguish ZeroTier traffic from any other encrypted traffic. + +Obfuscation isn't essential. The protocol would function normally without it and its security guarantees around data privacy and authentication would not be impacted. The purpose of obfuscation is to harden the protocol against tracking and de-anonymization of users through bulk traffic analysis and to provide a very low CPU overhead way for nodes to filter out unwanted packets and maintain "radio silence." + +(Even if someone did manage to guess (with a one in 2^53 chance) a valid obfuscated INIT header, they would still receive no response since the rest of the packet wouldn't be correct. They'd just manage to use a few more CPU cycles on the recipient for this one guess to be further examined and discarded when payload decoding or full HMAC authentication failed.) + +## Session Setup + +## Re-Keying + +## Sessionless Datagrams + +DATAGRAM packets are not associated with a session and do not initiate one. They are used when nodes need to exchange sporadic control plane messages and the overhead of establishing a session is not justified. + +DATAGRAM packets look just like INIT but contain only the sender's identity, a data payload, and an optional auth data payload. The ephemeral key is used to derive a setup key as in INIT but these keys are only used once and then discarded. + +## Credits + +This protocol design is based on the [Noise protocol framework](http://noiseprotocol.org) by Trevor Perrin and [Wireguard](https://www.wireguard.com) by Jason A. Donenfeld. Most of the credit goes to them and whomever else was involved in these designs for the way this is constructed. diff --git a/aes-gmac-siv/README.md b/aes-gmac-siv/README.md index 46d00b813..18ea7066e 100644 --- a/aes-gmac-siv/README.md +++ b/aes-gmac-siv/README.md @@ -1,6 +1,8 @@ AES-GMAC-SIV ====== +**DEPRECATION NOTICE: this mode has been deprecated in ZeroTier V2 via its secure session protocol. It is still included for use with older nodes but the V2 session protocol removes the advantage of a SIV mode and allows us to use more standard and faster simple AEAD modes like GCM.** + This is a Rust implementation of AES-GMAC-SIV, a FIPS-compliant SIV AEAD construction for AES-256. An interface to simple AES-CTR mode is also included. It's implemented in terms of OpenSSL (libcrypto), macOS/iOS CryptoCore, or (soon) WinCrypt, with the latter two automatically selected at compile time on those platforms. diff --git a/rustfmt.toml b/rustfmt.toml index d4085d6df..0160241c8 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 180 +max_width = 200 use_small_heuristics = "Max" tab_spaces = 4 newline_style = "Unix" diff --git a/zerotier-core-crypto/src/aes.rs b/zerotier-core-crypto/src/aes.rs index 1097a4bd5..6723b2122 100644 --- a/zerotier-core-crypto/src/aes.rs +++ b/zerotier-core-crypto/src/aes.rs @@ -6,19 +6,19 @@ mod fruit_flavored { use std::os::raw::{c_int, c_void}; use std::ptr::{null, null_mut}; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] const kCCModeECB: i32 = 1; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] const kCCModeCTR: i32 = 4; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] const kCCModeGCM: i32 = 11; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] const kCCEncrypt: i32 = 0; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] const kCCDecrypt: i32 = 1; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] const kCCAlgorithmAES: i32 = 0; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] const kCCOptionECBMode: i32 = 2; extern "C" { @@ -37,7 +37,7 @@ mod fruit_flavored { cryyptor_ref: *mut *mut c_void, ) -> i32; fn CCCryptorUpdate(cryptor_ref: *mut c_void, data_in: *const c_void, data_in_len: usize, data_out: *mut c_void, 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; @@ -64,14 +64,8 @@ mod fruit_flavored { panic!("AES supports 128, 192, or 256 bits keys"); } let mut aes: Self = std::mem::zeroed(); - let enc = CCCryptorCreateWithMode(kCCEncrypt, kCCModeECB, kCCAlgorithmAES, 0, null(), k.as_ptr().cast(), k.len(), null(), 0, 0, kCCOptionECBMode, &mut aes.0); - if enc != 0 { - panic!("CCCryptorCreateWithMode for ECB encrypt mode returned {}", enc); - } - let dec = CCCryptorCreateWithMode(kCCDecrypt, kCCModeECB, kCCAlgorithmAES, 0, null(), k.as_ptr().cast(), k.len(), null(), 0, 0, kCCOptionECBMode, &mut aes.1); - if dec != 0 { - panic!("CCCryptorCreateWithMode for ECB decrypt mode returned {}", dec); - } + assert_eq!(CCCryptorCreateWithMode(kCCEncrypt, kCCModeECB, kCCAlgorithmAES, 0, null(), k.as_ptr().cast(), k.len(), null(), 0, 0, kCCOptionECBMode, &mut aes.0), 0); + assert_eq!(CCCryptorCreateWithMode(kCCDecrypt, kCCModeECB, kCCAlgorithmAES, 0, null(), k.as_ptr().cast(), k.len(), null(), 0, 0, kCCOptionECBMode, &mut aes.1), 0); aes } } @@ -118,74 +112,6 @@ 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 init(&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); impl Drop for AesGcm { @@ -202,33 +128,40 @@ mod fruit_flavored { } unsafe { let mut ptr: *mut c_void = null_mut(); - let result = CCCryptorCreateWithMode( - if encrypt { kCCEncrypt } else { kCCDecrypt }, - kCCModeGCM, - kCCAlgorithmAES, - 0, - [0_u64; 2].as_ptr().cast(), - k.as_ptr().cast(), - k.len(), - null(), - 0, - 0, - 0, - &mut ptr, + assert_eq!( + CCCryptorCreateWithMode( + if encrypt { kCCEncrypt } else { kCCDecrypt }, + kCCModeGCM, + kCCAlgorithmAES, + 0, + [0_u64; 2].as_ptr().cast(), + k.as_ptr().cast(), + k.len(), + null(), + 0, + 0, + 0, + &mut ptr, + ), + 0 ); - if result != 0 { - panic!("CCCryptorCreateWithMode for GCM mode returned {}", result); - } AesGcm(ptr) } } #[inline(always)] pub fn init(&mut self, iv: &[u8]) { - assert_eq!(iv.len(), 12); unsafe { assert_eq!(CCCryptorGCMReset(self.0), 0); - CCCryptorGCMSetIV(self.0, iv.as_ptr().cast(), 12); + if iv.len() == 16 { + assert_eq!(CCCryptorGCMSetIV(self.0, iv.as_ptr().cast(), 16), 0); + } else if iv.len() < 16 { + let mut tmp = [0_u8; 16]; + tmp[..iv.len()].copy_from_slice(iv); + assert_eq!(CCCryptorGCMSetIV(self.0, tmp.as_ptr().cast(), 16), 0); + } else { + panic!(); + } } } @@ -241,10 +174,11 @@ mod fruit_flavored { #[inline(always)] pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) { - assert_eq!(input.len(), output.len()); unsafe { + assert_eq!(input.len(), output.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); + assert_eq!(data_out_written, input.len()); } } @@ -310,8 +244,7 @@ mod openssl { impl Aes { #[inline(always)] pub fn new(k: &[u8]) -> Self { - let (mut c, mut d) = - (Crypter::new(aes_ecb_by_key_size(k.len()), Mode::Encrypt, k, None).unwrap(), Crypter::new(aes_ecb_by_key_size(k.len()), Mode::Decrypt, k, None).unwrap()); + let (mut c, mut d) = (Crypter::new(aes_ecb_by_key_size(k.len()), Mode::Encrypt, k, None).unwrap(), Crypter::new(aes_ecb_by_key_size(k.len()), Mode::Decrypt, k, None).unwrap()); c.pad(false); d.pad(false); Self(UnsafeCell::new(c), UnsafeCell::new(d)) @@ -361,49 +294,6 @@ mod openssl { 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 init(&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, Option, bool); impl AesGcm { @@ -467,7 +357,7 @@ mod openssl { } #[cfg(target_os = "macos")] -pub use fruit_flavored::{Aes, AesCtr, AesGcm}; +pub use fruit_flavored::{Aes, AesGcm}; #[cfg(not(target_os = "macos"))] -pub use openssl::{Aes, AesCtr, AesGcm}; +pub use openssl::{Aes, AesGcm}; diff --git a/zerotier-core-crypto/src/kbkdf.rs b/zerotier-core-crypto/src/kbkdf.rs index 40445bc1e..fedfaacb0 100644 --- a/zerotier-core-crypto/src/kbkdf.rs +++ b/zerotier-core-crypto/src/kbkdf.rs @@ -1,5 +1,6 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. +use crate::hash::{hmac_sha384, hmac_sha512}; use crate::secret::Secret; /* @@ -13,10 +14,10 @@ use crate::secret::Secret; /// Derive a key using HMAC-SHA384 and a single byte label, ZeroTier variant with "ZT" preface. pub fn zt_kbkdf_hmac_sha384(key: &[u8], label: u8) -> Secret<48> { - Secret(crate::hash::hmac_sha384(key, &[0, 0, 0, 0, b'Z', b'T', label, 0, 0, 0, 0, 0x01, 0x80])) + Secret(hmac_sha384(key, &[0, 0, 0, 0, b'Z', b'T', label, 0, 0, 0, 0, 0x01, 0x80])) } /// Derive a key using HMAC-SHA512 and a single byte label, ZeroTier variant with "ZT" preface. pub fn zt_kbkdf_hmac_sha512(key: &[u8], label: u8) -> Secret<64> { - Secret(crate::hash::hmac_sha512(key, &[0, 0, 0, 0, b'Z', b'T', label, 0, 0, 0, 0, 0x02, 0x00])) + Secret(hmac_sha512(key, &[0, 0, 0, 0, b'Z', b'T', label, 0, 0, 0, 0, 0x02, 0x00])) } diff --git a/zerotier-core-crypto/src/salsa.rs b/zerotier-core-crypto/src/salsa.rs index adc2e2a03..5e5368879 100644 --- a/zerotier-core-crypto/src/salsa.rs +++ b/zerotier-core-crypto/src/salsa.rs @@ -65,7 +65,8 @@ impl Salsa { ); while !plaintext.is_empty() { - let (mut x0, mut x1, mut x2, mut x3, mut x4, mut x5, mut x6, mut x7, mut x8, mut x9, mut x10, mut x11, mut x12, mut x13, mut x14, mut x15) = (j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15); + let (mut x0, mut x1, mut x2, mut x3, mut x4, mut x5, mut x6, mut x7, mut x8, mut x9, mut x10, mut x11, mut x12, mut x13, mut x14, mut x15) = + (j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15); for _ in 0..(ROUNDS / 2) { x4 ^= x0.wrapping_add(x12).rotate_left(7); @@ -123,7 +124,7 @@ impl Salsa { j9 = j9.wrapping_add((j8 == 0) as u32); if plaintext.len() >= 64 { - #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))] + #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64"))] { // Slightly faster keystream XOR for little-endian platforms with unaligned load/store. unsafe { @@ -148,7 +149,24 @@ impl Salsa { #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))] { // Portable keystream XOR with alignment-safe access and native to little-endian conversion. - let keystream = [x0.to_le(), x1.to_le(), x2.to_le(), x3.to_le(), x4.to_le(), x5.to_le(), x6.to_le(), x7.to_le(), x8.to_le(), x9.to_le(), x10.to_le(), x11.to_le(), x12.to_le(), x13.to_le(), x14.to_le(), x15.to_le()]; + let keystream = [ + x0.to_le(), + x1.to_le(), + x2.to_le(), + x3.to_le(), + x4.to_le(), + x5.to_le(), + x6.to_le(), + x7.to_le(), + x8.to_le(), + x9.to_le(), + x10.to_le(), + x11.to_le(), + x12.to_le(), + x13.to_le(), + x14.to_le(), + x15.to_le(), + ]; for i in 0..64 { ciphertext[i] = plaintext[i] ^ unsafe { *keystream.as_ptr().cast::().add(i) }; } @@ -157,7 +175,24 @@ impl Salsa { plaintext = &plaintext[64..]; ciphertext = &mut ciphertext[64..]; } else { - let keystream = [x0.to_le(), x1.to_le(), x2.to_le(), x3.to_le(), x4.to_le(), x5.to_le(), x6.to_le(), x7.to_le(), x8.to_le(), x9.to_le(), x10.to_le(), x11.to_le(), x12.to_le(), x13.to_le(), x14.to_le(), x15.to_le()]; + let keystream = [ + x0.to_le(), + x1.to_le(), + x2.to_le(), + x3.to_le(), + x4.to_le(), + x5.to_le(), + x6.to_le(), + x7.to_le(), + x8.to_le(), + x9.to_le(), + x10.to_le(), + x11.to_le(), + x12.to_le(), + x13.to_le(), + x14.to_le(), + x15.to_le(), + ]; for i in 0..plaintext.len() { ciphertext[i] = plaintext[i] ^ unsafe { *keystream.as_ptr().cast::().add(i) }; } @@ -179,11 +214,15 @@ impl Salsa { mod tests { use crate::salsa::*; - const SALSA_20_TV0_KEY: [u8; 32] = [0x0f, 0x62, 0xb5, 0x08, 0x5b, 0xae, 0x01, 0x54, 0xa7, 0xfa, 0x4d, 0xa0, 0xf3, 0x46, 0x99, 0xec, 0x3f, 0x92, 0xe5, 0x38, 0x8b, 0xde, 0x31, 0x84, 0xd7, 0x2a, 0x7d, 0xd0, 0x23, 0x76, 0xc9, 0x1c]; + const SALSA_20_TV0_KEY: [u8; 32] = [ + 0x0f, 0x62, 0xb5, 0x08, 0x5b, 0xae, 0x01, 0x54, 0xa7, 0xfa, 0x4d, 0xa0, 0xf3, 0x46, 0x99, 0xec, 0x3f, 0x92, 0xe5, 0x38, 0x8b, 0xde, 0x31, 0x84, 0xd7, 0x2a, 0x7d, 0xd0, + 0x23, 0x76, 0xc9, 0x1c, + ]; const SALSA_20_TV0_IV: [u8; 8] = [0x28, 0x8f, 0xf6, 0x5d, 0xc4, 0x2b, 0x92, 0xf9]; const SALSA_20_TV0_KS: [u8; 64] = [ - 0x5e, 0x5e, 0x71, 0xf9, 0x01, 0x99, 0x34, 0x03, 0x04, 0xab, 0xb2, 0x2a, 0x37, 0xb6, 0x62, 0x5b, 0xf8, 0x83, 0xfb, 0x89, 0xce, 0x3b, 0x21, 0xf5, 0x4a, 0x10, 0xb8, 0x10, 0x66, 0xef, 0x87, 0xda, 0x30, 0xb7, 0x76, 0x99, 0xaa, 0x73, 0x79, 0xda, 0x59, - 0x5c, 0x77, 0xdd, 0x59, 0x54, 0x2d, 0xa2, 0x08, 0xe5, 0x95, 0x4f, 0x89, 0xe4, 0x0e, 0xb7, 0xaa, 0x80, 0xa8, 0x4a, 0x61, 0x76, 0x66, 0x3f, + 0x5e, 0x5e, 0x71, 0xf9, 0x01, 0x99, 0x34, 0x03, 0x04, 0xab, 0xb2, 0x2a, 0x37, 0xb6, 0x62, 0x5b, 0xf8, 0x83, 0xfb, 0x89, 0xce, 0x3b, 0x21, 0xf5, 0x4a, 0x10, 0xb8, 0x10, + 0x66, 0xef, 0x87, 0xda, 0x30, 0xb7, 0x76, 0x99, 0xaa, 0x73, 0x79, 0xda, 0x59, 0x5c, 0x77, 0xdd, 0x59, 0x54, 0x2d, 0xa2, 0x08, 0xe5, 0x95, 0x4f, 0x89, 0xe4, 0x0e, 0xb7, + 0xaa, 0x80, 0xa8, 0x4a, 0x61, 0x76, 0x66, 0x3f, ]; #[test] diff --git a/zerotier-network-hypervisor/src/util/buffer.rs b/zerotier-network-hypervisor/src/util/buffer.rs index 30e8ce1dd..df8f30af8 100644 --- a/zerotier-network-hypervisor/src/util/buffer.rs +++ b/zerotier-network-hypervisor/src/util/buffer.rs @@ -1,6 +1,6 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. -use std::io::Write; +use std::io::{Read, Write}; use std::mem::{size_of, MaybeUninit}; use crate::util::pool::PoolFactory; @@ -26,6 +26,9 @@ impl Default for Buffer { } } +// Setting attributes this way causes the 'overflow' branches to be treated as unlikely by LLVM. +#[inline(never)] +#[cold] fn overflow_err() -> std::io::Error { std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "buffer overflow") } @@ -98,12 +101,12 @@ impl Buffer { #[inline(always)] pub fn as_bytes(&self) -> &[u8] { - &self.1[0..self.0] + &self.1[..self.0] } #[inline(always)] pub fn as_bytes_mut(&mut self) -> &mut [u8] { - &mut self.1[0..self.0] + &mut self.1[..self.0] } #[inline(always)] @@ -134,6 +137,15 @@ impl Buffer { } } + #[inline(always)] + pub fn as_byte_range(&self, start: usize, end: usize) -> std::io::Result<&[u8]> { + if end <= self.0 { + Ok(&self.1[start..end]) + } else { + Err(overflow_err()) + } + } + pub fn clear(&mut self) { self.1[0..self.0].fill(0); self.0 = 0; @@ -322,6 +334,19 @@ impl Buffer { } } + #[inline(always)] + pub fn append_u64_le(&mut self, i: u64) -> std::io::Result<()> { + let ptr = self.0; + let end = ptr + 8; + if end <= L { + self.0 = end; + unsafe { self.write_obj_internal(ptr, i.to_le()) }; + Ok(()) + } else { + Err(overflow_err()) + } + } + #[inline(always)] pub fn bytes_fixed_at(&self, ptr: usize) -> std::io::Result<&[u8; S]> { if (ptr + S) <= self.0 { @@ -504,6 +529,19 @@ impl Buffer { Err(overflow_err()) } } + + #[inline(always)] + pub fn read_u64_le(&self, cursor: &mut usize) -> std::io::Result { + let ptr = *cursor; + let end = ptr + 8; + debug_assert!(end <= L); + if end <= self.0 { + *cursor = end; + Ok(u64::from_le(unsafe { self.read_obj_internal(ptr) })) + } else { + Err(overflow_err()) + } + } } impl Write for Buffer { @@ -554,6 +592,24 @@ impl From<&[u8; L]> for Buffer { } } +/// Implements std::io::Read for a buffer and a cursor. +pub struct BufferReader<'a, 'b, const L: usize>(&'a Buffer, &'b mut usize); + +impl<'a, 'b, const L: usize> BufferReader<'a, 'b, L> { + #[inline(always)] + pub fn new(b: &'a Buffer, cursor: &'b mut usize) -> Self { + Self(b, cursor) + } +} + +impl<'a, 'b, const L: usize> Read for BufferReader<'a, 'b, L> { + #[inline(always)] + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + buf.copy_from_slice(self.0.read_bytes(buf.len(), self.1)?); + Ok(buf.len()) + } +} + pub struct PooledBufferFactory; impl PooledBufferFactory { diff --git a/zerotier-network-hypervisor/src/util/mod.rs b/zerotier-network-hypervisor/src/util/mod.rs index 0bb686ac6..08ce8e66c 100644 --- a/zerotier-network-hypervisor/src/util/mod.rs +++ b/zerotier-network-hypervisor/src/util/mod.rs @@ -9,8 +9,6 @@ pub(crate) mod pool; pub use zerotier_core_crypto::hex; pub use zerotier_core_crypto::varint; -pub(crate) const ZEROES: [u8; 64] = [0_u8; 64]; - /// A value for ticks that indicates that something never happened, and is thus very long before zero ticks. pub(crate) const NEVER_HAPPENED_TICKS: i64 = -2147483648; @@ -34,9 +32,31 @@ macro_rules! debug_event { #[allow(unused_imports)] pub(crate) use debug_event; -/// Obtain a reference to a sub-array within an existing byte array. +/// Obtain a view into a byte array cast as another byte array. #[inline(always)] pub(crate) fn byte_array_range(a: &[u8; A]) -> &[u8; LEN] { assert!((START + LEN) <= A); unsafe { &*a.as_ptr().add(START).cast::<[u8; LEN]>() } } + +/// View a flat (Copy) object as a byte array. +#[inline(always)] +pub(crate) fn flat_object_as_bytes(t: &T) -> &[u8] { + unsafe { &*std::ptr::slice_from_raw_parts((t as *const T).cast::(), std::mem::size_of::()) } +} + +/// Trait that annotates a type as being alignment neutral, such as a packed struct of all bytes and byte arrays. +pub(crate) unsafe trait AlignmentNeutral: Copy {} + +/// View a byte array as a flat (Copy) object. +/// To be safe this can only be used with annotated alignment-neutral structs. +#[inline(always)] +pub(crate) fn bytes_as_flat_object(b: &[u8]) -> &T { + assert!(b.len() >= std::mem::size_of::()); + unsafe { &*b.as_ptr().cast() } +} + +/// Include this in a branch to hint the compiler that it's unlikely. +#[inline(never)] +#[cold] +pub(crate) extern "C" fn unlikely_branch() {} diff --git a/zerotier-network-hypervisor/src/vl1/identity.rs b/zerotier-network-hypervisor/src/vl1/identity.rs index 576e09e38..465b47d58 100644 --- a/zerotier-network-hypervisor/src/vl1/identity.rs +++ b/zerotier-network-hypervisor/src/vl1/identity.rs @@ -3,7 +3,7 @@ use std::cmp::Ordering; use std::convert::TryInto; use std::hash::{Hash, Hasher}; -use std::io::Write; +use std::io::{Read, Write}; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -16,9 +16,7 @@ use zerotier_core_crypto::secret::Secret; use zerotier_core_crypto::x25519::*; use crate::error::{InvalidFormatError, InvalidParameterError}; -use crate::util::buffer::Buffer; -use crate::util::marshalable::Marshalable; -use crate::util::ZEROES; +use crate::util::{bytes_as_flat_object, flat_object_as_bytes, AlignmentNeutral}; use crate::vl1::protocol::{ADDRESS_SIZE, ADDRESS_SIZE_STRING, IDENTITY_FINGERPRINT_SIZE, IDENTITY_POW_THRESHOLD}; use crate::vl1::Address; @@ -41,7 +39,7 @@ pub struct IdentityP384Public { /// Secret keys associated with an identity. #[derive(Clone)] pub struct IdentitySecret { - pub c25519: C25519KeyPair, + pub x25519: C25519KeyPair, pub ed25519: Ed25519KeyPair, pub p384: Option, } @@ -59,7 +57,7 @@ pub struct IdentitySecret { #[derive(Clone)] pub struct Identity { pub address: Address, - pub c25519: [u8; C25519_PUBLIC_KEY_SIZE], + pub x25519: [u8; C25519_PUBLIC_KEY_SIZE], pub ed25519: [u8; ED25519_PUBLIC_KEY_SIZE], pub p384: Option, pub secret: Option, @@ -94,7 +92,7 @@ fn zt_address_derivation_work_function(digest: &mut [u8; 64]) { assert!(!genmem.is_null()); let mut salsa: Salsa<20> = Salsa::new(&digest[..32], &digest[32..40]); - salsa.crypt(&ZEROES, &mut *genmem.cast::<[u8; 64]>()); + salsa.crypt(&[0_u8; 64], &mut *genmem.cast::<[u8; 64]>()); let mut k = 0; while k < (ADDRESS_DERIVATION_HASH_MEMORY_SIZE - 64) { let i = k + 64; @@ -131,14 +129,21 @@ fn zt_address_derivation_work_function(digest: &mut [u8; 64]) { } impl Identity { - /// Curve25519 and Ed25519 - pub const ALGORITHM_X25519: u8 = 0x01; + /// Length of an x25519-only public identity in byte array form. + pub const BYTE_LENGTH_X25519_PUBLIC: usize = ADDRESS_SIZE + 1 + C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + 1 + 1 + 2; - /// NIST P-384 ECDH and ECDSA - pub const ALGORITHM_EC_NIST_P384: u8 = 0x02; + /// Length of an x25519-only secret identity in byte array form. + pub const BYTE_LENGTH_X25519_SECRET: usize = Self::BYTE_LENGTH_X25519_PUBLIC + C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE; - /// Bit mask to include all algorithms. - pub const ALGORITHM_ALL: u8 = 0xff; + /// Length of a new dual-key public identity in byte array form. + pub const BYTE_LENGTH_X25519P384_PUBLIC: usize = Self::BYTE_LENGTH_X25519_PUBLIC + 1 + P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE; + + /// Length of a new dual-key secret identity in byte array form. + pub const BYTE_LENGTH_X25519P384_SECRET: usize = Self::BYTE_LENGTH_X25519P384_PUBLIC + C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P384_SECRET_KEY_SIZE + P384_SECRET_KEY_SIZE; + + const ALGORITHM_X25519: u8 = 0x01; + const ALGORITHM_EC_NIST_P384: u8 = 0x02; + const FLAG_INCLUDES_SECRET: u8 = 0x80; /// Generate a new identity. pub fn generate() -> Self { @@ -147,13 +152,13 @@ impl Identity { let ed25519 = Ed25519KeyPair::generate(); let ed25519_pub = ed25519.public_bytes(); let address; - let mut c25519; - let mut c25519_pub; + let mut x25519; + let mut x25519_pub; loop { - c25519 = C25519KeyPair::generate(); - c25519_pub = c25519.public_bytes(); + x25519 = C25519KeyPair::generate(); + x25519_pub = x25519.public_bytes(); - sha.update(&c25519_pub); + sha.update(&x25519_pub); sha.update(&ed25519_pub); let mut digest = sha.finish(); zt_address_derivation_work_function(&mut digest); @@ -170,10 +175,10 @@ impl Identity { } let mut id = Self { address, - c25519: c25519_pub, + x25519: x25519_pub, ed25519: ed25519_pub, p384: None, - secret: Some(IdentitySecret { c25519, ed25519, p384: None }), + secret: Some(IdentitySecret { x25519, ed25519, p384: None }), fingerprint: [0_u8; IDENTITY_FINGERPRINT_SIZE], // replaced in upgrade() }; @@ -203,9 +208,10 @@ impl Identity { let p384_ecdh = P384KeyPair::generate(); let p384_ecdsa = P384KeyPair::generate(); - let mut self_sign_buf: Vec = Vec::with_capacity(ADDRESS_SIZE + C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + 4); + let mut self_sign_buf: Vec = + Vec::with_capacity(ADDRESS_SIZE + C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + 4); let _ = self_sign_buf.write_all(&self.address.to_bytes()); - let _ = self_sign_buf.write_all(&self.c25519); + let _ = self_sign_buf.write_all(&self.x25519); let _ = self_sign_buf.write_all(&self.ed25519); self_sign_buf.push(Self::ALGORITHM_EC_NIST_P384); let _ = self_sign_buf.write_all(p384_ecdh.public_key_bytes()); @@ -240,20 +246,11 @@ impl Identity { pub fn clone_without_secret(&self) -> Identity { Self { address: self.address, - c25519: self.c25519.clone(), - ed25519: self.ed25519.clone(), + x25519: self.x25519, + ed25519: self.ed25519, p384: self.p384.clone(), secret: None, - fingerprint: self.fingerprint.clone(), - } - } - - /// Get a bit mask of algorithms present in this identity. - pub fn algorithms(&self) -> u8 { - if self.p384.is_some() { - Self::ALGORITHM_X25519 | Self::ALGORITHM_EC_NIST_P384 - } else { - Self::ALGORITHM_X25519 + fingerprint: self.fingerprint, } } @@ -264,7 +261,7 @@ impl Identity { if let Some(p384) = self.p384.as_ref() { let mut self_sign_buf: Vec = Vec::with_capacity(ADDRESS_SIZE + 4 + C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE); let _ = self_sign_buf.write_all(&self.address.to_bytes()); - let _ = self_sign_buf.write_all(&self.c25519); + let _ = self_sign_buf.write_all(&self.x25519); let _ = self_sign_buf.write_all(&self.ed25519); self_sign_buf.push(Self::ALGORITHM_EC_NIST_P384); let _ = self_sign_buf.write_all(p384.ecdh.as_bytes()); @@ -283,7 +280,7 @@ impl Identity { // NOTE: fingerprint is always computed on generation or deserialize so no need to check. let mut sha = SHA512::new(); - sha.update(&self.c25519); + sha.update(&self.x25519); sha.update(&self.ed25519); let mut digest = sha.finish(); zt_address_derivation_work_function(&mut digest); @@ -295,12 +292,12 @@ impl Identity { /// /// An error can occur if this identity does not hold its secret portion or if either key is invalid. /// - /// If both sides have NIST P-384 keys then key agreement is performed using both Curve25519 and - /// NIST P-384 and the result is HMAC-SHA512(Curve25519 secret, NIST P-384 secret). This is FIPS - /// compliant since the Curve25519 secret is treated as a "salt" in HKDF. + /// For new identities with P-384 keys a hybrid agreement is performed using both X25519 and NIST P-384 ECDH. + /// The final key is derived as HMAC(x25519 secret, p-384 secret) to yield a FIPS-compliant key agreement with + /// the X25519 secret being used as a "salt" as far as FIPS is concerned. pub fn agree(&self, other: &Identity) -> Option> { if let Some(secret) = self.secret.as_ref() { - let c25519_secret: Secret<64> = Secret(SHA512::hash(&secret.c25519.agree(&other.c25519).0)); + let c25519_secret: Secret<64> = Secret(SHA512::hash(&secret.x25519.agree(&other.x25519).0)); // FIPS note: FIPS-compliant exchange algorithms must be the last algorithms in any HKDF chain // for the final result to be technically FIPS compliant. Non-FIPS algorithm secrets are considered @@ -358,106 +355,274 @@ impl Identity { return false; } - pub fn to_buffer_with_options(&self, include_algorithms: u8, include_private: bool) -> Buffer<{ Self::MAX_MARSHAL_SIZE }> { - let mut b: Buffer<{ Self::MAX_MARSHAL_SIZE }> = Buffer::new(); - assert!(self.marshal_with_options(&mut b, include_algorithms, include_private).is_ok()); - b + pub fn to_public_bytes(&self) -> IdentityBytes { + if let Some(p384) = self.p384.as_ref() { + IdentityBytes::X25519P384Public( + flat_object_as_bytes(&packed::V1 { + v0: packed::V0 { + address: self.address.to_bytes(), + key_type: 0, + x25519: self.x25519, + ed25519: self.ed25519, + secret_length: 0, + reserved: 0x03, + ext_len: ((Self::BYTE_LENGTH_X25519P384_PUBLIC - Self::BYTE_LENGTH_X25519_PUBLIC) as u16).to_be_bytes(), + }, + key_type_flags: Self::ALGORITHM_EC_NIST_P384, + ecdh: p384.ecdh.as_bytes().clone(), + ecdsa: p384.ecdsa.as_bytes().clone(), + ecdsa_self_signature: p384.ecdsa_self_signature, + ed25519_self_signature: p384.ed25519_self_signature, + }) + .try_into() + .unwrap(), + ) + } else { + IdentityBytes::X25519Public( + flat_object_as_bytes(&packed::V0 { + address: self.address.to_bytes(), + key_type: 0, + x25519: self.x25519, + ed25519: self.ed25519, + secret_length: 0, + reserved: 0x03, + ext_len: [0; 2], + }) + .try_into() + .unwrap(), + ) + } } - pub fn marshal_with_options(&self, buf: &mut Buffer, include_algorithms: u8, include_private: bool) -> std::io::Result<()> { - let algorithms = self.algorithms() & include_algorithms; - let secret = self.secret.as_ref(); - - buf.append_bytes_fixed(&self.address.to_bytes())?; - - if (algorithms & Self::ALGORITHM_X25519) != 0 { - buf.append_u8(0x00)?; // 0x00 is used for X25519 for backward compatibility with v0 identities - buf.append_bytes_fixed(&self.c25519)?; - buf.append_bytes_fixed(&self.ed25519)?; - if include_private && secret.is_some() { - let secret = secret.unwrap(); - buf.append_u8((C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8)?; - buf.append_bytes_fixed(&secret.c25519.secret_bytes().0)?; - buf.append_bytes_fixed(&secret.ed25519.secret_bytes().0)?; + pub fn to_secret_bytes(&self) -> Option { + self.secret.as_ref().map(|s| { + if let Some(p384) = s.p384.as_ref() { + IdentityBytes::X25519P384Secret( + flat_object_as_bytes(&packed::V1S { + v0s: packed::V0S { + address: self.address.to_bytes(), + key_type: 0, + x25519: self.x25519, + ed25519: self.ed25519, + secret_length: (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8, + x25519_secret: s.x25519.secret_bytes().0.clone(), + ed25519_secret: s.ed25519.secret_bytes().0.clone(), + reserved: 0x03, + ext_len: ((Self::BYTE_LENGTH_X25519P384_SECRET - Self::BYTE_LENGTH_X25519_SECRET) as u16).to_be_bytes(), + }, + key_type_flags: Self::ALGORITHM_EC_NIST_P384 | Self::FLAG_INCLUDES_SECRET, + ecdh: p384.ecdh.public_key_bytes().clone(), + ecdsa: p384.ecdsa.public_key_bytes().clone(), + ecdsa_self_signature: self.p384.as_ref().unwrap().ecdsa_self_signature, + ed25519_self_signature: self.p384.as_ref().unwrap().ed25519_self_signature, + ecdh_secret: p384.ecdh.secret_key_bytes().0.clone(), + ecdsa_secret: p384.ecdsa.secret_key_bytes().0.clone(), + }) + .try_into() + .unwrap(), + ) } else { - buf.append_u8(0)?; + IdentityBytes::X25519Secret( + flat_object_as_bytes(&packed::V0S { + address: self.address.to_bytes(), + key_type: 0, + x25519: self.x25519, + ed25519: self.ed25519, + secret_length: (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8, + x25519_secret: s.x25519.secret_bytes().0.clone(), + ed25519_secret: s.ed25519.secret_bytes().0.clone(), + reserved: 0x03, + ext_len: [0; 2], + }) + .try_into() + .unwrap(), + ) } - } - - /* - * LEGACY: - * - * The prefix of 0x03 is for backward compatibility. Older nodes will interpret this as - * an empty unidentified InetAddress object and will skip the number of bytes following it. - * - * For future compatibility the size field here will allow this to be extended, something - * that should have been in the protocol from the beginning. - */ - buf.append_u8(0x03)?; - let remaining_data_size_field_at = buf.len(); - buf.append_padding(0, 2)?; - - if (algorithms & Self::ALGORITHM_EC_NIST_P384) != 0 && self.p384.is_some() { - let p384 = self.p384.as_ref().unwrap(); - let p384s = if include_private { secret.clone().map_or(None, |s| s.p384.as_ref()) } else { None }; - - buf.append_u8(Self::ALGORITHM_EC_NIST_P384)?; - buf.append_varint(if p384s.is_some() { - ((P384_PUBLIC_KEY_SIZE * 2) + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE + (P384_SECRET_KEY_SIZE * 2)) as u64 - } else { - ((P384_PUBLIC_KEY_SIZE * 2) + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE) as u64 - })?; - buf.append_bytes_fixed(p384.ecdh.as_bytes())?; - buf.append_bytes_fixed(p384.ecdsa.as_bytes())?; - buf.append_bytes_fixed(&p384.ecdsa_self_signature)?; - buf.append_bytes_fixed(&p384.ed25519_self_signature)?; - if let Some(p384s) = p384s { - buf.append_bytes_fixed(&p384s.ecdh.secret_key_bytes().0)?; - buf.append_bytes_fixed(&p384s.ecdsa.secret_key_bytes().0)?; - } - } - - // Fill in the remaining data field earmarked above. - *buf.bytes_fixed_mut_at(remaining_data_size_field_at).unwrap() = (((buf.len() - remaining_data_size_field_at) - 2) as u16).to_be_bytes(); - - Ok(()) + }) } - /// Marshal this identity as a string. + /// Convert a byte respresentation into an identity. /// - /// The include_algorithms bitmap controls which algorithms will be included, provided we have them. - /// If include_private is true private keys will be included, again if we have them. - pub fn to_string_with_options(&self, include_algorithms: u8, include_private: bool) -> String { - let include_p384 = self.p384.is_some() && ((include_algorithms & Self::ALGORITHM_EC_NIST_P384) != 0); + /// WARNING: this performs basic sanity checking but does NOT perform a full validation of address derivation or self-signatures. + pub fn from_bytes(b: &IdentityBytes) -> Option { + match b { + IdentityBytes::X25519Public(b) => { + let b: &packed::V0 = bytes_as_flat_object(b); + if b.key_type == 0 && b.secret_length == 0 && b.reserved == 0x03 && u16::from_be_bytes(b.ext_len) == 0 { + Some(Self { + address: Address::from_bytes_fixed(&b.address)?, + x25519: b.x25519, + ed25519: b.ed25519, + p384: None, + secret: None, + fingerprint: { + let mut sha = SHA384::new(); + sha.update(&b.address); + sha.update(&b.x25519); + sha.update(&b.ed25519); + sha.finish() + }, + }) + } else { + None + } + } + IdentityBytes::X25519Secret(b) => { + let b: &packed::V0S = bytes_as_flat_object(b); + if b.key_type == 0 && b.secret_length == (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8 && b.reserved == 0x03 && u16::from_be_bytes(b.ext_len) == 0 { + Some(Self { + address: Address::from_bytes_fixed(&b.address)?, + x25519: b.x25519, + ed25519: b.ed25519, + p384: None, + secret: Some(IdentitySecret { + x25519: C25519KeyPair::from_bytes(&b.x25519, &b.x25519_secret)?, + ed25519: Ed25519KeyPair::from_bytes(&b.ed25519, &b.ed25519_secret)?, + p384: None, + }), + fingerprint: { + let mut sha = SHA384::new(); + sha.update(&b.address); + sha.update(&b.x25519); + sha.update(&b.ed25519); + sha.finish() + }, + }) + } else { + None + } + } + IdentityBytes::X25519P384Public(b) => { + let b: &packed::V1 = bytes_as_flat_object(b); + if b.v0.key_type == 0 + && b.v0.secret_length == 0 + && b.v0.reserved == 0x03 + && u16::from_be_bytes(b.v0.ext_len) == (Self::BYTE_LENGTH_X25519P384_PUBLIC - Self::BYTE_LENGTH_X25519_PUBLIC) as u16 + && b.key_type_flags == Self::ALGORITHM_EC_NIST_P384 + { + Some(Self { + address: Address::from_bytes_fixed(&b.v0.address)?, + x25519: b.v0.x25519, + ed25519: b.v0.ed25519, + p384: Some(IdentityP384Public { + ecdh: P384PublicKey::from_bytes(&b.ecdh)?, + ecdsa: P384PublicKey::from_bytes(&b.ecdsa)?, + ecdsa_self_signature: b.ecdsa_self_signature, + ed25519_self_signature: b.ed25519_self_signature, + }), + secret: None, + fingerprint: { + let mut sha = SHA384::new(); + sha.update(&b.v0.address); + sha.update(&b.v0.x25519); + sha.update(&b.v0.ed25519); + sha.update(&[Self::ALGORITHM_EC_NIST_P384]); + sha.update(&b.ecdh); + sha.update(&b.ecdsa); + sha.finish() + }, + }) + } else { + None + } + } + IdentityBytes::X25519P384Secret(b) => { + let b: &packed::V1S = bytes_as_flat_object(b); + if b.v0s.key_type == 0 + && b.v0s.secret_length == (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8 + && b.v0s.reserved == 0x03 + && u16::from_be_bytes(b.v0s.ext_len) == (Self::BYTE_LENGTH_X25519P384_SECRET - Self::BYTE_LENGTH_X25519_SECRET) as u16 + && b.key_type_flags == (Self::ALGORITHM_EC_NIST_P384 | Self::FLAG_INCLUDES_SECRET) + { + Some(Self { + address: Address::from_bytes_fixed(&b.v0s.address)?, + x25519: b.v0s.x25519, + ed25519: b.v0s.ed25519, + p384: Some(IdentityP384Public { + ecdh: P384PublicKey::from_bytes(&b.ecdh)?, + ecdsa: P384PublicKey::from_bytes(&b.ecdsa)?, + ecdsa_self_signature: b.ecdsa_self_signature, + ed25519_self_signature: b.ed25519_self_signature, + }), + secret: Some(IdentitySecret { + x25519: C25519KeyPair::from_bytes(&b.v0s.x25519, &b.v0s.x25519_secret)?, + ed25519: Ed25519KeyPair::from_bytes(&b.v0s.ed25519, &b.v0s.ed25519_secret)?, + p384: Some(IdentityP384Secret { + ecdh: P384KeyPair::from_bytes(&b.ecdh, &b.ecdh_secret)?, + ecdsa: P384KeyPair::from_bytes(&b.ecdsa, &b.ecdsa_secret)?, + }), + }), + fingerprint: { + let mut sha = SHA384::new(); + sha.update(&b.v0s.address); + sha.update(&b.v0s.x25519); + sha.update(&b.v0s.ed25519); + sha.update(&[Self::ALGORITHM_EC_NIST_P384]); + sha.update(&b.ecdh); + sha.update(&b.ecdsa); + sha.finish() + }, + }) + } else { + None + } + } + } + } - let mut s = String::with_capacity(Self::MAX_MARSHAL_SIZE * 2); + /// Read an identity from a reader, inferring its total length from the stream. + pub fn read_bytes(r: &mut R) -> std::io::Result { + let mut buf = [0_u8; 512]; + r.read_exact(&mut buf[..Self::BYTE_LENGTH_X25519_PUBLIC])?; + let x25519_public = bytes_as_flat_object::(&buf); + let ext_len = u16::from_be_bytes(x25519_public.ext_len) as usize; + let obj_len = if x25519_public.secret_length == 0 { + let obj_len = ext_len + Self::BYTE_LENGTH_X25519_PUBLIC; + if ext_len > 0 { + r.read_exact(&mut buf[Self::BYTE_LENGTH_X25519_PUBLIC..obj_len])?; + } + obj_len + } else { + let obj_len = ext_len + Self::BYTE_LENGTH_X25519_SECRET; + if ext_len > 0 { + r.read_exact(&mut buf[Self::BYTE_LENGTH_X25519_PUBLIC..obj_len])?; + } + obj_len + }; + IdentityBytes::try_from(&buf[..obj_len]).map_or_else( + |_| Err(std::io::Error::new(std::io::ErrorKind::Other, "invalid identity")), + |b| Identity::from_bytes(&b).map_or_else(|| Err(std::io::Error::new(std::io::ErrorKind::Other, "invalid identity")), |id| Ok(id)), + ) + } + + fn to_string_internal(&self, include_private: bool) -> String { + let mut s = String::with_capacity(1024); s.push_str(self.address.to_string().as_str()); - if (include_algorithms & Self::ALGORITHM_X25519) != 0 { - s.push_str(":0:"); // 0 used for x25519 for legacy reasons just like in marshal() - s.push_str(hex::to_string(&self.c25519).as_str()); - s.push_str(hex::to_string(&self.ed25519).as_str()); - if self.secret.is_some() && include_private { - let secret = self.secret.as_ref().unwrap(); - s.push(':'); - s.push_str(hex::to_string(secret.c25519.secret_bytes().as_bytes()).as_str()); - s.push_str(hex::to_string(secret.ed25519.secret_bytes().as_bytes()).as_str()); - } else if include_p384 { - s.push(':'); - } + s.push_str(":0:"); // 0 used for x25519 for legacy reasons just like in marshal() + s.push_str(hex::to_string(&self.x25519).as_str()); + s.push_str(hex::to_string(&self.ed25519).as_str()); + if self.secret.is_some() && include_private { + let secret = self.secret.as_ref().unwrap(); + s.push(':'); + s.push_str(hex::to_string(secret.x25519.secret_bytes().as_bytes()).as_str()); + s.push_str(hex::to_string(secret.ed25519.secret_bytes().as_bytes()).as_str()); } - if include_p384 { - let p384 = self.p384.as_ref().unwrap(); - + if let Some(p384) = self.p384.as_ref() { + if self.secret.is_none() || !include_private { + s.push(':'); + } s.push_str(":2:"); // 2 == IDENTITY_ALGORITHM_EC_NIST_P384 - let p384_joined: [u8; P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE] = concat_arrays_4(p384.ecdh.as_bytes(), p384.ecdsa.as_bytes(), &p384.ecdsa_self_signature, &p384.ed25519_self_signature); + let p384_joined: [u8; P384_PUBLIC_KEY_SIZE + P384_PUBLIC_KEY_SIZE + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE] = + concat_arrays_4(p384.ecdh.as_bytes(), p384.ecdsa.as_bytes(), &p384.ecdsa_self_signature, &p384.ed25519_self_signature); s.push_str(base64::encode_config(p384_joined, base64::URL_SAFE_NO_PAD).as_str()); if self.secret.is_some() && include_private { let secret = self.secret.as_ref().unwrap(); if secret.p384.is_some() { let p384_secret = secret.p384.as_ref().unwrap(); - let p384_secret_joined: [u8; P384_SECRET_KEY_SIZE + P384_SECRET_KEY_SIZE] = concat_arrays_2(p384_secret.ecdh.secret_key_bytes().as_bytes(), p384_secret.ecdsa.secret_key_bytes().as_bytes()); + let p384_secret_joined: [u8; P384_SECRET_KEY_SIZE + P384_SECRET_KEY_SIZE] = + concat_arrays_2(p384_secret.ecdh.secret_key_bytes().as_bytes(), p384_secret.ecdsa.secret_key_bytes().as_bytes()); s.push(':'); s.push_str(base64::encode_config(p384_secret_joined, base64::URL_SAFE_NO_PAD).as_str()); } @@ -467,17 +632,21 @@ impl Identity { s } - /// Get this identity in string form with all ciphers and with secrets (if present) + #[inline(always)] + pub fn to_public_string(&self) -> String { + self.to_string_internal(false) + } + + #[inline(always)] pub fn to_secret_string(&self) -> String { - self.to_string_with_options(Self::ALGORITHM_ALL, true) + self.to_string_internal(true) } } impl ToString for Identity { - /// Get only the public portion of this identity as a string, including all cipher suites. #[inline(always)] fn to_string(&self) -> String { - self.to_string_with_options(Self::ALGORITHM_ALL, false) + self.to_string_internal(false) } } @@ -549,7 +718,7 @@ impl FromStr for Identity { Ok(Identity { address, - c25519: keys[0].as_slice()[0..32].try_into().unwrap(), + x25519: keys[0].as_slice()[0..32].try_into().unwrap(), ed25519: keys[0].as_slice()[32..64].try_into().unwrap(), p384: if keys[2].is_empty() { None @@ -573,7 +742,7 @@ impl FromStr for Identity { return Err(InvalidFormatError); } Some(IdentitySecret { - c25519: { + x25519: { let tmp = C25519KeyPair::from_bytes(&keys[0].as_slice()[0..32], &keys[1].as_slice()[0..32]); if tmp.is_none() { return Err(InvalidFormatError); @@ -614,161 +783,6 @@ impl FromStr for Identity { } } -impl Marshalable for Identity { - /// Current sanity limit for the size of a marshaled Identity - /// This is padded just a little up to 512 and can be increased if new key types are ever added. - const MAX_MARSHAL_SIZE: usize = 25 - + ADDRESS_SIZE - + C25519_PUBLIC_KEY_SIZE - + ED25519_PUBLIC_KEY_SIZE - + C25519_SECRET_KEY_SIZE - + ED25519_SECRET_KEY_SIZE - + P384_PUBLIC_KEY_SIZE - + P384_PUBLIC_KEY_SIZE - + P384_SECRET_KEY_SIZE - + P384_SECRET_KEY_SIZE - + P384_ECDSA_SIGNATURE_SIZE - + ED25519_SIGNATURE_SIZE; - - #[inline(always)] - fn marshal(&self, buf: &mut Buffer) -> std::io::Result<()> { - self.marshal_with_options(buf, Self::ALGORITHM_ALL, false) - } - - fn unmarshal(buf: &Buffer, cursor: &mut usize) -> std::io::Result { - let address = Address::from_bytes(buf.read_bytes_fixed::(cursor)?); - if !address.is_some() { - return std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid address")); - } - let address = address.unwrap(); - - let mut x25519_public: Option<(&[u8; C25519_PUBLIC_KEY_SIZE], &[u8; ED25519_PUBLIC_KEY_SIZE])> = None; - let mut x25519_secret: Option<(&[u8; C25519_SECRET_KEY_SIZE], &[u8; ED25519_SECRET_KEY_SIZE])> = None; - let mut p384_ecdh_ecdsa_public: Option<(P384PublicKey, P384PublicKey, &[u8; P384_ECDSA_SIGNATURE_SIZE], &[u8; ED25519_SIGNATURE_SIZE])> = None; - let mut p384_ecdh_ecdsa_secret: Option<(&[u8; P384_SECRET_KEY_SIZE], &[u8; P384_SECRET_KEY_SIZE])> = None; - - let mut eof = buf.len(); - while *cursor < eof { - let algorithm = buf.read_u8(cursor)?; - match algorithm { - 0_u8 | Self::ALGORITHM_X25519 => { - let a = buf.read_bytes_fixed::(cursor)?; - let b = buf.read_bytes_fixed::(cursor)?; - x25519_public = Some((a, b)); - let sec_size = buf.read_u8(cursor)?; - if sec_size == (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8 { - let a = buf.read_bytes_fixed::(cursor)?; - let b = buf.read_bytes_fixed::(cursor)?; - x25519_secret = Some((a, b)); - } else if sec_size != 0 { - return std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid x25519 secret")); - } - } - 0x03 => { - // This isn't an algorithm; each algorithm is identified by just one bit. This - // indicates the total size of the section after the x25519 keys for backward - // compatibility. See comments in marshal(). New versions can ignore this field. - let bytes_remaining = buf.read_u16(cursor)? as usize; - eof = *cursor + bytes_remaining; - } - Self::ALGORITHM_EC_NIST_P384 => { - let field_length = buf.read_varint(cursor)?; - let has_secret = if field_length == ((P384_PUBLIC_KEY_SIZE * 2) + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE) as u64 { - false - } else { - if field_length == ((P384_PUBLIC_KEY_SIZE * 2) + P384_ECDSA_SIGNATURE_SIZE + ED25519_SIGNATURE_SIZE + (P384_SECRET_KEY_SIZE * 2)) as u64 { - true - } else { - return std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid p384 public key")); - } - }; - - let a = P384PublicKey::from_bytes(buf.read_bytes_fixed::(cursor)?); - let b = P384PublicKey::from_bytes(buf.read_bytes_fixed::(cursor)?); - let c = buf.read_bytes_fixed::(cursor)?; - let d = buf.read_bytes_fixed::(cursor)?; - if a.is_none() || b.is_none() { - return std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid p384 public key")); - } - p384_ecdh_ecdsa_public = Some((a.unwrap(), b.unwrap(), c, d)); - - if has_secret { - let a = buf.read_bytes_fixed::(cursor)?; - let b = buf.read_bytes_fixed::(cursor)?; - p384_ecdh_ecdsa_secret = Some((a, b)); - } - } - _ => { - *cursor += buf.read_varint(cursor)? as usize; - } - } - } - - if x25519_public.is_none() { - return std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "x25519 key missing")); - } - let x25519_public = x25519_public.unwrap(); - - let mut sha = SHA384::new(); - sha.update(&address.to_bytes()); - sha.update(x25519_public.0); - sha.update(x25519_public.1); - if p384_ecdh_ecdsa_public.is_some() { - let p384 = p384_ecdh_ecdsa_public.as_ref().unwrap(); - sha.update(&[Self::ALGORITHM_EC_NIST_P384]); - sha.update(p384.0.as_bytes()); - sha.update(p384.1.as_bytes()); - } - - Ok(Identity { - address, - c25519: x25519_public.0.clone(), - ed25519: x25519_public.1.clone(), - p384: if p384_ecdh_ecdsa_public.is_some() { - let p384_ecdh_ecdsa_public = p384_ecdh_ecdsa_public.as_ref().unwrap(); - Some(IdentityP384Public { - ecdh: p384_ecdh_ecdsa_public.0.clone(), - ecdsa: p384_ecdh_ecdsa_public.1.clone(), - ecdsa_self_signature: p384_ecdh_ecdsa_public.2.clone(), - ed25519_self_signature: p384_ecdh_ecdsa_public.3.clone(), - }) - } else { - None - }, - secret: if x25519_secret.is_some() { - let x25519_secret = x25519_secret.unwrap(); - let c25519_secret = C25519KeyPair::from_bytes(x25519_public.0, x25519_secret.0); - let ed25519_secret = Ed25519KeyPair::from_bytes(x25519_public.1, x25519_secret.1); - if c25519_secret.is_none() || ed25519_secret.is_none() { - return std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "x25519 public key invalid")); - } - Some(IdentitySecret { - c25519: c25519_secret.unwrap(), - ed25519: ed25519_secret.unwrap(), - p384: if p384_ecdh_ecdsa_secret.is_some() && p384_ecdh_ecdsa_public.is_some() { - let p384_ecdh_ecdsa_public = p384_ecdh_ecdsa_public.as_ref().unwrap(); - let p384_ecdh_ecdsa_secret = p384_ecdh_ecdsa_secret.as_ref().unwrap(); - let p384_ecdh_secret = P384KeyPair::from_bytes(p384_ecdh_ecdsa_public.0.as_bytes(), p384_ecdh_ecdsa_secret.0); - let p384_ecdsa_secret = P384KeyPair::from_bytes(p384_ecdh_ecdsa_public.1.as_bytes(), p384_ecdh_ecdsa_secret.1); - if p384_ecdh_secret.is_none() || p384_ecdsa_secret.is_none() { - return std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "p384 secret key invalid")); - } - Some(IdentityP384Secret { - ecdh: p384_ecdh_secret.unwrap(), - ecdsa: p384_ecdsa_secret.unwrap(), - }) - } else { - None - }, - }) - } else { - None - }, - fingerprint: sha.finish(), - }) - } -} - impl PartialEq for Identity { #[inline(always)] fn eq(&self, other: &Self) -> bool { @@ -779,6 +793,7 @@ impl PartialEq for Identity { impl Eq for Identity {} impl Ord for Identity { + #[inline(always)] fn cmp(&self, other: &Self) -> Ordering { self.address.cmp(&other.address).then_with(|| self.fingerprint.cmp(&other.fingerprint)) } @@ -798,17 +813,121 @@ impl Hash for Identity { } } +mod packed { + use super::*; + + #[derive(Copy, Clone)] + #[repr(C, packed)] + pub(super) struct V0 { + pub address: [u8; ADDRESS_SIZE], + pub key_type: u8, + pub x25519: [u8; C25519_PUBLIC_KEY_SIZE], + pub ed25519: [u8; ED25519_PUBLIC_KEY_SIZE], + pub secret_length: u8, + pub reserved: u8, + pub ext_len: [u8; 2], + } + + #[derive(Copy, Clone)] + #[repr(C, packed)] + pub(super) struct V0S { + pub address: [u8; ADDRESS_SIZE], + pub key_type: u8, + pub x25519: [u8; C25519_PUBLIC_KEY_SIZE], + pub ed25519: [u8; ED25519_PUBLIC_KEY_SIZE], + pub secret_length: u8, + pub x25519_secret: [u8; C25519_SECRET_KEY_SIZE], + pub ed25519_secret: [u8; ED25519_SECRET_KEY_SIZE], + pub reserved: u8, + pub ext_len: [u8; 2], + } + + #[derive(Copy, Clone)] + #[repr(C, packed)] + pub(super) struct V1 { + pub v0: V0, + pub key_type_flags: u8, + pub ecdh: [u8; P384_PUBLIC_KEY_SIZE], + pub ecdsa: [u8; P384_PUBLIC_KEY_SIZE], + pub ecdsa_self_signature: [u8; P384_ECDSA_SIGNATURE_SIZE], + pub ed25519_self_signature: [u8; ED25519_SIGNATURE_SIZE], + } + + #[derive(Copy, Clone)] + #[repr(C, packed)] + pub(super) struct V1S { + pub v0s: V0S, + pub key_type_flags: u8, + pub ecdh: [u8; P384_PUBLIC_KEY_SIZE], + pub ecdsa: [u8; P384_PUBLIC_KEY_SIZE], + pub ecdsa_self_signature: [u8; P384_ECDSA_SIGNATURE_SIZE], + pub ed25519_self_signature: [u8; ED25519_SIGNATURE_SIZE], + pub ecdh_secret: [u8; P384_SECRET_KEY_SIZE], + pub ecdsa_secret: [u8; P384_SECRET_KEY_SIZE], + } + + unsafe impl AlignmentNeutral for V0 {} + unsafe impl AlignmentNeutral for V0S {} + unsafe impl AlignmentNeutral for V1 {} + unsafe impl AlignmentNeutral for V1S {} +} + +/// Identity rendered as a flat byte array. +/// +/// Note that try_from() and into() here perform a straight conversion or cast between this and +/// byte slices. They don't check the actual format of the contained identity data. +#[derive(Clone, PartialEq, Eq)] +pub enum IdentityBytes { + X25519Public([u8; Identity::BYTE_LENGTH_X25519_PUBLIC]), + X25519Secret([u8; Identity::BYTE_LENGTH_X25519_SECRET]), + X25519P384Public([u8; Identity::BYTE_LENGTH_X25519P384_PUBLIC]), + X25519P384Secret([u8; Identity::BYTE_LENGTH_X25519P384_SECRET]), +} + +impl<'a> From<&'a IdentityBytes> for &'a [u8] { + fn from(b: &'a IdentityBytes) -> Self { + match b { + IdentityBytes::X25519Public(b) => b, + IdentityBytes::X25519Secret(b) => b, + IdentityBytes::X25519P384Public(b) => b, + IdentityBytes::X25519P384Secret(b) => b, + } + } +} + +impl TryFrom<&[u8]> for IdentityBytes { + type Error = std::array::TryFromSliceError; + + fn try_from(b: &[u8]) -> Result { + match b.len() { + Identity::BYTE_LENGTH_X25519_PUBLIC => Ok(Self::X25519Public(b.try_into()?)), + Identity::BYTE_LENGTH_X25519_SECRET => Ok(Self::X25519Secret(b.try_into()?)), + Identity::BYTE_LENGTH_X25519P384_PUBLIC => Ok(Self::X25519P384Public(b.try_into()?)), + _ => Ok(Self::X25519P384Secret(b.try_into()?)), + } + } +} + +impl Drop for IdentityBytes { + fn drop(&mut self) { + // This can contain secrets, so zero it like Secret<> in the crypto core. + unsafe { + for i in 0..std::mem::size_of::() { + std::ptr::write_volatile((self as *mut Self).cast::().add(i), 0); + } + } + } +} + impl Serialize for Identity { fn serialize(&self, serializer: S) -> Result where S: Serializer, { if serializer.is_human_readable() { - serializer.serialize_str(self.to_string_with_options(Self::ALGORITHM_ALL, false).as_str()) + serializer.serialize_str(self.to_public_string().as_str()) } else { - let mut tmp: Buffer<{ Self::MAX_MARSHAL_SIZE }> = Buffer::new(); - assert!(self.marshal_with_options(&mut tmp, Self::ALGORITHM_ALL, false).is_ok()); - serializer.serialize_bytes(tmp.as_bytes()) + serializer.serialize_bytes((&self.to_public_bytes()).into()) } } } @@ -826,14 +945,7 @@ impl<'de> serde::de::Visitor<'de> for IdentityVisitor { where E: serde::de::Error, { - if v.len() <= Identity::MAX_MARSHAL_SIZE { - let mut tmp: Buffer<{ Identity::MAX_MARSHAL_SIZE }> = Buffer::new(); - let _ = tmp.append_bytes(v); - let mut cursor = 0; - Identity::unmarshal(&tmp, &mut cursor).map_err(|e| E::custom(e.to_string())) - } else { - Err(E::custom("object too large")) - } + IdentityBytes::try_from(v).map_or_else(|e| Err(E::custom(e.to_string())), |b| Identity::from_bytes(&b).map_or_else(|| Err(E::custom("invalid identity")), |id| Ok(id))) } fn visit_str(self, v: &str) -> Result @@ -859,7 +971,6 @@ impl<'de> Deserialize<'de> for Identity { #[cfg(test)] mod tests { - use crate::util::marshalable::Marshalable; use crate::vl1::identity::*; use std::str::FromStr; use zerotier_core_crypto::hex; @@ -892,10 +1003,10 @@ mod tests { "ba0c4a4edd:0:4b75790dce1979b4cec38ca1eb81e0f348f757047c4ad5e8a463fe54f32142739ffd8c0bc9c95a45572d96173a11def1e653e6975343e4bc78d5b504e023aab8:28fa6bf3c103186c41575c91ee86887d21e0bdf77cdf4c36c9430c32e83affbee0b04da61312f4c990a18f2acf9031a6a2c4c69362f79f7f6d5621a3c8abf33c", ]; const GOOD_V1_IDENTITIES: [&'static str; 4] = [ - "174cd00112:0:fd7e144befe03a8bca114094f576a6848224f35ef2c764f73d4b6f51ce54392127163722755be3e1de4375bec6d704e823acfa40180a39b7d76600c7776483b6::2:A9WKCt_BhL9EnKAb8SnPisFbCIXNDxFbtTxZiTjki9t5cu1xqEIOjk94s4r2CEaR5gOdpDyUGcoY0e1JjRRFK3CzVivW35eUhMKS1qQorcts35bYblMASQGp9ek047ROlKuzq4M5a-2Ymqb_fo1lhPxhaxLTmgsjmtllJJNknSOwGGYvQXukwzH4Vf0E-OWmkGrSWo6n7FkbuHwvKe7oPbhKwRQU03ya3-kM5vHGBglTOnN1NceXZvKwUhSiQ0zynt4OHrM5eJFlX3rE9mal5ml0l6CYs0Wh52byTHcav8A6tiSGNxxqU-BJ1EYEBG_kWB94gvGwImmBpaAR0xKfndIJ", - "4a23204ebb:0:ff61cddca9062501edc4390a8f218728fd58876ecfe2d757f611b9895d8e4e3a9cf8c6b49e18d5112f15c1a04715f0a579c2c43d7af0f3bb81de9a05135dcfcb::2:A4C5X0VLw98-g2Nc0ksfF50rt3-G-K5GGwmam2jUOGuz0IUH6cbryz3p4QjxFVEh9wM8VJUHF6yN7dpTwTWp6UMTVG5pFH6jSel-Z8GTSOJi3sybB-PFp0huY9oc0OOg1yuGbzKJFodkRmSCTku935hWVcV01HtNpNIScouXNVLIwDnxMd7iJ5J2pLJLS79Ofbf5VJWNXSIm4Ykse1BIv6vzbPZToQg_PXuIx8LFY1pGm8kb2Hx6FRUEDtP6jc3q5BDwVO8qVqatvpCTyjXWdBBURynH2FnmHeH_8u0mYse__wJoK5ICDRTxDKWWBOAOcS9pEQpjLuHnQex5cz9VI-gM", - "47e4f45e96:0:8a159c1ae1a81fb9f6d12027533e98178fca97c02edcb3b6f08c59578d54651cde7c596f9b7a8bb001b5ca7337ae99321a046bfcb8d5bdfd68a184a918000e7f::2:Aq0WCQ2jorXo2hlxKV8lQ0GX46DuisOSIRR8V3kg9oFbvnfa1c5W2lLMFtkqVqccAQPNPfU-Qfxh9lFUDKWNeS_bCx9Akt5Kw2yBi28eeySQP1DcHek4nVez9DG5-aTqnyyKnU3fNX34LuqMdTxZws4id33VXicY-sqKRobpyqFosC1U1SIiwqgSy290WBj_RKeZFU7QQF26cmbVF6bZ-OdYpcmNhdu_yzZ42Hh2W4duBHmJG1gDKGD-dXByaJVcnLIDwUMne8KSDZ7VaM_YfxWBmBBUqWUIpeeHnKLIlgF2ZJKgdUb8xzfUk0WzPkJfu2dcYduYGtVAG7r7jMatffMH", - "f172595d9d:0:739b4146fc7fff9d0234bd37b6d8c846b52bbad6f286bfc725580214b3a14149bf013dc23080d844a8cb1a0a32a6bb7caa455eeedc356660bee6cf80a7b586c1::2:A1_KXkfl5aHO6r7KmEf39Y5enSxWdiiVy6B5ZBWbhsT4sZV1Aqh4c_K2dH7s_QAydAIi-jrgvKshER7Dfn__xArPU_zWNvcW0-rCY3D0_y-Lr6b8_OjeZeN3Ry9cFkMhAyJE0x_n4uKGgFvtYFDjAtnGIzzzfnNf_h0dOFHZMRwcFRxtvo-e2hz-PuDsEQ_s350phi9N1Djtcy6aj-Cn_A0KzNpA5HFlWoOHTcNUDjZ64Eus47wemNl4lt3PBgQdcsk7MPsq122Da_LalAk_26CnbNzwifsfRnXHbHn7mf14r7YBb-8r-88LzL_Rtlnvcvhm6ib4pGFpv7TVkmkLvBoD", + "75a8a7a199:0:6322fe1ca7941571458bb0bd14faff0af915c2ca5ea5856a682de1040c8cbd1f79054a0c052b253316b17abd9cf34609c6ac0e26dd85ad169c26aa980a69d5e0:b8fcd2a25a0708176d81455a7b576b6835d87cc7b6b4701c914d65688f730f6d1a725018472570440e0ba1d6038be81e3415e8ba6dfa685ae2b582b12b90ad67:2:Az1IZD-cuJS11XWhCMEc6ZPwMpM_nQOwCHqHJaOywRxtkU9UC8BfOdGxYet-7fh3lgMxKntz_iqWD-7PAXoTpg2bDo-hKRT4zLkNm-KnUvqjiXl9W7RLYhvVd_qUXxQlY709m9hQ-omy_zRi3ZIysOMDaPwfyuwWUzhB09FiWn-MjiiUrujqqx66VfZ9rx_u63ZUvrTxWu6motVbS6eXMemIGUtU6UyhIXDIk2_WJceF9k7Bs7C7Ay00zuCTEHIH878e7LR4qPNnPPDRxQV3y5rO7WHsutl9wHSEINJAtYz7xJdk7IuccJfnDzhODroATwdSNtG0sASJU9UToUvWioEF:P5zAldkVK9P5uso9iOQ2hqgXS-J326gK-Z3l1_ZZehes8-0OYxUoHfs80ddG1MVGIOb3AFA0vi7S93wNUxRkg4kzzUl5rFAmy85iuiMCRpPY-8SG1500RI4dhWkRTuPR", + "a311a9d217:0:477c48a712daf785d7d5f2d2d693361047b102dbcd7eea2d6cabcb7149b0806815e4c4d7cd03a9b3ac87c3e27161724a5ad5c52d1c487c5484869e55e34966e5:f06d080800e3529ac2f5229cb1c07c6761fa1f2f1bc2447807f814e38252b24d6faf2da1ab6fb7db2460081dae877b9658714f3a2976f9ffcbe432151f69c3eb:2:A9_zhw9S4Kh-iOfCqEYjKg9Hyd9OabtE3F4FwlX87AEbeGRl0bRTOnqRNLpZPbwtNwJVJwKaY7dFCSc7Z03TJDSnFwz4HXy5KfdLEMHkJUv8ebfgNMuyc52f5ku3eyNE41uduXppdKzDbEJXiyBElMzaFBwK0q1zuDrRM1sjlaUto6bwsB8wPFC08PHsU98-sgJQXsWYYIlzhFRlW5Cjm-4QAfGOQ4BX1M5LLfzwxV0G4s895iLTAShb_fms8ITqiOGL4CjLFX2HYJ2fYanzrEFH3eYSQjZw0iu1iXVnrcaHpeICuFOIbbrR3Uhewa_kwyzHqIa1pUPONYak6Rje5PcD:1tf-W-tMafIY3VprzVNvcBtyC9i0JQCkVwUpTo7ej8wwdbRHntijqxcyZGEwQ0f_c-sucRPxzYIpA9l5V-QnXRNbWNNuA-1lHINJUVD4DCEc9Km2yJiWl2AmXVpN51KE", + "4a74fb8416:0:a6ce3d09a384e6d0b28c1f18ca63a95e3d15fb6eaa3177b3b6bc9f11c3dd1b312b50aaed5b919bafadd9c1e902e1f1f4d3944d43f289a01f9408974b9fe49455:10f8f8557856274c9d78b111ce2fe5e696e43a3139d6c685c6d620bb4946427873c80272f3573f460e156f8ac9eb4a8cf7aa7e9e7df152491be23405dd0fdf86:2:A55YK8R0MYVQi47wlDZxk7Ja3o_ahA88AxzmzQon79MF8HmMbxExwovGEk2TkuQ4xQLgWsM_vs-N-nlJUtUkxsd3ECHYs2s68LxzvpcmROIxakMr02ibclwJGnA_deivI2so8RjGsv4d18Up9lt3qNkor8N6SBuyhhEYPjjwFYKMn7oCqj_LDxVshYa9YtcHtjTABQPiKJaqJzpyZfquKrrofr18SBqJOPgtsw6Omuqv-IY384a-uT-51ic-RhW5eVqWQdVFj3-mIJEWUXIk9TKaAuh5soLFK9awXogG_cXCHTzQ9_POiJ8KW8VSE7OzC46wzYgllWXUKzKZCmwJ-7YP:IoLCcuTeUPiRv_TSSBkPX9ps0pfpcww9WR39SQsi4Q0H4sDk1q5tIgeLMNvwk2weIZpMo28n0evlyvuXw1P3LEEm2wY97D3ceGgwKgnAHwERLh6pM9de1_nZYAQYZOiS", + "466906c1fc:0:7e1df36857083f367b0301af07709748d074eef482f500493280abf22a95e3260ef73caf4496d4b847f6b6f0b9e45abc46c3fa4bb46bb35af207dac98a713609:58ff3e8bac5f1960152d4db8f149c426da2c5b29e53a4e22d86e8b10aee9c44453cd4d19103dd63fa920e9a6a7ff52ac326ed13499eea7b05a44911aaff85524:2:A4BWd-ehtSraBUI7NY_XgI-WHMbN6yVlAJQDiRH8vTk4824OCyNjQ19K5jGuLeEfawJtXG5njz03bvezpOToESZ0HKxPMz-6nz3fYpXMi-0bM6yyNij37t-Gb1_Ee_7JrLplVNfVUKokeG3I6W2W0pWWuSYeyfSSgXwdc3MkfIbLU7xPcR-7UU1FXHzSRRq9OxO3QMe15kVYpSFoxNGCyaMzDwgSLiXxqD3exi9zlKnlZl743vZuZmCjwq0bA7LT20GwT7Zr4qSnC7_XWokj8vofNlUKUziF5zfB5uM3Ml9_zz0HRc-oj42zrdWkXuJbJ7zXjXiFy82kNUllKykoTDoJ:bgOVQS8FrzIrIq_pNvwQfYDJ9HdxAPeAvfN_J8XxDFO2gvV-LGZmR_WWsiNiXVUhF9Y0mU_yZjw7kbenObxYua0OpTXpn-9TAQS1eYDCvR015eAGzpwFjlT85HJWbYfO", ]; #[test] @@ -903,12 +1014,11 @@ mod tests { let gen = Identity::generate(); assert!(gen.agree(&gen).is_some()); assert!(gen.validate_identity()); - let bytes = gen.to_buffer_with_options(Identity::ALGORITHM_ALL, true); - let string = gen.to_string_with_options(Identity::ALGORITHM_ALL, true); + let bytes = gen.to_secret_bytes().unwrap(); + let string = gen.to_secret_string(); assert!(Identity::from_str(string.as_str()).unwrap().eq(&gen)); - let mut cursor = 0_usize; - let gen_unmarshaled = Identity::unmarshal(&bytes, &mut cursor).unwrap(); + let gen_unmarshaled = Identity::from_bytes(&bytes).unwrap(); assert!(gen_unmarshaled.secret.is_some()); if !gen_unmarshaled.eq(&gen) { println!("{} != {}", hex::to_string(&gen_unmarshaled.fingerprint), hex::to_string(&gen.fingerprint)); @@ -922,20 +1032,18 @@ mod tests { for id_str in GOOD_V0_IDENTITIES { let mut id = Identity::from_str(id_str).unwrap(); - assert_eq!(id.to_string_with_options(Identity::ALGORITHM_ALL, true).as_str(), id_str); + assert_eq!(id.to_secret_string().as_str(), id_str); assert!(id.validate_identity()); assert!(id.p384.is_none()); - let idb = id.to_buffer_with_options(Identity::ALGORITHM_ALL, true); - let mut cursor = 0; - let id_unmarshal = Identity::unmarshal(&idb, &mut cursor).unwrap(); + let idb = id.to_secret_bytes().unwrap(); + let id_unmarshal = Identity::from_bytes(&idb).unwrap(); assert!(id == id_unmarshal); assert!(id_unmarshal.secret.is_some()); - let idb2 = id_unmarshal.to_buffer_with_options(Identity::ALGORITHM_ALL, false); - cursor = 0; - let id_unmarshal2 = Identity::unmarshal(&idb2, &mut cursor).unwrap(); + let idb2 = id_unmarshal.to_public_bytes(); + let id_unmarshal2 = Identity::from_bytes(&idb2).unwrap(); assert!(id_unmarshal2 == id_unmarshal); assert!(id_unmarshal2 == id); assert!(id_unmarshal2.secret.is_none()); @@ -953,19 +1061,18 @@ mod tests { } for id_str in GOOD_V1_IDENTITIES { let id = Identity::from_str(id_str).unwrap(); - assert_eq!(id.to_string_with_options(Identity::ALGORITHM_ALL, true).as_str(), id_str); + assert_eq!(id.to_secret_string().as_str(), id_str); assert!(id.validate_identity()); assert!(id.p384.is_some()); + assert!(id.secret.as_ref().unwrap().p384.is_some()); - let idb = id.to_buffer_with_options(Identity::ALGORITHM_ALL, true); - let mut cursor = 0; - let id_unmarshal = Identity::unmarshal(&idb, &mut cursor).unwrap(); + let idb = id.to_secret_bytes().unwrap(); + let id_unmarshal = Identity::from_bytes(&idb).unwrap(); assert!(id == id_unmarshal); - cursor = 0; - let idb2 = id_unmarshal.to_buffer_with_options(Identity::ALGORITHM_ALL, false); - let id_unmarshal2 = Identity::unmarshal(&idb2, &mut cursor).unwrap(); + let idb2 = id_unmarshal.to_public_bytes(); + let id_unmarshal2 = Identity::from_bytes(&idb2).unwrap(); assert!(id_unmarshal2 == id_unmarshal); assert!(id_unmarshal2 == id); diff --git a/zerotier-network-hypervisor/src/vl1/mod.rs b/zerotier-network-hypervisor/src/vl1/mod.rs index 583b3fa18..f17a10e5c 100644 --- a/zerotier-network-hypervisor/src/vl1/mod.rs +++ b/zerotier-network-hypervisor/src/vl1/mod.rs @@ -11,7 +11,6 @@ mod mac; mod path; mod peer; mod rootset; -mod session; mod symmetricsecret; mod whoisqueue; diff --git a/zerotier-network-hypervisor/src/vl1/node.rs b/zerotier-network-hypervisor/src/vl1/node.rs index 76fc52ed4..b97143999 100644 --- a/zerotier-network-hypervisor/src/vl1/node.rs +++ b/zerotier-network-hypervisor/src/vl1/node.rs @@ -68,14 +68,7 @@ pub trait SystemInterface: Sync + Send + 'static { /// For endpoint types that support a packet TTL, the implementation may set the TTL /// if the 'ttl' parameter is not zero. If the parameter is zero or TTL setting is not /// supported, the default TTL should be used. - async fn wire_send( - &self, - endpoint: &Endpoint, - local_socket: Option<&Self::LocalSocket>, - local_interface: Option<&Self::LocalInterface>, - data: &[&[u8]], - packet_ttl: u8, - ) -> bool; + async fn wire_send(&self, endpoint: &Endpoint, local_socket: Option<&Self::LocalSocket>, local_interface: Option<&Self::LocalInterface>, data: &[&[u8]], packet_ttl: u8) -> bool; /// Called to check and see if a physical address should be used for ZeroTier traffic to a node. async fn check_path(&self, id: &Identity, endpoint: &Endpoint, local_socket: Option<&Self::LocalSocket>, local_interface: Option<&Self::LocalInterface>) -> bool; @@ -101,23 +94,13 @@ pub trait InnerProtocolInterface: Sync + Send + 'static { /// Handle a packet, returning true if it was handled by the next layer. /// /// Do not attempt to handle OK or ERROR. Instead implement handle_ok() and handle_error(). - async fn handle_packet( - &self, - source: &Peer, - source_path: &Path, - forward_secrecy: bool, - extended_authentication: bool, - verb: u8, - payload: &PacketBuffer, - ) -> bool; + async fn handle_packet(&self, source: &Peer, source_path: &Path, verb: u8, payload: &PacketBuffer) -> bool; /// Handle errors, returning true if the error was recognized. async fn handle_error( &self, source: &Peer, source_path: &Path, - forward_secrecy: bool, - extended_authentication: bool, in_re_verb: u8, in_re_message_id: u64, error_code: u8, @@ -126,17 +109,7 @@ pub trait InnerProtocolInterface: Sync + Send + 'static { ) -> bool; /// Handle an OK, returing true if the OK was recognized. - async fn handle_ok( - &self, - source: &Peer, - source_path: &Path, - forward_secrecy: bool, - extended_authentication: bool, - in_re_verb: u8, - in_re_message_id: u64, - payload: &PacketBuffer, - cursor: &mut usize, - ) -> bool; + async fn handle_ok(&self, source: &Peer, source_path: &Path, in_re_verb: u8, in_re_message_id: u64, payload: &PacketBuffer, cursor: &mut usize) -> bool; /// Check if this remote peer has a trust relationship with this node. /// @@ -455,10 +428,7 @@ impl Node { ))); } for i in bad_identities.iter() { - si.event(Event::SecurityWarning(format!( - "bad identity detected for address {} in at least one root set, ignoring (error creating peer object)", - i.address.to_string() - ))); + si.event(Event::SecurityWarning(format!("bad identity detected for address {} in at least one root set, ignoring (error creating peer object)", i.address.to_string()))); } let mut new_root_identities: Vec = new_roots.iter().map(|(p, _)| p.identity.clone()).collect(); @@ -590,17 +560,9 @@ impl Node { if fragment_header.is_fragment() { #[cfg(debug_assertions)] let fragment_header_id = u64::from_be_bytes(fragment_header.id); - debug_event!( - si, - "[vl1] #{:0>16x} fragment {} of {} received", - u64::from_be_bytes(fragment_header.id), - fragment_header.fragment_no(), - fragment_header.total_fragments() - ); + debug_event!(si, "[vl1] #{:0>16x} fragment {} of {} received", u64::from_be_bytes(fragment_header.id), fragment_header.fragment_no(), fragment_header.total_fragments()); - if let Some(assembled_packet) = - path.receive_fragment(fragment_header.packet_id(), fragment_header.fragment_no(), fragment_header.total_fragments(), data, time_ticks) - { + if let Some(assembled_packet) = path.receive_fragment(fragment_header.packet_id(), fragment_header.fragment_no(), fragment_header.total_fragments(), data, time_ticks) { if let Some(frag0) = assembled_packet.frags[0].as_ref() { #[cfg(debug_assertions)] debug_event!(si, "[vl1] #{:0>16x} packet fully assembled!", fragment_header_id); @@ -608,8 +570,7 @@ impl Node { if let Ok(packet_header) = frag0.struct_at::(0) { if let Some(source) = Address::from_bytes(&packet_header.src) { if let Some(peer) = self.peer(source) { - peer.receive(self, si, ph, time_ticks, &path, &packet_header, frag0, &assembled_packet.frags[1..(assembled_packet.have as usize)]) - .await; + peer.receive(self, si, ph, time_ticks, &path, &packet_header, frag0, &assembled_packet.frags[1..(assembled_packet.have as usize)]).await; } else { self.whois.query(self, si, source, Some(QueuedPacket::Fragmented(assembled_packet))); } diff --git a/zerotier-network-hypervisor/src/vl1/peer.rs b/zerotier-network-hypervisor/src/vl1/peer.rs index 8051c1b4a..8651f0b61 100644 --- a/zerotier-network-hypervisor/src/vl1/peer.rs +++ b/zerotier-network-hypervisor/src/vl1/peer.rs @@ -7,12 +7,11 @@ use std::sync::{Arc, Weak}; use parking_lot::{Mutex, RwLock}; -use zerotier_core_crypto::aes_gmac_siv::AesCtr; -use zerotier_core_crypto::hash::*; -use zerotier_core_crypto::random::{get_bytes_secure, next_u64_secure}; +use zerotier_core_crypto::random::next_u64_secure; use zerotier_core_crypto::salsa::Salsa; use zerotier_core_crypto::secret::Secret; +use crate::util::buffer::BufferReader; use crate::util::byte_array_range; use crate::util::canonicalobject::CanonicalObject; use crate::util::debug_event; @@ -39,7 +38,6 @@ struct RemoteNodeInfo { reported_local_endpoints: HashMap, care_of: Option, remote_version: u64, - hello_extended_authentication: bool, remote_protocol_version: u8, } @@ -209,7 +207,6 @@ impl Peer { reported_local_endpoints: HashMap::new(), care_of: None, remote_version: 0, - hello_extended_authentication: false, remote_protocol_version: 0, }), } @@ -353,8 +350,8 @@ impl Peer { let mut pos = UDP_DEFAULT_MTU; let overrun_size = (packet_size - UDP_DEFAULT_MTU) as u32; - let fragment_count = (overrun_size / (UDP_DEFAULT_MTU - packet_constants::FRAGMENT_HEADER_SIZE) as u32) - + (((overrun_size % (UDP_DEFAULT_MTU - packet_constants::FRAGMENT_HEADER_SIZE) as u32) != 0) as u32); + let fragment_count = + (overrun_size / (UDP_DEFAULT_MTU - packet_constants::FRAGMENT_HEADER_SIZE) as u32) + (((overrun_size % (UDP_DEFAULT_MTU - packet_constants::FRAGMENT_HEADER_SIZE) as u32) != 0) as u32); debug_assert!(fragment_count <= packet_constants::FRAGMENT_COUNT_MAX as u32); let mut header = FragmentHeader { @@ -404,11 +401,8 @@ impl Peer { }; let max_fragment_size = if path.endpoint.requires_fragmentation() { UDP_DEFAULT_MTU } else { usize::MAX }; - let flags_cipher_hops = if packet.len() > max_fragment_size { - packet_constants::HEADER_FLAG_FRAGMENTED | security_constants::CIPHER_AES_GMAC_SIV - } else { - security_constants::CIPHER_AES_GMAC_SIV - }; + let flags_cipher_hops = + if packet.len() > max_fragment_size { packet_constants::HEADER_FLAG_FRAGMENTED | security_constants::CIPHER_AES_GMAC_SIV } else { security_constants::CIPHER_AES_GMAC_SIV }; let mut aes_gmac_siv = self.identity_symmetric_key.aes_gmac_siv.get(); aes_gmac_siv.encrypt_init(&self.next_message_id().to_ne_bytes()); @@ -509,47 +503,8 @@ impl Peer { } debug_assert_eq!(packet.len(), 41); + assert!(packet.append_bytes((&node.identity.to_public_bytes()).into()).is_ok()); - // Full identity of this node. - assert!(node.identity.marshal_with_options(&mut packet, Identity::ALGORITHM_ALL, false).is_ok()); - - // Create session meta-data and append length of this section. - let session_metadata = self.create_session_metadata(node, None); - let session_metadata_len = session_metadata.len() + 16; // plus nonce - assert!(session_metadata_len <= 0xffff); // sanity check, should be impossible - assert!(packet.append_u16(session_metadata_len as u16).is_ok()); - - // Append a 16-byte AES-CTR nonce. LEGACY: for compatibility the last two bytes of this nonce - // are in fact an encryption of two zeroes with Salsa20/12, which old nodes will interpret as - // zero "moons." New nodes will just use these as part of the nonce. - let mut nonce = get_bytes_secure::<16>(); - let mut salsa_iv = message_id.to_ne_bytes(); - salsa_iv[7] &= 0xf8; - Salsa::<12>::new(&self.identity_symmetric_key.key.0[0..32], &salsa_iv).crypt(&crate::util::ZEROES[..2], &mut nonce[14..]); - assert!(packet.append_bytes_fixed(&nonce).is_ok()); - - // Write session meta-data, encrypted. - nonce[12] &= 0x7f; // mask off the MSB of the 32-bit counter part of the CTR nonce for compatibility with AES libraries that don't wrap - let salted_key = Secret(hmac_sha384(&message_id.to_ne_bytes(), self.identity_symmetric_key.hello_private_section_key.as_bytes())); - let mut aes = AesCtr::new(&salted_key.as_bytes()[0..32]); - aes.init(&nonce); - aes.crypt(session_metadata.as_slice(), packet.append_bytes_get_mut(session_metadata.len()).unwrap()); - - // Set fragment flag if the packet will need to be fragmented. - if (packet.len() + SHA384_HASH_SIZE) > max_fragment_size { - set_packet_fragment_flag(&mut packet); - } - - // Seal packet with HMAC-SHA384 extended authentication. - let mut hmac = HMACSHA384::new(self.identity_symmetric_key.packet_hmac_key.as_bytes()); - hmac.update(&self.identity.fingerprint); - hmac.update(&node.identity.fingerprint); - hmac.update(&message_id.to_ne_bytes()); - hmac.update(&packet.as_bytes()[packet_constants::HEADER_SIZE..]); - assert!(packet.append_bytes_fixed(&hmac.finish()).is_ok()); - drop(hmac); - - // Set poly1305 in header, which is the only authentication for old nodes. let (_, poly1305_key) = salsa_poly_create(&self.identity_symmetric_key, packet.struct_at::(0).unwrap(), packet.len()); let mac = zerotier_core_crypto::poly1305::compute(&poly1305_key, packet.as_bytes_starting_at(packet_constants::HEADER_SIZE).unwrap()); packet.as_mut()[packet_constants::MAC_FIELD_INDEX..packet_constants::MAC_FIELD_INDEX + 8].copy_from_slice(&mac[0..8]); @@ -601,24 +556,6 @@ impl Peer { }; if let Ok(mut verb) = payload.u8_at(0) { - let extended_authentication = (verb & packet_constants::VERB_FLAG_EXTENDED_AUTHENTICATION) != 0; - if extended_authentication { - if payload.len() >= SHA384_HASH_SIZE { - let actual_end_of_payload = payload.len() - SHA384_HASH_SIZE; - let mut hmac = HMACSHA384::new(self.identity_symmetric_key.packet_hmac_key.as_bytes()); - hmac.update(&node.identity.fingerprint); - hmac.update(&self.identity.fingerprint); - hmac.update(&message_id.to_ne_bytes()); - hmac.update(&payload.as_bytes()[..actual_end_of_payload]); - if !hmac.finish().eq(&payload.as_bytes()[actual_end_of_payload..]) { - return false; - } - payload.set_size(actual_end_of_payload); - } else { - return false; - } - } - if (verb & packet_constants::VERB_FLAG_COMPRESSED) != 0 { let mut decompressed_payload = [0_u8; packet_constants::SIZE_MAX]; decompressed_payload[0] = verb; @@ -649,19 +586,15 @@ impl Peer { if match verb { verbs::VL1_NOP => true, - verbs::VL1_HELLO => { - self.handle_incoming_hello(si, ph, node, time_ticks, message_id, source_path, packet_header.hops(), extended_authentication, &payload).await - } - verbs::VL1_ERROR => self.handle_incoming_error(si, ph, node, time_ticks, source_path, false, extended_authentication, &payload).await, - verbs::VL1_OK => { - self.handle_incoming_ok(si, ph, node, time_ticks, source_path, packet_header.hops(), path_is_known, false, extended_authentication, &payload).await - } + verbs::VL1_HELLO => self.handle_incoming_hello(si, ph, node, time_ticks, message_id, source_path, packet_header.hops(), &payload).await, + verbs::VL1_ERROR => self.handle_incoming_error(si, ph, node, time_ticks, source_path, &payload).await, + verbs::VL1_OK => self.handle_incoming_ok(si, ph, node, time_ticks, source_path, packet_header.hops(), path_is_known, &payload).await, verbs::VL1_WHOIS => self.handle_incoming_whois(si, ph, node, time_ticks, message_id, &payload).await, verbs::VL1_RENDEZVOUS => self.handle_incoming_rendezvous(si, node, time_ticks, message_id, source_path, &payload).await, verbs::VL1_ECHO => self.handle_incoming_echo(si, ph, node, time_ticks, message_id, &payload).await, verbs::VL1_PUSH_DIRECT_PATHS => self.handle_incoming_push_direct_paths(si, node, time_ticks, source_path, &payload).await, verbs::VL1_USER_MESSAGE => self.handle_incoming_user_message(si, node, time_ticks, source_path, &payload).await, - _ => ph.handle_packet(self, &source_path, false, extended_authentication, verb, &payload).await, + _ => ph.handle_packet(self, &source_path, verb, &payload).await, } { // This needs to be saved AFTER processing the packet since some message types may use it to try to filter for replays. self.last_incoming_message_id.store(message_id, Ordering::Relaxed); @@ -682,7 +615,6 @@ impl Peer { message_id: MessageId, source_path: &Arc>, hops: u8, - extended_authentication: bool, payload: &PacketBuffer, ) -> bool { if !(ph.has_trust_relationship(&self.identity) || node.this_node_is_root() || node.is_peer_root(self)) { @@ -692,55 +624,14 @@ impl Peer { let mut cursor = 0; if let Ok(hello_fixed_headers) = payload.read_struct::(&mut cursor) { - if let Ok(identity) = Identity::unmarshal(payload, &mut cursor) { + if let Ok(identity) = Identity::read_bytes(&mut BufferReader::new(payload, &mut cursor)) { if identity.eq(&self.identity) { { let mut remote_node_info = self.remote_node_info.write(); - remote_node_info.remote_protocol_version = hello_fixed_headers.version_proto; - remote_node_info.hello_extended_authentication = extended_authentication; remote_node_info.remote_version = (hello_fixed_headers.version_major as u64).wrapping_shl(48) | (hello_fixed_headers.version_minor as u64).wrapping_shl(32) | (u16::from_be_bytes(hello_fixed_headers.version_revision) as u64).wrapping_shl(16); - - if hello_fixed_headers.version_proto >= 20 { - let mut session_metadata_len = payload.read_u16(&mut cursor).unwrap_or(0) as usize; - if session_metadata_len > 16 { - session_metadata_len -= 16; - if let Ok(nonce) = payload.read_bytes_fixed::<16>(&mut cursor) { - let mut nonce = nonce.clone(); - if let Ok(session_metadata) = payload.read_bytes(session_metadata_len, &mut cursor) { - let mut session_metadata = session_metadata.to_vec(); - - nonce[12] &= 0x7f; - let salted_key = Secret(hmac_sha384(&message_id.to_ne_bytes(), self.identity_symmetric_key.hello_private_section_key.as_bytes())); - let mut aes = AesCtr::new(&salted_key.as_bytes()[0..32]); - aes.init(&nonce); - aes.crypt_in_place(session_metadata.as_mut_slice()); - - if let Some(session_metadata) = Dictionary::from_bytes(session_metadata.as_slice()) { - if let Some(instance_id) = session_metadata.get_bytes(session_metadata::INSTANCE_ID) { - // For new nodes that send an instance ID, we can try to filter out any possible replaying of old - // packets. A replay of an old HELLO could be used to at least probe for the existence of a node at - // a given IP address and port by eliciting an OK(HELLO) reply. - if remote_node_info.remote_instance_id.eq(instance_id) { - if message_id.wrapping_sub(self.last_incoming_message_id.load(Ordering::Relaxed)) > PACKET_RESPONSE_COUNTER_DELTA_MAX - && time_ticks.wrapping_sub(self.last_receive_time_ticks.load(Ordering::Relaxed)) < PEER_HELLO_INTERVAL_MAX - { - debug_event!(si, "[vl1] dropping HELLO from {} due to possible replay of old packet", self.identity.address.to_string()); - return true; - } - } else { - remote_node_info.remote_instance_id.fill(0); - let l = instance_id.len().min(remote_node_info.remote_instance_id.len()); - (&mut remote_node_info.remote_instance_id[..l]).copy_from_slice(&instance_id[..l]); - } - } - } - } - } - } - } } let mut packet = PacketBuffer::new(); @@ -770,36 +661,14 @@ impl Peer { return false; } - async fn handle_incoming_error( - &self, - _si: &SI, - ph: &PH, - _node: &Node, - _time_ticks: i64, - source_path: &Arc>, - forward_secrecy: bool, - extended_authentication: bool, - payload: &PacketBuffer, - ) -> bool { + async fn handle_incoming_error(&self, _si: &SI, ph: &PH, _node: &Node, _time_ticks: i64, source_path: &Arc>, payload: &PacketBuffer) -> bool { let mut cursor = 0; if let Ok(error_header) = payload.read_struct::(&mut cursor) { let in_re_message_id: MessageId = u64::from_ne_bytes(error_header.in_re_message_id); if self.message_id_counter.load(Ordering::Relaxed).wrapping_sub(in_re_message_id) <= PACKET_RESPONSE_COUNTER_DELTA_MAX { match error_header.in_re_verb { _ => { - return ph - .handle_error( - self, - &source_path, - forward_secrecy, - extended_authentication, - error_header.in_re_verb, - in_re_message_id, - error_header.error_code, - payload, - &mut cursor, - ) - .await; + return ph.handle_error(self, &source_path, error_header.in_re_verb, in_re_message_id, error_header.error_code, payload, &mut cursor).await; } } } @@ -816,8 +685,6 @@ impl Peer { source_path: &Arc>, hops: u8, path_is_known: bool, - forward_secrecy: bool, - extended_authentication: bool, payload: &PacketBuffer, ) -> bool { let mut cursor = 0; @@ -892,12 +759,7 @@ impl Peer { let reported_endpoint2 = reported_endpoint.clone(); if self.remote_node_info.write().reported_local_endpoints.insert(reported_endpoint, time_ticks).is_none() { #[cfg(debug_assertions)] - debug_event!( - si, - "[vl1] {} reported new remote perspective, local endpoint: {}", - self.identity.address.to_string(), - reported_endpoint2.to_string() - ); + debug_event!(si, "[vl1] {} reported new remote perspective, local endpoint: {}", self.identity.address.to_string(), reported_endpoint2.to_string()); } } } @@ -914,7 +776,7 @@ impl Peer { verbs::VL1_WHOIS => { if node.is_peer_root(self) { while cursor < payload.len() { - if let Ok(_whois_response) = Identity::unmarshal(payload, &mut cursor) { + if let Ok(_whois_response) = Identity::read_bytes(&mut BufferReader::new(payload, &mut cursor)) { // TODO } else { break; @@ -926,7 +788,7 @@ impl Peer { } _ => { - return ph.handle_ok(self, &source_path, forward_secrecy, extended_authentication, ok_header.in_re_verb, in_re_message_id, payload, &mut cursor).await; + return ph.handle_ok(self, &source_path, ok_header.in_re_verb, in_re_message_id, payload, &mut cursor).await; } } } @@ -949,7 +811,7 @@ impl Peer { while cursor < payload.len() { if let Ok(zt_address) = Address::unmarshal(payload, &mut cursor) { if let Some(peer) = node.peer(zt_address) { - if !peer.identity.marshal(&mut packet).is_ok() { + if !packet.append_bytes((&peer.identity.to_public_bytes()).into()).is_ok() { debug_event!(si, "unexpected error serializing an identity into a WHOIS packet response"); return false; } diff --git a/zerotier-network-hypervisor/src/vl1/protocol.rs b/zerotier-network-hypervisor/src/vl1/protocol.rs index 169d02edd..46cbcf1d3 100644 --- a/zerotier-network-hypervisor/src/vl1/protocol.rs +++ b/zerotier-network-hypervisor/src/vl1/protocol.rs @@ -219,10 +219,10 @@ pub mod security_constants { pub const KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1: u8 = b'1'; /// KBKDF usage label for AES-GCM session keys. - pub const KBKDF_KEY_USAGE_LABEL_AES_GCM_SESSION_KEY: u8 = b's'; + pub const KBKDF_KEY_USAGE_LABEL_AES_GCM_SESSION_KEY: u8 = b'g'; - /// KBKDF usage label for ephemeral session key ratcheting. - pub const KBKDF_KEY_USAGE_LABEL_RATCHET_KEY: u8 = b'r'; + /// KBKDF usage label for AES-GCM session keys. + pub const KBKDF_KEY_USAGE_LABEL_AES_CTR_SESSION_KEY: u8 = b'c'; /// Try to re-key ephemeral keys after this time. pub const EPHEMERAL_SECRET_REKEY_AFTER_TIME: i64 = 300000; // 5 minutes diff --git a/zerotier-network-hypervisor/src/vl1/rootset.rs b/zerotier-network-hypervisor/src/vl1/rootset.rs index 361fba47c..262b552b6 100644 --- a/zerotier-network-hypervisor/src/vl1/rootset.rs +++ b/zerotier-network-hypervisor/src/vl1/rootset.rs @@ -3,7 +3,7 @@ use std::collections::BTreeSet; use std::io::Write; -use crate::util::buffer::Buffer; +use crate::util::buffer::{Buffer, BufferReader}; use crate::util::marshalable::Marshalable; use crate::vl1::identity::*; use crate::vl1::Endpoint; @@ -109,7 +109,7 @@ impl RootSet { buf.append_varint(self.revision)?; buf.append_varint(self.members.len() as u64)?; for m in self.members.iter() { - m.identity.marshal_with_options(buf, Identity::ALGORITHM_ALL, false)?; + buf.append_bytes((&m.identity.to_public_bytes()).into())?; if m.endpoints.is_some() { let endpoints = m.endpoints.as_ref().unwrap(); buf.append_varint(endpoints.len() as u64)?; @@ -269,7 +269,7 @@ impl Marshalable for RootSet { let member_count = buf.read_varint(cursor)?; for _ in 0..member_count { let mut m = Root { - identity: Identity::unmarshal(buf, cursor)?, + identity: Identity::read_bytes(&mut BufferReader::new(buf, cursor))?, endpoints: None, signature: Vec::new(), priority: 0, diff --git a/zerotier-network-hypervisor/src/vl1/session.rs b/zerotier-network-hypervisor/src/vl1/session.rs index 4d4b4a269..c5a3b940b 100644 --- a/zerotier-network-hypervisor/src/vl1/session.rs +++ b/zerotier-network-hypervisor/src/vl1/session.rs @@ -1,11 +1,14 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. -use std::mem::size_of; -use std::sync::atomic::AtomicU32; +use std::fmt::{Debug, Display}; +use std::num::NonZeroU64; +use std::sync::Arc; + +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use zerotier_core_crypto::aes::*; -use zerotier_core_crypto::hash::{hmac_sha384, SHA384}; -use zerotier_core_crypto::kbkdf::zt_kbkdf_hmac_sha512; +use zerotier_core_crypto::hash::*; +use zerotier_core_crypto::kbkdf::*; use zerotier_core_crypto::p384::*; use zerotier_core_crypto::pqc_kyber; use zerotier_core_crypto::random; @@ -16,571 +19,381 @@ use crate::util::buffer::Buffer; use crate::util::marshalable::Marshalable; use crate::util::pool::*; use crate::vl1::identity::Identity; -use crate::vl1::symmetricsecret::SymmetricSecret; - -use parking_lot::RwLock; - -/* - -Basic outline of the ZeroTier V2 session protocol: - -*** Three-way connection setup handshake: - -(1) Initiator sends INIT: - -[12] random IV -[4] always zero -[4] session ID -[1] FFFFTTTT where F == flags, T == message type (1 for INIT) -[1] ZeroTier protocol version -[1] field ID of unencrypted initial ephemeral key -[...] outer ephemeral public key (currently always NIST P-384) --- begin AES-CTR encryption using ephemeral/static AES key ("setup key") -[...] additional tuples of field ID and field data --- end AES-CTR encryption -[48] HMAC-SHA384 using static HMAC key - -Additional fields in INIT: - - Optional: additional ephemeral public keys - - Optional: first 16 bytes of SHA384 of current session key (to check ratchet) - - Required: static ZeroTier identity of initiator - - Required: timestamp to be echoed in ACK - -(2) Responder sends ACK: - -[12] random IV -[4] always zero -[4] session ID -[1] FFFFTTTT where F == flags, T == message type (2 for ACK) -[1] ZeroTier protocol version --- begin AES-CTR encryption using SAME ephemeral/static AES key ("setup key") -[...] tuples of field ID and field data --- end AES-CTR encryption -[48] HMAC-SHA384 using static HMAC key - -Fields in ACK: - - Required: ephemeral public key matching at least one ephemeral sent - - Optional: additional matching ephemeral keys - - Required: new ratchet count, 0 if no starting key supplied or can't ratchet - - Required: SHA384 of all (field ID, key) ephemeral keys from INIT in order sent - - Required: timestamp to be echoed in CONFIRM - - Required: echo of timestamp from INIT - -(3) Initiator sends CONFIRM: - -[12] AES-GCM tag -[4] counter (little-endian) -[4] session ID -[1] FFFFTTTT where F == flags, T == message type (3 for CONFIRM) --- begin AES-GCM encryption -[...] tuples of field ID and field data -?[48] optional extended authentication HMAC-SHA384 w/static key - -Fields in CONFIRM: - - Required: echo of timestamp from ACK - -CONFIRM has almost the same format as DATA and serves to notify the responder -side that a session is fully established. - -It's important that the counter be incremented for CONFIRM and that this is -the same counter as DATA because this counts as a use of AES-GCM. - -*** DATA packets: - -[12] AES-GCM tag -[4] counter (little-endian) -[4] session ID -[1] FFFFTTTT where F == flags, T == message type (0 for DATA) --- begin AES-GCM encrypted data packet -[1] LNNNNNNN where N == fragment number and L is set if it's the last fragment -[...] data payload, typically starting with a ZeroTier VL1/VL2 protocol verb -?[48] optional extended authentication HMAC-SHA384 w/static key - -DATA payload must be at least two bytes in length. If it is not it will be zero -padded. - -*** SINGLETON packets: - -A singleton packet has the same format as an INIT packet, but includes no -additional public keys or session key info. Instead it includes a data payload -field and it elicits no ACK response. The session ID must be zero. - -Singleton packets can be used to send unidirectional sparse messages without -incurring the overhead of a full session. There is no replay attack prevention -in this case, so these messages should only be used for things that are -idempotent or have their own resistance to replay. There is also no automatic -fragmentation, so the full packet must fit in the underlying transport. - -*** REJECT: - -[12] random IV -[4] always zero -[4] session ID -[1] FFFFTTTT where F == flags, T == message type (2 for ACK) -[1] ZeroTier protocol version -[1] Error code -[48] HMAC-SHA384 using static HMAC key - -A REJECT packet can be sent in response to INIT if the recipient will not accept -the connection attempt. REJECT is optional in that a rejected INIT can simply be -ignored. - -*** Notes: - -The initiator creates one or more ephemeral public keys and sends the first of -these ephemeral keys in unencrypted form. Key agreement (or KEX if applicable) is -performed against the responder's static identity key by both the initiator and the -responder to create an ephemeral/static key that is only used for INIT and ACK and -not afterwords. (The ephemeral sent in the clear must have a counterpart in the -recipient's static identity.) - -When the responder receives INIT it computes the session key as follows: - -(1) A starting ratchet key is chosen. If INIT contains a hash of the current - (being replaced) session key and it matches the one at the responder, a - derived ratchet key from the current session is used. Otherwise a ratchet - key derived from the static/static key (the permanent key) is used. -(2) For each ephemeral key supplied by the initiator, the responder optionally - generates its own ephemeral counterpart. While the responder is not required - to match all supplied keys it must compute and supply at least one to create - a valid forward-secure session. The responder then sends these keys in an - ACK message encrypted using the same key as INIT but authenticated via HMAC - using the new session key. Once the responder generates its own ephemeral - keys it may compute the session key in the same manner as the initiator. -(3) When the initiator receives ACK it can compute the session key. Starting - with the ratchet key from step (1) the initator performs key agreement using - each ephemeral key pair for which both sides have furnished a key. These are - chained together using HMAC-SHA512(last, next) where the last key is the - "key" in HMAC and the next key is the "message." - -Key agreements in (3) are performed in the following order, skipping any where both -sides have not furnished a key: - -(1) Curve25519 ECDH (not currently sent but supported) -(2) Kyber768 (optional, for long duration forward secrecy against QC) -(3) NIST P-384 ECDH (the default outer ephemeral type) - -The NIST key must be last for FIPS compliance reasons as it's a FIPS-compliant -algorithm and elliptic curve. FIPS allows HKDF using HMAC(salt, key) and allows -the salt to be anything, so we can use the results of previous non-FIPS agreements -as this "salt." - -Kyber is a post-quantum algorithm, the first to be standardized by NIST. Its -purpose is to provide long-term forward secrecy against adversaries who warehouse -data in anticipation of future quantum computing capability. When enabled a future -QC adversary could de-anonymize identities by breaking e.g. NIST P-384 but could -still not decrypt actual session payload. - -Kyber is a key encapsulation algorithm rather than a Diffie-Hellman style -algorithm. When used the initiator generates a key pair and then sends its public -key to the responder. The responder then uses this public key to generate a shared -secret that is sent back to the initiator. The responder does not have to generate -its own key pair for this exchange. The raw Kyber algorithm is used since the -authentication in this session protocol is provided by HMAC-SHA384 using identity -keys. - -HMAC always uses the static HMAC key derived from agreement between the sender and -the recipient identity, providing identity authentication of the exchange. It is -computed and appended after encryption but before obfuscation and is not itself -encrypted. - -*** AES-CTR and AES-GCM IVs: - -It's critically important never to duplicate and IV with CTR or GCM mode. - -The AES-CTR IV is composed of the 12-byte random IV provided in the packet header -but with byte 0 set 0x00 if this side was the initiator (INIT sender) or 0x01 if -this side was the responder (ACK sender). The setup key is only used twice, for -INIT and ACK, so this makes a collision impossible instead of just improbable. - -The AES-GCM IV is 12 bytes long and is created as follows: - -RTSSIIIICCCC - - R: role: 0x00 if this side was the initiator, 0x01 if it was the responder - - T: type/flags field from outer header - - S: packet size modulo 65536 (little-endian) - - I: session ID - - C: counter (little-endian) - -The first byte being 0 or 1 is critical here because the same key is used at -both ends and both counters will start at zero. If all else were the same then -both sides would use the same series of IVs. The placement of type/flags in -the second byte accomplishes the same as feeding the type byte in as AAD but -with less performance impact. The packet size in bytes 2 and 3 is technically -filler but may provide margin if someone figures out a way to do some kind of -length extension attack or something. Session ID inclusion also indirectly -includes the session ID as AAD. The counter MUST be incremented for each packet -or leopards will eat your face. - -A single AES-GCM key can be used to send up to a maximum of 2^32 packets. Re-key -thresholds are set far below this, but the implementation will refuse to send -packets if the counter actually reaches the end. - -*** Flags: - -0x80 - use extended authentication: meaningful only in CONFIRM and DATA, this - flag indicates that a final HMAC-SHA384 has been appended to the packet - after encryption (but before DPI obfuscation) as in INIT and ACK. This - should be checked in addition to the GCM tag, providing even stronger - authentication at the expense of extra overhead. HMAC always uses the - static/static ("permanent") key. - -*** Anti-DPI Obfuscation: - -Obfuscation is not technically part of our security posture with regard to either data -privacy or authentication. Its purpose is to make it much more difficult for deep packet -inspection (DPI) devices to classify ZeroTier traffic and therefore to provide some -additional margin against de-anonymization at scale. - -It's applied by using AES-128 to encrypt one block (ECB) of each packet starting at byte -index 8. The key for encryption is the first 16 bytes of the recipient's ZeroTier -identity fingerprint hash. The receiving side then decrypts this block before processing -the packet. This 16-byte block covers all parts of the packet that aren't random. - -This forces a DPI device to know the identity of the recipient and to perform one AES -decryption per packet in order to distinguish ZeroTier traffic from pure noise. - -*** Credits: - -Designed by Adam Ierymenko with heavy influence from the Noise protocol specification by -Trevor Perrin and the Wireguard VPN protocol by Jason Donenfeld. - -*/ pub const SESSION_SETUP_PACKET_SIZE_MAX: usize = crate::vl1::protocol::UDP_DEFAULT_MTU; -pub const SESSION_PACKET_SIZE_MIN: usize = 24; +pub const AUTH_DATA_SIZE_MAX: usize = 1024; -const FLAGS_TYPE_INDEX: usize = 20; -const FLAGS_TYPE_TYPE_MASK: u8 = 0x0f; +pub const KEX_NISTP384: u8 = 0x01; +pub const KEX_KYBER768: u8 = 0x02; +pub const KEX_C25519: u8 = 0x04; -const MESSAGE_TYPE_DATA: u8 = 0x00; -const MESSAGE_TYPE_INIT: u8 = 0x01; -const MESSAGE_TYPE_ACK: u8 = 0x02; -const MESSAGE_TYPE_CONFIRM: u8 = 0x03; -const MESSAGE_TYPE_REJECT: u8 = 0x04; -const MESSAGE_TYPE_SINGLETON: u8 = 0x05; +pub const REKEY_AFTER_PACKET_COUNT: u32 = 1073741824; +pub const REKEY_AFTER_TIME_MS: i64 = 3600000; // one hour -const MESSAGE_FLAGS_EXTENDED_AUTH: u8 = 0x80; +const HEADER_SIZE: usize = 10; -const FIELD_DATA: u8 = 0x00; -const FIELD_INITIATOR_IDENTITY: u8 = 0x01; -const FIELD_EPHEMERAL_C25519: u8 = 0x02; -const FIELD_EPHEMERAL_NISTP384: u8 = 0x03; -const FIELD_EPHEMERAL_KYBER_PUBLIC: u8 = 0x04; -const FIELD_EPHEMERAL_KYBER_ENCAPSULATED_SECRET: u8 = 0x05; -const FIELD_RATCHET_STARTING_KEY_HASH: u8 = 0x06; -const FIELD_ACK_RATCHET_COUNT: u8 = 0x07; -const FIELD_TIMESTAMP: u8 = 0x08; -const FIELD_TIMESTAMP_ECHO: u8 = 0x09; +const TYPE_MASK: u8 = 0x03; -const OBFUSCATION_INDEX: usize = 8; +const TYPE_DATA: u8 = 0; +const TYPE_INIT: u8 = 1; +const TYPE_REKEY: u8 = 2; +const TYPE_ACK: u8 = 3; -const ROLE_INITIATOR: u8 = 0x00; -const ROLE_RESPONDER: u8 = 0x01; +const EPHEMERAL_TYPE_EOF: u8 = 0; +const EPHEMERAL_TYPE_X25519: u8 = 1; +const EPHEMERAL_TYPE_NISTP384: u8 = 2; +const EPHEMERAL_TYPE_KYBER_PUBLIC: u8 = 3; +const EPHEMERAL_TYPE_KYBER_CIPHERTEXT: u8 = 4; -#[derive(Clone, Copy)] -#[repr(C, packed)] -struct InitAckSingletonHeader { - iv: [u8; 12], - zero: u32, - session_id: u32, - flags_type: u8, - protocol_version: u8, +const KBKDF_KEY_USAGE_INIT_PAYLOAD_ENCRYPT: u8 = b'i'; +const KBKDF_KEY_USAGE_INIT_PAYLOAD_HMAC: u8 = b'h'; + +const KEY_HISTORY_SIZE: usize = 4; + +#[derive(Debug)] +enum Error { + IoError(std::io::Error), + Invalid, } -#[derive(Clone, Copy)] -#[repr(C, packed)] -struct InitSingletonHeader { - h: InitAckSingletonHeader, - outer_ephemeral_field_id: u8, - outer_ephemeral: [u8; P384_PUBLIC_KEY_SIZE], +impl From for Error { + #[inline(always)] + fn from(e: std::io::Error) -> Self { + Self::IoError(e) + } } -#[derive(Clone, Copy)] -#[repr(C, packed)] -struct ConfirmDataHeader { - tag: [u8; 12], - counter: u32, - session_id: u32, - flags_type: u8, +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(e) => Display::fmt(e, f), + Self::Invalid => f.write_str("Invalid"), + } + } } -struct InitiatorOfferedKeys { - p384: P384KeyPair, - kyber: Option, - ratchet_starting_key: Secret<64>, - ratchet_starting_count: u64, - setup_key: Secret<32>, +impl std::error::Error for Error {} + +pub struct Session { + pub local_session_id: NonZeroU64, + m: RwLock, + pub associated_object: O, + pub enabled_kex_algorithms: u8, } -struct Keys { - /// Keys offered by local node and sent to remote, generated by initiate(). - local_offered: Option>, - - /// Final key ratcheted from previous or starting key via agreement between all matching ephemeral pairs. - session_key: Option, - - /// Pool of encryptors and decryptors for AES-GCM initialized with the session key. - aes_gcm_pool: Option<(Pool, Pool)>, - - /// Number of times ephemeral ratcheting has taken place, starts at 0 if starting from static. +struct Key { + secret: Secret<64>, + gcm_pool: Pool, ratchet_count: u64, - - /// Set to true on initiator side when ACK is received on and on responder once CONFIRM is received. - established: bool, } -/// ZeroTier V2 forward-secure session -/// -/// The current version always uses NIST P-384 as the outer ephemeral key and optionally -/// Kyber for the internal ephemeral key. Curve25519 is supported if sent by the remote -/// side though. -/// -/// The RD template argument is used to specify a type to be attached to the session such -/// as a ZeroTier peer. -#[allow(unused)] -pub(crate) struct Session { - /// Arbitrary object that may be attached by external code to this session. - pub related_data: RD, - - /// Session keys of various types. - keys: RwLock, - - /// Timestamp when session was created. - creation_time: i64, - - /// A random number added to sent timestamps to not reveal exact local tick counter. - latency_timestamp_delta: u32, - - /// Number of times session key has been used to encrypt data with AES-GCM. - outgoing_counter: AtomicU32, - - /// Most recent incoming counter value. - last_incoming_counter: AtomicU32, - - /// Most recent measured latency in milliseconds. - latency: AtomicU32, - - /// Random session ID generated by initiator. - pub id: u32, - - /// 0x00 for the initiator, 0x01 for the responder side. - role: u8, +impl Key { + fn new(secret: Secret<64>, ratchet_count: u64) -> Self { + Self { + secret, + gcm_pool: Pool::new(2, AesGcmPoolFactory(secret.first_n(), false)), + ratchet_count, + } + } } -pub(crate) trait SessionContext { - /// Iterate through all sessions matching an ID until the supplied function returns false. - fn sessions_with_id) -> bool>(&self, id: u32, f: F); - - /// Check whether or not to accept a new session and get initial related data value. - /// - /// This is called when this side gets an INIT. A return of None causes the request to be - /// silently ignored. Otherwise this must return the initial value of the related_data - /// field for the new session. - fn incoming_session(&self, new_session_id: u32, remote_identity: &Identity) -> Option; +struct EphemeralOffer { + p384: Option, + x25519: Option<[u8; C25519_PUBLIC_KEY_SIZE]>, + kyber_public: Option<[u8; pqc_kyber::KYBER_PUBLICKEYBYTES]>, + kyber_ciphertext: Option<[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>, + expected_secret_hash: [u8; 48], } -impl Session { - /// Create an initiator session and return it and the packet to be sent. - /// - /// ZeroTier V2 sessions can only currently be used with identities that also contain - /// a NIST P-384 ECDH public key. - /// - /// The obfuscation key must be an AES-128 ECB instance initialized from the first 16 - /// bytes of the remote identity's fingerprint hash. - pub fn initiate( - local_identity: &Identity, - remote_identity: &Identity, - obfuscation_key: &Aes, - static_key: &SymmetricSecret, - current_session: Option<&Self>, - current_time: i64, - initial_related_data: RD, - ) -> Option<(Self, Buffer)> { - let mut packet: Buffer = Buffer::new(); - let id = random::next_u32_secure(); - - let ephemeral_p384 = P384KeyPair::generate(); - let mut ctr_iv = { - let h: &mut InitSingletonHeader = packet.append_struct_get_mut().unwrap(); - random::fill_bytes_secure(&mut h.h.iv); - h.h.session_id = id; // actually [u8; 4] so endian is irrelevant - h.h.flags_type = MESSAGE_FLAGS_EXTENDED_AUTH | MESSAGE_TYPE_INIT; - h.h.protocol_version = crate::vl1::protocol::PROTOCOL_VERSION; - h.outer_ephemeral_field_id = FIELD_EPHEMERAL_NISTP384; - h.outer_ephemeral.copy_from_slice(ephemeral_p384.public_key_bytes()); - h.h.iv.clone() +impl EphemeralOffer { + fn read(b: &Buffer, cursor: &mut usize) -> Result { + let mut offer = Self { + p384: None, + x25519: None, + kyber_public: None, + kyber_ciphertext: None, + expected_secret_hash: [0_u8; 48], }; - ctr_iv[0] = ROLE_INITIATOR; - - assert!(packet.append_u8(FIELD_INITIATOR_IDENTITY).is_ok()); - assert!(local_identity.marshal(&mut packet).is_ok()); - - let ephemeral_kyber = pqc_kyber::keypair(&mut random::SecureRandom::get()); - assert!(packet.append_u8(FIELD_EPHEMERAL_KYBER_PUBLIC).is_ok()); - assert!(packet.append_bytes_fixed(&ephemeral_kyber.public).is_ok()); - - let mut ratchet_starting_count = 0; - let ratchet_starting_key = current_session - .and_then(|cs| { - let keys = cs.keys.read(); - keys.session_key.as_ref().map(|cs_key| { - ratchet_starting_count = keys.ratchet_count; - assert!(packet.append_u8(FIELD_RATCHET_STARTING_KEY_HASH).is_ok()); - assert!(packet.append_bytes(&SHA384::hash(&cs_key.key.0)[..16]).is_ok()); - zt_kbkdf_hmac_sha512(cs_key.key.as_bytes(), crate::vl1::protocol::security_constants::KBKDF_KEY_USAGE_LABEL_RATCHET_KEY) - }) - }) - .unwrap_or_else(|| zt_kbkdf_hmac_sha512(static_key.key.as_bytes(), crate::vl1::protocol::security_constants::KBKDF_KEY_USAGE_LABEL_RATCHET_KEY)); - - let latency_timestamp_delta = random::xorshift64_random() as u32; - assert!(packet.append_u8(FIELD_TIMESTAMP).is_ok()); - assert!(packet.append_u64((current_time as u64).wrapping_add(latency_timestamp_delta as u64)).is_ok()); - - let setup_key; - if let Some(responder_p384) = remote_identity.p384.as_ref() { - if let Some(sk) = ephemeral_p384.agree(&responder_p384.ecdh) { - setup_key = Secret(SHA384::hash(sk.as_bytes())[..32].try_into().unwrap()); - let mut ctr = AesCtr::new(setup_key.as_bytes()); - ctr.init(&ctr_iv); - ctr.crypt_in_place(&mut packet.as_bytes_mut()[size_of::()..]); - } else { - return None; + loop { + match b.read_u8(cursor)? { + EPHEMERAL_TYPE_EOF => break, + EPHEMERAL_TYPE_NISTP384 => { + if offer.p384.is_some() { + return Err(Error::Invalid); + } else if let Some(p384) = P384PublicKey::from_bytes(b.read_bytes_fixed::(cursor)?) { + offer.p384.replace(p384); + } else { + return Err(Error::Invalid); + } + } + EPHEMERAL_TYPE_X25519 => { + if offer.x25519.is_some() { + return Err(Error::Invalid); + } else { + offer.x25519.replace(b.read_bytes_fixed(cursor)?.clone()); + } + } + EPHEMERAL_TYPE_KYBER_PUBLIC => { + if offer.kyber_public.is_some() { + return Err(Error::Invalid); + } else { + offer.kyber_public.replace(b.read_bytes_fixed(cursor)?.clone()); + } + } + EPHEMERAL_TYPE_KYBER_CIPHERTEXT => { + if offer.kyber_ciphertext.is_some() { + return Err(Error::Invalid); + } else { + offer.kyber_ciphertext.replace(b.read_bytes_fixed(cursor)?.clone()); + } + } + _ => { + let key_size = b.read_varint(cursor)?; + *cursor += key_size as usize; + } } + } + Ok(offer) + } +} + +struct SessionMut { + receive_key: Key, + remote_kyber_public: Option, + remote_session_id: Option, +} + +pub fn initiate( + local_identity: &Identity, + remote_identity: &Identity, + static_key: &Secret<64>, + local_session_id: u64, + current_time: i64, + enable_kex_algorithms: u8, + associated_object: O, +) -> Session { + todo!() +} + +pub fn receive< + GetSessionByLocalId: FnOnce(NonZeroU64) -> Option>>, + OnData: FnOnce(&Arc>, u32, &mut Buffer), + OnDatagram: FnOnce(Identity, &mut Buffer), + SendReply: FnOnce(&Arc>, &mut Buffer), + AllowNewSession: FnOnce(&Identity, Option<&[u8]>) -> Option<(NonZeroU64, O)>, + OnNewSession: FnOnce(Identity, Arc>), + O, + const L: usize, +>( + local_identity: &Identity, + local_obfuscation_cipher: &Aes, + current_time: i64, + enable_kex_algorithms: u8, + incoming_packet: &Buffer, + get_session_by_local_id: GetSessionByLocalId, + on_data: OnData, + on_datagram: OnDatagram, + send_reply: SendReply, + allow_new_session: AllowNewSession, + on_new_session: OnNewSession, +) -> std::io::Result { + let len_without_gcm_tag = incoming_packet.len().wrapping_sub(16); + let gcm_tag = incoming_packet.bytes_fixed_at::<16>(len_without_gcm_tag)?; + + let (encryption_nonce, counter, mut local_session_id) = incoming_packet.bytes_fixed_at::(0).map(|h| { + let mut deobfuscated_header = unsafe { std::mem::MaybeUninit::<[u8; 16]>::uninit().assume_init() }; + local_obfuscation_cipher.encrypt_block(gcm_tag, &mut deobfuscated_header); + for i in 0..HEADER_SIZE { + deobfuscated_header[i] ^= h[i]; + } + deobfuscated_header[HEADER_SIZE..].fill(0); + (deobfuscated_header, u32::from_le_bytes(deobfuscated_header[0..4].try_into().unwrap()), NonZeroU64::new(u64::from_le_bytes(deobfuscated_header[4..12].try_into().unwrap()))) + })?; + + let (mut session, session_m) = if let Some(lsid) = local_session_id { + if let Some(s) = get_session_by_local_id(lsid) { + let m = s.m.upgradable_read(); + (Some(s), Some(m)) } else { - return None; - }; + return Ok(false); + } + } else if counter == 0 { + // This can only be an INIT packet, and counter must also be zero. + (None, None) + } else { + return Ok(false); + }; - assert!(packet.append_bytes(&hmac_sha384(static_key.packet_hmac_key.as_bytes(), packet.as_bytes())).is_ok()); + let mut init_cipher = None; + let mut pooled_cipher = None; + let aes_gcm: &mut AesGcm = if let Some(m) = session_m.as_ref() { + pooled_cipher = Some(m.receive_key.gcm_pool.get()); + pooled_cipher.as_mut().unwrap() + } else { + init_cipher = Some(AesGcm::new(&local_identity.fingerprint[32..], false)); + init_cipher.as_mut().unwrap() + }; - obfuscation_key.encrypt_block_in_place(&mut packet.as_bytes_mut()[OBFUSCATION_INDEX..(OBFUSCATION_INDEX + 16)]); - - return Some(( - Self { - related_data: initial_related_data, - keys: RwLock::new(Keys { - local_offered: Some(Box::new(InitiatorOfferedKeys { - p384: ephemeral_p384, - kyber: Some(ephemeral_kyber), - ratchet_starting_key, - ratchet_starting_count, - setup_key, - })), - session_key: None, - aes_gcm_pool: None, - ratchet_count: 0, // updated if the remote side can also ratchet - established: false, - }), - creation_time: current_time, - latency_timestamp_delta, - outgoing_counter: AtomicU32::new(0), - last_incoming_counter: AtomicU32::new(0), - latency: AtomicU32::new(0), - id, - role: ROLE_INITIATOR, - }, - packet, - )); + let mut packet = unsafe { Buffer::::new_without_memzero() }; + aes_gcm.init(&encryption_nonce); + aes_gcm.crypt(incoming_packet.as_byte_range(HEADER_SIZE, len_without_gcm_tag)?, packet.append_bytes_get_mut(len_without_gcm_tag - HEADER_SIZE)?); + if !aes_gcm.finish().eq(gcm_tag) { + return Ok(false); } - pub fn receive>( - local_identity: &Identity, - obfuscation_key: &Aes, - static_key: &SymmetricSecret, - current_time: i64, - sc: &SC, - packet: &mut Buffer, - ) -> bool { - if packet.len() >= SESSION_PACKET_SIZE_MIN { - obfuscation_key.decrypt_block_in_place(&mut packet.as_bytes_mut()[OBFUSCATION_INDEX..(OBFUSCATION_INDEX + 16)]); + let mut cursor = 0; + let packet_info = packet.read_u8(&mut cursor)?; + let packet_type = packet_info & TYPE_MASK; + match packet_type { + TYPE_DATA => { + if let Some(session) = session { + } else { + return Ok(false); + } + } - let flags = packet.u8_at(FLAGS_TYPE_INDEX).unwrap(); - let message_type = flags & FLAGS_TYPE_TYPE_MASK; + TYPE_INIT => { + if session.is_none() { + let local_identity_secrets = local_identity.secret.as_ref().unwrap(); + let init_data_start = cursor; - match message_type { - MESSAGE_TYPE_DATA | MESSAGE_TYPE_CONFIRM => if let Ok(header) = packet.struct_at::(0) {}, - - MESSAGE_TYPE_INIT | MESSAGE_TYPE_ACK | MESSAGE_TYPE_SINGLETON => { - if let Ok(header) = packet.struct_at::(0) { - let ( - mut remote_identity, - mut remote_offered_c25519, - mut remote_offered_nistp384, - mut remote_offered_kyber_public, - mut remote_timestamp, - mut remote_session_key_hash, - ) = (None, None, None, None, -1, None); - - let mut cursor = size_of::(); - loop { - if let Ok(field_type) = packet.read_u8(&mut cursor) { - match field_type { - FIELD_DATA => {} - FIELD_INITIATOR_IDENTITY => { - if let Ok(id) = Identity::unmarshal(packet, &mut cursor) { - remote_identity = Some(id); - } else { - return false; - } - } - FIELD_EPHEMERAL_C25519 => { - if let Ok(k) = packet.read_bytes_fixed::(&mut cursor) { - remote_offered_c25519 = Some(k); - } else { - return false; - } - } - FIELD_EPHEMERAL_NISTP384 => { - if let Ok(k) = packet.read_bytes_fixed::(&mut cursor) { - remote_offered_nistp384 = Some(k); - } else { - return false; - } - } - FIELD_EPHEMERAL_KYBER_PUBLIC => { - if let Ok(k) = packet.read_bytes_fixed::<{ pqc_kyber::KYBER_PUBLICKEYBYTES }>(&mut cursor) { - remote_offered_kyber_public = Some(k); - } else { - return false; - } - } - FIELD_EPHEMERAL_KYBER_ENCAPSULATED_SECRET => {} - FIELD_CURRENT_SESSION_KEY_HASH => { - if let Ok(k) = packet.read_bytes_fixed::<16>(&mut cursor) { - remote_session_key_hash = Some(k); - } else { - return false; - } - } - FIELD_TIMESTAMP => { - if let Ok(ts) = packet.read_varint(&mut cursor) { - remote_timestamp = ts as i64; - } else { - return false; - } - } - FIELD_TIMESTAMP_ECHO => { - if let Ok(ts) = packet.read_varint(&mut cursor) { - } else { - return false; - } - } - _ => {} + // Bootstrap secret is agree(ephemeral key, static identity key) + let mut bootstrap_secret = Secret(SHA512::hash(&local_identity.fingerprint)); + let mut remote_kyber_public = None; + loop { + match packet.read_u8(&mut cursor)? { + EPHEMERAL_TYPE_EOF => break, + EPHEMERAL_TYPE_NISTP384 => { + if let Some(local_p384_secret) = local_identity_secrets.p384.as_ref().map(|p384| &p384.ecdh) { + if let Some(s) = P384PublicKey::from_bytes(packet.read_bytes_fixed::(&mut cursor)?).and_then(|p| local_p384_secret.agree(&p)) { + bootstrap_secret.0.copy_from_slice(&hmac_sha512(bootstrap_secret.as_bytes(), s.as_bytes())); + } else { + return Ok(false); } - - if message_type == MESSAGE_TYPE_INIT {} } else { - break; + return Ok(false); } } + EPHEMERAL_TYPE_C25519 => { + bootstrap_secret.0.copy_from_slice(&hmac_sha512( + bootstrap_secret.as_bytes(), + local_identity_secrets.c25519.agree(packet.read_bytes_fixed::(&mut cursor)?).as_bytes(), + )); + } + EPHEMERAL_TYPE_KYBER_PUBLIC => { + remote_kyber_public = Some(packet.read_bytes_fixed::<{ pqc_kyber::KYBER_PUBLICKEYBYTES }>(&mut cursor)?.clone()); + } + _ => return Ok(false), } } - _ => {} + // Decrypt and authenticate init payload using bootstrap secret. + let mut init_payload_bytes: Buffer = Buffer::new(); + let init_payload_len = (packet.read_varint(&mut cursor)? as usize).wrapping_sub(16); + let mut init_payload_crypt = AesGcm::new(&zt_kbkdf_hmac_sha384(&bootstrap_secret.as_bytes()[..48], KBKDF_KEY_USAGE_INIT_PAYLOAD_ENCRYPT).as_bytes()[..32], false); + init_payload_crypt.init(&encryption_nonce); + init_payload_crypt.crypt(packet.read_bytes(init_payload_len, &mut cursor)?, init_payload_bytes.append_bytes_get_mut(init_payload_len).unwrap()); + if !init_payload_crypt.finish().eq(packet.read_bytes_fixed::<16>(&mut cursor)?) { + return Ok(false); + } + + // Decode rest of init payload, compute first real key incorporating the static secret + // to authenticate both sides, then authenticate the INIT payload again with this key. + let mut init_payload_cursor = 0; + let remote_session_id = init_payload_bytes.read_bytes_fixed::<6>(&mut init_payload_cursor)?; + let remote_session_id = (remote_session_id[0] as u64) + | (remote_session_id[1] as u64).wrapping_shl(8) + | (remote_session_id[2] as u64).wrapping_shl(16) + | (remote_session_id[3] as u64).wrapping_shl(24) + | (remote_session_id[4] as u64).wrapping_shl(32) + | (remote_session_id[5] as u64).wrapping_shl(40); + if remote_session_id != 0 { + let remote_identity = Identity::unmarshal(&init_payload_bytes, &mut init_payload_cursor)?; + let auth_data_len = init_payload_bytes.read_varint(&mut init_payload_cursor).unwrap_or(0) as usize; + if let Some(static_secret) = local_identity.agree(&remote_identity) { + let new_session_key = Secret(hmac_sha512(static_secret.as_bytes(), bootstrap_secret.as_bytes())); + + if !hmac_sha384(zt_kbkdf_hmac_sha384(&new_session_key.as_bytes()[..48], KBKDF_KEY_USAGE_INIT_PAYLOAD_HMAC).as_bytes(), packet.as_byte_range(init_data_start, cursor)?) + .eq(packet.read_bytes_fixed::<48>(&mut cursor)?) + { + return Ok(false); + } + + if let Some((id, obj)) = + allow_new_session(&remote_identity, if auth_data_len > 0 { Some(init_payload_bytes.read_bytes(auth_data_len, &mut init_payload_cursor)?) } else { None }) + { + session = Some(Arc::new(Session { + local_session_id: id, + m: RwLock::new(SessionMut { + receive_key: Key::new(new_session_key, 0), + remote_kyber_public, + remote_session_id: NonZeroU64::new(remote_session_id), + }), + associated_object: obj, + enabled_kex_algorithms: enable_kex_algorithms, + })); + on_new_session(remote_identity, session.as_ref().unwrap().clone()); + // TODO: send REKEY + return Ok(true); + } else { + return Ok(false); + } + } else { + return Ok(false); + } + } else { + return Ok(false); + } + } else { + return Ok(false); + } + } + + TYPE_REKEY => { + if let Some(session) = session { + loop { + match packet.read_u8(&mut cursor)? { + EPHEMERAL_TYPE_EOF => break, + EPHEMERAL_TYPE_NISTP384 => {} + EPHEMERAL_TYPE_C25519 => {} + EPHEMERAL_TYPE_KYBER_PUBLIC => {} + EPHEMERAL_TYPE_KYBER_CIPHERTEXT => {} + _ => {} + } + } + } else { + return Ok(false); + } + } + + TYPE_ACK => { + if let Some(session) = session { + } else { + return Ok(false); } } - return false; } + + /* + if let Some(mut next_ratchet_key) = next_ratchet_key { + let next_ratchet_counter = if let Some(ck) = current_key.and_then(|cc| cc.as_ref()) { + next_ratchet_key = Secret(hmac_sha512(ck.1.as_bytes(), next_ratchet_key.as_bytes())); + ck.2 + 1 + } else { + 0 + }; + let mut session_m = RwLockUpgradableReadGuard::upgrade(session_m); + let mut old_receive_key = session_m.receive_key_history[0].replace((Pool::new(2, AesGcmPoolFactory(next_ratchet_key.first_n(), false)), next_ratchet_key, next_ratchet_counter)); + for i in 1..KEY_HISTORY_SIZE { + std::mem::swap(&mut session_m.receive_key_history[i], &mut old_receive_key); + } + session_m.last_successful_receive_key = 0; + } else if session_m.last_successful_receive_key != trying_cipher { + RwLockUpgradableReadGuard::upgrade(session_m).last_successful_receive_key = trying_cipher; + } + */ + + return Ok(true); } pub(crate) struct AesGcmPoolFactory(Secret<32>, bool); @@ -595,14 +408,1167 @@ impl PoolFactory for AesGcmPoolFactory { fn reset(&self, _: &mut AesGcm) {} } -#[cfg(test)] -mod tests { - use super::*; +/* - #[test] - fn sizing() { - assert_eq!(size_of::(), 26); - assert_eq!(size_of::(), 26 + 1 + P384_PUBLIC_KEY_SIZE); - assert_eq!(size_of::(), 21); +/// Internal method to read and extract data from a series of field ID, value, ... +/// Returns false if a parse or other error occurs. +fn read_fields( + cursor: &mut usize, + packet: &Buffer, + data: &mut Option<(usize, usize)>, + auth_data: &mut Option<(usize, usize)>, + identity: &mut Option, + session_id: &mut Option, + ratchet_counter: &mut Option, + ephemeral_c25519: &mut Option<[u8; C25519_PUBLIC_KEY_SIZE]>, + ephemeral_p384: &mut Option, + ephemeral_kyber_public: &mut Option<[u8; pqc_kyber::KYBER_PUBLICKEYBYTES]>, + ephemeral_kyber_encapsulated_secret: &mut Option<[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>, +) -> bool { + while *cursor < packet.len() { + let field_type = packet.read_u8(cursor).unwrap(); // bounds already checked by while condition + match field_type { + FIELD_DATA => { + if data.is_some() { + return false; + } + if let Ok(dlen) = packet.read_varint(cursor) { + let start = *cursor; + let end = *cursor + (dlen as usize); + if end > packet.len() { + return false; + } + data.insert((start, end)); + } else { + return false; + } + } + FIELD_AUTH_DATA => { + if auth_data.is_some() { + return false; + } + if let Ok(dlen) = packet.read_varint(cursor) { + let start = *cursor; + let end = *cursor + (dlen as usize); + if end > packet.len() { + return false; + } + auth_data.insert((start, end)); + } else { + return false; + } + } + FIELD_STATIC_IDENTITY => { + if identity.is_some() { + return false; + } + if let Ok(id) = Identity::unmarshal(packet, cursor) { + identity.insert(id); + } else { + return false; + } + } + FIELD_SESSION_ID => { + if session_id.is_some() { + return false; + } + if let Ok(sid) = packet.read_bytes_fixed::<6>(cursor) { + session_id.insert( + (sid[0] as u64) + | (sid[1] as u64).wrapping_shl(8) + | (sid[2] as u64).wrapping_shl(16) + | (sid[3] as u64).wrapping_shl(24) + | (sid[4] as u64).wrapping_shl(32) + | (sid[5] as u64).wrapping_shl(40), + ); + } else { + return false; + } + } + FIELD_EPHEMERAL_C25519 => { + if ephemeral_c25519.is_some() { + return false; + } + if let Ok(c25519) = packet.read_bytes_fixed::(cursor) { + ephemeral_c25519.insert(c25519.clone()); + } else { + return false; + } + } + FIELD_EPHEMERAL_NISTP384 => { + if ephemeral_p384.is_some() { + return false; + } + if let Ok(remote_p384) = packet.read_bytes_fixed::(cursor) { + if let Some(remote_p384) = P384PublicKey::from_bytes(remote_p384) { + ephemeral_p384.insert(remote_p384); + } else { + return false; + } + } else { + return false; + } + } + FIELD_EPHEMERAL_KYBER_PUBLIC => { + if ephemeral_kyber_public.is_some() { + return false; + } + if let Ok(remote_kyber) = packet.read_bytes_fixed::<{ pqc_kyber::KYBER_PUBLICKEYBYTES }>(cursor) { + ephemeral_kyber_public.insert(remote_kyber.clone()); + } else { + return false; + } + } + FIELD_EPHEMERAL_KYBER_ENCAPSULATED_SECRET => { + if ephemeral_kyber_encapsulated_secret.is_some() { + return false; + } + if let Ok(remote_kyber_secret) = packet.read_bytes_fixed::<{ pqc_kyber::KYBER_CIPHERTEXTBYTES }>(cursor) { + ephemeral_kyber_encapsulated_secret.insert(remote_kyber_secret.clone()); + } else { + return false; + } + } + FIELD_TIMESTAMP => { + if timestamp.is_some() { + return false; + } + if let Ok(ts) = packet.read_u64_le(cursor) { + timestamp.insert(ts); + } else { + return false; + } + } + FIELD_TIMESTAMP_ECHO => { + if timestamp_echo.is_some() { + return false; + } + if let Ok(ts) = packet.read_u64_le(cursor) { + timestamp_echo.insert(ts); + } else { + return false; + } + } + FIELD_RATCHET_COUNTER => { + if ratchet_counter.is_some() { + return false; + } + if let Ok(c) = packet.read_varint(cursor) { + ratchet_counter.insert(c); + } else { + return false; + } + } + _ => { + // Any fields we add in the future will need to be prefixed by their size. + if let Ok(additional_field_len) = packet.read_varint(cursor) { + *cursor += additional_field_len as usize; + } else { + return false; + } + } + } + } + return true; +} + +/// Trait to be implemented by whatever "owns" sessions. +pub(crate) trait SessionContext { + /// Get a session with a given local session ID + fn lookup_session(&self, local_session_id: u64) -> Option>>; + + /// Check if a new session is allowed, and if so return local session ID and value for 'obj' field. + fn authorize_new_session(&self, remote_identity: &Identity, auth_data: Option>) -> Option<(u64, O)>; + + /// Locally validate identity + /// + /// This is provided as an external function to be called because local validation in + /// identities is slightly CPU-intensive. This information might therefore be cached. + /// This also allows any other checks to be performed + fn validate_identity(&self, id: &Identity) -> bool; +} + +/// Role: Alice sends INIT and Bob responds with INIT_ACK, otherwise basically identical. +#[repr(u8)] +pub enum Role { + Alice = 0_u8, + Bob = 1_u8, +} + +/// ZeroTier Secure Session Protocol (ZSSP) session +/// +/// The O template argument is used to specify a type to be attached to the session such +/// as a ZeroTier peer. +#[allow(unused)] +pub(crate) struct Session { + /// Arbitrary object that may be attached by external code to this session. + pub obj: O, + + /// SHA512(agree(unencrypted ephemeral sent with INIT, matching key in recipient identity)) + setup_key: Secret<64>, + + /// Static key resulting from sender and recipient identity key agreement + static_key: Secret<64>, + + /// Key for the HMAC-SHA384 part of control message auth + hmac_key: Secret<48>, + + /// AES-128 initialized with the first 16 bytes of the recipients' identity fingerprint. + outgoing_obfuscation_cipher: Aes, + + /// Mutable state + state: RwLock, + + /// Time session was created (not most recent re-key) + pub creation_time: i64, + + /// Local session ID + local_session_id: u64, + + /// Outgoing packet counter, incremented for each send of any kind. + outbound_counter: AtomicU32, + + /// Most recently witnessed inbound counter value. + last_inbound_counter: AtomicU32, + + /// Deduplication hash table for incoming packet IDs (each ID mod 256). + inbound_counter_dedup: [AtomicU32; 256], + + /// Role: Alice (0) or Bob (1) + role: Role, + + /// Bit mask of enabled asymmetric key exchange algorithms + enabled_kex_algorithms: u8, + + /// Which of the KEX algorithms was offered for setup? + setup_kex_algorithm: u8, +} + +struct MutState { + keys: [Option>; SESSION_KEY_INDEX_COUNT], + current_key: usize, + offer: Option>, + remote_session_id: u64, +} + +pub(crate) struct AesGcmPoolFactory(Secret<32>, bool); + +impl PoolFactory for AesGcmPoolFactory { + #[inline(always)] + fn create(&self) -> AesGcm { + AesGcm::new(&self.0 .0, self.1) + } + + #[inline(always)] + fn reset(&self, _: &mut AesGcm) {} +} + +struct EphemeralOffer { + ratchet_counter: u64, + timestamp: i64, + c25519: Option, + kyber768_alice_keypair: Option, + kyber768_bob_ciphertext: Option<[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>, + p384: Option, +} + +struct SessionKey { + aes_ctr_key: Secret<32>, + aes_gcm_encrypt_pool: Pool, + aes_gcm_decrypt_pool: Pool, + timestamp: i64, + ratchet_counter: u64, + initial_packet_counter: u32, // session is dead if counter hits this + acknowledged: bool, + session_kex_algorithms: u8, +} + +impl SessionKey { + fn new(key: Secret<64>, timestamp: i64, ratchet_counter: u64, initial_packet_counter: u32, session_kex_algorithms: u8) -> Self { + let gcm_key = zt_kbkdf_hmac_sha384(&key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_AES_GCM); + Self { + aes_ctr_key: zt_kbkdf_hmac_sha384(&key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_AES_CTR).first_n(), + aes_gcm_encrypt_pool: Pool::new(2, AesGcmPoolFactory(gcm_key.first_n(), true)), + aes_gcm_decrypt_pool: Pool::new(2, AesGcmPoolFactory(gcm_key.first_n(), false)), + timestamp, + ratchet_counter, + initial_packet_counter, + acknowledged: false, + session_kex_algorithms, + } } } + +pub enum ReceiveError { + Invalid, + UnknownLocalSessionId, + NoSessionKeyAtIndex, + Duplicate, + CounterTooOutOfSync, + UnsupportedCipher, + CipherNotAllowedInContext, + MissingRequiredFields, + FailedAuthentication, + NewSessionRejected, +} + +pub enum ReceiveResult { + /// Packet OK, but no additional action needs to be taken. + Ok, + + /// Packet OK, 'output' contains a reply that should be sent back. + OkReply, + + /// Packet OK, new session created via remote INIT, 'output' contains reply. + /// + /// The authorize_new_session() session context method will have been called during processing. + OkNewSession(Identity, Arc>), + + /// Packet OK, data was received and is in 'output'. + OkData(Arc>), + + /// Sessionless datagram received, data is in 'output' within the provided range (start, end). + OkDatagram(Identity, (usize, usize)), + + /// An error occurred and the packet should be discarded. + Error(ReceiveError), +} + +pub fn initiate( + local_identity: &Identity, + remote_identity: &Identity, + static_key: &Secret<64>, + local_session_id: u64, + current_time: i64, + enable_kex_algorithms: u8, + obj: O, +) -> Option<(Arc>, Buffer)> { + let mut packet: Buffer = Buffer::new(); + packet.append_bytes(&INIT_HEADER_AND_CTR_NONCE[..HEADER_SIZE]).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + + let (mut offer_c25519, mut offer_p384, mut offer_kyber, mut setup_kex_algorithm, mut setup_key, mut start_encrypted_payload_at) = (None, None, None, 0, None, 0); + + // FIPS note: for setup P-384 must be first if available and enabled so the setup key is compliant. + if (enable_kex_algorithms & KEX_NISTP384) != 0 && remote_identity.p384.is_some() { + if let Some(local_secret) = local_identity.secret.as_ref() { + if let Some(local_p384_secret) = local_secret.p384.as_ref() { + let kp = P384KeyPair::generate(); + setup_key = setup_key.or_else(|| { + kp.agree(&remote_identity.p384.as_ref().unwrap().ecdh).map(|sk| { + setup_kex_algorithm = KEX_NISTP384; + Secret(SHA512::hash(sk.as_bytes())) + }) + }); + packet.append_u8(FIELD_EPHEMERAL_NISTP384).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + packet.append_bytes_fixed(kp.public_key_bytes()).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + if start_encrypted_payload_at == 0 { + start_encrypted_payload_at = packet.len(); + } + offer_p384 = Some(kp); + } + } + } + + if (enable_kex_algorithms & KEX_C25519) != 0 { + let kp = C25519KeyPair::generate(); + setup_key = setup_key.or_else(|| { + setup_kex_algorithm = KEX_C25519; + Some(Secret(SHA512::hash(kp.agree(&remote_identity.c25519).as_bytes()))) + }); + packet.append_u8(FIELD_EPHEMERAL_C25519).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + packet.append_bytes_fixed(&kp.public_bytes()).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + if start_encrypted_payload_at == 0 { + start_encrypted_payload_at = packet.len(); + } + offer_c25519 = Some(kp); + } + + if (enable_kex_algorithms & KEX_KYBER768) != 0 { + let ephemeral_kyber = pqc_kyber::Keypair::generate(&mut random::SecureRandom::default()); + packet.append_u8(FIELD_EPHEMERAL_KYBER_PUBLIC).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + packet.append_bytes_fixed(&ephemeral_kyber.public).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + offer_kyber = Some(ephemeral_kyber); + } + + let setup_key = setup_key?; // will return None if no mutually usable algorithms were enabled + + packet.append_u8(FIELD_STATIC_IDENTITY).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + local_identity.marshal(&mut packet).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + + packet.append_u8(FIELD_SESSION_ID).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + packet.append_bytes(&local_session_id.to_le_bytes()[..HEADER_SESSION_ID_LENGTH_BYTES]).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + + let outgoing_obfuscation_cipher = Aes::new(&remote_identity.fingerprint[..32]); + let hmac_key: Secret<48> = zt_kbkdf_hmac_sha384(&static_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_HMAC).first_n(); + armor_control_packet( + &mut packet, + &INIT_HEADER_AND_CTR_NONCE, + start_encrypted_payload_at, + &outgoing_obfuscation_cipher, + &zt_kbkdf_hmac_sha384(&setup_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_AES_CTR).first_n(), + &hmac_key, + ); + + Some(( + Arc::new(Session:: { + obj, + setup_key, + static_key: static_key.clone(), + hmac_key, + outgoing_obfuscation_cipher, + state: RwLock::new(MutState { + keys: [None, None, None, None, None, None, None, None], + current_key: 0, + offer: Some(Box::new(EphemeralOffer { + ratchet_counter: 0, + timestamp: current_time, + c25519: offer_c25519, + kyber768_alice_keypair: offer_kyber, + kyber768_bob_ciphertext: None, + p384: offer_p384, + })), + remote_session_id: 0, // not known yet + }), + creation_time: current_time, + local_session_id, + outbound_counter: AtomicU32::new(1), // INIT was counter==0 + last_inbound_counter: AtomicU32::new(0), + inbound_counter_dedup: array_from_fn(|| AtomicU32::new(0xffffffff)), + role: Role::Alice, + enabled_kex_algorithms: enable_kex_algorithms, + setup_kex_algorithm, + }), + packet, + )) +} + +pub fn receive>( + sc: &SC, + local_identity: &Identity, + local_obfuscation_cipher: &Aes, + current_time: i64, + enable_kex_algorithms: u8, + packet: &Buffer, + output: &mut Buffer, +) -> ReceiveResult { + // Mini AES-CTR to deobfuscate the packet. Usually we only have to deobfuscate 11 bytes. + let obfuscation_counter = if let Ok(obfuscation_nonce) = packet.bytes_fixed_at::<12>(packet.len() - 12) { + let mut tmp = [0_u8; 16]; + tmp[..12].copy_from_slice(obfuscation_nonce); + tmp + } else { + unlikely_branch(); + return ReceiveResult::Error(ReceiveError::Invalid); + }; + let mut obfuscation_keystream = [0_u8; 16]; + local_obfuscation_cipher.encrypt_block(&obfuscation_counter, &mut obfuscation_keystream); + + // Deobfuscate and extract values from common header. + let (counter, local_session_id, key_index, packet_type, nonce) = if let Ok(h) = packet.bytes_fixed_mut_at::(0) { + let ct = h[0] ^ obfuscation_keystream[0]; + let mut h = [ + ct, + h[1] ^ obfuscation_keystream[1], + h[2] ^ obfuscation_keystream[2], + h[3] ^ obfuscation_keystream[3], + h[4] ^ obfuscation_keystream[4], + h[5] ^ obfuscation_keystream[5], + h[6] ^ obfuscation_keystream[6], + h[7] ^ obfuscation_keystream[7], + h[8] ^ obfuscation_keystream[8], + h[9] ^ obfuscation_keystream[9], + h[10] ^ obfuscation_keystream[10], + 0, // last byte of CTR/GCM nonce will be set to 'role' + ]; + ( + u32::from_le_bytes(*byte_array_range::<12, 1, 4>(&h)), + u64::from_le_bytes(*byte_array_range::<12, 3, 8>(&h)).wrapping_shr(16), // read u64 then shift to get u48 + ct.wrapping_shr(HEADER_KT_KEY_ID_SHIFT), + ct & HEADER_KT_PACKET_TYPE_MASK, + h, + ) + } else { + unlikely_branch(); + return ReceiveResult::Error(ReceiveError::Invalid); + }; + + if packet_type == PACKET_TYPE_DATA { + if let Some(session) = sc.lookup_session(local_session_id) { + let state = session.state.read(); + if let Some(session_key) = state.keys[key_index as usize].as_ref() { + if let Some(e) = session.check_incoming_packet_counter(counter) { + unlikely_branch(); + return ReceiveResult::Error(e); + } + todo!() + } else { + unlikely_branch(); + return ReceiveResult::Error(ReceiveError::FailedAuthentication); + } + } else { + unlikely_branch(); + return ReceiveResult::Error(ReceiveError::UnknownLocalSessionId); + } + } else { + unlikely_branch(); + + // These optional values are filled in by the private read_fields() function. It has to be called + // differently depending on whether this is INIT/DATAGRAM or something else. For those two the + // payload must be decrypted first, then parsed, then authenticated because we don't know the + // identity of the other side to get the static key to authenticate yet. For other types we do + // have the static key and can authenticate first. + let mut cursor = HEADER_SIZE; + let ( + mut remote_data, + mut remote_auth_data, + mut remote_ephemeral_c25519, + mut remote_ephemeral_p384, + mut remote_identity, + mut remote_session_id, + mut remote_ephemeral_kyber_public, + mut remote_ephemeral_kyber_encapsulated_secret, + mut remote_timestamp, + mut remote_timestamp_echo, + mut remote_ratchet_counter, + ) = (None, None, None, None, None, None, None, None, None, None, None); + + if packet_type == PACKET_TYPE_DATAGRAM || packet_type == PACKET_TYPE_INIT { + // DATAGRAM and INIT are sent to initiate a session or pass a bit of sessionless data. They + // carry an unencrypted epheemral key before their encrypted payload. This key agrees with + // the recipient's static identity and creates a "setup key" used for INIT/INIT_ACK or only + // once for DATAGRAM. + + if key_index != 0 || local_session_id != 0 { + return ReceiveResult::Error(ReceiveError::Invalid); + } + + // Save deobfuscated outer ephemeral data in a buffer for performing auth later, as we'll need to + // auth the whole packet as it was before the sender obfuscated it. + let mut outer_data_for_auth = [0_u8; 64]; + let mut outer_data_for_auth_ptr = 0; + + // Create a little deobufscator closure to deobfuscate outer ephemeral keys, also adds to outer + // data for auth buffer above. Start at HEADER_SIZE since we used those bytes at the top. + debug_assert!(HEADER_SIZE < 16); // sanity check + let mut obfuscation_keystream_ptr = HEADER_SIZE; + let deobfuscate = |b: &mut [u8]| { + for i in 0..b.len() { + if obfuscation_keystream_ptr == 16 { + obfuscation_counter[15] += 1; + local_obfuscation_cipher.encrypt_block(&obfuscation_counter, &mut obfuscation_keystream); + obfuscation_keystream_ptr = 0; + } + let deob = b[i] ^ obfuscation_keystream[obfuscation_keystream_ptr]; + obfuscation_keystream_ptr += 1; + b[i] = deob; + outer_data_for_auth[outer_data_for_auth_ptr] = deob; + outer_data_for_auth_ptr += 1; + } + }; + + // Parse and extract the outer ephemeral key and use key agreement between it and the local identity + // to create the setup key, which is used for INIT and INITACK. + let (setup_key, setup_kex_algorithm) = if let Ok(mut outer_ephemeral_type) = packet.read_u8(&mut cursor) { + // Deobfuscate the outer ephemeral key type byte (and save it for future auth inclusion). + outer_ephemeral_type ^= obfuscation_keystream[obfuscation_keystream_ptr]; + obfuscation_keystream_ptr += 1; + outer_data_for_auth[outer_data_for_auth_ptr] = outer_ephemeral_type; + outer_data_for_auth_ptr += 1; + + if outer_ephemeral_type == FIELD_EPHEMERAL_NISTP384 { + // NIST P-384, the current default. + if let Some(local_static_p384) = local_identity.secret.as_ref().and_then(|lss| lss.p384.as_ref()) { + if let Ok(remote_p384) = packet.read_bytes_fixed::(&mut cursor) { + let mut remote_p384 = remote_p384.clone(); + deobfuscate(&mut remote_p384); + if let Some(sk) = P384PublicKey::from_bytes(&remote_p384).and_then(|rp384| { + remote_ephemeral_p384.insert(rp384); + local_static_p384.ecdh.agree(&rp384) + }) { + (Secret(SHA512::hash(sk.as_bytes())), KEX_NISTP384) + } else { + return ReceiveResult::Error(ReceiveError::FailedAuthentication); + } + } else { + return ReceiveResult::Error(ReceiveError::Invalid); + } + } else { + return ReceiveResult::Error(ReceiveError::UnsupportedCipher); + } + } else if outer_ephemeral_type == FIELD_EPHEMERAL_C25519 { + if let Some(local_static_secret) = local_identity.secret.as_ref() { + if let Ok(remote_c25519) = packet.read_bytes_fixed::(&mut cursor) { + let mut remote_c25519 = remote_c25519.clone(); + deobfuscate(&mut remote_c25519); + let sk = Secret(SHA512::hash(local_static_secret.c25519.agree(&remote_c25519).as_bytes())); + remote_ephemeral_c25519.insert(remote_c25519); + (sk, KEX_C25519) + } else { + return ReceiveResult::Error(ReceiveError::Invalid); + } + } else { + return ReceiveResult::Error(ReceiveError::UnsupportedCipher); + } + } else { + return ReceiveResult::Error(ReceiveError::UnsupportedCipher); + } + } else { + return ReceiveResult::Error(ReceiveError::Invalid); + }; + + if (packet.len() - cursor) < SHA384_HASH_SIZE { + return ReceiveResult::Error(ReceiveError::Invalid); + } + let (ciphertext, packet_hmac) = { + let ct_len = packet.len() - SHA384_HASH_SIZE; + let pb = packet.as_bytes(); + (&pb[cursor..ct_len], &pb[ct_len..]) + }; + + let mut ctr = AesCtr::new(&zt_kbkdf_hmac_sha384(&setup_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_AES_CTR).as_bytes()[..32]); + nonce[11] = Role::Alice as u8; // INIT and DATAGRAM always sent by "alice" + ctr.init(&nonce[..12]); + debug_assert!(output.is_empty()); + ctr.crypt(ciphertext, output.append_bytes_get_mut(ciphertext.len()).unwrap()); + + cursor = 0; // start reading from 'output' now + if !read_fields( + &mut cursor, + output, + &mut remote_data, + &mut remote_auth_data, + &mut remote_ephemeral_c25519, + &mut remote_ephemeral_p384, + &mut remote_identity, + &mut remote_session_id, + &mut remote_ephemeral_kyber_public, + &mut remote_ephemeral_kyber_encapsulated_secret, + &mut remote_timestamp, + &mut remote_timestamp_echo, + &mut remote_ratchet_counter, + ) { + return ReceiveResult::Error(ReceiveError::Invalid); + } + + let remote_identity = if let Some(id) = remote_identity { + id + } else { + return ReceiveResult::Error(ReceiveError::MissingRequiredFields); + }; + if !sc.validate_identity(&remote_identity) { + return ReceiveResult::Error(ReceiveError::FailedAuthentication); + } + let static_key = if let Some(k) = local_identity.agree(&remote_identity) { + k + } else { + return ReceiveResult::Error(ReceiveError::FailedAuthentication); + }; + + let mut hmac = HMACSHA384::new(zt_kbkdf_hmac_sha384(&static_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_HMAC).as_bytes()); + hmac.update(&nonce[..HEADER_SIZE]); // first 11 bytes of nonce are deobfuscated header + hmac.update(&outer_data_for_auth[..outer_data_for_auth_ptr]); // deobfuscated outer unencrypted data + hmac.update(ciphertext); // encrypted ciphertext as it would have been HMAC'd by sender + if !hmac.finish().eq(packet_hmac) { + return ReceiveResult::Error(ReceiveError::FailedAuthentication); + } + + if packet_type == PACKET_TYPE_INIT { + let (offer, session_key, session_kex_algorithms) = + ephemeral_counter_offer(0, current_time, &static_key, &remote_ephemeral_c25519, &remote_ephemeral_kyber_public, &remote_ephemeral_p384, enable_kex_algorithms); + if session_kex_algorithms != 0 { + let remote_session_id = if let Some(sid) = remote_session_id { + sid + } else { + return ReceiveResult::Error(ReceiveError::MissingRequiredFields); + }; + let (local_session_id, obj) = if let Some(auth) = sc.authorize_new_session( + &remote_identity, + remote_auth_data + .and_then(|(start, end)| output.as_byte_range(start, end).map_or(None, |br| Buffer::::from_bytes(br).map_or(None, |br| Some(br)))), + ) { + auth + } else { + return ReceiveResult::Error(ReceiveError::NewSessionRejected); + }; + + let outgoing_obfuscation_cipher = Aes::new(&remote_identity.fingerprint[..32]); + let hmac_key: Secret<48> = zt_kbkdf_hmac_sha384(&static_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_HMAC).first_n(); + + output.clear(); + create_ack_packet( + &mut output, + Some(local_session_id), + remote_session_id, + 0, + 0, + &offer, + &session_key, + &zt_kbkdf_hmac_sha384(&setup_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_AES_CTR).first_n(), + &hmac_key, + &outgoing_obfuscation_cipher, + PACKET_TYPE_INIT_ACK, + Role::Bob, + ); + + return ReceiveResult::OkNewSession( + remote_identity, + Arc::new(Session:: { + obj, + setup_key, + static_key: static_key.clone(), + hmac_key, + outgoing_obfuscation_cipher, + state: RwLock::new(MutState { + keys: [Some(Box::new(SessionKey::new(session_key, current_time, 0, 0, session_kex_algorithms))), None, None, None, None, None, None, None], + current_key: 0, + offer: Some(offer), + remote_session_id, + }), + creation_time: current_time, + local_session_id, + outbound_counter: AtomicU32::new(1), // ACK was counter==0 + last_inbound_counter: AtomicU32::new(counter), + inbound_counter_dedup: array_from_fn(|| AtomicU32::new(0xffffffff)), + role: Role::Bob, + enabled_kex_algorithms: enable_kex_algorithms, + setup_kex_algorithm, + }), + ); + } else { + return ReceiveResult::Error(ReceiveError::MissingRequiredFields); + } + } else if let Some(d) = remote_data { + return ReceiveResult::OkDatagram(remote_identity, d); + } else { + return ReceiveResult::Error(ReceiveError::MissingRequiredFields); + } + } else if let Some(session) = sc.lookup_session(local_session_id) { + // With everything but INIT and DATAGRAM we can look up the session and auth first. + + // It shouldn't be possible to make it here with these values. (In non-debug they'd be ignored.) + debug_assert_ne!(packet_type, PACKET_TYPE_DATA); + debug_assert_ne!(packet_type, PACKET_TYPE_DATAGRAM); + debug_assert_ne!(packet_type, PACKET_TYPE_INIT); + + if (packet.len() - HEADER_SIZE) < SHA384_HASH_SIZE { + return ReceiveResult::Error(ReceiveError::Invalid); + } + let (ciphertext, packet_hmac) = { + let ct_len = packet.len() - SHA384_HASH_SIZE; + let pb = packet.as_bytes(); + (&pb[HEADER_SIZE..ct_len], &pb[ct_len..]) + }; + + let mut hmac = HMACSHA384::new(zt_kbkdf_hmac_sha384(&session.static_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_HMAC).as_bytes()); + hmac.update(&nonce[..HEADER_SIZE]); + hmac.update(ciphertext); + if !hmac.finish().eq(packet_hmac) { + return ReceiveResult::Error(ReceiveError::FailedAuthentication); + } + + if let Some(e) = session.check_incoming_packet_counter(counter) { + return ReceiveResult::Error(e); + } + + let session_m = session.state.upgradable_read(); + + let mut ctr = if packet_type == PACKET_TYPE_INIT_ACK { + AesCtr::new(&zt_kbkdf_hmac_sha384(&session.setup_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_AES_CTR).as_bytes()[..32]) + } else { + if let Some(k) = session_m.keys[key_index as usize].as_ref() { + AesCtr::new(k.aes_ctr_key.as_bytes()) + } else { + return ReceiveResult::Error(ReceiveError::NoSessionKeyAtIndex); + } + }; + ctr.init(&nonce); + debug_assert!(output.is_empty()); + ctr.crypt(ciphertext, output.append_bytes_get_mut(ciphertext.len()).unwrap()); + + let mut cursor = HEADER_SIZE; + if !read_fields( + &mut cursor, + output, + &mut remote_data, + &mut remote_auth_data, + &mut remote_ephemeral_c25519, + &mut remote_ephemeral_p384, + &mut remote_identity, + &mut remote_session_id, + &mut remote_ephemeral_kyber_public, + &mut remote_ephemeral_kyber_encapsulated_secret, + &mut remote_timestamp, + &mut remote_timestamp_echo, + &mut remote_ratchet_counter, + ) { + return ReceiveResult::Error(ReceiveError::Invalid); + } + + match packet_type { + PACKET_TYPE_INIT_ACK | PACKET_TYPE_REKEY_ACK => { + // ACKs carrying ephemeral key counter-offers; the only difference between INIT_ACK + // REKEY_ACK is that INIT_ACK is encrypted with the setup key not a session key. + } + PACKET_TYPE_REKEY => {} + _ => return ReceiveResult::Ok, // passed auth, but ignore unknown packet types + } + todo!() + } else { + return ReceiveResult::Error(ReceiveError::UnknownLocalSessionId); + } + } +} + +impl Session { + #[inline(always)] + fn check_incoming_packet_counter(&self, counter: u32) -> Option { + let last_counter = self.last_inbound_counter.load(Ordering::Relaxed); + if counter.wrapping_sub(last_counter).min(last_counter.wrapping_sub(counter)) > MAX_COUNTER_DIFFERENCE { + unlikely_branch(); + return Some(ReceiveError::CounterTooOutOfSync); + } + if self.inbound_counter_dedup[(counter & 0xff) as usize].swap(counter, Ordering::Relaxed) == counter { + unlikely_branch(); + return Some(ReceiveError::Duplicate); + } + self.last_inbound_counter.store(counter, Ordering::Relaxed); + None + } +} + +/// Create both 11-byte header and 12-byte nonce (header is just first 11 bytes) +#[inline(always)] +fn make_header_and_nonce(ratchet_counter: u64, packet_type: u8, counter: u32, session_id: u64, role: u8) -> [u8; HEADER_SIZE + 1] { + let counter = counter.to_le_bytes(); + let session_id = session_id.to_le_bytes(); + [ + (ratchet_counter as u8).wrapping_shl(HEADER_KT_KEY_ID_SHIFT) | packet_type, + counter[0], + counter[1], + counter[2], + counter[3], + session_id[0], + session_id[1], + session_id[2], + session_id[3], + session_id[4], + session_id[5], + role, + ] +} + +/// Creates an INIT_ACK or REKEY_ACK packet to send back an ephemeral "counter-offer." +fn create_ack_packet( + output: &mut Buffer, + local_session_id: Option, + remote_session_id: u64, + ratchet_counter: u64, + packet_counter: u32, + offer: &EphemeralOffer, + new_session_key: &Secret<64>, + ctr_key: &Secret<32>, + hmac_key: &Secret<48>, + outgoing_obfuscation_cipher: &Aes, + packet_type: u8, + role: Role, +) { + let header_and_nonce = make_header_and_nonce(ratchet_counter, packet_type, packet_counter, remote_session_id, role as u8); + debug_assert!(output.is_empty()); + output.append_bytes(&header_and_nonce[..HEADER_SIZE]).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + + if let Some(local_session_id) = local_session_id { + // Only included in INIT_ACK + output.append_u8(FIELD_SESSION_ID).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + output.append_bytes(&local_session_id.to_le_bytes()[..HEADER_SESSION_ID_LENGTH_BYTES]).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + } + + output.append_u8(FIELD_RATCHET_COUNTER).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + output.append_varint(ratchet_counter).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + + if let Some(c25519_offer) = offer.c25519.as_ref() { + output.append_u8(FIELD_EPHEMERAL_C25519).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + output.append_bytes_fixed(&c25519_offer.public_bytes()).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + } + if let Some(kyber768_bob_ciphertext) = offer.kyber768_bob_ciphertext.as_ref() { + output.append_u8(FIELD_EPHEMERAL_KYBER_CIPHERTEXT).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + output.append_bytes_fixed(&kyber768_bob_ciphertext).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + } + if let Some(p384_offer) = offer.p384.as_ref() { + output.append_u8(FIELD_EPHEMERAL_NISTP384).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + output.append_bytes_fixed(p384_offer.public_key_bytes()).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + } + output.append_u8(FIELD_SESSION_KEY_HASH).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + output.append_bytes_fixed(&SHA384::hash(new_session_key.as_bytes())).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + + armor_control_packet(&mut output, &header_and_nonce, HEADER_SIZE, &outgoing_obfuscation_cipher, ctr_key, hmac_key); +} + +/// Armor a packet with AES-256-CTR+HMAC-SHA384 +fn armor_control_packet( + packet: &mut Buffer, + ctr_nonce: &[u8], + start_encrypted_payload_at: usize, + outgoing_obfuscation_cipher: &Aes, + ctr_key: &Secret<32>, + hmac_key: &Secret<48>, +) { + let mut ctr = AesCtr::new(ctr_key.as_bytes()); + ctr.init(ctr_nonce); + ctr.crypt_in_place(&mut packet.as_bytes_mut()[start_encrypted_payload_at..]); + + let mut hmac = HMACSHA384::new(hmac_key.as_bytes()); + hmac.update(packet.as_bytes()); + let hmac = hmac.finish(); + packet.append_bytes_fixed(&hmac).expect(UNEXPECTED_OVERFLOW_PANIC_MSG); + + let mut obfuscation_keystream = [0_u8; 64]; + let mut obfuscation_counter = [0_u8; 16]; + obfuscation_counter[..12].copy_from_slice(&hmac[36..48]); + for i in 0..4 { + obfuscation_counter[15] = i as u8; + outgoing_obfuscation_cipher.encrypt_block(&obfuscation_counter, &mut obfuscation_keystream[(16 * i)..(16 * (i + 1))]); + } + let packet_bytes = packet.as_bytes_mut(); + for i in 0..start_encrypted_payload_at { + packet_bytes[i] ^= obfuscation_keystream[i]; + } +} + +/// Generate a counter-offer to ephemeral keys from an INIT or REKEY, and agree to get session key. +/// Return: offer, session secret, algorithm bit map (check that the latter is not zero!) +fn ephemeral_counter_offer( + ratchet_counter: u64, + current_time: i64, + static_key: &Secret<64>, + remote_ephemeral_c25519: &Option<[u8; C25519_PUBLIC_KEY_SIZE]>, + remote_ephemeral_kyber_public: &Option<[u8; pqc_kyber::KYBER_PUBLICKEYBYTES]>, + remote_ephemeral_p384: &Option, + enabled_kex_algorithms: u8, +) -> (Box, Secret<64>, u8) { + let mut session_key = Secret(hmac_sha512(&ratchet_counter.to_le_bytes(), static_key.as_bytes())); + let mut session_kex_algorithms = 0; + + let c25519 = if (enabled_kex_algorithms & KEX_C25519) != 0 { + remote_ephemeral_c25519.as_ref().map(|pk| { + session_kex_algorithms |= KEX_C25519; + let e = C25519KeyPair::generate(); + session_key = Secret(hmac_sha512(session_key.as_bytes(), pk)); + e + }) + } else { + None + }; + + let kyber_ciphertext = if (enabled_kex_algorithms & KEX_KYBER768) != 0 { + remote_ephemeral_kyber_public.as_ref().and_then(|pk| { + if let Ok((ct, ss)) = pqc_kyber::encapsulate(pk, &mut random::SecureRandom::get()) { + session_kex_algorithms |= KEX_KYBER768; + session_key = Secret(hmac_sha512(session_key.as_bytes(), &ss)); + Some(ct) + } else { + None + } + }) + } else { + None + }; + + // FIPS note: for hybrid key agreement P-384 must be last so the session key is HMAC(salt, compliant key) as far as FIPS is concerned. + let p384 = if (enabled_kex_algorithms & KEX_NISTP384) != 0 { + remote_ephemeral_p384.as_ref().and_then(|pk| { + let e = P384KeyPair::generate(); + let k = e.agree(pk); + k.map(|k| { + session_kex_algorithms |= KEX_NISTP384; + session_key = Secret(hmac_sha512(session_key.as_bytes(), k.as_bytes())); + e + }) + }) + } else { + None + }; + + ( + Box::new(EphemeralOffer { + ratchet_counter, + timestamp: current_time, + c25519, + kyber768_alice_keypair: None, + kyber768_bob_ciphertext: kyber_ciphertext, + p384, + }), + session_key, + session_kex_algorithms, + ) +} + +/// Internal method to read and extract data from a series of field ID, value, ... +/// Returns false if a parse or other error occurs. +fn read_fields( + cursor: &mut usize, + packet: &Buffer, + data: &mut Option<(usize, usize)>, + auth_data: &mut Option<(usize, usize)>, + ephemeral_c25519: &mut Option<[u8; C25519_PUBLIC_KEY_SIZE]>, + ephemeral_p384: &mut Option, + identity: &mut Option, + session_id: &mut Option, + ephemeral_kyber_public: &mut Option<[u8; pqc_kyber::KYBER_PUBLICKEYBYTES]>, + ephemeral_kyber_encapsulated_secret: &mut Option<[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>, + timestamp: &mut Option, + timestamp_echo: &mut Option, + ratchet_counter: &mut Option, +) -> bool { + while *cursor < packet.len() { + let field_type = packet.read_u8(cursor).unwrap(); // bounds already checked by while condition + match field_type { + FIELD_DATA => { + if data.is_some() { + return false; + } + if let Ok(dlen) = packet.read_varint(cursor) { + let start = *cursor; + let end = *cursor + (dlen as usize); + if end > packet.len() { + return false; + } + data.insert((start, end)); + } else { + return false; + } + } + FIELD_AUTH_DATA => { + if auth_data.is_some() { + return false; + } + if let Ok(dlen) = packet.read_varint(cursor) { + let start = *cursor; + let end = *cursor + (dlen as usize); + if end > packet.len() { + return false; + } + auth_data.insert((start, end)); + } else { + return false; + } + } + FIELD_STATIC_IDENTITY => { + if identity.is_some() { + return false; + } + if let Ok(id) = Identity::unmarshal(packet, cursor) { + identity.insert(id); + } else { + return false; + } + } + FIELD_SESSION_ID => { + if session_id.is_some() { + return false; + } + if let Ok(sid) = packet.read_bytes_fixed::<6>(cursor) { + session_id.insert( + (sid[0] as u64) + | (sid[1] as u64).wrapping_shl(8) + | (sid[2] as u64).wrapping_shl(16) + | (sid[3] as u64).wrapping_shl(24) + | (sid[4] as u64).wrapping_shl(32) + | (sid[5] as u64).wrapping_shl(40), + ); + } else { + return false; + } + } + FIELD_EPHEMERAL_C25519 => { + if ephemeral_c25519.is_some() { + return false; + } + if let Ok(c25519) = packet.read_bytes_fixed::(cursor) { + ephemeral_c25519.insert(c25519.clone()); + } else { + return false; + } + } + FIELD_EPHEMERAL_NISTP384 => { + if ephemeral_p384.is_some() { + return false; + } + if let Ok(remote_p384) = packet.read_bytes_fixed::(cursor) { + if let Some(remote_p384) = P384PublicKey::from_bytes(remote_p384) { + ephemeral_p384.insert(remote_p384); + } else { + return false; + } + } else { + return false; + } + } + FIELD_EPHEMERAL_KYBER_PUBLIC => { + if ephemeral_kyber_public.is_some() { + return false; + } + if let Ok(remote_kyber) = packet.read_bytes_fixed::<{ pqc_kyber::KYBER_PUBLICKEYBYTES }>(cursor) { + ephemeral_kyber_public.insert(remote_kyber.clone()); + } else { + return false; + } + } + FIELD_EPHEMERAL_KYBER_ENCAPSULATED_SECRET => { + if ephemeral_kyber_encapsulated_secret.is_some() { + return false; + } + if let Ok(remote_kyber_secret) = packet.read_bytes_fixed::<{ pqc_kyber::KYBER_CIPHERTEXTBYTES }>(cursor) { + ephemeral_kyber_encapsulated_secret.insert(remote_kyber_secret.clone()); + } else { + return false; + } + } + FIELD_TIMESTAMP => { + if timestamp.is_some() { + return false; + } + if let Ok(ts) = packet.read_u64_le(cursor) { + timestamp.insert(ts); + } else { + return false; + } + } + FIELD_TIMESTAMP_ECHO => { + if timestamp_echo.is_some() { + return false; + } + if let Ok(ts) = packet.read_u64_le(cursor) { + timestamp_echo.insert(ts); + } else { + return false; + } + } + FIELD_RATCHET_COUNTER => { + if ratchet_counter.is_some() { + return false; + } + if let Ok(c) = packet.read_varint(cursor) { + ratchet_counter.insert(c); + } else { + return false; + } + } + _ => { + // Any fields we add in the future will need to be prefixed by their size. + if let Ok(additional_field_len) = packet.read_varint(cursor) { + *cursor += additional_field_len as usize; + } else { + return false; + } + } + } + } + return true; +} + +#[cfg(test)] +mod tests { + //use super::*; +} + +*/ diff --git a/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs b/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs index 675d17e92..f8ded7ae7 100644 --- a/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs +++ b/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs @@ -14,9 +14,6 @@ pub(crate) struct SymmetricSecret { /// Master key from which other keys are derived. pub key: Secret<64>, - /// Key used for HMAC extended validation on packets like HELLO. - pub packet_hmac_key: Secret<48>, - /// Pool of keyed AES-GMAC-SIV engines (pooled to avoid AES re-init every time). pub aes_gmac_siv: Pool, } @@ -24,16 +21,11 @@ pub(crate) struct SymmetricSecret { impl SymmetricSecret { /// Create a new symmetric secret, deriving all sub-keys and such. pub fn new(key: Secret<64>) -> SymmetricSecret { - let packet_hmac_key = zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_PACKET_HMAC); let aes_factory = AesGmacSivPoolFactory( zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0).first_n(), zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1).first_n(), ); - SymmetricSecret { - key, - packet_hmac_key, - aes_gmac_siv: Pool::new(2, aes_factory), - } + SymmetricSecret { key, aes_gmac_siv: Pool::new(2, aes_factory) } } } diff --git a/zerotier-network-hypervisor/src/vl2/switch.rs b/zerotier-network-hypervisor/src/vl2/switch.rs index b0eb7c95a..5b883c258 100644 --- a/zerotier-network-hypervisor/src/vl2/switch.rs +++ b/zerotier-network-hypervisor/src/vl2/switch.rs @@ -13,7 +13,7 @@ pub struct Switch {} #[async_trait] impl InnerProtocolInterface for Switch { #[allow(unused)] - async fn handle_packet(&self, peer: &Peer, source_path: &Path, forward_secrecy: bool, extended_authentication: bool, verb: u8, payload: &PacketBuffer) -> bool { + async fn handle_packet(&self, peer: &Peer, source_path: &Path, verb: u8, payload: &PacketBuffer) -> bool { false } @@ -22,8 +22,6 @@ impl InnerProtocolInterface for Switch { &self, peer: &Peer, source_path: &Path, - forward_secrecy: bool, - extended_authentication: bool, in_re_verb: u8, in_re_message_id: u64, error_code: u8, @@ -34,7 +32,7 @@ impl InnerProtocolInterface for Switch { } #[allow(unused)] - async fn handle_ok(&self, peer: &Peer, source_path: &Path, forward_secrecy: bool, extended_authentication: bool, in_re_verb: u8, in_re_message_id: u64, payload: &PacketBuffer, cursor: &mut usize) -> bool { + async fn handle_ok(&self, peer: &Peer, source_path: &Path, in_re_verb: u8, in_re_message_id: u64, payload: &PacketBuffer, cursor: &mut usize) -> bool { false }