Now a real Noise_IK session.

This commit is contained in:
Adam Ierymenko 2022-08-10 16:05:40 -04:00
parent 248eab1a16
commit 015fe83905
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
8 changed files with 824 additions and 696 deletions

View file

@ -1,4 +1,4 @@
max_width = 300
max_width = 180
use_small_heuristics = "Max"
tab_spaces = 4
newline_style = "Unix"

View file

@ -1,7 +1,6 @@
// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
use std::ffi::c_void;
use std::mem::MaybeUninit;
extern "C" {
fn OPENSSL_cleanse(ptr: *mut c_void, len: usize);
@ -37,12 +36,15 @@ impl<const L: usize> Secret<L> {
&self.0
}
/// Get a clone of the first N bytes of this secret.
#[inline(always)]
pub fn first_n<const N: usize>(&self) -> Secret<N> {
let mut tmp: Secret<N> = unsafe { MaybeUninit::uninit().assume_init() };
tmp.0.copy_from_slice(&self.0[..N]);
tmp
pub fn first_n<const N: usize>(&self) -> &[u8; N] {
assert!(N <= L);
unsafe { &*self.0.as_ptr().cast() }
}
#[inline(always)]
pub fn first_n_clone<const N: usize>(&self) -> Secret<N> {
Secret::<N>(self.first_n().clone())
}
}
@ -50,7 +52,6 @@ impl<const L: usize> Drop for Secret<L> {
#[inline(always)]
fn drop(&mut self) {
unsafe { OPENSSL_cleanse(self.0.as_mut_ptr().cast(), L) };
std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
}
}

View file

@ -11,7 +11,7 @@ debug_events = []
[dependencies]
zerotier-core-crypto = { path = "../zerotier-core-crypto" }
pqc_kyber = { path = "../third_party/kyber", features = ["kyber512"], default-features = false }
pqc_kyber = { path = "../third_party/kyber", features = ["kyber512", "90s", "reference"], default-features = false }
async-trait = "^0"
base64 = "^0"
lz4_flex = { version = "^0", features = ["safe-encode", "safe-decode", "checked-decode"] }

View file

@ -10,10 +10,10 @@ mod inetaddress;
mod mac;
mod path;
mod peer;
mod pinknoise;
mod rootset;
mod symmetricsecret;
mod whoisqueue;
mod zssp;
pub(crate) mod node;
#[allow(unused)]

View file

@ -162,7 +162,7 @@ fn try_aead_decrypt(
/// (Note that this is a legacy cipher suite.)
fn salsa_poly_create(secret: &SymmetricSecret, header: &PacketHeader, packet_size: usize) -> (Salsa<12>, [u8; 32]) {
// Create a per-packet key from the IV, source, destination, and packet size.
let mut key: Secret<32> = secret.key.first_n();
let mut key: Secret<32> = secret.key.first_n_clone();
let hb = header.as_bytes();
for i in 0..18 {
key.0[i] ^= hb[i];

View file

@ -0,0 +1,810 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* (c)2022 ZeroTier, Inc.
* https://www.zerotier.com/
*/
use std::sync::atomic::{AtomicU64, Ordering};
use zerotier_core_crypto::aes::{Aes, AesGcm};
use zerotier_core_crypto::hash::{hmac_sha384, hmac_sha512, SHA512};
use zerotier_core_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE};
use zerotier_core_crypto::random;
use zerotier_core_crypto::secret::Secret;
use parking_lot::{Mutex, RwLock};
/// Minimum suggested buffer size for work and output buffer supplied to functions.
/// Supplying work buffers smaller than this will likely result in panics.
pub const MIN_BUFFER_SIZE: usize = 1400;
/// Start attempting to rekey after a key has been used to send packets this many times.
pub const REKEY_AFTER_USES: u64 = 1073741824;
/// Maximum random jitter to add to rekey-after usage count.
pub const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576;
/// Hard expiration after this many uses.
pub const EXPIRE_AFTER_USES: u64 = (u32::MAX - 1024) as u64;
/// Start attempting to rekey after a key has been in use for this many milliseconds.
pub const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60;
/// Maximum random jitter to add to rekey-after time.
pub const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 5;
const PACKET_TYPE_DATA: u8 = 0;
const PACKET_TYPE_NOP: u8 = 1;
const PACKET_TYPE_KEY_OFFER: u8 = 2;
const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3;
const E1_TYPE_NONE: u8 = 0;
const E1_TYPE_KYBER512_90S: u8 = 1;
// [4] counter | [6] destination session ID | [1] type
const HEADER_SIZE: usize = 11;
const AES_GCM_TAG_SIZE: usize = 16;
const HMAC_SIZE: usize = 48; // HMAC-SHA384
const SESSION_ID_SIZE: usize = 6;
// on macOS: echo -n 'pinkNoise_IKpsk2_hybrid_NISTP384_AESGCM_SHA512' | shasum -a 512 | cut -d ' ' -f 1 | xxd -r -p | xxd -i
const KEY_COMPUTATION_STARTING_SALT: [u8; 64] = [
0xa8, 0x4c, 0x50, 0x1e, 0x41, 0x84, 0x5a, 0x6e, 0x73, 0x0b, 0x39, 0xad, 0x99, 0xaa, 0x10, 0x0e, 0x79, 0x42, 0x7c, 0x52, 0xc7, 0x10, 0x91, 0xb3, 0x87, 0x96, 0xe4, 0x98, 0x76,
0x11, 0x15, 0x42, 0xd2, 0xfc, 0x3d, 0xe6, 0x19, 0xbf, 0x36, 0xab, 0x22, 0xf1, 0x62, 0xb6, 0x92, 0x3b, 0x80, 0x26, 0x0d, 0xcb, 0x16, 0xfc, 0x25, 0x4a, 0xad, 0x9a, 0x32, 0x4f,
0x37, 0xf8, 0x63, 0xeb, 0x10, 0x94,
];
const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'h';
const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'a';
const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'b';
pub enum Error {
/// The packet was addressed to an unrecognized local session
UnknownLocalSessionId(u64),
/// Packet was not well formed
InvalidPacket,
/// An invalid paramter was supplied to the function
InvalidParameter,
/// Packet failed one or more authentication checks
FailedAuthentication,
/// The supplied authenticator function rejected a new session
NewSessionRejected,
/// Rekeying failed and session secret has reached its maximum usage count
MaxKeyLifetimeExceeded,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownLocalSessionId(_) => f.write_str("UnknownLocalSessionId"),
Self::InvalidPacket => f.write_str("InvalidPacket"),
Self::InvalidParameter => f.write_str("InvalidParameter"),
Self::FailedAuthentication => f.write_str("FailedAuthentication"),
Self::NewSessionRejected => f.write_str("NewSessionRejected"),
Self::MaxKeyLifetimeExceeded => f.write_str("MaxKeyLifetimeExceeded"),
}
}
}
impl std::error::Error for Error {}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
/// Obfuscator/deobfuscator for privacy and indistinguishability masking of packets on the wire.
pub struct Obfuscator(Aes);
impl Obfuscator {
/// Create a new obfuscator for sending packets TO the provided static public identity.
pub fn new(recipient_static_public: &[u8]) -> Self {
Self(Aes::new(&SHA512::hash(recipient_static_public)[..16]))
}
}
#[allow(unused)]
pub enum ReceiveResult<'a, O> {
/// Packet is valid and contained a data payload.
OkData(&'a [u8]),
/// Packet is valid and the provided reply should be sent back.
OkSendReply(&'a [u8]),
/// Packet is valid and a new session was created, also includes a reply to be sent back.
OkNewSession(Session<O>, &'a [u8]),
/// Packet is valid, no action needs to be taken.
Ok,
/// Packet appears valid but was ignored as a duplicate.
Duplicate,
/// Packet apperas valid but was ignored for another reason.
Ignored,
}
pub struct Session<O> {
pub local_session_id: u64,
remote_session_id: AtomicU64,
outgoing_packet_counter: Counter,
psk: Secret<64>,
ss: Secret<48>,
remote_s_public_p384: [u8; P384_PUBLIC_KEY_SIZE],
outgoing_obfuscator: Obfuscator,
offer: Mutex<Option<Box<EphemeralOffer>>>,
keys: RwLock<[Option<SessionKey>; 2]>, // current, next
pub associated_object: O,
}
impl<O> Session<O> {
/// Create a new session and return this plus an outgoing packet to send to the other end.
#[allow(unused)]
pub fn new<'a, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
buffer: &'a mut [u8; MAX_PACKET_SIZE],
local_session_id: u64,
local_s_public: &[u8; STATIC_PUBLIC_SIZE],
local_s_keypair_p384: &P384KeyPair,
remote_s_public: &[u8; STATIC_PUBLIC_SIZE],
remote_s_public_p384: &P384PublicKey,
psk: &Secret<64>,
associated_object: O,
jedi: bool,
) -> Result<(Self, &'a [u8]), Error> {
debug_assert!(MAX_PACKET_SIZE >= MIN_BUFFER_SIZE);
if local_session_id > 0 && local_session_id <= 0xffffffffffff {
let counter = Counter::new();
if let Some(ss) = local_s_keypair_p384.agree(remote_s_public_p384) {
let outgoing_obfuscator = Obfuscator::new(remote_s_public);
if let Some((offer, psize)) =
EphemeralOffer::create_alice_offer(buffer, counter.next(), local_session_id, 0, local_s_public, remote_s_public_p384, &ss, &outgoing_obfuscator, jedi)
{
return Ok((
Self {
local_session_id,
remote_session_id: AtomicU64::new(0),
outgoing_packet_counter: counter,
psk: psk.clone(),
ss,
remote_s_public_p384: remote_s_public_p384.as_bytes().clone(),
outgoing_obfuscator,
offer: Mutex::new(Some(offer)),
keys: RwLock::new([None, None]),
associated_object,
},
&buffer[..psize],
));
}
}
}
return Err(Error::InvalidParameter);
}
}
/// Receive a packet from the network and take the appropriate action.
///
/// Check ReceiveResult to see if it includes data or a reply packet.
#[allow(unused)]
pub fn receive<
'a,
SessionLookupFunction: FnOnce(u64) -> Option<S>,
NewSessionAuthenticatorFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<(u64, [u8; P384_PUBLIC_KEY_SIZE], Secret<64>, O)>,
S: std::ops::Deref<Target = Session<O>>,
O,
const MAX_PACKET_SIZE: usize,
const STATIC_PUBLIC_SIZE: usize,
>(
incoming_packet: &[u8],
buffer: &'a mut [u8; MAX_PACKET_SIZE],
local_s_keypair_p384: &P384KeyPair,
incoming_obfuscator: &Obfuscator,
session_lookup: SessionLookupFunction,
new_session_auth: NewSessionAuthenticatorFunction,
current_time: i64,
jedi: bool,
) -> Result<ReceiveResult<'a, O>, Error> {
debug_assert!(MAX_PACKET_SIZE >= MIN_BUFFER_SIZE);
if incoming_packet.len() > MAX_PACKET_SIZE || incoming_packet.len() <= 16 {
unlikely_branch();
return Err(Error::InvalidPacket);
}
incoming_obfuscator.0.decrypt_block(&incoming_packet[0..16], &mut buffer[0..16]);
let local_session_id = u64::from_le_bytes(buffer[2..10].try_into().unwrap()).wrapping_shr(16);
let packet_type = buffer[10];
if packet_type <= PACKET_TYPE_NOP {
if let Some(session) = session_lookup(local_session_id) {
let keys = session.keys.read();
for ki in 0..2 {
if let Some(key) = keys[ki].as_ref() {
let mut c = key.get_receive_cipher();
c.init(&get_aes_gcm_nonce(buffer));
c.crypt_in_place(&mut buffer[HEADER_SIZE..16]);
let data_len = incoming_packet.len() - AES_GCM_TAG_SIZE;
c.crypt(&incoming_packet[16..data_len], &mut buffer[16..data_len]);
let tag = c.finish();
key.return_receive_cipher(c);
if tag.eq(&incoming_packet[data_len..]) {
// If this is the "next" key, a valid packet using it indicates that it should become the current key.
if ki == 1 {
unlikely_branch();
drop(keys);
let mut keys = session.keys.write();
keys[0] = keys[1].take();
}
if packet_type == PACKET_TYPE_DATA {
return Ok(ReceiveResult::OkData(&buffer[HEADER_SIZE..data_len]));
} else {
unlikely_branch();
return Ok(ReceiveResult::Ok);
}
}
}
}
return Err(Error::FailedAuthentication);
} else {
unlikely_branch();
return Err(Error::UnknownLocalSessionId(local_session_id));
}
} else {
unlikely_branch();
let session = if local_session_id == 0 {
None
} else {
let s = session_lookup(local_session_id);
if s.is_none() {
return Err(Error::UnknownLocalSessionId(local_session_id));
}
s
};
if incoming_packet.len() >= (HEADER_SIZE + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + HMAC_SIZE) {
incoming_obfuscator.0.decrypt_block(&incoming_packet[16..32], &mut buffer[16..32]);
incoming_obfuscator.0.decrypt_block(&incoming_packet[32..48], &mut buffer[32..48]);
incoming_obfuscator.0.decrypt_block(&incoming_packet[48..64], &mut buffer[48..64]);
buffer[64..incoming_packet.len()].copy_from_slice(&incoming_packet[64..]);
} else {
return Err(Error::InvalidPacket);
}
match packet_type {
PACKET_TYPE_KEY_OFFER => {
// alice (remote) -> bob (local)
let (alice_e0_public, e0s) = P384PublicKey::from_bytes(&buffer[HEADER_SIZE..HEADER_SIZE + P384_PUBLIC_KEY_SIZE])
.and_then(|pk| local_s_keypair_p384.agree(&pk).map(move |s| (pk, s)))
.ok_or(Error::FailedAuthentication)?;
let key = Secret(hmac_sha512(&hmac_sha512(&KEY_COMPUTATION_STARTING_SALT, alice_e0_public.as_bytes()), e0s.as_bytes()));
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::<32>(), false);
c.init(&get_aes_gcm_nonce(buffer));
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..incoming_packet.len() - (AES_GCM_TAG_SIZE + HMAC_SIZE)]);
if !c.finish().eq(&buffer[incoming_packet.len() - (AES_GCM_TAG_SIZE + HMAC_SIZE)..incoming_packet.len() - HMAC_SIZE]) {
return Err(Error::FailedAuthentication);
}
drop(c);
let (alice_session_id, alice_s_public, alice_e1_public) = parse_KEY_OFFER_after_header(buffer)?;
let new_session = if let Some(session) = session.as_ref() {
None
} else {
if let Some((local_session_id, remote_s_public_p384, psk, associated_object)) = new_session_auth(&alice_s_public) {
if let Some(ss) = P384PublicKey::from_bytes(&remote_s_public_p384).and_then(|pk| local_s_keypair_p384.agree(&pk)) {
Some(Session::<O> {
local_session_id, // Bob's session ID
remote_session_id: AtomicU64::new(alice_session_id),
outgoing_packet_counter: Counter::new(),
psk,
ss,
remote_s_public_p384, // Bob's P-384 static public key
outgoing_obfuscator: Obfuscator::new(&alice_s_public),
offer: Mutex::new(None),
keys: RwLock::new([None, None]),
associated_object,
})
} else {
return Err(Error::FailedAuthentication);
}
} else {
return Err(Error::NewSessionRejected);
}
};
let session = session.as_ref().map_or_else(|| new_session.as_ref().unwrap(), |s| &*s);
let key = Secret(hmac_sha512(key.as_bytes(), session.ss.as_bytes()));
if !hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &buffer[..incoming_packet.len() - HMAC_SIZE])
.eq(&buffer[incoming_packet.len() - HMAC_SIZE..incoming_packet.len()])
{
return Err(Error::FailedAuthentication);
}
// Alice's offer has been verified and her key state reconstructed.
session.remote_session_id.store(alice_session_id, Ordering::Relaxed);
let counter = session.outgoing_packet_counter.next();
let bob_e0_keypair = P384KeyPair::generate();
let e0e0 = bob_e0_keypair.agree(&alice_e0_public).ok_or(Error::FailedAuthentication)?;
let se0 = P384PublicKey::from_bytes(&session.remote_s_public_p384).and_then(|pk| bob_e0_keypair.agree(&pk)).ok_or(Error::FailedAuthentication)?;
let (bob_e1_public, e1e1) = if jedi && alice_e1_public.is_some() {
if let Ok((bob_e1_public, e1e1)) = pqc_kyber::encapsulate(alice_e1_public.as_ref().unwrap(), &mut random::SecureRandom::default()) {
(Some(bob_e1_public), Secret(e1e1))
} else {
return Err(Error::FailedAuthentication);
}
} else {
(None, Secret::default()) // use all zero Kyber secret if disabled
};
// FIPS note: the order of HMAC parameters are flipped here from the usual Noise HMAC(key, X). That's because
// NIST/FIPS allows HKDF with HMAC(salt, key) and salt is allowed to be anything. This way if the PSK is not
// FIPS compliant the compliance of the entire key derivation is not invalidated. It can just be considered a
// salt. Since both inputs are fixed size secrets nobody else can control this shouldn't be cryptographically
// meaningful.
let key = Secret(hmac_sha512(
session.psk.as_bytes(),
&hmac_sha512(&hmac_sha512(&hmac_sha512(key.as_bytes(), bob_e0_keypair.public_key_bytes()), e0e0.as_bytes()), se0.as_bytes()),
));
// At this point we've completed standard Noise_IK key derivation, but see the extra step after AES-GCM below...
let mut counter_offer_size =
assemble_KEY_COUNTER_OFFER(buffer, counter, alice_session_id, bob_e0_keypair.public_key(), session.local_session_id, bob_e1_public.as_ref());
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::<32>(), true);
c.init(&get_aes_gcm_nonce(buffer));
c.crypt_in_place(&mut buffer[HEADER_SIZE + P384_PUBLIC_KEY_SIZE..counter_offer_size]);
buffer[counter_offer_size..counter_offer_size + AES_GCM_TAG_SIZE].copy_from_slice(&c.finish());
counter_offer_size += AES_GCM_TAG_SIZE;
// NOTE: this is the only major departure from standard Noise_IK: we mix the optional Kyber key
// AFTER mixing the PSK. The Kyber key could have been mixed with the PSK but that would create a
// chicken-or-egg problem due to the fact that we encrypt Kyber. How would Alice decrypt it? The
// extra HMAC below authenticates the entire exchange including Kyber.
let key = Secret(hmac_sha512(e1e1.as_bytes(), key.as_bytes()));
let hmac = hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &buffer[..counter_offer_size]);
buffer[counter_offer_size..counter_offer_size + HMAC_SIZE].copy_from_slice(&hmac);
counter_offer_size += HMAC_SIZE;
let _ = session.keys.write()[1].replace(SessionKey::new(key, Role::Bob, current_time, counter, jedi));
// Bob now has final key state for this exchange. Yay! Now reply to Alice so she can construct it.
session.outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[0..16]);
session.outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[16..32]);
session.outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[32..48]);
session.outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[48..64]);
return new_session
.map_or_else(|| Ok(ReceiveResult::OkSendReply(&buffer[..counter_offer_size])), |ns| Ok(ReceiveResult::OkNewSession(ns, &buffer[..counter_offer_size])));
}
PACKET_TYPE_KEY_COUNTER_OFFER => {
// bob (remote) -> alice (local)
if let Some(session) = session {
} else {
return Err(Error::InvalidPacket);
}
/*
let (remote_e0_public, local_static_remote_e0) = decrypt_aead_key_exchange::<BS, STATIC_PUBLIC_SIZE>(incoming_packet, buffer, remote_to_local_obfuscator, local_static_p384_secret)?;
if let Some(session) = session {
let (remote_session_id, in_re_offer_id, remote_e1_public) = parse_KEY_COUNTER_OFFER_after_header(buffer)?;
if let Some(last_offer) = session.offer.lock().take() {
if last_offer.id.eq(&in_re_offer_id) {
if let Some(ee0) = last_offer.local_e0_secret.agree(&remote_e0_public) {
let local_e1_remote_e1 = last_offer.local_e1_secret.and_then(|e1| {
if let Some(remote_e1_public) = remote_e1_public {
if let Ok(ee1) = pqc_kyber::decapsulate(&remote_e1_public, &e1.secret) {
Some(Secret(ee1))
} else {
None
}
} else {
None
}
});
let counter = session.outgoing_packet_counter.next();
let new_shared_secret = SessionKey::new(
&last_offer.local_e0_remote_static,
&local_static_remote_e0,
&session.static_secret,
ee0,
local_e1_remote_e1,
current_time,
counter,
false,
);
let new_secret_key = new_shared_secret.key.clone();
let mut session_shared_secret = session.shared_secret.write();
session_shared_secret.previous = session_shared_secret.current.replace(new_shared_secret);
let _ = session_shared_secret.next.take();
drop(session_shared_secret); // release lock
let mut reply: packed::KeyCounterOfferAck = packed::zeroed();
reply.t = PACKET_TYPE_KEY_COUNTER_OFFER_ACK;
reply.counter = counter.to_bytes();
reply.to_session_id.copy_from_slice(&remote_session_id.to_le_bytes()[..6]);
let mut hmac = HMACSHA384::new(zt_kbkdf_hmac_sha384(&new_secret_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_HMAC).as_bytes());
hmac.update(&packed::as_bytes(&reply)[..HEADER_SIZE]);
hmac.update(new_secret_key.as_bytes());
reply.hmac_shared_secret = hmac.finish();
reply.hmac_static_key = hmac_sha384(
zt_kbkdf_hmac_sha384(&session.static_secret.0[..48], KBKDF_KEY_USAGE_LABEL_HMAC).as_bytes(),
&packed::as_bytes(&reply)[..size_of::<packed::KeyCounterOfferAck>() - 48],
);
buffer[..size_of::<packed::KeyCounterOfferAck>()].copy_from_slice(&packed::as_bytes(&reply)[..size_of::<packed::KeyCounterOfferAck>()]);
session.local_to_remote_obfuscator.0.encrypt_block_in_place(&mut buffer[0..16]);
return Ok(ReceiveResult::OkSendReply(&buffer[..HEADER_SIZE + HMAC_SIZE + HMAC_SIZE]));
} else {
return Err(Error::InvalidPacket);
}
}
}
}
return Ok(ReceiveResult::Ignored);
*/
todo!()
}
_ => return Err(Error::InvalidPacket),
}
}
}
struct Counter(AtomicU64);
impl Counter {
fn new() -> Self {
Self(AtomicU64::new(0))
}
#[inline(always)]
fn next(&self) -> CounterValue {
CounterValue(self.0.fetch_add(1, Ordering::SeqCst))
}
}
/// A value of the outgoing packet counter.
///
/// The counter is internally 64-bit so we can more easily track usage limits without
/// confusing modular difference stuff. The counter as seen externally and placed in
/// packets is the least significant 32 bits.
#[repr(transparent)]
#[derive(Copy, Clone)]
struct CounterValue(u64);
impl CounterValue {
#[inline(always)]
pub fn to_bytes(&self) -> [u8; 4] {
(self.0 as u32).to_le_bytes()
}
}
struct KeyLifetime {
rekey_at_or_after_counter: u64,
hard_expire_at_counter: u64,
rekey_at_or_after_timestamp: i64,
}
impl KeyLifetime {
fn new(current_counter: CounterValue, current_time: i64) -> Self {
Self {
rekey_at_or_after_counter: current_counter.0 + REKEY_AFTER_USES + (random::next_u32_secure() % REKEY_AFTER_TIME_MS_MAX_JITTER) as u64,
hard_expire_at_counter: current_counter.0 + EXPIRE_AFTER_USES,
rekey_at_or_after_timestamp: current_time + REKEY_AFTER_TIME_MS + (random::next_u32_secure() % REKEY_AFTER_TIME_MS_MAX_JITTER) as i64,
}
}
#[inline(always)]
fn expired(&self, counter: CounterValue) -> bool {
counter.0 < self.hard_expire_at_counter
}
}
/// Ephemeral offer sent with KEY_OFFER and rememebered so state can be reconstructed on COUNTER_OFFER.
#[allow(unused)]
struct EphemeralOffer {
key: Secret<64>, // key "under construction"
alice_e0_keypair: P384KeyPair,
alice_e1_keypair: Option<pqc_kyber::Keypair>,
}
impl EphemeralOffer {
fn create_alice_offer<const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
buffer: &mut [u8; MAX_PACKET_SIZE],
counter: CounterValue,
alice_session_id: u64,
bob_session_id: u64,
alice_s_public: &[u8; STATIC_PUBLIC_SIZE],
bob_s_public_p384: &P384PublicKey,
ss: &Secret<48>,
outgoing_obfuscator: &Obfuscator, // bobfuscator?
jedi: bool,
) -> Option<(Box<EphemeralOffer>, usize)> {
debug_assert!(MAX_PACKET_SIZE >= MIN_BUFFER_SIZE);
let alice_e0_keypair = P384KeyPair::generate();
let e0s = alice_e0_keypair.agree(bob_s_public_p384)?;
let alice_e1_keypair = if jedi { Some(pqc_kyber::keypair(&mut random::SecureRandom::get())) } else { None };
let key = Secret(hmac_sha512(&hmac_sha512(&KEY_COMPUTATION_STARTING_SALT, alice_e0_keypair.public_key_bytes()), e0s.as_bytes()));
let mut packet_size =
assemble_KEY_OFFER(buffer, counter, bob_session_id, alice_e0_keypair.public_key(), alice_session_id, alice_s_public, alice_e1_keypair.as_ref().map(|s| &s.public));
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<32>(), true);
c.init(&get_aes_gcm_nonce(buffer));
c.crypt_in_place(&mut buffer[HEADER_SIZE + P384_PUBLIC_KEY_SIZE..packet_size]);
buffer[packet_size..packet_size + AES_GCM_TAG_SIZE].copy_from_slice(&c.finish());
packet_size += AES_GCM_TAG_SIZE;
let key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes()));
let hmac = hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &buffer[..packet_size]);
buffer[packet_size..packet_size + HMAC_SIZE].copy_from_slice(&hmac);
packet_size += HMAC_SIZE;
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[0..16]);
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[16..32]);
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[32..48]);
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[48..64]);
Some((Box::new(EphemeralOffer { key, alice_e0_keypair, alice_e1_keypair }), packet_size + AES_GCM_TAG_SIZE + HMAC_SIZE))
}
}
enum Role {
Alice,
Bob,
}
#[allow(unused)]
struct SessionKey {
lifetime: KeyLifetime,
receive_key: Secret<32>,
send_key: Secret<32>,
receive_cipher_pool: Mutex<Vec<Box<AesGcm>>>,
send_cipher_pool: Mutex<Vec<Box<AesGcm>>>,
role: Role,
jedi: bool, // true if kyber was used
}
impl SessionKey {
/// Create a new symmetric shared session key and set its key expiration times, etc.
fn new(key: Secret<64>, role: Role, current_time: i64, current_counter: CounterValue, jedi: bool) -> Self {
let a2b: Secret<32> = kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n_clone();
let b2a: Secret<32> = kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n_clone();
let (receive_key, send_key) = match role {
Role::Alice => (b2a, a2b),
Role::Bob => (a2b, b2a),
};
Self {
lifetime: KeyLifetime::new(current_counter, current_time),
receive_key,
send_key,
receive_cipher_pool: Mutex::new(Vec::with_capacity(2)),
send_cipher_pool: Mutex::new(Vec::with_capacity(2)),
role,
jedi,
}
}
#[inline(always)]
fn get_send_cipher(&self, counter: CounterValue) -> Result<Box<AesGcm>, Error> {
if !self.lifetime.expired(counter) {
Ok(self.send_cipher_pool.lock().pop().unwrap_or_else(|| Box::new(AesGcm::new(self.send_key.as_bytes(), true))))
} else {
unlikely_branch();
Err(Error::MaxKeyLifetimeExceeded)
}
}
#[inline(always)]
fn return_send_cipher(&self, c: Box<AesGcm>) {
self.send_cipher_pool.lock().push(c);
}
#[inline(always)]
fn get_receive_cipher(&self) -> Box<AesGcm> {
self.receive_cipher_pool.lock().pop().unwrap_or_else(|| Box::new(AesGcm::new(self.receive_key.as_bytes(), false)))
}
#[inline(always)]
fn return_receive_cipher(&self, c: Box<AesGcm>) {
self.receive_cipher_pool.lock().push(c);
}
}
#[allow(non_snake_case)]
fn assemble_KEY_OFFER<const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
buffer: &mut [u8; MAX_PACKET_SIZE],
counter: CounterValue,
bob_session_id: u64,
alice_e0_public: &P384PublicKey,
alice_session_id: u64,
alice_s_public: &[u8; STATIC_PUBLIC_SIZE],
alice_e1_public: Option<&[u8; pqc_kyber::KYBER_PUBLICKEYBYTES]>,
) -> usize {
buffer[0..4].copy_from_slice(&counter.to_bytes());
buffer[4..10].copy_from_slice(&bob_session_id.to_le_bytes()[..SESSION_ID_SIZE]);
buffer[10] = PACKET_TYPE_KEY_OFFER;
let mut b = &mut buffer[HEADER_SIZE..];
b[..P384_PUBLIC_KEY_SIZE].copy_from_slice(alice_e0_public.as_bytes());
b = &mut b[P384_PUBLIC_KEY_SIZE..];
b[..SESSION_ID_SIZE].copy_from_slice(&alice_session_id.to_le_bytes()[..SESSION_ID_SIZE]);
b = &mut b[SESSION_ID_SIZE..];
b[..STATIC_PUBLIC_SIZE].copy_from_slice(alice_s_public);
b = &mut b[STATIC_PUBLIC_SIZE..];
if let Some(k) = alice_e1_public {
b[0] = E1_TYPE_KYBER512_90S;
b[1..1 + pqc_kyber::KYBER_PUBLICKEYBYTES].copy_from_slice(k);
b = &mut b[1 + pqc_kyber::KYBER_PUBLICKEYBYTES..];
} else {
b[0] = E1_TYPE_NONE;
b = &mut b[1..];
}
b[0] = 0;
b[1] = 0; // reserved for future use
b = &mut b[2..];
let random_padding_len = (random::next_u32_secure() as usize) % (MAX_PACKET_SIZE - (b.len() + AES_GCM_TAG_SIZE + HMAC_SIZE));
random::fill_bytes_secure(&mut b[..random_padding_len]);
b = &mut b[random_padding_len..];
b.len()
}
#[allow(non_snake_case)]
fn parse_KEY_OFFER_after_header<const STATIC_PUBLIC_SIZE: usize>(mut b: &[u8]) -> Result<(u64, [u8; STATIC_PUBLIC_SIZE], Option<[u8; pqc_kyber::KYBER_PUBLICKEYBYTES]>), Error> {
if b.len() >= SESSION_ID_SIZE {
let alice_session_id = u48_from_le_bytes(b);
b = &b[SESSION_ID_SIZE..];
if b.len() >= STATIC_PUBLIC_SIZE {
let alice_s_public: [u8; STATIC_PUBLIC_SIZE] = b[..STATIC_PUBLIC_SIZE].try_into().unwrap();
b = &b[STATIC_PUBLIC_SIZE..];
if b.len() >= 1 {
let e1_type = b[0];
b = &b[1..];
let alice_e1_public = if e1_type == E1_TYPE_KYBER512_90S {
if b.len() >= pqc_kyber::KYBER_PUBLICKEYBYTES {
let k: [u8; pqc_kyber::KYBER_PUBLICKEYBYTES] = b[..pqc_kyber::KYBER_PUBLICKEYBYTES].try_into().unwrap();
b = &b[pqc_kyber::KYBER_PUBLICKEYBYTES..];
Some(k)
} else {
return Err(Error::InvalidPacket);
}
} else {
None
};
if b.len() >= 2 {
return Ok((alice_session_id, alice_s_public, alice_e1_public));
}
}
}
}
return Err(Error::InvalidPacket);
}
#[allow(non_snake_case)]
fn assemble_KEY_COUNTER_OFFER<const MAX_PACKET_SIZE: usize>(
buffer: &mut [u8; MAX_PACKET_SIZE],
counter: CounterValue,
alice_session_id: u64,
bob_e0_public: &P384PublicKey,
bob_session_id: u64,
bob_e1_public: Option<&[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>,
) -> usize {
buffer[0..4].copy_from_slice(&counter.to_bytes());
buffer[4..10].copy_from_slice(&alice_session_id.to_le_bytes()[..SESSION_ID_SIZE]);
buffer[10] = PACKET_TYPE_KEY_COUNTER_OFFER;
let mut b = &mut buffer[HEADER_SIZE..];
b[..P384_PUBLIC_KEY_SIZE].copy_from_slice(bob_e0_public.as_bytes());
b = &mut b[P384_PUBLIC_KEY_SIZE..];
b[..SESSION_ID_SIZE].copy_from_slice(&bob_session_id.to_le_bytes()[..SESSION_ID_SIZE]);
b = &mut b[SESSION_ID_SIZE..];
if let Some(k) = bob_e1_public {
b[0] = E1_TYPE_KYBER512_90S;
b[1..1 + pqc_kyber::KYBER_CIPHERTEXTBYTES].copy_from_slice(k);
b = &mut b[1 + pqc_kyber::KYBER_CIPHERTEXTBYTES..];
} else {
b[0] = E1_TYPE_NONE;
b = &mut b[1..];
}
b[0] = 0;
b[1] = 0; // reserved for future use
b = &mut b[2..];
let random_padding_len = (random::next_u32_secure() as usize) % (MAX_PACKET_SIZE - (b.len() + AES_GCM_TAG_SIZE + HMAC_SIZE));
random::fill_bytes_secure(&mut b[..random_padding_len]);
b = &mut b[random_padding_len..];
b.len()
}
#[allow(non_snake_case)]
fn parse_KEY_COUNTER_OFFER_after_header(mut b: &[u8]) -> Result<(u64, Option<[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>), Error> {
if b.len() >= SESSION_ID_SIZE {
let bob_session_id = u48_from_le_bytes(b);
b = &b[SESSION_ID_SIZE..];
if b.len() >= 1 {
let e1_type = b[0];
b = &b[1..];
let bob_e1_public = if e1_type == E1_TYPE_KYBER512_90S {
if b.len() >= pqc_kyber::KYBER_CIPHERTEXTBYTES {
let k: [u8; pqc_kyber::KYBER_CIPHERTEXTBYTES] = b[..pqc_kyber::KYBER_CIPHERTEXTBYTES].try_into().unwrap();
b = &b[pqc_kyber::KYBER_CIPHERTEXTBYTES..];
Some(k)
} else {
return Err(Error::InvalidPacket);
}
} else {
None
};
if b.len() >= 1 && b[0] == 0 {
return Ok((bob_session_id, bob_e1_public));
}
}
}
return Err(Error::InvalidPacket);
}
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]))
}
#[inline(always)]
fn get_aes_gcm_nonce(deobfuscated_packet: &[u8]) -> [u8; 16] {
let mut tmp = 0_u128.to_ne_bytes();
tmp[..HEADER_SIZE].copy_from_slice(deobfuscated_packet);
tmp
}
#[inline(always)]
fn u48_from_le_bytes(b: &[u8]) -> u64 {
(b[0] as u64)
| (b[1] as u64).wrapping_shl(8)
| (b[2] as u64).wrapping_shl(16)
| (b[3] as u64).wrapping_shl(24)
| (b[4] as u64).wrapping_shl(32)
| (b[5] as u64).wrapping_shl(40)
}
#[cold]
#[inline(never)]
extern "C" fn unlikely_branch() {}

