It opens a session.

This commit is contained in:
Adam Ierymenko 2023-02-28 17:03:00 -05:00
commit a2e0854d96
3 changed files with 402 additions and 222 deletions

View file

@ -1,3 +1,184 @@
use zssp::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {}
use zerotier_crypto::p384::{P384KeyPair, P384PublicKey};
use zerotier_crypto::secret::Secret;
use zerotier_utils::ms_monotonic;
const TEST_MTU: usize = 1500;
struct TestApplication {
identity_key: P384KeyPair,
}
impl zssp::ApplicationLayer for TestApplication {
type Data = ();
type IncomingPacketBuffer = Vec<u8>;
fn get_local_s_public_blob(&self) -> &[u8] {
self.identity_key.public_key_bytes()
}
fn get_local_s_keypair(&self) -> &zerotier_crypto::p384::P384KeyPair {
&self.identity_key
}
}
fn alice_main(
run: &AtomicBool,
alice_app: &TestApplication,
bob_app: &TestApplication,
alice_out: mpsc::SyncSender<Vec<u8>>,
alice_in: mpsc::Receiver<Vec<u8>>,
) {
let context = zssp::Context::<TestApplication>::new(16);
let mut data_buf = [0u8; 65536];
let mut next_service = ms_monotonic() + 500;
let alice_session = context
.open(
alice_app,
|b| {
let _ = alice_out.send(b.to_vec());
},
TEST_MTU,
bob_app.identity_key.public_key(),
Secret::default(),
None,
(),
ms_monotonic(),
)
.unwrap();
println!("[alice] opening session {}", alice_session.id.to_string());
while run.load(Ordering::Relaxed) {
let pkt = alice_in.try_recv();
let current_time = ms_monotonic();
if let Ok(pkt) = pkt {
//println!("bob >> alice {}", pkt.len());
match context.receive(
alice_app,
|| true,
|s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())),
|_, b| {
let _ = alice_out.send(b.to_vec());
},
&mut data_buf,
pkt,
TEST_MTU,
current_time,
) {
Ok(zssp::ReceiveResult::Ok) => {
println!("[alice] ok");
}
Ok(zssp::ReceiveResult::OkData(_, data)) => {
println!("[alice] received {}", data.len());
}
Ok(zssp::ReceiveResult::OkNewSession(s)) => {
println!("[alice] new session {}", s.id.to_string());
}
Ok(zssp::ReceiveResult::Rejected) => {}
Err(e) => {
println!("[alice] ERROR {}", e.to_string());
}
}
}
if current_time >= next_service {
next_service = current_time
+ context.service(
|_, b| {
let _ = alice_out.send(b.to_vec());
},
TEST_MTU,
current_time,
);
}
}
}
fn bob_main(
run: &AtomicBool,
_alice_app: &TestApplication,
bob_app: &TestApplication,
bob_out: mpsc::SyncSender<Vec<u8>>,
bob_in: mpsc::Receiver<Vec<u8>>,
) {
let context = zssp::Context::<TestApplication>::new(16);
let mut data_buf = [0u8; 65536];
let mut next_service = ms_monotonic() + 500;
while run.load(Ordering::Relaxed) {
let pkt = bob_in.recv_timeout(Duration::from_millis(10));
let current_time = ms_monotonic();
if let Ok(pkt) = pkt {
//println!("alice >> bob {}", pkt.len());
match context.receive(
bob_app,
|| true,
|s_public, _| Some((P384PublicKey::from_bytes(s_public).unwrap(), Secret::default(), ())),
|_, b| {
let _ = bob_out.send(b.to_vec());
},
&mut data_buf,
pkt,
TEST_MTU,
current_time,
) {
Ok(zssp::ReceiveResult::Ok) => {
println!("[bob] ok");
}
Ok(zssp::ReceiveResult::OkData(_, data)) => {
println!("[bob] received {}", data.len());
}
Ok(zssp::ReceiveResult::OkNewSession(s)) => {
println!("[bob] new session {}", s.id.to_string());
}
Ok(zssp::ReceiveResult::Rejected) => {}
Err(e) => {
println!("[bob] ERROR {}", e.to_string());
}
}
}
if current_time >= next_service {
next_service = current_time
+ context.service(
|_, b| {
let _ = bob_out.send(b.to_vec());
},
TEST_MTU,
current_time,
);
}
}
}
fn main() {
let run = AtomicBool::new(true);
let alice_app = TestApplication { identity_key: P384KeyPair::generate() };
let bob_app = TestApplication { identity_key: P384KeyPair::generate() };
let (alice_out, bob_in) = mpsc::sync_channel::<Vec<u8>>(128);
let (bob_out, alice_in) = mpsc::sync_channel::<Vec<u8>>(128);
thread::scope(|ts| {
let alice_thread = ts.spawn(|| alice_main(&run, &alice_app, &bob_app, alice_out, alice_in));
let bob_thread = ts.spawn(|| bob_main(&run, &alice_app, &bob_app, bob_out, bob_in));
thread::sleep(Duration::from_secs(60 * 10));
run.store(false, Ordering::SeqCst);
let _ = alice_thread.join();
let _ = bob_thread.join();
});
std::process::exit(0);
}

