diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cded91568..f3c63087c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,12 +1,25 @@ on: [ push ] - -jobs: + +jobs: build_ubuntu: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 - + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + - name: make run: make @@ -15,16 +28,42 @@ jobs: steps: - name: checkout uses: actions/checkout@v3 - + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + - name: make - run: make + run: make test_ubuntu: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 - + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + - name: cargo test run: cargo test -v @@ -33,16 +72,29 @@ jobs: steps: - name: checkout uses: actions/checkout@v3 - + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + - name: cargo test run: cargo test -v - + # build_windows: # runs-on: windows-latest # steps: # - name: checkout # uses: actions/checkout@v3 - + # - name: setup msbuild # uses: microsoft/setup-msbuild@v1.1.3 diff --git a/Cargo.toml b/Cargo.toml index 5e8d1f598..e0bde91e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crypto", + "zssp", "network-hypervisor", "controller", "service", diff --git a/crypto/README.md b/crypto/README.md index 527e7d50a..bcad4ae38 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -3,5 +3,3 @@ ------ Most of this library is just glue to provide a simple safe API around things like OpenSSL or OS-specific crypto APIs. - -It also contains [ZSSP](ZSSP.md), the V2 ZeroTier Secure Session Protocol. diff --git a/crypto/src/aes.rs b/crypto/src/aes.rs index b13e21a45..3a8f9d142 100644 --- a/crypto/src/aes.rs +++ b/crypto/src/aes.rs @@ -298,6 +298,9 @@ mod fruit_flavored { #[cfg(not(target_os = "macos"))] mod openssl_aes { use crate::secret::Secret; + use foreign_types::ForeignTypeRef; + use openssl::cipher::CipherRef; + use openssl::cipher_ctx::{CipherCtx, CipherCtxRef}; use openssl::symm::{Cipher, Crypter, Mode}; use std::cell::UnsafeCell; use std::mem::MaybeUninit; @@ -385,7 +388,7 @@ mod openssl_aes { unsafe impl Send for Aes {} unsafe impl Sync for Aes {} - pub struct AesGcm(Secret<32>, usize, Option, bool); + pub struct AesGcm(Secret<32>, usize, CipherCtx, bool); impl AesGcm { /// Construct a new AES-GCM cipher. @@ -395,7 +398,7 @@ mod openssl_aes { match k.len() { 16 | 24 | 32 => { s.0[..k.len()].copy_from_slice(k); - Self(s, k.len(), None, encrypt) + Self(s, k.len(), CipherCtx::new().unwrap(), encrypt) } _ => { panic!("AES supports 128, 192, or 256 bits keys"); @@ -408,58 +411,64 @@ mod openssl_aes { #[inline] pub fn reset_init_gcm(&mut self, iv: &[u8]) { assert_eq!(iv.len(), 12); - let mut c = Crypter::new( - aes_gcm_by_key_size(self.1), - if self.3 { - Mode::Encrypt - } else { - Mode::Decrypt - }, - &self.0 .0[..self.1], - Some(iv), - ) - .unwrap(); - c.pad(false); - //let _ = c.set_tag_len(16); - let _ = self.2.replace(c); + let t = aes_gcm_by_key_size(self.1); + let key = &self.0 .0[..self.1]; + { + let f = match self.3 { + true => CipherCtxRef::encrypt_init, + false => CipherCtxRef::decrypt_init, + }; + + f( + &mut self.2, + Some(unsafe { CipherRef::from_ptr(t.as_ptr() as *mut _) }), + None, + None, + ).unwrap(); + + self.2.set_key_length(key.len()).unwrap(); + + if let Some(iv_len) = t.iv_len() { + if iv.len() != iv_len { + self.2.set_iv_length(iv.len()).unwrap(); + } + } + + f(&mut self.2, None, Some(key), Some(iv)).unwrap(); + } + + self.2.set_padding(false); } #[inline(always)] pub fn aad(&mut self, aad: &[u8]) { - assert!(self.2.as_mut().unwrap().aad_update(aad).is_ok()); + self.2.cipher_update(aad, None).unwrap(); } /// Encrypt or decrypt (same operation with CTR mode) #[inline(always)] pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) { - assert!(self.2.as_mut().unwrap().update(input, output).is_ok()); + self.2.cipher_update(input, Some(output)).unwrap(); } /// Encrypt or decrypt in place (same operation with CTR mode) #[inline(always)] pub fn crypt_in_place(&mut self, data: &mut [u8]) { - assert!(self - .2 - .as_mut() - .unwrap() - .update(unsafe { &*std::slice::from_raw_parts(data.as_ptr(), data.len()) }, data) - .is_ok()); + self.2.cipher_update(unsafe { &*std::slice::from_raw_parts(data.as_ptr(), data.len()) }, Some(data)).unwrap(); } #[inline(always)] pub fn finish_encrypt(&mut self) -> [u8; 16] { let mut tag = [0_u8; 16]; - let mut c = self.2.take().unwrap(); - assert!(c.finalize(&mut tag).is_ok()); - assert!(c.get_tag(&mut tag).is_ok()); + self.2.cipher_final(&mut tag).unwrap(); + self.2.tag(&mut tag).unwrap(); tag } #[inline(always)] pub fn finish_decrypt(&mut self, expected_tag: &[u8]) -> bool { - let mut c = self.2.take().unwrap(); - if c.set_tag(expected_tag).is_ok() { - let result = c.finalize(&mut []).is_ok(); + if self.2.set_tag(expected_tag).is_ok() { + let result = self.2.cipher_final(&mut []).is_ok(); result } else { false diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 2e1d9b05f..f4c6d574d 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -10,6 +10,5 @@ pub mod salsa; pub mod secret; pub mod verified; pub mod x25519; -pub mod zssp; pub const ZEROES: [u8; 64] = [0_u8; 64]; diff --git a/utils/src/varint.rs b/utils/src/varint.rs index e1f91f5a6..76e4750f0 100644 --- a/utils/src/varint.rs +++ b/utils/src/varint.rs @@ -33,6 +33,28 @@ pub fn write(w: &mut W, v: u64) -> std::io::Result<()> { w.write_all(&b[0..i]) } +/// Dencode up to 10 bytes as a varint. +/// +/// if the supplied byte slice does not contain a valid varint encoding this will return None. +/// if the supplied byte slice is shorter than expected this will return None. +pub fn decode(b: &[u8]) -> Option<(u64, usize)> { + let mut v = 0_u64; + let mut pos = 0; + let mut i = 0_usize; + while i < b.len() && i < VARINT_MAX_SIZE_BYTES { + let b = b[i]; + i += 1; + if b <= 0x7f { + v |= (b as u64).wrapping_shl(pos); + pos += 7; + } else { + v |= ((b & 0x7f) as u64).wrapping_shl(pos); + return Some((v, i)); + } + } + return None; +} + /// Read a variable length integer, returning the value and the number of bytes written. pub fn read(r: &mut R) -> std::io::Result<(u64, usize)> { let mut v = 0_u64; diff --git a/zssp/Cargo.toml b/zssp/Cargo.toml new file mode 100644 index 000000000..0f37d6aa3 --- /dev/null +++ b/zssp/Cargo.toml @@ -0,0 +1,35 @@ +[package] +authors = ["ZeroTier, Inc. ", "Adam Ierymenko "] +edition = "2021" +license = "MPL-2.0" +name = "zerotier-zssp" +version = "0.1.0" + +[dependencies] +zerotier-utils = { path = "../utils" } +zerotier-crypto = { path = "../crypto" } +pqc_kyber = { path = "../third_party/kyber", features = ["kyber1024", "reference"], default-features = false } +#ed25519-dalek = { version = "1.0.1", features = ["std", "u64_backend"], default-features = false } +#foreign-types = "0.3.1" +#lazy_static = "^1" +#poly1305 = { version = "0.8.0", features = [], default-features = false } +#pqc_kyber = { path = "../third_party/kyber", features = ["kyber1024", "reference"], default-features = false } +#pqc_kyber = { version = "^0", features = ["kyber1024", "reference"], default-features = false } +#rand_core = "0.5.1" +#rand_core_062 = { package = "rand_core", version = "0.6.2" } +#subtle = "2.4.1" +#x25519-dalek = { version = "1.2.0", features = ["std", "u64_backend"], default-features = false } + +#[target."cfg(windows)".dependencies] +#openssl = { version = "^0", features = ["vendored"], default-features = false } +#winapi = { version = "^0", features = ["handleapi", "ws2ipdef", "ws2tcpip"] } + +#[target."cfg(not(windows))".dependencies] +#openssl = { version = "^0", features = [], default-features = false } +#libc = "^0" +#signal-hook = "^0" + +#[dev-dependencies] +#criterion = "0.3" +#sha2 = "^0" +#hex-literal = "^0" diff --git a/crypto/ZSSP.md b/zssp/ZSSP.md similarity index 100% rename from crypto/ZSSP.md rename to zssp/ZSSP.md diff --git a/zssp/changes.txt b/zssp/changes.txt new file mode 100644 index 000000000..ff8a5efdc --- /dev/null +++ b/zssp/changes.txt @@ -0,0 +1,19 @@ +zssp has been moved into it's own crate. + +zssp has been cut up into several files, only the new zssp.rs file contains the critical security path. + +Standardized the naming conventions for security variables throughout zssp. + +Implemented a safer version of write_all for zssp to use. This has 3 benefits: it completely prevents unknown io errors, making error handling easier and self-documenting; it completely prevents src from being truncated in dest, putting in an extra barrier to prevent catastrophic key truncation; and it has slightly less performance overhead than a write_all. + +Implemented a safer version of read_exact for zssp to use. This has similar benefits to the previous change. + +Refactored most buffer logic to use safe_read_exact and safe_write_all, the resulting code is less verbose and easier to analyze: Because of this refactor the buffer overrun below was caught. + +Fixed a buffer overrun panic when decoding alice_ratchet_key_fingerprint + +Renamed variables and added extra intermediate values so encoding and decoding are more obviously symmetric. + +Added multiple comments. + +Removed Box, EphemeralOffer is now passed out by reference instead of returned up the stack. diff --git a/zssp/rustfmt.toml b/zssp/rustfmt.toml new file mode 120000 index 000000000..39f97b043 --- /dev/null +++ b/zssp/rustfmt.toml @@ -0,0 +1 @@ +../rustfmt.toml \ No newline at end of file diff --git a/zssp/src/app_layer.rs b/zssp/src/app_layer.rs new file mode 100644 index 000000000..fe7da3533 --- /dev/null +++ b/zssp/src/app_layer.rs @@ -0,0 +1,72 @@ +use std::ops::Deref; + +use zerotier_crypto::{p384::{P384KeyPair, P384PublicKey}, secret::Secret}; + +use crate::{zssp::{Session, ReceiveContext}, ints::SessionId}; + + +/// Trait to implement to integrate the session into an application. +/// +/// Templating the session on this trait lets the code here be almost entirely transport, OS, +/// and use case independent. +pub trait ApplicationLayer: Sized { + /// Arbitrary opaque object associated with a session, such as a connection state object. + type SessionUserData; + + /// Arbitrary object that dereferences to the session, such as Arc>. + type SessionRef: Deref>; + + /// A buffer containing data read from the network that can be cached. + /// + /// This can be e.g. a pooled buffer that automatically returns itself to the pool when dropped. + /// It can also just be a Vec or Box<[u8]> or something like that. + type IncomingPacketBuffer: AsRef<[u8]>; + + /// Remote physical address on whatever transport this session is using. + type RemoteAddress; + + /// Rate limit for attempts to rekey existing sessions in milliseconds (default: 2000). + const REKEY_RATE_LIMIT_MS: i64 = 2000; + + /// Get a reference to this host's static public key blob. + /// + /// This must contain a NIST P-384 public key but can contain other information. In ZeroTier this + /// is a byte serialized identity. It could just be a naked NIST P-384 key if that's all you need. + fn get_local_s_public_blob(&self) -> &[u8]; + + /// Get SHA384(this host's static public key blob). + /// + /// This allows us to avoid computing SHA384(public key blob) over and over again. + fn get_local_s_public_blob_hash(&self) -> &[u8; 48]; + + /// Get a reference to this hosts' static public key's NIST P-384 secret key pair. + /// + /// This must return the NIST P-384 public key that is contained within the static public key blob. + fn get_local_s_keypair(&self) -> &P384KeyPair; + + /// Extract the NIST P-384 ECC public key component from a static public key blob or return None on failure. + /// + /// This is called to parse the static public key blob from the other end and extract its NIST P-384 public + /// key. SECURITY NOTE: the information supplied here is from the wire so care must be taken to parse it + /// safely and fail on any error or corruption. + fn extract_s_public_from_raw(static_public: &[u8]) -> Option; + + /// Look up a local session by local session ID or return None if not found. + fn lookup_session(&self, local_session_id: SessionId) -> Option; + + /// Rate limit and check an attempted new session (called before accept_new_session). + fn check_new_session(&self, rc: &ReceiveContext, remote_address: &Self::RemoteAddress) -> bool; + + /// Check whether a new session should be accepted. + /// + /// On success a tuple of local session ID, static secret, and associated object is returned. The + /// static secret is whatever results from agreement between the local and remote static public + /// keys. + fn accept_new_session( + &self, + receive_context: &ReceiveContext, + remote_address: &Self::RemoteAddress, + remote_static_public: &[u8], + remote_metadata: &[u8], + ) -> Option<(SessionId, Secret<64>, Self::SessionUserData)>; +} diff --git a/zssp/src/constants.rs b/zssp/src/constants.rs new file mode 100644 index 000000000..708c53ee5 --- /dev/null +++ b/zssp/src/constants.rs @@ -0,0 +1,108 @@ + +/// Minimum size of a valid physical ZSSP packet or packet fragment. +pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; + +/// Minimum physical MTU for ZSSP to function. +pub const MIN_TRANSPORT_MTU: usize = 1280; + +/// Minimum recommended interval between calls to service() on each session, in milliseconds. +pub const SERVICE_INTERVAL: u64 = 10000; + +/// Setting this to true enables kyber1024 post-quantum forward secrecy. +/// +/// Kyber1024 is used for data forward secrecy but not authentication. Authentication would +/// require Kyber1024 in identities, which would make them huge, and isn't needed for our +/// threat model which is data warehousing today to decrypt tomorrow. Breaking authentication +/// is only relevant today, not in some mid to far future where a QC that can break 384-bit ECC +/// exists. +/// +/// This is normally enabled but could be disabled at build time for e.g. very small devices. +/// It might not even be necessary there to disable it since it's not that big and is usually +/// faster than NIST P-384 ECDH. +pub(crate) const JEDI: bool = true; + +/// Maximum number of fragments for data packets. +pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 + +/// Maximum number of fragments for key exchange packets (can be smaller to save memory, only a few needed) +pub(crate) const KEY_EXCHANGE_MAX_FRAGMENTS: usize = 2; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. + +/// Start attempting to rekey after a key has been used to send packets this many times. +/// +/// This is 1/4 the NIST recommended maximum and 1/8 the absolute limit where u32 wraps. +/// As such it should leave plenty of margin against nearing key reuse bounds w/AES-GCM. +pub(crate) const REKEY_AFTER_USES: u64 = 536870912; + +/// Maximum random jitter to add to rekey-after usage count. +pub(crate) const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576; + +/// Hard expiration after this many uses. +/// +/// Use of the key beyond this point is prohibited. If we reach this number of key uses +/// the key will be destroyed in memory and the session will cease to function. A hard +/// error is also generated. +pub(crate) const EXPIRE_AFTER_USES: u64 = (u32::MAX - 1024) as u64; + +/// Start attempting to rekey after a key has been in use for this many milliseconds. +pub(crate) const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour + +/// Maximum random jitter to add to rekey-after time. +pub(crate) const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 minutes + +/// Version 0: AES-256-GCM + NIST P-384 + optional Kyber1024 PQ forward secrecy +pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; + +/// Secondary key type: none, use only P-384 for forward secrecy. +pub(crate) const E1_TYPE_NONE: u8 = 0; + +/// Secondary key type: Kyber1024, PQ forward secrecy enabled. +pub(crate) const E1_TYPE_KYBER1024: u8 = 1; + +/// Size of packet header +pub(crate) const HEADER_SIZE: usize = 16; + +/// Size of AES-GCM keys (256 bits) +pub(crate) const AES_KEY_SIZE: usize = 32; + +/// Size of AES-GCM MAC tags +pub(crate) const AES_GCM_TAG_SIZE: usize = 16; + +/// Size of HMAC-SHA384 MAC tags +pub(crate) const HMAC_SIZE: usize = 48; + +/// Size of a session ID, which behaves a bit like a TCP port number. +/// +/// This is large since some ZeroTier nodes handle huge numbers of links, like roots and controllers. +pub(crate) const SESSION_ID_SIZE: usize = 6; + +/// Number of session keys to hold at a given time (current, previous, next). +pub(crate) const KEY_HISTORY_SIZE: usize = 3; + +// Packet types can range from 0 to 15 (4 bits) -- 0-3 are defined and 4-15 are reserved for future use +pub(crate) const PACKET_TYPE_DATA: u8 = 0; +pub(crate) const PACKET_TYPE_NOP: u8 = 1; +pub(crate) const PACKET_TYPE_INITIAL_KEY_OFFER: u8 = 2; // "alice" +pub(crate) const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3; // "bob" + +// Key usage labels for sub-key derivation using NIST-style KBKDF (basically just HMAC KDF). +pub(crate) const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'M'; // HMAC-SHA384 authentication for key exchanges +pub(crate) const KBKDF_KEY_USAGE_LABEL_HEADER_CHECK: u8 = b'H'; // AES-based header check code generation +pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction +pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction +pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHETING: u8 = b'R'; // Key input for next ephemeral ratcheting + +// AES key size for header check code generation +pub(crate) const HEADER_CHECK_AES_KEY_SIZE: usize = 16; + +/// Aribitrary starting value for master key derivation. +/// +/// It doesn't matter very much what this is but it's good for it to be unique. It should +/// be changed if this code is changed in any cryptographically meaningful way like changing +/// the primary algorithm from NIST P-384 or the transport cipher from AES-GCM. +pub(crate) const INITIAL_KEY: [u8; 64] = [ + // macOS command line to generate: + // echo -n 'ZSSP_Noise_IKpsk2_NISTP384_?KYBER1024_AESGCM_SHA512' | shasum -a 512 | cut -d ' ' -f 1 | xxd -r -p | xxd -i + 0x35, 0x6a, 0x75, 0xc0, 0xbf, 0xbe, 0xc3, 0x59, 0x70, 0x94, 0x50, 0x69, 0x4c, 0xa2, 0x08, 0x40, 0xc7, 0xdf, 0x67, 0xa8, 0x68, 0x52, + 0x6e, 0xd5, 0xdd, 0x77, 0xec, 0x59, 0x6f, 0x8e, 0xa1, 0x99, 0xb4, 0x32, 0x85, 0xaf, 0x7f, 0x0d, 0xa9, 0x6c, 0x01, 0xfb, 0x72, 0x46, + 0xc0, 0x09, 0x58, 0xb8, 0xe0, 0xa8, 0xcf, 0xb1, 0x58, 0x04, 0x6e, 0x32, 0xba, 0xa8, 0xb8, 0xf9, 0x0a, 0xa4, 0xbf, 0x36, +]; diff --git a/zssp/src/ints.rs b/zssp/src/ints.rs new file mode 100644 index 000000000..00bd89df3 --- /dev/null +++ b/zssp/src/ints.rs @@ -0,0 +1,119 @@ + +use std::{sync::atomic::{AtomicU64, Ordering}}; + +use zerotier_crypto::random; +use zerotier_utils::memory; + + +/// "Canonical header" for generating 96-bit AES-GCM nonce and for inclusion in HMACs. +/// +/// This is basically the actual header but with fragment count and fragment total set to zero. +/// Fragmentation is not considered when authenticating the entire packet. A separate header +/// check code is used to make fragmentation itself more robust, but that's outside the scope +/// of AEAD authentication. +#[derive(Clone, Copy)] +#[repr(C, packed)] +pub(crate) struct CanonicalHeader(pub u64, pub u32); +impl CanonicalHeader { + #[inline(always)] + pub fn make(session_id: SessionId, packet_type: u8, counter: u32) -> Self { + CanonicalHeader( + (u64::from(session_id) | (packet_type as u64).wrapping_shl(48)).to_le(), + counter.to_le(), + ) + } + + #[inline(always)] + pub fn as_bytes(&self) -> &[u8; 12] { + memory::as_byte_array(self) + } +} + +/// 48-bit session ID (most significant 16 bits of u64 are unused) +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(transparent)] +pub struct SessionId(pub(crate) u64); + +impl SessionId { + /// The nil session ID used in messages initiating a new session. + /// + /// This is all 1's so that ZeroTier can easily tell the difference between ZSSP init packets + /// and ZeroTier V1 packets. + pub const NIL: SessionId = SessionId(0xffffffffffff); + + #[inline] + pub fn new_from_u64(i: u64) -> Option { + if i < Self::NIL.0 { + Some(Self(i)) + } else { + None + } + } + + + #[inline] + pub fn new_random() -> Self { + Self(random::next_u64_secure() % Self::NIL.0) + } +} + +impl From for u64 { + #[inline(always)] + fn from(sid: SessionId) -> Self { + sid.0 + } +} + + + +/// Outgoing packet counter with strictly ordered atomic semantics. +#[repr(transparent)] +pub(crate) struct Counter(AtomicU64); + +impl Counter { + #[inline(always)] + pub fn new() -> Self { + // Using a random value has no security implication. Zero would be fine. This just + // helps randomize packet contents a bit. + Self(AtomicU64::new(random::next_u32_secure() as u64)) + } + + /// Get the value most recently used to send a packet. + #[inline(always)] + pub fn previous(&self) -> CounterValue { + CounterValue(self.0.load(Ordering::SeqCst)) + } + + /// Get a counter value for the next packet being sent. + #[inline(always)] + pub fn next(&self) -> CounterValue { + CounterValue(self.0.fetch_add(1, Ordering::SeqCst)) + } +} + +/// A value of the outgoing packet counter. +/// +/// The used portion of the packet counter is the least significant 32 bits, but the internal +/// counter state is kept as a 64-bit integer. This makes it easier to correctly handle +/// key expiration after usage limits are reached without complicated logic to handle 32-bit +/// wrapping. Usage limits are below 2^32 so the actual 32-bit counter will not wrap for a +/// given shared secret key. +#[repr(transparent)] +#[derive(Copy, Clone)] +pub(crate) struct CounterValue(pub u64); + +impl CounterValue { + #[inline(always)] + pub fn to_u32(&self) -> u32 { + self.0 as u32 + } +} + + +/// Was this side the one who sent the first offer (Alice) or countered (Bob). +/// Note that role is not fixed. Either side can take either role. It's just who +/// initiated first. +pub enum Role { + Alice, + Bob, +} diff --git a/zssp/src/lib.rs b/zssp/src/lib.rs new file mode 100644 index 000000000..cdef07c08 --- /dev/null +++ b/zssp/src/lib.rs @@ -0,0 +1,10 @@ + +mod zssp; +mod app_layer; +mod ints; +mod tests; + +pub mod constants; +pub use zssp::{Error, ReceiveResult, ReceiveContext, Session}; +pub use app_layer::ApplicationLayer; +pub use ints::{SessionId, Role}; diff --git a/zssp/src/tests.rs b/zssp/src/tests.rs new file mode 100644 index 000000000..610d0a7d4 --- /dev/null +++ b/zssp/src/tests.rs @@ -0,0 +1,226 @@ + +#[cfg(test)] +mod tests { + use std::collections::LinkedList; + use std::sync::{Arc, Mutex}; + use zerotier_crypto::hash::SHA384; + use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; + use zerotier_crypto::random; + use zerotier_crypto::secret::Secret; + use zerotier_utils::hex; + + #[allow(unused_imports)] + use crate::*; + use constants::*; + + struct TestHost { + local_s: P384KeyPair, + local_s_hash: [u8; 48], + psk: Secret<64>, + session: Mutex>>>>, + session_id_counter: Mutex, + queue: Mutex>>, + key_id: Mutex<[u8; 16]>, + this_name: &'static str, + other_name: &'static str, + } + + impl TestHost { + fn new(psk: Secret<64>, this_name: &'static str, other_name: &'static str) -> Self { + let local_s = P384KeyPair::generate(); + let local_s_hash = SHA384::hash(local_s.public_key_bytes()); + Self { + local_s, + local_s_hash, + psk, + session: Mutex::new(None), + session_id_counter: Mutex::new(1), + queue: Mutex::new(LinkedList::new()), + key_id: Mutex::new([0; 16]), + this_name, + other_name, + } + } + } + + impl ApplicationLayer for Box { + type SessionUserData = u32; + type SessionRef = Arc>>; + type IncomingPacketBuffer = Vec; + type RemoteAddress = u32; + + const REKEY_RATE_LIMIT_MS: i64 = 0; + + fn get_local_s_public_blob(&self) -> &[u8] { + self.local_s.public_key_bytes() + } + + fn get_local_s_public_blob_hash(&self) -> &[u8; 48] { + &self.local_s_hash + } + + fn get_local_s_keypair(&self) -> &P384KeyPair { + &self.local_s + } + + fn extract_s_public_from_raw(static_public: &[u8]) -> Option { + P384PublicKey::from_bytes(static_public) + } + + fn lookup_session(&self, local_session_id: SessionId) -> Option { + self.session.lock().unwrap().as_ref().and_then(|s| { + if s.id == local_session_id { + Some(s.clone()) + } else { + None + } + }) + } + + fn check_new_session(&self, _: &ReceiveContext, _: &Self::RemoteAddress) -> bool { + true + } + + fn accept_new_session( + &self, + _: &ReceiveContext, + _: &u32, + _: &[u8], + _: &[u8], + ) -> Option<(SessionId, Secret<64>, Self::SessionUserData)> { + loop { + let mut new_id = self.session_id_counter.lock().unwrap(); + *new_id += 1; + return Some((SessionId::new_from_u64(*new_id).unwrap(), self.psk.clone(), 0)); + } + } + } + + #[allow(unused_variables)] + #[test] + fn establish_session() { + let mut data_buf = [0_u8; (1280 - 32) * MAX_FRAGMENTS]; + let mut mtu_buffer = [0_u8; 1280]; + let mut psk: Secret<64> = Secret::default(); + random::fill_bytes_secure(&mut psk.0); + + let alice_host = Box::new(TestHost::new(psk.clone(), "alice", "bob")); + let bob_host = Box::new(TestHost::new(psk.clone(), "bob", "alice")); + let alice_rc: Box>> = Box::new(ReceiveContext::new(&alice_host)); + let bob_rc: Box>> = Box::new(ReceiveContext::new(&bob_host)); + + //println!("zssp: size of session (bytes): {}", std::mem::size_of::>>()); + + let _ = alice_host.session.lock().unwrap().insert(Arc::new( + Session::start_new( + &alice_host, + |data| bob_host.queue.lock().unwrap().push_front(data.to_vec()), + SessionId::new_random(), + bob_host.local_s.public_key_bytes(), + &[], + &psk, + 1, + mtu_buffer.len(), + 1, + ) + .unwrap(), + )); + + let mut ts = 0; + for test_loop in 0..256 { + for host in [&alice_host, &bob_host] { + let send_to_other = |data: &mut [u8]| { + if std::ptr::eq(host, &alice_host) { + bob_host.queue.lock().unwrap().push_front(data.to_vec()); + } else { + alice_host.queue.lock().unwrap().push_front(data.to_vec()); + } + }; + + let rc = if std::ptr::eq(host, &alice_host) { + &alice_rc + } else { + &bob_rc + }; + + loop { + if let Some(qi) = host.queue.lock().unwrap().pop_back() { + let qi_len = qi.len(); + ts += 1; + let r = rc.receive(host, &0, send_to_other, &mut data_buf, qi, mtu_buffer.len(), ts); + if r.is_ok() { + let r = r.unwrap(); + match r { + ReceiveResult::Ok => { + //println!("zssp: {} => {} ({}): Ok", host.other_name, host.this_name, qi_len); + } + ReceiveResult::OkData(data) => { + //println!("zssp: {} => {} ({}): OkData length=={}", host.other_name, host.this_name, qi_len, data.len()); + assert!(!data.iter().any(|x| *x != 0x12)); + } + ReceiveResult::OkNewSession(new_session) => { + println!( + "zssp: {} => {} ({}): OkNewSession ({})", + host.other_name, + host.this_name, + qi_len, + u64::from(new_session.id) + ); + let mut hs = host.session.lock().unwrap(); + assert!(hs.is_none()); + let _ = hs.insert(Arc::new(new_session)); + } + ReceiveResult::Ignored => { + println!("zssp: {} => {} ({}): Ignored", host.other_name, host.this_name, qi_len); + } + } + } else { + println!( + "zssp: {} => {} ({}): error: {}", + host.other_name, + host.this_name, + qi_len, + r.err().unwrap().to_string() + ); + panic!(); + } + } else { + break; + } + } + + data_buf.fill(0x12); + if let Some(session) = host.session.lock().unwrap().as_ref().cloned() { + if session.established() { + { + let mut key_id = host.key_id.lock().unwrap(); + let security_info = session.status().unwrap(); + if !security_info.0.eq(key_id.as_ref()) { + *key_id = security_info.0; + println!( + "zssp: new key at {}: fingerprint {} ratchet {} kyber {}", + host.this_name, + hex::to_string(key_id.as_ref()), + security_info.2, + security_info.3 + ); + } + } + for _ in 0..4 { + assert!(session + .send( + send_to_other, + &mut mtu_buffer, + &data_buf[..((random::xorshift64_random() as usize) % data_buf.len())] + ) + .is_ok()); + } + if (test_loop % 8) == 0 && test_loop >= 8 && host.this_name.eq("alice") { + session.service(host, send_to_other, &[], mtu_buffer.len(), test_loop as i64, true); + } + } + } + } + } + } +} diff --git a/crypto/src/zssp.rs b/zssp/src/zssp.rs similarity index 55% rename from crypto/src/zssp.rs rename to zssp/src/zssp.rs index 6db42cfc9..db2f3bfbd 100644 --- a/crypto/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -3,16 +3,13 @@ // ZSSP: ZeroTier Secure Session Protocol // FIPS compliant Noise_IK with Jedi powers and built-in attack-resistant large payload (fragmentation) support. -use std::io::{Read, Write}; -use std::ops::Deref; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, RwLock}; -use crate::aes::{Aes, AesGcm}; -use crate::hash::{hmac_sha512, HMACSHA384, SHA384}; -use crate::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE}; -use crate::random; -use crate::secret::Secret; +use zerotier_crypto::aes::{Aes, AesGcm}; +use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, SHA384}; +use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE}; +use zerotier_crypto::random; +use zerotier_crypto::secret::Secret; use zerotier_utils::gatherarray::GatherArray; use zerotier_utils::memory; @@ -20,113 +17,14 @@ use zerotier_utils::ringbuffermap::RingBufferMap; use zerotier_utils::unlikely_branch; use zerotier_utils::varint; -/// Minimum size of a valid physical ZSSP packet or packet fragment. -pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; +use crate::app_layer::ApplicationLayer; +use crate::ints::*; +use crate::constants::*; -/// Minimum physical MTU for ZSSP to function. -pub const MIN_TRANSPORT_MTU: usize = 1280; -/// Minimum recommended interval between calls to service() on each session, in milliseconds. -pub const SERVICE_INTERVAL: u64 = 10000; - -/// Setting this to true enables kyber1024 post-quantum forward secrecy. -/// -/// Kyber1024 is used for data forward secrecy but not authentication. Authentication would -/// require Kyber1024 in identities, which would make them huge, and isn't needed for our -/// threat model which is data warehousing today to decrypt tomorrow. Breaking authentication -/// is only relevant today, not in some mid to far future where a QC that can break 384-bit ECC -/// exists. -/// -/// This is normally enabled but could be disabled at build time for e.g. very small devices. -/// It might not even be necessary there to disable it since it's not that big and is usually -/// faster than NIST P-384 ECDH. -const JEDI: bool = true; - -/// Maximum number of fragments for data packets. -const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 - -/// Maximum number of fragments for key exchange packets (can be smaller to save memory, only a few needed) -const KEY_EXCHANGE_MAX_FRAGMENTS: usize = 2; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. - -/// Start attempting to rekey after a key has been used to send packets this many times. -/// -/// This is 1/4 the NIST recommended maximum and 1/8 the absolute limit where u32 wraps. -/// As such it should leave plenty of margin against nearing key reuse bounds w/AES-GCM. -const REKEY_AFTER_USES: u64 = 536870912; - -/// Maximum random jitter to add to rekey-after usage count. -const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576; - -/// Hard expiration after this many uses. -/// -/// Use of the key beyond this point is prohibited. If we reach this number of key uses -/// the key will be destroyed in memory and the session will cease to function. A hard -/// error is also generated. -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. -const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour - -/// Maximum random jitter to add to rekey-after time. -const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 minutes - -/// Version 0: AES-256-GCM + NIST P-384 + optional Kyber1024 PQ forward secrecy -const SESSION_PROTOCOL_VERSION: u8 = 0x00; - -/// Secondary key type: none, use only P-384 for forward secrecy. -const E1_TYPE_NONE: u8 = 0; - -/// Secondary key type: Kyber1024, PQ forward secrecy enabled. -const E1_TYPE_KYBER1024: u8 = 1; - -/// Size of packet header -const HEADER_SIZE: usize = 16; - -/// Size of AES-GCM keys (256 bits) -const AES_KEY_SIZE: usize = 32; - -/// Size of AES-GCM MAC tags -const AES_GCM_TAG_SIZE: usize = 16; - -/// Size of HMAC-SHA384 MAC tags -const HMAC_SIZE: usize = 48; - -/// Size of a session ID, which behaves a bit like a TCP port number. -/// -/// This is large since some ZeroTier nodes handle huge numbers of links, like roots and controllers. -const SESSION_ID_SIZE: usize = 6; - -/// Number of session keys to hold at a given time (current, previous, next). -const KEY_HISTORY_SIZE: usize = 3; - -// Packet types can range from 0 to 15 (4 bits) -- 0-3 are defined and 4-15 are reserved for future use -const PACKET_TYPE_DATA: u8 = 0; -const PACKET_TYPE_NOP: u8 = 1; -const PACKET_TYPE_KEY_OFFER: u8 = 2; // "alice" -const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3; // "bob" - -// Key usage labels for sub-key derivation using NIST-style KBKDF (basically just HMAC KDF). -const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'M'; // HMAC-SHA384 authentication for key exchanges -const KBKDF_KEY_USAGE_LABEL_HEADER_CHECK: u8 = b'H'; // AES-based header check code generation -const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction -const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction -const KBKDF_KEY_USAGE_LABEL_RATCHETING: u8 = b'R'; // Key input for next ephemeral ratcheting - -// AES key size for header check code generation -const HEADER_CHECK_AES_KEY_SIZE: usize = 16; - -/// Aribitrary starting value for master key derivation. -/// -/// It doesn't matter very much what this is but it's good for it to be unique. It should -/// be changed if this code is changed in any cryptographically meaningful way like changing -/// the primary algorithm from NIST P-384 or the transport cipher from AES-GCM. -const INITIAL_KEY: [u8; 64] = [ - // macOS command line to generate: - // echo -n 'ZSSP_Noise_IKpsk2_NISTP384_?KYBER1024_AESGCM_SHA512' | shasum -a 512 | cut -d ' ' -f 1 | xxd -r -p | xxd -i - 0x35, 0x6a, 0x75, 0xc0, 0xbf, 0xbe, 0xc3, 0x59, 0x70, 0x94, 0x50, 0x69, 0x4c, 0xa2, 0x08, 0x40, 0xc7, 0xdf, 0x67, 0xa8, 0x68, 0x52, - 0x6e, 0xd5, 0xdd, 0x77, 0xec, 0x59, 0x6f, 0x8e, 0xa1, 0x99, 0xb4, 0x32, 0x85, 0xaf, 0x7f, 0x0d, 0xa9, 0x6c, 0x01, 0xfb, 0x72, 0x46, - 0xc0, 0x09, 0x58, 0xb8, 0xe0, 0xa8, 0xcf, 0xb1, 0x58, 0x04, 0x6e, 0x32, 0xba, 0xa8, 0xb8, 0xf9, 0x0a, 0xa4, 0xbf, 0x36, -]; +//////////////////////////////////////////////////////////////// +// types +//////////////////////////////////////////////////////////////// pub enum Error { /// The packet was addressed to an unrecognized local session (should usually be ignored) @@ -162,47 +60,12 @@ pub enum Error { /// Data object is too large to send, even with fragmentation DataTooLarge, - /// An unexpected I/O error such as a buffer overrun occurred (possible bug) - UnexpectedIoError(std::io::Error), -} - -impl From for Error { - #[cold] - #[inline(never)] - fn from(e: std::io::Error) -> Self { - Self::UnexpectedIoError(e) - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::UnknownLocalSessionId(id) => f.write_str(format!("UnknownLocalSessionId({})", id.0).as_str()), - Self::InvalidPacket => f.write_str("InvalidPacket"), - Self::InvalidParameter => f.write_str("InvalidParameter"), - Self::FailedAuthentication => f.write_str("FailedAuthentication"), - Self::NewSessionRejected => f.write_str("NewSessionRejected"), - Self::MaxKeyLifetimeExceeded => f.write_str("MaxKeyLifetimeExceeded"), - Self::SessionNotEstablished => f.write_str("SessionNotEstablished"), - Self::RateLimited => f.write_str("RateLimited"), - Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"), - Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"), - Self::DataTooLarge => f.write_str("DataTooLarge"), - Self::UnexpectedIoError(e) => f.write_str(format!("UnexpectedIoError({})", e.to_string()).as_str()), - } - } -} - -impl std::error::Error for Error {} - -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(self, f) - } + /// An unexpected buffer overrun occured while attempting to encode or decode a packet, this can only ever happen if exceptionally large key blobs or metadata are being used, or as the result of an internal encoding bug. + UnexpectedBufferOverrun, } /// Result generated by the packet receive function, with possible payloads. -pub enum ReceiveResult<'a, H: Host> { +pub enum ReceiveResult<'a, H: ApplicationLayer> { /// Packet is valid, no action needs to be taken. Ok, @@ -220,209 +83,213 @@ pub enum ReceiveResult<'a, H: Host> { Ignored, } -/// 48-bit session ID (most significant 16 bits of u64 are unused) -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[repr(transparent)] -pub struct SessionId(u64); - -impl SessionId { - /// The nil session ID used in messages initiating a new session. - /// - /// This is all 1's so that ZeroTier can easily tell the difference between ZSSP init packets - /// and ZeroTier V1 packets. - pub const NIL: SessionId = SessionId(0xffffffffffff); - - #[inline] - pub fn new_from_u64(i: u64) -> Option { - if i < Self::NIL.0 { - Some(Self(i)) - } else { - None - } - } - - #[inline] - pub fn new_from_reader(r: &mut R) -> std::io::Result> { - let mut tmp = 0_u64.to_ne_bytes(); - r.read_exact(&mut tmp[..SESSION_ID_SIZE])?; - Ok(Self::new_from_u64(u64::from_le_bytes(tmp))) - } - - #[inline] - pub fn new_random() -> Self { - Self(random::next_u64_secure() % Self::NIL.0) - } -} - -impl From for u64 { - #[inline(always)] - fn from(sid: SessionId) -> Self { - sid.0 - } -} - /// State information to associate with receiving contexts such as sockets or remote paths/endpoints. /// /// This holds the data structures used to defragment incoming packets that are not associated with an /// existing session, which would be new attempts to create sessions. Typically one of these is associated /// with a single listen socket, local bound port, or other inbound endpoint. -pub struct ReceiveContext { +pub struct ReceiveContext { initial_offer_defrag: Mutex, 1024, 128>>, incoming_init_header_check_cipher: Aes, } -/// Trait to implement to integrate the session into an application. -/// -/// Templating the session on this trait lets the code here be almost entirely transport, OS, -/// and use case independent. -pub trait Host: Sized { - /// Arbitrary opaque object associated with a session, such as a connection state object. - type AssociatedObject; - - /// Arbitrary object that dereferences to the session, such as Arc>. - type SessionRef: Deref>; - - /// A buffer containing data read from the network that can be cached. - /// - /// This can be e.g. a pooled buffer that automatically returns itself to the pool when dropped. - /// It can also just be a Vec or Box<[u8]> or something like that. - type IncomingPacketBuffer: AsRef<[u8]>; - - /// Remote physical address on whatever transport this session is using. - type RemoteAddress; - - /// Rate limit for attempts to rekey existing sessions in milliseconds (default: 2000). - const REKEY_RATE_LIMIT_MS: i64 = 2000; - - /// Get a reference to this host's static public key blob. - /// - /// This must contain a NIST P-384 public key but can contain other information. In ZeroTier this - /// is a byte serialized identity. It could just be a naked NIST P-384 key if that's all you need. - fn get_local_s_public(&self) -> &[u8]; - - /// Get SHA384(this host's static public key blob). - /// - /// This allows us to avoid computing SHA384(public key blob) over and over again. - fn get_local_s_public_hash(&self) -> &[u8; 48]; - - /// Get a reference to this hosts' static public key's NIST P-384 secret key pair. - /// - /// This must return the NIST P-384 public key that is contained within the static public key blob. - fn get_local_s_keypair_p384(&self) -> &P384KeyPair; - - /// Extract the NIST P-384 ECC public key component from a static public key blob or return None on failure. - /// - /// This is called to parse the static public key blob from the other end and extract its NIST P-384 public - /// key. SECURITY NOTE: the information supplied here is from the wire so care must be taken to parse it - /// safely and fail on any error or corruption. - fn extract_p384_static(static_public: &[u8]) -> Option; - - /// Look up a local session by local session ID or return None if not found. - fn session_lookup(&self, local_session_id: SessionId) -> Option; - - /// Rate limit and check an attempted new session (called before accept_new_session). - fn check_new_session_attempt(&self, rc: &ReceiveContext, remote_address: &Self::RemoteAddress) -> bool; - - /// Check whether a new session should be accepted. - /// - /// On success a tuple of local session ID, static secret, and associated object is returned. The - /// static secret is whatever results from agreement between the local and remote static public - /// keys. - fn accept_new_session( - &self, - receive_context: &ReceiveContext, - remote_address: &Self::RemoteAddress, - remote_static_public: &[u8], - remote_metadata: &[u8], - ) -> Option<(SessionId, Secret<64>, Self::AssociatedObject)>; -} - /// ZSSP bi-directional packet transport channel. -pub struct Session { +pub struct Session { /// This side's session ID (unique on this side) pub id: SessionId, /// An arbitrary object associated with session (type defined in Host trait) - pub associated_object: H::AssociatedObject, + pub user_data: Layer::SessionUserData, send_counter: Counter, // Outgoing packet counter and nonce state psk: Secret<64>, // Arbitrary PSK provided by external code - ss: Secret<48>, // Static raw shared ECDH NIST P-384 key + noise_ss: Secret<48>, // Static raw shared ECDH NIST P-384 key header_check_cipher: Aes, // Cipher used for header MAC (fragmentation) state: RwLock, // Mutable parts of state (other than defrag buffers) - remote_s_public_hash: [u8; 48], // SHA384(remote static public key blob) - remote_s_public_p384: [u8; P384_PUBLIC_KEY_SIZE], // Remote NIST P-384 static public key + remote_s_public_blob_hash: [u8; 48], // SHA384(remote static public key blob) + remote_s_public_raw: [u8; P384_PUBLIC_KEY_SIZE], // Remote NIST P-384 static public key - defrag: Mutex, 8, 8>>, + defrag: Mutex, 8, 8>>, } struct SessionMutableState { - remote_session_id: Option, // The other side's 48-bit session ID - keys: [Option; KEY_HISTORY_SIZE], // Buffers to store current, next, and last active key - key_ptr: usize, // Pointer used for keys[] circular buffer - offer: Option>, // Most recent ephemeral offer sent to remote - last_remote_offer: i64, // Time of most recent ephemeral offer (ms) + remote_session_id: Option, // The other side's 48-bit session ID + session_keys: [Option; KEY_HISTORY_SIZE], // Buffers to store current, next, and last active key + cur_session_key_idx: usize, // Pointer used for keys[] circular buffer + offer: Option, // Most recent ephemeral offer sent to remote + last_remote_offer: i64, // Time of most recent ephemeral offer (ms) } -impl Session { +/// A shared symmetric session key. +struct SessionKey { + secret_fingerprint: [u8; 16], // First 128 bits of a SHA384 computed from the secret + establish_time: i64, // Time session key was established + establish_counter: u64, // Counter value at which session was established + lifetime: KeyLifetime, // Key expiration time and counter + ratchet_key: Secret<64>, // Ratchet key for deriving the next session key + receive_key: Secret, // Receive side AES-GCM key + send_key: Secret, // Send side AES-GCM key + receive_cipher_pool: Mutex>>, // Pool of initialized sending ciphers + send_cipher_pool: Mutex>>, // Pool of initialized receiving ciphers + ratchet_count: u64, // Number of new keys negotiated in this session + jedi: bool, // True if Kyber1024 was used (both sides enabled) +} + +/// Alice's KEY_OFFER, remembered so Noise agreement process can resume on KEY_COUNTER_OFFER. +struct EphemeralOffer { + id: [u8; 16], // Arbitrary random offer ID + creation_time: i64, // Local time when offer was created + ratchet_count: u64, // Ratchet count starting at zero for initial offer + ratchet_key: Option>, // Ratchet key from previous offer + ss_key: Secret<64>, // Shared secret in-progress, at state after offer sent + alice_e_keypair: P384KeyPair, // NIST P-384 key pair (Noise ephemeral key for Alice) + alice_hk_keypair: Option, // Kyber1024 key pair (agreement result mixed post-Noise) +} + +/// Key lifetime manager state and logic (separate to spotlight and keep clean) +struct KeyLifetime { + rekey_at_or_after_counter: u64, + hard_expire_at_counter: u64, + rekey_at_or_after_timestamp: i64, +} + + + +//////////////////////////////////////////////////////////////// +// functions +//////////////////////////////////////////////////////////////// + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnknownLocalSessionId(id) => f.write_str(format!("UnknownLocalSessionId({})", id.0).as_str()), + Self::InvalidPacket => f.write_str("InvalidPacket"), + Self::InvalidParameter => f.write_str("InvalidParameter"), + Self::FailedAuthentication => f.write_str("FailedAuthentication"), + Self::NewSessionRejected => f.write_str("NewSessionRejected"), + Self::MaxKeyLifetimeExceeded => f.write_str("MaxKeyLifetimeExceeded"), + Self::SessionNotEstablished => f.write_str("SessionNotEstablished"), + Self::RateLimited => f.write_str("RateLimited"), + Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"), + Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"), + Self::DataTooLarge => f.write_str("DataTooLarge"), + Self::UnexpectedBufferOverrun => f.write_str("UnexpectedBufferOverrun"), + } + } +} + +impl std::error::Error for Error {} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + + +/// Write src into buffer starting at the index idx. If buffer cannot fit src at that location, nothing at all is written and Error::UnexpectedBufferOverrun is returned. No other errors can be returned by this function. An idx incremented by the amount written is returned. +fn safe_write_all(buffer: &mut [u8], idx: usize, src: &[u8]) -> Result { + let dest = &mut buffer[idx..]; + let amt = src.len(); + if dest.len() >= amt { + dest[..amt].copy_from_slice(src); + Ok(idx + amt) + } else { + unlikely_branch(); + Err(Error::UnexpectedBufferOverrun) + } +} +/// Write a variable length integer, which can consume up to 10 bytes. Uses safe_write_all to do so. +#[inline(always)] +fn varint_safe_write(buffer: &mut [u8], idx: usize, v: u64) -> Result { + let mut b = [0_u8; varint::VARINT_MAX_SIZE_BYTES]; + let i = varint::encode(&mut b, v); + safe_write_all(buffer, idx, &b[0..i]) +} + +/// Read exactly amt bytes from src and return the slice those bytes reside in. If src is smaller than amt, Error::InvalidPacket is returned. if the read was successful src is incremented to start at the first unread byte. +fn safe_read_exact<'a>(src: &mut &'a [u8], amt: usize) -> Result<&'a [u8], Error> { + if src.len() >= amt { + let (a, b) = src.split_at(amt); + *src = b; + Ok(a) + } else { + unlikely_branch(); + Err(Error::InvalidPacket) + } +} +/// Read a variable length integer, which can consume up to 10 bytes. Uses varint_safe_read to do so. +#[inline(always)] +fn varint_safe_read(src: &mut &[u8]) -> Result { + let (v, amt) = varint::decode(*src).ok_or(Error::InvalidPacket)?; + let (_, b) = src.split_at(amt); + *src = b; + Ok(v) +} + + +impl Session { /// Create a new session and send an initial key offer message to the other end. /// /// * `host` - Interface to application using ZSSP - /// * `local_session_id` - ID for this side of the session, must be locally unique - /// * `remote_s_public` - Remote side's public key/identity + /// * `local_session_id` - ID for this side (Alice) of the session, must be locally unique + /// * `remote_s_public_raw` - Remote side's (Bob's) public key/identity /// * `offer_metadata` - Arbitrary meta-data to send with key offer (empty if none) /// * `psk` - Arbitrary pre-shared key to include as initial key material (use all zero secret if none) - /// * `associated_object` - Arbitrary object to put into session - /// * `mtu` - Physical wire MTU + /// * `user_data` - Arbitrary object to put into session + /// * `mtu` - Physical wire maximum transmition unit /// * `current_time` - Current monotonic time in milliseconds - pub fn new( - host: &H, + pub fn start_new( + host: &Layer, mut send: SendFunction, local_session_id: SessionId, - remote_s_public: &[u8], + remote_s_public_blob: &[u8], offer_metadata: &[u8], psk: &Secret<64>, - associated_object: H::AssociatedObject, + user_data: Layer::SessionUserData, mtu: usize, current_time: i64, ) -> Result { - if let Some(remote_s_public_p384) = H::extract_p384_static(remote_s_public) { - if let Some(ss) = host.get_local_s_keypair_p384().agree(&remote_s_public_p384) { + let bob_s_public_blob = remote_s_public_blob; + if let Some(bob_s_public) = Layer::extract_s_public_from_raw(bob_s_public_blob) { + if let Some(noise_ss) = host.get_local_s_keypair().agree(&bob_s_public) { let send_counter = Counter::new(); - let remote_s_public_hash = SHA384::hash(remote_s_public); + let bob_s_public_blob_hash = SHA384::hash(bob_s_public_blob); let header_check_cipher = - Aes::new(kbkdf512(ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::()); - if let Ok(offer) = send_ephemeral_offer( + Aes::new(kbkdf512(noise_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::()); + let mut offer = None; + if send_ephemeral_offer( &mut send, send_counter.next(), local_session_id, None, - host.get_local_s_public(), + host.get_local_s_public_blob(), offer_metadata, - &remote_s_public_p384, - &remote_s_public_hash, - &ss, + &bob_s_public, + &bob_s_public_blob_hash, + &noise_ss, None, None, mtu, current_time, - ) { + &mut offer + ).is_ok() { return Ok(Self { id: local_session_id, - associated_object, + user_data, send_counter, psk: psk.clone(), - ss, + noise_ss, header_check_cipher, state: RwLock::new(SessionMutableState { remote_session_id: None, - keys: [None, None, None], - key_ptr: 0, - offer: Some(offer), + session_keys: [None, None, None], + cur_session_key_idx: 0, + offer, last_remote_offer: i64::MIN, }), - remote_s_public_hash, - remote_s_public_p384: remote_s_public_p384.as_bytes().clone(), + remote_s_public_blob_hash: bob_s_public_blob_hash, + remote_s_public_raw: bob_s_public.as_bytes().clone(), defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), }); } @@ -446,13 +313,17 @@ impl Session { debug_assert!(mtu_buffer.len() >= MIN_TRANSPORT_MTU); let state = self.state.read().unwrap(); if let Some(remote_session_id) = state.remote_session_id { - if let Some(key) = state.keys[state.key_ptr].as_ref() { + if let Some(session_key) = state.session_keys[state.cur_session_key_idx].as_ref() { // Total size of the armored packet we are going to send (may end up being fragmented) - let mut packet_len = data.len() + HEADER_SIZE + AES_GCM_TAG_SIZE; + let packet_len = data.len() + HEADER_SIZE + AES_GCM_TAG_SIZE; // This outgoing packet's nonce counter value. let counter = self.send_counter.next(); + //////////////////////////////////////////////////////////////// + // packet encoding for post-noise transport + //////////////////////////////////////////////////////////////// + // Create initial header for first fragment of packet and place in first HEADER_SIZE bytes of buffer. create_packet_header( mtu_buffer, @@ -465,10 +336,11 @@ impl Session { // Get an initialized AES-GCM cipher and re-initialize with a 96-bit IV built from remote session ID, // packet type, and counter. - let mut c = key.get_send_cipher(counter)?; + let mut c = session_key.get_send_cipher(counter)?; c.reset_init_gcm(CanonicalHeader::make(remote_session_id, PACKET_TYPE_DATA, counter.to_u32()).as_bytes()); // Send first N-1 fragments of N total fragments. + let last_fragment_size; if packet_len > mtu_buffer.len() { let mut header: [u8; 16] = mtu_buffer[..HEADER_SIZE].try_into().unwrap(); let fragment_data_mtu = mtu_buffer.len() - HEADER_SIZE; @@ -489,18 +361,21 @@ impl Session { break; } } - packet_len = data.len() + HEADER_SIZE + AES_GCM_TAG_SIZE; + last_fragment_size = data.len() + HEADER_SIZE + AES_GCM_TAG_SIZE; + } else { + last_fragment_size = packet_len; } // Send final fragment (or only fragment if no fragmentation was needed) - let gcm_tag_idx = data.len() + HEADER_SIZE; - c.crypt(data, &mut mtu_buffer[HEADER_SIZE..gcm_tag_idx]); - mtu_buffer[gcm_tag_idx..packet_len].copy_from_slice(&c.finish_encrypt()); + let payload_end = data.len() + HEADER_SIZE; + c.crypt(data, &mut mtu_buffer[HEADER_SIZE..payload_end]); + let gcm_tag = c.finish_encrypt(); + mtu_buffer[payload_end..last_fragment_size].copy_from_slice(&gcm_tag); set_header_check_code(mtu_buffer, &self.header_check_cipher); - send(&mut mtu_buffer[..packet_len]); + send(&mut mtu_buffer[..last_fragment_size]); // Check reusable AES-GCM instance back into pool. - key.return_send_cipher(c); + session_key.return_send_cipher(c); return Ok(()); } else { @@ -515,7 +390,7 @@ impl Session { /// Check whether this session is established. pub fn established(&self) -> bool { let state = self.state.read().unwrap(); - state.remote_session_id.is_some() && state.keys[state.key_ptr].is_some() + state.remote_session_id.is_some() && state.session_keys[state.cur_session_key_idx].is_some() } /// Get information about this session's security state. @@ -524,7 +399,7 @@ impl Session { /// and whether Kyber1024 was used. None is returned if the session isn't established. pub fn status(&self) -> Option<([u8; 16], i64, u64, bool)> { let state = self.state.read().unwrap(); - if let Some(key) = state.keys[state.key_ptr].as_ref() { + if let Some(key) = state.session_keys[state.cur_session_key_idx].as_ref() { Some((key.secret_fingerprint, key.establish_time, key.ratchet_count, key.jedi)) } else { None @@ -541,7 +416,7 @@ impl Session { /// * `force_rekey` - Re-key the session now regardless of key aging (still subject to rate limiting) pub fn service( &self, - host: &H, + host: &Layer, mut send: SendFunction, offer_metadata: &[u8], mtu: usize, @@ -549,27 +424,29 @@ impl Session { force_rekey: bool, ) { let state = self.state.read().unwrap(); - if (force_rekey - || state.keys[state.key_ptr] - .as_ref() - .map_or(true, |key| key.lifetime.should_rekey(self.send_counter.previous(), current_time))) + if ( + force_rekey + || state.session_keys[state.cur_session_key_idx] + .as_ref() + .map_or(true, |key| key.lifetime.should_rekey(self.send_counter.previous(), current_time))) && state - .offer - .as_ref() - .map_or(true, |o| (current_time - o.creation_time) > H::REKEY_RATE_LIMIT_MS) - { - if let Some(remote_s_public_p384) = P384PublicKey::from_bytes(&self.remote_s_public_p384) { - if let Ok(offer) = send_ephemeral_offer( + .offer + .as_ref() + .map_or(true, |o| (current_time - o.creation_time) > Layer::REKEY_RATE_LIMIT_MS + ) { + if let Some(remote_s_public) = P384PublicKey::from_bytes(&self.remote_s_public_raw) { + let mut offer = None; + if send_ephemeral_offer( &mut send, self.send_counter.next(), self.id, state.remote_session_id, - host.get_local_s_public(), + host.get_local_s_public_blob(), offer_metadata, - &remote_s_public_p384, - &self.remote_s_public_hash, - &self.ss, - state.keys[state.key_ptr].as_ref(), + &remote_s_public, + &self.remote_s_public_blob_hash, + &self.noise_ss, + state.session_keys[state.cur_session_key_idx].as_ref(), if state.remote_session_id.is_some() { Some(&self.header_check_cipher) } else { @@ -577,21 +454,22 @@ impl Session { }, mtu, current_time, - ) { + &mut offer + ).is_ok() { drop(state); - let _ = self.state.write().unwrap().offer.replace(offer); + let _ = self.state.write().unwrap().offer.replace(offer.unwrap()); } } } } } -impl ReceiveContext { - pub fn new(host: &H) -> Self { +impl ReceiveContext { + pub fn new(host: &Layer) -> Self { Self { initial_offer_defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), incoming_init_header_check_cipher: Aes::new( - kbkdf512(host.get_local_s_public_hash(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), + kbkdf512(host.get_local_s_public_blob_hash(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), ), } } @@ -607,14 +485,14 @@ impl ReceiveContext { #[inline] pub fn receive<'a, SendFunction: FnMut(&mut [u8])>( &self, - host: &H, - remote_address: &H::RemoteAddress, + host: &Layer, + remote_address: &Layer::RemoteAddress, mut send: SendFunction, data_buf: &'a mut [u8], - incoming_packet_buf: H::IncomingPacketBuffer, + incoming_packet_buf: Layer::IncomingPacketBuffer, mtu: usize, current_time: i64, - ) -> Result, Error> { + ) -> Result, Error> { let incoming_packet = incoming_packet_buf.as_ref(); if incoming_packet.len() < MIN_PACKET_SIZE { unlikely_branch(); @@ -629,7 +507,7 @@ impl ReceiveContext { if let Some(local_session_id) = SessionId::new_from_u64(u64::from_le(memory::load_raw(&incoming_packet[8..16])) & 0xffffffffffffu64) { - if let Some(session) = host.session_lookup(local_session_id) { + if let Some(session) = host.lookup_session(local_session_id) { if verify_header_check_code(incoming_packet, &session.header_check_cipher) { let canonical_header = CanonicalHeader::make(local_session_id, packet_type, counter); if fragment_count > 1 { @@ -730,17 +608,17 @@ impl ReceiveContext { #[inline] fn receive_complete<'a, SendFunction: FnMut(&mut [u8])>( &self, - host: &H, - remote_address: &H::RemoteAddress, + host: &Layer, + remote_address: &Layer::RemoteAddress, send: &mut SendFunction, data_buf: &'a mut [u8], canonical_header_bytes: &[u8; 12], - fragments: &[H::IncomingPacketBuffer], + fragments: &[Layer::IncomingPacketBuffer], packet_type: u8, - session: Option, + session: Option, mtu: usize, current_time: i64, - ) -> Result, Error> { + ) -> Result, Error> { debug_assert!(fragments.len() >= 1); // The first 'if' below should capture both DATA and NOP but not other types. Sanity check this. @@ -751,10 +629,13 @@ impl ReceiveContext { if let Some(session) = session { let state = session.state.read().unwrap(); for p in 0..KEY_HISTORY_SIZE { - let key_ptr = (state.key_ptr + p) % KEY_HISTORY_SIZE; - if let Some(key) = state.keys[key_ptr].as_ref() { - let mut c = key.get_receive_cipher(); + let key_idx = (state.cur_session_key_idx + p) % KEY_HISTORY_SIZE; + if let Some(session_key) = state.session_keys[key_idx].as_ref() { + let mut c = session_key.get_receive_cipher(); c.reset_init_gcm(canonical_header_bytes); + //////////////////////////////////////////////////////////////// + // packet decoding for post-noise transport + //////////////////////////////////////////////////////////////// let mut data_len = 0; @@ -766,7 +647,7 @@ impl ReceiveContext { data_len += f.len() - HEADER_SIZE; if data_len > data_buf.len() { unlikely_branch(); - key.return_receive_cipher(c); + session_key.return_receive_cipher(c); return Err(Error::DataBufferTooSmall); } c.crypt(&f[HEADER_SIZE..], &mut data_buf[current_frag_data_start..data_len]); @@ -782,30 +663,32 @@ impl ReceiveContext { data_len += last_fragment.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE); if data_len > data_buf.len() { unlikely_branch(); - key.return_receive_cipher(c); + session_key.return_receive_cipher(c); return Err(Error::DataBufferTooSmall); } + let payload_end = last_fragment.len() - AES_GCM_TAG_SIZE; c.crypt( - &last_fragment[HEADER_SIZE..(last_fragment.len() - AES_GCM_TAG_SIZE)], + &last_fragment[HEADER_SIZE..payload_end], &mut data_buf[current_frag_data_start..data_len], ); - let aead_authentication_ok = c.finish_decrypt(&last_fragment[(last_fragment.len() - AES_GCM_TAG_SIZE)..]); - key.return_receive_cipher(c); + let gcm_tag = &last_fragment[payload_end..]; + let aead_authentication_ok = c.finish_decrypt(gcm_tag); + session_key.return_receive_cipher(c); if aead_authentication_ok { // Select this key as the new default if it's newer than the current key. if p > 0 - && state.keys[state.key_ptr] - .as_ref() - .map_or(true, |old| old.establish_counter < key.establish_counter) + && state.session_keys[state.cur_session_key_idx] + .as_ref() + .map_or(true, |old| old.establish_counter < session_key.establish_counter) { drop(state); let mut state = session.state.write().unwrap(); - state.key_ptr = key_ptr; + state.cur_session_key_idx = key_idx; for i in 0..KEY_HISTORY_SIZE { - if i != key_ptr { - if let Some(old_key) = state.keys[key_ptr].as_ref() { + if i != key_idx { + if let Some(old_key) = state.session_keys[key_idx].as_ref() { // Release pooled cipher memory from old keys. old_key.receive_cipher_pool.lock().unwrap().clear(); old_key.send_cipher_pool.lock().unwrap().clear(); @@ -859,61 +742,65 @@ impl ReceiveContext { } match packet_type { - PACKET_TYPE_KEY_OFFER => { + PACKET_TYPE_INITIAL_KEY_OFFER => { // alice (remote) -> bob (local) - + //////////////////////////////////////////////////////////////// + // packet decoding for noise initial key offer + // -> e, es, s, ss + //////////////////////////////////////////////////////////////// if kex_packet_len < (HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + HMAC_SIZE + HMAC_SIZE) { return Err(Error::InvalidPacket); } + let plaintext_end = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; let payload_end = kex_packet_len - (AES_GCM_TAG_SIZE + HMAC_SIZE + HMAC_SIZE); let aes_gcm_tag_end = kex_packet_len - (HMAC_SIZE + HMAC_SIZE); let hmac1_end = kex_packet_len - HMAC_SIZE; // Check the second HMAC first, which proves that the sender knows the recipient's full static identity. + let hmac2 = &kex_packet[hmac1_end..kex_packet_len]; if !hmac_sha384_2( - host.get_local_s_public_hash(), + host.get_local_s_public_blob_hash(), canonical_header_bytes, &kex_packet[HEADER_SIZE..hmac1_end], ) - .eq(&kex_packet[hmac1_end..kex_packet_len]) + .eq(hmac2) { return Err(Error::FailedAuthentication); } // Check rate limits. if let Some(session) = session.as_ref() { - if (current_time - session.state.read().unwrap().last_remote_offer) < H::REKEY_RATE_LIMIT_MS { + if (current_time - session.state.read().unwrap().last_remote_offer) < Layer::REKEY_RATE_LIMIT_MS { return Err(Error::RateLimited); } } else { - if !host.check_new_session_attempt(self, remote_address) { + if !host.check_new_session(self, remote_address) { return Err(Error::RateLimited); } } // Key agreement: alice (remote) ephemeral NIST P-384 <> local static NIST P-384 - let (alice_e0_public, e0s) = - P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)]) - .and_then(|pk| host.get_local_s_keypair_p384().agree(&pk).map(move |s| (pk, s))) - .ok_or(Error::FailedAuthentication)?; + let alice_e_public = P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..plaintext_end]).ok_or(Error::FailedAuthentication)?; + let noise_es = host.get_local_s_keypair().agree(&alice_e_public).ok_or(Error::FailedAuthentication)?; - // Initial key derivation from starting point, mixing in alice's ephemeral public and the e0s. - let mut key = Secret(hmac_sha512(&hmac_sha512(&INITIAL_KEY, alice_e0_public.as_bytes()), e0s.as_bytes())); + // Initial key derivation from starting point, mixing in alice's ephemeral public and the es. + let es_key = Secret(hmac_sha512(&hmac_sha512(&INITIAL_KEY, alice_e_public.as_bytes()), noise_es.as_bytes())); // Decrypt the encrypted part of the packet payload and authenticate the above key exchange via AES-GCM auth. let mut c = AesGcm::new( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::(), + kbkdf512(es_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::(), false, ); c.reset_init_gcm(canonical_header_bytes); - c.crypt_in_place(&mut kex_packet[(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)..payload_end]); - if !c.finish_decrypt(&kex_packet[payload_end..aes_gcm_tag_end]) { + c.crypt_in_place(&mut kex_packet[plaintext_end..payload_end]); + let gcm_tag = &kex_packet[payload_end..aes_gcm_tag_end]; + if !c.finish_decrypt(gcm_tag) { return Err(Error::FailedAuthentication); } // Parse payload and get alice's session ID, alice's public blob, metadata, and (if present) Alice's Kyber1024 public. - let (offer_id, alice_session_id, alice_s_public, alice_metadata, alice_e1_public, alice_ratchet_key_fingerprint) = - parse_key_offer_after_header(&kex_packet[(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)..kex_packet_len], packet_type)?; + let (offer_id, alice_session_id, alice_s_public_blob, alice_metadata, alice_hk_public_raw, alice_ratchet_key_fingerprint) = + parse_dec_key_offer_after_header(&kex_packet[plaintext_end..kex_packet_len], packet_type)?; // We either have a session, in which case they should have supplied a ratchet key fingerprint, or // we don't and they should not have supplied one. @@ -922,25 +809,27 @@ impl ReceiveContext { } // Extract alice's static NIST P-384 public key from her public blob. - let alice_s_public_p384 = H::extract_p384_static(alice_s_public).ok_or(Error::InvalidPacket)?; + let alice_s_public = Layer::extract_s_public_from_raw(alice_s_public_blob).ok_or(Error::InvalidPacket)?; // Key agreement: both sides' static P-384 keys. - let ss = host - .get_local_s_keypair_p384() - .agree(&alice_s_public_p384) + let noise_ss = host + .get_local_s_keypair() + .agree(&alice_s_public) .ok_or(Error::FailedAuthentication)?; // Mix result of 'ss' agreement into master key. - key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes())); + let ss_key = Secret(hmac_sha512(es_key.as_bytes(), noise_ss.as_bytes())); + drop(es_key); // Authenticate entire packet with HMAC-SHA384, verifying alice's identity via 'ss' secret that was // just mixed into the key. + let hmac1 = &kex_packet[aes_gcm_tag_end..hmac1_end]; if !hmac_sha384_2( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), + kbkdf512(ss_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), canonical_header_bytes, &kex_packet_saved_ciphertext[HEADER_SIZE..aes_gcm_tag_end], ) - .eq(&kex_packet[aes_gcm_tag_end..hmac1_end]) + .eq(hmac1) { return Err(Error::FailedAuthentication); } @@ -951,16 +840,16 @@ impl ReceiveContext { // then create new sessions. let (new_session, ratchet_key, ratchet_count) = if let Some(session) = session.as_ref() { // Existing session identity must match the one in this offer. - if !session.remote_s_public_hash.eq(&SHA384::hash(&alice_s_public)) { + if !session.remote_s_public_blob_hash.eq(&SHA384::hash(&alice_s_public_blob)) { return Err(Error::FailedAuthentication); } // Match ratchet key fingerprint and fail if no match, which likely indicates an old offer packet. - let alice_ratchet_key_fingerprint = alice_ratchet_key_fingerprint.as_ref().unwrap(); + let alice_ratchet_key_fingerprint = alice_ratchet_key_fingerprint.unwrap(); let mut ratchet_key = None; let mut ratchet_count = 0; let state = session.state.read().unwrap(); - for k in state.keys.iter() { + for k in state.session_keys.iter() { if let Some(k) = k.as_ref() { if secret_fingerprint(k.ratchet_key.as_bytes())[..16].eq(alice_ratchet_key_fingerprint) { ratchet_key = Some(k.ratchet_key.clone()); @@ -976,28 +865,28 @@ impl ReceiveContext { (None, ratchet_key, ratchet_count) } else { if let Some((new_session_id, psk, associated_object)) = - host.accept_new_session(self, remote_address, alice_s_public, alice_metadata) + host.accept_new_session(self, remote_address, alice_s_public_blob, alice_metadata) { let header_check_cipher = Aes::new( - kbkdf512(ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), + kbkdf512(noise_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), ); ( - Some(Session:: { + Some(Session:: { id: new_session_id, - associated_object, + user_data: associated_object, send_counter: Counter::new(), psk, - ss, + noise_ss, header_check_cipher, state: RwLock::new(SessionMutableState { remote_session_id: Some(alice_session_id), - keys: [None, None, None], - key_ptr: 0, + session_keys: [None, None, None], + cur_session_key_idx: 0, offer: None, last_remote_offer: current_time, }), - remote_s_public_hash: SHA384::hash(&alice_s_public), - remote_s_public_p384: alice_s_public_p384.as_bytes().clone(), + remote_s_public_blob_hash: SHA384::hash(&alice_s_public_blob), + remote_s_public_raw: alice_s_public.as_bytes().clone(), defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), }), None, @@ -1013,34 +902,35 @@ impl ReceiveContext { let session = existing_session.as_ref().map_or_else(|| new_session.as_ref().unwrap(), |s| &*s); // Generate our ephemeral NIST P-384 key pair. - let bob_e0_keypair = P384KeyPair::generate(); + let bob_e_keypair = P384KeyPair::generate(); // Key agreement: both sides' ephemeral P-384 public keys. - let e0e0 = bob_e0_keypair.agree(&alice_e0_public).ok_or(Error::FailedAuthentication)?; + let noise_ee = bob_e_keypair.agree(&alice_e_public).ok_or(Error::FailedAuthentication)?; // Key agreement: bob (local) static NIST P-384, alice (remote) ephemeral P-384. - let se0 = bob_e0_keypair.agree(&alice_s_public_p384).ok_or(Error::FailedAuthentication)?; + let noise_se = bob_e_keypair.agree(&alice_s_public).ok_or(Error::FailedAuthentication)?; - // Mix in the psk, the key to this point, our ephemeral public, e0e0, and se0, completing Noise_IK. + // Mix in the psk, the key to this point, our ephemeral public, ee, and se, completing Noise_IK. // // FIPS note: the order of HMAC parameters are flipped here from the usual Noise HMAC(key, X). That's because // NIST/FIPS allows HKDF with HMAC(salt, key) and salt is allowed to be anything. This way if the PSK is not // FIPS compliant the compliance of the entire key derivation is not invalidated. Both inputs are secrets of // fixed size so this shouldn't matter cryptographically. - key = Secret(hmac_sha512( + let noise_ik_key = Secret(hmac_sha512( session.psk.as_bytes(), &hmac_sha512( - &hmac_sha512(&hmac_sha512(key.as_bytes(), bob_e0_keypair.public_key_bytes()), e0e0.as_bytes()), - se0.as_bytes(), + &hmac_sha512(&hmac_sha512(ss_key.as_bytes(), bob_e_keypair.public_key_bytes()), noise_ee.as_bytes()), + noise_se.as_bytes(), ), )); + drop(ss_key); // At this point we've completed Noise_IK key derivation with NIST P-384 ECDH, but now for hybrid and ratcheting... // Generate a Kyber encapsulated ciphertext if Kyber is enabled and the other side sent us a public key. - let (bob_e1_public, e1e1) = if JEDI && alice_e1_public.len() > 0 { - if let Ok((bob_e1_public, e1e1)) = pqc_kyber::encapsulate(alice_e1_public, &mut random::SecureRandom::default()) { - (Some(bob_e1_public), Some(Secret(e1e1))) + let (bob_hk_public, hybrid_kk) = if JEDI && alice_hk_public_raw.len() > 0 { + if let Ok((bob_hk_public, hybrid_kk)) = pqc_kyber::encapsulate(alice_hk_public_raw, &mut random::SecureRandom::default()) { + (Some(bob_hk_public), Some(Secret(hybrid_kk))) } else { return Err(Error::FailedAuthentication); } @@ -1048,37 +938,39 @@ impl ReceiveContext { (None, None) }; - // Create reply packet. + //////////////////////////////////////////////////////////////// + // packet encoding for noise key counter offer + // <- e, ee, se + //////////////////////////////////////////////////////////////// let mut reply_buf = [0_u8; KEX_BUF_LEN]; let reply_counter = session.send_counter.next(); - let mut reply_len = { - let mut rp = &mut reply_buf[HEADER_SIZE..]; + let mut idx = HEADER_SIZE; - rp.write_all(&[SESSION_PROTOCOL_VERSION])?; - rp.write_all(bob_e0_keypair.public_key_bytes())?; + idx = safe_write_all(&mut reply_buf, idx, &[SESSION_PROTOCOL_VERSION])?; + idx = safe_write_all(&mut reply_buf, idx, bob_e_keypair.public_key_bytes())?; + let plaintext_end = idx; - rp.write_all(&offer_id)?; - rp.write_all(&session.id.0.to_le_bytes()[..SESSION_ID_SIZE])?; - varint::write(&mut rp, 0)?; // they don't need our static public; they have it - varint::write(&mut rp, 0)?; // no meta-data in counter-offers (could be used in the future) - if let Some(bob_e1_public) = bob_e1_public.as_ref() { - rp.write_all(&[E1_TYPE_KYBER1024])?; - rp.write_all(bob_e1_public)?; - } else { - rp.write_all(&[E1_TYPE_NONE])?; - } - if ratchet_key.is_some() { - rp.write_all(&[0x01])?; - rp.write_all(alice_ratchet_key_fingerprint.as_ref().unwrap())?; - } else { - rp.write_all(&[0x00])?; - } + idx = safe_write_all(&mut reply_buf, idx, offer_id)?; + idx = safe_write_all(&mut reply_buf, idx, &session.id.0.to_le_bytes()[..SESSION_ID_SIZE])?; + idx = varint_safe_write(&mut reply_buf, idx, 0)?; // they don't need our static public; they have it + idx = varint_safe_write(&mut reply_buf, idx, 0)?; // no meta-data in counter-offers (could be used in the future) + if let Some(bob_hk_public) = bob_hk_public.as_ref() { + idx = safe_write_all(&mut reply_buf, idx, &[E1_TYPE_KYBER1024])?; + idx = safe_write_all(&mut reply_buf, idx, bob_hk_public)?; + } else { + idx = safe_write_all(&mut reply_buf, idx, &[E1_TYPE_NONE])?; + } + if ratchet_key.is_some() { + idx = safe_write_all(&mut reply_buf, idx, &[0x01])?; + idx = safe_write_all(&mut reply_buf, idx, alice_ratchet_key_fingerprint.unwrap())?; + } else { + idx = safe_write_all(&mut reply_buf, idx, &[0x00])?; + } + let payload_end = idx; - KEX_BUF_LEN - rp.len() - }; create_packet_header( &mut reply_buf, - reply_len, + payload_end, mtu, PACKET_TYPE_KEY_COUNTER_OFFER, alice_session_id.into(), @@ -1090,21 +982,22 @@ impl ReceiveContext { // Encrypt reply packet using final Noise_IK key BEFORE mixing hybrid or ratcheting, since the other side // must decrypt before doing these things. let mut c = AesGcm::new( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::(), + kbkdf512(noise_ik_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::(), true, ); c.reset_init_gcm(reply_canonical_header.as_bytes()); - c.crypt_in_place(&mut reply_buf[(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)..reply_len]); - let c = c.finish_encrypt(); - reply_buf[reply_len..(reply_len + AES_GCM_TAG_SIZE)].copy_from_slice(&c); - reply_len += AES_GCM_TAG_SIZE; + c.crypt_in_place(&mut reply_buf[plaintext_end..payload_end]); + let gcm_tag = c.finish_encrypt(); + idx = safe_write_all(&mut reply_buf, idx, &gcm_tag)?; + let aes_gcm_tag_end = idx; // Mix ratchet key from previous session key (if any) and Kyber1024 hybrid shared key (if any). + let mut session_key = noise_ik_key; if let Some(ratchet_key) = ratchet_key { - key = Secret(hmac_sha512(ratchet_key.as_bytes(), key.as_bytes())); + session_key = Secret(hmac_sha512(ratchet_key.as_bytes(), session_key.as_bytes())); } - if let Some(e1e1) = e1e1.as_ref() { - key = Secret(hmac_sha512(e1e1.as_bytes(), key.as_bytes())); + if let Some(hybrid_kk) = hybrid_kk.as_ref() { + session_key = Secret(hmac_sha512(hybrid_kk.as_bytes(), session_key.as_bytes())); } // Authenticate packet using HMAC-SHA384 with final key. Note that while the final key now has the Kyber secret @@ -1112,24 +1005,24 @@ impl ReceiveContext { // associated with the remote identity. An attacker who can break NIST P-384 (and has the psk) could MITM the // Kyber exchange, but you'd need a not-yet-existing quantum computer for that. let hmac = hmac_sha384_2( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), + kbkdf512(session_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), reply_canonical_header.as_bytes(), - &reply_buf[HEADER_SIZE..reply_len], + &reply_buf[HEADER_SIZE..aes_gcm_tag_end], ); - reply_buf[reply_len..(reply_len + HMAC_SIZE)].copy_from_slice(&hmac); - reply_len += HMAC_SIZE; + idx = safe_write_all(&mut reply_buf, idx, &hmac)?; + let packet_end = idx; - let key = SessionKey::new(key, Role::Bob, current_time, reply_counter, ratchet_count + 1, e1e1.is_some()); + let session_key = SessionKey::new(session_key, Role::Bob, current_time, reply_counter, ratchet_count + 1, hybrid_kk.is_some()); let mut state = session.state.write().unwrap(); let _ = state.remote_session_id.replace(alice_session_id); - let next_key_ptr = (state.key_ptr + 1) % KEY_HISTORY_SIZE; - let _ = state.keys[next_key_ptr].replace(key); + let next_key_ptr = (state.cur_session_key_idx + 1) % KEY_HISTORY_SIZE; + let _ = state.session_keys[next_key_ptr].replace(session_key); drop(state); // Bob now has final key state for this exchange. Yay! Now reply to Alice so she can construct it. - send_with_fragmentation(send, &mut reply_buf[..reply_len], mtu, &session.header_check_cipher); + send_with_fragmentation(send, &mut reply_buf[..packet_end], mtu, &session.header_check_cipher); if new_session.is_some() { return Ok(ReceiveResult::OkNewSession(new_session.unwrap())); @@ -1140,57 +1033,62 @@ impl ReceiveContext { PACKET_TYPE_KEY_COUNTER_OFFER => { // bob (remote) -> alice (local) + //////////////////////////////////////////////////////////////// + // packet decoding for noise key counter offer + // <- e, ee, se + //////////////////////////////////////////////////////////////// if kex_packet_len < (HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + HMAC_SIZE) { return Err(Error::InvalidPacket); } + let plaintext_end = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; let payload_end = kex_packet_len - (AES_GCM_TAG_SIZE + HMAC_SIZE); let aes_gcm_tag_end = kex_packet_len - HMAC_SIZE; if let Some(session) = session { let state = session.state.read().unwrap(); if let Some(offer) = state.offer.as_ref() { - let (bob_e0_public, e0e0) = - P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)]) - .and_then(|pk| offer.alice_e0_keypair.agree(&pk).map(move |s| (pk, s))) - .ok_or(Error::FailedAuthentication)?; - let se0 = host - .get_local_s_keypair_p384() - .agree(&bob_e0_public) + + let bob_e_public = P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..plaintext_end]).ok_or(Error::FailedAuthentication)?; + let noise_ee = offer.alice_e_keypair.agree(&bob_e_public).ok_or(Error::FailedAuthentication)?; + let noise_se = host + .get_local_s_keypair() + .agree(&bob_e_public) .ok_or(Error::FailedAuthentication)?; - let mut key = Secret(hmac_sha512( + let noise_ik_key = Secret(hmac_sha512( session.psk.as_bytes(), &hmac_sha512( - &hmac_sha512(&hmac_sha512(offer.key.as_bytes(), bob_e0_public.as_bytes()), e0e0.as_bytes()), - se0.as_bytes(), + &hmac_sha512(&hmac_sha512(offer.ss_key.as_bytes(), bob_e_public.as_bytes()), noise_ee.as_bytes()), + noise_se.as_bytes(), ), )); let mut c = AesGcm::new( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::(), + kbkdf512(noise_ik_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::(), false, ); c.reset_init_gcm(canonical_header_bytes); - c.crypt_in_place(&mut kex_packet[(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)..payload_end]); - if !c.finish_decrypt(&kex_packet[payload_end..aes_gcm_tag_end]) { + c.crypt_in_place(&mut kex_packet[plaintext_end..payload_end]); + let gcm_tag = &kex_packet[payload_end..aes_gcm_tag_end]; + if !c.finish_decrypt(gcm_tag) { return Err(Error::FailedAuthentication); } // Alice has now completed Noise_IK with NIST P-384 and verified with GCM auth, but now for hybrid... - let (offer_id, bob_session_id, _, _, bob_e1_public, bob_ratchet_key_id) = parse_key_offer_after_header( - &kex_packet[(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)..kex_packet_len], + let (offer_id, bob_session_id, _, _, bob_hk_public_raw, bob_ratchet_key_id) = parse_dec_key_offer_after_header( + &kex_packet[plaintext_end..kex_packet_len], packet_type, )?; - if !offer.id.eq(&offer_id) { + if !offer.id.eq(offer_id) { return Ok(ReceiveResult::Ignored); } - let e1e1 = if JEDI && bob_e1_public.len() > 0 && offer.alice_e1_keypair.is_some() { - if let Ok(e1e1) = pqc_kyber::decapsulate(bob_e1_public, &offer.alice_e1_keypair.as_ref().unwrap().secret) { - Some(Secret(e1e1)) + let hybrid_kk = if JEDI && bob_hk_public_raw.len() > 0 && offer.alice_hk_keypair.is_some() { + if let Ok(hybrid_kk) = pqc_kyber::decapsulate(bob_hk_public_raw, &offer.alice_hk_keypair.as_ref().unwrap().secret) { + Some(Secret(hybrid_kk)) } else { return Err(Error::FailedAuthentication); } @@ -1198,21 +1096,24 @@ impl ReceiveContext { None }; + // Mix ratchet key from previous session key (if any) and Kyber1024 hybrid shared key (if any). let mut ratchet_count = 0; + let mut session_key = noise_ik_key; if bob_ratchet_key_id.is_some() && offer.ratchet_key.is_some() { - key = Secret(hmac_sha512(offer.ratchet_key.as_ref().unwrap().as_bytes(), key.as_bytes())); + session_key = Secret(hmac_sha512(offer.ratchet_key.as_ref().unwrap().as_bytes(), session_key.as_bytes())); ratchet_count = offer.ratchet_count; } - if let Some(e1e1) = e1e1.as_ref() { - key = Secret(hmac_sha512(e1e1.as_bytes(), key.as_bytes())); + if let Some(hybrid_kk) = hybrid_kk.as_ref() { + session_key = Secret(hmac_sha512(hybrid_kk.as_bytes(), session_key.as_bytes())); } + let hmac = &kex_packet[aes_gcm_tag_end..kex_packet_len]; if !hmac_sha384_2( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), + kbkdf512(session_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), canonical_header_bytes, &kex_packet_saved_ciphertext[HEADER_SIZE..aes_gcm_tag_end], ) - .eq(&kex_packet[aes_gcm_tag_end..kex_packet_len]) + .eq(hmac) { return Err(Error::FailedAuthentication); } @@ -1220,8 +1121,11 @@ impl ReceiveContext { // Alice has now completed and validated the full hybrid exchange. let counter = session.send_counter.next(); - let key = SessionKey::new(key, Role::Alice, current_time, counter, ratchet_count + 1, e1e1.is_some()); + let session_key = SessionKey::new(session_key, Role::Alice, current_time, counter, ratchet_count + 1, hybrid_kk.is_some()); + //////////////////////////////////////////////////////////////// + // packet encoding for post-noise session start ack + //////////////////////////////////////////////////////////////// let mut reply_buf = [0_u8; HEADER_SIZE + AES_GCM_TAG_SIZE]; create_packet_header( &mut reply_buf, @@ -1232,10 +1136,11 @@ impl ReceiveContext { counter, )?; - let mut c = key.get_send_cipher(counter)?; + let mut c = session_key.get_send_cipher(counter)?; c.reset_init_gcm(CanonicalHeader::make(bob_session_id.into(), PACKET_TYPE_NOP, counter.to_u32()).as_bytes()); - reply_buf[HEADER_SIZE..].copy_from_slice(&c.finish_encrypt()); - key.return_send_cipher(c); + let gcm_tag = c.finish_encrypt(); + safe_write_all(&mut reply_buf, HEADER_SIZE, &gcm_tag)?; + session_key.return_send_cipher(c); set_header_check_code(&mut reply_buf, &session.header_check_cipher); send(&mut reply_buf); @@ -1243,8 +1148,8 @@ impl ReceiveContext { drop(state); let mut state = session.state.write().unwrap(); let _ = state.remote_session_id.replace(bob_session_id); - let next_key_ptr = (state.key_ptr + 1) % KEY_HISTORY_SIZE; - let _ = state.keys[next_key_ptr].replace(key); + let next_key_idx = (state.cur_session_key_idx + 1) % KEY_HISTORY_SIZE; + let _ = state.session_keys[next_key_idx].replace(session_key); let _ = state.offer.take(); return Ok(ReceiveResult::Ok); @@ -1262,84 +1167,6 @@ impl ReceiveContext { } } -/// Outgoing packet counter with strictly ordered atomic semantics. -#[repr(transparent)] -struct Counter(AtomicU64); - -impl Counter { - #[inline(always)] - fn new() -> Self { - // Using a random value has no security implication. Zero would be fine. This just - // helps randomize packet contents a bit. - Self(AtomicU64::new(random::next_u32_secure() as u64)) - } - - /// Get the value most recently used to send a packet. - #[inline(always)] - fn previous(&self) -> CounterValue { - CounterValue(self.0.load(Ordering::SeqCst)) - } - - /// Get a counter value for the next packet being sent. - #[inline(always)] - fn next(&self) -> CounterValue { - CounterValue(self.0.fetch_add(1, Ordering::SeqCst)) - } -} - -/// A value of the outgoing packet counter. -/// -/// The used portion of the packet counter is the least significant 32 bits, but the internal -/// counter state is kept as a 64-bit integer. This makes it easier to correctly handle -/// key expiration after usage limits are reached without complicated logic to handle 32-bit -/// wrapping. Usage limits are below 2^32 so the actual 32-bit counter will not wrap for a -/// given shared secret key. -#[repr(transparent)] -#[derive(Copy, Clone)] -struct CounterValue(u64); - -impl CounterValue { - #[inline(always)] - pub fn to_u32(&self) -> u32 { - self.0 as u32 - } -} - -/// "Canonical header" for generating 96-bit AES-GCM nonce and for inclusion in HMACs. -/// -/// This is basically the actual header but with fragment count and fragment total set to zero. -/// Fragmentation is not considered when authenticating the entire packet. A separate header -/// check code is used to make fragmentation itself more robust, but that's outside the scope -/// of AEAD authentication. -#[derive(Clone, Copy)] -#[repr(C, packed)] -struct CanonicalHeader(u64, u32); - -impl CanonicalHeader { - #[inline(always)] - pub fn make(session_id: SessionId, packet_type: u8, counter: u32) -> Self { - CanonicalHeader( - (u64::from(session_id) | (packet_type as u64).wrapping_shl(48)).to_le(), - counter.to_le(), - ) - } - - #[inline(always)] - pub fn as_bytes(&self) -> &[u8; 12] { - memory::as_byte_array(self) - } -} - -/// Alice's KEY_OFFER, remembered so Noise agreement process can resume on KEY_COUNTER_OFFER. -struct EphemeralOffer { - id: [u8; 16], // Arbitrary random offer ID - creation_time: i64, // Local time when offer was created - ratchet_count: u64, // Ratchet count starting at zero for initial offer - ratchet_key: Option>, // Ratchet key from previous offer - key: Secret<64>, // Shared secret in-progress, at state after offer sent - alice_e0_keypair: P384KeyPair, // NIST P-384 key pair (Noise ephemeral key for Alice) - alice_e1_keypair: Option, // Kyber1024 key pair (agreement result mixed post-Noise) -} /// Create and send an ephemeral offer, returning the EphemeralOffer part that must be saved. fn send_ephemeral_offer( @@ -1347,24 +1174,25 @@ fn send_ephemeral_offer( counter: CounterValue, alice_session_id: SessionId, bob_session_id: Option, - alice_s_public: &[u8], + alice_s_public_blob: &[u8], alice_metadata: &[u8], - bob_s_public_p384: &P384PublicKey, - bob_s_public_hash: &[u8], - ss: &Secret<48>, + bob_s_public: &P384PublicKey, + bob_s_public_blob_hash: &[u8], + noise_ss: &Secret<48>, current_key: Option<&SessionKey>, header_check_cipher: Option<&Aes>, // None to use one based on the recipient's public key for initial contact mtu: usize, current_time: i64, -) -> Result, Error> { + ret_ephemeral_offer: &mut Option//We want to prevent copying the EphemeralOffer up the stack because it's very big. ret_ephemeral_offer will be overwritten with the returned EphemeralOffer when the call completes. +) -> Result<(), Error> { // Generate a NIST P-384 pair. - let alice_e0_keypair = P384KeyPair::generate(); + let alice_e_keypair = P384KeyPair::generate(); // Perform key agreement with the other side's static P-384 public key. - let e0s = alice_e0_keypair.agree(bob_s_public_p384).ok_or(Error::InvalidPacket)?; + let noise_es = alice_e_keypair.agree(bob_s_public).ok_or(Error::InvalidPacket)?; // Generate a Kyber1024 pair if enabled. - let alice_e1_keypair = if JEDI { + let alice_hk_keypair = if JEDI { Some(pqc_kyber::keypair(&mut random::SecureRandom::get())) } else { None @@ -1380,98 +1208,108 @@ fn send_ephemeral_offer( // Random ephemeral offer ID let id: [u8; 16] = random::get_bytes_secure(); + + //////////////////////////////////////////////////////////////// + // packet encoding for noise initial key offer and for noise rekeying + // -> e, es, s, ss + //////////////////////////////////////////////////////////////// + // Create ephemeral offer packet (not fragmented yet). const PACKET_BUF_SIZE: usize = MIN_TRANSPORT_MTU * KEY_EXCHANGE_MAX_FRAGMENTS; let mut packet_buf = [0_u8; PACKET_BUF_SIZE]; - let mut packet_len = { - let mut p = &mut packet_buf[HEADER_SIZE..]; + let mut idx = HEADER_SIZE; - p.write_all(&[SESSION_PROTOCOL_VERSION])?; - p.write_all(alice_e0_keypair.public_key_bytes())?; + idx = safe_write_all(&mut packet_buf, idx, &[SESSION_PROTOCOL_VERSION])?; + //TODO: check this, the below line is supposed to be the blob, not just the key, right? + idx = safe_write_all(&mut packet_buf, idx, alice_e_keypair.public_key_bytes())?; + let plaintext_end = idx; - p.write_all(&id)?; - p.write_all(&alice_session_id.0.to_le_bytes()[..SESSION_ID_SIZE])?; - varint::write(&mut p, alice_s_public.len() as u64)?; - p.write_all(alice_s_public)?; - varint::write(&mut p, alice_metadata.len() as u64)?; - p.write_all(alice_metadata)?; - if let Some(e1kp) = alice_e1_keypair { - p.write_all(&[E1_TYPE_KYBER1024])?; - p.write_all(&e1kp.public)?; - } else { - p.write_all(&[E1_TYPE_NONE])?; - } - if let Some(ratchet_key) = ratchet_key.as_ref() { - p.write_all(&[0x01])?; - p.write_all(&secret_fingerprint(ratchet_key.as_bytes())[..16])?; - } else { - p.write_all(&[0x00])?; - } + idx = safe_write_all(&mut packet_buf, idx, &id)?; + idx = safe_write_all(&mut packet_buf, idx, &alice_session_id.0.to_le_bytes()[..SESSION_ID_SIZE])?; + idx = varint_safe_write(&mut packet_buf, idx, alice_s_public_blob.len() as u64)?; + idx = safe_write_all(&mut packet_buf, idx, alice_s_public_blob)?; + idx = varint_safe_write(&mut packet_buf, idx, alice_metadata.len() as u64)?; + idx = safe_write_all(&mut packet_buf, idx, alice_metadata)?; + if let Some(hkp) = alice_hk_keypair { + idx = safe_write_all(&mut packet_buf, idx, &[E1_TYPE_KYBER1024])?; + idx = safe_write_all(&mut packet_buf, idx, &hkp.public)?; + } else { + idx = safe_write_all(&mut packet_buf, idx, &[E1_TYPE_NONE])?; + } + if let Some(ratchet_key) = ratchet_key.as_ref() { + idx = safe_write_all(&mut packet_buf, idx, &[0x01])?; + idx = safe_write_all(&mut packet_buf, idx, &secret_fingerprint(ratchet_key.as_bytes())[..16])?; + } else { + idx = safe_write_all(&mut packet_buf, idx, &[0x00])?; + } + let payload_end = idx; - PACKET_BUF_SIZE - p.len() - }; // Create ephemeral agreement secret. - let key = Secret(hmac_sha512( - &hmac_sha512(&INITIAL_KEY, alice_e0_keypair.public_key_bytes()), - e0s.as_bytes(), + let es_key = Secret(hmac_sha512( + &hmac_sha512(&INITIAL_KEY, alice_e_keypair.public_key_bytes()), + noise_es.as_bytes(), )); let bob_session_id = bob_session_id.unwrap_or(SessionId::NIL); - create_packet_header(&mut packet_buf, packet_len, mtu, PACKET_TYPE_KEY_OFFER, bob_session_id, counter)?; + create_packet_header(&mut packet_buf, payload_end, mtu, PACKET_TYPE_INITIAL_KEY_OFFER, bob_session_id, counter)?; - let canonical_header = CanonicalHeader::make(bob_session_id, PACKET_TYPE_KEY_OFFER, counter.to_u32()); + let canonical_header = CanonicalHeader::make(bob_session_id, PACKET_TYPE_INITIAL_KEY_OFFER, counter.to_u32()); // Encrypt packet and attach AES-GCM tag. let gcm_tag = { let mut c = AesGcm::new( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::(), + kbkdf512(es_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::(), true, ); c.reset_init_gcm(canonical_header.as_bytes()); - c.crypt_in_place(&mut packet_buf[(HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE)..packet_len]); + c.crypt_in_place(&mut packet_buf[plaintext_end..payload_end]); c.finish_encrypt() }; - packet_buf[packet_len..(packet_len + AES_GCM_TAG_SIZE)].copy_from_slice(&gcm_tag); - packet_len += AES_GCM_TAG_SIZE; + + idx = safe_write_all(&mut packet_buf, idx, &gcm_tag)?; + let aes_gcm_tag_end = idx; // Mix in static secret. - let key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes())); + let ss_key = Secret(hmac_sha512(es_key.as_bytes(), noise_ss.as_bytes())); + drop(es_key); // HMAC packet using static + ephemeral key. - let hmac = hmac_sha384_2( - kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), + let hmac1 = hmac_sha384_2( + kbkdf512(ss_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), canonical_header.as_bytes(), - &packet_buf[HEADER_SIZE..packet_len], + &packet_buf[HEADER_SIZE..aes_gcm_tag_end], ); - packet_buf[packet_len..(packet_len + HMAC_SIZE)].copy_from_slice(&hmac); - packet_len += HMAC_SIZE; + idx = safe_write_all(&mut packet_buf, idx, &hmac1)?; + let hmac1_end = idx; // Add secondary HMAC to verify that the caller knows the recipient's full static public identity. - let hmac = hmac_sha384_2(bob_s_public_hash, canonical_header.as_bytes(), &packet_buf[HEADER_SIZE..packet_len]); - packet_buf[packet_len..(packet_len + HMAC_SIZE)].copy_from_slice(&hmac); - packet_len += HMAC_SIZE; + let hmac2 = hmac_sha384_2(bob_s_public_blob_hash, canonical_header.as_bytes(), &packet_buf[HEADER_SIZE..hmac1_end]); + idx = safe_write_all(&mut packet_buf, idx, &hmac2)?; + let packet_end = idx; if let Some(header_check_cipher) = header_check_cipher { - send_with_fragmentation(send, &mut packet_buf[..packet_len], mtu, header_check_cipher); + send_with_fragmentation(send, &mut packet_buf[..packet_end], mtu, header_check_cipher); } else { send_with_fragmentation( send, - &mut packet_buf[..packet_len], + &mut packet_buf[..packet_end], mtu, - &Aes::new(kbkdf512(&bob_s_public_hash, KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::()), + &Aes::new(kbkdf512(&bob_s_public_blob_hash, KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::()), ); } - Ok(Box::new(EphemeralOffer { + *ret_ephemeral_offer = Some(EphemeralOffer { id, creation_time: current_time, ratchet_count, ratchet_key, - key, - alice_e0_keypair, - alice_e1_keypair, - })) + ss_key, + alice_e_keypair, + alice_hk_keypair, + }); + + Ok(()) } /// Populate all but the header check code in the first 16 bytes of a packet or fragment. @@ -1504,7 +1342,7 @@ fn create_packet_header( memory::store_raw((counter.to_u32() as u64).to_le(), header); memory::store_raw( (u64::from(recipient_session_id) | (packet_type as u64).wrapping_shl(48) | ((fragment_count - 1) as u64).wrapping_shl(52)) - .to_le(), + .to_le(), &mut header[8..], ); Ok(()) @@ -1561,49 +1399,29 @@ fn verify_header_check_code(packet: &[u8], header_check_cipher: &Aes) -> bool { } /// Parse KEY_OFFER and KEY_COUNTER_OFFER starting after the unencrypted public key part. -fn parse_key_offer_after_header( +fn parse_dec_key_offer_after_header( incoming_packet: &[u8], packet_type: u8, -) -> Result<([u8; 16], SessionId, &[u8], &[u8], &[u8], Option<[u8; 16]>), Error> { +) -> Result<(&[u8], SessionId, &[u8], &[u8], &[u8], Option<&[u8]>), Error> { let mut p = &incoming_packet[..]; - let mut offer_id = [0_u8; 16]; - p.read_exact(&mut offer_id)?; - let alice_session_id = SessionId::new_from_reader(&mut p)?; - if alice_session_id.is_none() { - return Err(Error::InvalidPacket); - } - let alice_session_id = alice_session_id.unwrap(); - let alice_s_public_len = varint::read(&mut p)?.0; - if (p.len() as u64) < alice_s_public_len { - return Err(Error::InvalidPacket); - } - let alice_s_public = &p[..(alice_s_public_len as usize)]; - p = &p[(alice_s_public_len as usize)..]; - let alice_metadata_len = varint::read(&mut p)?.0; - if (p.len() as u64) < alice_metadata_len { - return Err(Error::InvalidPacket); - } - let alice_metadata = &p[..(alice_metadata_len as usize)]; - p = &p[(alice_metadata_len as usize)..]; - if p.is_empty() { - return Err(Error::InvalidPacket); - } - let alice_e1_public = match p[0] { + let offer_id = safe_read_exact(&mut p, 16)?; + + let mut session_id_buf = 0_u64.to_ne_bytes(); + session_id_buf[..SESSION_ID_SIZE].copy_from_slice(safe_read_exact(&mut p, SESSION_ID_SIZE)?); + let alice_session_id = SessionId::new_from_u64(u64::from_le_bytes(session_id_buf)).ok_or(Error::InvalidPacket)?; + + let alice_s_public_blob_len = varint_safe_read(&mut p)?; + let alice_s_public_blob = safe_read_exact(&mut p, alice_s_public_blob_len as usize)?; + + let alice_metadata_len = varint_safe_read(&mut p)?; + let alice_metadata = safe_read_exact(&mut p, alice_metadata_len as usize)?; + + let alice_hk_public_raw = match safe_read_exact(&mut p, 1)?[0] { E1_TYPE_KYBER1024 => { - if packet_type == PACKET_TYPE_KEY_OFFER { - if p.len() < (pqc_kyber::KYBER_PUBLICKEYBYTES + 1) { - return Err(Error::InvalidPacket); - } - let e1p = &p[1..(pqc_kyber::KYBER_PUBLICKEYBYTES + 1)]; - p = &p[(pqc_kyber::KYBER_PUBLICKEYBYTES + 1)..]; - e1p + if packet_type == PACKET_TYPE_INITIAL_KEY_OFFER { + safe_read_exact(&mut p, pqc_kyber::KYBER_PUBLICKEYBYTES)? } else { - if p.len() < (pqc_kyber::KYBER_CIPHERTEXTBYTES + 1) { - return Err(Error::InvalidPacket); - } - let e1p = &p[1..(pqc_kyber::KYBER_CIPHERTEXTBYTES + 1)]; - p = &p[(pqc_kyber::KYBER_CIPHERTEXTBYTES + 1)..]; - e1p + safe_read_exact(&mut p, pqc_kyber::KYBER_CIPHERTEXTBYTES)? } } _ => &[], @@ -1611,49 +1429,32 @@ fn parse_key_offer_after_header( if p.is_empty() { return Err(Error::InvalidPacket); } - let alice_ratchet_key_fingerprint = if p[0] == 0x01 { - if p.len() < 16 { - return Err(Error::InvalidPacket); - } - Some(p[1..17].try_into().unwrap()) + let alice_ratchet_key_fingerprint = if safe_read_exact(&mut p, 1)?[0] == 0x01 { + Some(safe_read_exact(&mut p, 16)?) } else { None }; Ok(( - offer_id, + offer_id,//always 16 bytes alice_session_id, - alice_s_public, + alice_s_public_blob, alice_metadata, - alice_e1_public, - alice_ratchet_key_fingerprint, + alice_hk_public_raw, + alice_ratchet_key_fingerprint,//always 16 bytes )) } -/// Was this side the one who sent the first offer (Alice) or countered (Bob). -/// Note that role is not fixed. Either side can take either role. It's just who -/// initiated first. -enum Role { - Alice, - Bob, -} - -/// Key lifetime manager state and logic (separate to spotlight and keep clean) -struct KeyLifetime { - rekey_at_or_after_counter: u64, - hard_expire_at_counter: u64, - rekey_at_or_after_timestamp: i64, -} impl KeyLifetime { fn new(current_counter: CounterValue, current_time: i64) -> Self { Self { rekey_at_or_after_counter: current_counter.0 - + REKEY_AFTER_USES - + (random::next_u32_secure() % REKEY_AFTER_USES_MAX_JITTER) as u64, + + REKEY_AFTER_USES + + (random::next_u32_secure() % REKEY_AFTER_USES_MAX_JITTER) as u64, hard_expire_at_counter: current_counter.0 + EXPIRE_AFTER_USES, rekey_at_or_after_timestamp: current_time - + REKEY_AFTER_TIME_MS - + (random::next_u32_secure() % REKEY_AFTER_TIME_MS_MAX_JITTER) as i64, + + REKEY_AFTER_TIME_MS + + (random::next_u32_secure() % REKEY_AFTER_TIME_MS_MAX_JITTER) as i64, } } @@ -1668,21 +1469,6 @@ impl KeyLifetime { } } -/// A shared symmetric session key. -struct SessionKey { - secret_fingerprint: [u8; 16], // First 128 bits of a SHA384 computed from the secret - establish_time: i64, // Time session key was established - establish_counter: u64, // Counter value at which session was established - lifetime: KeyLifetime, // Key expiration time and counter - ratchet_key: Secret<64>, // Ratchet key for deriving the next session key - receive_key: Secret, // Receive side AES-GCM key - send_key: Secret, // Send side AES-GCM key - receive_cipher_pool: Mutex>>, // Pool of initialized sending ciphers - send_cipher_pool: Mutex>>, // Pool of initialized receiving ciphers - ratchet_count: u64, // Number of new keys negotiated in this session - jedi: bool, // True if Kyber1024 was used (both sides enabled) -} - impl SessionKey { /// Create a new symmetric shared session key and set its key expiration times, etc. fn new(key: Secret<64>, role: Role, current_time: i64, current_counter: CounterValue, ratchet_count: u64, jedi: bool) -> Self { @@ -1710,12 +1496,14 @@ impl SessionKey { #[inline] fn get_send_cipher(&self, counter: CounterValue) -> Result, Error> { if !self.lifetime.expired(counter) { - Ok(self + Ok( + self .send_cipher_pool .lock() .unwrap() .pop() - .unwrap_or_else(|| Box::new(AesGcm::new(self.send_key.as_bytes(), true)))) + .unwrap_or_else(|| Box::new(AesGcm::new(self.send_key.as_bytes(), true))) + ) } else { // Not only do we return an error, but we also destroy the key. let mut scp = self.send_cipher_pool.lock().unwrap(); @@ -1734,10 +1522,10 @@ impl SessionKey { #[inline] fn get_receive_cipher(&self) -> Box { self.receive_cipher_pool - .lock() - .unwrap() - .pop() - .unwrap_or_else(|| Box::new(AesGcm::new(self.receive_key.as_bytes(), false))) + .lock() + .unwrap() + .pop() + .unwrap_or_else(|| Box::new(AesGcm::new(self.receive_key.as_bytes(), false))) } #[inline] @@ -1767,224 +1555,3 @@ fn secret_fingerprint(key: &[u8]) -> [u8; 48] { tmp.update(key); tmp.finish() } - -#[cfg(test)] -mod tests { - use std::collections::LinkedList; - use std::sync::{Arc, Mutex}; - use zerotier_utils::hex; - - #[allow(unused_imports)] - use super::*; - - struct TestHost { - local_s: P384KeyPair, - local_s_hash: [u8; 48], - psk: Secret<64>, - session: Mutex>>>>, - session_id_counter: Mutex, - queue: Mutex>>, - key_id: Mutex<[u8; 16]>, - this_name: &'static str, - other_name: &'static str, - } - - impl TestHost { - fn new(psk: Secret<64>, this_name: &'static str, other_name: &'static str) -> Self { - let local_s = P384KeyPair::generate(); - let local_s_hash = SHA384::hash(local_s.public_key_bytes()); - Self { - local_s, - local_s_hash, - psk, - session: Mutex::new(None), - session_id_counter: Mutex::new(1), - queue: Mutex::new(LinkedList::new()), - key_id: Mutex::new([0; 16]), - this_name, - other_name, - } - } - } - - impl Host for Box { - type AssociatedObject = u32; - type SessionRef = Arc>>; - type IncomingPacketBuffer = Vec; - type RemoteAddress = u32; - - const REKEY_RATE_LIMIT_MS: i64 = 0; - - fn get_local_s_public(&self) -> &[u8] { - self.local_s.public_key_bytes() - } - - fn get_local_s_public_hash(&self) -> &[u8; 48] { - &self.local_s_hash - } - - fn get_local_s_keypair_p384(&self) -> &P384KeyPair { - &self.local_s - } - - fn extract_p384_static(static_public: &[u8]) -> Option { - P384PublicKey::from_bytes(static_public) - } - - fn session_lookup(&self, local_session_id: SessionId) -> Option { - self.session.lock().unwrap().as_ref().and_then(|s| { - if s.id == local_session_id { - Some(s.clone()) - } else { - None - } - }) - } - - fn check_new_session_attempt(&self, _: &ReceiveContext, _: &Self::RemoteAddress) -> bool { - true - } - - fn accept_new_session( - &self, - _: &ReceiveContext, - _: &u32, - _: &[u8], - _: &[u8], - ) -> Option<(SessionId, Secret<64>, Self::AssociatedObject)> { - loop { - let mut new_id = self.session_id_counter.lock().unwrap(); - *new_id += 1; - return Some((SessionId::new_from_u64(*new_id).unwrap(), self.psk.clone(), 0)); - } - } - } - - #[allow(unused_variables)] - #[test] - fn establish_session() { - let mut data_buf = [0_u8; (1280 - 32) * MAX_FRAGMENTS]; - let mut mtu_buffer = [0_u8; 1280]; - let mut psk: Secret<64> = Secret::default(); - random::fill_bytes_secure(&mut psk.0); - - let alice_host = Box::new(TestHost::new(psk.clone(), "alice", "bob")); - let bob_host = Box::new(TestHost::new(psk.clone(), "bob", "alice")); - let alice_rc: Box>> = Box::new(ReceiveContext::new(&alice_host)); - let bob_rc: Box>> = Box::new(ReceiveContext::new(&bob_host)); - - //println!("zssp: size of session (bytes): {}", std::mem::size_of::>>()); - - let _ = alice_host.session.lock().unwrap().insert(Arc::new( - Session::new( - &alice_host, - |data| bob_host.queue.lock().unwrap().push_front(data.to_vec()), - SessionId::new_random(), - bob_host.local_s.public_key_bytes(), - &[], - &psk, - 1, - mtu_buffer.len(), - 1, - ) - .unwrap(), - )); - - let mut ts = 0; - for test_loop in 0..256 { - for host in [&alice_host, &bob_host] { - let send_to_other = |data: &mut [u8]| { - if std::ptr::eq(host, &alice_host) { - bob_host.queue.lock().unwrap().push_front(data.to_vec()); - } else { - alice_host.queue.lock().unwrap().push_front(data.to_vec()); - } - }; - - let rc = if std::ptr::eq(host, &alice_host) { - &alice_rc - } else { - &bob_rc - }; - - loop { - if let Some(qi) = host.queue.lock().unwrap().pop_back() { - let qi_len = qi.len(); - ts += 1; - let r = rc.receive(host, &0, send_to_other, &mut data_buf, qi, mtu_buffer.len(), ts); - if r.is_ok() { - let r = r.unwrap(); - match r { - ReceiveResult::Ok => { - //println!("zssp: {} => {} ({}): Ok", host.other_name, host.this_name, qi_len); - } - ReceiveResult::OkData(data) => { - //println!("zssp: {} => {} ({}): OkData length=={}", host.other_name, host.this_name, qi_len, data.len()); - assert!(!data.iter().any(|x| *x != 0x12)); - } - ReceiveResult::OkNewSession(new_session) => { - println!( - "zssp: {} => {} ({}): OkNewSession ({})", - host.other_name, - host.this_name, - qi_len, - u64::from(new_session.id) - ); - let mut hs = host.session.lock().unwrap(); - assert!(hs.is_none()); - let _ = hs.insert(Arc::new(new_session)); - } - ReceiveResult::Ignored => { - println!("zssp: {} => {} ({}): Ignored", host.other_name, host.this_name, qi_len); - } - } - } else { - println!( - "zssp: {} => {} ({}): error: {}", - host.other_name, - host.this_name, - qi_len, - r.err().unwrap().to_string() - ); - panic!(); - } - } else { - break; - } - } - - data_buf.fill(0x12); - if let Some(session) = host.session.lock().unwrap().as_ref().cloned() { - if session.established() { - { - let mut key_id = host.key_id.lock().unwrap(); - let security_info = session.status().unwrap(); - if !security_info.0.eq(key_id.as_ref()) { - *key_id = security_info.0; - println!( - "zssp: new key at {}: fingerprint {} ratchet {} kyber {}", - host.this_name, - hex::to_string(key_id.as_ref()), - security_info.2, - security_info.3 - ); - } - } - for _ in 0..4 { - assert!(session - .send( - send_to_other, - &mut mtu_buffer, - &data_buf[..((random::xorshift64_random() as usize) % data_buf.len())] - ) - .is_ok()); - } - if (test_loop % 8) == 0 && test_loop >= 8 && host.this_name.eq("alice") { - session.service(host, send_to_other, &[], mtu_buffer.len(), test_loop as i64, true); - } - } - } - } - } - } -}