From 6bc69d1465ebeb7abab9a4077b4de062caa6608a Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Mon, 27 Feb 2023 13:36:35 -0500 Subject: [PATCH] Almost ready to test... --- zssp/Cargo.toml | 16 + zssp/src/applicationlayer.rs | 57 ++- zssp/src/constants.rs | 38 -- zssp/src/error.rs | 40 +-- zssp/src/lib.rs | 3 +- zssp/src/main.rs | 3 + zssp/src/proto.rs | 44 ++- zssp/src/zssp.rs | 648 ++++++++++++++++++++++------------- 8 files changed, 531 insertions(+), 318 deletions(-) delete mode 100644 zssp/src/constants.rs create mode 100644 zssp/src/main.rs diff --git a/zssp/Cargo.toml b/zssp/Cargo.toml index 924691225..d273a4013 100644 --- a/zssp/Cargo.toml +++ b/zssp/Cargo.toml @@ -5,6 +5,22 @@ license = "MPL-2.0" name = "zssp" version = "0.1.0" +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = 'abort' + +[lib] +name = "zssp" +path = "src/lib.rs" +doc = true + +[[bin]] +name = "zssp_test" +path = "src/main.rs" +doc = false + [dependencies] zerotier-utils = { path = "../utils" } zerotier-crypto = { path = "../crypto" } diff --git a/zssp/src/applicationlayer.rs b/zssp/src/applicationlayer.rs index f57f22fb0..b8d1d7c84 100644 --- a/zssp/src/applicationlayer.rs +++ b/zssp/src/applicationlayer.rs @@ -6,32 +6,59 @@ * https://www.zerotier.com/ */ -use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; +use zerotier_crypto::p384::P384KeyPair; /// Trait to implement to integrate the session into an application. /// /// Templating the session on this trait lets the code here be almost entirely transport, OS, /// and use case independent. +/// +/// The constants exposed in this trait can be redefined from their defaults to change rekey +/// and negotiation timeout behavior. This is discouraged except for testing purposes when low +/// key lifetime values may be desirable to test rekeying. Also note that each side takes turns +/// initiating rekey, so if both sides don't have the same values you'll get asymmetric timing +/// behavior. This will still work as long as the key usage counter doesn't exceed the +/// EXPIRE_AFTER_USES limit. pub trait ApplicationLayer: Sized { - /// Arbitrary opaque object associated with a session, such as a connection state object. + /// Rekey after this many key uses. + /// + /// The default is 1/4 the recommended NIST limit for AES-GCM. Unless you are transferring + /// a massive amount of data REKEY_AFTER_TIME_MS is probably going to kick in first. + const REKEY_AFTER_USES: u64 = 536870912; + + /// Hard expiration after this many uses. + /// + /// Attempting to encrypt more than this many messages with a key will cause a hard error + /// and the internal erasure of ephemeral key material. You'll only ever hit this if something + /// goes wrong and rekeying fails. + const EXPIRE_AFTER_USES: u64 = 2147483648; + + /// Start attempting to rekey after a key has been in use for this many milliseconds. + /// + /// Default is two hours. + const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60 * 2; + + /// Maximum random jitter to add to rekey-after time. + /// + /// Default is ten minutes. + const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; + + /// Timeout for Noise_XK session negotiation in milliseconds. + /// + /// Default is two seconds, which should be enough for even extremely slow links or links + /// over very long distances. + const SESSION_NEGOTIATION_TIMEOUT_MS: i64 = 2000; + + /// Type for arbitrary opaque object for use by the application that is attached to each session. type Data; - /// A buffer containing data read from the network that can be cached. + /// Data type for incoming packet buffers. /// - /// This can be e.g. a pooled buffer that automatically returns itself to the pool when dropped. - /// It can also just be a Vec or Box<[u8]> or something like that. + /// This can be something like Vec or Box<[u8]> or it can be something like a pooled reusable + /// buffer that automatically returns to its pool when ZSSP is done with it. ZSSP may hold these + /// for a short period of time when assembling fragmented packets on the receive path. type IncomingPacketBuffer: AsRef<[u8]> + AsMut<[u8]>; - /// Rate limit for attempts to rekey existing sessions in milliseconds (default: 2000). - const REKEY_RATE_LIMIT_MS: i64 = 2000; - - /// 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_static_public_blob(static_public: &[u8]) -> Option; - /// Get a reference to this host's static public key blob. /// /// This must contain a NIST P-384 public key but can contain other information. In ZeroTier this diff --git a/zssp/src/constants.rs b/zssp/src/constants.rs deleted file mode 100644 index e5737c958..000000000 --- a/zssp/src/constants.rs +++ /dev/null @@ -1,38 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - -use crate::proto::{AES_GCM_TAG_SIZE, HEADER_SIZE}; - -/// Minimum size of a valid physical ZSSP packet of any type. Anything smaller is discarded. -pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; - -/// Minimum physical MTU for ZSSP to function. -pub const MIN_TRANSPORT_MTU: usize = 128; - -/// Maximum size of init meta-data objects. -pub const MAX_METADATA_SIZE: usize = 256; - -/// Start attempting to rekey after a key has been used to send packets this many times. -/// This is 1/4 the recommended NIST limit for AES-GCM key lifetimes under most conditions. -pub(crate) const REKEY_AFTER_USES: u64 = 536870912; - -/// Hard expiration after this many uses. -/// -/// Use of the key beyond this point is prohibited. If we reach this number of key uses -/// the key will be destroyed in memory and the session will cease to function. A hard -/// error is also generated. -pub(crate) const EXPIRE_AFTER_USES: u64 = REKEY_AFTER_USES * 2; - -/// Start attempting to rekey after a key has been in use for this many milliseconds. -pub(crate) const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour - -/// Maximum random jitter to add to rekey-after time. -pub(crate) const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 minutes - -/// Timeout for incoming sessions in incomplete state in milliseconds. -pub(crate) const INCOMPLETE_SESSION_TIMEOUT: i64 = 1000; diff --git a/zssp/src/error.rs b/zssp/src/error.rs index ebc74207b..e416a8954 100644 --- a/zssp/src/error.rs +++ b/zssp/src/error.rs @@ -6,11 +6,9 @@ * https://www.zerotier.com/ */ -use crate::sessionid::SessionId; - pub enum Error { /// The packet was addressed to an unrecognized local session (should usually be ignored) - UnknownLocalSessionId(SessionId), + UnknownLocalSessionId, /// Packet was not well formed InvalidPacket, @@ -29,12 +27,6 @@ pub enum Error { /// Attempt to send using session without established key SessionNotEstablished, - /// Packet ignored by rate limiter. - RateLimited, - - /// Packet counter is too far outside window. - OutOfCounterWindow, - /// The other peer specified an unrecognized protocol version UnknownProtocolVersion, @@ -44,6 +36,9 @@ pub enum Error { /// Data object is too large to send, even with fragmentation DataTooLarge, + /// Packet counter was outside window or packet arrived with session in an unexpected state. + OutOfSequence, + /// An unexpected buffer overrun occured while attempting to encode or decode a packet. /// /// This can only ever happen if exceptionally large key blobs or metadata are being used, @@ -61,20 +56,19 @@ impl From for Error { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::UnknownLocalSessionId(id) => f.write_str(format!("UnknownLocalSessionId({})", id).as_str()), - Self::InvalidPacket => f.write_str("InvalidPacket"), - Self::InvalidParameter => f.write_str("InvalidParameter"), - Self::FailedAuthentication => f.write_str("FailedAuthentication"), - Self::MaxKeyLifetimeExceeded => f.write_str("MaxKeyLifetimeExceeded"), - Self::SessionNotEstablished => f.write_str("SessionNotEstablished"), - Self::RateLimited => f.write_str("RateLimited"), - Self::OutOfCounterWindow => f.write_str("OutOfCounterWindow"), - Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"), - Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"), - Self::DataTooLarge => f.write_str("DataTooLarge"), - Self::UnexpectedBufferOverrun => f.write_str("UnexpectedBufferOverrun"), - } + f.write_str(match self { + Self::UnknownLocalSessionId => "UnknownLocalSessionId", + Self::InvalidPacket => "InvalidPacket", + Self::InvalidParameter => "InvalidParameter", + Self::FailedAuthentication => "FailedAuthentication", + Self::MaxKeyLifetimeExceeded => "MaxKeyLifetimeExceeded", + Self::SessionNotEstablished => "SessionNotEstablished", + Self::UnknownProtocolVersion => "UnknownProtocolVersion", + Self::DataBufferTooSmall => "DataBufferTooSmall", + Self::DataTooLarge => "DataTooLarge", + Self::OutOfSequence => "OutOfSequence", + Self::UnexpectedBufferOverrun => "UnexpectedBufferOverrun", + }) } } diff --git a/zssp/src/lib.rs b/zssp/src/lib.rs index b6ad4e30e..456591e97 100644 --- a/zssp/src/lib.rs +++ b/zssp/src/lib.rs @@ -13,9 +13,8 @@ mod sessionid; mod tests; mod zssp; -pub mod constants; - pub use crate::applicationlayer::ApplicationLayer; pub use crate::error::Error; +pub use crate::proto::{MAX_METADATA_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU}; pub use crate::sessionid::SessionId; pub use crate::zssp::{Context, ReceiveResult, Session}; diff --git a/zssp/src/main.rs b/zssp/src/main.rs new file mode 100644 index 000000000..1a0d226c0 --- /dev/null +++ b/zssp/src/main.rs @@ -0,0 +1,3 @@ +use zssp::*; + +fn main() {} diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index cfee97845..40d4e6dd7 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -13,10 +13,18 @@ use zerotier_crypto::hash::{HMAC_SHA384_SIZE, SHA384_HASH_SIZE}; use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE; use crate::applicationlayer::ApplicationLayer; -use crate::constants::*; use crate::error::Error; use crate::sessionid::SessionId; +/// Minimum size of a valid physical ZSSP packet of any type. Anything smaller is discarded. +pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; + +/// Minimum physical MTU for ZSSP to function. +pub const MIN_TRANSPORT_MTU: usize = 128; + +/// Maximum size of init meta-data objects. +pub const MAX_METADATA_SIZE: usize = 256; + pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 16; @@ -30,8 +38,8 @@ pub(crate) const PACKET_TYPE_ALICE_REKEY_INIT: u8 = 4; pub(crate) const PACKET_TYPE_BOB_REKEY_ACK: u8 = 5; pub(crate) const HEADER_SIZE: usize = 16; -pub(crate) const HEADER_CHECK_ENCRYPT_START: usize = 6; -pub(crate) const HEADER_CHECK_ENCRYPT_END: usize = 22; +pub(crate) const HEADER_PROTECT_ENCRYPT_START: usize = 6; +pub(crate) const HEADER_PROTECT_ENCRYPT_END: usize = 22; pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION: u8 = b'X'; // intermediate keys used in key exchanges pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION: u8 = b'x'; // intermediate keys used in key exchanges @@ -46,7 +54,7 @@ pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS pub(crate) const BASE_KEY_SIZE: usize = 64; pub(crate) const AES_KEY_SIZE: usize = 32; -pub(crate) const AES_HEADER_CHECK_KEY_SIZE: usize = 16; +pub(crate) const AES_HEADER_PROTECTION_KEY_SIZE: usize = 16; pub(crate) const AES_GCM_TAG_SIZE: usize = 16; pub(crate) const AES_GCM_NONCE_SIZE: usize = 12; pub(crate) const AES_CTR_NONCE_SIZE: usize = 12; @@ -62,14 +70,14 @@ pub(crate) struct AliceNoiseXKInit { // -- start AES-CTR(es) encrypted section (IV is last 12 bytes of alice_noise_e)) pub alice_session_id: [u8; SessionId::SIZE], pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES], - pub header_check_cipher_key: [u8; AES_HEADER_CHECK_KEY_SIZE], + pub header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE], // -- end encrypted section pub hmac_es: [u8; HMAC_SHA384_SIZE], } impl AliceNoiseXKInit { pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; - pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_CHECK_KEY_SIZE; + pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_PROTECTION_KEY_SIZE; pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; } @@ -100,7 +108,7 @@ impl BobNoiseXKAck { pub(crate) struct AliceNoiseXKAck { pub header: [u8; HEADER_SIZE], pub session_protocol_version: u8, - // -- start AES-CTR(es_ee) encrypted section (IV is first 12 bytes of SHA384(hk)) + // -- start AES-CTR(es_ee) encrypted section (IV is first 12 bytes of hk) pub alice_static_blob_length: [u8; 2], pub alice_static_blob: [u8; ???], pub alice_metadata_length: [u8; 2], @@ -111,27 +119,43 @@ pub(crate) struct AliceNoiseXKAck { } */ +pub(crate) const ALICE_NOISE_XK_ACK_ENC_START: usize = HEADER_SIZE + 1; +pub(crate) const ALICE_NOISE_XK_ACK_AUTH_SIZE: usize = HMAC_SHA384_SIZE + HMAC_SHA384_SIZE; +pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = ALICE_NOISE_XK_ACK_ENC_START + 2 + 2 + ALICE_NOISE_XK_ACK_AUTH_SIZE; + #[allow(unused)] #[repr(C, packed)] pub(crate) struct AliceRekeyInit { pub header: [u8; HEADER_SIZE], // -- start AES-GCM encrypted portion (using current key) - pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE], + pub alice_e: [u8; P384_PUBLIC_KEY_SIZE], // -- end AES-GCM encrypted portion pub gcm_mac: [u8; AES_GCM_TAG_SIZE], } +impl AliceRekeyInit { + pub const ENC_START: usize = HEADER_SIZE; + pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE; + pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; +} + #[allow(unused)] #[repr(C, packed)] pub(crate) struct BobRekeyAck { pub header: [u8; HEADER_SIZE], // -- start AES-GCM encrypted portion (using current key) - pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE], - pub ee_fingerprint: [u8; SHA384_HASH_SIZE], + pub bob_e: [u8; P384_PUBLIC_KEY_SIZE], + pub next_key_fingerprint: [u8; SHA384_HASH_SIZE], // -- end AES-GCM encrypted portion pub gcm_mac: [u8; AES_GCM_TAG_SIZE], } +impl BobRekeyAck { + pub const ENC_START: usize = HEADER_SIZE; + pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE + SHA384_HASH_SIZE; + pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; +} + /// Assemble a series of fragments into a buffer and return the length of the assembled packet in bytes. pub(crate) fn assemble_fragments_into(fragments: &[A::IncomingPacketBuffer], d: &mut [u8]) -> Result { let mut l = 0; diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index cb3e69b95..5811975cc 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -16,7 +16,7 @@ use std::sync::{Arc, Mutex, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesCtr, AesGcm}; use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384}; -use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE}; +use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE, P384_PUBLIC_KEY_SIZE}; use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; @@ -28,7 +28,6 @@ use zerotier_utils::ringbuffermap::RingBufferMap; use pqc_kyber::{KYBER_SECRETKEYBYTES, KYBER_SSBYTES}; use crate::applicationlayer::ApplicationLayer; -use crate::constants::*; use crate::error::Error; use crate::proto::*; use crate::sessionid::SessionId; @@ -52,11 +51,8 @@ pub enum ReceiveResult<'b, Application: ApplicationLayer> { /// Packet was valid and a data payload was decoded and authenticated. OkData(Arc>, &'b mut [u8]), - /// Packet was valid and a new session was created, with static public blob and optional meta-data. - OkNewSession(Arc>, &'b [u8], Option<&'b [u8]>), - - /// Packet appears valid but was ignored as a duplicate or as meaningless given the current state. - Ignored, + /// Packet was valid and a new session was created. + OkNewSession(Arc>), /// Packet appears valid but was rejected by the application layer, e.g. a rejected new session attempt. Rejected, @@ -75,8 +71,7 @@ pub struct Session { psk: Secret, send_counter: AtomicU64, receive_window: [AtomicU64; COUNTER_WINDOW_MAX_OOO], - header_check_cipher: Aes, - offer: Mutex, + header_protection_cipher: Aes, state: RwLock, defrag: Mutex, 16, 16>>, } @@ -89,41 +84,36 @@ struct SessionMaps { incomplete: HashMap>, } -/// State for an incoming incomplete Noise_XK session that isn't fully negotiated yet. struct NoiseXKIncoming { timestamp: i64, alice_session_id: SessionId, bob_session_id: SessionId, noise_es_ee: Secret, hk: Secret, - header_check_cipher_key: Secret, + header_protection_key: Secret, bob_noise_e_secret: P384KeyPair, } -/// State that needs to be cached for the most recent outgoing offer. +struct NoiseXKOutgoing { + timestamp: i64, + alice_noise_e_secret: P384KeyPair, + noise_es: Secret, + alice_hk_secret: Secret, +} + enum EphemeralOffer { None, - NoiseXKInit( - // boxed to discard memory after use since we only enter this state once at setup - Box<( - // alice_e_secret, metadata, noise_es, alice_hk_public, alice_hk_secret, header check key - P384KeyPair, - Option>, - Secret<48>, - Secret, - )>, - ), + NoiseXKInit(Box), RekeyInit(P384KeyPair), } -/// Other mutable state within the session. struct State { remote_session_id: Option, keys: [Option; 2], current_key: usize, + offer: EphemeralOffer, } -/// A session key with lifetime information. struct SessionKey { ratchet_key: Secret, // Key used in derivation of the next session key receive_key: Secret, // Receive side AES-GCM key @@ -154,19 +144,28 @@ impl Context { /// Perform periodic background service and cleanup tasks. /// /// This returns the number of milliseconds until it should be called again. - pub fn service(&self, current_time: i64) -> i64 { + pub fn service>, &mut [u8])>(&self, mut send: SendFunction, current_time: i64) -> i64 { let mut dead_active = Vec::new(); let mut dead_pending = Vec::new(); { let sessions = self.sessions.read().unwrap(); for (id, s) in sessions.active.iter() { - if s.strong_count() == 0 { + if let Some(session) = s.upgrade() { + let state = session.state.read().unwrap(); + if let Some(key) = state.keys[state.current_key].as_ref() { + if key.role_is_bob + && (current_time >= key.rekey_at_time || session.send_counter.load(Ordering::Relaxed) >= key.rekey_at_counter) + { + session.send_rekey(|b| send(&session, b)); + } + } + } else { dead_active.push(*id); } } for (id, p) in sessions.incomplete.iter() { - if (p.timestamp - current_time) > INCOMPLETE_SESSION_TIMEOUT { + if (p.timestamp - current_time) > Application::SESSION_NEGOTIATION_TIMEOUT_MS { dead_pending.push(*id); } } @@ -182,7 +181,7 @@ impl Context { } } - INCOMPLETE_SESSION_TIMEOUT * 2 + Application::SESSION_NEGOTIATION_TIMEOUT_MS * 2 } /// Create a new session and send initial packet(s) to other side. @@ -191,9 +190,9 @@ impl Context { /// * `send` - User-supplied packet sending function /// * `mtu` - Physical MTU for calls to send() /// * `local_session_id` - This side's session ID - /// * `remote_s_public_blob` - Remote side's public key/identity blob - /// * `metadata` - Optional metadata to be included in initial handshake + /// * `remote_s_public_p384` - Remote side's static public NIST P-384 key /// * `psk` - Pre-shared key (use all zero if none) + /// * `metadata` - Optional metadata to be included in initial handshake /// * `application_data` - Arbitrary opaque data to include with session object #[allow(unused_variables)] pub fn open( @@ -201,10 +200,11 @@ impl Context { app: &Application, mut send: SendFunction, mtu: usize, - remote_s_public_blob: &[u8], - metadata: Option<&[u8]>, + remote_s_public_p384: &P384PublicKey, psk: Secret, + metadata: Option<&[u8]>, application_data: Application::Data, + current_time: i64, ) -> Result>, Error> { if let Some(md) = metadata.as_ref() { if md.len() > MAX_METADATA_SIZE { @@ -212,71 +212,71 @@ impl Context { } } - if let Some(bob_s_public) = Application::extract_s_public_from_static_public_blob(remote_s_public_blob) { - let alice_noise_e_secret = P384KeyPair::generate(); - let alice_noise_e = alice_noise_e_secret.public_key_bytes().clone(); - let noise_es = alice_noise_e_secret.agree(&bob_s_public).ok_or(Error::InvalidParameter)?; - let alice_hk_secret = pqc_kyber::keypair(&mut random::SecureRandom::default()); - let header_check_cipher_key: [u8; AES_HEADER_CHECK_KEY_SIZE] = random::get_bytes_secure(); + let alice_noise_e_secret = P384KeyPair::generate(); + let alice_noise_e = alice_noise_e_secret.public_key_bytes().clone(); + let noise_es = alice_noise_e_secret.agree(&remote_s_public_p384).ok_or(Error::InvalidParameter)?; + let alice_hk_secret = pqc_kyber::keypair(&mut random::SecureRandom::default()); + let header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE] = random::get_bytes_secure(); - let (local_session_id, session) = { - let mut sessions = self.sessions.write().unwrap(); + let (local_session_id, session) = { + let mut sessions = self.sessions.write().unwrap(); - let mut local_session_id; - loop { - local_session_id = SessionId::random(); - if !sessions.active.contains_key(&local_session_id) && !sessions.incomplete.contains_key(&local_session_id) { - break; - } + let mut local_session_id; + loop { + local_session_id = SessionId::random(); + if !sessions.active.contains_key(&local_session_id) && !sessions.incomplete.contains_key(&local_session_id) { + break; } + } - let session = Arc::new(Session { - id: local_session_id, - application_data, - psk, - send_counter: AtomicU64::new(2), // 1 is the counter value for this INIT message - receive_window: std::array::from_fn(|_| AtomicU64::new(0)), - header_check_cipher: Aes::new(&header_check_cipher_key), - offer: Mutex::new(EphemeralOffer::NoiseXKInit(Box::new(( + let session = Arc::new(Session { + id: local_session_id, + application_data, + psk, + send_counter: AtomicU64::new(2), // 1 is the counter value for this INIT message + receive_window: std::array::from_fn(|_| AtomicU64::new(0)), + header_protection_cipher: Aes::new(&header_protection_key), + state: RwLock::new(State { + remote_session_id: None, + keys: [None, None], + current_key: 0, + offer: EphemeralOffer::NoiseXKInit(Box::new(NoiseXKOutgoing { + timestamp: current_time, alice_noise_e_secret, - metadata.map(|md| ArrayVec::try_from(md).unwrap()), - noise_es.clone(), - Secret(alice_hk_secret.secret), - )))), - state: RwLock::new(State { remote_session_id: None, keys: [None, None], current_key: 0 }), - defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), - }); + noise_es: noise_es.clone(), + alice_hk_secret: Secret(alice_hk_secret.secret), + })), + }), + defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), + }); - sessions.active.insert(local_session_id, Arc::downgrade(&session)); + sessions.active.insert(local_session_id, Arc::downgrade(&session)); - (local_session_id, session) - }; + (local_session_id, session) + }; - let mut init_buffer = [0u8; AliceNoiseXKInit::SIZE]; - let init: &mut AliceNoiseXKInit = byte_array_as_proto_buffer_mut(&mut init_buffer).unwrap(); - init.session_protocol_version = SESSION_PROTOCOL_VERSION; - init.alice_noise_e = alice_noise_e; - init.alice_session_id = *local_session_id.as_bytes(); - init.alice_hk_public = alice_hk_secret.public; - init.header_check_cipher_key = header_check_cipher_key; + let mut init_buffer = [0u8; AliceNoiseXKInit::SIZE]; + let init: &mut AliceNoiseXKInit = byte_array_as_proto_buffer_mut(&mut init_buffer).unwrap(); + init.session_protocol_version = SESSION_PROTOCOL_VERSION; + init.alice_noise_e = alice_noise_e; + init.alice_session_id = *local_session_id.as_bytes(); + init.alice_hk_public = alice_hk_secret.public; + init.header_protection_key = header_protection_key; - let mut ctr = AesCtr::new(kbkdf::(noise_es.as_bytes()).as_bytes()); - ctr.reset_set_iv(&alice_noise_e[P384_PUBLIC_KEY_SIZE - AES_CTR_NONCE_SIZE..]); - ctr.crypt_in_place(&mut init_buffer[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]); + let mut ctr = AesCtr::new(kbkdf::(noise_es.as_bytes()).as_bytes()); + ctr.reset_set_iv(&alice_noise_e[P384_PUBLIC_KEY_SIZE - AES_CTR_NONCE_SIZE..]); + ctr.crypt_in_place(&mut init_buffer[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]); - let hmac = hmac_sha384_2( - kbkdf::(noise_es.as_bytes()).as_bytes(), - &create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_INIT, 1), - &init_buffer[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], - ); - init_buffer[AliceNoiseXKInit::AUTH_START..AliceNoiseXKInit::AUTH_START + HMAC_SHA384_SIZE].copy_from_slice(&hmac); + let hmac = hmac_sha384_2( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_INIT, 1), + &init_buffer[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], + ); + init_buffer[AliceNoiseXKInit::AUTH_START..AliceNoiseXKInit::AUTH_START + HMAC_SHA384_SIZE].copy_from_slice(&hmac); - send_with_fragmentation(&mut send, &mut init_buffer, mtu, PACKET_TYPE_ALICE_NOISE_XK_INIT, None, 0, 1, None)?; + send_with_fragmentation(&mut send, &mut init_buffer, mtu, PACKET_TYPE_ALICE_NOISE_XK_INIT, None, 0, 1, None)?; - return Ok(session); - } else { - return Err(Error::InvalidParameter); - } + return Ok(session); } /// Receive, authenticate, decrypt, and process a physical wire packet. @@ -285,18 +285,38 @@ impl Context { /// wtth an active session this session is supplied, otherwise this parameter is None. The size /// of packets to be sent will not exceed the supplied mtu. /// + /// The check_allow_incoming_session function is called when an initial Noise_XK init message is + /// received. This is before anything is known about the caller. A return value of true proceeds + /// with negotiation. False drops the packet. + /// + /// The check_accept_session function is called at the end of negotiation for an incoming session + /// with the caller's static public blob and meta-data if any. It must return the P-384 static public + /// key extracted from the supplied blob, a PSK (or all zeroes if none), and application data to + /// associate with the new session. A return of None abandons the session. + /// + /// Note that if check_accept_session accepts and returns Some() the session could still fail with + /// receive() returning an error. A Some() return from check_accept_sesion doesn't guarantee + /// successful new session init. + /// /// * `app` - Interface to application using ZSSP /// * `check_allow_incoming_session` - Function to call to check whether an unidentified new session should be accepted + /// * `check_accept_session` - Function to accept sessions after final negotiation, or returns None if rejected /// * `send` - Function to call to send packets /// * `data_buf` - Buffer to receive decrypted and authenticated object data (an error is returned if too small) /// * `incoming_packet_buf` - Buffer containing incoming wire packet (receive() takes ownership) /// * `mtu` - Physical wire MTU for sending packets /// * `current_time` - Current monotonic time in milliseconds #[inline] - pub fn receive<'b, SendFunction: FnMut(Option<&Arc>>, &mut [u8]), CheckAllowIncomingSession: FnMut() -> bool>( + pub fn receive< + 'b, + SendFunction: FnMut(Option<&Arc>>, &mut [u8]), + CheckAllowIncomingSession: FnMut() -> bool, + CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>, + >( &self, app: &Application, mut check_allow_incoming_session: CheckAllowIncomingSession, + mut check_accept_session: CheckAcceptSession, mut send: SendFunction, data_buf: &'b mut [u8], mut incoming_packet_buf: Application::IncomingPacketBuffer, @@ -308,12 +328,12 @@ impl Context { return Err(Error::InvalidPacket); } - let mut pending = None; + let mut incomplete = None; if let Some(local_session_id) = SessionId::new_from_u64_le(memory::load_raw(incoming_packet)) { if let Some(session) = self.look_up_session(local_session_id) { session - .header_check_cipher - .decrypt_block_in_place(&mut incoming_packet[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); + .header_protection_cipher + .decrypt_block_in_place(&mut incoming_packet[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); let (key_index, packet_type, fragment_count, fragment_no, counter) = parse_packet_header(&incoming_packet); if session.check_receive_window(counter) { @@ -327,6 +347,7 @@ impl Context { app, &mut send, &mut check_allow_incoming_session, + &mut check_accept_session, data_buf, counter, assembled_packet.as_ref(), @@ -348,6 +369,7 @@ impl Context { app, &mut send, &mut check_allow_incoming_session, + &mut check_accept_session, data_buf, counter, &[incoming_packet_buf], @@ -360,15 +382,15 @@ impl Context { ); } } else { - return Err(Error::OutOfCounterWindow); + return Err(Error::OutOfSequence); } } else { if let Some(p) = self.sessions.read().unwrap().incomplete.get(&local_session_id).cloned() { - Aes::new(p.header_check_cipher_key.as_bytes()) - .decrypt_block_in_place(&mut incoming_packet[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); - pending = Some(p); + Aes::new(p.header_protection_key.as_bytes()) + .decrypt_block_in_place(&mut incoming_packet[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); + incomplete = Some(p); } else { - return Err(Error::UnknownLocalSessionId(local_session_id)); + return Err(Error::UnknownLocalSessionId); } } } @@ -386,12 +408,13 @@ impl Context { app, &mut send, &mut check_allow_incoming_session, + &mut check_accept_session, data_buf, counter, assembled_packet.as_ref(), packet_type, None, - pending, + incomplete, key_index, mtu, current_time, @@ -402,12 +425,13 @@ impl Context { app, &mut send, &mut check_allow_incoming_session, + &mut check_accept_session, data_buf, counter, &[incoming_packet_buf], packet_type, None, - pending, + incomplete, key_index, mtu, current_time, @@ -421,30 +445,32 @@ impl Context { 'b, SendFunction: FnMut(Option<&Arc>>, &mut [u8]), CheckAllowIncomingSession: FnMut() -> bool, + CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>, >( &self, app: &Application, send: &mut SendFunction, check_allow_incoming_session: &mut CheckAllowIncomingSession, + check_accept_session: &mut CheckAcceptSession, data_buf: &'b mut [u8], counter: u64, fragments: &[Application::IncomingPacketBuffer], packet_type: u8, session: Option>>, - pending: Option>, + incomplete: Option>, key_index: usize, mtu: usize, current_time: i64, ) -> Result, Error> { debug_assert!(fragments.len() >= 1); - let message_nonce = create_message_nonce(packet_type, counter); + let incoming_message_nonce = create_message_nonce(packet_type, counter); if packet_type == PACKET_TYPE_DATA { if let Some(session) = session { let state = session.state.read().unwrap(); - if let Some(session_key) = state.keys[key_index].as_ref() { - let mut c = session_key.get_receive_cipher(); - c.reset_init_gcm(&message_nonce); + if let Some(key) = state.keys[key_index].as_ref() { + let mut c = key.get_receive_cipher(); + c.reset_init_gcm(&incoming_message_nonce); let mut data_len = 0; @@ -455,7 +481,7 @@ impl Context { let current_frag_data_start = data_len; data_len += f.len() - HEADER_SIZE; if data_len > data_buf.len() { - session_key.return_receive_cipher(c); + key.return_receive_cipher(c); return Err(Error::DataBufferTooSmall); } c.crypt(&f[HEADER_SIZE..], &mut data_buf[current_frag_data_start..data_len]); @@ -469,7 +495,7 @@ impl Context { } data_len += last_fragment.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE); if data_len > data_buf.len() { - session_key.return_receive_cipher(c); + key.return_receive_cipher(c); return Err(Error::DataBufferTooSmall); } let payload_end = last_fragment.len() - AES_GCM_TAG_SIZE; @@ -479,17 +505,17 @@ impl Context { ); let aead_authentication_ok = c.finish_decrypt(&last_fragment[payload_end..]); - session_key.return_receive_cipher(c); + key.return_receive_cipher(c); if aead_authentication_ok { if session.update_receive_window(counter) { // If the packet authenticated, this confirms that the other side indeed // knows this session key. In that case mark the session key as confirmed // and if the current active key is older switch it to point to this one. - if session_key.confirmed { + if key.confirmed { drop(state); } else { - let key_created_at_counter = session_key.created_at_counter; + let key_created_at_counter = key.created_at_counter; drop(state); let mut state = session.state.write().unwrap(); @@ -507,14 +533,14 @@ impl Context { return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); } else { - return Err(Error::OutOfCounterWindow); + return Err(Error::OutOfSequence); } } } return Err(Error::FailedAuthentication); } else { - return Err(Error::SessionNotEstablished); + return Err(Error::UnknownLocalSessionId); } } else { // For Noise setup/KEX packets go ahead and pre-assemble all fragments to simplify the code below. @@ -547,8 +573,8 @@ impl Context { * to the current exchange. */ - if session.is_some() || counter != 1 { - return Err(Error::OutOfCounterWindow); + if session.is_some() || incomplete.is_some() || counter != 1 { + return Err(Error::OutOfSequence); } let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; @@ -560,7 +586,7 @@ impl Context { &pkt.hmac_es, &hmac_sha384_2( kbkdf::(noise_es.as_bytes()).as_bytes(), - &message_nonce, + &incoming_message_nonce, &pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], ), ) { @@ -610,10 +636,10 @@ impl Context { if sessions.incomplete.len() >= self.max_incomplete_session_queue_size { // If this queue is too big, we remove the latest entry and replace it. The latest // is used because under flood conditions this is most likely to be another bogus - // entry. If we find one that is actually timed out, that always gets replaced. + // entry. If we find one that is actually timed out, that one is replaced instead. let mut newest = i64::MIN; let mut replace_id = None; - let cutoff_time = current_time - INCOMPLETE_SESSION_TIMEOUT; + let cutoff_time = current_time - Application::SESSION_NEGOTIATION_TIMEOUT_MS; for (id, s) in sessions.incomplete.iter() { if s.timestamp <= cutoff_time { replace_id = Some(*id); @@ -635,7 +661,7 @@ impl Context { noise_es_ee: noise_es_ee.clone(), hk, bob_noise_e_secret, - header_check_cipher_key: Secret(pkt.header_check_cipher_key), + header_protection_key: Secret(pkt.header_protection_key), }), ); @@ -672,7 +698,7 @@ impl Context { Some(alice_session_id), 0, 1, - Some(&Aes::new(&pkt.header_check_cipher_key)), + Some(&Aes::new(&pkt.header_protection_key)), )?; return Ok(ReceiveResult::Ok); @@ -687,13 +713,13 @@ impl Context { * the negotiation. */ - if counter != 1 { - return Err(Error::OutOfCounterWindow); + if counter != 1 || incomplete.is_some() { + return Err(Error::OutOfSequence); } if let Some(session) = session { - let mut offer = session.offer.lock().unwrap(); - if let EphemeralOffer::NoiseXKInit(boxed_offer) = &*offer { + let state = session.state.read().unwrap(); + if let EphemeralOffer::NoiseXKInit(boxed_offer) = &state.offer { let (alice_e_secret, metadata, noise_es, alice_hk_secret) = boxed_offer.as_ref(); let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; @@ -714,7 +740,7 @@ impl Context { &pkt.hmac_es_ee, &hmac_sha384_2( noise_es_ee_kex_hmac_key.as_bytes(), - &message_nonce, + &incoming_message_nonce, &pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START], ), ) { @@ -749,6 +775,7 @@ impl Context { kbkdf::(noise_es_ee_se_hk_psk.as_bytes()); let reply_counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; + debug_assert_eq!(reply_counter.get(), 2); let reply_message_nonce = create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_ACK, reply_counter.get()); // Create reply informing Bob of our static identity now that we've verified Bob and set @@ -777,7 +804,7 @@ impl Context { // is a hash of 'hk' making it actually a secret and "borrowing" a little PQ // forward secrecy for Alice's identity. let mut ctr = AesCtr::new(noise_es_ee_kex_enc_key.as_bytes()); - ctr.reset_set_iv(&SHA384::hash(hk.as_bytes())[..AES_CTR_NONCE_SIZE]); + ctr.reset_set_iv(&hk.as_bytes()[..AES_CTR_NONCE_SIZE]); ctr.crypt_in_place(&mut reply_buffer[HEADER_SIZE + 1..reply_len]); // First attach HMAC allowing Bob to verify that this is from the same Alice and to @@ -801,15 +828,11 @@ impl Context { reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee_se_hk_psk); reply_len += HMAC_SHA384_SIZE; - // Clear the offer field since we're finished handling a response to our initial offer. - *offer = EphemeralOffer::None; - drop(offer); - - // Learn Bob's session ID and the first session key. + drop(state); { let mut state = session.state.write().unwrap(); let _ = state.remote_session_id.insert(bob_session_id); - let _ = state.keys[0].insert(SessionKey::new( + let _ = state.keys[0].insert(SessionKey::new::( noise_es_ee_se_hk_psk, current_time, reply_counter.get(), @@ -817,6 +840,7 @@ impl Context { false, )); state.current_key = 0; + state.offer = EphemeralOffer::None; } send_with_fragmentation( @@ -827,7 +851,7 @@ impl Context { Some(bob_session_id), 0, reply_counter.get(), - Some(&session.header_check_cipher), + Some(&session.header_protection_cipher), )?; return Ok(ReceiveResult::Ok); @@ -835,10 +859,10 @@ impl Context { return Err(Error::InvalidPacket); } } else { - return Ok(ReceiveResult::Ignored); + return Err(Error::OutOfSequence); } } else { - return Err(Error::SessionNotEstablished); + return Err(Error::UnknownLocalSessionId); } } @@ -853,120 +877,245 @@ impl Context { * that Alice must return. */ - if session.is_some() { - return Ok(ReceiveResult::Ignored); + if session.is_some() || counter != 2 { + return Err(Error::OutOfSequence); } - - /* - // Restore state from note to self, returning to where we were after Alice's first contact. - let alice_session_id = SessionId::new_from_bytes(&bob_note_to_self.alice_session_id).ok_or(Error::InvalidPacket)?; - let bob_noise_e_secret = P384KeyPair::from_bytes(&bob_note_to_self.bob_noise_e, &bob_note_to_self.bob_noise_e_secret) - .ok_or(Error::InvalidPacket)?; - let hk = Secret(bob_note_to_self.hk); - let noise_es_ee = Secret(bob_note_to_self.noise_es_ee); - let header_check_cipher = Aes::new(&bob_note_to_self.header_check_cipher_key); - drop(bob_note_to_self_buffer); - - // Authenticate packet with noise_es_ee (first HMAC) before decrypting and parsing static info. - let pkt_assembled_enc_end = pkt_assembled.len() - (HMAC_SHA384_SIZE * 2); - if !secure_eq( - &pkt_assembled[pkt_assembled_enc_end..pkt_assembled.len() - HMAC_SHA384_SIZE], - &hmac_sha384_2( - &kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION).as_bytes()[..HMAC_SHA384_SIZE], - &message_nonce, - &pkt_assembled[HEADER_SIZE..pkt_assembled_enc_end], - ), - ) { - return Err(Error::FailedAuthentication); - } - - // Save a copy of the encrypted unmodified packet for final HMAC. - let mut pkt_saved_for_final_hmac = [0u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE]; - pkt_saved_for_final_hmac[..pkt_assembled.len()].copy_from_slice(pkt_assembled); - - // Decrypt Alice's static identity and decode. - let mut ctr = - AesCtr::new(&kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION).as_bytes()[..AES_KEY_SIZE]); - ctr.reset_set_iv(&SHA384::hash(hk.as_bytes())[..AES_CTR_NONCE_SIZE]); - ctr.crypt_in_place(&mut pkt_assembled[ALICE_NOISE_XK_ACK_ENC_START..pkt_assembled_enc_end]); - - let mut alice_static_info = &pkt_assembled[ALICE_NOISE_XK_ACK_ENC_START..pkt_assembled_enc_end]; - if alice_static_info.len() < 2 { + if pkt_assembled.len() < ALICE_NOISE_XK_ACK_MIN_SIZE { return Err(Error::InvalidPacket); } - let alice_static_public_blob_len = u16::from_le_bytes(alice_static_info[..2].try_into().unwrap()) as usize; - alice_static_info = &alice_static_info[2..]; - if alice_static_info.len() < alice_static_public_blob_len { - return Err(Error::InvalidPacket); - } - let alice_static_public_blob = &alice_static_info[..alice_static_public_blob_len]; - alice_static_info = &alice_static_info[alice_static_public_blob_len..]; - if alice_static_info.len() < 2 { - return Err(Error::InvalidPacket); - } - let meta_data_len = u16::from_le_bytes(alice_static_info[..2].try_into().unwrap()) as usize; - alice_static_info = &alice_static_info[2..]; - if alice_static_info.len() < meta_data_len { - return Err(Error::InvalidPacket); - } - let meta_data = &alice_static_info[..meta_data_len]; - if let Some((bob_session_id, psk, app_data)) = - app.accept_new_session(self, remote_address, alice_static_public_blob, meta_data) - { - // Create final Noise_XKpsk3 shared secret on this side. - let noise_es_ee_se_hk_psk = Secret(hmac_sha512( - &hmac_sha512( - noise_es_ee.as_bytes(), - bob_noise_e_secret - .agree( - &Application::extract_s_public_from_raw(alice_static_public_blob) - .ok_or(Error::FailedAuthentication)?, - ) - .ok_or(Error::FailedAuthentication)? - .as_bytes(), - ), - &hmac_sha512(psk.as_bytes(), hk.as_bytes()), - )); + if let Some(incomplete) = incomplete { + // Check timeout, negotiations aren't allowed to take longer than this. + if (current_time - incomplete.timestamp) > Application::SESSION_NEGOTIATION_TIMEOUT_MS { + return Err(Error::UnknownLocalSessionId); + } - // Final authentication with the whole enchelada. + // Check the first HMAC to verify against the currently known noise_es_ee key, which verifies + // that this reply is part of this session. + let auth_start = pkt_assembled.len() - ALICE_NOISE_XK_ACK_AUTH_SIZE; if !secure_eq( - &pkt_assembled[pkt_assembled_enc_end + HMAC_SHA384_SIZE..], + &pkt_assembled[auth_start..pkt_assembled.len() - HMAC_SHA384_SIZE], &hmac_sha384_2( - &kbkdf512(noise_es_ee_se_hk_psk.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION).as_bytes() - [..HMAC_SHA384_SIZE], - &message_nonce, - &pkt_saved_for_final_hmac[HEADER_SIZE..pkt_assembled_enc_end + HMAC_SHA384_SIZE], + kbkdf::(incomplete.noise_es_ee.as_bytes()) + .as_bytes(), + &incoming_message_nonce, + &pkt_assembled[HEADER_SIZE..auth_start], ), ) { return Err(Error::FailedAuthentication); } - return Ok(ReceiveResult::OkNewSession(Arc::new(Session { - id: bob_session_id, - application_data: app_data, + // Make a copy of pkt_assembled so we can check the second HMAC against original ciphertext later. + let mut pkt_assembly_buffer_copy = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; + pkt_assembly_buffer_copy[..pkt_assembled.len()].copy_from_slice(pkt_assembled); + + // Decrypt encrypted section so we can finally learn Alice's static identity. + let mut ctr = AesCtr::new( + kbkdf::(incomplete.noise_es_ee.as_bytes()).as_bytes(), + ); + ctr.reset_set_iv(&incomplete.hk.as_bytes()[..AES_CTR_NONCE_SIZE]); + ctr.crypt_in_place(&mut pkt_assembled[ALICE_NOISE_XK_ACK_ENC_START..auth_start]); + + // Read the static public blob and optional meta-data. + let mut pkt_assembled_ptr = HEADER_SIZE + 1; + let mut pkt_assembled_field_end = pkt_assembled_ptr + 2; + if pkt_assembled_field_end >= pkt_assembled.len() { + return Err(Error::InvalidPacket); + } + let alice_static_public_blob_size = + u16::from_le(memory::load_raw::(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end])) as usize; + pkt_assembled_ptr = pkt_assembled_field_end; + pkt_assembled_field_end = pkt_assembled_ptr + alice_static_public_blob_size; + if pkt_assembled_field_end >= pkt_assembled.len() { + return Err(Error::InvalidPacket); + } + let alice_static_public_blob = &pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end]; + pkt_assembled_ptr = pkt_assembled_field_end; + pkt_assembled_field_end = pkt_assembled_ptr + 2; + if pkt_assembled_field_end >= pkt_assembled.len() { + return Err(Error::InvalidPacket); + } + let alice_meta_data_size = + u16::from_le(memory::load_raw::(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end])) as usize; + pkt_assembled_ptr = pkt_assembled_field_end; + pkt_assembled_field_end = pkt_assembled_ptr + alice_meta_data_size; + let alice_meta_data = if alice_meta_data_size > 0 { + Some(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end]) + } else { + None + }; + + // Check session acceptance and fish Alice's NIST P-384 static public key out of + // her static public blob. + let check_result = check_accept_session(alice_static_public_blob, alice_meta_data); + if check_result.is_none() { + self.sessions.write().unwrap().incomplete.remove(&incomplete.bob_session_id); + return Ok(ReceiveResult::Rejected); + } + let (alice_noise_s, psk, application_data) = check_result.unwrap(); + + // Complete Noise_XKpsk3 on Bob's side. + let noise_es_ee_se_hk_psk = Secret(hmac_sha512( + &hmac_sha512( + incomplete.noise_es_ee.as_bytes(), + incomplete + .bob_noise_e_secret + .agree(&alice_noise_s) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + ), + &hmac_sha512(psk.as_bytes(), incomplete.hk.as_bytes()), + )); + + // Verify the packet using the final key to verify the whole key exchange. + if !secure_eq( + &pkt_assembly_buffer_copy[auth_start + HMAC_SHA384_SIZE..pkt_assembled.len()], + &hmac_sha384_2( + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()) + .as_bytes(), + &incoming_message_nonce, + &pkt_assembly_buffer_copy[HEADER_SIZE..auth_start], + ), + ) { + return Err(Error::FailedAuthentication); + } + + let session = Arc::new(Session { + id: incomplete.bob_session_id, + application_data, psk, - send_counter: AtomicU64::new(2), // 1 was already used in our first reply + send_counter: AtomicU64::new(2), // 1 was already used during negotiation receive_window: std::array::from_fn(|_| AtomicU64::new(0)), - header_check_cipher, - offer: Mutex::new(EphemeralOffer::None), + header_protection_cipher: Aes::new(incomplete.header_protection_key.as_bytes()), state: RwLock::new(State { - remote_session_id: Some(alice_session_id), - keys: [Some(SessionKey::new(noise_es_ee_se_hk_psk, current_time, 2, true, true)), None], + remote_session_id: Some(incomplete.alice_session_id), + keys: [ + Some(SessionKey::new::(noise_es_ee_se_hk_psk, current_time, 2, true, true)), + None, + ], current_key: 0, + offer: EphemeralOffer::None, }), defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), - }))); + }); + + { + let mut sessions = self.sessions.write().unwrap(); + sessions.incomplete.remove(&incomplete.bob_session_id); + sessions.active.insert(incomplete.bob_session_id, Arc::downgrade(&session)); + } + + return Ok(ReceiveResult::OkNewSession(session)); } else { - return Err(Error::NewSessionRejected); + return Err(Error::UnknownLocalSessionId); } - */ - todo!() } - PACKET_TYPE_ALICE_REKEY_INIT => todo!(), + PACKET_TYPE_ALICE_REKEY_INIT => { + if pkt_assembled.len() != AliceRekeyInit::SIZE { + return Err(Error::InvalidPacket); + } + if let Some(session) = session { + let state = session.state.read().unwrap(); + if let Some(key) = state.keys[key_index].as_ref() { + // Only the current "Alice" accepts rekeys initiated by the current "Bob." + if !key.role_is_bob { + let mut c = key.get_receive_cipher(); + c.reset_init_gcm(&incoming_message_nonce); + c.crypt_in_place(&mut pkt_assembled[AliceRekeyInit::ENC_START..AliceRekeyInit::AUTH_START]); + let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[AliceRekeyInit::AUTH_START..]); + key.return_receive_cipher(c); - PACKET_TYPE_BOB_REKEY_ACK => todo!(), + if aead_authentication_ok { + let pkt: &AliceRekeyInit = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); + if let Some(alice_e) = P384PublicKey::from_bytes(&pkt.alice_e) { + let bob_e_secret = P384KeyPair::generate(); + let ee = bob_e_secret.agree(&alice_e).ok_or(Error::FailedAuthentication)?; + let next_session_key = Secret(hmac_sha512(key.ratchet_key.as_bytes(), ee.as_bytes())); + + let mut reply_buf = [0u8; BobRekeyAck::SIZE]; + let reply: &mut BobRekeyAck = byte_array_as_proto_buffer_mut(&mut reply_buf).unwrap(); + reply.bob_e = *bob_e_secret.public_key_bytes(); + reply.next_key_fingerprint = SHA384::hash(next_session_key.as_bytes()); + + let counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; + let mut c = key.get_send_cipher(counter.get())?; + c.reset_init_gcm(&create_message_nonce(PACKET_TYPE_BOB_REKEY_ACK, counter.get())); + c.crypt_in_place(&mut reply_buf[BobRekeyAck::ENC_START..BobRekeyAck::AUTH_START]); + reply_buf[BobRekeyAck::AUTH_START..].copy_from_slice(&c.finish_encrypt()); + key.return_send_cipher(c); + + send(Some(&session), &mut reply_buf); + + drop(state); + let mut state = session.state.write().unwrap(); + let _ = state.keys[key_index ^ 1].replace(SessionKey::new::( + next_session_key, + current_time, + counter.get(), + false, + true, + )); + + return Ok(ReceiveResult::Ok); + } + } + return Err(Error::FailedAuthentication); + } + } + return Err(Error::OutOfSequence); + } else { + return Err(Error::UnknownLocalSessionId); + } + } + + PACKET_TYPE_BOB_REKEY_ACK => { + if pkt_assembled.len() != BobRekeyAck::SIZE { + return Err(Error::InvalidPacket); + } + if let Some(session) = session { + let state = session.state.read().unwrap(); + if let EphemeralOffer::RekeyInit(alice_e_secret) = &state.offer { + if let Some(key) = state.keys[key_index].as_ref() { + // Only the current "Bob" initiates rekeys and expects this ACK. + if key.role_is_bob { + let mut c = key.get_receive_cipher(); + c.reset_init_gcm(&incoming_message_nonce); + c.crypt_in_place(&mut pkt_assembled[BobRekeyAck::ENC_START..BobRekeyAck::AUTH_START]); + let aead_authentication_ok = c.finish_decrypt(&pkt_assembled[BobRekeyAck::AUTH_START..]); + key.return_receive_cipher(c); + + if aead_authentication_ok { + let pkt: &BobRekeyAck = byte_array_as_proto_buffer(&pkt_assembled).unwrap(); + if let Some(bob_e) = P384PublicKey::from_bytes(&pkt.bob_e) { + let ee = alice_e_secret.agree(&bob_e).ok_or(Error::FailedAuthentication)?; + let next_session_key = Secret(hmac_sha512(key.ratchet_key.as_bytes(), ee.as_bytes())); + + if secure_eq(&pkt.next_key_fingerprint, &SHA384::hash(next_session_key.as_bytes())) { + drop(state); + let mut state = session.state.write().unwrap(); + let _ = state.keys[key_index ^ 1].replace(SessionKey::new::( + next_session_key, + current_time, + counter, + true, + false, + )); + state.offer = EphemeralOffer::None; + + return Ok(ReceiveResult::Ok); + } + } + } + return Err(Error::FailedAuthentication); + } + } + } + return Err(Error::OutOfSequence); + } else { + return Err(Error::UnknownLocalSessionId); + } + } _ => { return Err(Error::InvalidPacket); @@ -1027,8 +1176,8 @@ impl Session { mtu_sized_buffer[fragment_size..tagged_fragment_size].copy_from_slice(&c.finish_encrypt()); fragment_size = tagged_fragment_size; } - self.header_check_cipher - .encrypt_block_in_place(&mut mtu_sized_buffer[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); + self.header_protection_cipher + .encrypt_block_in_place(&mut mtu_sized_buffer[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); send(&mut mtu_sized_buffer[..fragment_size]); } debug_assert!(data.is_empty()); @@ -1047,6 +1196,36 @@ impl Session { state.keys[state.current_key].is_some() } + /// Send a rekey init message. + /// + /// This is called from the session context's service() method when it's time to rekey. + /// It should only be called when the current key was established in the 'bob' role. This + /// is checked when rekey time is checked. + fn send_rekey(&self, mut send: SendFunction) { + let rekey_e = P384KeyPair::generate(); + + let mut rekey_buf = [0u8; AliceRekeyInit::SIZE]; + let pkt: &mut AliceRekeyInit = byte_array_as_proto_buffer_mut(&mut rekey_buf).unwrap(); + pkt.alice_e = *rekey_e.public_key_bytes(); + + let state = self.state.read().unwrap(); + if let Some(key) = state.keys[state.current_key].as_ref() { + if let Some(counter) = self.get_next_outgoing_counter() { + if let Ok(mut gcm) = key.get_send_cipher(counter.get()) { + gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_ALICE_REKEY_INIT, counter.get())); + gcm.crypt_in_place(&mut rekey_buf[AliceRekeyInit::ENC_START..AliceRekeyInit::AUTH_START]); + rekey_buf[AliceRekeyInit::AUTH_START..].copy_from_slice(&gcm.finish_encrypt()); + key.return_send_cipher(gcm); + + send(&mut rekey_buf); + + drop(state); + self.state.write().unwrap().offer = EphemeralOffer::RekeyInit(rekey_e); + } + } + } + } + /// Get the next outgoing counter value. #[inline(always)] fn get_next_outgoing_counter(&self) -> Option { @@ -1142,7 +1321,7 @@ fn send_with_fragmentation( recipient_session_id: Option, key_index: usize, counter: u64, - header_check_cipher: Option<&Aes>, + header_protect_cipher: Option<&Aes>, ) -> Result<(), Error> { let packet_len = packet.len(); let recipient_session_id = recipient_session_id.map_or(SessionId::NONE, |s| u64::from(s)); @@ -1160,8 +1339,8 @@ fn send_with_fragmentation( key_index, counter, ); - if let Some(hcc) = header_check_cipher { - hcc.encrypt_block_in_place(&mut fragment[6..22]); + if let Some(hcc) = header_protect_cipher { + hcc.encrypt_block_in_place(&mut fragment[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); } send(fragment); fragment_start = fragment_end - HEADER_SIZE; @@ -1171,7 +1350,13 @@ fn send_with_fragmentation( } impl SessionKey { - fn new(key: Secret, current_time: i64, current_counter: u64, confirmed: bool, role_is_bob: bool) -> Self { + fn new( + key: Secret, + current_time: i64, + current_counter: u64, + confirmed: bool, + role_is_bob: bool, + ) -> Self { let a2b = kbkdf::(key.as_bytes()); let b2a = kbkdf::(key.as_bytes()); let (receive_key, send_key) = if role_is_bob { @@ -1186,11 +1371,14 @@ impl SessionKey { receive_cipher_pool: Mutex::new(Vec::with_capacity(2)), send_cipher_pool: Mutex::new(Vec::with_capacity(2)), rekey_at_time: current_time - .checked_add(REKEY_AFTER_TIME_MS + ((random::xorshift64_random() as u32) % REKEY_AFTER_TIME_MS_MAX_JITTER) as i64) + .checked_add( + Application::REKEY_AFTER_TIME_MS + + ((random::xorshift64_random() as u32) % Application::REKEY_AFTER_TIME_MS_MAX_JITTER) as i64, + ) .unwrap(), created_at_counter: current_counter, - rekey_at_counter: current_counter.checked_add(REKEY_AFTER_USES).unwrap(), - expire_at_counter: current_counter.checked_add(EXPIRE_AFTER_USES).unwrap(), + rekey_at_counter: current_counter.checked_add(Application::REKEY_AFTER_USES).unwrap(), + expire_at_counter: current_counter.checked_add(Application::EXPIRE_AFTER_USES).unwrap(), confirmed, role_is_bob, }