View file

@ -52,11 +52,10 @@ pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS
pub(crate) const BASE_KEY_SIZE: usize = 64;
pub(crate) const AES_KEY_SIZE: usize = 32;
pub(crate) const AES_256_KEY_SIZE: usize = 32;
pub(crate) const AES_HEADER_PROTECTION_KEY_SIZE: usize = 16;
pub(crate) const AES_GCM_TAG_SIZE: usize = 16;
pub(crate) const AES_GCM_NONCE_SIZE: usize = 12;
pub(crate) const AES_CTR_NONCE_SIZE: usize = 12;
/// The first packet in Noise_XK exchange containing Alice's ephemeral keys, session ID, and a random
/// symmetric key to protect header fragmentation fields for this session.
@ -66,7 +65,7 @@ pub(crate) struct AliceNoiseXKInit {
pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8,
pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE],
// -- start AES-CTR(es) encrypted section (IV is last 12 bytes of alice_noise_e))
// -- start AES-CTR(es) encrypted section
pub alice_session_id: [u8; SessionId::SIZE],
pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES],
pub header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE],
@ -87,7 +86,7 @@ pub(crate) struct BobNoiseXKAck {
pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8,
pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE],
// -- start AES-CTR(es_ee) encrypted section (IV is last 12 bytes of bob_noise_e)
// -- start AES-CTR(es_ee) encrypted section
pub bob_session_id: [u8; SessionId::SIZE],
pub bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES],
// -- end encrypted sectiion
@ -107,7 +106,7 @@ impl BobNoiseXKAck {
pub(crate) struct AliceNoiseXKAck {
pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8,
// -- start AES-CTR(es_ee) encrypted section (IV is first 12 bytes of hk)
// -- start AES-CTR(es_ee_hk) encrypted section
pub alice_static_blob_length: [u8; 2],
pub alice_static_blob: [u8; ???],
pub alice_metadata_length: [u8; 2],

View file

@ -38,8 +38,7 @@ use crate::sessionid::SessionId;
/// defragment incoming packets that are not yet associated with a session.
pub struct Context<Application: ApplicationLayer> {
max_incomplete_session_queue_size: usize,
initial_offer_defrag:
Mutex<RingBufferMap<u64, GatherArray<Application::IncomingPacketBuffer, MAX_NOISE_HANDSHAKE_FRAGMENTS>, 256, 256>>,
defrag: Mutex<RingBufferMap<u64, GatherArray<Application::IncomingPacketBuffer, MAX_NOISE_HANDSHAKE_FRAGMENTS>, 256, 256>>,
sessions: RwLock<SessionsById<Application>>,
}
@ -49,7 +48,7 @@ struct SessionsById<Application: ApplicationLayer> {
active: HashMap<SessionId, Weak<Session<Application>>>,
// Incomplete sessions in the middle of three-phase Noise_XK negotiation, expired after timeout.
incomplete: HashMap<SessionId, Arc<IncompleteIncomingSession>>,
incoming: HashMap<SessionId, Arc<IncomingIncompleteSession>>,
}
/// Result generated by the context packet receive function, with possible payloads.
@ -94,7 +93,7 @@ struct State {
}
/// State related to an incoming session not yet fully established.
struct IncompleteIncomingSession {
struct IncomingIncompleteSession {
timestamp: i64,
request_hash: [u8; SHA384_HASH_SIZE],
alice_session_id: SessionId,
@ -126,8 +125,8 @@ enum Offer {
/// An ephemeral session key with expiration info.
struct SessionKey {
ratchet_key: Secret<BASE_KEY_SIZE>, // Key used in derivation of 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_key: Secret<AES_256_KEY_SIZE>, // Receive side AES-GCM key
send_key: Secret<AES_256_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
rekey_at_time: i64, // Rekey at or after this time (ticks)
@ -142,10 +141,10 @@ impl<Application: ApplicationLayer> Context<Application> {
pub fn new(max_incomplete_session_queue_size: usize) -> Self {
Self {
max_incomplete_session_queue_size,
initial_offer_defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())),
defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())),
sessions: RwLock::new(SessionsById {
active: HashMap::with_capacity(64),
incomplete: HashMap::with_capacity(64),
incoming: HashMap::with_capacity(64),
}),
}
}
@ -212,8 +211,8 @@ impl<Application: ApplicationLayer> Context<Application> {
}
}
for (id, incomplete) in sessions.incomplete.iter() {
if incomplete.timestamp < negotiation_timeout_cutoff {
for (id, incoming) in sessions.incoming.iter() {
if incoming.timestamp < negotiation_timeout_cutoff {
dead_pending.push(*id);
}
}
@ -225,7 +224,7 @@ impl<Application: ApplicationLayer> Context<Application> {
sessions.active.remove(id);
}
for id in dead_pending.iter() {
sessions.incomplete.remove(id);
sessions.incoming.remove(id);
}
}
@ -272,7 +271,7 @@ impl<Application: ApplicationLayer> Context<Application> {
let mut local_session_id;
loop {
local_session_id = SessionId::random();
if !sessions.active.contains_key(&local_session_id) && !sessions.incomplete.contains_key(&local_session_id) {
if !sessions.active.contains_key(&local_session_id) && !sessions.incoming.contains_key(&local_session_id) {
break;
}
}
@ -320,9 +319,10 @@ impl<Application: ApplicationLayer> Context<Application> {
init.alice_hk_public = alice_hk_secret.public;
init.header_protection_key = header_protection_key.0;
let mut ctr = AesCtr::new(kbkdf::<AES_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es.as_bytes()).as_bytes());
ctr.reset_set_iv(&alice_noise_e[P384_PUBLIC_KEY_SIZE - AES_CTR_NONCE_SIZE..]);
ctr.crypt_in_place(&mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]);
aes_ctr_crypt_one_time_use_key(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es.as_bytes()).as_bytes(),
&mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START],
);
let hmac = hmac_sha384_2(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION>(noise_es.as_bytes()).as_bytes(),
@ -394,7 +394,7 @@ impl<Application: ApplicationLayer> Context<Application> {
return Err(Error::InvalidPacket);
}
let mut incomplete = None;
let mut incoming = None;
if let Some(local_session_id) = SessionId::new_from_u64_le(memory::load_raw(incoming_packet)) {
if let Some(session) = self
.sessions
@ -404,27 +404,27 @@ impl<Application: ApplicationLayer> Context<Application> {
.get(&local_session_id)
.and_then(|s| s.upgrade())
{
debug_assert!(self.sessions.read().unwrap().incomplete.contains_key(&local_session_id));
debug_assert!(!self.sessions.read().unwrap().incoming.contains_key(&local_session_id));
session
.header_protection_cipher
.decrypt_block_in_place(&mut incoming_packet[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]);
let (key_index, packet_type, fragment_count, fragment_no, counter) = parse_packet_header(&incoming_packet);
let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_packet);
if session.check_receive_window(counter) {
if session.check_receive_window(incoming_counter) {
if fragment_count > 1 {
if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count {
let mut defrag = session.defrag.lock().unwrap();
let fragment_gather_array = defrag.get_or_create_mut(&counter, || GatherArray::new(fragment_count));
let fragment_gather_array = defrag.get_or_create_mut(&incoming_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(
return self.process_complete_incoming_packet(
app,
&mut send,
&mut check_allow_incoming_session,
&mut check_accept_session,
data_buf,
counter,
incoming_counter,
assembled_packet.as_ref(),
packet_type,
Some(session),
@ -440,13 +440,13 @@ impl<Application: ApplicationLayer> Context<Application> {
return Err(Error::InvalidPacket);
}
} else {
return self.receive_complete(
return self.process_complete_incoming_packet(
app,
&mut send,
&mut check_allow_incoming_session,
&mut check_accept_session,
data_buf,
counter,
incoming_counter,
&[incoming_packet_buf],
packet_type,
Some(session),
@ -460,53 +460,54 @@ impl<Application: ApplicationLayer> Context<Application> {
return Err(Error::OutOfSequence);
}
} else {
if let Some(p) = self.sessions.read().unwrap().incomplete.get(&local_session_id).cloned() {
Aes::new(p.header_protection_key.as_bytes())
if let Some(i) = self.sessions.read().unwrap().incoming.get(&local_session_id).cloned() {
Aes::new(i.header_protection_key.as_bytes())
.decrypt_block_in_place(&mut incoming_packet[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]);
incomplete = Some(p);
incoming = Some(i);
} else {
println!("unknown {}", local_session_id.to_string());
return Err(Error::UnknownLocalSessionId);
}
}
}
// If we make it here the packet is not associated with a session or is associated with an
// incomplete session (Noise_XK mid-negotiation).
// incoming session (Noise_XK mid-negotiation).
let (key_index, packet_type, fragment_count, fragment_no, counter) = parse_packet_header(&incoming_packet);
let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_packet);
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));
let mut defrag = self.defrag.lock().unwrap();
let fragment_gather_array = defrag.get_or_create_mut(&incoming_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(
return self.process_complete_incoming_packet(
app,
&mut send,
&mut check_allow_incoming_session,
&mut check_accept_session,
data_buf,
counter,
incoming_counter,
assembled_packet.as_ref(),
packet_type,
None,
incomplete,
incoming,
key_index,
mtu,
current_time,
);
}
} else {
return self.receive_complete(
return self.process_complete_incoming_packet(
app,
&mut send,
&mut check_allow_incoming_session,
&mut check_accept_session,
data_buf,
counter,
incoming_counter,
&[incoming_packet_buf],
packet_type,
None,
incomplete,
incoming,
key_index,
mtu,
current_time,
@ -516,7 +517,7 @@ impl<Application: ApplicationLayer> Context<Application> {
return Ok(ReceiveResult::Ok);
}
fn receive_complete<
fn process_complete_incoming_packet<
'b,
SendFunction: FnMut(Option<&Arc<Session<Application>>>, &mut [u8]),
CheckAllowIncomingSession: FnMut() -> bool,
@ -532,14 +533,16 @@ impl<Application: ApplicationLayer> Context<Application> {
fragments: &[Application::IncomingPacketBuffer],
packet_type: u8,
session: Option<Arc<Session<Application>>>,
incomplete: Option<Arc<IncompleteIncomingSession>>,
incoming: Option<Arc<IncomingIncompleteSession>>,
key_index: usize,
mtu: usize,
current_time: i64,
) -> Result<ReceiveResult<'b, Application>, Error> {
debug_assert!(fragments.len() >= 1);
// Generate incoming message nonce for decryption and authentication.
let incoming_message_nonce = create_message_nonce(packet_type, incoming_counter);
if packet_type == PACKET_TYPE_DATA {
if let Some(session) = session {
let state = session.state.read().unwrap();
@ -648,30 +651,27 @@ impl<Application: ApplicationLayer> Context<Application> {
return Err(Error::OutOfSequence);
}
// Hash the init packet so we can check to see if it's just being retransmitted.
// Hash the init packet so we can check to see if it's just being retransmitted. Alice may
// attempt to retransmit this packet until she receives a response.
let request_hash = SHA384::hash(&pkt_assembled);
let (alice_session_id, bob_session_id, noise_es_ee, bob_hk_ciphertext, header_protection_key, bob_noise_e) =
if let Some(incomplete) = incomplete {
// If we already have an incoming incomplete session record and the hash matches, recall the
// previous state so we can send an identical reply in response to a retransmit.
if secure_eq(&request_hash, &incomplete.request_hash) {
(
incomplete.alice_session_id,
incomplete.bob_session_id,
incomplete.noise_es_ee.clone(),
incomplete.bob_hk_ciphertext,
incomplete.header_protection_key.clone(),
*incomplete.bob_noise_e_secret.public_key_bytes(),
)
let (alice_session_id, mut bob_session_id, noise_es_ee, bob_hk_ciphertext, header_protection_key, bob_noise_e);
if let Some(incoming) = incoming {
// If we've already seen this exact packet before, just recall the same state so we send the
// same response.
if secure_eq(&request_hash, &incoming.request_hash) {
alice_session_id = incoming.alice_session_id;
bob_session_id = incoming.bob_session_id;
noise_es_ee = incoming.noise_es_ee.clone();
bob_hk_ciphertext = incoming.bob_hk_ciphertext;
header_protection_key = incoming.header_protection_key.clone();
bob_noise_e = *incoming.bob_noise_e_secret.public_key_bytes();
} else {
return Err(Error::FailedAuthentication);
}
} else {
// Otherwise parse the packet, authenticate, generate keys, etc. and record state in an
// incomplete state object until this phase of the negotiation is done.
// incoming state object until this phase of the negotiation is done.
let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?;
let alice_noise_e = P384PublicKey::from_bytes(&pkt.alice_noise_e).ok_or(Error::FailedAuthentication)?;
let noise_es = app.get_local_s_keypair().agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?;
@ -694,48 +694,48 @@ impl<Application: ApplicationLayer> Context<Application> {
}
// Decrypt encrypted part of payload.
let mut ctr =
AesCtr::new(kbkdf::<AES_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es.as_bytes()).as_bytes());
ctr.reset_set_iv(&SHA384::hash(&pkt.alice_noise_e)[..AES_CTR_NONCE_SIZE]);
ctr.crypt_in_place(&mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]);
aes_ctr_crypt_one_time_use_key(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es.as_bytes()).as_bytes(),
&mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START],
);
let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?;
let alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?;
alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?;
header_protection_key = Secret(pkt.header_protection_key);
// Create Bob's ephemeral keys and derive noise_es_ee by agreeing with Alice's. Also create
// a Kyber ciphertext to send back to Alice.
let bob_noise_e_secret = P384KeyPair::generate();
let bob_noise_e = bob_noise_e_secret.public_key_bytes().clone();
let noise_es_ee = Secret(hmac_sha512(
bob_noise_e = bob_noise_e_secret.public_key_bytes().clone();
noise_es_ee = Secret(hmac_sha512(
noise_es.as_bytes(),
bob_noise_e_secret
.agree(&alice_noise_e)
.ok_or(Error::FailedAuthentication)?
.as_bytes(),
));
let (bob_hk_ciphertext, hk) =
pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default())
let (hk_ct, hk) = pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default())
.map_err(|_| Error::FailedAuthentication)
.map(|(ct, hk)| (ct, Secret(hk)))?;
bob_hk_ciphertext = hk_ct;
let mut sessions = self.sessions.write().unwrap();
let mut bob_session_id;
loop {
bob_session_id = SessionId::random();
if !sessions.active.contains_key(&bob_session_id) && !sessions.incomplete.contains_key(&bob_session_id) {
if !sessions.active.contains_key(&bob_session_id) && !sessions.incoming.contains_key(&bob_session_id) {
break;
}
}
if sessions.incomplete.len() >= self.max_incomplete_session_queue_size {
if sessions.incoming.len() >= self.max_incomplete_session_queue_size {
// If this queue is too big, we remove the latest entry and replace it. The latest
// is used because under flood conditions this is most likely to be another bogus
// entry. If we find one that is actually timed out, that one is replaced instead.
let mut newest = i64::MIN;
let mut replace_id = None;
let cutoff_time = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS;
for (id, s) in sessions.incomplete.iter() {
for (id, s) in sessions.incoming.iter() {
if s.timestamp <= cutoff_time {
replace_id = Some(*id);
break;
@ -744,13 +744,13 @@ impl<Application: ApplicationLayer> Context<Application> {
replace_id = Some(*id);
}
}
let _ = sessions.incomplete.remove(replace_id.as_ref().unwrap());
let _ = sessions.incoming.remove(replace_id.as_ref().unwrap());
}
// Reserve session ID on this side and record incomplete session state.
sessions.incomplete.insert(
sessions.incoming.insert(
bob_session_id,
Arc::new(IncompleteIncomingSession {
Arc::new(IncomingIncompleteSession {
timestamp: current_time,
request_hash,
alice_session_id,
@ -762,16 +762,7 @@ impl<Application: ApplicationLayer> Context<Application> {
header_protection_key: Secret(pkt.header_protection_key),
}),
);
(
alice_session_id,
bob_session_id,
noise_es_ee,
bob_hk_ciphertext,
Secret(pkt.header_protection_key),
bob_noise_e,
)
};
}
// Create Bob's ephemeral counter-offer reply.
let mut ack_packet = [0u8; BobNoiseXKAck::SIZE];
@ -781,11 +772,11 @@ impl<Application: ApplicationLayer> Context<Application> {
ack.bob_session_id = *bob_session_id.as_bytes();
ack.bob_hk_ciphertext = bob_hk_ciphertext;
// Encrypt main section of reply. Technically we could get away without this but why not?
let mut ctr =
AesCtr::new(kbkdf::<AES_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes());
ctr.reset_set_iv(&bob_noise_e[P384_PUBLIC_KEY_SIZE - AES_CTR_NONCE_SIZE..]);
ctr.crypt_in_place(&mut ack_packet[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START]);
// Encrypt main section of reply.
aes_ctr_crypt_one_time_use_key(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes(),
&mut ack_packet[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START],
);
// Add HMAC-SHA384 to reply packet.
let reply_hmac = hmac_sha384_2(
@ -818,7 +809,7 @@ impl<Application: ApplicationLayer> Context<Application> {
* the negotiation.
*/
if incoming_counter != 1 || incomplete.is_some() {
if incoming_counter != 1 || incoming.is_some() {
return Err(Error::OutOfSequence);
}
@ -827,7 +818,6 @@ impl<Application: ApplicationLayer> Context<Application> {
if let Offer::NoiseXKInit(outgoing_offer) = &state.current_offer {
let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?;
if let Some(bob_session_id) = SessionId::new_from_bytes(&pkt.bob_session_id) {
// Derive noise_es_ee from Bob's ephemeral public key.
let bob_noise_e = P384PublicKey::from_bytes(&pkt.bob_noise_e).ok_or(Error::FailedAuthentication)?;
let noise_es_ee = Secret(hmac_sha512(
@ -838,8 +828,7 @@ impl<Application: ApplicationLayer> Context<Application> {
.ok_or(Error::FailedAuthentication)?
.as_bytes(),
));
let noise_es_ee_kex_enc_key =
kbkdf::<AES_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es_ee.as_bytes());
let noise_es_ee_kex_hmac_key =
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION>(noise_es_ee.as_bytes());
@ -856,11 +845,13 @@ impl<Application: ApplicationLayer> Context<Application> {
}
// Decrypt encrypted portion of message.
let mut ctr = AesCtr::new(noise_es_ee_kex_enc_key.as_bytes());
ctr.reset_set_iv(&SHA384::hash(&pkt.bob_noise_e)[..AES_CTR_NONCE_SIZE]);
ctr.crypt_in_place(&mut pkt_assembled[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START]);
aes_ctr_crypt_one_time_use_key(
kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(noise_es_ee.as_bytes()).as_bytes(),
&mut pkt_assembled[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START],
);
let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?;
if let Some(bob_session_id) = SessionId::new_from_bytes(&pkt.bob_session_id) {
// Complete Noise_XKpsk3 by mixing in noise_se followed by the PSK. The PSK as far as
// the Noise pattern is concerned is the result of mixing the externally supplied PSK
// with the Kyber1024 shared secret (hk). Kyber is treated as part of the PSK because
@ -903,12 +894,12 @@ impl<Application: ApplicationLayer> Context<Application> {
reply_buffer_append(&[0u8, 0u8]); // no meta-data
}
// Encrypt Alice's static identity and other inner payload items. The IV here
// is a hash of 'hk' making it actually a secret and "borrowing" a little PQ
// forward secrecy for Alice's identity.
let mut ctr = AesCtr::new(noise_es_ee_kex_enc_key.as_bytes());
ctr.reset_set_iv(&hk.as_bytes()[..AES_CTR_NONCE_SIZE]);
ctr.crypt_in_place(&mut reply_buffer[HEADER_SIZE + 1..reply_len]);
// Encrypt Alice's static identity and other inner payload items. The key used here is
// mixed with 'hk' to make identity secrecy PQ forward secure.
aes_ctr_crypt_one_time_use_key(
&hmac_sha512(noise_es_ee.as_bytes(), hk.as_bytes())[..AES_256_KEY_SIZE],
&mut reply_buffer[HEADER_SIZE + 1..reply_len],
);
// First attach HMAC allowing Bob to verify that this is from the same Alice and to
// verify the authenticity of encrypted data.
@ -983,9 +974,9 @@ impl<Application: ApplicationLayer> Context<Application> {
return Err(Error::InvalidPacket);
}
if let Some(incomplete) = incomplete {
if let Some(incoming) = incoming {
// Check timeout, negotiations aren't allowed to take longer than this.
if (current_time - incomplete.timestamp) > Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS {
if (current_time - incoming.timestamp) > Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS {
return Err(Error::UnknownLocalSessionId);
}
@ -995,7 +986,7 @@ impl<Application: ApplicationLayer> Context<Application> {
if !secure_eq(
&pkt_assembled[auth_start..pkt_assembled.len() - HMAC_SHA384_SIZE],
&hmac_sha384_2(
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION>(incomplete.noise_es_ee.as_bytes())
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION>(incoming.noise_es_ee.as_bytes())
.as_bytes(),
&incoming_message_nonce,
&pkt_assembled[HEADER_SIZE..auth_start],
@ -1009,11 +1000,10 @@ impl<Application: ApplicationLayer> Context<Application> {
pkt_assembly_buffer_copy[..pkt_assembled.len()].copy_from_slice(pkt_assembled);
// Decrypt encrypted section so we can finally learn Alice's static identity.
let mut ctr = AesCtr::new(
kbkdf::<AES_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION>(incomplete.noise_es_ee.as_bytes()).as_bytes(),
aes_ctr_crypt_one_time_use_key(
&hmac_sha512(incoming.noise_es_ee.as_bytes(), incoming.hk.as_bytes())[..AES_256_KEY_SIZE],
&mut pkt_assembled[ALICE_NOISE_XK_ACK_ENC_START..auth_start],
);
ctr.reset_set_iv(&incomplete.hk.as_bytes()[..AES_CTR_NONCE_SIZE]);
ctr.crypt_in_place(&mut pkt_assembled[ALICE_NOISE_XK_ACK_ENC_START..auth_start]);
// Read the static public blob and optional meta-data.
let mut pkt_assembled_ptr = HEADER_SIZE + 1;
@ -1048,7 +1038,7 @@ impl<Application: ApplicationLayer> Context<Application> {
// her static public blob.
let check_result = check_accept_session(alice_static_public_blob, alice_meta_data);
if check_result.is_none() {
self.sessions.write().unwrap().incomplete.remove(&incomplete.bob_session_id);
self.sessions.write().unwrap().incoming.remove(&incoming.bob_session_id);
return Ok(ReceiveResult::Rejected);
}
let (alice_noise_s, psk, application_data) = check_result.unwrap();
@ -1056,14 +1046,14 @@ impl<Application: ApplicationLayer> Context<Application> {
// Complete Noise_XKpsk3 on Bob's side.
let noise_es_ee_se_hk_psk = Secret(hmac_sha512(
&hmac_sha512(
incomplete.noise_es_ee.as_bytes(),
incomplete
incoming.noise_es_ee.as_bytes(),
incoming
.bob_noise_e_secret
.agree(&alice_noise_s)
.ok_or(Error::FailedAuthentication)?
.as_bytes(),
),
&hmac_sha512(psk.as_bytes(), incomplete.hk.as_bytes()),
&hmac_sha512(psk.as_bytes(), incoming.hk.as_bytes()),
));
// Verify the packet using the final key to verify the whole key exchange.
@ -1073,21 +1063,21 @@ impl<Application: ApplicationLayer> Context<Application> {
kbkdf::<HMAC_SHA384_SIZE, KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION>(noise_es_ee_se_hk_psk.as_bytes())
.as_bytes(),
&incoming_message_nonce,
&pkt_assembly_buffer_copy[HEADER_SIZE..auth_start],
&pkt_assembly_buffer_copy[HEADER_SIZE..auth_start + HMAC_SHA384_SIZE],
),
) {
return Err(Error::FailedAuthentication);
}
let session = Arc::new(Session {
id: incomplete.bob_session_id,
id: incoming.bob_session_id,
application_data,
psk,
send_counter: AtomicU64::new(2), // 1 was already used during negotiation
receive_window: std::array::from_fn(|_| AtomicU64::new(0)),
header_protection_cipher: Aes::new(incomplete.header_protection_key.as_bytes()),
header_protection_cipher: Aes::new(incoming.header_protection_key.as_bytes()),
state: RwLock::new(State {
remote_session_id: Some(incomplete.alice_session_id),
remote_session_id: Some(incoming.alice_session_id),
keys: [
Some(SessionKey::new::<Application>(noise_es_ee_se_hk_psk, current_time, 2, true)),
None,
@ -1101,8 +1091,8 @@ impl<Application: ApplicationLayer> Context<Application> {
// Promote this from an incomplete session to an established session.
{
let mut sessions = self.sessions.write().unwrap();
sessions.incomplete.remove(&incomplete.bob_session_id);
sessions.active.insert(incomplete.bob_session_id, Arc::downgrade(&session));
sessions.incoming.remove(&incoming.bob_session_id);
sessions.active.insert(incoming.bob_session_id, Arc::downgrade(&session));
}
return Ok(ReceiveResult::OkNewSession(session));
@ -1115,7 +1105,7 @@ impl<Application: ApplicationLayer> Context<Application> {
if pkt_assembled.len() != AliceRekeyInit::SIZE {
return Err(Error::InvalidPacket);
}
if incomplete.is_some() {
if incoming.is_some() {
return Err(Error::OutOfSequence);
}
@ -1178,7 +1168,7 @@ impl<Application: ApplicationLayer> Context<Application> {
if pkt_assembled.len() != BobRekeyAck::SIZE {
return Err(Error::InvalidPacket);
}
if incomplete.is_some() {
if incoming.is_some() {
return Err(Error::OutOfSequence);
}
@ -1480,8 +1470,8 @@ fn assemble_fragments_into<A: ApplicationLayer>(fragments: &[A::IncomingPacketBu
impl SessionKey {
fn new<Application: ApplicationLayer>(key: Secret<BASE_KEY_SIZE>, current_time: i64, current_counter: u64, role_is_bob: bool) -> Self {
let a2b = kbkdf::<AES_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB>(key.as_bytes());
let b2a = kbkdf::<AES_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE>(key.as_bytes());
let a2b = kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB>(key.as_bytes());
let b2a = kbkdf::<AES_256_KEY_SIZE, KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE>(key.as_bytes());
let (receive_key, send_key) = if role_is_bob {
(a2b, b2a)
} else {
@ -1549,6 +1539,16 @@ fn hmac_sha384_2(key: &[u8], a: &[u8], b: &[u8]) -> [u8; 48] {
hmac.finish()
}
/// Shortcut to AES-CTR encrypt or decrypt with a zero IV.
///
/// This is used during Noise_XK handshaking. Each stage uses a different key to encrypt the
/// payload that is used only once per handshake and per session.
fn aes_ctr_crypt_one_time_use_key(key: &[u8], data: &mut [u8]) {
let mut ctr = AesCtr::new(key);
ctr.reset_set_iv(&[0u8; 12]);
ctr.crypt_in_place(data);
}
/// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 7)
/// Cryptographically this isn't meaningfully different from HMAC(key, [label]) but this is how NIST rolls.
fn kbkdf<const OUTPUT_BYTES: usize, const LABEL: u8>(key: &[u8]) -> Secret<OUTPUT_BYTES> {