From 59ebba800356a4e98696687722385fbc4a2b92da Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Sun, 14 Aug 2022 14:00:43 -0600 Subject: [PATCH] Docs. --- zerotier-core-crypto/README.md | 9 ++ zerotier-network-hypervisor/Cargo.toml | 2 +- zerotier-network-hypervisor/src/vl1/noise.rs | 153 ++++++++++++------- 3 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 zerotier-core-crypto/README.md diff --git a/zerotier-core-crypto/README.md b/zerotier-core-crypto/README.md new file mode 100644 index 000000000..2fb55cac8 --- /dev/null +++ b/zerotier-core-crypto/README.md @@ -0,0 +1,9 @@ +# ZeroTier Core Cryptography Library + +------ + +This is mostly just glue to provide a simple consistent API in front of OpenSSL and some platform-specific crypto APIs. + +It's thin and simple enough that we can easily create variants of it in the future for e.g. if we need to support some proprietary FIPS module or something. + +It also contains a few utilities and helper functions. diff --git a/zerotier-network-hypervisor/Cargo.toml b/zerotier-network-hypervisor/Cargo.toml index 6676d202f..6a5e6e201 100644 --- a/zerotier-network-hypervisor/Cargo.toml +++ b/zerotier-network-hypervisor/Cargo.toml @@ -11,7 +11,7 @@ debug_events = [] [dependencies] zerotier-core-crypto = { path = "../zerotier-core-crypto" } -pqc_kyber = { path = "../third_party/kyber", features = ["kyber512", "90s", "reference"], default-features = false } +pqc_kyber = { path = "../third_party/kyber", features = ["kyber512", "reference"], default-features = false } async-trait = "^0" base64 = "^0" lz4_flex = { version = "^0", features = ["safe-encode", "safe-decode", "checked-decode"] } diff --git a/zerotier-network-hypervisor/src/vl1/noise.rs b/zerotier-network-hypervisor/src/vl1/noise.rs index 48ba29c07..7c1904acd 100644 --- a/zerotier-network-hypervisor/src/vl1/noise.rs +++ b/zerotier-network-hypervisor/src/vl1/noise.rs @@ -10,36 +10,80 @@ use zerotier_core_crypto::secret::Secret; use parking_lot::{Mutex, RwLock}; +/* + +ZeroTier V2 Noise(-like?) Session Protocol + +This protocol implements the Noise_IK key exchange pattern using NIST P-384 ECDH, AES-GCM, +and SHA512. So yes, Virginia, it's a FIPS-compliant Noise implementation. NIST P-384 is +not listed in official Noise documentation though, so consider it "Noise-like" if you +prefer. + +See also: http://noiseprotocol.org/noise.html + +Secondary hybrid exchange using Kyber512, the recently approved post-quantum KEX algorithm, +is also supported but is optional. When it is enabled the additional shared secret is +mixed into the final Noise_IK secret with HMAC/HKDF. This provides an exchange at least as +strong as the stronger of the two algorithms (ECDH and Kyber) since hashing anything with +a secret yields a secret. + +Kyber theoretically provides data forward secrecy into the post-quantum era if and when it +arrives. It might also reassure those paranoid about NIST elliptic curves a little, though +we tend to accept the arguments of Koblitz and Menezes against the curves being backdoored. +These arguments are explained at the end of this post: + +https://blog.cryptographyengineering.com/2015/10/22/a-riddle-wrapped-in-curve/ + +Kyber is used as long as both sides set the "jedi" parameter to true. It should be used +by default but can be disabled on tiny and slow devices or systems that talk to vast +numbers of endpoints and don't want the extra overhead. + +Last and least, this includes an obfuscation step. AES in simple ECB mode is used to encrypt +the first block (or few blocks for key exchanges) of each packet using a hash of the +recipient's public static identity as a key. Packets are indistinguishable from random by +any observer who doesn't know the identity of the recipient, making bulk de-anonymization +or filtering via DPI more difficult. Since only someone knowing the identity of the recipient +can form a valid initial key exchange packet, it renders nodes invisible to naive scanners +as well. + +*/ + /// Minimum supported size for work buffers / minimum packet size. pub const MIN_BUFFER_SIZE: usize = 1400; +/// Maximum possible value of a session ID +/// +/// Session IDs are 48 bits, so this is also a bit mask that can just be ANDed to get +/// a valid session ID. +pub const SESSION_ID_MAX: u64 = 0xffffffffffff; + /// Start attempting to rekey after a key has been used to send packets this many times. -pub const REKEY_AFTER_USES: u64 = 1073741824; +const REKEY_AFTER_USES: u64 = 1073741824; /// Maximum random jitter to add to rekey-after usage count. -pub const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576; +const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576; /// Hard expiration after this many uses. -pub const EXPIRE_AFTER_USES: u64 = (u32::MAX - 1024) as u64; +const EXPIRE_AFTER_USES: u64 = (u32::MAX - 1024) as u64; /// Start attempting to rekey after a key has been in use for this many milliseconds. -pub const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour +const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour /// Maximum random jitter to add to rekey-after time. -pub const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 5; - -/// Maximum possible value of a session ID -pub const SESSION_ID_MAX: u64 = 0xffffffffffff; +const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 5; const PACKET_TYPE_DATA: u8 = 0; const PACKET_TYPE_NOP: u8 = 1; -const PACKET_TYPE_KEY_OFFER: u8 = 2; -const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3; +const PACKET_TYPE_KEY_OFFER: u8 = 2; // "alice" +const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3; // "bob" +/// Secondary (hybrid) ephemeral key disabled. const E1_TYPE_NONE: u8 = 0; -const E1_TYPE_KYBER512_90S: u8 = 1; -// [4] counter | [6] destination session ID | [1] type +/// Secondary (hybrid) ephemeral key is Kyber512 +const E1_TYPE_KYBER512: u8 = 1; + +/// Header size; header is: [4] counter | [6] destination session ID | [1] type const HEADER_SIZE: usize = 11; const AES_GCM_TAG_SIZE: usize = 16; @@ -106,6 +150,11 @@ impl std::fmt::Debug for Error { } /// Obfuscator/deobfuscator for privacy and indistinguishability masking of packets on the wire. +/// +/// This is used to ECB encrypt the first block or for KEX packets the first few blocks using +/// the recipient's public static key as a key. That way a third party must know the identity +/// of the recipient to even see that this is ZeroTier traffic or trivial things like header +/// info, and bulk DPI becomes harder because you now have to do AES decrypts. pub struct Obfuscator(Aes); impl Obfuscator { @@ -154,45 +203,43 @@ struct State { keys: [Option; 2], // current, next } -impl Session { - /// Create a new session and return this plus an outgoing packet to send to the other end. - #[allow(unused)] - pub fn new<'a, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>( - buffer: &'a mut [u8; MAX_PACKET_SIZE], - local_session_id: u64, - local_s_public: &[u8; STATIC_PUBLIC_SIZE], - local_s_keypair_p384: &P384KeyPair, - remote_s_public: &[u8; STATIC_PUBLIC_SIZE], - remote_s_public_p384: &P384PublicKey, - psk: &Secret<64>, - associated_object: O, - jedi: bool, - ) -> Result<(Self, &'a [u8]), Error> { - debug_assert!(MAX_PACKET_SIZE >= MIN_BUFFER_SIZE); - assert!(local_session_id > 0 && local_session_id <= SESSION_ID_MAX); - let counter = Counter::new(); - if let Some(ss) = local_s_keypair_p384.agree(remote_s_public_p384) { - let outgoing_obfuscator = Obfuscator::new(remote_s_public); - if let Some((offer, psize)) = EphemeralOffer::create_alice_offer(buffer, counter.next(), local_session_id, 0, local_s_public, remote_s_public_p384, &ss, &outgoing_obfuscator, jedi) { - return Ok(( - Self { - id: local_session_id, - outgoing_packet_counter: counter, - remote_s_public_hash: SHA384::hash(remote_s_public), - psk: psk.clone(), - ss, - outgoing_obfuscator, - offer: Mutex::new(Some(offer)), - state: RwLock::new(State { remote_session_id: 0, keys: [None, None] }), - associated_object, - remote_s_public_p384: remote_s_public_p384.as_bytes().clone(), - }, - &buffer[..psize], - )); - } +/// Create a new session and return this plus an outgoing packet to send to the other end. +#[allow(unused)] +pub fn initiate<'a, O, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>( + buffer: &'a mut [u8; MAX_PACKET_SIZE], + local_session_id: u64, + local_s_public: &[u8; STATIC_PUBLIC_SIZE], + local_s_keypair_p384: &P384KeyPair, + remote_s_public: &[u8; STATIC_PUBLIC_SIZE], + remote_s_public_p384: &P384PublicKey, + psk: &Secret<64>, + associated_object: O, + jedi: bool, +) -> Result<(Session, &'a [u8]), Error> { + debug_assert!(MAX_PACKET_SIZE >= MIN_BUFFER_SIZE); + assert!(local_session_id > 0 && local_session_id <= SESSION_ID_MAX); + let counter = Counter::new(); + if let Some(ss) = local_s_keypair_p384.agree(remote_s_public_p384) { + let outgoing_obfuscator = Obfuscator::new(remote_s_public); + if let Some((offer, psize)) = EphemeralOffer::create_alice_offer(buffer, counter.next(), local_session_id, 0, local_s_public, remote_s_public_p384, &ss, &outgoing_obfuscator, jedi) { + return Ok(( + Session:: { + id: local_session_id, + outgoing_packet_counter: counter, + remote_s_public_hash: SHA384::hash(remote_s_public), + psk: psk.clone(), + ss, + outgoing_obfuscator, + offer: Mutex::new(Some(offer)), + state: RwLock::new(State { remote_session_id: 0, keys: [None, None] }), + associated_object, + remote_s_public_p384: remote_s_public_p384.as_bytes().clone(), + }, + &buffer[..psize], + )); } - return Err(Error::InvalidParameter); } + return Err(Error::InvalidParameter); } /// Receive a packet from the network and take the appropriate action. @@ -705,7 +752,7 @@ fn assemble_KEY_OFFER(mut b: &[u8]) - if b.len() >= 1 { let e1_type = b[0]; b = &b[1..]; - let alice_e1_public = if e1_type == E1_TYPE_KYBER512_90S { + let alice_e1_public = if e1_type == E1_TYPE_KYBER512 { if b.len() >= pqc_kyber::KYBER_PUBLICKEYBYTES { let k: [u8; pqc_kyber::KYBER_PUBLICKEYBYTES] = b[..pqc_kyber::KYBER_PUBLICKEYBYTES].try_into().unwrap(); b = &b[pqc_kyber::KYBER_PUBLICKEYBYTES..]; @@ -776,7 +823,7 @@ fn assemble_KEY_COUNTER_OFFER( b = &mut b[SESSION_ID_SIZE..]; if let Some(k) = bob_e1_public { - b[0] = E1_TYPE_KYBER512_90S; + b[0] = E1_TYPE_KYBER512; b[1..1 + pqc_kyber::KYBER_CIPHERTEXTBYTES].copy_from_slice(k); b = &mut b[1 + pqc_kyber::KYBER_CIPHERTEXTBYTES..]; } else { @@ -803,7 +850,7 @@ fn parse_KEY_COUNTER_OFFER_after_header(mut b: &[u8]) -> Result<(u64, Option<[u8 if b.len() >= 1 { let e1_type = b[0]; b = &b[1..]; - let bob_e1_public = if e1_type == E1_TYPE_KYBER512_90S { + let bob_e1_public = if e1_type == E1_TYPE_KYBER512 { if b.len() >= pqc_kyber::KYBER_CIPHERTEXTBYTES { let k: [u8; pqc_kyber::KYBER_CIPHERTEXTBYTES] = b[..pqc_kyber::KYBER_CIPHERTEXTBYTES].try_into().unwrap(); b = &b[pqc_kyber::KYBER_CIPHERTEXTBYTES..];