View file

@ -22,8 +22,8 @@ impl SymmetricSecret {
/// Create a new symmetric secret, deriving all sub-keys and such.
pub fn new(key: Secret<64>) -> SymmetricSecret {
let aes_factory = AesGmacSivPoolFactory(
zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0).first_n(),
zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1).first_n(),
zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0).first_n_clone(),
zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1).first_n_clone(),
);
SymmetricSecret { key, aes_gmac_siv: Pool::new(2, aes_factory) }
}

View file

@ -1,683 +0,0 @@
// (c) 2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
use std::fmt::Display;
use std::mem::size_of;
use std::sync::atomic::{AtomicU64, Ordering};
use zerotier_core_crypto::aes::{Aes, AesGcm};
use zerotier_core_crypto::hash::{hmac_sha384, hmac_sha512, HMACSHA384, SHA384, SHA512};
use zerotier_core_crypto::kbkdf::zt_kbkdf_hmac_sha384;
use zerotier_core_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE};
use zerotier_core_crypto::random;
use zerotier_core_crypto::secret::Secret;
use parking_lot::{Mutex, RwLock};
/// Minimum suggested buffer size for work and output buffer supplied to functions.
/// Supplying work buffers smaller than this will likely result in panics.
pub const BUFFER_SIZE_MIN: usize = 1400;
/// Start attempting to rekey after a key has been used to send packets this many times.
pub const REKEY_AFTER_USES: u64 = 1073741824;
/// Start attempting to rekey after a key has been in use for this many milliseconds.
pub const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60;
#[derive(Debug)]
pub enum Error {
/// The packet was addressed to an unrecognized local session.
UnknownLocalSessionId,
/// Packet was not well formed.
InvalidPacket,
/// An invalid paramter was supplied.
InvalidParameter,
/// Packet failed one or more authentication checks.
FailedAuthentication,
/// The supplied authenticator function rejected a new session.
NewSessionRejected,
/// An unexpected error occurred, such as an internal error.
UnexpectedError,
/// Shared secret has far exceeded rekey thresholds (will only happen if rekeying isn't done or repeatedly fails)
MaxKeyLifetimeExceeded,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownLocalSessionId => f.write_str("zssp::Error::UnknownLocalSessionId"),
Self::InvalidPacket => f.write_str("zssp::Error::InvalidPacket"),
Self::InvalidParameter => f.write_str("zssp::Error::InvalidParameter"),
Self::FailedAuthentication => f.write_str("zssp::Error::FailedAuthentication"),
Self::NewSessionRejected => f.write_str("zssp::Error::NewSessionRejected"),
Self::UnexpectedError => f.write_str("zssp::Error::UnexpectedError"),
Self::MaxKeyLifetimeExceeded => f.write_str("zssp::Error::MaxKeyLifetimeExceeded"),
}
}
}
impl std::error::Error for Error {}
pub enum ReceiveResult<'a> {
/// Packet is valid and contained a data payload.
OkData(&'a [u8]),
/// Packet is valid, no action needs to be taken.
Ok,
/// Packet is valid and the provided reply should be sent back.
OkSendReply(&'a [u8]),
/// Packet appears valid but was ignored as a duplicate.
Duplicate,
/// Packet apperas valid but was ignored for another reason.
Ignored,
}
pub struct Obfuscator(Aes);
impl Obfuscator {
pub fn new(static_public: &[u8]) -> Self {
Self(Aes::new(&SHA384::hash(static_public)[..16]))
}
}
pub struct Session<O> {
pub local_session_id: u64,
pub creation_time: i64,
remote_session_id: AtomicU64,
outgoing_packet_counter: Counter,
static_secret: Secret<64>,
local_to_remote_obfuscator: Obfuscator,
offer: Mutex<Option<Box<EphemeralOffer>>>,
shared_secret: RwLock<SymmetricKeyQueue>, // previous, current, next
pub associated_object: O,
}
impl<O> Session<O> {
/// Create a new session and return this plus an outgoing packet to call the other side.
///
/// If 'jedi' is true, the newly approved NIST post-quantum Kyber algorithm is used.
///
/// The supplied 'local_session_id' must not be zero or this will return an error. An error may also be
/// returned if a supplied key is invalid or an internal error occurs.
pub fn new<'a, const BS: usize, const STATIC_PUBLIC_SIZE: usize>(
buffer: &'a mut [u8; BS],
associated_object: O,
local_session_id: u64,
local_static_public: &[u8; STATIC_PUBLIC_SIZE],
static_secret: &Secret<64>,
remote_static_public: &[u8; STATIC_PUBLIC_SIZE],
remote_static_p384_public: &P384PublicKey,
current_time: i64,
jedi: bool,
) -> Result<(Self, &'a [u8]), Error> {
debug_assert!(BS >= BUFFER_SIZE_MIN);
if local_session_id != 0 {
let counter = Counter::new();
let local_to_remote_obfuscator = Obfuscator::new(remote_static_public);
if let Some((offer, offer_packet_size)) = EphemeralOffer::create(buffer, counter.next(), local_session_id, 0, local_static_public, remote_static_p384_public, static_secret, &local_to_remote_obfuscator, jedi) {
return Ok((
Self {
local_session_id,
creation_time: current_time,
remote_session_id: AtomicU64::new(0),
outgoing_packet_counter: counter,
static_secret: static_secret.clone(),
local_to_remote_obfuscator,
offer: Mutex::new(Some(offer)),
shared_secret: RwLock::new(SymmetricKeyQueue { current: None, previous: None, next: None }),
associated_object,
},
&buffer[..offer_packet_size],
));
}
}
return Err(Error::InvalidParameter);
}
}
/// Receive a packet from the network and take the appropriate action.
///
/// Sessions returned by 'session_lookup' are anything that implements Deref for the correct session type.
/// This allows external code to use Arc<>, Box<>, etc. to contain sessions at its discretion.
///
/// The authenticator must return a non-zero new local session ID and a value for associated_object if
/// the new session should be accepted.
///
/// If 'jedi' is true this side will support and reciprocate Kyber key exchanges. Otherwise these will
/// be ignored and our responses will not contain them, causing the session to not be quantum-forward-secure.
///
/// Check ReceiveResult to see if it includes data or a reply packet.
#[inline(always)]
pub fn receive<
'a,
StaticStaticAgreeFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<Secret<64>>,
P384PublicKeyExtractFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<P384PublicKey>,
SessionLookupFunction: FnOnce(u64) -> Option<S>,
NewSessionAuthenticatorFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<(u64, O)>,
S: std::ops::Deref<Target = Session<O>>,
O,
const BS: usize,
const STATIC_PUBLIC_SIZE: usize,
>(
incoming_packet: &[u8],
buffer: &'a mut [u8; BS],
local_static_p384_secret: &P384KeyPair,
remote_to_local_obfuscator: &Obfuscator,
static_static_agree: StaticStaticAgreeFunction,
extract_static_p384_public: P384PublicKeyExtractFunction,
session_lookup: SessionLookupFunction,
new_session_auth: NewSessionAuthenticatorFunction,
current_time: i64,
jedi: bool,
) -> Result<ReceiveResult<'a>, Error> {
debug_assert!(BS >= BUFFER_SIZE_MIN);
debug_assert!(BS >= size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>());
if incoming_packet.len() > BS {
unlikely_branch();
return Err(Error::InvalidParameter);
}
if incoming_packet.len() < 16 {
unlikely_branch();
return Err(Error::InvalidPacket);
}
remote_to_local_obfuscator.0.decrypt_block(&incoming_packet[0..16], &mut buffer[0..16]);
let local_session_id = u64::from_le_bytes(buffer[3..11].try_into().unwrap()).wrapping_shr(16);
let packet_type = buffer[0];
if packet_type == PACKET_TYPE_DATA {
// This is checked immediately so it becomes the fast path and all other branches can be treated as unlikely by the compiler.
if let Some(session) = session_lookup(local_session_id) {
todo!()
} else {
unlikely_branch();
return Err(Error::UnknownLocalSessionId);
}
} else {
unlikely_branch();
let session = if local_session_id == 0 {
None
} else {
let s = session_lookup(local_session_id);
if s.is_none() {
return Err(Error::UnknownLocalSessionId);
}
s
};
match packet_type {
PACKET_TYPE_KEY_OFFER => {
if incoming_packet.len() < size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() {
return Err(Error::InvalidPacket);
}
let (remote_e0, local_static_remote_e0) = decrypt_and_auth_key_exchange_packet::<BS, STATIC_PUBLIC_SIZE>(incoming_packet, buffer, remote_to_local_obfuscator, local_static_p384_secret)?;
let p = packed::as_packed_struct::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>(buffer).unwrap();
let remote_static_public = p.static_public; // eliminate possibly spurious alignment error
let remote_session_id = u48_from_le_bytes(&p.from_session_id);
if let Some(session) = session {
let ss_hmac_key = zt_kbkdf_hmac_sha384(&session.static_secret.0[..48], KBKDF_KEY_USAGE_LABEL_HMAC);
if !hmac_sha384(ss_hmac_key.as_bytes(), &buffer[..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - 48]).eq(&buffer[size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - 48..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>()]) {
return Err(Error::FailedAuthentication);
}
// Learn remote session ID. If already set this does nothing. It could theoretically change without consequence.
session.remote_session_id.store(remote_session_id, Ordering::Relaxed);
// Drop any old offers we've made since we are now making a counter offer to their offer.
let _ = session.offer.lock().take();
let local_e0_secret = P384KeyPair::generate();
let (local_e1_secret, local_e1_remote_e1) = if jedi && p.e1.iter().any(|b| *b != 0) {
if let Ok((local_e1_secret, local_e1_remote_e1)) = pqc_kyber::encapsulate(&p.e1, &mut random::SecureRandom::default()) {
(Some(local_e1_secret), Some(local_e1_remote_e1))
} else {
(None, None)
}
} else {
(None, None)
};
// TODO: locally agree, make local shared secret, etc.
let mut reply: packed::KeyCounterOffer<STATIC_PUBLIC_SIZE> = packed::zeroed();
reply.t = PACKET_TYPE_KEY_COUNTER_OFFER;
//rp.counter = session.outgoing_packet_counter.fetch_add(1, Ordering::SeqCst).to_le_bytes();
reply.to_session_id.copy_from_slice(&remote_session_id.to_le_bytes()[..6]);
reply.e0 = local_e0_secret.public_key_bytes().clone();
reply.from_session_id.copy_from_slice(&session.local_session_id.to_le_bytes()[..6]);
todo!()
} else {
// This is an initial key exchange attempt, so do full packet validation and then check to see if we should accept.
let static_secret = if let Some(static_secret) = static_static_agree(&remote_static_public) {
static_secret
} else {
return Err(Error::InvalidPacket);
};
let ss_hmac_key = zt_kbkdf_hmac_sha384(&static_secret.0[..48], KBKDF_KEY_USAGE_LABEL_HMAC);
if !hmac_sha384(ss_hmac_key.as_bytes(), &buffer[..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - 48]).eq(&buffer[size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - 48..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>()]) {
return Err(Error::FailedAuthentication);
}
if let Some((local_session_id, obj)) = new_session_auth(&remote_static_public) {
todo!()
} else {
return Err(Error::NewSessionRejected);
}
}
}
PACKET_TYPE_KEY_COUNTER_OFFER => {
if incoming_packet.len() < size_of::<packed::KeyCounterOffer<STATIC_PUBLIC_SIZE>>() {
return Err(Error::InvalidPacket);
}
let (remote_e0, local_static_remote_e0) = decrypt_and_auth_key_exchange_packet::<BS, STATIC_PUBLIC_SIZE>(incoming_packet, buffer, remote_to_local_obfuscator, local_static_p384_secret)?;
if let Some(session) = session {
let p = packed::as_packed_struct::<packed::KeyCounterOffer<STATIC_PUBLIC_SIZE>>(buffer).unwrap();
let remote_session_id = u48_from_le_bytes(&p.from_session_id);
if let Some(last_offer) = session.offer.lock().take() {
if last_offer.id.eq(&p.in_re_offer_id) {
if let Some(ee0) = last_offer.alice_e0.agree(&remote_e0) {
let local_e1_remote_e1 = last_offer.alice_e1.and_then(|e1| {
if p.e1.iter().any(|b| *b != 0) {
if let Ok(ee1) = pqc_kyber::decapsulate(&p.e1[..pqc_kyber::KYBER_CIPHERTEXTBYTES], &e1.secret) {
Some(ee1)
} else {
None
}
} else {
None
}
});
let counter = session.outgoing_packet_counter.next();
let new_shared_secret = SymmetricKey::new(&last_offer.alice_e0_bob_static, &local_static_remote_e0, &session.static_secret, ee0, local_e1_remote_e1, current_time, counter, false);
let new_secret_key = new_shared_secret.key.clone();
let mut session_shared_secret = session.shared_secret.write();
session_shared_secret.previous = session_shared_secret.current.replace(new_shared_secret);
let _ = session_shared_secret.next.take();
drop(session_shared_secret); // release lock
let mut reply: packed::KeyAck = packed::zeroed();
reply.t = PACKET_TYPE_KEY_COUNTER_OFFER_ACK;
reply.counter = counter.to_bytes();
reply.to_session_id.copy_from_slice(&remote_session_id.to_le_bytes()[..6]);
let mut hmac = HMACSHA384::new(zt_kbkdf_hmac_sha384(&new_secret_key.as_bytes()[..48], KBKDF_KEY_USAGE_LABEL_HMAC).as_bytes());
hmac.update(&packed::as_bytes(&reply)[..COMMON_HEADER_SIZE]);
hmac.update(new_secret_key.as_bytes());
reply.hmac_shared_secret = hmac.finish();
reply.hmac_static_key = hmac_sha384(zt_kbkdf_hmac_sha384(&session.static_secret.0[..48], KBKDF_KEY_USAGE_LABEL_HMAC).as_bytes(), &packed::as_bytes(&reply)[..size_of::<packed::KeyAck>() - 48]);
buffer[..size_of::<packed::KeyAck>()].copy_from_slice(&packed::as_bytes(&reply)[..size_of::<packed::KeyAck>()]);
session.local_to_remote_obfuscator.0.encrypt_block_in_place(&mut buffer[0..16]);
return Ok(ReceiveResult::OkSendReply(&buffer[..size_of::<packed::KeyAck>()]));
} else {
return Err(Error::InvalidPacket);
}
}
}
}
return Ok(ReceiveResult::Ignored);
}
PACKET_TYPE_KEY_COUNTER_OFFER_ACK => {
todo!()
}
_ => return Err(Error::InvalidPacket),
}
}
}
fn decrypt_and_auth_key_exchange_packet<const BS: usize, const STATIC_PUBLIC_SIZE: usize>(incoming_packet: &[u8], buffer: &mut [u8; BS], incoming_obfuscator: &Obfuscator, local_static_p384_secret: &P384KeyPair) -> Result<(P384PublicKey, Secret<64>), Error> {
incoming_obfuscator.0.decrypt_block(&incoming_packet[16..32], &mut buffer[16..32]);
incoming_obfuscator.0.decrypt_block(&incoming_packet[32..48], &mut buffer[32..48]);
incoming_obfuscator.0.decrypt_block(&incoming_packet[48..64], &mut buffer[48..64]);
buffer[64..incoming_packet.len()].copy_from_slice(&incoming_packet[64..]);
// Decrypt and authenticate with agree(remote e0, local static)
let (remote_e0, local_static_remote_e0) = if let Some(incoming_e0) = P384PublicKey::from_bytes(&incoming_packet[packed::KEY_EXCHANGE_PACKET_E0_INDEX..packed::KEY_EXCHANGE_PACKET_E0_INDEX + P384_PUBLIC_KEY_SIZE]) {
if let Some(se) = local_static_p384_secret.agree(&incoming_e0) {
(incoming_e0, Secret(SHA512::hash(se.as_bytes())))
} else {
return Err(Error::InvalidPacket);
}
} else {
return Err(Error::InvalidPacket);
};
let mut c = AesGcm::new(&local_static_remote_e0.0[..32], true);
let mut nonce: [u8; 16] = 0_u128.to_ne_bytes();
for i in 0..COMMON_HEADER_SIZE {
nonce[i] = buffer[i];
}
c.init(&nonce);
c.crypt_in_place(&mut buffer[packed::KEY_EXCHANGE_PACKET_ENCRYPT_ENVELOPE_START..incoming_packet.len() - (16 + 48)]);
if !c.finish().eq(&buffer[incoming_packet.len() - (16 + 48)..incoming_packet.len() - 48]) {
return Err(Error::FailedAuthentication);
}
return Ok((remote_e0, local_static_remote_e0));
}
struct EphemeralOffer {
alice_e0_bob_static: Secret<64>,
id: [u8; 48],
alice_e0: P384KeyPair,
alice_e1: Option<pqc_kyber::Keypair>,
}
impl EphemeralOffer {
fn create<const BS: usize, const STATIC_PUBLIC_SIZE: usize>(
buffer: &mut [u8; BS],
counter: CounterValue,
local_session_id: u64,
remote_session_id: u64,
local_static_public: &[u8; STATIC_PUBLIC_SIZE],
remote_static_p384: &P384PublicKey,
static_secret: &Secret<64>,
outgoing_obfuscator: &Obfuscator,
jedi: bool,
) -> Option<(Box<EphemeralOffer>, usize)> {
debug_assert!(BS >= BUFFER_SIZE_MIN);
let alice_e0 = P384KeyPair::generate();
let alice_e0_bob_static = Secret(SHA512::hash(alice_e0.agree(remote_static_p384)?.as_bytes())); // es == agree(my ephemeral (e0), their static)
let alice_e1 = if jedi { Some(pqc_kyber::keypair(&mut random::SecureRandom::get())) } else { None };
let mut p: packed::KeyOffer<STATIC_PUBLIC_SIZE> = packed::zeroed();
p.t = PACKET_TYPE_KEY_OFFER;
p.counter = counter.to_bytes();
p.to_session_id.copy_from_slice(&remote_session_id.to_le_bytes()[..6]);
p.e0 = alice_e0.public_key_bytes().clone();
p.from_session_id.copy_from_slice(&local_session_id.to_le_bytes()[..6]);
if let Some(e1) = alice_e1.as_ref() {
p.e1 = e1.public;
}
p.static_public = local_static_public.clone();
let mut offer_fingerprint_hash = SHA384::new();
offer_fingerprint_hash.update(&p.e0);
offer_fingerprint_hash.update(&p.e1);
let offer_fingerprint_hash = offer_fingerprint_hash.finish()[..16].try_into().unwrap();
let mut c = AesGcm::new(&alice_e0_bob_static.0[..32], true);
c.init(&packed::get_aes_gcm_nonce(&p));
buffer[..packed::KEY_EXCHANGE_PACKET_ENCRYPT_ENVELOPE_START].copy_from_slice(&packed::as_bytes(&p)[..packed::KEY_EXCHANGE_PACKET_ENCRYPT_ENVELOPE_START]);
c.crypt(&packed::as_bytes(&p)[packed::KEY_EXCHANGE_PACKET_ENCRYPT_ENVELOPE_START..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - (16 + 48)], &mut buffer[packed::KEY_EXCHANGE_PACKET_ENCRYPT_ENVELOPE_START..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - (16 + 48)]);
buffer[size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - (16 + 48)..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - 48].copy_from_slice(&c.finish());
let hmac = hmac_sha384(zt_kbkdf_hmac_sha384(&static_secret.0[..48], KBKDF_KEY_USAGE_LABEL_HMAC).as_bytes(), &buffer[..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - 48]);
buffer[size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>() - 48..size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>()].copy_from_slice(&hmac);
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[0..16]);
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[16..32]);
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[32..48]);
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[48..64]);
Some((
Box::new(EphemeralOffer {
alice_e0_bob_static,
id: offer_fingerprint_hash,
alice_e0,
alice_e1,
}),
size_of::<packed::KeyOffer<STATIC_PUBLIC_SIZE>>(),
))
}
}
#[derive(Copy, Clone)]
struct CounterValue(u64);
impl CounterValue {
#[inline(always)]
pub fn to_bytes(&self) -> [u8; 4] {
(self.0 as u32).to_le_bytes()
}
}
struct KeyLifetime {
rekey_at_or_after: u64,
hard_expire_at: u64,
}
impl KeyLifetime {
fn new(current_counter: CounterValue) -> Self {
Self {
rekey_at_or_after: current_counter.0 + REKEY_AFTER_USES,
hard_expire_at: current_counter.0 + 0xfffffffe,
}
}
fn expired(&self, current_counter: CounterValue) -> bool {
current_counter.0 >= self.hard_expire_at
}
fn needs_rekey(&self, current_counter: CounterValue) -> bool {
current_counter.0 >= self.rekey_at_or_after
}
}
struct Counter(AtomicU64);
impl Counter {
fn new() -> Self {
// Counter starts at 1 because it's always created after an initial packet is sent.
Self(AtomicU64::new(0))
}
fn next(&self) -> CounterValue {
CounterValue(self.0.fetch_add(1, Ordering::SeqCst))
}
}
struct SymmetricKeyQueue {
current: Option<SymmetricKey>,
previous: Option<SymmetricKey>,
next: Option<SymmetricKey>,
}
struct SymmetricKeyCipherInstance {
pool: Vec<Box<AesGcm>>,
lifetime: KeyLifetime,
}
impl SymmetricKeyCipherInstance {
fn new(counter_at_creation: CounterValue) -> Self {
Self {
pool: Vec::with_capacity(2),
lifetime: KeyLifetime::new(counter_at_creation),
}
}
fn get(&mut self, key: &[u8], encrypt: bool, current_counter: CounterValue) -> Option<Box<AesGcm>> {
if !self.lifetime.expired(current_counter) {
self.pool.pop().or_else(|| Some(Box::new(AesGcm::new(key, encrypt))))
} else {
unlikely_branch();
None
}
}
}
struct SymmetricKey {
creation_time: i64,
key: Secret<64>,
receive_key_index: usize,
send_key_index: usize,
receive_cipher: Mutex<SymmetricKeyCipherInstance>,
send_cipher: Mutex<SymmetricKeyCipherInstance>,
jedi: bool, // true if kyber was used
}
impl SymmetricKey {
fn new(alice_e0_bob_static: &Secret<64>, se: &Secret<64>, alice_static_bob_static: &Secret<64>, alice_e0_bob_e0: Secret<48>, alice_e1_bob_e1: Option<[u8; pqc_kyber::KYBER_SSBYTES]>, current_time: i64, current_counter: CounterValue, this_side_is_alice: bool) -> Self {
// The 'alice' flag is true on the side that sends the counter offer, false on the side that receives it.
// This determines which half of the 512-bit secret is used to key AES for sending or receiving.
let (receive_key_index, send_key_index) = if this_side_is_alice { (0, 32) } else { (32, 0) };
let jedi = alice_e1_bob_e1.is_some();
Self {
creation_time: current_time,
// It would be fine to just hash SHA384(es | se | ee0 | ee1 | ss) but NIST/FIPS specifies that the preferred
// KDF method is HMAC(salt, key). The key must be from a FIPS-compliant exchange but the salt may be anything.
// So we hash all but ee0 (NIST P-384 secret) into a salt and then HMAC to get the final key.
key: Secret(hmac_sha512(
&{
let mut sha = SHA512::new();
sha.update(alice_e0_bob_static.as_bytes());
sha.update(se.as_bytes());
if let Some(ee1) = alice_e1_bob_e1 {
sha.update(&ee1);
} else {
sha.update(&[0_u8; pqc_kyber::KYBER_SSBYTES]); // input is always the same length
}
sha.update(alice_static_bob_static.as_bytes());
sha.finish()
},
alice_e0_bob_e0.as_bytes(),
)),
receive_key_index,
send_key_index,
receive_cipher: Mutex::new(SymmetricKeyCipherInstance::new(current_counter)),
send_cipher: Mutex::new(SymmetricKeyCipherInstance::new(current_counter)),
jedi,
}
}
fn get_receive_cipher(&self, current_counter: CounterValue) -> Option<Box<AesGcm>> {
self.receive_cipher.lock().get(&self.key.0[self.receive_key_index..self.receive_key_index + 32], false, current_counter)
}
fn return_receive_cipher(&self, gcm: Box<AesGcm>) {
self.receive_cipher.lock().pool.push(gcm);
}
fn get_send_cipher(&self, current_counter: CounterValue) -> Option<Box<AesGcm>> {
self.send_cipher.lock().get(&self.key.0[self.send_key_index..self.send_key_index + 32], true, current_counter)
}
fn return_send_cipher(&self, gcm: Box<AesGcm>) {
self.send_cipher.lock().pool.push(gcm);
}
}
const PACKET_TYPE_DATA: u8 = 0;
const PACKET_TYPE_KEY_OFFER: u8 = 1;
const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 2;
const PACKET_TYPE_KEY_COUNTER_OFFER_ACK: u8 = 3;
const COMMON_HEADER_SIZE: usize = 11;
const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'h';
// Code for dealing with flat packed structures. A little "unsafe" exists here but nowhere else.
mod packed {
use super::*;
pub(super) trait PackedStruct {}
pub const KEY_EXCHANGE_PACKET_E0_INDEX: usize = COMMON_HEADER_SIZE;
pub const KEY_EXCHANGE_PACKET_ENCRYPT_ENVELOPE_START: usize = KEY_EXCHANGE_PACKET_E0_INDEX + P384_PUBLIC_KEY_SIZE;
#[repr(C, packed)]
pub(super) struct KeyOffer<const STATIC_PUBLIC_SIZE: usize> {
pub t: u8,
pub counter: [u8; 4],
pub to_session_id: [u8; 6], // zero if unknown
pub e0: [u8; P384_PUBLIC_KEY_SIZE],
// -- AES-GCM(ECDH(sender_e0, recipient_static)) --
pub from_session_id: [u8; 6],
pub e1: [u8; pqc_kyber::KYBER_PUBLICKEYBYTES],
pub static_public: [u8; STATIC_PUBLIC_SIZE],
// --
pub gcm_tag: [u8; 16],
pub hmac_static_key: [u8; 48], // HMAC-SHA384(static secret (derived HMAC key), packet)
}
impl<const STATIC_PUBLIC_SIZE: usize> PackedStruct for KeyOffer<STATIC_PUBLIC_SIZE> {}
#[repr(C, packed)]
pub(super) struct KeyCounterOffer<const STATIC_PUBLIC_SIZE: usize> {
pub t: u8,
pub counter: [u8; 4],
pub to_session_id: [u8; 6], // zero if unknown
pub e0: [u8; P384_PUBLIC_KEY_SIZE],
// -- AES-GCM(ECDH(sender_e0, recipient_static)) --
pub from_session_id: [u8; 6],
pub e1: [u8; pqc_kyber::KYBER_CIPHERTEXTBYTES],
pub in_re_offer_id: [u8; 48],
// --
pub gcm_tag: [u8; 16],
pub hmac_static_key: [u8; 48], // HMAC-SHA384(static secret (derived HMAC key), packet)
}
impl<const STATIC_PUBLIC_SIZE: usize> PackedStruct for KeyCounterOffer<STATIC_PUBLIC_SIZE> {}
#[repr(C, packed)]
pub(super) struct KeyAck {
pub t: u8,
pub counter: [u8; 4],
pub to_session_id: [u8; 6],
pub hmac_shared_secret: [u8; 48], // HMAC-SHA384(shared secret (derived HMAC key), packet header | shared secret)
pub hmac_static_key: [u8; 48], // HMAC-SHA384(static secret (derived HMAC key), packet)
}
impl PackedStruct for KeyAck {}
#[inline(always)]
pub(super) fn as_bytes<T: PackedStruct>(t: &T) -> &[u8] {
unsafe { &*std::ptr::slice_from_raw_parts((t as *const T).cast::<u8>(), size_of::<T>()) }
}
#[inline(always)]
pub(super) fn as_packed_struct<T: PackedStruct>(t: &[u8]) -> Option<&T> {
if t.len() >= size_of::<T>() {
Some(unsafe { &*t.as_ptr().cast::<T>() })
} else {
None
}
}
#[inline(always)]
pub(super) fn get_aes_gcm_nonce<T: PackedStruct>(t: &T) -> [u8; 16] {
debug_assert!(size_of::<T>() >= 16);
unsafe {
let mut tmp: [u8; 16] = std::mem::MaybeUninit::uninit().assume_init();
std::ptr::copy_nonoverlapping((t as *const T).cast(), tmp.as_mut_ptr(), COMMON_HEADER_SIZE);
std::ptr::write_bytes(tmp.as_mut_ptr().add(COMMON_HEADER_SIZE), 0, 16 - COMMON_HEADER_SIZE);
tmp
}
}
#[inline(always)]
pub(super) fn zeroed<T: PackedStruct>() -> T {
unsafe { std::mem::zeroed() }
}
}
#[inline(always)]
fn u48_from_le_bytes<const S: usize>(b: &[u8; S]) -> u64 {
(b[0] as u64) | (b[1] as u64).wrapping_shl(8) | (b[2] as u64).wrapping_shl(16) | (b[3] as u64).wrapping_shl(24) | (b[4] as u64).wrapping_shl(32) | (b[5] as u64).wrapping_shl(40)
}
#[cold]
#[inline(never)]
extern "C" fn unlikely_branch() {}