diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 1a0d226c0..f9080c2a2 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -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; + + 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>, + alice_in: mpsc::Receiver>, +) { + let context = zssp::Context::::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>, + bob_in: mpsc::Receiver>, +) { + let context = zssp::Context::::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::>(128); + let (bob_out, alice_in) = mpsc::sync_channel::>(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); +} diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index aa7d3fc87..210107efd 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -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], diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index fe340bb5d..80448040e 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -38,8 +38,7 @@ use crate::sessionid::SessionId; /// defragment incoming packets that are not yet associated with a session. pub struct Context { max_incomplete_session_queue_size: usize, - initial_offer_defrag: - Mutex, 256, 256>>, + defrag: Mutex, 256, 256>>, sessions: RwLock>, } @@ -49,7 +48,7 @@ struct SessionsById { active: HashMap>>, // Incomplete sessions in the middle of three-phase Noise_XK negotiation, expired after timeout. - incomplete: HashMap>, + incoming: HashMap>, } /// 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, // Key used in derivation of the next session key - receive_key: Secret, // Receive side AES-GCM key - send_key: Secret, // Send side AES-GCM key + receive_key: Secret, // Receive side AES-GCM key + send_key: Secret, // Send side AES-GCM key receive_cipher_pool: Mutex>>, // Pool of reusable sending ciphers send_cipher_pool: Mutex>>, // Pool of reusable receiving ciphers rekey_at_time: i64, // Rekey at or after this time (ticks) @@ -142,10 +141,10 @@ impl Context { 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 Context { } } - 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 Context { sessions.active.remove(id); } for id in dead_pending.iter() { - sessions.incomplete.remove(id); + sessions.incoming.remove(id); } } @@ -272,7 +271,7 @@ impl Context { 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 Context { init.alice_hk_public = alice_hk_secret.public; init.header_protection_key = header_protection_key.0; - let mut ctr = AesCtr::new(kbkdf::(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::(noise_es.as_bytes()).as_bytes(), + &mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START], + ); let hmac = hmac_sha384_2( kbkdf::(noise_es.as_bytes()).as_bytes(), @@ -394,7 +394,7 @@ impl Context { 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 Context { .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 Context { 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 Context { 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 Context { return Ok(ReceiveResult::Ok); } - fn receive_complete< + fn process_complete_incoming_packet< 'b, SendFunction: FnMut(Option<&Arc>>, &mut [u8]), CheckAllowIncomingSession: FnMut() -> bool, @@ -532,14 +533,16 @@ impl Context { fragments: &[Application::IncomingPacketBuffer], packet_type: u8, session: Option>>, - incomplete: Option>, + incoming: Option>, key_index: usize, mtu: usize, current_time: i64, ) -> Result, 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,130 +651,118 @@ impl Context { 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(), - ) - } else { - return Err(Error::FailedAuthentication); - } + 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 { - // Otherwise parse the packet, authenticate, generate keys, etc. and record state in an - // incomplete state object until this phase of the negotiation is done. + return Err(Error::FailedAuthentication); + } + } else { + // Otherwise parse the packet, authenticate, generate keys, etc. and record state in an + // 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)?; - 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)?; + // Authenticate packet and also prove that Alice knows our static public key. + if !secure_eq( + &pkt.hmac_es, + &hmac_sha384_2( + kbkdf::(noise_es.as_bytes()).as_bytes(), + &incoming_message_nonce, + &pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], + ), + ) { + return Err(Error::FailedAuthentication); + } - // Authenticate packet and also prove that Alice knows our static public key. - if !secure_eq( - &pkt.hmac_es, - &hmac_sha384_2( - kbkdf::(noise_es.as_bytes()).as_bytes(), - &incoming_message_nonce, - &pkt_assembled[HEADER_SIZE..AliceNoiseXKInit::AUTH_START], - ), - ) { - return Err(Error::FailedAuthentication); + // Let application filter incoming connection attempt by whatever criteria it wants. + if !check_allow_incoming_session() { + return Ok(ReceiveResult::Rejected); + } + + // Decrypt encrypted part of payload. + aes_ctr_crypt_one_time_use_key( + kbkdf::(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)?; + 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(); + 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 (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(); + + loop { + bob_session_id = SessionId::random(); + if !sessions.active.contains_key(&bob_session_id) && !sessions.incoming.contains_key(&bob_session_id) { + break; } + } - // Let application filter incoming connection attempt by whatever criteria it wants. - if !check_allow_incoming_session() { - return Ok(ReceiveResult::Rejected); - } - - // Decrypt encrypted part of payload. - let mut ctr = - AesCtr::new(kbkdf::(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]); - - 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)?; - - // 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( - 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()) - .map_err(|_| Error::FailedAuthentication) - .map(|(ct, hk)| (ct, Secret(hk)))?; - - 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.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.incoming.iter() { + if s.timestamp <= cutoff_time { + replace_id = Some(*id); break; + } else if s.timestamp >= newest { + newest = s.timestamp; + replace_id = Some(*id); } } + let _ = sessions.incoming.remove(replace_id.as_ref().unwrap()); + } - if sessions.incomplete.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() { - if s.timestamp <= cutoff_time { - replace_id = Some(*id); - break; - } else if s.timestamp >= newest { - newest = s.timestamp; - replace_id = Some(*id); - } - } - let _ = sessions.incomplete.remove(replace_id.as_ref().unwrap()); - } - - // Reserve session ID on this side and record incomplete session state. - sessions.incomplete.insert( - bob_session_id, - Arc::new(IncompleteIncomingSession { - timestamp: current_time, - request_hash, - alice_session_id, - bob_session_id, - noise_es_ee: noise_es_ee.clone(), - bob_hk_ciphertext, - hk, - bob_noise_e_secret, - header_protection_key: Secret(pkt.header_protection_key), - }), - ); - - ( + // Reserve session ID on this side and record incomplete session state. + sessions.incoming.insert( + bob_session_id, + Arc::new(IncomingIncompleteSession { + timestamp: current_time, + request_hash, alice_session_id, bob_session_id, - noise_es_ee, + noise_es_ee: noise_es_ee.clone(), bob_hk_ciphertext, - Secret(pkt.header_protection_key), - bob_noise_e, - ) - }; + hk, + bob_noise_e_secret, + header_protection_key: Secret(pkt.header_protection_key), + }), + ); + } // Create Bob's ephemeral counter-offer reply. let mut ack_packet = [0u8; BobNoiseXKAck::SIZE]; @@ -781,11 +772,11 @@ impl Context { 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::(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::(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 Context { * the negotiation. */ - if incoming_counter != 1 || incomplete.is_some() { + if incoming_counter != 1 || incoming.is_some() { return Err(Error::OutOfSequence); } @@ -827,40 +818,40 @@ impl Context { if let Offer::NoiseXKInit(outgoing_offer) = &state.current_offer { let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; + // 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( + outgoing_offer.noise_es.as_bytes(), + outgoing_offer + .alice_noise_e_secret + .agree(&bob_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + )); + + let noise_es_ee_kex_hmac_key = + kbkdf::(noise_es_ee.as_bytes()); + + // Authenticate Bob's reply and the validity of bob_noise_e. + if !secure_eq( + &pkt.hmac_es_ee, + &hmac_sha384_2( + noise_es_ee_kex_hmac_key.as_bytes(), + &incoming_message_nonce, + &pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START], + ), + ) { + return Err(Error::FailedAuthentication); + } + + // Decrypt encrypted portion of message. + aes_ctr_crypt_one_time_use_key( + kbkdf::(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) { - // 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( - outgoing_offer.noise_es.as_bytes(), - outgoing_offer - .alice_noise_e_secret - .agree(&bob_noise_e) - .ok_or(Error::FailedAuthentication)? - .as_bytes(), - )); - let noise_es_ee_kex_enc_key = - kbkdf::(noise_es_ee.as_bytes()); - let noise_es_ee_kex_hmac_key = - kbkdf::(noise_es_ee.as_bytes()); - - // Authenticate Bob's reply and the validity of bob_noise_e. - if !secure_eq( - &pkt.hmac_es_ee, - &hmac_sha384_2( - noise_es_ee_kex_hmac_key.as_bytes(), - &incoming_message_nonce, - &pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START], - ), - ) { - return Err(Error::FailedAuthentication); - } - - // 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]); - let pkt: &BobNoiseXKAck = byte_array_as_proto_buffer(pkt_assembled)?; - // 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 Context { 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 Context { 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 Context { if !secure_eq( &pkt_assembled[auth_start..pkt_assembled.len() - HMAC_SHA384_SIZE], &hmac_sha384_2( - kbkdf::(incomplete.noise_es_ee.as_bytes()) + kbkdf::(incoming.noise_es_ee.as_bytes()) .as_bytes(), &incoming_message_nonce, &pkt_assembled[HEADER_SIZE..auth_start], @@ -1009,11 +1000,10 @@ impl Context { 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::(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 Context { // 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 Context { // 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 Context { kbkdf::(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::(noise_es_ee_se_hk_psk, current_time, 2, true)), None, @@ -1101,8 +1091,8 @@ impl Context { // 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 Context { 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 Context { 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(fragments: &[A::IncomingPacketBu impl SessionKey { fn new(key: Secret, current_time: i64, current_counter: u64, role_is_bob: bool) -> Self { - let a2b = kbkdf::(key.as_bytes()); - let b2a = kbkdf::(key.as_bytes()); + let a2b = kbkdf::(key.as_bytes()); + let b2a = kbkdf::(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(key: &[u8]) -> Secret {