mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-07-24 03:02:50 +02:00
1692 lines
83 KiB
Rust
1692 lines
83 KiB
Rust
// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md.
|
|
|
|
// ZSSP: ZeroTier Secure Session Protocol
|
|
// FIPS compliant Noise_IK with Jedi powers and built-in attack-resistant large payload (fragmentation) support.
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Mutex, RwLock};
|
|
|
|
use zerotier_crypto::aes::{Aes, AesGcm};
|
|
use zerotier_crypto::hash::{hmac_sha512, HMACSHA384, SHA384};
|
|
use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE};
|
|
use zerotier_crypto::random;
|
|
use zerotier_crypto::secret::Secret;
|
|
use zerotier_crypto::secure_eq;
|
|
|
|
use zerotier_utils::gatherarray::GatherArray;
|
|
use zerotier_utils::memory;
|
|
use zerotier_utils::ringbuffermap::RingBufferMap;
|
|
use zerotier_utils::unlikely_branch;
|
|
use zerotier_utils::varint;
|
|
|
|
use crate::applicationlayer::ApplicationLayer;
|
|
use crate::constants::*;
|
|
use crate::counter::{Counter, CounterValue, CounterWindow};
|
|
use crate::sessionid::SessionId;
|
|
|
|
pub enum Error {
|
|
/// The packet was addressed to an unrecognized local session (should usually be ignored).
|
|
UnknownLocalSessionId(SessionId),
|
|
|
|
/// Packet was not well formed.
|
|
InvalidPacket,
|
|
|
|
/// An invalid parameter was supplied to the function.
|
|
InvalidParameter,
|
|
|
|
/// Packet failed one or more authentication (MAC) checks.
|
|
///
|
|
/// **IMPORTANT**: Do not reply to a peer who has sent a packet that has failed authentication. Any response at all will leak to an attacker what authentication step their packet failed at (timing attack), which lowers the total authentication entropy they have to brute force.
|
|
/// There is a safe way to reply if absolutely necessary, by sending the reply back after a constant amount of time, but this is very difficult to get correct.
|
|
FailedAuthentication,
|
|
|
|
/// New session was rejected by the application layer.
|
|
NewSessionRejected,
|
|
|
|
/// Rekeying failed and session secret has reached its hard usage count limit.
|
|
MaxKeyLifetimeExceeded,
|
|
|
|
/// Attempt to send using session without established key.
|
|
SessionNotEstablished,
|
|
|
|
/// Packet ignored by rate limiter.
|
|
RateLimited,
|
|
|
|
/// The other peer specified an unrecognized protocol version.
|
|
UnknownProtocolVersion,
|
|
|
|
/// Caller supplied data buffer is too small to receive data.
|
|
DataBufferTooSmall,
|
|
|
|
/// Data object is too large to send, even with fragmentation.
|
|
DataTooLarge,
|
|
|
|
/// An unexpected buffer overrun occured while attempting to encode or decode a packet.
|
|
///
|
|
/// This can only ever happen if exceptionally large key blobs or metadata are being used,
|
|
/// or as the result of an internal encoding bug.
|
|
UnexpectedBufferOverrun,
|
|
}
|
|
|
|
/// Result generated by the packet receive function, with possible payloads.
|
|
pub enum ReceiveResult<'a, H: ApplicationLayer> {
|
|
/// Packet is valid, no action needs to be taken.
|
|
Ok,
|
|
|
|
/// Packet is valid and a data payload was decoded and authenticated.
|
|
///
|
|
/// The returned reference is to the filled parts of the data buffer supplied to receive.
|
|
OkData(&'a mut [u8]),
|
|
|
|
/// Packet is valid and a new session was created.
|
|
///
|
|
/// The session will have already been gated by the accept_new_session() method in the Host trait.
|
|
OkNewSession(Session<H>),
|
|
|
|
/// Packet superficially appears valid but was ignored e.g. as a duplicate.
|
|
///
|
|
/// **IMPORTANT**: This packet was not authenticated, so for the most part treat this the same as an Error::FailedAuthentication.
|
|
Ignored,
|
|
}
|
|
|
|
/// Was this side the one who sent the first offer (Alice) or countered (Bob).
|
|
///
|
|
/// Note that the role can switch through the course of a session. It's the side that most recently
|
|
/// initiated a session or a rekey event. Initiator is Alice, responder is Bob.
|
|
#[derive(PartialEq)]
|
|
pub enum Role {
|
|
Alice,
|
|
Bob,
|
|
}
|
|
|
|
/// State information to associate with receiving contexts such as sockets or remote paths/endpoints.
|
|
///
|
|
/// This holds the data structures used to defragment incoming packets that are not associated with an
|
|
/// existing session, which would be new attempts to create sessions. Typically one of these is associated
|
|
/// with a single listen socket, local bound port, or other inbound endpoint.
|
|
pub struct ReceiveContext<H: ApplicationLayer> {
|
|
initial_offer_defrag: Mutex<RingBufferMap<u32, GatherArray<H::IncomingPacketBuffer, KEY_EXCHANGE_MAX_FRAGMENTS>, 1024, 128>>,
|
|
incoming_init_header_check_cipher: Aes,
|
|
}
|
|
|
|
/// A FIPS compliant variant of Noise_IK with hybrid Kyber1024 PQ data forward secrecy.
|
|
pub struct Session<Application: ApplicationLayer> {
|
|
/// This side's locally unique session ID
|
|
pub id: SessionId,
|
|
|
|
/// An arbitrary application defined object associated with each session
|
|
pub application_data: Application::Data,
|
|
|
|
ratchet_counts: [AtomicU64; 2], // Number of session keys in ratchet, starts at 1
|
|
header_check_cipher: Aes, // Cipher used for header check codes (not Noise related)
|
|
receive_windows: [CounterWindow; 2], // Receive window for anti-replay and deduplication
|
|
state: RwLock<SessionMutableState>, // Mutable parts of state (other than defrag buffers)
|
|
psk: Secret<64>, // Arbitrary PSK provided by external code
|
|
noise_ss: Secret<48>, // Static raw shared ECDH NIST P-384 key
|
|
remote_s_public_blob_hash: [u8; 48], // SHA384(remote static public key blob)
|
|
remote_s_public_p384_bytes: [u8; P384_PUBLIC_KEY_SIZE], // Remote NIST P-384 static public key
|
|
|
|
defrag: Mutex<RingBufferMap<u32, GatherArray<Application::IncomingPacketBuffer, MAX_FRAGMENTS>, 8, 8>>,
|
|
}
|
|
|
|
struct SessionMutableState {
|
|
remote_session_id: Option<SessionId>, // The other side's 48-bit session ID
|
|
send_counters: [Counter; 2], // Outgoing packet counter and nonce state, starts at 1
|
|
session_keys: [Option<SessionKey>; 2], // Buffers to store current, next, and last active key
|
|
cur_session_key_id: bool, // Pointer used for keys[] circular buffer
|
|
offer: Option<EphemeralOffer>, // Most recent ephemeral offer sent to remote
|
|
last_remote_offer: i64, // Time of most recent ephemeral offer (ms)
|
|
}
|
|
|
|
/// A shared symmetric session key.
|
|
/// sessions always start at counter 1u32
|
|
struct SessionKey {
|
|
secret_fingerprint: [u8; 16], // First 128 bits of a SHA384 computed from the secret
|
|
creation_time: i64, // Time session key was established
|
|
lifetime: KeyLifetime, // Key expiration time and counter
|
|
ratchet_key: Secret<64>, // Ratchet key for deriving the next session key
|
|
receive_key: Secret<AES_KEY_SIZE>, // Receive side AES-GCM key
|
|
send_key: Secret<AES_KEY_SIZE>, // Send side AES-GCM key
|
|
receive_cipher_pool: Mutex<Vec<Box<AesGcm>>>, // Pool of reusable sending ciphers
|
|
send_cipher_pool: Mutex<Vec<Box<AesGcm>>>, // Pool of reusable receiving ciphers
|
|
jedi: bool, // True if Kyber1024 was used (both sides enabled)
|
|
role: Role, // The role of the local party that created this key
|
|
}
|
|
|
|
/// Key lifetime state
|
|
struct KeyLifetime {
|
|
rekey_at_or_after_counter: u32,
|
|
rekey_at_or_after_timestamp: i64,
|
|
}
|
|
|
|
/// Alice's KEY_OFFER, remembered so Noise agreement process can resume on KEY_COUNTER_OFFER.
|
|
struct EphemeralOffer {
|
|
id: [u8; 16], // Arbitrary random offer ID
|
|
key_id: bool, // The key_id bound to this offer, for handling OOO rekeying
|
|
creation_time: i64, // Local time when offer was created
|
|
ss_key: Secret<64>, // Noise session key "under construction" at state after offer sent
|
|
alice_e_keypair: P384KeyPair, // NIST P-384 key pair (Noise ephemeral key for Alice)
|
|
alice_hk_keypair: Option<pqc_kyber::Keypair>, // Kyber1024 key pair (PQ hybrid ephemeral key for Alice)
|
|
}
|
|
|
|
/// "Canonical header" for generating 96-bit AES-GCM nonce and for inclusion in key exchange HMACs.
|
|
///
|
|
/// This is basically the actual header but with fragment count and fragment total set to zero.
|
|
/// Fragmentation is not considered when authenticating the entire packet. A separate header
|
|
/// check code is used to make fragmentation itself more robust, but that's outside the scope
|
|
/// of AEAD authentication.
|
|
#[derive(Clone, Copy)]
|
|
#[repr(C, packed)]
|
|
struct CanonicalHeader(pub u64, pub u32);
|
|
|
|
impl std::fmt::Display for Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::UnknownLocalSessionId(id) => f.write_str(format!("UnknownLocalSessionId({})", id).as_str()),
|
|
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"),
|
|
Self::SessionNotEstablished => f.write_str("SessionNotEstablished"),
|
|
Self::RateLimited => f.write_str("RateLimited"),
|
|
Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"),
|
|
Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"),
|
|
Self::DataTooLarge => f.write_str("DataTooLarge"),
|
|
Self::UnexpectedBufferOverrun => f.write_str("UnexpectedBufferOverrun"),
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
impl<Application: ApplicationLayer> Session<Application> {
|
|
/// Create a new session and send an initial key offer message to the other end.
|
|
///
|
|
/// * `app` - Interface to application using ZSSP
|
|
/// * `local_session_id` - ID for this side (Alice) of the session, must be locally unique
|
|
/// * `remote_s_public_blob` - Remote side's (Bob's) public key/identity
|
|
/// * `offer_metadata` - Arbitrary meta-data to send with key offer (empty if none)
|
|
/// * `psk` - Arbitrary pre-shared key to include as initial key material (use all zeroes if none)
|
|
/// * `application_data` - Arbitrary object to put into session
|
|
/// * `mtu` - Physical wire maximum transmission unit (current value, can change through the course of a session)
|
|
/// * `current_time` - Current monotonic time in milliseconds since an arbitrary time in the past
|
|
pub fn start_new<SendFunction: FnMut(&mut [u8])>(
|
|
app: &Application,
|
|
mut send: SendFunction,
|
|
local_session_id: SessionId,
|
|
remote_s_public_blob: &[u8],
|
|
offer_metadata: &[u8],
|
|
psk: &Secret<64>,
|
|
application_data: Application::Data,
|
|
mtu: usize,
|
|
current_time: i64,
|
|
) -> Result<Self, Error> {
|
|
let bob_s_public_blob = remote_s_public_blob;
|
|
if let Some(bob_s_public) = Application::extract_s_public_from_raw(bob_s_public_blob) {
|
|
if let Some(noise_ss) = app.get_local_s_keypair().agree(&bob_s_public) {
|
|
let send_counter = Counter::new();
|
|
let bob_s_public_blob_hash = SHA384::hash(bob_s_public_blob);
|
|
let header_check_cipher =
|
|
Aes::new(kbkdf512(noise_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::<HEADER_CHECK_AES_KEY_SIZE>());
|
|
let mut offer = None;
|
|
if send_ephemeral_offer(
|
|
&mut send,
|
|
send_counter.next(),
|
|
false,
|
|
false,
|
|
local_session_id,
|
|
None,
|
|
app.get_local_s_public_blob(),
|
|
offer_metadata,
|
|
&bob_s_public,
|
|
&bob_s_public_blob_hash,
|
|
&noise_ss,
|
|
None,
|
|
1,
|
|
None,
|
|
mtu,
|
|
current_time,
|
|
&mut offer,
|
|
)
|
|
.is_ok()
|
|
{
|
|
return Ok(Self {
|
|
id: local_session_id,
|
|
application_data,
|
|
ratchet_counts: [AtomicU64::new(1), AtomicU64::new(0)],
|
|
header_check_cipher,
|
|
receive_windows: [CounterWindow::new(), CounterWindow::new_invalid()],
|
|
state: RwLock::new(SessionMutableState {
|
|
send_counters: [send_counter, Counter::new()],
|
|
remote_session_id: None,
|
|
session_keys: [None, None],
|
|
cur_session_key_id: false,
|
|
offer,
|
|
last_remote_offer: i64::MIN,
|
|
}),
|
|
psk: psk.clone(),
|
|
noise_ss,
|
|
remote_s_public_blob_hash: bob_s_public_blob_hash,
|
|
remote_s_public_p384_bytes: bob_s_public.as_bytes().clone(),
|
|
defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
|
|
/// Send data over the session.
|
|
///
|
|
/// * `send` - Function to call to send physical packet(s)
|
|
/// * `mtu_sized_buffer` - A writable work buffer whose size also specifies the physical MTU
|
|
/// * `data` - Data to send
|
|
#[inline]
|
|
pub fn send<SendFunction: FnMut(&mut [u8])>(
|
|
&self,
|
|
mut send: SendFunction,
|
|
mtu_sized_buffer: &mut [u8],
|
|
mut data: &[u8],
|
|
) -> Result<(), Error> {
|
|
debug_assert!(mtu_sized_buffer.len() >= MIN_TRANSPORT_MTU);
|
|
let state = self.state.read().unwrap();
|
|
if let Some(remote_session_id) = state.remote_session_id {
|
|
let key_id = state.cur_session_key_id;
|
|
if let Some(session_key) = state.session_keys[key_id as usize].as_ref() {
|
|
// Total size of the armored packet we are going to send (may end up being fragmented)
|
|
let packet_len = data.len() + HEADER_SIZE + AES_GCM_TAG_SIZE;
|
|
//key ratchet count to be used for salting
|
|
let ratchet_count = self.ratchet_counts[key_id as usize].load(Ordering::Relaxed);
|
|
// This outgoing packet's nonce counter value.
|
|
let counter = state.send_counters[key_id as usize].next();
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
// packet encoding for post-noise transport
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
// Create initial header for first fragment of packet and place in first HEADER_SIZE bytes of buffer.
|
|
create_packet_header(
|
|
mtu_sized_buffer,
|
|
packet_len,
|
|
mtu_sized_buffer.len(),
|
|
PACKET_TYPE_DATA,
|
|
remote_session_id.into(),
|
|
counter,
|
|
key_id
|
|
)?;
|
|
|
|
// Get an initialized AES-GCM cipher and re-initialize with a 96-bit IV built from remote session ID,
|
|
// packet type, and counter.
|
|
let mut c = session_key.get_send_cipher(counter)?;
|
|
c.reset_init_gcm(CanonicalHeader::make(remote_session_id, PACKET_TYPE_DATA, counter.to_u32()).as_bytes());
|
|
|
|
// Send first N-1 fragments of N total fragments.
|
|
let last_fragment_size;
|
|
if packet_len > mtu_sized_buffer.len() {
|
|
let mut header: [u8; 16] = mtu_sized_buffer[..HEADER_SIZE].try_into().unwrap();
|
|
let fragment_data_mtu = mtu_sized_buffer.len() - HEADER_SIZE;
|
|
let last_fragment_data_mtu = mtu_sized_buffer.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE);
|
|
loop {
|
|
let fragment_data_size = fragment_data_mtu.min(data.len());
|
|
let fragment_size = fragment_data_size + HEADER_SIZE;
|
|
c.crypt(&data[..fragment_data_size], &mut mtu_sized_buffer[HEADER_SIZE..fragment_size]);
|
|
data = &data[fragment_data_size..];
|
|
set_header_check_code(mtu_sized_buffer, ratchet_count, &self.header_check_cipher);
|
|
send(&mut mtu_sized_buffer[..fragment_size]);
|
|
|
|
debug_assert!(header[15].wrapping_shr(2) < 63);
|
|
header[15] += 0x04; // increment fragment number
|
|
mtu_sized_buffer[..HEADER_SIZE].copy_from_slice(&header);
|
|
|
|
if data.len() <= last_fragment_data_mtu {
|
|
break;
|
|
}
|
|
}
|
|
last_fragment_size = data.len() + HEADER_SIZE + AES_GCM_TAG_SIZE;
|
|
} else {
|
|
last_fragment_size = packet_len;
|
|
}
|
|
|
|
// Send final fragment (or only fragment if no fragmentation was needed)
|
|
let payload_end = data.len() + HEADER_SIZE;
|
|
c.crypt(data, &mut mtu_sized_buffer[HEADER_SIZE..payload_end]);
|
|
let gcm_tag = c.finish_encrypt();
|
|
mtu_sized_buffer[payload_end..last_fragment_size].copy_from_slice(&gcm_tag);
|
|
set_header_check_code(mtu_sized_buffer, ratchet_count, &self.header_check_cipher);
|
|
send(&mut mtu_sized_buffer[..last_fragment_size]);
|
|
|
|
// Check reusable AES-GCM instance back into pool.
|
|
session_key.return_send_cipher(c);
|
|
|
|
return Ok(());
|
|
} else {
|
|
unlikely_branch();
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
}
|
|
return Err(Error::SessionNotEstablished);
|
|
}
|
|
|
|
/// Check whether this session is established.
|
|
pub fn established(&self) -> bool {
|
|
let state = self.state.read().unwrap();
|
|
state.remote_session_id.is_some() && state.session_keys[state.cur_session_key_id as usize].is_some()
|
|
}
|
|
|
|
/// Get information about this session's security state.
|
|
///
|
|
/// This returns a tuple of: the key fingerprint, the time it was established, the length of its ratchet chain,
|
|
/// and whether Kyber1024 was used. None is returned if the session isn't established.
|
|
pub fn status(&self) -> Option<([u8; 16], i64, u64, bool)> {
|
|
let state = self.state.read().unwrap();
|
|
let key_id = state.cur_session_key_id;
|
|
if let Some(key) = state.session_keys[key_id as usize].as_ref() {
|
|
Some((key.secret_fingerprint, key.creation_time, self.ratchet_counts[key_id as usize].load(Ordering::Relaxed), key.jedi))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// This function needs to be called on each session at least every SERVICE_INTERVAL milliseconds.
|
|
///
|
|
/// * `app` - Interface to application using ZSSP
|
|
/// * `send` - Function to call to send physical packet(s)
|
|
/// * `offer_metadata' - Any meta-data to include with initial key offers sent.
|
|
/// * `mtu` - Current physical transport MTU
|
|
/// * `current_time` - Current monotonic time in milliseconds
|
|
/// * `assume_key_is_too_old` - Re-key the session now if the protocol allows it (subject to rate limits and whether it is the local party's turn to rekey)
|
|
pub fn service<SendFunction: FnMut(&mut [u8])>(
|
|
&self,
|
|
app: &Application,
|
|
mut send: SendFunction,
|
|
offer_metadata: &[u8],
|
|
mtu: usize,
|
|
current_time: i64,
|
|
assume_key_is_too_old: bool,
|
|
) {
|
|
let state = self.state.read().unwrap();
|
|
let current_key_id = state.cur_session_key_id;
|
|
if (state.session_keys[state.cur_session_key_id as usize]
|
|
.as_ref()
|
|
.map_or(true, |key| key.role == Role::Bob && (assume_key_is_too_old || key.lifetime.should_rekey(state.send_counters[current_key_id as usize].current(), current_time))))
|
|
&& state
|
|
.offer
|
|
.as_ref()
|
|
.map_or(true, |o| (current_time - o.creation_time) > Application::REKEY_RATE_LIMIT_MS)
|
|
{
|
|
if let Some(remote_s_public) = P384PublicKey::from_bytes(&self.remote_s_public_p384_bytes) {
|
|
//this routine handles sending a rekeying packet, resending lost rekeying packets, and resending lost initial offer packets
|
|
//the protocol has been designed such that initial rekeying packets are identical to resent rekeying packets, except for the counter, so we can reuse the same code for doing both
|
|
let has_existing_session = state.remote_session_id.is_some();
|
|
//mark the previous key as no longer being supported because it is about to be overwritten
|
|
//it should not be possible for a session to accidentally invalidate the key currently in use solely because of the read lock
|
|
self.receive_windows[(!current_key_id) as usize].invalidate();
|
|
let mut offer = None;
|
|
//the session will keep sending ephemeral offers until rekeying is successful
|
|
if send_ephemeral_offer(
|
|
&mut send,
|
|
state.send_counters[current_key_id as usize].next(),
|
|
current_key_id,
|
|
has_existing_session && !current_key_id,
|
|
self.id,
|
|
state.remote_session_id,
|
|
app.get_local_s_public_blob(),
|
|
offer_metadata,
|
|
&remote_s_public,
|
|
&self.remote_s_public_blob_hash,
|
|
&self.noise_ss,
|
|
state.session_keys[current_key_id as usize].as_ref(),
|
|
self.ratchet_counts[current_key_id as usize].load(Ordering::Relaxed),
|
|
if has_existing_session { Some(&self.header_check_cipher) } else { None },
|
|
mtu,
|
|
current_time,
|
|
&mut offer,
|
|
)
|
|
.is_ok()
|
|
{
|
|
drop(state);
|
|
self.state.write().unwrap().offer = offer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Application: ApplicationLayer> ReceiveContext<Application> {
|
|
pub fn new(app: &Application) -> Self {
|
|
Self {
|
|
initial_offer_defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())),
|
|
incoming_init_header_check_cipher: Aes::new(
|
|
kbkdf512(app.get_local_s_public_blob_hash(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::<HEADER_CHECK_AES_KEY_SIZE>(),
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Receive, authenticate, decrypt, and process a physical wire packet.
|
|
///
|
|
/// * `app` - Interface to application using ZSSP
|
|
/// * `remote_address` - Remote physical address of source endpoint
|
|
/// * `data_buf` - Buffer to receive decrypted and authenticated object data (an error is returned if too small)
|
|
/// * `incoming_packet_buf` - Buffer containing incoming wire packet (receive() takes ownership)
|
|
/// * `mtu` - Physical wire MTU for sending packets
|
|
/// * `current_time` - Current monotonic time in milliseconds
|
|
#[inline]
|
|
pub fn receive<'a, SendFunction: FnMut(&mut [u8])>(
|
|
&self,
|
|
app: &Application,
|
|
remote_address: &Application::RemoteAddress,
|
|
mut send: SendFunction,
|
|
data_buf: &'a mut [u8],
|
|
incoming_packet_buf: Application::IncomingPacketBuffer,
|
|
mtu: usize,
|
|
current_time: i64,
|
|
) -> Result<ReceiveResult<'a, Application>, Error> {
|
|
let incoming_packet = incoming_packet_buf.as_ref();
|
|
if incoming_packet.len() < MIN_PACKET_SIZE {
|
|
unlikely_branch();
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
|
|
let raw_counter = u32::from_le(memory::load_raw(&incoming_packet[4..8]));
|
|
let key_id = (raw_counter & 1) > 0;
|
|
let counter = raw_counter.wrapping_shr(1);
|
|
let packet_type_fragment_info = u16::from_le(memory::load_raw(&incoming_packet[14..16]));
|
|
let packet_type = (packet_type_fragment_info & 0x0f) as u8;
|
|
let fragment_count = ((packet_type_fragment_info.wrapping_shr(4) + 1) as u8) & 63;
|
|
let fragment_no = packet_type_fragment_info.wrapping_shr(10) as u8; // & 63 not needed
|
|
|
|
if let Some(local_session_id) = SessionId::new_from_u64(u64::from_le(memory::load_raw(&incoming_packet[8..16])) & 0xffffffffffffu64)
|
|
{
|
|
if let Some(session) = app.lookup_session(local_session_id) {
|
|
//this is the only time ratchet_counts is ever accessed outside of a lock
|
|
//as such this read can be wrong, but that is incredibly unlikely since we are tracking the last two ratchet counts, and if it's wrong it just means we drop a packet that would have been dropped anyways for being too old or too new
|
|
let ratchet_count = session.ratchet_counts[key_id as usize].load(Ordering::SeqCst);
|
|
if verify_header_check_code(incoming_packet, ratchet_count, &session.header_check_cipher) {
|
|
if session.receive_windows[key_id as usize].message_received(counter) {
|
|
let canonical_header = CanonicalHeader::make(local_session_id, packet_type, counter);
|
|
if fragment_count > 1 {
|
|
if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count {
|
|
let mut defrag = session.defrag.lock().unwrap();
|
|
// by using the counter + the key_id as the key we can prevent packet collisions, this only works if defrag hashes
|
|
let fragment_gather_array = defrag.get_or_create_mut(&raw_counter, || GatherArray::new(fragment_count));
|
|
if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) {
|
|
drop(defrag); // release lock
|
|
return self.receive_complete(
|
|
app,
|
|
remote_address,
|
|
&mut send,
|
|
data_buf,
|
|
counter,
|
|
key_id,
|
|
canonical_header.as_bytes(),
|
|
assembled_packet.as_ref(),
|
|
packet_type,
|
|
Some(session),
|
|
mtu,
|
|
current_time,
|
|
);
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
} else {
|
|
return self.receive_complete(
|
|
app,
|
|
remote_address,
|
|
&mut send,
|
|
data_buf,
|
|
counter,
|
|
key_id,
|
|
canonical_header.as_bytes(),
|
|
&[incoming_packet_buf],
|
|
packet_type,
|
|
Some(session),
|
|
mtu,
|
|
current_time,
|
|
);
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
return Ok(ReceiveResult::Ignored);
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::UnknownLocalSessionId(local_session_id));
|
|
}
|
|
} else {
|
|
unlikely_branch(); // we want data receive to be the priority branch, this is only occasionally used
|
|
//salt with a known value so new sessions can be established
|
|
//NOTE: this check is trivial to bypass by just replaying recorded packets
|
|
//this check isn't security critical so that is fine
|
|
if verify_header_check_code(incoming_packet, 1u64, &self.incoming_init_header_check_cipher) {
|
|
let canonical_header = CanonicalHeader::make(SessionId::NIL, packet_type, counter);
|
|
if fragment_count > 1 {
|
|
let mut defrag = self.initial_offer_defrag.lock().unwrap();
|
|
let fragment_gather_array = defrag.get_or_create_mut(&counter, || GatherArray::new(fragment_count));
|
|
if let Some(assembled_packet) = fragment_gather_array.add(fragment_no, incoming_packet_buf) {
|
|
drop(defrag); // release lock
|
|
return self.receive_complete(
|
|
app,
|
|
remote_address,
|
|
&mut send,
|
|
data_buf,
|
|
counter,
|
|
key_id,
|
|
canonical_header.as_bytes(),
|
|
assembled_packet.as_ref(),
|
|
packet_type,
|
|
None,
|
|
mtu,
|
|
current_time,
|
|
);
|
|
}
|
|
} else {
|
|
return self.receive_complete(
|
|
app,
|
|
remote_address,
|
|
&mut send,
|
|
data_buf,
|
|
counter,
|
|
key_id,
|
|
canonical_header.as_bytes(),
|
|
&[incoming_packet_buf],
|
|
packet_type,
|
|
None,
|
|
mtu,
|
|
current_time,
|
|
);
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
};
|
|
|
|
return Ok(ReceiveResult::Ok);
|
|
}
|
|
|
|
/// Called internally when all fragments of a packet are received.
|
|
///
|
|
/// NOTE: header check codes will already have been validated on receipt of each fragment. AEAD authentication
|
|
/// and decryption has NOT yet been performed, and is done here.
|
|
fn receive_complete<'a, SendFunction: FnMut(&mut [u8])>(
|
|
&self,
|
|
app: &Application,
|
|
remote_address: &Application::RemoteAddress,
|
|
send: &mut SendFunction,
|
|
data_buf: &'a mut [u8],
|
|
counter: u32,
|
|
key_id: bool,
|
|
canonical_header_bytes: &[u8; 12],
|
|
fragments: &[Application::IncomingPacketBuffer],
|
|
packet_type: u8,
|
|
session: Option<Application::SessionRef>,
|
|
mtu: usize,
|
|
current_time: i64,
|
|
) -> Result<ReceiveResult<'a, Application>, Error> {
|
|
debug_assert!(fragments.len() >= 1);
|
|
|
|
// The first 'if' below should capture both DATA and NOP but not other types. Sanity check this.
|
|
debug_assert_eq!(PACKET_TYPE_DATA, 0);
|
|
debug_assert_eq!(PACKET_TYPE_NOP, 1);
|
|
|
|
if packet_type <= PACKET_TYPE_NOP {
|
|
if let Some(session) = session {
|
|
let state = session.state.read().unwrap();
|
|
if let Some(session_key) = state.session_keys[key_id as usize].as_ref() {
|
|
let mut c = session_key.get_receive_cipher();
|
|
c.reset_init_gcm(canonical_header_bytes);
|
|
////////////////////////////////////////////////////////////////
|
|
// packet decoding for post-noise transport
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
let mut data_len = 0;
|
|
|
|
// Decrypt fragments 0..N-1 where N is the number of fragments.
|
|
for f in fragments[..(fragments.len() - 1)].iter() {
|
|
let f = f.as_ref();
|
|
debug_assert!(f.len() >= HEADER_SIZE);
|
|
let current_frag_data_start = data_len;
|
|
data_len += f.len() - HEADER_SIZE;
|
|
if data_len > data_buf.len() {
|
|
unlikely_branch();
|
|
session_key.return_receive_cipher(c);
|
|
return Err(Error::DataBufferTooSmall);
|
|
}
|
|
c.crypt(&f[HEADER_SIZE..], &mut data_buf[current_frag_data_start..data_len]);
|
|
}
|
|
|
|
// Decrypt final fragment (or only fragment if not fragmented)
|
|
let current_frag_data_start = data_len;
|
|
let last_fragment = fragments.last().unwrap().as_ref();
|
|
if last_fragment.len() < (HEADER_SIZE + AES_GCM_TAG_SIZE) {
|
|
unlikely_branch();
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
data_len += last_fragment.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE);
|
|
if data_len > data_buf.len() {
|
|
unlikely_branch();
|
|
session_key.return_receive_cipher(c);
|
|
return Err(Error::DataBufferTooSmall);
|
|
}
|
|
let payload_end = last_fragment.len() - AES_GCM_TAG_SIZE;
|
|
c.crypt(
|
|
&last_fragment[HEADER_SIZE..payload_end],
|
|
&mut data_buf[current_frag_data_start..data_len],
|
|
);
|
|
|
|
let gcm_tag = &last_fragment[payload_end..];
|
|
let aead_authentication_ok = c.finish_decrypt(gcm_tag);
|
|
session_key.return_receive_cipher(c);
|
|
|
|
if aead_authentication_ok {
|
|
if session.receive_windows[key_id as usize].message_authenticated(counter) {
|
|
if packet_type == PACKET_TYPE_DATA {
|
|
return Ok(ReceiveResult::OkData(&mut data_buf[..data_len]));
|
|
} else {
|
|
unlikely_branch();
|
|
return Ok(ReceiveResult::Ok);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no known key authenticated the packet, decryption has failed.
|
|
return Err(Error::FailedAuthentication);
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::SessionNotEstablished);
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
|
|
// To greatly simplify logic handling key exchange packets, assemble these first.
|
|
// Handling KEX packets isn't the fast path so the extra copying isn't significant.
|
|
const KEX_BUF_LEN: usize = MIN_TRANSPORT_MTU * KEY_EXCHANGE_MAX_FRAGMENTS;
|
|
let mut kex_packet = [0_u8; KEX_BUF_LEN];
|
|
let mut kex_packet_len = 0;
|
|
for i in 0..fragments.len() {
|
|
let mut ff = fragments[i].as_ref();
|
|
if ff.len() < MIN_PACKET_SIZE {
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
if i > 0 {
|
|
ff = &ff[HEADER_SIZE..];
|
|
}
|
|
let j = kex_packet_len + ff.len();
|
|
if j > KEX_BUF_LEN {
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
kex_packet[kex_packet_len..j].copy_from_slice(ff);
|
|
kex_packet_len = j;
|
|
}
|
|
let kex_packet_saved_ciphertext = kex_packet.clone(); // save for HMAC check later
|
|
|
|
// Key exchange packets begin (after header) with the session protocol version. This could be
|
|
// changed in the future to support a different cipher suite.
|
|
if kex_packet[HEADER_SIZE] != SESSION_PROTOCOL_VERSION {
|
|
return Err(Error::UnknownProtocolVersion);
|
|
}
|
|
|
|
match packet_type {
|
|
PACKET_TYPE_INITIAL_KEY_OFFER => {
|
|
// alice (remote) -> bob (local)
|
|
////////////////////////////////////////////////////////////////
|
|
// packet decoding for noise initial key offer
|
|
// -> e, es, s, ss
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
if kex_packet_len < (HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + HMAC_SIZE + HMAC_SIZE) {
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
|
|
let plaintext_end = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE;
|
|
let payload_end = kex_packet_len - (AES_GCM_TAG_SIZE + HMAC_SIZE + HMAC_SIZE);
|
|
let aes_gcm_tag_end = kex_packet_len - (HMAC_SIZE + HMAC_SIZE);
|
|
let hmac1_end = kex_packet_len - HMAC_SIZE;
|
|
|
|
// Check the secondary HMAC first, which proves that the sender knows the recipient's full static identity.
|
|
if !secure_eq(
|
|
&hmac_sha384_2(
|
|
app.get_local_s_public_blob_hash(),
|
|
canonical_header_bytes,
|
|
&kex_packet[HEADER_SIZE..hmac1_end],
|
|
),
|
|
&kex_packet[hmac1_end..kex_packet_len],
|
|
) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Check rate limits.
|
|
if let Some(session) = session.as_ref() {
|
|
if current_time < session.state.read().unwrap().last_remote_offer + Application::REKEY_RATE_LIMIT_MS {
|
|
return Err(Error::RateLimited);
|
|
}
|
|
} else {
|
|
if !app.check_new_session(self, remote_address) {
|
|
return Err(Error::RateLimited);
|
|
}
|
|
}
|
|
|
|
// Key agreement: alice (remote) ephemeral NIST P-384 <> local static NIST P-384
|
|
let alice_e_public =
|
|
P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..plaintext_end]).ok_or(Error::FailedAuthentication)?;
|
|
let noise_es = app
|
|
.get_local_s_keypair()
|
|
.agree(&alice_e_public)
|
|
.ok_or(Error::FailedAuthentication)?;
|
|
|
|
// Initial key derivation from starting point, mixing in alice's ephemeral public and the es.
|
|
let noise_ik_incomplete_es = Secret(hmac_sha512(
|
|
&hmac_sha512(&INITIAL_KEY, alice_e_public.as_bytes()),
|
|
noise_es.as_bytes(),
|
|
));
|
|
|
|
// Decrypt the encrypted part of the packet payload and authenticate the above key exchange via AES-GCM auth.
|
|
let mut c = AesGcm::new(
|
|
kbkdf512(noise_ik_incomplete_es.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<AES_KEY_SIZE>(),
|
|
false,
|
|
);
|
|
c.reset_init_gcm(canonical_header_bytes);
|
|
c.crypt_in_place(&mut kex_packet[plaintext_end..payload_end]);
|
|
let gcm_tag = &kex_packet[payload_end..aes_gcm_tag_end];
|
|
if !c.finish_decrypt(gcm_tag) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Parse payload and get alice's session ID, alice's public blob, metadata, and (if present) Alice's Kyber1024 public.
|
|
let (
|
|
offer_id,
|
|
alice_session_id,
|
|
alice_s_public_blob,
|
|
alice_metadata,
|
|
alice_hk_public_raw,
|
|
alice_ratchet_key_fingerprint,
|
|
) = parse_dec_key_offer_after_header(&kex_packet[plaintext_end..kex_packet_len], packet_type)?;
|
|
|
|
// We either have a session, in which case they should have supplied a ratchet key fingerprint, or
|
|
// we don't and they should not have supplied one.
|
|
if session.is_some() != alice_ratchet_key_fingerprint.is_some() {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Extract alice's static NIST P-384 public key from her public blob.
|
|
let alice_s_public = Application::extract_s_public_from_raw(alice_s_public_blob).ok_or(Error::InvalidPacket)?;
|
|
|
|
// Key agreement: both sides' static P-384 keys.
|
|
let noise_ss = app
|
|
.get_local_s_keypair()
|
|
.agree(&alice_s_public)
|
|
.ok_or(Error::FailedAuthentication)?;
|
|
|
|
// Mix result of 'ss' agreement into master key.
|
|
let noise_ik_incomplete_es_ss = Secret(hmac_sha512(noise_ik_incomplete_es.as_bytes(), noise_ss.as_bytes()));
|
|
drop(noise_ik_incomplete_es);
|
|
|
|
// Authenticate entire packet with HMAC-SHA384, verifying alice's identity via 'ss' secret that was
|
|
// just mixed into the key.
|
|
if !secure_eq(
|
|
&hmac_sha384_2(
|
|
kbkdf512(noise_ik_incomplete_es_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(),
|
|
canonical_header_bytes,
|
|
&kex_packet_saved_ciphertext[HEADER_SIZE..aes_gcm_tag_end],
|
|
),
|
|
&kex_packet[aes_gcm_tag_end..hmac1_end],
|
|
) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Alice's offer has been verified and her current key state reconstructed.
|
|
|
|
// Perform checks and match ratchet key if there's an existing session, or gate (via host) and
|
|
// then create new sessions.
|
|
let (new_session, reply_counter, new_key_id, ratchet_key) = if let Some(session) = session.as_ref() {
|
|
// Existing session identity must match the one in this offer.
|
|
if !secure_eq(&session.remote_s_public_blob_hash, &SHA384::hash(&alice_s_public_blob)) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Match ratchet key fingerprint and fail if no match, which likely indicates an old offer packet.
|
|
let alice_ratchet_key_fingerprint = alice_ratchet_key_fingerprint.unwrap();
|
|
let mut ratchet_key = None;
|
|
let state = session.state.read().unwrap();
|
|
if let Some(k) = state.session_keys[key_id as usize].as_ref() {
|
|
if k.role == Role::Bob {
|
|
// The local party is not allowed to be Bob twice in a row
|
|
// This prevents rekeying failure from both parties attempting to rekey at the same time
|
|
return Ok(ReceiveResult::Ignored);
|
|
}
|
|
if public_fingerprint_of_secret(k.ratchet_key.as_bytes())[..16].eq(alice_ratchet_key_fingerprint) {
|
|
ratchet_key = Some(k.ratchet_key.clone());
|
|
}
|
|
}
|
|
if ratchet_key.is_none() {
|
|
return Ok(ReceiveResult::Ignored); // old packet?
|
|
}
|
|
|
|
(None, state.send_counters[key_id as usize].next(), !key_id, ratchet_key)
|
|
} else {
|
|
if key_id != false {
|
|
// All new sessions must start with key_id 0
|
|
// This has no security implications, it just makes programming the initial offer easier
|
|
return Ok(ReceiveResult::Ignored);
|
|
}
|
|
if let Some((new_session_id, psk, associated_object)) =
|
|
app.accept_new_session(self, remote_address, alice_s_public_blob, alice_metadata)
|
|
{
|
|
let header_check_cipher = Aes::new(
|
|
kbkdf512(noise_ss.as_bytes(), KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::<HEADER_CHECK_AES_KEY_SIZE>(),
|
|
);
|
|
let send_counter = Counter::new();
|
|
let reply_counter = send_counter.next();
|
|
(
|
|
Some(Session::<Application> {
|
|
id: new_session_id,
|
|
application_data: associated_object,
|
|
ratchet_counts: [AtomicU64::new(1), AtomicU64::new(0)],
|
|
header_check_cipher,
|
|
receive_windows: [CounterWindow::new(), CounterWindow::new_invalid()],
|
|
state: RwLock::new(SessionMutableState {
|
|
send_counters: [send_counter, Counter::new()],
|
|
remote_session_id: Some(alice_session_id),
|
|
session_keys: [None, None],//this is the only value which will be writen later
|
|
cur_session_key_id: false,
|
|
offer: None,
|
|
last_remote_offer: current_time,
|
|
}),
|
|
psk,
|
|
noise_ss,
|
|
remote_s_public_blob_hash: SHA384::hash(&alice_s_public_blob),
|
|
remote_s_public_p384_bytes: alice_s_public.as_bytes().clone(),
|
|
defrag: Mutex::new(RingBufferMap::new(random::xorshift64_random() as u32)),
|
|
}),
|
|
reply_counter,
|
|
false,
|
|
None
|
|
)
|
|
} else {
|
|
return Err(Error::NewSessionRejected);
|
|
}
|
|
};
|
|
|
|
// Set 'session' to a reference to either the existing or the new session.
|
|
let existing_session = session;
|
|
let session = existing_session.as_ref().map_or_else(|| new_session.as_ref().unwrap(), |s| &*s);
|
|
|
|
// Generate our ephemeral NIST P-384 key pair.
|
|
let bob_e_keypair = P384KeyPair::generate();
|
|
|
|
// Key agreement: both sides' ephemeral P-384 public keys.
|
|
let noise_ee = bob_e_keypair.agree(&alice_e_public).ok_or(Error::FailedAuthentication)?;
|
|
|
|
// Key agreement: bob (local) static NIST P-384, alice (remote) ephemeral P-384.
|
|
let noise_se = bob_e_keypair.agree(&alice_s_public).ok_or(Error::FailedAuthentication)?;
|
|
|
|
// Mix in the psk, the key to this point, our ephemeral public, ee, and se, completing Noise_IK.
|
|
//
|
|
// 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. Both inputs are secrets of
|
|
// fixed size so this shouldn't matter cryptographically.
|
|
let noise_ik_complete = Secret(hmac_sha512(
|
|
session.psk.as_bytes(),
|
|
&hmac_sha512(
|
|
&hmac_sha512(
|
|
&hmac_sha512(noise_ik_incomplete_es_ss.as_bytes(), bob_e_keypair.public_key_bytes()),
|
|
noise_ee.as_bytes(),
|
|
),
|
|
noise_se.as_bytes(),
|
|
),
|
|
));
|
|
drop(noise_ik_incomplete_es_ss);
|
|
drop(noise_ee);
|
|
drop(noise_se);
|
|
|
|
// At this point we've completed Noise_IK key derivation with NIST P-384 ECDH, but now for hybrid and ratcheting...
|
|
|
|
// Generate a Kyber encapsulated ciphertext if Kyber is enabled and the other side sent us a public key.
|
|
let (bob_hk_public, hybrid_kk) = if JEDI && alice_hk_public_raw.len() > 0 {
|
|
if let Ok((bob_hk_public, hybrid_kk)) =
|
|
pqc_kyber::encapsulate(alice_hk_public_raw, &mut random::SecureRandom::default())
|
|
{
|
|
(Some(bob_hk_public), Some(Secret(hybrid_kk)))
|
|
} else {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
// packet encoding for noise key counter offer
|
|
// <- e, ee, se
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
let mut reply_buf = [0_u8; KEX_BUF_LEN];
|
|
let mut idx = HEADER_SIZE;
|
|
|
|
idx = safe_write_all(&mut reply_buf, idx, &[SESSION_PROTOCOL_VERSION])?;
|
|
idx = safe_write_all(&mut reply_buf, idx, bob_e_keypair.public_key_bytes())?;
|
|
let plaintext_end = idx;
|
|
|
|
idx = safe_write_all(&mut reply_buf, idx, offer_id)?;
|
|
idx = safe_write_all(&mut reply_buf, idx, session.id.as_bytes())?;
|
|
idx = varint_safe_write(&mut reply_buf, idx, 0)?; // they don't need our static public; they have it
|
|
idx = varint_safe_write(&mut reply_buf, idx, 0)?; // no meta-data in counter-offers (could be used in the future)
|
|
if let Some(bob_hk_public) = bob_hk_public.as_ref() {
|
|
idx = safe_write_all(&mut reply_buf, idx, &[HYBRID_KEY_TYPE_KYBER1024])?;
|
|
idx = safe_write_all(&mut reply_buf, idx, bob_hk_public)?;
|
|
} else {
|
|
idx = safe_write_all(&mut reply_buf, idx, &[HYBRID_KEY_TYPE_NONE])?;
|
|
}
|
|
if ratchet_key.is_some() {
|
|
idx = safe_write_all(&mut reply_buf, idx, &[0x01])?;
|
|
idx = safe_write_all(&mut reply_buf, idx, alice_ratchet_key_fingerprint.unwrap())?;
|
|
} else {
|
|
idx = safe_write_all(&mut reply_buf, idx, &[0x00])?;
|
|
}
|
|
let payload_end = idx;
|
|
|
|
create_packet_header(
|
|
&mut reply_buf,
|
|
payload_end,
|
|
mtu,
|
|
PACKET_TYPE_KEY_COUNTER_OFFER,
|
|
alice_session_id.into(),
|
|
reply_counter,
|
|
key_id,
|
|
)?;
|
|
let reply_canonical_header =
|
|
CanonicalHeader::make(alice_session_id.into(), PACKET_TYPE_KEY_COUNTER_OFFER, reply_counter.to_u32());
|
|
|
|
// Encrypt reply packet using final Noise_IK key BEFORE mixing hybrid or ratcheting, since the other side
|
|
// must decrypt before doing these things.
|
|
let mut c = AesGcm::new(
|
|
kbkdf512(noise_ik_complete.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::<AES_KEY_SIZE>(),
|
|
true,
|
|
);
|
|
c.reset_init_gcm(reply_canonical_header.as_bytes());
|
|
c.crypt_in_place(&mut reply_buf[plaintext_end..payload_end]);
|
|
let gcm_tag = c.finish_encrypt();
|
|
|
|
idx = safe_write_all(&mut reply_buf, idx, &gcm_tag)?;
|
|
let aes_gcm_tag_end = idx;
|
|
|
|
// Mix ratchet key from previous session key (if any) and Kyber1024 hybrid shared key (if any).
|
|
let mut session_key = noise_ik_complete;
|
|
if let Some(ratchet_key) = ratchet_key {
|
|
session_key = Secret(hmac_sha512(ratchet_key.as_bytes(), session_key.as_bytes()));
|
|
}
|
|
if let Some(hybrid_kk) = hybrid_kk.as_ref() {
|
|
session_key = Secret(hmac_sha512(hybrid_kk.as_bytes(), session_key.as_bytes()));
|
|
}
|
|
|
|
// Authenticate packet using HMAC-SHA384 with final key. Note that while the final key now has the Kyber secret
|
|
// mixed in, this doesn't constitute session authentication with Kyber because there's no static Kyber key
|
|
// associated with the remote identity. An attacker who can break NIST P-384 (and has the psk) could MITM the
|
|
// Kyber exchange, but you'd need a not-yet-existing quantum computer for that.
|
|
let hmac = hmac_sha384_2(
|
|
kbkdf512(session_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(),
|
|
reply_canonical_header.as_bytes(),
|
|
&reply_buf[HEADER_SIZE..aes_gcm_tag_end],
|
|
);
|
|
idx = safe_write_all(&mut reply_buf, idx, &hmac)?;
|
|
let packet_end = idx;
|
|
if session.receive_windows[key_id as usize].message_authenticated(counter) {
|
|
//initial key offers should only check this if this is a rekey
|
|
let session_key = SessionKey::new(
|
|
session_key,
|
|
Role::Bob,
|
|
current_time,
|
|
hybrid_kk.is_some(),
|
|
);
|
|
|
|
let mut state = session.state.write().unwrap();
|
|
let _ = state.session_keys[new_key_id as usize].replace(session_key);
|
|
let _ = state.remote_session_id.replace(alice_session_id);
|
|
let ratchet_count = session.ratchet_counts[key_id as usize].load(Ordering::SeqCst);
|
|
if state.cur_session_key_id != new_key_id {//this prevents anything from being reset twice if the key offer was made twice
|
|
debug_assert!(new_key_id != key_id);
|
|
// receive_windows only has race conditions with the counter of the remote party. It is theoretically possible that the local host receives counters under new_key_id while the receive_window is still in the process of resetting, but this is very unlikely. If it does happen, two things could happen:
|
|
// 1) The received counter is less than what is currently stored in the window, so a valid packet would be rejected
|
|
// 2) The received counter is greater than what is currently stored in the window, so a valid packet would be accepted *but* its counter is deleted from the window so it can be replayed
|
|
// To prevent these race conditions, we only update the ratchet_count for salting the check code after the window has reset. So if a counter passes the initial check code: it either means the thread sees ratchet count has been update, therefore it either sees receive_window has been reset (due to memory orderings), or it means a rare accidental check forge has occurred.
|
|
session.receive_windows[new_key_id as usize].reset_for_new_key_offer();
|
|
session.ratchet_counts[new_key_id as usize].fetch_add(2, Ordering::SeqCst);
|
|
|
|
// if the following wasn't done inside a lock, a theoretical race condition exists where a thread uses the new key id before the counter is reset, or worse: a thread has held onto the previous key_id == new_key_id, and attempts to use the reset counter
|
|
// for this reason do not access send_counters without holding the read lock
|
|
state.cur_session_key_id = new_key_id;
|
|
state.send_counters[new_key_id as usize].reset_for_new_key_offer();
|
|
}
|
|
drop(state);
|
|
|
|
// Bob now has final key state for this exchange. Yay! Now reply to Alice so she can construct it.
|
|
|
|
send_with_fragmentation(send, &mut reply_buf[..packet_end], mtu, ratchet_count, &session.header_check_cipher);
|
|
|
|
if let Some(new_session) = new_session {
|
|
return Ok(ReceiveResult::OkNewSession(new_session));
|
|
} else {
|
|
return Ok(ReceiveResult::Ok);
|
|
}
|
|
} else {
|
|
return Ok(ReceiveResult::Ignored);
|
|
}
|
|
|
|
}
|
|
|
|
PACKET_TYPE_KEY_COUNTER_OFFER => {
|
|
// bob (remote) -> alice (local)
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
// packet decoding for noise key counter offer
|
|
// <- e, ee, se
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
if kex_packet_len < (HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + HMAC_SIZE) {
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
let plaintext_end = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE;
|
|
let payload_end = kex_packet_len - (AES_GCM_TAG_SIZE + HMAC_SIZE);
|
|
let aes_gcm_tag_end = kex_packet_len - HMAC_SIZE;
|
|
|
|
if let Some(session) = session {
|
|
let state = session.state.read().unwrap();
|
|
if let Some(offer) = state.offer.as_ref() {
|
|
let bob_e_public = P384PublicKey::from_bytes(&kex_packet[(HEADER_SIZE + 1)..plaintext_end])
|
|
.ok_or(Error::FailedAuthentication)?;
|
|
let noise_ee = offer.alice_e_keypair.agree(&bob_e_public).ok_or(Error::FailedAuthentication)?;
|
|
let noise_se = app.get_local_s_keypair().agree(&bob_e_public).ok_or(Error::FailedAuthentication)?;
|
|
|
|
let noise_ik_complete = Secret(hmac_sha512(
|
|
session.psk.as_bytes(),
|
|
&hmac_sha512(
|
|
&hmac_sha512(&hmac_sha512(offer.ss_key.as_bytes(), bob_e_public.as_bytes()), noise_ee.as_bytes()),
|
|
noise_se.as_bytes(),
|
|
),
|
|
));
|
|
drop(noise_ee);
|
|
drop(noise_se);
|
|
|
|
let mut c = AesGcm::new(
|
|
kbkdf512(noise_ik_complete.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE)
|
|
.first_n::<AES_KEY_SIZE>(),
|
|
false,
|
|
);
|
|
c.reset_init_gcm(canonical_header_bytes);
|
|
c.crypt_in_place(&mut kex_packet[plaintext_end..payload_end]);
|
|
let gcm_tag = &kex_packet[payload_end..aes_gcm_tag_end];
|
|
if !c.finish_decrypt(gcm_tag) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Alice has now completed Noise_IK with NIST P-384 and verified with GCM auth, but now for hybrid...
|
|
|
|
let (offer_id, bob_session_id, _, _, bob_hk_public_raw, bob_ratchet_key_id) =
|
|
parse_dec_key_offer_after_header(&kex_packet[plaintext_end..kex_packet_len], packet_type)?;
|
|
|
|
// Check that this is a counter offer to the original offer we sent.
|
|
if !(offer.id.eq(offer_id)) {
|
|
return Ok(ReceiveResult::Ignored);
|
|
|
|
}
|
|
|
|
// Kyber1024 key agreement if enabled.
|
|
let hybrid_kk = if JEDI && bob_hk_public_raw.len() > 0 && offer.alice_hk_keypair.is_some() {
|
|
if let Ok(hybrid_kk) =
|
|
pqc_kyber::decapsulate(bob_hk_public_raw, &offer.alice_hk_keypair.as_ref().unwrap().secret)
|
|
{
|
|
Some(Secret(hybrid_kk))
|
|
} else {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// The session key starts with the final noise_ik key and may have other things mixed into it below.
|
|
let mut session_key = noise_ik_complete;
|
|
|
|
// Mix ratchet key from previous session key (if any) and Kyber1024 hybrid shared key (if any).
|
|
// We either have a session, in which case they should have supplied a ratchet key fingerprint, or
|
|
// we don't and they should not have supplied one.
|
|
if let Some(cur_session_key) = state.session_keys[key_id as usize].as_ref() {
|
|
if bob_ratchet_key_id.is_some() {
|
|
session_key = Secret(hmac_sha512(cur_session_key.ratchet_key.as_bytes(), session_key.as_bytes()));
|
|
} else {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
} else if bob_ratchet_key_id.is_some() {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
if let Some(hybrid_kk) = hybrid_kk.as_ref() {
|
|
session_key = Secret(hmac_sha512(hybrid_kk.as_bytes(), session_key.as_bytes()));
|
|
}
|
|
|
|
// Check main packet HMAC for full validation of session key.
|
|
if !secure_eq(
|
|
&hmac_sha384_2(
|
|
kbkdf512(session_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(),
|
|
canonical_header_bytes,
|
|
&kex_packet_saved_ciphertext[HEADER_SIZE..aes_gcm_tag_end],
|
|
),
|
|
&kex_packet[aes_gcm_tag_end..kex_packet_len],
|
|
) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
if session.receive_windows[key_id as usize].message_authenticated(counter) {
|
|
// Alice has now completed and validated the full hybrid exchange.
|
|
|
|
let session_key = SessionKey::new(
|
|
session_key,
|
|
Role::Alice,
|
|
current_time,
|
|
hybrid_kk.is_some(),
|
|
);
|
|
|
|
let new_key_id = offer.key_id;
|
|
drop(state);
|
|
//TODO: check for correct orderings
|
|
let mut state = session.state.write().unwrap();
|
|
let _ = state.remote_session_id.replace(bob_session_id);
|
|
let _ = state.session_keys[new_key_id as usize].replace(session_key);
|
|
if state.cur_session_key_id != new_key_id {
|
|
//when an brand new key offer is sent, it is sent using the new_key_id==false counter, we cannot reset it in that case.
|
|
//NOTE: the following code should be properly threadsafe, see the large comment above at the end of KEY_OFFER decoding for more info
|
|
session.receive_windows[new_key_id as usize].reset_for_new_key_offer();
|
|
let _ = session.ratchet_counts[new_key_id as usize].fetch_add(2, Ordering::SeqCst);
|
|
state.cur_session_key_id = new_key_id;
|
|
state.send_counters[new_key_id as usize].reset_for_new_key_offer();
|
|
}
|
|
let _ = state.offer.take();
|
|
|
|
return Ok(ReceiveResult::Ok);
|
|
} else {
|
|
return Ok(ReceiveResult::Ignored);
|
|
}
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::SessionNotEstablished);
|
|
}
|
|
|
|
// Just ignore counter-offers that are out of place. They probably indicate that this side
|
|
// restarted and needs to establish a new session.
|
|
return Ok(ReceiveResult::Ignored);
|
|
}
|
|
|
|
_ => return Err(Error::InvalidPacket),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create an send an ephemeral offer, populating ret_ephemeral_offer on success.
|
|
/// If there is no current session key set `current_key_id == new_key_id == false`
|
|
fn send_ephemeral_offer<SendFunction: FnMut(&mut [u8])>(
|
|
send: &mut SendFunction,
|
|
counter: CounterValue,
|
|
current_key_id: bool,
|
|
new_key_id: bool,
|
|
alice_session_id: SessionId,
|
|
bob_session_id: Option<SessionId>,
|
|
alice_s_public_blob: &[u8],
|
|
alice_metadata: &[u8],
|
|
bob_s_public: &P384PublicKey,
|
|
bob_s_public_blob_hash: &[u8],
|
|
noise_ss: &Secret<48>,
|
|
current_key: Option<&SessionKey>,
|
|
ratchet_count: u64,
|
|
header_check_cipher: Option<&Aes>, // None to use one based on the recipient's public key for initial contact
|
|
mtu: usize,
|
|
current_time: i64,
|
|
ret_ephemeral_offer: &mut Option<EphemeralOffer>, // We want to prevent copying the EphemeralOffer up the stack because it's very big. ret_ephemeral_offer will be overwritten with the returned EphemeralOffer when the call completes.
|
|
) -> Result<(), Error> {
|
|
// Generate a NIST P-384 pair.
|
|
let alice_e_keypair = P384KeyPair::generate();
|
|
|
|
// Perform key agreement with the other side's static P-384 public key.
|
|
let noise_es = alice_e_keypair.agree(bob_s_public).ok_or(Error::InvalidPacket)?;
|
|
|
|
// Generate a Kyber1024 (hybrid PQ crypto) pair if enabled.
|
|
let alice_hk_keypair = if JEDI {
|
|
Some(pqc_kyber::keypair(&mut random::SecureRandom::get()))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Random ephemeral offer ID
|
|
let id: [u8; 16] = random::get_bytes_secure();
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
// packet encoding for noise initial key offer and for noise rekeying
|
|
// -> e, es, s, ss
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
// Create ephemeral offer packet (not fragmented yet).
|
|
const PACKET_BUF_SIZE: usize = MIN_TRANSPORT_MTU * KEY_EXCHANGE_MAX_FRAGMENTS;
|
|
let mut packet_buf = [0_u8; PACKET_BUF_SIZE];
|
|
let mut idx = HEADER_SIZE;
|
|
|
|
idx = safe_write_all(&mut packet_buf, idx, &[SESSION_PROTOCOL_VERSION])?;
|
|
//TODO: check this, the below line is supposed to be the blob, not just the key, right?
|
|
idx = safe_write_all(&mut packet_buf, idx, alice_e_keypair.public_key_bytes())?;
|
|
let plaintext_end = idx;
|
|
|
|
idx = safe_write_all(&mut packet_buf, idx, &id)?;
|
|
idx = safe_write_all(&mut packet_buf, idx, alice_session_id.as_bytes())?;
|
|
idx = varint_safe_write(&mut packet_buf, idx, alice_s_public_blob.len() as u64)?;
|
|
idx = safe_write_all(&mut packet_buf, idx, alice_s_public_blob)?;
|
|
idx = varint_safe_write(&mut packet_buf, idx, alice_metadata.len() as u64)?;
|
|
idx = safe_write_all(&mut packet_buf, idx, alice_metadata)?;
|
|
if let Some(hkp) = alice_hk_keypair {
|
|
idx = safe_write_all(&mut packet_buf, idx, &[HYBRID_KEY_TYPE_KYBER1024])?;
|
|
idx = safe_write_all(&mut packet_buf, idx, &hkp.public)?;
|
|
} else {
|
|
idx = safe_write_all(&mut packet_buf, idx, &[HYBRID_KEY_TYPE_NONE])?;
|
|
}
|
|
if let Some(current_key) = current_key {
|
|
idx = safe_write_all(&mut packet_buf, idx, &[0x01])?;
|
|
idx = safe_write_all(&mut packet_buf, idx, &public_fingerprint_of_secret(current_key.ratchet_key.as_bytes())[..16])?;
|
|
} else {
|
|
idx = safe_write_all(&mut packet_buf, idx, &[0x00])?;
|
|
}
|
|
let payload_end = idx;
|
|
|
|
// Create ephemeral agreement secret.
|
|
let es_key = Secret(hmac_sha512(
|
|
&hmac_sha512(&INITIAL_KEY, alice_e_keypair.public_key_bytes()),
|
|
noise_es.as_bytes(),
|
|
));
|
|
|
|
let bob_session_id = bob_session_id.unwrap_or(SessionId::NIL);
|
|
create_packet_header(
|
|
&mut packet_buf,
|
|
payload_end,
|
|
mtu,
|
|
PACKET_TYPE_INITIAL_KEY_OFFER,
|
|
bob_session_id,
|
|
counter,
|
|
current_key_id,
|
|
)?;
|
|
|
|
let canonical_header = CanonicalHeader::make(bob_session_id, PACKET_TYPE_INITIAL_KEY_OFFER, counter.to_u32());
|
|
|
|
// Encrypt packet and attach AES-GCM tag.
|
|
let gcm_tag = {
|
|
let mut c = AesGcm::new(
|
|
kbkdf512(es_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<AES_KEY_SIZE>(),
|
|
true,
|
|
);
|
|
c.reset_init_gcm(canonical_header.as_bytes());
|
|
c.crypt_in_place(&mut packet_buf[plaintext_end..payload_end]);
|
|
c.finish_encrypt()
|
|
};
|
|
|
|
idx = safe_write_all(&mut packet_buf, idx, &gcm_tag)?;
|
|
let aes_gcm_tag_end = idx;
|
|
|
|
// Mix in static secret.
|
|
let ss_key = Secret(hmac_sha512(es_key.as_bytes(), noise_ss.as_bytes()));
|
|
drop(es_key);
|
|
|
|
// HMAC packet using static + ephemeral key.
|
|
let hmac1 = hmac_sha384_2(
|
|
kbkdf512(ss_key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(),
|
|
canonical_header.as_bytes(),
|
|
&packet_buf[HEADER_SIZE..aes_gcm_tag_end],
|
|
);
|
|
idx = safe_write_all(&mut packet_buf, idx, &hmac1)?;
|
|
let hmac1_end = idx;
|
|
|
|
// Add secondary HMAC to verify that the caller knows the recipient's full static public identity.
|
|
let hmac2 = hmac_sha384_2(
|
|
bob_s_public_blob_hash,
|
|
canonical_header.as_bytes(),
|
|
&packet_buf[HEADER_SIZE..hmac1_end],
|
|
);
|
|
idx = safe_write_all(&mut packet_buf, idx, &hmac2)?;
|
|
let packet_end = idx;
|
|
|
|
if let Some(header_check_cipher) = header_check_cipher {
|
|
send_with_fragmentation(send, &mut packet_buf[..packet_end], mtu, ratchet_count, header_check_cipher);
|
|
} else {
|
|
send_with_fragmentation(
|
|
send,
|
|
&mut packet_buf[..packet_end],
|
|
mtu,
|
|
ratchet_count,
|
|
&Aes::new(kbkdf512(&bob_s_public_blob_hash, KBKDF_KEY_USAGE_LABEL_HEADER_CHECK).first_n::<HEADER_CHECK_AES_KEY_SIZE>()),
|
|
);
|
|
}
|
|
|
|
*ret_ephemeral_offer = Some(EphemeralOffer {
|
|
id,
|
|
key_id: new_key_id,
|
|
creation_time: current_time,
|
|
ss_key,
|
|
alice_e_keypair,
|
|
alice_hk_keypair,
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Populate all but the header check code in the first 16 bytes of a packet or fragment.
|
|
fn create_packet_header(
|
|
header_destination_buffer: &mut [u8],
|
|
packet_len: usize,
|
|
mtu: usize,
|
|
packet_type: u8,
|
|
recipient_session_id: SessionId,
|
|
counter: CounterValue,
|
|
key_id: bool
|
|
) -> Result<(), Error> {
|
|
let fragment_count = ((packet_len as f32) / (mtu - HEADER_SIZE) as f32).ceil() as usize;
|
|
|
|
debug_assert!(header_destination_buffer.len() >= HEADER_SIZE);
|
|
debug_assert!(mtu >= MIN_TRANSPORT_MTU);
|
|
debug_assert!(packet_len >= MIN_PACKET_SIZE);
|
|
debug_assert!(fragment_count > 0);
|
|
debug_assert!(fragment_count <= MAX_FRAGMENTS);
|
|
debug_assert!(packet_type <= 0x0f); // packet type is 4 bits
|
|
|
|
if fragment_count <= MAX_FRAGMENTS {
|
|
// Header indexed by bit/byte:
|
|
// [0-31]/[0-3] header check code (computed later)
|
|
// [32-32]/[4-] key id
|
|
// [33-63]/[-7] counter
|
|
// [64-111]/[8-13] recipient's session ID (unique on their side)
|
|
// [112-115]/[14-] packet type (0-15)
|
|
// [116-121]/[-] number of fragments (0..63 for 1..64 fragments total)
|
|
// [122-127]/[-15] fragment number (0, 1, 2, ...)
|
|
memory::store_raw((counter.to_u32().wrapping_shl(1) | (key_id as u32)).to_le(), &mut header_destination_buffer[4..]);
|
|
memory::store_raw(
|
|
(u64::from(recipient_session_id) | (packet_type as u64).wrapping_shl(48) | ((fragment_count - 1) as u64).wrapping_shl(52))
|
|
.to_le(),
|
|
&mut header_destination_buffer[8..],
|
|
);
|
|
Ok(())
|
|
} else {
|
|
unlikely_branch();
|
|
Err(Error::DataTooLarge)
|
|
}
|
|
}
|
|
|
|
/// Break a packet into fragments and send them all.
|
|
fn send_with_fragmentation<SendFunction: FnMut(&mut [u8])>(
|
|
send: &mut SendFunction,
|
|
packet: &mut [u8],
|
|
mtu: usize,
|
|
ratchet_count: u64,
|
|
header_check_cipher: &Aes,
|
|
) {
|
|
let packet_len = packet.len();
|
|
let mut fragment_start = 0;
|
|
let mut fragment_end = packet_len.min(mtu);
|
|
let mut header: [u8; 16] = packet[..HEADER_SIZE].try_into().unwrap();
|
|
loop {
|
|
let fragment = &mut packet[fragment_start..fragment_end];
|
|
set_header_check_code(fragment, ratchet_count, header_check_cipher);
|
|
send(fragment);
|
|
if fragment_end < packet_len {
|
|
debug_assert!(header[15].wrapping_shr(2) < 63);
|
|
header[15] += 0x04; // increment fragment number
|
|
fragment_start = fragment_end - HEADER_SIZE;
|
|
fragment_end = (fragment_start + mtu).min(packet_len);
|
|
packet[fragment_start..(fragment_start + HEADER_SIZE)].copy_from_slice(&header);
|
|
} else {
|
|
debug_assert_eq!(fragment_end, packet_len);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set 32-bit header check code, used to make fragmentation mechanism robust.
|
|
fn set_header_check_code(packet: &mut [u8], ratchet_count: u64, header_check_cipher: &Aes) {
|
|
debug_assert!(packet.len() >= MIN_PACKET_SIZE);
|
|
//4 bytes is the ratchet key
|
|
//12 bytes is the header we want to verify
|
|
let mut header_mac = 0u128.to_le_bytes();
|
|
memory::store_raw((ratchet_count as u32).to_le_bytes(), &mut header_mac[0..4]);
|
|
header_mac[4..16].copy_from_slice(&packet[4..16]);
|
|
header_check_cipher.encrypt_block_in_place(&mut header_mac);
|
|
|
|
packet[..4].copy_from_slice(&header_mac[..4]);
|
|
}
|
|
|
|
/// Verify 32-bit header check code.
|
|
/// This is not nearly enough entropy to be cryptographically secure, it only is meant for making DOS attacks very hard
|
|
fn verify_header_check_code(packet: &[u8], ratchet_count: u64, header_check_cipher: &Aes) -> bool {
|
|
debug_assert!(packet.len() >= MIN_PACKET_SIZE);
|
|
//4 bytes is the ratchet key
|
|
//12 bytes is the header we want to verify
|
|
let mut header_mac = 0u128.to_le_bytes();
|
|
memory::store_raw((ratchet_count as u32).to_le_bytes(), &mut header_mac[0..4]);
|
|
header_mac[4..16].copy_from_slice(&packet[4..16]);
|
|
header_check_cipher.encrypt_block_in_place(&mut header_mac);
|
|
|
|
memory::load_raw::<u32>(&packet[..4]) == memory::load_raw::<u32>(&header_mac[..4])
|
|
}
|
|
|
|
/// Parse KEY_OFFER and KEY_COUNTER_OFFER starting after the unencrypted public key part.
|
|
fn parse_dec_key_offer_after_header(
|
|
incoming_packet: &[u8],
|
|
packet_type: u8,
|
|
) -> Result<(&[u8], SessionId, &[u8], &[u8], &[u8], Option<&[u8]>), Error> {
|
|
let mut p = &incoming_packet[..];
|
|
let offer_id = safe_read_exact(&mut p, 16)?;
|
|
|
|
let mut session_id_buf = 0_u64.to_ne_bytes();
|
|
session_id_buf[..SESSION_ID_SIZE].copy_from_slice(safe_read_exact(&mut p, SESSION_ID_SIZE)?);
|
|
let alice_session_id = SessionId::new_from_u64(u64::from_le_bytes(session_id_buf)).ok_or(Error::InvalidPacket)?;
|
|
|
|
let alice_s_public_blob_len = varint_safe_read(&mut p)?;
|
|
let alice_s_public_blob = safe_read_exact(&mut p, alice_s_public_blob_len as usize)?;
|
|
|
|
let alice_metadata_len = varint_safe_read(&mut p)?;
|
|
let alice_metadata = safe_read_exact(&mut p, alice_metadata_len as usize)?;
|
|
|
|
let alice_hk_public_raw = match safe_read_exact(&mut p, 1)?[0] {
|
|
HYBRID_KEY_TYPE_KYBER1024 => {
|
|
if packet_type == PACKET_TYPE_INITIAL_KEY_OFFER {
|
|
safe_read_exact(&mut p, pqc_kyber::KYBER_PUBLICKEYBYTES)?
|
|
} else {
|
|
safe_read_exact(&mut p, pqc_kyber::KYBER_CIPHERTEXTBYTES)?
|
|
}
|
|
}
|
|
_ => &[],
|
|
};
|
|
|
|
if p.is_empty() {
|
|
return Err(Error::InvalidPacket);
|
|
}
|
|
let alice_ratchet_key_fingerprint = if safe_read_exact(&mut p, 1)?[0] == 0x01 {
|
|
Some(safe_read_exact(&mut p, 16)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok((
|
|
offer_id, //always 16 bytes
|
|
alice_session_id,
|
|
alice_s_public_blob,
|
|
alice_metadata,
|
|
alice_hk_public_raw,
|
|
alice_ratchet_key_fingerprint, //always 16 bytes
|
|
))
|
|
}
|
|
|
|
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,
|
|
jedi: bool,
|
|
) -> Self {
|
|
let a2b: Secret<AES_KEY_SIZE> = kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n_clone();
|
|
let b2a: Secret<AES_KEY_SIZE> = 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 {
|
|
secret_fingerprint: public_fingerprint_of_secret(key.as_bytes())[..16].try_into().unwrap(),
|
|
creation_time: current_time,
|
|
lifetime: KeyLifetime::new(current_time),
|
|
ratchet_key: kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_RATCHETING),
|
|
receive_key,
|
|
send_key,
|
|
receive_cipher_pool: Mutex::new(Vec::with_capacity(2)),
|
|
send_cipher_pool: Mutex::new(Vec::with_capacity(2)),
|
|
jedi,
|
|
role,
|
|
}
|
|
}
|
|
|
|
fn get_send_cipher(&self, counter: CounterValue) -> Result<Box<AesGcm>, Error> {
|
|
if !self.lifetime.expired(counter) {
|
|
Ok(self
|
|
.send_cipher_pool
|
|
.lock()
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap_or_else(|| Box::new(AesGcm::new(self.send_key.as_bytes(), true))))
|
|
} else {
|
|
// Not only do we return an error, but we also destroy the key.
|
|
let mut scp = self.send_cipher_pool.lock().unwrap();
|
|
scp.clear();
|
|
self.send_key.nuke();
|
|
|
|
Err(Error::MaxKeyLifetimeExceeded)
|
|
}
|
|
}
|
|
|
|
fn return_send_cipher(&self, c: Box<AesGcm>) {
|
|
self.send_cipher_pool.lock().unwrap().push(c);
|
|
}
|
|
|
|
fn get_receive_cipher(&self) -> Box<AesGcm> {
|
|
self.receive_cipher_pool
|
|
.lock()
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap_or_else(|| Box::new(AesGcm::new(self.receive_key.as_bytes(), false)))
|
|
}
|
|
|
|
fn return_receive_cipher(&self, c: Box<AesGcm>) {
|
|
self.receive_cipher_pool.lock().unwrap().push(c);
|
|
}
|
|
}
|
|
|
|
impl KeyLifetime {
|
|
fn new(current_time: i64) -> Self {
|
|
Self {
|
|
rekey_at_or_after_counter: REKEY_AFTER_USES as u32 + (random::next_u32_secure() % REKEY_AFTER_USES_MAX_JITTER),
|
|
rekey_at_or_after_timestamp: current_time
|
|
+ REKEY_AFTER_TIME_MS
|
|
+ (random::next_u32_secure() % REKEY_AFTER_TIME_MS_MAX_JITTER) as i64,
|
|
}
|
|
}
|
|
|
|
fn should_rekey(&self, counter: CounterValue, current_time: i64) -> bool {
|
|
counter.to_u32() >= self.rekey_at_or_after_counter || current_time >= self.rekey_at_or_after_timestamp
|
|
}
|
|
|
|
fn expired(&self, counter: CounterValue) -> bool {
|
|
counter.to_u32() >= EXPIRE_AFTER_USES as u32
|
|
}
|
|
}
|
|
|
|
impl CanonicalHeader {
|
|
pub fn make(session_id: SessionId, packet_type: u8, counter: u32) -> Self {
|
|
CanonicalHeader(
|
|
(u64::from(session_id) | (packet_type as u64).wrapping_shl(48)).to_le(),
|
|
counter.to_le(),
|
|
)
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn as_bytes(&self) -> &[u8; 12] {
|
|
memory::as_byte_array(self)
|
|
}
|
|
}
|
|
|
|
/// Write src into buffer starting at the index idx. If buffer cannot fit src at that location, nothing at all is written and Error::UnexpectedBufferOverrun is returned. No other errors can be returned by this function. An idx incremented by the amount written is returned.
|
|
fn safe_write_all(buffer: &mut [u8], idx: usize, src: &[u8]) -> Result<usize, Error> {
|
|
let dest = &mut buffer[idx..];
|
|
let amt = src.len();
|
|
if dest.len() >= amt {
|
|
dest[..amt].copy_from_slice(src);
|
|
Ok(idx + amt)
|
|
} else {
|
|
unlikely_branch();
|
|
Err(Error::UnexpectedBufferOverrun)
|
|
}
|
|
}
|
|
|
|
/// Write a variable length integer, which can consume up to 10 bytes. Uses safe_write_all to do so.
|
|
fn varint_safe_write(buffer: &mut [u8], idx: usize, v: u64) -> Result<usize, Error> {
|
|
let mut b = [0_u8; varint::VARINT_MAX_SIZE_BYTES];
|
|
let i = varint::encode(&mut b, v);
|
|
safe_write_all(buffer, idx, &b[0..i])
|
|
}
|
|
|
|
/// Read exactly amt bytes from src and return the slice those bytes reside in. If src is smaller than amt, Error::InvalidPacket is returned. if the read was successful src is incremented to start at the first unread byte.
|
|
fn safe_read_exact<'a>(src: &mut &'a [u8], amt: usize) -> Result<&'a [u8], Error> {
|
|
if src.len() >= amt {
|
|
let (a, b) = src.split_at(amt);
|
|
*src = b;
|
|
Ok(a)
|
|
} else {
|
|
unlikely_branch();
|
|
Err(Error::InvalidPacket)
|
|
}
|
|
}
|
|
|
|
/// Read a variable length integer, which can consume up to 10 bytes. Uses varint_safe_read to do so.
|
|
fn varint_safe_read(src: &mut &[u8]) -> Result<u64, Error> {
|
|
let (v, amt) = varint::decode(*src).ok_or(Error::InvalidPacket)?;
|
|
let (_, b) = src.split_at(amt);
|
|
*src = b;
|
|
Ok(v)
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
|
|
/// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 12)
|
|
///
|
|
/// Cryptographically this isn't meaningfully different from HMAC(key, [label]),
|
|
/// but NIST seems to like it this way.
|
|
fn kbkdf512(key: &[u8], label: u8) -> Secret<64> {
|
|
Secret(hmac_sha512(key, &[0, 0, 0, 0, b'Z', b'T', label, 0, 0, 0, 0, 0x02, 0x00]))
|
|
}
|
|
|
|
/// Get a hash of a secret that can be used as a public key fingerprint to check ratcheting during key exchange.
|
|
fn public_fingerprint_of_secret(key: &[u8]) -> [u8; 48] {
|
|
let mut tmp = SHA384::new();
|
|
tmp.update(&[0xf0, 0x0d]); // arbitrary salt
|
|
tmp.update(key);
|
|
tmp.finish()
|
|
}
|