Merge branch 'tetanus' into tetanus-vl2

This commit is contained in:
Adam Ierymenko 2023-03-08 15:34:00 -05:00
commit 5329910a56
6 changed files with 266 additions and 237 deletions

View file

@ -1,2 +1,21 @@
ZeroTier Secure Socket Protocol ZeroTier Secure Socket Protocol
====== ======
# Introduction
ZeroTier Secure Socket Protocol (ZSSP) is a [Noise](http://noiseprotocol.org) protocol implementation using NIST/FIPS/CfSC compliant cryptographic primitives plus post-quantum forward secrecy via [Kyber1024](https://pq-crystals.org/kyber/). It also includes built-in support for fragmentation and defragmentation of large messages with strong resistance against denial of service attacks targeted against the fragmentation protocol.
Specifically ZSSP implements the [Noise XK](http://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental) interactive handshake pattern which provides strong forward secrecy not only for data but for the identities of the two participants in the sesssion. The XK pattern was chosen instead of the more popular IK pattern used in popular Noise implementations like Wireguard due to ZeroTier identities being long lived and potentially tied to the real world identity of the user. As a result a Noise pattern providing identity forward secrecy was considered preferable as it offers some level of deniability for recorded traffic even after secrec key compromise.
Hybrid post-quantum forward secrecy using Kyber1024 is performed alongside Noise with the result being mixed in alongside an optional pre-shared key at the end of session negotiation.
ZSSP is designed for use in ZeroTier 2 but is payload-agnostic and could easily be adapted for use in other projects.
## Cryptographic Primitives Used
- AES-256-GCM: Authenticated encryption
- HMAC-SHA384: Key mixing, sub-key derivation in key-based KDF construction
- NIST P-384 ECDH: Elliptic curve key exchange during initial handshake and for periodic re-keying during the session
- Kyber1024: Quantum attack resistant lattice-based key exchange during initial handshake
- AES-256-ECB: Single 128-bit block encryption of header information to harden the fragmentation protocol against denial of service attack (see section on header protection)

View file

@ -47,14 +47,6 @@ pub enum Error {
UnexpectedBufferOverrun, 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 { impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self { f.write_str(match self {

View file

@ -31,9 +31,13 @@ impl<Fragment, const MAX_FRAGMENTS: usize> Drop for Assembled<Fragment, MAX_FRAG
impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> { impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> {
#[inline(always)] #[inline(always)]
pub fn new() -> Self { pub fn new() -> Self {
debug_assert!(MAX_FRAGMENTS <= 64); // These assertions should be optimized out at compile time and check to make sure
debug_assert_eq!(size_of::<MaybeUninit<Fragment>>(), size_of::<Fragment>()); // that the array of MaybeUninit<Fragment> can be freely cast into an array of
debug_assert_eq!( // Fragment. They also check that the maximum number of fragments is not too large
// for the fact that we use bits in a u64 to track which fragments are received.
assert!(MAX_FRAGMENTS <= 64);
assert_eq!(size_of::<MaybeUninit<Fragment>>(), size_of::<Fragment>());
assert_eq!(
size_of::<[MaybeUninit<Fragment>; MAX_FRAGMENTS]>(), size_of::<[MaybeUninit<Fragment>; MAX_FRAGMENTS]>(),
size_of::<[Fragment; MAX_FRAGMENTS]>() size_of::<[Fragment; MAX_FRAGMENTS]>()
); );
@ -47,10 +51,9 @@ impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> {
#[inline(always)] #[inline(always)]
pub fn assemble(&mut self, counter: u64, fragment: Fragment, fragment_no: u8, fragment_count: u8) -> Option<Assembled<Fragment, MAX_FRAGMENTS>> { pub fn assemble(&mut self, counter: u64, fragment: Fragment, fragment_no: u8, fragment_count: u8) -> Option<Assembled<Fragment, MAX_FRAGMENTS>> {
if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS {
debug_assert!((fragment_count as usize) <= MAX_FRAGMENTS);
debug_assert!((fragment_no as usize) < MAX_FRAGMENTS);
let mut have = self.have; let mut have = self.have;
// If the counter has changed, reset the structure to receive a new packet.
if counter != self.counter { if counter != self.counter {
self.counter = counter; self.counter = counter;
if needs_drop::<Fragment>() { if needs_drop::<Fragment>() {

View file

@ -61,6 +61,7 @@ fn alice_main(
let _ = alice_out.send(b.to_vec()); let _ = alice_out.send(b.to_vec());
}, },
TEST_MTU, TEST_MTU,
bob_app.identity_key.public_key_bytes(),
bob_app.identity_key.public_key(), bob_app.identity_key.public_key(),
Secret::default(), Secret::default(),
None, None,
@ -80,7 +81,7 @@ fn alice_main(
match context.receive( match context.receive(
alice_app, alice_app,
|| true, || true,
|s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), |s_public| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())),
|_, b| { |_, b| {
let _ = alice_out.send(b.to_vec()); let _ = alice_out.send(b.to_vec());
}, },
@ -96,7 +97,7 @@ fn alice_main(
Ok(zssp::ReceiveResult::OkData(_, _)) => { Ok(zssp::ReceiveResult::OkData(_, _)) => {
//println!("[alice] received {}", data.len()); //println!("[alice] received {}", data.len());
} }
Ok(zssp::ReceiveResult::OkNewSession(s)) => { Ok(zssp::ReceiveResult::OkNewSession(s, _)) => {
println!("[alice] new session {}", s.id.to_string()); println!("[alice] new session {}", s.id.to_string());
} }
Ok(zssp::ReceiveResult::Rejected) => {} Ok(zssp::ReceiveResult::Rejected) => {}
@ -178,7 +179,7 @@ fn bob_main(
match context.receive( match context.receive(
bob_app, bob_app,
|| true, || true,
|s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())), |s_public| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())),
|_, b| { |_, b| {
let _ = bob_out.send(b.to_vec()); let _ = bob_out.send(b.to_vec());
}, },
@ -204,7 +205,7 @@ fn bob_main(
.is_ok()); .is_ok());
transferred += data.len() as u64 * 2; // *2 because we are also sending this many bytes back transferred += data.len() as u64 * 2; // *2 because we are also sending this many bytes back
} }
Ok(zssp::ReceiveResult::OkNewSession(s)) => { Ok(zssp::ReceiveResult::OkNewSession(s, _)) => {
println!("[bob] new session {}", s.id.to_string()); println!("[bob] new session {}", s.id.to_string());
let _ = bob_session.replace(s); let _ = bob_session.replace(s);
} }

View file

@ -9,7 +9,7 @@
use std::mem::size_of; use std::mem::size_of;
use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_PUBLICKEYBYTES}; use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_PUBLICKEYBYTES};
use zerotier_crypto::hash::{HMAC_SHA384_SIZE, SHA384_HASH_SIZE}; use zerotier_crypto::hash::SHA384_HASH_SIZE;
use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE; use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE;
use crate::error::Error; use crate::error::Error;
@ -24,6 +24,13 @@ pub const MIN_TRANSPORT_MTU: usize = 128;
/// Maximum combined size of static public blob and metadata. /// Maximum combined size of static public blob and metadata.
pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_XK_ACK_MIN_SIZE; pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_XK_ACK_MIN_SIZE;
/// Initial value of 'h'
/// echo -n 'Noise_XKpsk3_P384_AESGCM_SHA384_hybridKyber1024' | shasum -a 384
pub(crate) const INITIAL_H: [u8; SHA384_HASH_SIZE] = [
0x35, 0x27, 0x16, 0x62, 0x58, 0x04, 0x0c, 0x7a, 0x99, 0xa8, 0x0b, 0x49, 0xb2, 0x6b, 0x25, 0xfb, 0xf5, 0x26, 0x2a, 0x26, 0xe7, 0xb3, 0x70, 0xcb,
0x2c, 0x3c, 0xcb, 0x7f, 0xca, 0x20, 0x06, 0x91, 0x20, 0x55, 0x52, 0x8e, 0xd4, 0x3c, 0x97, 0xc3, 0xd5, 0x6c, 0xb4, 0x13, 0x02, 0x54, 0x83, 0x12,
];
/// Version 0: Noise_XK with NIST P-384 plus Kyber1024 hybrid exchange on session init. /// Version 0: Noise_XK with NIST P-384 plus Kyber1024 hybrid exchange on session init.
pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00;
@ -45,8 +52,7 @@ pub(crate) const HEADER_SIZE: usize = 16;
pub(crate) const HEADER_PROTECT_ENCRYPT_START: usize = 6; pub(crate) const HEADER_PROTECT_ENCRYPT_START: usize = 6;
pub(crate) const HEADER_PROTECT_ENCRYPT_END: usize = 22; pub(crate) const HEADER_PROTECT_ENCRYPT_END: usize = 22;
pub(crate) const KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION: u8 = b'x'; // AES-CTR encryption during initial setup pub(crate) const KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION: u8 = b'x'; // AES-GCM encryption during initial setup
pub(crate) const KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION: u8 = b'X'; // HMAC-SHA384 during initial setup
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_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction
pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction
pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHET: u8 = b'R'; // Key used in derivatin of next session key pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHET: u8 = b'R'; // Key used in derivatin of next session key
@ -71,18 +77,18 @@ pub(crate) struct AliceNoiseXKInit {
pub header: [u8; HEADER_SIZE], pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8, pub session_protocol_version: u8,
pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE], pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE],
// -- start AES-CTR(es) encrypted section // -- start AES-GCM(es) encrypted section
pub alice_session_id: [u8; SessionId::SIZE], pub alice_session_id: [u8; SessionId::SIZE],
pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES], pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES],
pub header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE], pub header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE],
// -- end encrypted section // -- end encrypted section
pub hmac_es: [u8; HMAC_SHA384_SIZE], pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
} }
impl AliceNoiseXKInit { impl AliceNoiseXKInit {
pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE;
pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_PROTECTION_KEY_SIZE; pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_PROTECTION_KEY_SIZE;
pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE;
} }
/// The response to AliceNoiceXKInit containing Bob's ephemeral keys. /// The response to AliceNoiceXKInit containing Bob's ephemeral keys.
@ -92,17 +98,17 @@ pub(crate) struct BobNoiseXKAck {
pub header: [u8; HEADER_SIZE], pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8, pub session_protocol_version: u8,
pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE], pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE],
// -- start AES-CTR(es_ee) encrypted section // -- start AES-GCM(es_ee) encrypted section
pub bob_session_id: [u8; SessionId::SIZE], pub bob_session_id: [u8; SessionId::SIZE],
pub bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES], pub bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES],
// -- end encrypted sectiion // -- end encrypted sectiion
pub hmac_es_ee: [u8; HMAC_SHA384_SIZE], pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
} }
impl BobNoiseXKAck { impl BobNoiseXKAck {
pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE;
pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_CIPHERTEXTBYTES; pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_CIPHERTEXTBYTES;
pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE;
} }
/// Alice's final response containing her identity (she already knows Bob's) and meta-data. /// Alice's final response containing her identity (she already knows Bob's) and meta-data.
@ -112,20 +118,20 @@ impl BobNoiseXKAck {
pub(crate) struct AliceNoiseXKAck { pub(crate) struct AliceNoiseXKAck {
pub header: [u8; HEADER_SIZE], pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8, pub session_protocol_version: u8,
// -- start AES-CTR(es_ee_hk) encrypted section
pub alice_static_blob_length: [u8; 2], pub alice_static_blob_length: [u8; 2],
// -- start AES-GCM(es_ee_hk) encrypted section
pub alice_static_blob: [u8; ???], pub alice_static_blob: [u8; ???],
// -- end encrypted section
pub gcm_tag_0: [u8; AES_GCM_TAG_SIZE],
pub alice_metadata_length: [u8; 2], pub alice_metadata_length: [u8; 2],
// -- start AES-GCM(es_ee_se_hk_psk) encrypted section
pub alice_metadata: [u8; ???], pub alice_metadata: [u8; ???],
// -- end encrypted section // -- end encrypted section
pub hmac_es_ee: [u8; HMAC_SHA384_SIZE], pub gcm_tag_1: [u8; AES_GCM_TAG_SIZE],
pub hmac_es_ee_se_hk_psk: [u8; HMAC_SHA384_SIZE],
} }
*/ */
pub(crate) const ALICE_NOISE_XK_ACK_ENC_START: usize = HEADER_SIZE + 1; pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = HEADER_SIZE + 1 + 2 + AES_GCM_TAG_SIZE + 2 + AES_GCM_TAG_SIZE;
pub(crate) const ALICE_NOISE_XK_ACK_AUTH_SIZE: usize = HMAC_SHA384_SIZE + HMAC_SHA384_SIZE;
pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = ALICE_NOISE_XK_ACK_ENC_START + 2 + 2 + ALICE_NOISE_XK_ACK_AUTH_SIZE;
#[allow(unused)] #[allow(unused)]
#[repr(C, packed)] #[repr(C, packed)]
@ -135,7 +141,7 @@ pub(crate) struct RekeyInit {
// -- start AES-GCM encrypted portion (using current key) // -- start AES-GCM encrypted portion (using current key)
pub alice_e: [u8; P384_PUBLIC_KEY_SIZE], pub alice_e: [u8; P384_PUBLIC_KEY_SIZE],
// -- end AES-GCM encrypted portion // -- end AES-GCM encrypted portion
pub gcm_mac: [u8; AES_GCM_TAG_SIZE], pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
} }
impl RekeyInit { impl RekeyInit {
@ -151,9 +157,9 @@ pub(crate) struct RekeyAck {
pub session_protocol_version: u8, pub session_protocol_version: u8,
// -- start AES-GCM encrypted portion (using current key) // -- start AES-GCM encrypted portion (using current key)
pub bob_e: [u8; P384_PUBLIC_KEY_SIZE], pub bob_e: [u8; P384_PUBLIC_KEY_SIZE],
pub next_key_fingerprint: [u8; SHA384_HASH_SIZE], pub next_key_fingerprint: [u8; SHA384_HASH_SIZE], // SHA384(next secret)
// -- end AES-GCM encrypted portion // -- end AES-GCM encrypted portion
pub gcm_mac: [u8; AES_GCM_TAG_SIZE], pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
} }
impl RekeyAck { impl RekeyAck {

View file

@ -14,8 +14,8 @@ use std::num::NonZeroU64;
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock, Weak}; use std::sync::{Arc, Mutex, RwLock, Weak};
use zerotier_crypto::aes::{Aes, AesCtr, AesGcm}; use zerotier_crypto::aes::{Aes, AesGcm};
use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384}; use zerotier_crypto::hash::{hmac_sha512, SHA384, SHA384_HASH_SIZE};
use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE}; use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE};
use zerotier_crypto::secret::Secret; use zerotier_crypto::secret::Secret;
use zerotier_crypto::{random, secure_eq}; use zerotier_crypto::{random, secure_eq};
@ -52,7 +52,7 @@ struct SessionsById<Application: ApplicationLayer> {
active: HashMap<SessionId, Weak<Session<Application>>>, active: HashMap<SessionId, Weak<Session<Application>>>,
// Incomplete sessions in the middle of three-phase Noise_XK negotiation, expired after timeout. // Incomplete sessions in the middle of three-phase Noise_XK negotiation, expired after timeout.
incoming: HashMap<SessionId, Arc<IncomingIncompleteSession>>, incoming: HashMap<SessionId, Arc<BobIncomingIncompleteSessionState>>,
} }
/// Result generated by the context packet receive function, with possible payloads. /// Result generated by the context packet receive function, with possible payloads.
@ -63,8 +63,8 @@ pub enum ReceiveResult<'b, Application: ApplicationLayer> {
/// Packet was valid and a data payload was decoded and authenticated. /// Packet was valid and a data payload was decoded and authenticated.
OkData(Arc<Session<Application>>, &'b mut [u8]), OkData(Arc<Session<Application>>, &'b mut [u8]),
/// Packet was valid and a new session was created. /// Packet was valid and a new session was created, with optional attached meta-data.
OkNewSession(Arc<Session<Application>>), OkNewSession(Arc<Session<Application>>, Option<&'b mut [u8]>),
/// Packet appears valid but was rejected by the application layer, e.g. a rejected new session attempt. /// Packet appears valid but was rejected by the application layer, e.g. a rejected new session attempt.
Rejected, Rejected,
@ -96,20 +96,22 @@ struct State {
current_offer: Offer, current_offer: Offer,
} }
struct IncomingIncompleteSession { struct BobIncomingIncompleteSessionState {
timestamp: i64, timestamp: i64,
alice_session_id: SessionId, alice_session_id: SessionId,
bob_session_id: SessionId, bob_session_id: SessionId,
noise_h: [u8; SHA384_HASH_SIZE],
noise_es_ee: Secret<BASE_KEY_SIZE>, noise_es_ee: Secret<BASE_KEY_SIZE>,
hk: Secret<KYBER_SSBYTES>, hk: Secret<KYBER_SSBYTES>,
header_protection_key: Secret<AES_HEADER_PROTECTION_KEY_SIZE>, header_protection_key: Secret<AES_HEADER_PROTECTION_KEY_SIZE>,
bob_noise_e_secret: P384KeyPair, bob_noise_e_secret: P384KeyPair,
} }
struct OutgoingSessionInit { struct AliceOutgoingIncompleteSessionState {
last_retry_time: AtomicI64, last_retry_time: AtomicI64,
alice_noise_e_secret: P384KeyPair, noise_h: [u8; SHA384_HASH_SIZE],
noise_es: Secret<P384_ECDH_SHARED_SECRET_SIZE>, noise_es: Secret<P384_ECDH_SHARED_SECRET_SIZE>,
alice_noise_e_secret: P384KeyPair,
alice_hk_secret: Secret<KYBER_SECRETKEYBYTES>, alice_hk_secret: Secret<KYBER_SECRETKEYBYTES>,
metadata: Option<Vec<u8>>, metadata: Option<Vec<u8>>,
init_packet: [u8; AliceNoiseXKInit::SIZE], init_packet: [u8; AliceNoiseXKInit::SIZE],
@ -123,7 +125,7 @@ struct OutgoingSessionAck {
enum Offer { enum Offer {
None, None,
NoiseXKInit(Box<OutgoingSessionInit>), NoiseXKInit(Box<AliceOutgoingIncompleteSessionState>),
NoiseXKAck(Box<OutgoingSessionAck>), NoiseXKAck(Box<OutgoingSessionAck>),
RekeyInit(P384KeyPair, i64), RekeyInit(P384KeyPair, i64),
} }
@ -266,6 +268,7 @@ impl<Application: ApplicationLayer> Context<Application> {
/// * `app` - Application layer instance /// * `app` - Application layer instance
/// * `send` - User-supplied packet sending function /// * `send` - User-supplied packet sending function
/// * `mtu` - Physical MTU for calls to send() /// * `mtu` - Physical MTU for calls to send()
/// * `remote_s_public_blob` - Remote side's opaque static public blob (which must contain remote_s_public_p384)
/// * `remote_s_public_p384` - Remote side's static public NIST P-384 key /// * `remote_s_public_p384` - Remote side's static public NIST P-384 key
/// * `psk` - Pre-shared key (use all zero if none) /// * `psk` - Pre-shared key (use all zero if none)
/// * `metadata` - Optional metadata to be included in initial handshake /// * `metadata` - Optional metadata to be included in initial handshake
@ -276,6 +279,7 @@ impl<Application: ApplicationLayer> Context<Application> {
app: &Application, app: &Application,
mut send: SendFunction, mut send: SendFunction,
mtu: usize, mtu: usize,
remote_s_public_blob: &[u8],
remote_s_public_p384: &P384PublicKey, remote_s_public_p384: &P384PublicKey,
psk: Secret<BASE_KEY_SIZE>, psk: Secret<BASE_KEY_SIZE>,
metadata: Option<Vec<u8>>, metadata: Option<Vec<u8>>,
@ -314,10 +318,11 @@ impl<Application: ApplicationLayer> Context<Application> {
remote_session_id: None, remote_session_id: None,
keys: [None, None], keys: [None, None],
current_key: 0, current_key: 0,
current_offer: Offer::NoiseXKInit(Box::new(OutgoingSessionInit { current_offer: Offer::NoiseXKInit(Box::new(AliceOutgoingIncompleteSessionState {
last_retry_time: AtomicI64::new(current_time), last_retry_time: AtomicI64::new(current_time),
alice_noise_e_secret, noise_h: mix_hash(&INITIAL_H, remote_s_public_blob),
noise_es: noise_es.clone(), noise_es: noise_es.clone(),
alice_noise_e_secret,
alice_hk_secret: Secret(alice_hk_secret.secret), alice_hk_secret: Secret(alice_hk_secret.secret),
metadata, metadata,
init_packet: [0u8; AliceNoiseXKInit::SIZE], init_packet: [0u8; AliceNoiseXKInit::SIZE],
@ -333,30 +338,35 @@ impl<Application: ApplicationLayer> Context<Application> {
{ {
let mut state = session.state.write().unwrap(); let mut state = session.state.write().unwrap();
let init_packet = if let Offer::NoiseXKInit(offer) = &mut state.current_offer { let offer = if let Offer::NoiseXKInit(offer) = &mut state.current_offer {
&mut offer.init_packet offer
} else { } else {
panic!(); // should be impossible panic!(); // should be impossible as this is what we initialized with
}; };
let init: &mut AliceNoiseXKInit = byte_array_as_proto_buffer_mut(init_packet).unwrap(); // Create Alice's initial outgoing state message.
init.session_protocol_version = SESSION_PROTOCOL_VERSION; let init_packet = &mut offer.init_packet;
init.alice_noise_e = alice_noise_e; {
init.alice_session_id = *local_session_id.as_bytes(); let init: &mut AliceNoiseXKInit = byte_array_as_proto_buffer_mut(init_packet).unwrap();
init.alice_hk_public = alice_hk_secret.public; init.session_protocol_version = SESSION_PROTOCOL_VERSION;
init.header_protection_key = header_protection_key.0; init.alice_noise_e = alice_noise_e;
init.alice_session_id = *local_session_id.as_bytes();
init.alice_hk_public = alice_hk_secret.public;
init.header_protection_key = header_protection_key.0;
}
aes_ctr_crypt_one_time_use_key( // Encrypt and add authentication tag.
let mut gcm = AesGcm::new(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es.as_bytes()).as_bytes(), kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es.as_bytes()).as_bytes(),
&mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START], true,
); );
gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_INIT, 1));
gcm.aad(&offer.noise_h);
gcm.crypt_in_place(&mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]);
init_packet[AliceNoiseXKInit::AUTH_START..AliceNoiseXKInit::AUTH_START + AES_GCM_TAG_SIZE].copy_from_slice(&gcm.finish_encrypt());
let hmac = hmac_sha384_2( // Update ongoing state hash with Alice's outgoing init ciphertext.
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es.as_bytes()).as_bytes(), offer.noise_h = mix_hash(&offer.noise_h, &init_packet[HEADER_SIZE..]);
&create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_INIT, 1),
&init_packet[HEADER_SIZE..AliceNoiseXKInit::AUTH_START],
);
init_packet[AliceNoiseXKInit::AUTH_START..AliceNoiseXKInit::AUTH_START + HMAC_SHA384_SIZE].copy_from_slice(&hmac);
send_with_fragmentation( send_with_fragmentation(
&mut send, &mut send,
@ -385,9 +395,9 @@ impl<Application: ApplicationLayer> Context<Application> {
/// with negotiation. False drops the packet. /// with negotiation. False drops the packet.
/// ///
/// The check_accept_session function is called at the end of negotiation for an incoming session /// The check_accept_session function is called at the end of negotiation for an incoming session
/// with the caller's static public blob and meta-data if any. It must return the P-384 static public /// with the caller's static public blob. It must return the P-384 static public key extracted from
/// key extracted from the supplied blob, a PSK (or all zeroes if none), and application data to /// the supplied blob, a PSK (or all zeroes if none), and application data to associate with the new
/// associate with the new session. A return of None abandons the session. /// session. A return of None rejects and abandons the session.
/// ///
/// Note that if check_accept_session accepts and returns Some() the session could still fail with /// Note that if check_accept_session accepts and returns Some() the session could still fail with
/// receive() returning an error. A Some() return from check_accept_sesion doesn't guarantee /// receive() returning an error. A Some() return from check_accept_sesion doesn't guarantee
@ -409,7 +419,7 @@ impl<Application: ApplicationLayer> Context<Application> {
'b, 'b,
SendFunction: FnMut(Option<&Arc<Session<Application>>>, &mut [u8]), SendFunction: FnMut(Option<&Arc<Session<Application>>>, &mut [u8]),
CheckAllowIncomingSession: FnMut() -> bool, CheckAllowIncomingSession: FnMut() -> bool,
CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>, CheckAcceptSession: FnMut(&[u8]) -> Option<(P384PublicKey, Secret<64>, Application::Data)>,
>( >(
&self, &self,
app: &Application, app: &Application,
@ -568,7 +578,7 @@ impl<Application: ApplicationLayer> Context<Application> {
'b, 'b,
SendFunction: FnMut(Option<&Arc<Session<Application>>>, &mut [u8]), SendFunction: FnMut(Option<&Arc<Session<Application>>>, &mut [u8]),
CheckAllowIncomingSession: FnMut() -> bool, CheckAllowIncomingSession: FnMut() -> bool,
CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>, CheckAcceptSession: FnMut(&[u8]) -> Option<(P384PublicKey, Secret<64>, Application::Data)>,
>( >(
&self, &self,
app: &Application, app: &Application,
@ -580,7 +590,7 @@ impl<Application: ApplicationLayer> Context<Application> {
fragments: &[Application::IncomingPacketBuffer], fragments: &[Application::IncomingPacketBuffer],
packet_type: u8, packet_type: u8,
session: Option<Arc<Session<Application>>>, session: Option<Arc<Session<Application>>>,
incoming: Option<Arc<IncomingIncompleteSession>>, incoming: Option<Arc<BobIncomingIncompleteSessionState>>,
key_index: usize, key_index: usize,
mtu: usize, mtu: usize,
current_time: i64, current_time: i64,
@ -716,15 +726,18 @@ impl<Application: ApplicationLayer> Context<Application> {
let alice_noise_e = P384PublicKey::from_bytes(&pkt.alice_noise_e).ok_or(Error::FailedAuthentication)?; 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 noise_es = app.get_local_s_keypair().agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?;
// Authenticate packet and also prove that Alice knows our static public key. let noise_h = mix_hash(&INITIAL_H, app.get_local_s_public_blob());
if !secure_eq( let noise_h_next = mix_hash(&noise_h, &pkt_assembled[HEADER_SIZE..]);
&pkt.hmac_es,
&hmac_sha384_2( // Decrypt and authenticate init packet, also proving that caller knows our static identity.
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es.as_bytes()).as_bytes(), let mut gcm = AesGcm::new(
&incoming_message_nonce, kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es.as_bytes()).as_bytes(),
&pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], false,
), );
) { gcm.reset_init_gcm(&incoming_message_nonce);
gcm.aad(&noise_h);
gcm.crypt_in_place(&mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]);
if !gcm.finish_decrypt(&pkt_assembled[AliceNoiseXKInit::AUTH_START..AliceNoiseXKInit::AUTH_START + AES_GCM_TAG_SIZE]) {
return Err(Error::FailedAuthentication); return Err(Error::FailedAuthentication);
} }
@ -733,12 +746,6 @@ impl<Application: ApplicationLayer> Context<Application> {
return Ok(ReceiveResult::Rejected); return Ok(ReceiveResult::Rejected);
} }
// Decrypt encrypted part of payload.
aes_ctr_crypt_one_time_use_key(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es.as_bytes()).as_bytes(),
&mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START],
);
let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?;
let alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?; let alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?;
let header_protection_key = Secret(pkt.header_protection_key); let header_protection_key = Secret(pkt.header_protection_key);
@ -765,6 +772,24 @@ impl<Application: ApplicationLayer> Context<Application> {
} }
} }
// Create Bob's ephemeral counter-offer reply.
let mut ack_packet = [0u8; BobNoiseXKAck::SIZE];
let ack: &mut BobNoiseXKAck = byte_array_as_proto_buffer_mut(&mut ack_packet)?;
ack.session_protocol_version = SESSION_PROTOCOL_VERSION;
ack.bob_noise_e = bob_noise_e;
ack.bob_session_id = *bob_session_id.as_bytes();
ack.bob_hk_ciphertext = bob_hk_ciphertext;
// Encrypt main section of reply and attach tag.
let mut gcm = AesGcm::new(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes(),
true,
);
gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_BOB_NOISE_XK_ACK, 1));
gcm.aad(&noise_h_next);
gcm.crypt_in_place(&mut ack_packet[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START]);
ack_packet[BobNoiseXKAck::AUTH_START..BobNoiseXKAck::AUTH_START + AES_GCM_TAG_SIZE].copy_from_slice(&gcm.finish_encrypt());
// If this queue is too big, we remove the latest entry and replace it. The latest // If this queue is too big, we remove the latest entry and replace it. The latest
// is used because under flood conditions this is most likely to be another bogus // is used because under flood conditions this is most likely to be another bogus
// entry. If we find one that is actually timed out, that one is replaced instead. // entry. If we find one that is actually timed out, that one is replaced instead.
@ -787,10 +812,11 @@ impl<Application: ApplicationLayer> Context<Application> {
// Reserve session ID on this side and record incomplete session state. // Reserve session ID on this side and record incomplete session state.
sessions.incoming.insert( sessions.incoming.insert(
bob_session_id, bob_session_id,
Arc::new(IncomingIncompleteSession { Arc::new(BobIncomingIncompleteSessionState {
timestamp: current_time, timestamp: current_time,
alice_session_id, alice_session_id,
bob_session_id, bob_session_id,
noise_h: mix_hash(&noise_h_next, &ack_packet[HEADER_SIZE..]),
noise_es_ee: noise_es_ee.clone(), noise_es_ee: noise_es_ee.clone(),
hk, hk,
bob_noise_e_secret, bob_noise_e_secret,
@ -799,30 +825,9 @@ impl<Application: ApplicationLayer> Context<Application> {
); );
debug_assert!(!sessions.active.contains_key(&bob_session_id)); debug_assert!(!sessions.active.contains_key(&bob_session_id));
// Release lock
drop(sessions); drop(sessions);
// Create Bob's ephemeral counter-offer reply.
let mut ack_packet = [0u8; BobNoiseXKAck::SIZE];
let ack: &mut BobNoiseXKAck = byte_array_as_proto_buffer_mut(&mut ack_packet)?;
ack.session_protocol_version = SESSION_PROTOCOL_VERSION;
ack.bob_noise_e = bob_noise_e;
ack.bob_session_id = *bob_session_id.as_bytes();
ack.bob_hk_ciphertext = bob_hk_ciphertext;
// Encrypt main section of reply.
aes_ctr_crypt_one_time_use_key(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes(),
&mut ack_packet[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START],
);
// Add HMAC-SHA384 to reply packet.
let reply_hmac = hmac_sha384_2(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es_ee.as_bytes()).as_bytes(),
&create_message_nonce(PACKET_TYPE_BOB_NOISE_XK_ACK, 1),
&ack_packet[HEADER_SIZE..BobNoiseXKAck::AUTH_START],
);
ack_packet[BobNoiseXKAck::AUTH_START..].copy_from_slice(&reply_hmac);
send_with_fragmentation( send_with_fragmentation(
|b| send(None, b), |b| send(None, b),
&mut ack_packet, &mut ack_packet,
@ -875,26 +880,21 @@ impl<Application: ApplicationLayer> Context<Application> {
.as_bytes(), .as_bytes(),
)); ));
let noise_es_ee_kex_hmac_key = // Go ahead and compute the next 'h' state before we lose the ciphertext in decrypt.
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es_ee.as_bytes()); let noise_h_next = mix_hash(&outgoing_offer.noise_h, &pkt_assembled[HEADER_SIZE..]);
// Authenticate Bob's reply and the validity of bob_noise_e. // Decrypt and authenticate Bob's reply.
if !secure_eq( let mut gcm = AesGcm::new(
&pkt.hmac_es_ee, kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes(),
&hmac_sha384_2( false,
noise_es_ee_kex_hmac_key.as_bytes(), );
&incoming_message_nonce, gcm.reset_init_gcm(&incoming_message_nonce);
&pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START], gcm.aad(&outgoing_offer.noise_h);
), gcm.crypt_in_place(&mut pkt_assembled[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START]);
) { if !gcm.finish_decrypt(&pkt_assembled[BobNoiseXKAck::AUTH_START..BobNoiseXKAck::AUTH_START + AES_GCM_TAG_SIZE]) {
return Err(Error::FailedAuthentication); return Err(Error::FailedAuthentication);
} }
// Decrypt encrypted portion of message.
aes_ctr_crypt_one_time_use_key(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes(),
&mut pkt_assembled[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START],
);
let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?;
if let Some(bob_session_id) = SessionId::new_from_bytes(&pkt.bob_session_id) { if let Some(bob_session_id) = SessionId::new_from_bytes(&pkt.bob_session_id) {
@ -923,50 +923,44 @@ impl<Application: ApplicationLayer> Context<Application> {
let mut reply_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; let mut reply_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE];
reply_buffer[HEADER_SIZE] = SESSION_PROTOCOL_VERSION; reply_buffer[HEADER_SIZE] = SESSION_PROTOCOL_VERSION;
let mut reply_len = HEADER_SIZE + 1; let mut reply_len = HEADER_SIZE + 1;
let mut reply_buffer_append = |b: &[u8]| {
let reply_len_new = reply_len + b.len();
debug_assert!(reply_len_new <= MAX_NOISE_HANDSHAKE_SIZE);
reply_buffer[reply_len..reply_len_new].copy_from_slice(b);
reply_len = reply_len_new;
};
let alice_s_public_blob = app.get_local_s_public_blob(); let alice_s_public_blob = app.get_local_s_public_blob();
assert!(alice_s_public_blob.len() <= (u16::MAX as usize)); assert!(alice_s_public_blob.len() <= (u16::MAX as usize));
reply_buffer_append(&(alice_s_public_blob.len() as u16).to_le_bytes()); reply_len = append_to_slice(&mut reply_buffer, reply_len, &(alice_s_public_blob.len() as u16).to_le_bytes())?;
reply_buffer_append(alice_s_public_blob); let mut enc_start = reply_len;
if let Some(md) = outgoing_offer.metadata.as_ref() { reply_len = append_to_slice(&mut reply_buffer, reply_len, alice_s_public_blob)?;
reply_buffer_append(&(md.len() as u16).to_le_bytes());
reply_buffer_append(md.as_ref());
} else {
reply_buffer_append(&[0u8, 0u8]); // no meta-data
}
// Encrypt Alice's static identity and other inner payload items. The key used here is let mut gcm = AesGcm::new(
// mixed with 'hk' to make identity secrecy PQ forward secure. kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(&hmac_sha512(
aes_ctr_crypt_one_time_use_key( noise_es_ee.as_bytes(),
&hmac_sha512(noise_es_ee.as_bytes(), hk.as_bytes())[..AES_256_KEY_SIZE], hk.as_bytes(),
&mut reply_buffer[HEADER_SIZE + 1..reply_len], ))
.as_bytes(),
true,
); );
gcm.reset_init_gcm(&reply_message_nonce);
gcm.aad(&noise_h_next);
gcm.crypt_in_place(&mut reply_buffer[enc_start..reply_len]);
reply_len = append_to_slice(&mut reply_buffer, reply_len, &gcm.finish_encrypt())?;
// First attach HMAC allowing Bob to verify that this is from the same Alice and to let metadata = outgoing_offer.metadata.as_ref().map_or(&[][..0], |md| md.as_slice());
// verify the authenticity of encrypted data.
let hmac_es_ee = hmac_sha384_2(
noise_es_ee_kex_hmac_key.as_bytes(),
&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 assert!(metadata.len() <= (u16::MAX as usize));
// key exchange. Bob won't be able to do this until he decrypts and parses Alice's reply_len = append_to_slice(&mut reply_buffer, reply_len, &(metadata.len() as u16).to_le_bytes())?;
// identity, so the first HMAC is to let him authenticate that first.
let hmac_es_ee_se_hk_psk = hmac_sha384_2( let noise_h_next = mix_hash(&noise_h_next, &reply_buffer[HEADER_SIZE..reply_len]);
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es_ee_se_hk_psk.as_bytes()).as_bytes(),
&reply_message_nonce, enc_start = reply_len;
&reply_buffer[HEADER_SIZE..reply_len], reply_len = append_to_slice(&mut reply_buffer, reply_len, metadata)?;
let mut gcm = AesGcm::new(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee_se_hk_psk.as_bytes()).as_bytes(),
true,
); );
reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee_se_hk_psk); gcm.reset_init_gcm(&reply_message_nonce);
reply_len += HMAC_SHA384_SIZE; gcm.aad(&noise_h_next);
gcm.crypt_in_place(&mut reply_buffer[enc_start..reply_len]);
reply_len = append_to_slice(&mut reply_buffer, reply_len, &gcm.finish_encrypt())?;
drop(state); drop(state);
{ {
@ -1025,62 +1019,28 @@ impl<Application: ApplicationLayer> Context<Application> {
} }
if let Some(incoming) = incoming { if let Some(incoming) = incoming {
// Check the first HMAC to verify against the currently known noise_es_ee key, which verifies let mut r = PktReader(pkt_assembled, HEADER_SIZE + 1);
// that this reply is part of this session.
let auth_start = pkt_assembled.len() - ALICE_NOISE_XK_ACK_AUTH_SIZE;
if !secure_eq(
&pkt_assembled[auth_start..pkt_assembled.len() - HMAC_SHA384_SIZE],
&hmac_sha384_2(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(incoming.noise_es_ee.as_bytes()).as_bytes(),
&incoming_message_nonce,
&pkt_assembled[HEADER_SIZE..auth_start],
),
) {
return Err(Error::FailedAuthentication);
}
// Make a copy of pkt_assembled so we can check the second HMAC against original ciphertext later. let alice_static_public_blob_size = r.read_u16()? as usize;
let mut pkt_assembly_buffer_copy = [0u8; MAX_NOISE_HANDSHAKE_SIZE];
pkt_assembly_buffer_copy[..pkt_assembled.len()].copy_from_slice(pkt_assembled);
// Decrypt encrypted section so we can finally learn Alice's static identity. let ciphertext_up_to_metadata_size = r.1 + alice_static_public_blob_size + AES_GCM_TAG_SIZE + 2;
aes_ctr_crypt_one_time_use_key( if r.0.len() < ciphertext_up_to_metadata_size {
&hmac_sha512(incoming.noise_es_ee.as_bytes(), incoming.hk.as_bytes())[..AES_256_KEY_SIZE],
&mut pkt_assembled[ALICE_NOISE_XK_ACK_ENC_START..auth_start],
);
// Read the static public blob and optional meta-data.
let mut pkt_assembled_ptr = HEADER_SIZE + 1;
let mut pkt_assembled_field_end = pkt_assembled_ptr + 2;
if pkt_assembled_field_end >= pkt_assembled.len() {
return Err(Error::InvalidPacket); return Err(Error::InvalidPacket);
} }
let alice_static_public_blob_size = let noise_h_next = mix_hash(&incoming.noise_h, &r.0[HEADER_SIZE..ciphertext_up_to_metadata_size]);
u16::from_le_bytes(pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end].try_into().unwrap()) as usize;
pkt_assembled_ptr = pkt_assembled_field_end;
pkt_assembled_field_end = pkt_assembled_ptr + alice_static_public_blob_size;
if pkt_assembled_field_end >= pkt_assembled.len() {
return Err(Error::InvalidPacket);
}
let alice_static_public_blob = &pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end];
pkt_assembled_ptr = pkt_assembled_field_end;
pkt_assembled_field_end = pkt_assembled_ptr + 2;
if pkt_assembled_field_end >= pkt_assembled.len() {
return Err(Error::InvalidPacket);
}
let alice_meta_data_size =
u16::from_le_bytes(pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end].try_into().unwrap()) as usize;
pkt_assembled_ptr = pkt_assembled_field_end;
pkt_assembled_field_end = pkt_assembled_ptr + alice_meta_data_size;
let alice_meta_data = if alice_meta_data_size > 0 {
Some(&pkt_assembled[pkt_assembled_ptr..pkt_assembled_field_end])
} else {
None
};
// Check session acceptance and fish Alice's NIST P-384 static public key out of let alice_static_public_blob = r.read_decrypt_auth(
// her static public blob. alice_static_public_blob_size,
let check_result = check_accept_session(alice_static_public_blob, alice_meta_data); kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(&hmac_sha512(
incoming.noise_es_ee.as_bytes(),
incoming.hk.as_bytes(),
)),
&incoming.noise_h,
&incoming_message_nonce,
)?;
// Check session acceptance and fish Alice's NIST P-384 static public key out of her static public blob.
let check_result = check_accept_session(alice_static_public_blob);
if check_result.is_none() { if check_result.is_none() {
self.sessions.write().unwrap().incoming.remove(&incoming.bob_session_id); self.sessions.write().unwrap().incoming.remove(&incoming.bob_session_id);
return Ok(ReceiveResult::Rejected); return Ok(ReceiveResult::Rejected);
@ -1100,17 +1060,19 @@ impl<Application: ApplicationLayer> Context<Application> {
&hmac_sha512(psk.as_bytes(), incoming.hk.as_bytes()), &hmac_sha512(psk.as_bytes(), incoming.hk.as_bytes()),
)); ));
// Verify the packet using the final key to verify the whole key exchange. // Decrypt meta-data and verify the final key in the process. Copy meta-data
if !secure_eq( // into the temporary data buffer to return.
&pkt_assembly_buffer_copy[auth_start + HMAC_SHA384_SIZE..pkt_assembled.len()], let alice_meta_data_size = r.read_u16()? as usize;
&hmac_sha384_2( let alice_meta_data = r.read_decrypt_auth(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es_ee_se_hk_psk.as_bytes()).as_bytes(), alice_meta_data_size,
&incoming_message_nonce, kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee_se_hk_psk.as_bytes()),
&pkt_assembly_buffer_copy[HEADER_SIZE..auth_start + HMAC_SHA384_SIZE], &noise_h_next,
), &incoming_message_nonce,
) { )?;
return Err(Error::FailedAuthentication); if alice_meta_data.len() > data_buf.len() {
return Err(Error::DataTooLarge);
} }
data_buf[..alice_meta_data.len()].copy_from_slice(alice_meta_data);
let session = Arc::new(Session { let session = Arc::new(Session {
id: incoming.bob_session_id, id: incoming.bob_session_id,
@ -1131,6 +1093,7 @@ impl<Application: ApplicationLayer> Context<Application> {
defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())),
}); });
// Promote incoming session to active.
{ {
let mut sessions = self.sessions.write().unwrap(); let mut sessions = self.sessions.write().unwrap();
sessions.incoming.remove(&incoming.bob_session_id); sessions.incoming.remove(&incoming.bob_session_id);
@ -1139,7 +1102,14 @@ impl<Application: ApplicationLayer> Context<Application> {
let _ = session.send_nop(|b| send(Some(&session), b)); let _ = session.send_nop(|b| send(Some(&session), b));
return Ok(ReceiveResult::OkNewSession(session)); return Ok(ReceiveResult::OkNewSession(
session,
if alice_meta_data.is_empty() {
None
} else {
Some(&mut data_buf[..alice_meta_data.len()])
},
));
} else { } else {
return Err(Error::UnknownLocalSessionId); return Err(Error::UnknownLocalSessionId);
} }
@ -1650,22 +1620,60 @@ impl SessionKey {
} }
} }
/// Shortcut to HMAC data split into two slices. /// Helper code for parsing variable length ALICE_NOISE_XK_ACK during negotiation.
fn hmac_sha384_2(key: &[u8], a: &[u8], b: &[u8]) -> [u8; 48] { struct PktReader<'a>(&'a mut [u8], usize);
let mut hmac = HMACSHA384::new(key);
hmac.update(a); impl<'a> PktReader<'a> {
hmac.update(b); fn read_u16(&mut self) -> Result<u16, Error> {
hmac.finish() let tmp = self.1 + 2;
if tmp <= self.0.len() {
let n = u16::from_le_bytes(self.0[self.1..tmp].try_into().unwrap());
self.1 = tmp;
Ok(n)
} else {
Err(Error::InvalidPacket)
}
}
fn read_decrypt_auth<'b>(&'b mut self, l: usize, k: Secret<AES_256_KEY_SIZE>, gcm_aad: &[u8], nonce: &[u8]) -> Result<&'b [u8], Error> {
let mut tmp = self.1 + l;
if (tmp + AES_GCM_TAG_SIZE) <= self.0.len() {
let mut gcm = AesGcm::new(k.as_bytes(), false);
gcm.reset_init_gcm(nonce);
gcm.aad(gcm_aad);
gcm.crypt_in_place(&mut self.0[self.1..tmp]);
let s = &self.0[self.1..tmp];
self.1 = tmp;
tmp += AES_GCM_TAG_SIZE;
if !gcm.finish_decrypt(&self.0[self.1..tmp]) {
Err(Error::FailedAuthentication)
} else {
self.1 = tmp;
Ok(s)
}
} else {
Err(Error::InvalidPacket)
}
}
} }
/// Shortcut to AES-CTR encrypt or decrypt with a zero IV. /// Helper function to append to a slice when we still want to be able to look back at it.
/// fn append_to_slice(s: &mut [u8], p: usize, d: &[u8]) -> Result<usize, Error> {
/// This is used during Noise_XK handshaking. Each stage uses a different key to encrypt the let tmp = p + d.len();
/// payload that is used only once per handshake and per session. if tmp <= s.len() {
fn aes_ctr_crypt_one_time_use_key(key: &[u8], data: &mut [u8]) { s[p..tmp].copy_from_slice(d);
let mut ctr = AesCtr::new(key); Ok(tmp)
ctr.reset_set_iv(&[0u8; 12]); } else {
ctr.crypt_in_place(data); Err(Error::UnexpectedBufferOverrun)
}
}
/// MixHash to update 'h' during negotiation.
fn mix_hash(h: &[u8; SHA384_HASH_SIZE], m: &[u8]) -> [u8; SHA384_HASH_SIZE] {
let mut hasher = SHA384::new();
hasher.update(h);
hasher.update(m);
hasher.finish()
} }
/// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 7) /// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 7)