mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-16 17:33:45 +02:00
1198 lines
51 KiB
Rust
1198 lines
51 KiB
Rust
// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
|
|
|
|
use std::num::NonZeroU64;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
use crate::aes::{Aes, AesGcm};
|
|
use crate::hash::{hmac_sha384, hmac_sha512, SHA384, SHA512};
|
|
use crate::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE};
|
|
use crate::random;
|
|
use crate::secret::Secret;
|
|
|
|
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
|
|
|
/*
|
|
|
|
ZeroTier V2 Noise(-like?) Session Protocol
|
|
|
|
This protocol implements the Noise_IK key exchange pattern using NIST P-384 ECDH, AES-GCM,
|
|
and SHA512. So yes, Virginia, it's a FIPS-compliant Noise implementation. NIST P-384 is
|
|
not listed in official Noise documentation though, so consider it "Noise-like" if you
|
|
prefer.
|
|
|
|
See also: http://noiseprotocol.org/noise.html
|
|
|
|
Secondary hybrid exchange using Kyber512, the recently approved post-quantum KEX algorithm,
|
|
is also supported but is optional. When it is enabled the additional shared secret is
|
|
mixed into the final Noise_IK secret with HMAC/HKDF. This provides an exchange at least as
|
|
strong as the stronger of the two algorithms (ECDH and Kyber) since hashing anything with
|
|
a secret yields a secret.
|
|
|
|
Kyber theoretically provides data forward secrecy into the post-quantum era if and when it
|
|
arrives. It might also reassure those paranoid about NIST elliptic curves a little, though
|
|
we tend to accept the arguments of Koblitz and Menezes against the curves being backdoored.
|
|
These arguments are explained at the end of this post:
|
|
|
|
https://blog.cryptographyengineering.com/2015/10/22/a-riddle-wrapped-in-curve/
|
|
|
|
Kyber is used as long as both sides set the "jedi" parameter to true. It should be used
|
|
by default but can be disabled on tiny and slow devices or systems that talk to vast
|
|
numbers of endpoints and don't want the extra overhead.
|
|
|
|
Lastly, this protocol includes obfusation using a hash of the recipient's public identity
|
|
as a key. AES is used to encrypt the first block of each packet (or the first few blocks
|
|
for key exchange packets), making packets appear as pure noise to anyone who does not know
|
|
the identity of the recipient.
|
|
|
|
Obfuscation renders ZeroTier traffic uncategorizable to those who do not know the identity
|
|
of a packet's recipient, helping to defend against bulk de-anonymization. It also makes it
|
|
easy for recipient nodes to silently discard packets from senders that do not know them,
|
|
maintaining invisibility from naive network scanners.
|
|
|
|
Obfuscation doesn't play any meaningful role in data privacy or authentication. It can be
|
|
ignored when analyzing the "real" security of the protocol.
|
|
|
|
*/
|
|
|
|
/// Minimum packet size / minimum size for work buffers.
|
|
pub const MIN_BUFFER_SIZE: usize = 1400;
|
|
|
|
/// Start attempting to rekey after a key has been used to send packets this many times.
|
|
const REKEY_AFTER_USES: u64 = 536870912;
|
|
|
|
/// Maximum random jitter to add to rekey-after usage count.
|
|
const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576;
|
|
|
|
/// Hard expiration after this many uses.
|
|
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.
|
|
const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour
|
|
|
|
/// Maximum random jitter to add to rekey-after time.
|
|
const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 5;
|
|
|
|
/// Don't send or process inbound offers more often than this.
|
|
const OFFER_RATE_LIMIT_MS: i64 = 1000;
|
|
|
|
const PACKET_TYPE_DATA: u8 = 0;
|
|
const PACKET_TYPE_NOP: u8 = 1;
|
|
const PACKET_TYPE_KEY_OFFER: u8 = 2; // "alice"
|
|
const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3; // "bob"
|
|
|
|
/// Secondary (hybrid) ephemeral key disabled.
|
|
const E1_TYPE_NONE: u8 = 0;
|
|
|
|
/// Secondary (hybrid) ephemeral key is Kyber512
|
|
const E1_TYPE_KYBER512: u8 = 1;
|
|
|
|
/// Header size; header is: [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;
|
|
|
|
/// Aribitrary starting value for key derivation chain.
|
|
///
|
|
/// It doesn't matter very much what this is, but it's good for it to be unique.
|
|
const KEY_DERIVATION_CHAIN_STARTING_SALT: [u8; 64] = [
|
|
// macOS command line to generate:
|
|
// echo -n 'Noise_IKpsk2_NISTP384+hybrid_AESGCM_SHA512' | shasum -a 512 | cut -d ' ' -f 1 | xxd -r -p | xxd -i
|
|
0xc7, 0x66, 0xf3, 0x71, 0xc8, 0xbc, 0xc3, 0x19, 0xc6, 0xf0, 0x2a, 0x6e, 0x5c, 0x4b, 0x3c, 0xc0, 0x83, 0x29, 0x09, 0x09, 0x14, 0x4a, 0xf0, 0xde, 0xea, 0x3d, 0xbd, 0x00, 0x4c, 0x9e, 0x01, 0xa0, 0x6e, 0xb6, 0x9b, 0x56, 0x47, 0x97, 0x86, 0x1d, 0x4e, 0x94,
|
|
0xc5, 0xdd, 0xde, 0x4a, 0x1c, 0xc3, 0x4e, 0xcc, 0x8b, 0x09, 0x3b, 0xb3, 0xc3, 0xb0, 0x03, 0xd7, 0xdf, 0x22, 0x49, 0x3f, 0xa5, 0x01,
|
|
];
|
|
|
|
const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'M';
|
|
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(Option<SessionId>),
|
|
|
|
/// Packet was not well formed
|
|
InvalidPacket,
|
|
|
|
/// An invalid paramter was supplied to the function
|
|
InvalidParameter,
|
|
|
|
/// Packet failed one or more authentication (MAC) checks
|
|
FailedAuthentication,
|
|
|
|
/// New session was rejected by caller's supplied authentication check function
|
|
NewSessionRejected,
|
|
|
|
/// Rekeying failed and session secret has reached its maximum usage count
|
|
MaxKeyLifetimeExceeded,
|
|
|
|
/// Attempt to send using session without established key.
|
|
SessionNotEstablished,
|
|
}
|
|
|
|
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"),
|
|
Self::SessionNotEstablished => f.write_str("SessionNotEstablished"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for Error {}
|
|
|
|
impl std::fmt::Debug for Error {
|
|
#[inline(always)]
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
std::fmt::Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
/// Obfuscator/deobfuscator for recipient privacy masking 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)[..32]))
|
|
}
|
|
}
|
|
|
|
pub enum ReceiveResult<'a, O> {
|
|
/// Packet is valid and contained a data payload.
|
|
OkData(&'a [u8], u32),
|
|
|
|
/// 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,
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
#[repr(transparent)]
|
|
pub struct SessionId(NonZeroU64);
|
|
|
|
impl SessionId {
|
|
pub const MAX_BIT_MASK: u64 = 0xffffffffffff;
|
|
|
|
#[inline(always)]
|
|
pub fn new_from_bytes(b: &[u8]) -> Option<SessionId> {
|
|
if b.len() >= 6 {
|
|
let value = (u32::from_le_bytes(b[..4].try_into().unwrap()) as u64) | (u16::from_le_bytes(b[4..6].try_into().unwrap()) as u64).wrapping_shl(32);
|
|
if value > 0 && value <= Self::MAX_BIT_MASK {
|
|
return Some(Self(NonZeroU64::new(value).unwrap()));
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn new_random() -> Self {
|
|
Self(NonZeroU64::new((random::next_u64_secure() & Self::MAX_BIT_MASK).max(1)).unwrap())
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn copy_to(&self, b: &mut [u8]) {
|
|
b[..6].copy_from_slice(&self.0.get().to_le_bytes()[..6])
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u64> for SessionId {
|
|
type Error = self::Error;
|
|
|
|
#[inline(always)]
|
|
fn try_from(value: u64) -> Result<Self, Self::Error> {
|
|
if value > 0 && value <= Self::MAX_BIT_MASK {
|
|
Ok(Self(NonZeroU64::new(value).unwrap()))
|
|
} else {
|
|
Err(Error::InvalidParameter)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<SessionId> for u64 {
|
|
#[inline(always)]
|
|
fn from(sid: SessionId) -> Self {
|
|
sid.0.get()
|
|
}
|
|
}
|
|
|
|
/// ZeroTier Noise encrypted channel session.
|
|
pub struct Session<O> {
|
|
/// Local session ID
|
|
pub id: SessionId,
|
|
|
|
send_counter: Counter,
|
|
remote_s_public_hash: [u8; 48],
|
|
psk: Secret<64>,
|
|
ss: Secret<48>,
|
|
outgoing_obfuscator: Obfuscator,
|
|
state: RwLock<State>,
|
|
remote_s_public_p384: [u8; P384_PUBLIC_KEY_SIZE],
|
|
|
|
/// Arbitrary object associated with this session
|
|
pub associated_object: O,
|
|
}
|
|
|
|
/// Mutable inner state of Session (except counter, which is atomic to avoid locks on packet sends)
|
|
struct State {
|
|
remote_session_id: Option<SessionId>,
|
|
keys: [Option<SessionKey>; 2], // current, next
|
|
offer: Option<EphemeralOffer>,
|
|
}
|
|
|
|
impl<O> Session<O> {
|
|
/// Check whether this session should initiate a re-key, returning a packet to send if true.
|
|
///
|
|
/// This must be checked often enough to ensure that the hard key usage limit is not reached, which in the
|
|
/// usual UDP use case means once every ~3TiB of traffic.
|
|
pub fn rekey_check<'a, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(&self, buffer: &'a mut [u8; MAX_PACKET_SIZE], local_s_public: &[u8; STATIC_PUBLIC_SIZE], current_time: i64, force: bool, jedi: bool) -> Option<&'a [u8]> {
|
|
let state = self.state.upgradable_read();
|
|
if let Some(key) = state.keys[0].as_ref() {
|
|
if force || (key.lifetime.should_rekey(self.send_counter.current(), current_time) && state.offer.as_ref().map_or(true, |o| (current_time - o.creation_time) > OFFER_RATE_LIMIT_MS)) {
|
|
if let Some(remote_s_public_p384) = P384PublicKey::from_bytes(&self.remote_s_public_p384) {
|
|
if let Some((offer, psize)) = EphemeralOffer::create_alice_offer(
|
|
buffer,
|
|
self.send_counter.next(),
|
|
self.id,
|
|
state.remote_session_id,
|
|
local_s_public,
|
|
&remote_s_public_p384,
|
|
&self.ss,
|
|
&self.outgoing_obfuscator,
|
|
current_time,
|
|
jedi,
|
|
) {
|
|
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
|
let _ = state.offer.replace(offer);
|
|
return Some(&buffer[..psize]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
/// Send a data packet to the other side, returning packet to send.
|
|
pub fn send<'a, const MAX_PACKET_SIZE: usize>(&self, buffer: &'a mut [u8; MAX_PACKET_SIZE], data: &[u8]) -> Result<&'a [u8], Error> {
|
|
let state = self.state.read();
|
|
if let Some(key) = state.keys[0].as_ref() {
|
|
if let Some(remote_session_id) = state.remote_session_id {
|
|
let data_len = assemble_and_armor_DATA(buffer, data, PACKET_TYPE_DATA, u64::from(remote_session_id), self.send_counter.next(), &key, &self.outgoing_obfuscator)?;
|
|
Ok(&buffer[..data_len])
|
|
} else {
|
|
unlikely_branch();
|
|
Err(Error::SessionNotEstablished)
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
Err(Error::SessionNotEstablished)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new session and return this plus an outgoing packet to send to the other end.
|
|
pub fn new_session<'a, O, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
|
|
buffer: &'a mut [u8; MAX_PACKET_SIZE],
|
|
local_session_id: SessionId,
|
|
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,
|
|
current_time: i64,
|
|
jedi: bool,
|
|
) -> Result<(Session<O>, &'a [u8]), Error> {
|
|
debug_assert!(MAX_PACKET_SIZE >= MIN_BUFFER_SIZE);
|
|
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, None, local_s_public, remote_s_public_p384, &ss, &outgoing_obfuscator, current_time, jedi) {
|
|
return Ok((
|
|
Session::<O> {
|
|
id: local_session_id,
|
|
send_counter: counter,
|
|
remote_s_public_hash: SHA384::hash(remote_s_public),
|
|
psk: psk.clone(),
|
|
ss,
|
|
outgoing_obfuscator,
|
|
state: RwLock::new(State {
|
|
remote_session_id: None,
|
|
keys: [None, None],
|
|
offer: Some(offer),
|
|
}),
|
|
remote_s_public_p384: remote_s_public_p384.as_bytes().clone(),
|
|
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.
|
|
pub fn receive<
|
|
'a,
|
|
ExtractP384PublicKeyFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<P384PublicKey>,
|
|
SessionLookupFunction: FnOnce(SessionId) -> Option<S>,
|
|
NewSessionAuthenticatorFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<(SessionId, 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,
|
|
extract_p384_static_public: ExtractP384PublicKeyFunction,
|
|
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 = SessionId::new_from_bytes(&buffer[4..10]);
|
|
let packet_type = buffer[10];
|
|
|
|
debug_assert_eq!(PACKET_TYPE_DATA, 0);
|
|
debug_assert_eq!(PACKET_TYPE_NOP, 1);
|
|
if packet_type <= PACKET_TYPE_NOP {
|
|
if let Some(local_session_id) = local_session_id {
|
|
if let Some(session) = session_lookup(local_session_id) {
|
|
let state = session.state.read();
|
|
for ki in 0..2 {
|
|
if let Some(key) = state.keys[ki].as_ref() {
|
|
let nonce = get_aes_gcm_nonce(buffer);
|
|
let mut c = key.get_receive_cipher();
|
|
c.init(&nonce);
|
|
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(state);
|
|
let mut state = session.state.write();
|
|
state.keys[0] = state.keys[1].take();
|
|
}
|
|
|
|
if packet_type == PACKET_TYPE_DATA {
|
|
return Ok(ReceiveResult::OkData(&buffer[HEADER_SIZE..data_len], u32::from_le_bytes(nonce[..4].try_into().unwrap())));
|
|
} else {
|
|
unlikely_branch();
|
|
return Ok(ReceiveResult::Ok);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Err(Error::FailedAuthentication);
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::UnknownLocalSessionId(Some(local_session_id)));
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
return Err(Error::UnknownLocalSessionId(None));
|
|
}
|
|
} else {
|
|
unlikely_branch();
|
|
|
|
let session = if let Some(local_session_id) = local_session_id {
|
|
let s = session_lookup(local_session_id);
|
|
if s.is_none() {
|
|
return Err(Error::UnknownLocalSessionId(Some(local_session_id)));
|
|
}
|
|
s
|
|
} else {
|
|
None
|
|
};
|
|
|
|
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);
|
|
}
|
|
let payload_end = incoming_packet.len() - (AES_GCM_TAG_SIZE + HMAC_SIZE);
|
|
let aes_gcm_tag_end = incoming_packet.len() - HMAC_SIZE;
|
|
|
|
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_DERIVATION_CHAIN_STARTING_SALT, alice_e0_public.as_bytes()), e0s.as_bytes()));
|
|
|
|
let original_ciphertext = buffer.clone();
|
|
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<32>(), false);
|
|
c.init(&get_aes_gcm_nonce(buffer));
|
|
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end]);
|
|
let c = c.finish();
|
|
if !c.eq(&buffer[payload_end..aes_gcm_tag_end]) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
let (alice_session_id, alice_s_public, alice_e1_public) = parse_KEY_OFFER_after_header(&buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end])?;
|
|
|
|
if let Some(session) = session.as_ref() {
|
|
// If we already have a session for this session ID, make sure this is the same node calling.
|
|
if !session.remote_s_public_hash.eq(&SHA384::hash(&alice_s_public)) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
}
|
|
|
|
let alice_s_public_p384 = extract_p384_static_public(&alice_s_public).ok_or(Error::InvalidPacket)?;
|
|
let ss = local_s_keypair_p384.agree(&alice_s_public_p384).ok_or(Error::FailedAuthentication)?;
|
|
|
|
let key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes()));
|
|
|
|
if !hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &original_ciphertext[..aes_gcm_tag_end]).eq(&buffer[aes_gcm_tag_end..incoming_packet.len()]) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Alice's offer has been verified and her current key state reconstructed.
|
|
|
|
let bob_e0_keypair = P384KeyPair::generate();
|
|
let e0e0 = bob_e0_keypair.agree(&alice_e0_public).ok_or(Error::FailedAuthentication)?;
|
|
let se0 = bob_e0_keypair.agree(&alice_s_public_p384).ok_or(Error::FailedAuthentication)?;
|
|
|
|
let new_session = if session.is_some() {
|
|
None
|
|
} else {
|
|
if let Some((local_session_id, psk, associated_object)) = new_session_auth(&alice_s_public) {
|
|
Some(Session::<O> {
|
|
id: local_session_id,
|
|
send_counter: Counter::new(),
|
|
remote_s_public_hash: SHA384::hash(&alice_s_public),
|
|
psk,
|
|
ss,
|
|
outgoing_obfuscator: Obfuscator::new(&alice_s_public),
|
|
state: RwLock::new(State {
|
|
remote_session_id: Some(alice_session_id),
|
|
keys: [None, None],
|
|
offer: None,
|
|
}),
|
|
remote_s_public_p384: alice_s_public_p384.as_bytes().clone(),
|
|
associated_object,
|
|
})
|
|
} else {
|
|
return Err(Error::NewSessionRejected);
|
|
}
|
|
};
|
|
let session_ref = session;
|
|
let session = session_ref.as_ref().map_or_else(|| new_session.as_ref().unwrap(), |s| &*s);
|
|
|
|
// 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 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 Noise_IK key derivation with NIST P-384 ECDH, but see final step below...
|
|
|
|
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
|
|
};
|
|
|
|
let counter = session.send_counter.next();
|
|
let mut reply_size = assemble_KEY_COUNTER_OFFER(buffer, counter, alice_session_id, bob_e0_keypair.public_key(), 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)..reply_size]);
|
|
let c = c.finish();
|
|
buffer[reply_size..(reply_size + AES_GCM_TAG_SIZE)].copy_from_slice(&c);
|
|
reply_size += AES_GCM_TAG_SIZE;
|
|
|
|
// Normal Noise_IK is done, but we have one more step: mix in the Kyber shared secret (or all zeroes if Kyber is
|
|
// disabled). We have to wait until this point because Kyber's keys are encrypted and can't be decrypted until
|
|
// the P-384 exchange is done. We also flip the HMAC parameter order here for the same reason we do in the previous
|
|
// key derivation step.
|
|
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[..reply_size]);
|
|
buffer[reply_size..reply_size + HMAC_SIZE].copy_from_slice(&hmac);
|
|
reply_size += HMAC_SIZE;
|
|
|
|
let mut state = session.state.write();
|
|
let _ = state.remote_session_id.replace(alice_session_id);
|
|
state.keys[1].replace(SessionKey::new(key, Role::Bob, current_time, counter, jedi));
|
|
drop(state);
|
|
|
|
// 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[..reply_size])), |ns| Ok(ReceiveResult::OkNewSession(ns, &buffer[..reply_size])));
|
|
}
|
|
|
|
PACKET_TYPE_KEY_COUNTER_OFFER => {
|
|
// bob (remote) -> alice (local)
|
|
|
|
if let Some(session) = session {
|
|
let state = session.state.upgradable_read();
|
|
if let Some(offer) = state.offer.as_ref() {
|
|
let (bob_e0_public, e0e0) = P384PublicKey::from_bytes(&buffer[HEADER_SIZE..(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)])
|
|
.and_then(|pk| offer.alice_e0_keypair.agree(&pk).map(move |s| (pk, s)))
|
|
.ok_or(Error::FailedAuthentication)?;
|
|
let se0 = local_s_keypair_p384.agree(&bob_e0_public).ok_or(Error::FailedAuthentication)?;
|
|
|
|
let key = Secret(hmac_sha512(
|
|
session.psk.as_bytes(),
|
|
&hmac_sha512(&hmac_sha512(&hmac_sha512(offer.key.as_bytes(), bob_e0_public.as_bytes()), e0e0.as_bytes()), se0.as_bytes()),
|
|
));
|
|
|
|
let original_ciphertext = buffer.clone();
|
|
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)..payload_end]);
|
|
let c = c.finish();
|
|
if !c.eq(&buffer[payload_end..aes_gcm_tag_end]) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Alice has now completed Noise_IK for P-384 and verified with GCM auth, now for the hybrid add-on.
|
|
|
|
let (bob_session_id, bob_e1_public) = parse_KEY_COUNTER_OFFER_after_header(&buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end])?;
|
|
|
|
let e1e1 = if jedi && bob_e1_public.is_some() && offer.alice_e1_keypair.is_some() {
|
|
if let Ok(e1e1) = pqc_kyber::decapsulate(bob_e1_public.as_ref().unwrap(), &offer.alice_e1_keypair.as_ref().unwrap().secret) {
|
|
Secret(e1e1)
|
|
} else {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
} else {
|
|
Secret::default()
|
|
};
|
|
|
|
let key = Secret(hmac_sha512(e1e1.as_bytes(), key.as_bytes()));
|
|
|
|
if !hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &original_ciphertext[..aes_gcm_tag_end]).eq(&buffer[aes_gcm_tag_end..incoming_packet.len()]) {
|
|
return Err(Error::FailedAuthentication);
|
|
}
|
|
|
|
// Alice has now completed and validated the full hybrid exchange. If this is the first exchange send
|
|
// a NOP back to Bob to acknowledge that the session is open and can now be used. Otherwise just queue
|
|
// this up as the next key to be promoted to current when Bob uses it.
|
|
|
|
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
|
let _ = state.offer.take();
|
|
let _ = state.remote_session_id.replace(bob_session_id);
|
|
if state.keys[0].is_some() {
|
|
let _ = state.keys[1].replace(SessionKey::new(key, Role::Alice, current_time, session.send_counter.current(), jedi));
|
|
return Ok(ReceiveResult::Ok);
|
|
} else {
|
|
let counter = session.send_counter.next();
|
|
let key = SessionKey::new(key, Role::Alice, current_time, counter, jedi);
|
|
|
|
let dummy_data_len = (random::next_u32_secure() % (MAX_PACKET_SIZE - (HEADER_SIZE + AES_GCM_TAG_SIZE)) as u32) as usize;
|
|
let mut dummy_data = [0_u8; MAX_PACKET_SIZE];
|
|
random::fill_bytes_secure(&mut dummy_data[..dummy_data_len]);
|
|
let nop_len = assemble_and_armor_DATA(buffer, &dummy_data[..dummy_data_len], PACKET_TYPE_NOP, u64::from(bob_session_id), counter, &key, &session.outgoing_obfuscator)?;
|
|
|
|
let _ = state.keys[0].replace(key);
|
|
let _ = state.keys[1].take();
|
|
|
|
return Ok(ReceiveResult::OkSendReply(&buffer[..nop_len]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(transparent)]
|
|
struct Counter(AtomicU64);
|
|
|
|
impl Counter {
|
|
fn new() -> Self {
|
|
Self(AtomicU64::new(0))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn current(&self) -> CounterValue {
|
|
CounterValue(self.0.load(Ordering::SeqCst))
|
|
}
|
|
|
|
#[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_USES_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 should_rekey(&self, counter: CounterValue, current_time: i64) -> bool {
|
|
counter.0 >= self.rekey_at_or_after_counter || current_time >= self.rekey_at_or_after_timestamp
|
|
}
|
|
|
|
#[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.
|
|
struct EphemeralOffer {
|
|
creation_time: i64,
|
|
key: Secret<64>,
|
|
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: SessionId,
|
|
bob_session_id: Option<SessionId>,
|
|
alice_s_public: &[u8; STATIC_PUBLIC_SIZE],
|
|
bob_s_public_p384: &P384PublicKey,
|
|
ss: &Secret<48>,
|
|
outgoing_obfuscator: &Obfuscator, // bobfuscator?
|
|
current_time: i64,
|
|
jedi: bool,
|
|
) -> Option<(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_DERIVATION_CHAIN_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));
|
|
|
|
debug_assert!(packet_size <= MAX_PACKET_SIZE);
|
|
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]);
|
|
let c = c.finish();
|
|
buffer[packet_size..packet_size + AES_GCM_TAG_SIZE].copy_from_slice(&c);
|
|
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((
|
|
EphemeralOffer {
|
|
creation_time: current_time,
|
|
key,
|
|
alice_e0_keypair,
|
|
alice_e1_keypair,
|
|
},
|
|
packet_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_and_armor_DATA<const MAX_PACKET_SIZE: usize>(buffer: &mut [u8; MAX_PACKET_SIZE], data: &[u8], packet_type: u8, remote_session_id: u64, counter: CounterValue, key: &SessionKey, outgoing_obfuscator: &Obfuscator) -> Result<usize, Error> {
|
|
buffer[0..4].copy_from_slice(&counter.to_bytes());
|
|
buffer[4..10].copy_from_slice(&remote_session_id.to_le_bytes()[..SESSION_ID_SIZE]);
|
|
debug_assert!(packet_type == PACKET_TYPE_DATA || packet_type == PACKET_TYPE_NOP);
|
|
buffer[10] = packet_type;
|
|
|
|
let payload_end = HEADER_SIZE + data.len();
|
|
let tag_end = payload_end + AES_GCM_TAG_SIZE;
|
|
if tag_end < MAX_PACKET_SIZE {
|
|
let mut c = key.get_send_cipher(counter)?;
|
|
buffer[11..16].fill(0);
|
|
c.init(&buffer[..16]);
|
|
c.crypt(data, &mut buffer[HEADER_SIZE..payload_end]);
|
|
buffer[payload_end..tag_end].copy_from_slice(&c.finish());
|
|
key.return_send_cipher(c);
|
|
|
|
outgoing_obfuscator.0.encrypt_block_in_place(&mut buffer[..16]);
|
|
|
|
Ok(tag_end)
|
|
} else {
|
|
unlikely_branch();
|
|
Err(Error::InvalidParameter)
|
|
}
|
|
}
|
|
|
|
fn append_random_padding(b: &mut [u8]) -> &mut [u8] {
|
|
if b.len() > AES_GCM_TAG_SIZE + HMAC_SIZE {
|
|
let random_padding_len = (random::next_u32_secure() as usize) % (b.len() - (AES_GCM_TAG_SIZE + HMAC_SIZE));
|
|
b[..random_padding_len].fill(0);
|
|
&mut b[random_padding_len..]
|
|
} else {
|
|
b
|
|
}
|
|
}
|
|
|
|
#[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: Option<SessionId>,
|
|
alice_e0_public: &P384PublicKey,
|
|
alice_session_id: SessionId,
|
|
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.map_or(0_u64, |i| i.into()).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..];
|
|
|
|
alice_session_id.copy_to(b);
|
|
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;
|
|
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..];
|
|
|
|
b = append_random_padding(b);
|
|
|
|
MAX_PACKET_SIZE - b.len()
|
|
}
|
|
|
|
#[allow(non_snake_case)]
|
|
fn parse_KEY_OFFER_after_header<const STATIC_PUBLIC_SIZE: usize>(mut b: &[u8]) -> Result<(SessionId, [u8; STATIC_PUBLIC_SIZE], Option<[u8; pqc_kyber::KYBER_PUBLICKEYBYTES]>), Error> {
|
|
if b.len() >= SESSION_ID_SIZE {
|
|
let alice_session_id = SessionId::new_from_bytes(b).ok_or(Error::InvalidPacket)?;
|
|
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 {
|
|
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: SessionId,
|
|
bob_e0_public: &P384PublicKey,
|
|
bob_session_id: SessionId,
|
|
bob_e1_public: Option<&[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>,
|
|
) -> usize {
|
|
buffer[0..4].copy_from_slice(&counter.to_bytes());
|
|
alice_session_id.copy_to(&mut buffer[4..10]);
|
|
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..];
|
|
|
|
bob_session_id.copy_to(b);
|
|
b = &mut b[SESSION_ID_SIZE..];
|
|
|
|
if let Some(k) = bob_e1_public {
|
|
b[0] = E1_TYPE_KYBER512;
|
|
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..];
|
|
|
|
b = append_random_padding(b);
|
|
|
|
MAX_PACKET_SIZE - b.len()
|
|
}
|
|
|
|
#[allow(non_snake_case)]
|
|
fn parse_KEY_COUNTER_OFFER_after_header(mut b: &[u8]) -> Result<(SessionId, Option<[u8; pqc_kyber::KYBER_CIPHERTEXTBYTES]>), Error> {
|
|
if b.len() >= SESSION_ID_SIZE {
|
|
let bob_session_id = SessionId::new_from_bytes(b).ok_or(Error::InvalidPacket)?;
|
|
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 {
|
|
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[..HEADER_SIZE]);
|
|
tmp
|
|
}
|
|
|
|
#[cold]
|
|
#[inline(never)]
|
|
extern "C" fn unlikely_branch() {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::rc::Rc;
|
|
|
|
#[allow(unused_imports)]
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn alice_bob() {
|
|
let psk: Secret<64> = Secret::default();
|
|
let mut a_buffer = [0_u8; 1500];
|
|
let mut b_buffer = [0_u8; 1500];
|
|
let alice_static_keypair = P384KeyPair::generate();
|
|
let bob_static_keypair = P384KeyPair::generate();
|
|
let outgoing_obfuscator_to_alice = Obfuscator::new(alice_static_keypair.public_key_bytes());
|
|
let outgoing_obfuscator_to_bob = Obfuscator::new(bob_static_keypair.public_key_bytes());
|
|
|
|
let mut from_alice: Vec<Vec<u8>> = Vec::new();
|
|
let mut from_bob: Vec<Vec<u8>> = Vec::new();
|
|
|
|
// Session TO Bob, on Alice's side.
|
|
let (alice, packet) = new_session(
|
|
&mut a_buffer,
|
|
SessionId::new_random(),
|
|
alice_static_keypair.public_key_bytes(),
|
|
&alice_static_keypair,
|
|
bob_static_keypair.public_key_bytes(),
|
|
bob_static_keypair.public_key(),
|
|
&psk,
|
|
0,
|
|
0,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
let alice = Rc::new(alice);
|
|
from_alice.push(packet.to_vec());
|
|
|
|
// Session FROM Alice, on Bob's side.
|
|
let mut bob: Option<Rc<Session<u32>>> = None;
|
|
|
|
for _ in 0..256 {
|
|
while !from_alice.is_empty() || !from_bob.is_empty() {
|
|
if let Some(packet) = from_alice.pop() {
|
|
let r = receive(
|
|
packet.as_slice(),
|
|
&mut b_buffer,
|
|
&bob_static_keypair,
|
|
&outgoing_obfuscator_to_bob,
|
|
|p: &[u8; P384_PUBLIC_KEY_SIZE]| P384PublicKey::from_bytes(p),
|
|
|sid| {
|
|
println!("[noise] [bob] session ID: {}", u64::from(sid));
|
|
if let Some(bob) = bob.as_ref() {
|
|
if sid == bob.id {
|
|
Some(bob.clone())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
|_: &[u8; P384_PUBLIC_KEY_SIZE]| {
|
|
if bob.is_none() {
|
|
Some((SessionId::new_random(), psk.clone(), 0))
|
|
} else {
|
|
panic!("[noise] [bob] Bob received a second new session request from Alice");
|
|
}
|
|
},
|
|
0,
|
|
true,
|
|
);
|
|
if let Ok(r) = r {
|
|
match r {
|
|
ReceiveResult::OkData(data, counter) => {
|
|
println!("[noise] [bob] DATA len=={} counter=={}", data.len(), counter);
|
|
}
|
|
ReceiveResult::OkSendReply(p) => {
|
|
println!("[noise] [bob] OK (reply: {} bytes)", p.len());
|
|
from_bob.push(p.to_vec());
|
|
}
|
|
ReceiveResult::OkNewSession(ns, p) => {
|
|
if bob.is_some() {
|
|
panic!("[noise] [bob] attempt to create new session on Bob's side when he already has one");
|
|
}
|
|
let id: u64 = ns.id.into();
|
|
let _ = bob.replace(Rc::new(ns));
|
|
from_bob.push(p.to_vec());
|
|
println!("[noise] [bob] NEW SESSION {}", id);
|
|
}
|
|
ReceiveResult::Ok => {
|
|
println!("[noise] [bob] OK");
|
|
}
|
|
ReceiveResult::Duplicate => {
|
|
println!("[noise] [bob] duplicate packet");
|
|
}
|
|
ReceiveResult::Ignored => {
|
|
println!("[noise] [bob] ignored packet");
|
|
}
|
|
}
|
|
} else {
|
|
println!("ERROR (bob): {}", r.err().unwrap().to_string());
|
|
panic!();
|
|
}
|
|
}
|
|
|
|
if let Some(packet) = from_bob.pop() {
|
|
let r = receive(
|
|
packet.as_slice(),
|
|
&mut b_buffer,
|
|
&alice_static_keypair,
|
|
&outgoing_obfuscator_to_alice,
|
|
|p: &[u8; P384_PUBLIC_KEY_SIZE]| P384PublicKey::from_bytes(p),
|
|
|sid| {
|
|
println!("[noise] [alice] session ID: {}", u64::from(sid));
|
|
if sid == alice.id {
|
|
Some(alice.clone())
|
|
} else {
|
|
panic!("[noise] [alice] received from Bob addressed to unknown session ID, not Alice");
|
|
}
|
|
},
|
|
|_: &[u8; P384_PUBLIC_KEY_SIZE]| {
|
|
panic!("[noise] [alice] Alice received an unexpected new session request from Bob");
|
|
},
|
|
0,
|
|
true,
|
|
);
|
|
if let Ok(r) = r {
|
|
match r {
|
|
ReceiveResult::OkData(data, counter) => {
|
|
println!("[noise] [alice] DATA len=={} counter=={}", data.len(), counter);
|
|
}
|
|
ReceiveResult::OkSendReply(p) => {
|
|
println!("[noise] [alice] OK (reply: {} bytes)", p.len());
|
|
from_alice.push(p.to_vec());
|
|
}
|
|
ReceiveResult::OkNewSession(_, _) => {
|
|
panic!("[noise] [alice] attempt to create new session on Alice's side; Bob should not initiate");
|
|
}
|
|
ReceiveResult::Ok => {
|
|
println!("[noise] [alice] OK");
|
|
}
|
|
ReceiveResult::Duplicate => {
|
|
println!("[noise] [alice] duplicate packet");
|
|
}
|
|
ReceiveResult::Ignored => {
|
|
println!("[noise] [alice] ignored packet");
|
|
}
|
|
}
|
|
} else {
|
|
println!("ERROR (alice): {}", r.err().unwrap().to_string());
|
|
panic!();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (random::next_u32_secure() & 1) == 0 {
|
|
from_alice.push(alice.send(&mut a_buffer, &[0_u8; 16]).unwrap().to_vec());
|
|
} else if bob.is_some() {
|
|
from_bob.push(bob.as_ref().unwrap().send(&mut b_buffer, &[0_u8; 16]).unwrap().to_vec());
|
|
}
|
|
}
|
|
}
|
|
}
|