A whole lot more Noise_XK work... exchange almost done.

This commit is contained in:
Adam Ierymenko 2023-01-20 20:21:42 -05:00
commit c97d5d28bb
10 changed files with 656 additions and 240 deletions

View file

@ -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<Crypter>);
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 {

View file

@ -27,3 +27,13 @@ pub fn secure_eq<A: AsRef<[u8]> + ?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()) };
}

View file

@ -37,6 +37,11 @@ impl<const L: usize> Secret<L> {
&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<const N: usize>(&self) -> &[u8; N] {

View file

@ -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<P384PublicKey>;
/// 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<P384PublicKey>;
/// 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<Self::SessionRef<'a>>;
/// Rate limit and check an attempted new session (called before accept_new_session).
fn check_new_session(&self, rc: &ReceiveContext<Self>, 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<Self>, 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<Self>,

View file

@ -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

View file

@ -43,6 +43,14 @@ pub enum Error {
UnexpectedBufferOverrun,
}
// An I/O error in the parser means an invalid packet.
impl From<std::io::Error> 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 {

View file

@ -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};

View file

@ -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::<NoiseXKAliceEphemeralOffer>() - 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::<BobNoteToSelf>()],
// -- 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::<NoiseXKBobEphemeralCounterOffer>() - 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::<BobNoteToSelf>() - 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::<BobNoteToSelf>();
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::<BobNoteToSelf>()],
// -- 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<A: ApplicationLayer>(fragments: &[A::IncomingPacketBuffer], d: &mut [u8]) -> Result<usize, Error> {
let mut l = 0;
@ -133,22 +144,31 @@ pub(crate) fn assemble_fragments_into<A: ApplicationLayer>(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: ProtocolFlatBuffer>() -> B {
unsafe { std::mem::zeroed() }
pub(crate) fn byte_array_as_proto_buffer<B: ProtocolFlatBuffer>(b: &[u8]) -> Result<&B, Error> {
if b.len() >= size_of::<B>() {
Ok(unsafe { &*b.as_ptr().cast() })
} else {
Err(Error::InvalidPacket)
}
}
#[inline(always)]
pub(crate) fn packet_buffer_as_bytes<B: ProtocolFlatBuffer>(b: &B) -> &[u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE] {
assert_eq!(size_of::<B>(), NOISE_MAX_HANDSHAKE_PACKET_SIZE);
unsafe { &*(b as *const B).cast() }
}
#[inline(always)]
pub(crate) fn packet_buffer_as_bytes_mut<B: ProtocolFlatBuffer>(b: &mut B) -> &mut [u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE] {
assert_eq!(size_of::<B>(), NOISE_MAX_HANDSHAKE_PACKET_SIZE);
unsafe { &mut *(b as *mut B).cast() }
pub(crate) fn byte_array_as_proto_buffer_mut<B: ProtocolFlatBuffer>(b: &mut [u8]) -> Result<&mut B, Error> {
if b.len() >= size_of::<B>() {
Ok(unsafe { &mut *b.as_mut_ptr().cast() })
} else {
Err(Error::InvalidPacket)
}
}

View file

@ -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.

View file

@ -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<H: ApplicationLayer> {
initial_offer_defrag: Mutex<RingBufferMap<u64, GatherArray<H::IncomingPacketBuffer, KEY_EXCHANGE_MAX_FRAGMENTS>, 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<Application: ApplicationLayer> {
/// 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<EphemeralOffer>, // Most recently sent ephemeral offer
state: RwLock<State>, // 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<SessionMutableState>, // 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<RingBufferMap<u64, GatherArray<Application::IncomingPacketBuffer, MAX_FRAGMENTS>, 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<SessionKey>; 2],
current_key: usize,
psk: Secret<64>,
}
/*
struct SessionMutableState {
remote_session_id: Option<SessionId>, // The other side's 48-bit session ID
session_keys: [Option<SessionKey>; 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<EphemeralOffer>, // 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<AES_KEY_SIZE>, // Receive side AES-GCM key
send_key: Secret<AES_KEY_SIZE>, // Send side AES-GCM key
receive_cipher_pool: Mutex<Vec<Box<AesGcm>>>, // Pool of reusable sending ciphers
send_cipher_pool: Mutex<Vec<Box<AesGcm>>>, // 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<Secret<64>>, // 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<pqc_kyber::Keypair>, // 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<Application: ApplicationLayer> Session<Application> {
@ -355,6 +308,12 @@ impl<Application: ApplicationLayer> Session<Application> {
*/
}
/// Get the next outgoing counter value.
#[inline(always)]
fn get_next_outgoing_counter(&self) -> Option<NonZeroU64> {
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<Application: ApplicationLayer> Session<Application> {
}
impl<Application: ApplicationLayer> ReceiveContext<Application> {
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::<HEADER_CHECK_AES_KEY_SIZE>(),
),
}
}
@ -467,10 +423,8 @@ impl<Application: ApplicationLayer> ReceiveContext<Application> {
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<Application: ApplicationLayer> ReceiveContext<Application> {
} 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::<Application>(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::<Application>(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::<NoiseXKBobEphemeralCounterOffer>()],
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::<Application>(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::<BobNoteToSelf>()]);
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<Application: ApplicationLayer> ReceiveContext<Application> {
}
}
/*
/// Create an send an ephemeral offer, populating ret_ephemeral_offer on success.
fn send_ephemeral_offer<SendFunction: FnMut(&mut [u8])>(
send: &mut SendFunction,
@ -1422,6 +1613,7 @@ fn send_ephemeral_offer<SendFunction: FnMut(&mut [u8])>(
Ok(())
}
*/
fn set_packet_header(
packet: &mut [u8],
@ -1486,12 +1678,13 @@ fn send_with_fragmentation<SendFunction: FnMut(&mut [u8])>(
packet: &mut [u8],
mtu: usize,
packet_type: u8,
recipient_session_id: u64,
recipient_session_id: Option<SessionId>,
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<SendFunction: FnMut(&mut [u8])>(
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<SendFunction: FnMut(&mut [u8])>(
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<u64, Error> {
*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<Option<Secret<64>>> = 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
}