From c97d5d28bbedaed2cd2d7204e546717a6e769d50 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 20 Jan 2023 20:21:42 -0500 Subject: [PATCH] A whole lot more Noise_XK work... exchange almost done. --- crypto/src/aes.rs | 159 ++++++++++- crypto/src/lib.rs | 10 + crypto/src/secret.rs | 5 + zssp/src/applicationlayer.rs | 31 +- zssp/src/constants.rs | 7 +- zssp/src/error.rs | 8 + zssp/src/lib.rs | 2 +- zssp/src/proto.rs | 142 ++++++---- zssp/src/sessionid.rs | 1 + zssp/src/zssp.rs | 531 ++++++++++++++++++++++++----------- 10 files changed, 656 insertions(+), 240 deletions(-) diff --git a/crypto/src/aes.rs b/crypto/src/aes.rs index 014d241ab..5d0cd2c32 100644 --- a/crypto/src/aes.rs +++ b/crypto/src/aes.rs @@ -46,7 +46,7 @@ mod fruit_flavored { data_out_len: usize, data_out_written: *mut usize, ) -> i32; - //fn CCCryptorReset(cryptor_ref: *mut c_void, iv: *const c_void) -> i32; + fn CCCryptorReset(cryptor_ref: *mut c_void, iv: *const c_void) -> i32; fn CCCryptorRelease(cryptor_ref: *mut c_void) -> i32; fn CCCryptorGCMSetIV(cryptor_ref: *mut c_void, iv: *const c_void, iv_len: usize) -> i32; fn CCCryptorGCMAddAAD(cryptor_ref: *mut c_void, aad: *const c_void, len: usize) -> i32; @@ -183,6 +183,101 @@ mod fruit_flavored { unsafe impl Send for Aes {} unsafe impl Sync for Aes {} + pub struct AesCtr(*mut c_void); + + impl Drop for AesCtr { + #[inline(always)] + fn drop(&mut self) { + unsafe { CCCryptorRelease(self.0) }; + } + } + + impl AesCtr { + /// Construct a new AES-CTR cipher. + /// Key must be 16, 24, or 32 bytes in length or a panic will occur. + pub fn new(k: &[u8]) -> Self { + if k.len() != 32 && k.len() != 24 && k.len() != 16 { + panic!("AES supports 128, 192, or 256 bits keys"); + } + unsafe { + let mut ptr: *mut c_void = null_mut(); + let result = CCCryptorCreateWithMode( + kCCEncrypt, + kCCModeCTR, + kCCAlgorithmAES, + 0, + [0_u64; 2].as_ptr().cast(), + k.as_ptr().cast(), + k.len(), + null(), + 0, + 0, + 0, + &mut ptr, + ); + if result != 0 { + panic!("CCCryptorCreateWithMode for CTR mode returned {}", result); + } + AesCtr(ptr) + } + } + + /// Initialize AES-CTR for encryption or decryption with the given IV. + /// If it's already been used, this also resets the cipher. There is no separate reset. + pub fn reset_set_iv(&mut self, iv: &[u8]) { + unsafe { + if iv.len() == 16 { + if CCCryptorReset(self.0, iv.as_ptr().cast()) != 0 { + panic!("CCCryptorReset for CTR mode failed (old MacOS bug)"); + } + } else if iv.len() < 16 { + let mut iv2 = [0_u8; 16]; + iv2[0..iv.len()].copy_from_slice(iv); + if CCCryptorReset(self.0, iv2.as_ptr().cast()) != 0 { + panic!("CCCryptorReset for CTR mode failed (old MacOS bug)"); + } + } else { + panic!("CTR IV must be less than or equal to 16 bytes in length"); + } + } + } + + /// Encrypt or decrypt (same operation with CTR mode) + #[inline(always)] + pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) { + unsafe { + assert!(output.len() >= input.len()); + let mut data_out_written: usize = 0; + CCCryptorUpdate( + self.0, + input.as_ptr().cast(), + input.len(), + output.as_mut_ptr().cast(), + output.len(), + &mut data_out_written, + ); + } + } + + /// Encrypt or decrypt in place (same operation with CTR mode) + #[inline(always)] + pub fn crypt_in_place(&mut self, data: &mut [u8]) { + unsafe { + let mut data_out_written: usize = 0; + CCCryptorUpdate( + self.0, + data.as_ptr().cast(), + data.len(), + data.as_mut_ptr().cast(), + data.len(), + &mut data_out_written, + ); + } + } + } + + unsafe impl Send for AesCtr {} + pub struct AesGcm(*mut c_void, bool); impl Drop for AesGcm { @@ -307,6 +402,17 @@ mod openssl_aes { use std::cell::UnsafeCell; use std::mem::MaybeUninit; + fn aes_ctr_by_key_size(ks: usize) -> Cipher { + match ks { + 16 => Cipher::aes_128_ctr(), + 24 => Cipher::aes_192_ctr(), + 32 => Cipher::aes_256_ctr(), + _ => { + panic!("AES supports 128, 192, or 256 bits keys"); + } + } + } + fn aes_gcm_by_key_size(ks: usize) -> Cipher { match ks { 16 => Cipher::aes_128_gcm(), @@ -390,6 +496,53 @@ mod openssl_aes { unsafe impl Send for Aes {} unsafe impl Sync for Aes {} + pub struct AesCtr(Secret<32>, usize, Option); + + impl AesCtr { + /// Construct a new AES-CTR cipher. + /// Key must be 16, 24, or 32 bytes in length or a panic will occur. + #[inline(always)] + pub fn new(k: &[u8]) -> Self { + let mut s: Secret<32> = Secret::default(); + match k.len() { + 16 | 24 | 32 => { + s.0[..k.len()].copy_from_slice(k); + Self(s, k.len(), None) + } + _ => { + panic!("AES supports 128, 192, or 256 bits keys"); + } + } + } + + /// Initialize AES-CTR for encryption or decryption with the given IV. + /// If it's already been used, this also resets the cipher. There is no separate reset. + #[inline(always)] + pub fn reset_set_iv(&mut self, iv: &[u8]) { + let mut c = Crypter::new(aes_ctr_by_key_size(self.1), Mode::Encrypt, &self.0 .0[..self.1], Some(iv)).unwrap(); + c.pad(false); + let _ = self.2.replace(c); + } + + /// Encrypt or decrypt (same operation with CTR mode) + #[inline(always)] + pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) { + let _ = self.2.as_mut().unwrap().update(input, output); + } + + /// Encrypt or decrypt in place (same operation with CTR mode) + #[inline(always)] + pub fn crypt_in_place(&mut self, data: &mut [u8]) { + let _ = self + .2 + .as_mut() + .unwrap() + .update(unsafe { &*std::slice::from_raw_parts(data.as_ptr(), data.len()) }, data); + } + } + + unsafe impl Send for AesCtr {} + pub struct AesGcm(Secret<32>, usize, CipherCtx, bool); impl AesGcm { @@ -479,10 +632,10 @@ mod openssl_aes { } #[cfg(target_os = "macos")] -pub use fruit_flavored::{Aes, AesGcm}; +pub use fruit_flavored::{Aes, AesCtr, AesGcm}; #[cfg(not(target_os = "macos"))] -pub use openssl_aes::{Aes, AesGcm}; +pub use openssl_aes::{Aes, AesCtr, AesGcm}; #[cfg(test)] mod tests { diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 94ab7ff7f..d2812429f 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -27,3 +27,13 @@ pub fn secure_eq + ?Sized, B: AsRef<[u8]> + ?Sized>(a: &A, b: &B) false } } + +extern "C" { + fn OPENSSL_cleanse(ptr: *mut std::ffi::c_void, len: usize); +} + +/// Destroy the contents of some memory +#[inline(always)] +pub fn burn(b: &mut [u8]) { + unsafe { OPENSSL_cleanse(b.as_mut_ptr().cast(), b.len()) }; +} diff --git a/crypto/src/secret.rs b/crypto/src/secret.rs index 666b30a7d..8705f37cf 100644 --- a/crypto/src/secret.rs +++ b/crypto/src/secret.rs @@ -37,6 +37,11 @@ impl Secret { &self.0 } + #[inline(always)] + pub fn as_bytes_mut(&mut self) -> &mut [u8; L] { + &mut self.0 + } + /// Get the first N bytes of this secret as a fixed length array. #[inline(always)] pub fn first_n(&self) -> &[u8; N] { diff --git a/zssp/src/applicationlayer.rs b/zssp/src/applicationlayer.rs index c29a9e8fd..b597075ac 100644 --- a/zssp/src/applicationlayer.rs +++ b/zssp/src/applicationlayer.rs @@ -33,6 +33,13 @@ pub trait ApplicationLayer: Sized { /// 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_raw(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 @@ -49,27 +56,21 @@ pub trait ApplicationLayer: Sized { /// This must return the NIST P-384 public key that is contained within the static public key blob. fn get_local_s_keypair(&self) -> &P384KeyPair; - /// Extract the NIST P-384 ECC public key component from a static public key blob or return None on failure. - /// - /// This is called to parse the static public key blob from the other end and extract its NIST P-384 public - /// key. SECURITY NOTE: the information supplied here is from the wire so care must be taken to parse it - /// safely and fail on any error or corruption. - fn extract_s_public_from_raw(static_public: &[u8]) -> Option; - /// Look up a local session by local session ID or return None if not found. fn lookup_session<'a>(&self, local_session_id: SessionId) -> Option>; - /// Rate limit and check an attempted new session (called before accept_new_session). - fn check_new_session(&self, rc: &ReceiveContext, remote_address: &Self::RemoteAddress) -> bool; - - /// Get a new locally unique session ID. - fn new_session(&self, remote_address: &Self::RemoteAddress) -> Option<(SessionId, Self::Data)>; + /// Rate limit and check an attempted new session. + fn prescreen_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. + /// This is called in the final phase of Noise_XK once Alice's identity is known. If it should be + /// accepted then this should return a session ID, a PSK or all zeroes if there is none, and application + /// specific data to attach to the session. + /// + /// This doesn't guarantee acceptance until the new session is returned from the receive call, since + /// something can still go wrong during final authentication and session setup. The session ID supplied + /// here should not be assumed to be in use. fn accept_new_session( &self, receive_context: &ReceiveContext, diff --git a/zssp/src/constants.rs b/zssp/src/constants.rs index 97ce0dfa1..18e09ba6d 100644 --- a/zssp/src/constants.rs +++ b/zssp/src/constants.rs @@ -52,6 +52,7 @@ pub(crate) const AES_KEY_SIZE: usize = 32; pub(crate) const AES_HEADER_CHECK_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; /// Size of a session ID, which behaves a bit like a TCP port number. /// @@ -61,6 +62,9 @@ pub(crate) const SESSION_ID_SIZE: usize = 6; /// Maximum difference between out-of-order incoming packet counters, and size of deduplication buffer. pub(crate) const COUNTER_WINDOW_MAX_OUT_OF_ORDER: usize = 16; +/// Timeout for a "note to self" that must be returned by a session initiator. +pub(crate) const BOB_NOTE_TO_SELF_TIMEOUT_MS: i64 = 500; + /// Maximum skip-ahead for counter. /// /// This is huge (2^24) because its real purpose is to filter out bad packets where decryption of @@ -68,7 +72,8 @@ pub(crate) const COUNTER_WINDOW_MAX_OUT_OF_ORDER: usize = 16; pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 16777216; // Key usage labels for sub-key derivation using NIST-style KBKDF (basically just HMAC KDF). -pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION: u8 = b'M'; // intermediate keys used in key exchanges +pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION: u8 = b'X'; // intermediate keys used in key exchanges +pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION: u8 = b'x'; // intermediate keys used in key exchanges pub(crate) const KBKDF_KEY_USAGE_LABEL_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 diff --git a/zssp/src/error.rs b/zssp/src/error.rs index 20ab254cc..0e25c56ac 100644 --- a/zssp/src/error.rs +++ b/zssp/src/error.rs @@ -43,6 +43,14 @@ pub enum Error { UnexpectedBufferOverrun, } +// An I/O error in the parser means an invalid packet. +impl From for Error { + #[inline(always)] + fn from(_: std::io::Error) -> Self { + Self::InvalidPacket + } +} + impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/zssp/src/lib.rs b/zssp/src/lib.rs index cad124c93..87ee92200 100644 --- a/zssp/src/lib.rs +++ b/zssp/src/lib.rs @@ -10,4 +10,4 @@ pub mod constants; pub use crate::applicationlayer::ApplicationLayer; pub use crate::error::Error; pub use crate::sessionid::SessionId; -pub use crate::zssp::{ReceiveContext, ReceiveResult, Role, Session}; +pub use crate::zssp::{ReceiveContext, ReceiveResult, Session}; diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index ac204a72d..17929854c 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -1,15 +1,13 @@ use std::mem::size_of; use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_PUBLICKEYBYTES}; -use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE; +use zerotier_crypto::hash::{HMAC_SHA384_SIZE, SHA384_HASH_SIZE}; +use zerotier_crypto::p384::{P384_PUBLIC_KEY_SIZE, P384_SECRET_KEY_SIZE}; use crate::applicationlayer::ApplicationLayer; use crate::constants::{AES_GCM_TAG_SIZE, HEADER_SIZE, MIN_PACKET_SIZE, SESSION_ID_SIZE}; use crate::error::Error; -/// Maximum packet size for handshake packets -/// -/// Packed structs are padded to this size so they can be recast to byte arrays of this size. pub(crate) const NOISE_MAX_HANDSHAKE_PACKET_SIZE: usize = 2048; pub(crate) const PACKET_TYPE_DATA: u8 = 0; @@ -19,33 +17,25 @@ pub(crate) const PACKET_TYPE_ALICE_STATIC_ACK: u8 = 3; pub(crate) const PACKET_TYPE_ALICE_REKEY_INIT: u8 = 4; pub(crate) const PACKET_TYPE_BOB_REKEY_ACK: u8 = 5; -pub(crate) const NOISE_XK_ALICE_EPHEMERAL_OFFER_ENCRYPTED_SECTION_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; -pub(crate) const NOISE_XK_ALICE_EPHEMERAL_OFFER_ENCRYPTED_SECTION_END: usize = - NOISE_XK_ALICE_EPHEMERAL_OFFER_ENCRYPTED_SECTION_START + SESSION_ID_SIZE + KYBER_PUBLICKEYBYTES + 8; -pub(crate) const NOISE_XK_ALICE_EPHEMERAL_OFFER_SIZE: usize = NOISE_XK_ALICE_EPHEMERAL_OFFER_ENCRYPTED_SECTION_END + AES_GCM_TAG_SIZE; - -pub(crate) trait ProtocolFlatBuffer {} - #[allow(unused)] #[repr(C, packed)] pub(crate) struct NoiseXKAliceEphemeralOffer { pub header: [u8; HEADER_SIZE], pub session_protocol_version: u8, + pub reserved: [u8; 8], pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE], - // -- start AES-GCM(es) encrypted section (IV is first 12 bytes of SHA384(alice_noise_e)) + // -- start AES-CTR(es) encrypted section (IV is first 12 bytes of SHA384(alice_noise_e)) pub alice_session_id: [u8; SESSION_ID_SIZE], pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES], pub salt: [u8; 8], // -- end encrypted section - pub gcm_mac: [u8; 16], - _padding: [u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE - NOISE_XK_ALICE_EPHEMERAL_OFFER_SIZE], + pub hmac_es: [u8; HMAC_SHA384_SIZE], } -pub(crate) const NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; -pub(crate) const NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_END: usize = - NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_START + SESSION_ID_SIZE + KYBER_CIPHERTEXTBYTES; -pub(crate) const NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_SIZE: usize = - NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_END + AES_GCM_TAG_SIZE; +impl NoiseXKAliceEphemeralOffer { + pub const ENC_START: usize = HEADER_SIZE + 1 + 8 + P384_PUBLIC_KEY_SIZE; + pub const AUTH_START: usize = size_of::() - HMAC_SHA384_SIZE; +} #[allow(unused)] #[repr(C, packed)] @@ -53,31 +43,62 @@ pub(crate) struct NoiseXKBobEphemeralCounterOffer { pub header: [u8; HEADER_SIZE], pub session_protocol_version: u8, pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE], - // -- start AES-GCM(es_ee) encrypted section (IV is first 12 bytes of SHA384(bob_noise_e)) - pub bob_session_id: [u8; SESSION_ID_SIZE], + // -- start AES-CTR(es_ee) encrypted section (IV is first 12 bytes of SHA384(bob_noise_e)) pub bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES], + pub bob_note_to_self: [u8; size_of::()], // -- end encrypted sectiion - pub gcm_mac: [u8; 16], - _padding: [u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE - NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_SIZE], + pub hmac_es_ee: [u8; HMAC_SHA384_SIZE], } -/* -pub(crate) const NOISE_XK_ALICE_STATIC_ACK_FIXED_FIELDS_SIZE: usize = HEADER_SIZE + 1; +impl NoiseXKBobEphemeralCounterOffer { + pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; + pub const AUTH_START: usize = size_of::() - HMAC_SHA384_SIZE; +} +#[allow(unused)] +#[repr(C, packed)] +pub(crate) struct BobNoteToSelf { + pub iv: [u8; 16], + // -- start AES-GCM encrypted section using ephemeral secret known only to Bob + pub timestamp: [u8; 8], + pub alice_session_id: [u8; SESSION_ID_SIZE], + pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE], + pub bob_noise_e_secret: [u8; P384_SECRET_KEY_SIZE], + pub hk: [u8; 32], + pub noise_es_ee: [u8; 64], + // -- end encrypted sectiion + pub gcm_mac: [u8; AES_GCM_TAG_SIZE], +} + +impl BobNoteToSelf { + pub const IV_SIZE: usize = 16; + pub const ENC_START: usize = Self::IV_SIZE; + pub const AUTH_START: usize = size_of::() - AES_GCM_TAG_SIZE; +} + +// These are variable length and so they're only here for documentation purposes. + +pub(crate) const NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_START: usize = HEADER_SIZE + 1; +pub(crate) const NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_END: usize = HEADER_SIZE + 1 + size_of::(); +pub(crate) const NOISE_XK_ALICE_STATIC_ACK_ENCRYPTED_SECTION_START: usize = NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_END; +pub(crate) const NOISE_XK_ALICE_STATIC_ACK_MIN_SIZE: usize = + NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_END + 2 + 2 + HMAC_SHA384_SIZE + HMAC_SHA384_SIZE; + +/* #[allow(unused)] #[repr(C, packed)] pub(crate) struct NoiseXKAliceStaticAck { pub header: [u8; HEADER_SIZE], pub session_protocol_version: u8, - // -- start AES-GCM(es_ee) encrypted section (IV is first 12 bytes of SHA384(hk)) - _var_length_fields_and_padding: [u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE - NOISE_XK_ALICE_STATIC_ACK_FIXED_FIELDS_SIZE], - // alice_static_blob_length: u16, - // alice_static_blob: [u8; ???], - // alice_metadata_length: u16, - // alice_metadata: [u8; ???], - // hmac_es_ee_se_hk_psk: [u8; HMAC_SHA384_SIZE], + pub bob_note_to_self: [u8; size_of::()], + // -- start AES-CTR(es_ee) encrypted section (IV is first 12 bytes of SHA384(hk)) + pub alice_static_blob_length: [u8; 2], + pub alice_static_blob: [u8; ???], + pub alice_metadata_length: [u8; 2], + pub alice_metadata: [u8; ???], // -- end encrypted section - // pub gcm_mac: [u8; 16], + pub hmac_es_ee: [u8; HMAC_SHA384_SIZE], + pub hmac_es_ee_se_hk_psk: [u8; HMAC_SHA384_SIZE], } */ @@ -86,10 +107,9 @@ pub(crate) struct NoiseXKAliceStaticAck { pub(crate) struct AliceRekeyInit { pub header: [u8; HEADER_SIZE], // -- start AES-GCM encrypted portion (using current key) - pub alice_e: [u8; P384_PUBLIC_KEY_SIZE], - pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES], + pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE], // -- end AES-GCM encrypted portion - pub gcm_mac: [u8; 16], + pub gcm_mac: [u8; AES_GCM_TAG_SIZE], } #[allow(unused)] @@ -97,21 +117,12 @@ pub(crate) struct AliceRekeyInit { pub(crate) struct BobRekeyAck { pub header: [u8; HEADER_SIZE], // -- start AES-GCM encrypted portion (using current key) - pub bob_e: [u8; P384_PUBLIC_KEY_SIZE], - pub bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES], + pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE], + pub ee_fingerprint: [u8; SHA384_HASH_SIZE], // -- end AES-GCM encrypted portion - pub gcm_mac: [u8; 16], + pub gcm_mac: [u8; AES_GCM_TAG_SIZE], } -// Annotate only these structs as being compatible with packet_buffer_as_bytes(). These structs -// are packed flat buffers containing only byte or byte array fields, making them safe to treat -// this way even on architectures that require type size aligned access. -impl ProtocolFlatBuffer for NoiseXKAliceEphemeralOffer {} -impl ProtocolFlatBuffer for NoiseXKBobEphemeralCounterOffer {} -//impl ProtocolFlatBuffer for NoiseXKAliceStaticAck {} -impl ProtocolFlatBuffer for AliceRekeyInit {} -impl ProtocolFlatBuffer for BobRekeyAck {} - /// 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; @@ -133,22 +144,31 @@ pub(crate) fn assemble_fragments_into(fragments: &[A::Incom return Ok(l); } -// Down here is where the only unsafe code here lives. It's instrumented with assertions wherever -// possible and just helps us efficiently cast to/from flat buffers. +// Annotate only these structs as being compatible with packet_buffer_as_bytes(). These structs +// are packed flat buffers containing only byte or byte array fields, making them safe to treat +// this way even on architectures that require type size aligned access. +pub(crate) trait ProtocolFlatBuffer {} +impl ProtocolFlatBuffer for NoiseXKAliceEphemeralOffer {} +impl ProtocolFlatBuffer for NoiseXKBobEphemeralCounterOffer {} +impl ProtocolFlatBuffer for BobNoteToSelf {} +//impl ProtocolFlatBuffer for NoiseXKAliceStaticAck {} +impl ProtocolFlatBuffer for AliceRekeyInit {} +impl ProtocolFlatBuffer for BobRekeyAck {} #[inline(always)] -pub(crate) fn new_packet_buffer() -> B { - unsafe { std::mem::zeroed() } +pub(crate) fn byte_array_as_proto_buffer(b: &[u8]) -> Result<&B, Error> { + if b.len() >= size_of::() { + Ok(unsafe { &*b.as_ptr().cast() }) + } else { + Err(Error::InvalidPacket) + } } #[inline(always)] -pub(crate) fn packet_buffer_as_bytes(b: &B) -> &[u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE] { - assert_eq!(size_of::(), NOISE_MAX_HANDSHAKE_PACKET_SIZE); - unsafe { &*(b as *const B).cast() } -} - -#[inline(always)] -pub(crate) fn packet_buffer_as_bytes_mut(b: &mut B) -> &mut [u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE] { - assert_eq!(size_of::(), NOISE_MAX_HANDSHAKE_PACKET_SIZE); - unsafe { &mut *(b as *mut B).cast() } +pub(crate) fn byte_array_as_proto_buffer_mut(b: &mut [u8]) -> Result<&mut B, Error> { + if b.len() >= size_of::() { + Ok(unsafe { &mut *b.as_mut_ptr().cast() }) + } else { + Err(Error::InvalidPacket) + } } diff --git a/zssp/src/sessionid.rs b/zssp/src/sessionid.rs index d0b6c2e42..22a4bc697 100644 --- a/zssp/src/sessionid.rs +++ b/zssp/src/sessionid.rs @@ -12,6 +12,7 @@ use crate::constants::SESSION_ID_SIZE; pub struct SessionId(NonZeroU64); // stored little endian internally impl SessionId { + pub const NONE: u64 = 0; pub const MAX: u64 = 0xffffffffffff; /// Create a new session ID, panicing if 'i' is zero or exceeds MAX. diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 6b90e80f0..b06c0b804 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -3,24 +3,23 @@ // ZSSP: ZeroTier Secure Session Protocol // FIPS compliant Noise_IK with Jedi powers and built-in attack-resistant large payload (fragmentation) support. -use std::io::Write; +use std::mem::size_of; +use std::num::NonZeroU64; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, RwLock}; use pqc_kyber::KYBER_SECRETKEYBYTES; -use zerotier_crypto::aes::{Aes, AesGcm}; -use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, SHA384}; -use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE}; -use zerotier_crypto::random; +use zerotier_crypto::aes::{Aes, AesCtr, AesGcm}; +use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384, SHA384_HASH_SIZE}; +use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; use zerotier_crypto::secret::Secret; -use zerotier_crypto::secure_eq; +use zerotier_crypto::{random, secure_eq}; use zerotier_utils::gatherarray::GatherArray; use zerotier_utils::memory; use zerotier_utils::ringbuffermap::RingBufferMap; use zerotier_utils::unlikely_branch; -use zerotier_utils::varint; use crate::applicationlayer::ApplicationLayer; use crate::constants::*; @@ -54,7 +53,6 @@ pub enum ReceiveResult<'a, H: ApplicationLayer> { /// with a single listen socket, local bound port, or other inbound endpoint. pub struct ReceiveContext { initial_offer_defrag: Mutex, 1024, 128>>, - incoming_init_header_check_cipher: Aes, } /// A FIPS compliant variant of Noise_IK with hybrid Kyber1024 PQ data forward secrecy. @@ -65,80 +63,35 @@ pub struct Session { /// An arbitrary application defined object associated with each session pub application_data: Application::Data, + psk: Secret<64>, // External PSK provided by application send_counter: AtomicU64, // Outgoing packet counter and nonce state receive_window: [AtomicU64; COUNTER_WINDOW_MAX_OUT_OF_ORDER], // Receive window for anti-replay and deduplication - header_check_cipher: Aes, // Cipher used for header check codes (not Noise related) + header_check_cipher: Aes, // Cipher used to protect header (not Noise related) offer: Mutex, // Most recently sent ephemeral offer state: RwLock, // Miscellaneous mutable state - //psk: Secret<64>, // Arbitrary PSK provided by external code - //noise_ss: Secret<48>, // Static raw shared ECDH NIST P-384 key - //state: RwLock, // Mutable parts of state (other than defrag buffers) - //remote_s_public_blob_hash: [u8; 48], // SHA384(remote static public key blob) - //remote_s_public_p384_bytes: [u8; P384_PUBLIC_KEY_SIZE], // Remote NIST P-384 static public key defrag: Mutex, 8, 8>>, } enum EphemeralOffer { None, - Alice(Secret<64>, P384KeyPair, [u8; KYBER_SECRETKEYBYTES]), - Bob(Secret<64>, P384KeyPair), + AliceNoiseXKInit(Secret<64>, P384KeyPair, [u8; KYBER_SECRETKEYBYTES]), // noise_es, alice_noise_e_secret, alice_hk_secret } struct State { keys: [Option; 2], current_key: usize, - psk: Secret<64>, } -/* -struct SessionMutableState { - remote_session_id: Option, // The other side's 48-bit session ID - session_keys: [Option; 2], // Buffers to store last and latest key by 1-bit key index - cur_session_key_idx: usize, // Pointer to latest session key other side is confirmed to have - offer: Option, // Most recent ephemeral offer sent to remote - last_remote_offer: i64, // Time of most recent ephemeral offer (ms) -} -*/ - -/// A shared symmetric session key. struct SessionKey { - ratchet_count: u64, // Number of preceding session keys in ratchet - rekey_at_time: i64, // Rekey at or after this time (ticks) - rekey_at_counter: u64, // Rekey at or after this counter - expire_at_counter: u64, // Hard error when this counter value is reached or exceeded - secret_fingerprint: [u8; 16], // First 128 bits of a SHA384 computed from the secret - ratchet_key: Secret<64>, // Ratchet key for deriving the next session key receive_key: Secret, // Receive side AES-GCM key send_key: Secret, // Send side AES-GCM key receive_cipher_pool: Mutex>>, // Pool of reusable sending ciphers send_cipher_pool: Mutex>>, // Pool of reusable receiving ciphers - role: Role, // Was this side Alice or Bob? + rekey_at_time: i64, // Rekey at or after this time (ticks) + rekey_at_counter: u64, // Rekey at or after this counter + expire_at_counter: u64, // Hard error when this counter value is reached or exceeded confirmed: bool, // We have confirmed that the other side has this key - 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 or None if first offer - ss_key: Secret<64>, // Noise session key "under construction" at state after offer sent - alice_e_keypair: P384KeyPair, // NIST P-384 key pair (Noise ephemeral key for Alice) - alice_hk_keypair: Option, // Kyber1024 key pair (PQ hybrid ephemeral key for Alice) -} -*/ - -/// Was this side the one who sent the first offer (Alice) or countered (Bob). -/// -/// Note that the role can switch through the course of a session. It's the side that most recently -/// initiated a session or a rekey event. Initiator is Alice, responder is Bob. -#[derive(Clone, Copy)] -pub enum Role { - Alice, - Bob, } impl Session { @@ -355,6 +308,12 @@ impl Session { */ } + /// Get the next outgoing counter value. + #[inline(always)] + fn get_next_outgoing_counter(&self) -> Option { + NonZeroU64::new(self.send_counter.fetch_add(1, Ordering::SeqCst)) + } + /// Check the receive window without mutating state. #[inline(always)] fn check_receive_window(&self, counter: u64) -> bool { @@ -372,12 +331,9 @@ impl Session { } impl ReceiveContext { - pub fn new(app: &Application) -> Self { + pub fn new(_: &Application) -> Self { Self { initial_offer_defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())), - incoming_init_header_check_cipher: Aes::new( - kbkdf512(app.get_local_s_public_blob_hash(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::(), - ), } } @@ -467,10 +423,8 @@ impl ReceiveContext { return Err(Error::UnknownLocalSessionId(local_session_id)); } } else { - unlikely_branch(); // we want data receive to be the priority branch, this is only occasionally used + unlikely_branch(); // the data path should always be the 'hot' branch - self.incoming_init_header_check_cipher - .decrypt_block_in_place(&mut incoming_packet[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); let raw_header_a = u16::from_le(memory::load_raw(&incoming_packet[6..])); let key_index = (raw_header_a & 1) as usize; let packet_type = (raw_header_a.wrapping_shr(1) & 7) as u8; @@ -625,153 +579,389 @@ impl ReceiveContext { } else { unlikely_branch(); + // For KEX packets go ahead and pre-assemble all fragments to simplify the code below. + let mut pkt_assembly_buffer = [0u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE]; + let pkt_assembled_size = assemble_fragments_into::(fragments, &mut pkt_assembly_buffer)?; + if pkt_assembled_size < MIN_PACKET_SIZE { + return Err(Error::InvalidPacket); + } + let pkt_assembled = &mut pkt_assembly_buffer[..pkt_assembled_size]; + if pkt_assembled[HEADER_SIZE] != SESSION_PROTOCOL_VERSION { + return Err(Error::UnknownProtocolVersion); + } + match packet_type { PACKET_TYPE_ALICE_EPHEMERAL_OFFER => { // Alice (remote) --> Bob (local) - let mut pkt: NoiseXKAliceEphemeralOffer = new_packet_buffer(); - if assemble_fragments_into::(fragments, packet_buffer_as_bytes_mut(&mut pkt))? - != NOISE_XK_ALICE_EPHEMERAL_OFFER_SIZE - { - return Err(Error::InvalidPacket); - } - if pkt.session_protocol_version != SESSION_PROTOCOL_VERSION { - return Err(Error::UnknownProtocolVersion); + /* + * This is the first message Bob receives from Alice, the initiator. It contains + * Alice's ephemeral keys but not her identity. Bob responds with his ephemeral + * keys. An opaque sealed object called Bob's "note to self" is also sent. Alice + * must return it with her final key exchange message. It contains Bob's state + * for the exchange to this point (encrypted and authenticated) so that Bob does + * not need to allocate any memory or mutate local state until Alice's identity + * is known and confirmed. + */ + + // There shouldn't be a session yet on Bob's end. + if session.is_some() { + return Ok(ReceiveResult::Ignored); } + let pkt: &NoiseXKAliceEphemeralOffer = byte_array_as_proto_buffer(pkt_assembled)?; + let alice_noise_e = P384PublicKey::from_bytes(&pkt.alice_noise_e).ok_or(Error::FailedAuthentication)?; let noise_es = app.get_local_s_keypair().agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?; - let mut gcm = AesGcm::new( - &kbkdf512(noise_es.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION).as_bytes()[..AES_KEY_SIZE], - false, - ); - gcm.reset_init_gcm(&SHA384::hash(&pkt.alice_noise_e)[..AES_GCM_NONCE_SIZE]); - gcm.crypt_in_place( - &mut packet_buffer_as_bytes_mut(&mut pkt) - [NOISE_XK_ALICE_EPHEMERAL_OFFER_ENCRYPTED_SECTION_START..NOISE_XK_ALICE_EPHEMERAL_OFFER_ENCRYPTED_SECTION_END], - ); - if !gcm.finish_decrypt(&pkt.gcm_mac) { + // Authenticate packet and prove that Alice knows our identity. + if !secure_eq( + &pkt.hmac_es, + &hmac_sha384_2( + &kbkdf512(noise_es.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION).as_bytes()[..HMAC_SHA384_SIZE], + &message_nonce, + &pkt_assembled[HEADER_SIZE..NoiseXKAliceEphemeralOffer::AUTH_START], + ), + ) { return Err(Error::FailedAuthentication); } + // Decrypt encrypted part of payload. + let mut ctr = AesCtr::new( + &kbkdf512(noise_es.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION).as_bytes()[..AES_KEY_SIZE], + ); + ctr.reset_set_iv(&SHA384::hash(&pkt.alice_noise_e)[..AES_CTR_NONCE_SIZE]); + ctr.crypt_in_place(&mut pkt_assembled[NoiseXKAliceEphemeralOffer::ENC_START..NoiseXKAliceEphemeralOffer::AUTH_START]); + let pkt: &NoiseXKAliceEphemeralOffer = byte_array_as_proto_buffer(pkt_assembled)?; + let alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?; + + // Create Bob's ephemeral keys and derive noise_es_ee. let (bob_hk_ciphertext, hk) = pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default()) .map_err(|_| Error::FailedAuthentication)?; - - let (bob_session_id, application_data) = app.new_session(remote_address).ok_or(Error::NewSessionRejected)?; - let bob_e_secret = P384KeyPair::generate(); - + let bob_noise_e_secret = P384KeyPair::generate(); let noise_es_ee = Secret(hmac_sha512( noise_es.as_bytes(), - bob_e_secret.agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?.as_bytes(), + bob_noise_e_secret + .agree(&alice_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), )); - let mut reply: NoiseXKBobEphemeralCounterOffer = new_packet_buffer(); + // Create Bob's ephemeral counter-offer reply. + let mut reply_buffer = [0u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE]; + let reply: &mut NoiseXKBobEphemeralCounterOffer = byte_array_as_proto_buffer_mut(&mut reply_buffer)?; reply.session_protocol_version = SESSION_PROTOCOL_VERSION; - reply.bob_noise_e = bob_e_secret.public_key_bytes().clone(); - reply.bob_session_id = bob_session_id.as_bytes().clone(); + reply.bob_noise_e = bob_noise_e_secret.public_key_bytes().clone(); reply.bob_hk_ciphertext = bob_hk_ciphertext; - let mut gcm = AesGcm::new( - &kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION).as_bytes()[..AES_KEY_SIZE], - true, - ); - gcm.reset_init_gcm(&SHA384::hash(bob_e_secret.public_key_bytes())[..AES_GCM_NONCE_SIZE]); - gcm.crypt_in_place( - &mut packet_buffer_as_bytes_mut(&mut reply)[NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_START - ..NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_END], - ); - reply.gcm_mac = gcm.finish_encrypt(); + // Create, encrypt, and tag Bob's opaque "note to self" that Alice must return. + let bob_note_to_self: &mut BobNoteToSelf = byte_array_as_proto_buffer_mut(&mut reply.bob_note_to_self)?; + bob_note_to_self.iv = random::get_bytes_secure(); + bob_note_to_self.timestamp = current_time.to_le_bytes(); + bob_note_to_self.alice_session_id = *alice_session_id.as_bytes(); + bob_note_to_self.bob_noise_e = *bob_noise_e_secret.public_key_bytes(); + bob_note_to_self.bob_noise_e_secret = *bob_noise_e_secret.secret_key_bytes().as_bytes(); + bob_note_to_self.hk = hk; + bob_note_to_self.noise_es_ee = *noise_es_ee.as_bytes(); + let mut gcm = get_note_to_self_cipher(true, &bob_note_to_self.iv); + gcm.crypt_in_place(&mut reply.bob_note_to_self[BobNoteToSelf::ENC_START..BobNoteToSelf::AUTH_START]); + reply.bob_note_to_self[BobNoteToSelf::AUTH_START..].copy_from_slice(&gcm.finish_encrypt()); + // Encrypt encrypted part of reply packet. + let mut ctr = AesCtr::new( + &kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION).as_bytes()[..AES_KEY_SIZE], + ); + ctr.reset_set_iv(&SHA384::hash(bob_noise_e_secret.public_key_bytes())[..AES_CTR_NONCE_SIZE]); + ctr.crypt_in_place( + &mut reply_buffer[NoiseXKBobEphemeralCounterOffer::ENC_START..NoiseXKBobEphemeralCounterOffer::AUTH_START], + ); + + // Add HMAC-SHA384 to reply packet, allowing Alice to derive noise_es_ee and authenticate. + let reply_hmac = hmac_sha384_2( + &kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION).as_bytes()[..HMAC_SHA384_SIZE], + &create_message_nonce(PACKET_TYPE_BOB_EPHEMERAL_COUNTER_OFFER, 1), + &reply_buffer[HEADER_SIZE..NoiseXKBobEphemeralCounterOffer::AUTH_START], + ); + reply_buffer[NoiseXKBobEphemeralCounterOffer::AUTH_START..].copy_from_slice(&reply_hmac); + + // This sub-key is used to protect the framing/fragmentation protocol, not Noise related. let header_check_cipher = Aes::new( - &kbkdf512(noise_es.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).as_bytes()[..AES_HEADER_CHECK_KEY_SIZE], + &kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).as_bytes()[..AES_HEADER_CHECK_KEY_SIZE], ); send_with_fragmentation( send, - &mut packet_buffer_as_bytes_mut(&mut reply)[..NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_SIZE], + &mut reply_buffer[..size_of::()], mtu, PACKET_TYPE_BOB_EPHEMERAL_COUNTER_OFFER, - u64::from(alice_session_id), + Some(alice_session_id), 0, 1, - &header_check_cipher, + Some(&header_check_cipher), )?; - return Ok(ReceiveResult::OkNewSession(Session { - id: bob_session_id, - application_data, - send_counter: AtomicU64::new(2), // 1 was used in reply - receive_window: std::array::from_fn(|_| AtomicU64::new(0)), - header_check_cipher, - offer: Mutex::new(EphemeralOffer::Bob(noise_es_ee, bob_e_secret)), - state: RwLock::new(State { keys: [None, None], current_key: 0, psk: Secret::default() }), - defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)), - })); + return Ok(ReceiveResult::Ok); } PACKET_TYPE_BOB_EPHEMERAL_COUNTER_OFFER => { // Bob (remote) --> Alice (local) - if let Some(session) = session { - let mut pkt: NoiseXKBobEphemeralCounterOffer = new_packet_buffer(); - if assemble_fragments_into::(fragments, packet_buffer_as_bytes_mut(&mut pkt))? - != NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_SIZE - { - return Err(Error::InvalidPacket); - } - if pkt.session_protocol_version != SESSION_PROTOCOL_VERSION { - return Err(Error::UnknownProtocolVersion); - } + /* + * This is Bob's reply to Alice's first message, allowing Alice to verify Bob's + * identity. Once this is done Alice can send her identity (encrypted) to complete + * the negotiation. + */ + if let Some(session) = session { let mut offer = session.offer.lock().unwrap(); match &*offer { - EphemeralOffer::Alice(noise_es, alice_e_secret, alice_hk_secret) => { + EphemeralOffer::AliceNoiseXKInit(noise_es, alice_e_secret, alice_hk_secret) => { + let pkt: &NoiseXKBobEphemeralCounterOffer = byte_array_as_proto_buffer(pkt_assembled)?; + + // Derive noise_es_ee from Bob's ephemeral public key. let bob_noise_e = P384PublicKey::from_bytes(&pkt.bob_noise_e).ok_or(Error::FailedAuthentication)?; let noise_es_ee = Secret(hmac_sha512( noise_es.as_bytes(), alice_e_secret.agree(&bob_noise_e).ok_or(Error::FailedAuthentication)?.as_bytes(), )); - let noise_es_ee_se = app.get_local_s_keypair().agree(&bob_noise_e).ok_or(Error::FailedAuthentication)?; - let mut gcm = AesGcm::new( - &kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION).as_bytes() - [..AES_KEY_SIZE], - false, - ); - gcm.reset_init_gcm(&SHA384::hash(&pkt.bob_noise_e)[..AES_GCM_NONCE_SIZE]); - gcm.crypt_in_place( - &mut packet_buffer_as_bytes_mut(&mut pkt)[NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_START - ..NOISE_XK_BOB_EPHEMERAL_COUNTER_OFFER_ENCRYPTED_SECTION_END], - ); - if !gcm.finish_decrypt(&pkt.gcm_mac) { + let noise_es_ee_kex_key = kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_PAYLOAD_ENCRYPTION); + let noise_es_ee_kex_hmac_key = kbkdf512(noise_es_ee.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION); + + // Authenticate Bob's reply and the validity of bob_noise_e. + if !secure_eq( + &pkt.hmac_es_ee, + &hmac_sha384_2( + &noise_es_ee_kex_hmac_key.as_bytes()[..HMAC_SHA384_SIZE], + &message_nonce, + &pkt_assembled[HEADER_SIZE..NoiseXKAliceEphemeralOffer::AUTH_START], + ), + ) { return Err(Error::FailedAuthentication); } - let bob_session_id = SessionId::new_from_bytes(&pkt.bob_session_id).ok_or(Error::InvalidPacket)?; + // Decrypt encrypted portion after authentication. + let mut ctr = AesCtr::new(&noise_es_ee_kex_key.as_bytes()[..AES_KEY_SIZE]); + ctr.reset_set_iv(&SHA384::hash(&pkt.bob_noise_e)[..AES_CTR_NONCE_SIZE]); + ctr.crypt_in_place( + &mut pkt_assembled + [NoiseXKBobEphemeralCounterOffer::ENC_START..NoiseXKBobEphemeralCounterOffer::AUTH_START], + ); + let pkt: &NoiseXKBobEphemeralCounterOffer = byte_array_as_proto_buffer(pkt_assembled)?; + + // Noise_XKpsk3 specifies mixing a PSK last. The kyber1024 hybrid key is mixed into + // the static PSK and then this is treated as the PSK as far as Noise is concerned. let hk = pqc_kyber::decapsulate(&pkt.bob_hk_ciphertext, alice_hk_secret) .map_err(|_| Error::FailedAuthentication)?; + let noise_es_ee_se_hk_psk = Secret(hmac_sha512( + &hmac_sha512( + noise_es_ee.as_bytes(), + app.get_local_s_keypair() + .agree(&bob_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + ), + &hmac_sha512(session.psk.as_bytes(), &hk), + )); + let reply_counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; + let reply_message_nonce = create_message_nonce(PACKET_TYPE_ALICE_STATIC_ACK, reply_counter.get()); + + // Create reply informing Bob of our static identity now that we've verified Bob and set + // up forward secrecy. Also return Bob's opaque note. + let mut reply_buffer = [0u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE]; + reply_buffer[HEADER_SIZE] = SESSION_PROTOCOL_VERSION; + let mut reply_len = HEADER_SIZE + 1; + let mut reply_buffer_append = |b: &[u8]| { + let reply_len_new = reply_len + b.len(); + reply_buffer[reply_len..reply_len_new].copy_from_slice(b); + reply_len = reply_len_new; + }; + reply_buffer_append(&pkt.bob_note_to_self); let alice_s_public_blob = app.get_local_s_public_blob(); - - let mut reply = [0u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE]; - let mut reply_w = &mut reply[..]; assert!(alice_s_public_blob.len() <= (u16::MAX as usize)); - let _ = reply_w.write_all(&(alice_s_public_blob.len() as u16).to_le_bytes()); - let _ = reply_w.write_all(alice_s_public_blob); - let _ = reply_w.write_all(&[0u8, 0u8]); // zero size meta-data, to be implemented later - } - _ => { - return Ok(ReceiveResult::Ignored); + reply_buffer_append(&(alice_s_public_blob.len() as u16).to_le_bytes()); + reply_buffer_append(alice_s_public_blob); + reply_buffer_append(&[0u8, 0u8]); // no meta-data + + // Encrypt Alice's static identity and other inner payload items. + let mut ctr = AesCtr::new(&noise_es_ee_kex_key.as_bytes()[..AES_KEY_SIZE]); + ctr.reset_set_iv(&SHA384::hash(&hk)[..AES_CTR_NONCE_SIZE]); + ctr.crypt_in_place(&mut reply_buffer[NOISE_XK_ALICE_STATIC_ACK_ENCRYPTED_SECTION_START..reply_len]); + + // First attach HMAC allowing Bob to verify that this is from the same Alice and to + // verify the authenticity of encrypted data. + let hmac_es_ee = hmac_sha384_2( + &noise_es_ee_kex_hmac_key.as_bytes()[..HMAC_SHA384_SIZE], + &reply_message_nonce, + &reply_buffer[HEADER_SIZE..reply_len], + ); + reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee); + reply_len += HMAC_SHA384_SIZE; + + // Then attach the final HMAC permitting Bob to verify the authenticity of the whole + // key exchange. + let hmac_es_ee_se_hk_psk = hmac_sha384_2( + &kbkdf512(noise_es_ee_se_hk_psk.as_bytes(), KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION).as_bytes() + [..HMAC_SHA384_SIZE], + &reply_message_nonce, + &reply_buffer[HEADER_SIZE..reply_len], + ); + reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee_se_hk_psk); + reply_len += HMAC_SHA384_SIZE; + + send_with_fragmentation( + send, + &mut reply_buffer[..reply_len], + mtu, + PACKET_TYPE_ALICE_STATIC_ACK, + None, + 0, + reply_counter.get(), + None, + )?; + + *offer = EphemeralOffer::None; + + return Ok(ReceiveResult::Ok); } + _ => return Ok(ReceiveResult::Ignored), } } else { return Err(Error::SessionNotEstablished); } } - PACKET_TYPE_ALICE_STATIC_ACK => {} + PACKET_TYPE_ALICE_STATIC_ACK => { + // Alice (remote) --> Bob (local) + + /* + * After negotiating a keyed session and Alice has had the opportunity to + * verify Bob, this is when Bob gets to learn who Alice is. At this point + * Bob can make a final decision about whether to keep talking to Alice + * and can create an actual session using the state memo-ized in the memo + * that Alice must return. + */ + + // There shouldn't be a session yet on Bob's end. + if session.is_some() { + return Ok(ReceiveResult::Ignored); + } + + if pkt_assembled.len() < NOISE_XK_ALICE_STATIC_ACK_MIN_SIZE { + return Err(Error::InvalidPacket); + } + + // Decrypt and authenticate our "note to self." + let mut bob_note_to_self_buffer = Secret([0u8; size_of::()]); + let mut gcm = get_note_to_self_cipher( + false, + &pkt_assembled + [NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_START..NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_START + 16], + ); + gcm.crypt( + &pkt_assembled[NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_START + 16 + ..NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_END - AES_GCM_TAG_SIZE], + &mut bob_note_to_self_buffer.as_bytes_mut()[BobNoteToSelf::ENC_START..BobNoteToSelf::AUTH_START], + ); + if !gcm.finish_decrypt( + &pkt_assembled[NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_END - AES_GCM_TAG_SIZE + ..NOISE_XK_ALICE_STATIC_ACK_BOB_NOTE_TO_SELF_END], + ) { + return Err(Error::FailedAuthentication); + } + let bob_note_to_self: &BobNoteToSelf = byte_array_as_proto_buffer(bob_note_to_self_buffer.as_bytes())?; + if (current_time - i64::from_le_bytes(bob_note_to_self.timestamp)) >= BOB_NOTE_TO_SELF_TIMEOUT_MS { + return Err(Error::FailedAuthentication); + } + + // Restore state from when Alice first tried to contact us. + 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); + drop(bob_note_to_self_buffer); + + let pkt_assembled_enc_end = pkt_assembled.len() - (HMAC_SHA384_SIZE * 2); + + // Authenticate packet with noise_es_ee (first HMAC) before decrypting and parsing static info. + 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); + } + + 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_PAYLOAD_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[NOISE_XK_ALICE_STATIC_ACK_ENCRYPTED_SECTION_START..pkt_assembled_enc_end]); + + let mut alice_static_info = &pkt_assembled[NOISE_XK_ALICE_STATIC_ACK_ENCRYPTED_SECTION_START..pkt_assembled_enc_end]; + if alice_static_info.len() < 2 { + 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) + { + 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()), + )); + + // Final authentication with the whole enchelada. + if !secure_eq( + &pkt_assembled[pkt_assembled_enc_end + 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_assembled[HEADER_SIZE..pkt_assembled_enc_end + HMAC_SHA384_SIZE], + ), + ) { + return Err(Error::FailedAuthentication); + } + } else { + return Err(Error::NewSessionRejected); + } + } PACKET_TYPE_ALICE_REKEY_INIT => {} @@ -1277,6 +1467,7 @@ impl ReceiveContext { } } +/* /// Create an send an ephemeral offer, populating ret_ephemeral_offer on success. fn send_ephemeral_offer( send: &mut SendFunction, @@ -1422,6 +1613,7 @@ fn send_ephemeral_offer( Ok(()) } +*/ fn set_packet_header( packet: &mut [u8], @@ -1486,12 +1678,13 @@ fn send_with_fragmentation( packet: &mut [u8], mtu: usize, packet_type: u8, - recipient_session_id: u64, + recipient_session_id: Option, ratchet_count: u64, counter: u64, - header_check_cipher: &Aes, + header_check_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)); let fragment_count = ((packet_len as f32) / (mtu as f32)).ceil() as usize; let mut fragment_start = 0; let mut fragment_end = packet_len.min(mtu); @@ -1506,7 +1699,9 @@ fn send_with_fragmentation( ratchet_count, counter, )?; - header_check_cipher.encrypt_block_in_place(&mut fragment[6..22]); + if let Some(hcc) = header_check_cipher { + hcc.encrypt_block_in_place(&mut fragment[6..22]); + } send(fragment); fragment_start = fragment_end - HEADER_SIZE; fragment_end = (fragment_start + mtu).min(packet_len); @@ -1514,6 +1709,7 @@ fn send_with_fragmentation( Ok(()) } +/* /// Parse KEY_OFFER and KEY_COUNTER_OFFER starting after the unencrypted public key part. fn parse_dec_key_offer_after_header( incoming_packet: &[u8], @@ -1666,6 +1862,7 @@ fn varint_safe_read(src: &mut &[u8]) -> Result { *src = b; Ok(v) } +*/ /// Shortcut to HMAC data split into two slices. fn hmac_sha384_2(key: &[u8], a: &[u8], b: &[u8]) -> [u8; 48] { @@ -1681,10 +1878,26 @@ fn kbkdf512(key: &[u8], label: u8) -> Secret<64> { Secret(hmac_sha512(key, &[0, 0, 0, 0, b'Z', b'T', label, 0, 0, 0, 0, 0x02, 0x00])) } -/// Get a hash of a secret that can be used as a public key fingerprint to check ratcheting during key exchange. -fn public_fingerprint_of_secret(key: &[u8]) -> [u8; 48] { - let mut tmp = SHA384::new(); - tmp.update(&[0xf0, 0x0d]); // arbitrary salt - tmp.update(key); - tmp.finish() +/// Get an initialized AesGcm cipher for Bob to encrypt or decrypt his "note to self." +/// This both creates the cipher and initializes it with an IV. +fn get_note_to_self_cipher(encrypt: bool, iv: &[u8]) -> AesGcm { + const NOTE_TO_SELF_MASTER_SECRET: RwLock>> = RwLock::new(None); + let msl_static = &NOTE_TO_SELF_MASTER_SECRET; + let msl = msl_static.read().unwrap(); + let s = if let Some(ms) = msl.as_ref() { + Secret(hmac_sha512(ms.as_bytes(), iv)) + } else { + drop(msl); + Secret(hmac_sha512( + msl_static + .write() + .unwrap() + .get_or_insert_with(|| Secret(random::get_bytes_secure())) + .as_bytes(), + iv, + )) + }; + let mut c = AesGcm::new(&s.as_bytes()[0..AES_KEY_SIZE], encrypt); + c.reset_init_gcm(&s.as_bytes()[AES_KEY_SIZE..AES_KEY_SIZE + AES_GCM_NONCE_SIZE]); + c }