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
======
# 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,
}
// 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 {
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> {
#[inline(always)]
pub fn new() -> Self {
debug_assert!(MAX_FRAGMENTS <= 64);
debug_assert_eq!(size_of::<MaybeUninit<Fragment>>(), size_of::<Fragment>());
debug_assert_eq!(
// These assertions should be optimized out at compile time and check to make sure
// that the array of MaybeUninit<Fragment> can be freely cast into an array of
// 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::<[Fragment; MAX_FRAGMENTS]>()
);
@ -47,10 +51,9 @@ impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> {
#[inline(always)]
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 {
debug_assert!((fragment_count as usize) <= MAX_FRAGMENTS);
debug_assert!((fragment_no as usize) < MAX_FRAGMENTS);
let mut have = self.have;
// If the counter has changed, reset the structure to receive a new packet.
if counter != self.counter {
self.counter = counter;
if needs_drop::<Fragment>() {

View file

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

View file

@ -9,7 +9,7 @@
use std::mem::size_of;
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 crate::error::Error;
@ -24,6 +24,13 @@ pub const MIN_TRANSPORT_MTU: usize = 128;
/// 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;
/// 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.
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_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_AUTHENTICATION: u8 = b'X'; // HMAC-SHA384 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_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_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 session_protocol_version: u8,
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_hk_public: [u8; KYBER_PUBLICKEYBYTES],
pub header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE],
// -- end encrypted section
pub hmac_es: [u8; HMAC_SHA384_SIZE],
pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
}
impl AliceNoiseXKInit {
pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE;
pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_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.
@ -92,17 +98,17 @@ pub(crate) struct BobNoiseXKAck {
pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8,
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_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES],
// -- end encrypted sectiion
pub hmac_es_ee: [u8; HMAC_SHA384_SIZE],
pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
}
impl BobNoiseXKAck {
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 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.
@ -112,20 +118,20 @@ impl BobNoiseXKAck {
pub(crate) struct AliceNoiseXKAck {
pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8,
// -- start AES-CTR(es_ee_hk) encrypted section
pub alice_static_blob_length: [u8; 2],
// -- start AES-GCM(es_ee_hk) encrypted section
pub alice_static_blob: [u8; ???],
// -- end encrypted section
pub gcm_tag_0: [u8; AES_GCM_TAG_SIZE],
pub alice_metadata_length: [u8; 2],
// -- start AES-GCM(es_ee_se_hk_psk) encrypted section
pub alice_metadata: [u8; ???],
// -- end encrypted section
pub hmac_es_ee: [u8; HMAC_SHA384_SIZE],
pub hmac_es_ee_se_hk_psk: [u8; HMAC_SHA384_SIZE],
pub gcm_tag_1: [u8; AES_GCM_TAG_SIZE],
}
*/
pub(crate) const ALICE_NOISE_XK_ACK_ENC_START: usize = HEADER_SIZE + 1;
pub(crate) const ALICE_NOISE_XK_ACK_AUTH_SIZE: usize = HMAC_SHA384_SIZE + HMAC_SHA384_SIZE;
pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = ALICE_NOISE_XK_ACK_ENC_START + 2 + 2 + ALICE_NOISE_XK_ACK_AUTH_SIZE;
pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = HEADER_SIZE + 1 + 2 + AES_GCM_TAG_SIZE + 2 + AES_GCM_TAG_SIZE;
#[allow(unused)]
#[repr(C, packed)]
@ -135,7 +141,7 @@ pub(crate) struct RekeyInit {
// -- start AES-GCM encrypted portion (using current key)
pub alice_e: [u8; P384_PUBLIC_KEY_SIZE],
// -- end AES-GCM encrypted portion
pub gcm_mac: [u8; AES_GCM_TAG_SIZE],
pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
}
impl RekeyInit {
@ -151,9 +157,9 @@ pub(crate) struct RekeyAck {
pub session_protocol_version: u8,
// -- start AES-GCM encrypted portion (using current key)
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
pub gcm_mac: [u8; AES_GCM_TAG_SIZE],
pub gcm_tag: [u8; AES_GCM_TAG_SIZE],
}
impl RekeyAck {

View file

@ -14,8 +14,8 @@ use std::num::NonZeroU64;
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock, Weak};
use zerotier_crypto::aes::{Aes, AesCtr, AesGcm};
use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, HMAC_SHA384_SIZE, SHA384};
use zerotier_crypto::aes::{Aes, AesGcm};
use zerotier_crypto::hash::{hmac_sha512, SHA384, SHA384_HASH_SIZE};
use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE};
use zerotier_crypto::secret::Secret;
use zerotier_crypto::{random, secure_eq};
@ -52,7 +52,7 @@ struct SessionsById<Application: ApplicationLayer> {
active: HashMap<SessionId, Weak<Session<Application>>>,
// 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.
@ -63,8 +63,8 @@ pub enum ReceiveResult<'b, Application: ApplicationLayer> {
/// Packet was valid and a data payload was decoded and authenticated.
OkData(Arc<Session<Application>>, &'b mut [u8]),
/// Packet was valid and a new session was created.
OkNewSession(Arc<Session<Application>>),
/// Packet was valid and a new session was created, with optional attached meta-data.
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.
Rejected,
@ -96,20 +96,22 @@ struct State {
current_offer: Offer,
}
struct IncomingIncompleteSession {
struct BobIncomingIncompleteSessionState {
timestamp: i64,
alice_session_id: SessionId,
bob_session_id: SessionId,
noise_h: [u8; SHA384_HASH_SIZE],
noise_es_ee: Secret<BASE_KEY_SIZE>,
hk: Secret<KYBER_SSBYTES>,
header_protection_key: Secret<AES_HEADER_PROTECTION_KEY_SIZE>,
bob_noise_e_secret: P384KeyPair,
}
struct OutgoingSessionInit {
struct AliceOutgoingIncompleteSessionState {
last_retry_time: AtomicI64,
alice_noise_e_secret: P384KeyPair,
noise_h: [u8; SHA384_HASH_SIZE],
noise_es: Secret<P384_ECDH_SHARED_SECRET_SIZE>,
alice_noise_e_secret: P384KeyPair,
alice_hk_secret: Secret<KYBER_SECRETKEYBYTES>,
metadata: Option<Vec<u8>>,
init_packet: [u8; AliceNoiseXKInit::SIZE],
@ -123,7 +125,7 @@ struct OutgoingSessionAck {
enum Offer {
None,
NoiseXKInit(Box<OutgoingSessionInit>),
NoiseXKInit(Box<AliceOutgoingIncompleteSessionState>),
NoiseXKAck(Box<OutgoingSessionAck>),
RekeyInit(P384KeyPair, i64),
}
@ -266,6 +268,7 @@ impl<Application: ApplicationLayer> Context<Application> {
/// * `app` - Application layer instance
/// * `send` - User-supplied packet sending function
/// * `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
/// * `psk` - Pre-shared key (use all zero if none)
/// * `metadata` - Optional metadata to be included in initial handshake
@ -276,6 +279,7 @@ impl<Application: ApplicationLayer> Context<Application> {
app: &Application,
mut send: SendFunction,
mtu: usize,
remote_s_public_blob: &[u8],
remote_s_public_p384: &P384PublicKey,
psk: Secret<BASE_KEY_SIZE>,
metadata: Option<Vec<u8>>,
@ -314,10 +318,11 @@ impl<Application: ApplicationLayer> Context<Application> {
remote_session_id: None,
keys: [None, None],
current_key: 0,
current_offer: Offer::NoiseXKInit(Box::new(OutgoingSessionInit {
current_offer: Offer::NoiseXKInit(Box::new(AliceOutgoingIncompleteSessionState {
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(),
alice_noise_e_secret,
alice_hk_secret: Secret(alice_hk_secret.secret),
metadata,
init_packet: [0u8; AliceNoiseXKInit::SIZE],
@ -333,30 +338,35 @@ impl<Application: ApplicationLayer> Context<Application> {
{
let mut state = session.state.write().unwrap();
let init_packet = if let Offer::NoiseXKInit(offer) = &mut state.current_offer {
&mut offer.init_packet
let offer = if let Offer::NoiseXKInit(offer) = &mut state.current_offer {
offer
} 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();
init.session_protocol_version = SESSION_PROTOCOL_VERSION;
init.alice_noise_e = alice_noise_e;
init.alice_session_id = *local_session_id.as_bytes();
init.alice_hk_public = alice_hk_secret.public;
init.header_protection_key = header_protection_key.0;
// Create Alice's initial outgoing state message.
let init_packet = &mut offer.init_packet;
{
let init: &mut AliceNoiseXKInit = byte_array_as_proto_buffer_mut(init_packet).unwrap();
init.session_protocol_version = SESSION_PROTOCOL_VERSION;
init.alice_noise_e = alice_noise_e;
init.alice_session_id = *local_session_id.as_bytes();
init.alice_hk_public = alice_hk_secret.public;
init.header_protection_key = header_protection_key.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(),
&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(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es.as_bytes()).as_bytes(),
&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);
// Update ongoing state hash with Alice's outgoing init ciphertext.
offer.noise_h = mix_hash(&offer.noise_h, &init_packet[HEADER_SIZE..]);
send_with_fragmentation(
&mut send,
@ -385,9 +395,9 @@ impl<Application: ApplicationLayer> Context<Application> {
/// with negotiation. False drops the packet.
///
/// The check_accept_session function is called at the end of negotiation for an incoming session
/// with the caller's static public blob and meta-data if any. It must return the P-384 static public
/// key extracted from the supplied blob, a PSK (or all zeroes if none), and application data to
/// associate with the new session. A return of None abandons the session.
/// with the caller's static public blob. It must return the P-384 static public key extracted from
/// the supplied blob, a PSK (or all zeroes if none), and application data to associate with the new
/// session. A return of None rejects and abandons the session.
///
/// Note that if check_accept_session accepts and returns Some() the session could still fail with
/// receive() returning an error. A Some() return from check_accept_sesion doesn't guarantee
@ -409,7 +419,7 @@ impl<Application: ApplicationLayer> Context<Application> {
'b,
SendFunction: FnMut(Option<&Arc<Session<Application>>>, &mut [u8]),
CheckAllowIncomingSession: FnMut() -> bool,
CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>,
CheckAcceptSession: FnMut(&[u8]) -> Option<(P384PublicKey, Secret<64>, Application::Data)>,
>(
&self,
app: &Application,
@ -568,7 +578,7 @@ impl<Application: ApplicationLayer> Context<Application> {
'b,
SendFunction: FnMut(Option<&Arc<Session<Application>>>, &mut [u8]),
CheckAllowIncomingSession: FnMut() -> bool,
CheckAcceptSession: FnMut(&[u8], Option<&[u8]>) -> Option<(P384PublicKey, Secret<64>, Application::Data)>,
CheckAcceptSession: FnMut(&[u8]) -> Option<(P384PublicKey, Secret<64>, Application::Data)>,
>(
&self,
app: &Application,
@ -580,7 +590,7 @@ impl<Application: ApplicationLayer> Context<Application> {
fragments: &[Application::IncomingPacketBuffer],
packet_type: u8,
session: Option<Arc<Session<Application>>>,
incoming: Option<Arc<IncomingIncompleteSession>>,
incoming: Option<Arc<BobIncomingIncompleteSessionState>>,
key_index: usize,
mtu: usize,
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 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.
if !secure_eq(
&pkt.hmac_es,
&hmac_sha384_2(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es.as_bytes()).as_bytes(),
&incoming_message_nonce,
&pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START],
),
) {
let noise_h = mix_hash(&INITIAL_H, app.get_local_s_public_blob());
let noise_h_next = mix_hash(&noise_h, &pkt_assembled[HEADER_SIZE..]);
// Decrypt and authenticate init packet, also proving that caller knows our static identity.
let mut gcm = AesGcm::new(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es.as_bytes()).as_bytes(),
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);
}
@ -733,12 +746,6 @@ impl<Application: ApplicationLayer> Context<Application> {
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 alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?;
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
// 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.
@ -787,10 +812,11 @@ impl<Application: ApplicationLayer> Context<Application> {
// Reserve session ID on this side and record incomplete session state.
sessions.incoming.insert(
bob_session_id,
Arc::new(IncomingIncompleteSession {
Arc::new(BobIncomingIncompleteSessionState {
timestamp: current_time,
alice_session_id,
bob_session_id,
noise_h: mix_hash(&noise_h_next, &ack_packet[HEADER_SIZE..]),
noise_es_ee: noise_es_ee.clone(),
hk,
bob_noise_e_secret,
@ -799,30 +825,9 @@ impl<Application: ApplicationLayer> Context<Application> {
);
debug_assert!(!sessions.active.contains_key(&bob_session_id));
// Release lock
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(
|b| send(None, b),
&mut ack_packet,
@ -875,26 +880,21 @@ impl<Application: ApplicationLayer> Context<Application> {
.as_bytes(),
));
let noise_es_ee_kex_hmac_key =
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es_ee.as_bytes());
// Go ahead and compute the next 'h' state before we lose the ciphertext in decrypt.
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.
if !secure_eq(
&pkt.hmac_es_ee,
&hmac_sha384_2(
noise_es_ee_kex_hmac_key.as_bytes(),
&incoming_message_nonce,
&pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START],
),
) {
// Decrypt and authenticate Bob's reply.
let mut gcm = AesGcm::new(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes(),
false,
);
gcm.reset_init_gcm(&incoming_message_nonce);
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);
}
// 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)?;
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];
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();
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();
assert!(alice_s_public_blob.len() <= (u16::MAX as usize));
reply_buffer_append(&(alice_s_public_blob.len() as u16).to_le_bytes());
reply_buffer_append(alice_s_public_blob);
if let Some(md) = outgoing_offer.metadata.as_ref() {
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
}
reply_len = append_to_slice(&mut reply_buffer, reply_len, &(alice_s_public_blob.len() as u16).to_le_bytes())?;
let mut enc_start = reply_len;
reply_len = append_to_slice(&mut reply_buffer, reply_len, alice_s_public_blob)?;
// Encrypt Alice's static identity and other inner payload items. The key used here is
// mixed with 'hk' to make identity secrecy PQ forward secure.
aes_ctr_crypt_one_time_use_key(
&hmac_sha512(noise_es_ee.as_bytes(), hk.as_bytes())[..AES_256_KEY_SIZE],
&mut reply_buffer[HEADER_SIZE + 1..reply_len],
let mut gcm = AesGcm::new(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(&hmac_sha512(
noise_es_ee.as_bytes(),
hk.as_bytes(),
))
.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
// 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;
let metadata = outgoing_offer.metadata.as_ref().map_or(&[][..0], |md| md.as_slice());
// Then attach the final HMAC permitting Bob to verify the authenticity of the whole
// key exchange. Bob won't be able to do this until he decrypts and parses Alice's
// identity, so the first HMAC is to let him authenticate that first.
let hmac_es_ee_se_hk_psk = hmac_sha384_2(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es_ee_se_hk_psk.as_bytes()).as_bytes(),
&reply_message_nonce,
&reply_buffer[HEADER_SIZE..reply_len],
assert!(metadata.len() <= (u16::MAX as usize));
reply_len = append_to_slice(&mut reply_buffer, reply_len, &(metadata.len() as u16).to_le_bytes())?;
let noise_h_next = mix_hash(&noise_h_next, &reply_buffer[HEADER_SIZE..reply_len]);
enc_start = 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);
reply_len += HMAC_SHA384_SIZE;
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())?;
drop(state);
{
@ -1025,62 +1019,28 @@ impl<Application: ApplicationLayer> Context<Application> {
}
if let Some(incoming) = incoming {
// Check the first HMAC to verify against the currently known noise_es_ee key, which verifies
// that this reply is part of this session.
let auth_start = pkt_assembled.len() - ALICE_NOISE_XK_ACK_AUTH_SIZE;
if !secure_eq(
&pkt_assembled[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);
}
let mut r = PktReader(pkt_assembled, HEADER_SIZE + 1);
// Make a copy of pkt_assembled so we can check the second HMAC against original ciphertext later.
let mut pkt_assembly_buffer_copy = [0u8; MAX_NOISE_HANDSHAKE_SIZE];
pkt_assembly_buffer_copy[..pkt_assembled.len()].copy_from_slice(pkt_assembled);
let alice_static_public_blob_size = r.read_u16()? as usize;
// Decrypt encrypted section so we can finally learn Alice's static identity.
aes_ctr_crypt_one_time_use_key(
&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() {
let ciphertext_up_to_metadata_size = r.1 + alice_static_public_blob_size + AES_GCM_TAG_SIZE + 2;
if r.0.len() < ciphertext_up_to_metadata_size {
return Err(Error::InvalidPacket);
}
let alice_static_public_blob_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
};
let noise_h_next = mix_hash(&incoming.noise_h, &r.0[HEADER_SIZE..ciphertext_up_to_metadata_size]);
// Check session acceptance and fish Alice's NIST P-384 static public key out of
// her static public blob.
let check_result = check_accept_session(alice_static_public_blob, alice_meta_data);
let alice_static_public_blob = r.read_decrypt_auth(
alice_static_public_blob_size,
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() {
self.sessions.write().unwrap().incoming.remove(&incoming.bob_session_id);
return Ok(ReceiveResult::Rejected);
@ -1100,17 +1060,19 @@ impl<Application: ApplicationLayer> Context<Application> {
&hmac_sha512(psk.as_bytes(), incoming.hk.as_bytes()),
));
// Verify the packet using the final key to verify the whole key exchange.
if !secure_eq(
&pkt_assembly_buffer_copy[auth_start + HMAC_SHA384_SIZE..pkt_assembled.len()],
&hmac_sha384_2(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_AUTHENTICATION>(noise_es_ee_se_hk_psk.as_bytes()).as_bytes(),
&incoming_message_nonce,
&pkt_assembly_buffer_copy[HEADER_SIZE..auth_start + HMAC_SHA384_SIZE],
),
) {
return Err(Error::FailedAuthentication);
// Decrypt meta-data and verify the final key in the process. Copy meta-data
// into the temporary data buffer to return.
let alice_meta_data_size = r.read_u16()? as usize;
let alice_meta_data = r.read_decrypt_auth(
alice_meta_data_size,
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_INIT_ENCRYPTION>(noise_es_ee_se_hk_psk.as_bytes()),
&noise_h_next,
&incoming_message_nonce,
)?;
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 {
id: incoming.bob_session_id,
@ -1131,6 +1093,7 @@ impl<Application: ApplicationLayer> Context<Application> {
defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())),
});
// Promote incoming session to active.
{
let mut sessions = self.sessions.write().unwrap();
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));
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 {
return Err(Error::UnknownLocalSessionId);
}
@ -1650,22 +1620,60 @@ impl SessionKey {
}
}
/// Shortcut to HMAC data split into two slices.
fn hmac_sha384_2(key: &[u8], a: &[u8], b: &[u8]) -> [u8; 48] {
let mut hmac = HMACSHA384::new(key);
hmac.update(a);
hmac.update(b);
hmac.finish()
/// Helper code for parsing variable length ALICE_NOISE_XK_ACK during negotiation.
struct PktReader<'a>(&'a mut [u8], usize);
impl<'a> PktReader<'a> {
fn read_u16(&mut self) -> Result<u16, Error> {
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.
///
/// This is used during Noise_XK handshaking. Each stage uses a different key to encrypt the
/// payload that is used only once per handshake and per session.
fn aes_ctr_crypt_one_time_use_key(key: &[u8], data: &mut [u8]) {
let mut ctr = AesCtr::new(key);
ctr.reset_set_iv(&[0u8; 12]);
ctr.crypt_in_place(data);
/// 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> {
let tmp = p + d.len();
if tmp <= s.len() {
s[p..tmp].copy_from_slice(d);
Ok(tmp)
} else {
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)