From 967dcaf377eda1091d0332254cfca3801feffc26 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 24 Feb 2023 12:37:34 -0500 Subject: [PATCH] More ZSSP work, add benchmarks for mimcvdf. --- crypto/benches/benchmark_crypto.rs | 18 +- crypto/src/mimcvdf.rs | 137 +++++------ zssp/src/constants.rs | 3 - zssp/src/error.rs | 4 + zssp/src/proto.rs | 7 + zssp/src/zssp.rs | 366 +++++++++++++++-------------- 6 files changed, 279 insertions(+), 256 deletions(-) diff --git a/crypto/benches/benchmark_crypto.rs b/crypto/benches/benchmark_crypto.rs index c8220e1bd..fd01855c9 100644 --- a/crypto/benches/benchmark_crypto.rs +++ b/crypto/benches/benchmark_crypto.rs @@ -1,10 +1,27 @@ use criterion::{criterion_group, criterion_main, Criterion}; use std::time::Duration; +use zerotier_crypto::mimcvdf; use zerotier_crypto::p384::*; use zerotier_crypto::x25519::*; pub fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("cryptography"); + + let mut input = 1; + let mut proof = 0; + group.bench_function("mimcvdf::delay(1000)", |b| { + b.iter(|| { + input += 1; + proof = mimcvdf::delay(input, 1000); + }) + }); + group.bench_function("mimcvdf::verify(1000)", |b| { + b.iter(|| { + assert!(mimcvdf::verify(proof, input, 1000)); + }) + }); + let p384_a = P384KeyPair::generate(); let p384_b = P384KeyPair::generate(); @@ -12,7 +29,6 @@ pub fn criterion_benchmark(c: &mut Criterion) { let x25519_b = X25519KeyPair::generate(); let x25519_b_pub = x25519_b.public_bytes(); - let mut group = c.benchmark_group("cryptography"); group.measurement_time(Duration::new(10, 0)); group.bench_function("ecdhp384", |b| { diff --git a/crypto/src/mimcvdf.rs b/crypto/src/mimcvdf.rs index 7e77fd114..bfb49921f 100644 --- a/crypto/src/mimcvdf.rs +++ b/crypto/src/mimcvdf.rs @@ -7,81 +7,71 @@ */ /* + * MIMC is a hash function originally designed for use with STARK and SNARK proofs. It's based + * on modular multiplication and exponentiation instead of the usual bit twiddling or ARX + * operations that underpin more common hash algorithms. + * + * It's useful as a verifiable delay function because it can be computed in both directions with + * one direction taking orders of magnitude longer than the other. The "backward" direction is + * used as the delay function as it requires modular exponentiation which is inherently more + * compute intensive. The "forward" direction simply requires modular cubing which is two modular + * multiplications and is much faster. + * + * It's also nice because it's incredibly simple with a tiny code footprint. + * + * This is used for anti-DOS and anti-spamming delay functions. It's not used for anything + * really "cryptographically hard," and if it were broken cryptographically it would still be + * useful as a VDF as long as the break didn't yield a significantly faster way of computing a + * delay proof than the straightforward iterative way implemented here. + * + * Here are two references on MIMC with the first being the original paper and the second being + * a blog post describing its use as a VDF. + * * https://eprint.iacr.org/2016/492.pdf * https://vitalik.ca/general/2018/07/21/starks_part_3.html */ -// 2^127 - 39 +// p = 2^127 - 39, the largest 127-bit prime of the form 6k + 5 const PRIME: u128 = 170141183460469231731687303715884105689; -// 2p-1/3 + +// (2p - 1) / 3 const PRIME_2P_MINUS_1_DIV_3: u128 = 113427455640312821154458202477256070459; -const K_COUNT_MASK: usize = 63; -const K: [u64; 64] = [ - 0x921cdfd99022340f, - 0xe7c65f78c70afaa8, - 0x72793744494c4fda, - 0x67759e2688bc9c0a, - 0x7681a224661f0ac0, - 0xa7b81b099925a2bf, - 0x16d43792e66b030a, - 0x841bd90742d26ee9, - 0xb1346ec08db97053, - 0xd044229c1173d972, - 0xf4813498dfdead0e, - 0xe46dca4c237d2c28, - 0xac64872778089599, - 0x67be75af74416e74, - 0xb9dec3aefd3ae012, - 0xf0497147953c4276, - 0xf6ac07fd3944177d, - 0xccf1c28813eb589b, - 0x49abb5e2b0bff5bd, - 0xd5c15eeb39587d69, - 0x9c6ff50ee6898649, - 0x763f3b25524a0fbf, - 0xa6029c37f715c02c, - 0xe458a5902b2b5629, - 0x8e4d6be6a1ba32c5, - 0x052aba0b61738f20, - 0xc18a6901fa026b12, - 0x137df11cf1dbe811, - 0x5da0310e419be602, - 0xc66ddec578f52891, - 0xe4eae4efc0f0d54f, - 0xf9d488269f118012, - 0xcf9b5108f66e77d1, - 0x443ba29939f5a657, - 0xa4e4b7d28c51e5c2, - 0xe030d1772f112c01, - 0xe136f0cf8da5e172, - 0x3e9ee638f9663dc2, - 0xbc5c1db73e639dfd, - 0xa9fbbaa873fedf73, - 0xffb2a5247d10ab8f, - 0x06e6f3b5ae4b67ac, - 0x475e7d427d331282, - 0xcac6237c40a9d653, - 0xe9a15c1d177beefa, - 0xa14ef2111c2175a3, - 0x8427d4b68982fc21, - 0x12171e2a55d43343, - 0x37715fdea87a0a60, - 0x24bc5d28cff8ecad, - 0x92276e4118304e62, - 0x824b66792f58dd45, - 0xe43973cf253b6947, - 0xd0db2c5a2a4f064d, - 0x734cdb241520ad04, - 0xcec4f2ce5013069e, - 0x2741c83c07bbf9e0, - 0x284be707dcbda1a4, - 0xd602f3d8545799b2, - 0xea3977f56573b4d2, - 0x0723fda64d57d0c6, - 0x04dc344d0dde863a, - 0x7584143462914be4, - 0x111307f7823dfcc6, +// Randomly generated round constants, each modulo PRIME. +const K_COUNT_MASK: usize = 31; +const K: [u128; 32] = [ + 0x1fdd07a761b611bb1ab9419a70599a7c, + 0x23056b05d5c6b925e333d7418047650a, + 0x77a638f9b437a307f8866fbd2672c705, + 0x60213dab83bab91d1c310bd87e9da332, + 0xf56bc883301ab373179e46b098b7a7, + 0x7914a0dbd2f971344173b350c28a838, + 0x44bb64af5e446e6ebdc068d10d318f26, + 0x1bca1921fd328bb725ae0cbcbc20a263, + 0xafa963242f5216a7da1cd5328b23659, + 0x7fe17c43782b883a63ee0a790e0b2b77, + 0x23bb62abf728bf453200ee528f902c33, + 0x75ec0c055be14955db6878567e3c0465, + 0x7902bb57876e0b08b4de02a66755e5d7, + 0xe5d7094f37b615f5a1e1594b0390de8, + 0x12d4ddee90653a26f5de63ff4651f2d, + 0xce4a15bc35633b5ed8bcae2c93d739c, + 0x23f25b935e52df87255db8c608ef9ab4, + 0x611a08d7464fb984c98104d77f1609a7, + 0x7aa825876a7f6acde5efa57992da9c43, + 0x2be9686f630fa28a0a0e1081a59755b4, + 0x50060dac9ac4656ba3f8ee7592f4e28a, + 0x4113abff6f5bb303eac2ca809d4d529d, + 0x2af9d01d4e753feb5834c14ca0543397, + 0x73c2d764691ced2b823dda887e22ae85, + 0x5b53dcd4750ff888dca2497cec4dacb7, + 0x5d8984a52c2d8f3cc9bcf61ef29f8a1, + 0x588d8cc99533d649aabb5f0f552140e, + 0x4dae04985fde8c8464ba08aaa7d8761e, + 0x53f0c4740b8c3bda3fc05109b9a2b71, + 0x3e918c88a6795e3bf840e0b74d91b9d7, + 0x1dbcb30d724f11200aebb1dff87def91, + 0x6086b0af0e1e68558170239d23be9780, ]; fn mulmod(mut a: u128, mut b: u128) -> u128 { @@ -116,21 +106,23 @@ fn powmod(mut base: u128, mut exp: u128) -> u128 { } } +/// Compute MIMC for the given number of iterations and return a proof that can be checked much more quickly. pub fn delay(mut input: u128, rounds: usize) -> u128 { debug_assert!(rounds > 0); input %= PRIME; for r in 1..(rounds + 1) { - input = powmod::(input ^ (K[(rounds - r) & K_COUNT_MASK] as u128), PRIME_2P_MINUS_1_DIV_3); + input = powmod::(input ^ K[(rounds - r) & K_COUNT_MASK], PRIME_2P_MINUS_1_DIV_3); } input } -pub fn verify(mut proof: u128, expected: u128, rounds: usize) -> bool { +/// Quickly verify the result of delay() given the returned proof, original input, and original number of rounds. +pub fn verify(mut proof: u128, original_input: u128, rounds: usize) -> bool { debug_assert!(rounds > 0); for r in 0..rounds { - proof = mulmod::(proof, mulmod::(proof, proof)) ^ (K[r & K_COUNT_MASK] as u128); + proof = mulmod::(proof, mulmod::(proof, proof)) ^ K[r & K_COUNT_MASK]; } - proof == (expected % PRIME) + proof == (original_input % PRIME) } #[cfg(test)] @@ -142,6 +134,7 @@ mod tests { for i in 1..5 { let input = (crate::random::xorshift64_random() as u128).wrapping_mul(crate::random::xorshift64_random() as u128); let proof = delay(input, i * 3); + //println!("{}", proof); assert!(verify(proof, input, i * 3)); } } diff --git a/zssp/src/constants.rs b/zssp/src/constants.rs index 5f3ea73c7..e5737c958 100644 --- a/zssp/src/constants.rs +++ b/zssp/src/constants.rs @@ -36,6 +36,3 @@ pub(crate) const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 min /// Timeout for incoming sessions in incomplete state in milliseconds. pub(crate) const INCOMPLETE_SESSION_TIMEOUT: i64 = 1000; - -/// Maximum number of pending incomplete sessions. -pub(crate) const INCOMPLETE_SESSION_MAX_QUEUE_SIZE: usize = 256; diff --git a/zssp/src/error.rs b/zssp/src/error.rs index 7fccd3017..ebc74207b 100644 --- a/zssp/src/error.rs +++ b/zssp/src/error.rs @@ -32,6 +32,9 @@ pub enum Error { /// Packet ignored by rate limiter. RateLimited, + /// Packet counter is too far outside window. + OutOfCounterWindow, + /// The other peer specified an unrecognized protocol version UnknownProtocolVersion, @@ -66,6 +69,7 @@ impl std::fmt::Display for Error { Self::MaxKeyLifetimeExceeded => f.write_str("MaxKeyLifetimeExceeded"), Self::SessionNotEstablished => f.write_str("SessionNotEstablished"), Self::RateLimited => f.write_str("RateLimited"), + Self::OutOfCounterWindow => f.write_str("OutOfCounterWindow"), Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"), Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"), Self::DataTooLarge => f.write_str("DataTooLarge"), diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index b52ee778b..cfee97845 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -37,17 +37,22 @@ pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION: u8 = b'X'; // intermediat pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION: u8 = b'x'; // intermediate keys used in key exchanges pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction +pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHET: u8 = b'R'; // Key used in derivatin of next session key pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 pub(crate) const MAX_NOISE_HANDSHAKE_FRAGMENTS: usize = 16; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS * MIN_TRANSPORT_MTU; +pub(crate) const BASE_KEY_SIZE: usize = 64; + pub(crate) const AES_KEY_SIZE: usize = 32; pub(crate) const AES_HEADER_CHECK_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. #[allow(unused)] #[repr(C, packed)] pub(crate) struct AliceNoiseXKInit { @@ -68,6 +73,7 @@ impl AliceNoiseXKInit { pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; } +/// The response to AliceNoiceXKInit containing Bob's ephemeral keys. #[allow(unused)] #[repr(C, packed)] pub(crate) struct BobNoiseXKAck { @@ -87,6 +93,7 @@ impl BobNoiseXKAck { pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; } +/// Alice's final response containing her identity (she already knows Bob's) and meta-data. /* #[allow(unused)] #[repr(C, packed)] diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index e182f83d0..cb3e69b95 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -20,6 +20,7 @@ use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE}; use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; +use zerotier_utils::arrayvec::ArrayVec; use zerotier_utils::gatherarray::GatherArray; use zerotier_utils::memory; use zerotier_utils::ringbuffermap::RingBufferMap; @@ -37,8 +38,9 @@ use crate::sessionid::SessionId; /// Each application using ZSSP must create an instance of this to own sessions and /// defragment incoming packets that are not yet associated with a session. pub struct Context { + max_incomplete_session_queue_size: usize, initial_offer_defrag: - Mutex, 1024, 1024>>, + Mutex, 256, 256>>, sessions: RwLock>, } @@ -50,13 +52,13 @@ pub enum ReceiveResult<'b, Application: ApplicationLayer> { /// Packet was valid and a data payload was decoded and authenticated. OkData(Arc>, &'b mut [u8]), - /// Packet was valid and a new session was created. - OkNewSession(Arc>), + /// Packet was valid and a new session was created, with static public blob and optional meta-data. + OkNewSession(Arc>, &'b [u8], Option<&'b [u8]>), - /// Packet appears valid but was ignored e.g. as a duplicate. + /// Packet appears valid but was ignored as a duplicate or as meaningless given the current state. Ignored, - /// Packet appears valid but new session was rejected by application layer. + /// Packet appears valid but was rejected by the application layer, e.g. a rejected new session attempt. Rejected, } @@ -70,7 +72,7 @@ pub struct Session { /// An arbitrary application defined object associated with each session pub application_data: Application::Data, - psk: Secret<64>, + psk: Secret, send_counter: AtomicU64, receive_window: [AtomicU64; COUNTER_WINDOW_MAX_OOO], header_check_cipher: Aes, @@ -92,12 +94,13 @@ struct NoiseXKIncoming { timestamp: i64, alice_session_id: SessionId, bob_session_id: SessionId, - noise_es_ee: Secret<64>, + noise_es_ee: Secret, hk: Secret, header_check_cipher_key: Secret, bob_noise_e_secret: P384KeyPair, } +/// State that needs to be cached for the most recent outgoing offer. enum EphemeralOffer { None, NoiseXKInit( @@ -105,7 +108,7 @@ enum EphemeralOffer { Box<( // alice_e_secret, metadata, noise_es, alice_hk_public, alice_hk_secret, header check key P384KeyPair, - Option>, + Option>, Secret<48>, Secret, )>, @@ -113,13 +116,16 @@ enum EphemeralOffer { RekeyInit(P384KeyPair), } +/// Other mutable state within the session. struct State { remote_session_id: Option, keys: [Option; 2], current_key: usize, } +/// A session key with lifetime information. 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_cipher_pool: Mutex>>, // Pool of reusable sending ciphers @@ -134,12 +140,13 @@ struct SessionKey { impl Context { /// Create a new session context. - pub fn new(_: &Application) -> Self { + pub fn new(_: &Application, max_incomplete_session_queue_size: usize) -> Self { Self { + max_incomplete_session_queue_size, initial_offer_defrag: Mutex::new(RingBufferMap::new(random::next_u32_secure())), sessions: RwLock::new(SessionMaps { active: HashMap::with_capacity(64), - incomplete: HashMap::with_capacity(16), + incomplete: HashMap::with_capacity(64), }), } } @@ -196,7 +203,7 @@ impl Context { mtu: usize, remote_s_public_blob: &[u8], metadata: Option<&[u8]>, - psk: Secret<64>, + psk: Secret, application_data: Application::Data, ) -> Result>, Error> { if let Some(md) = metadata.as_ref() { @@ -232,7 +239,7 @@ impl Context { header_check_cipher: Aes::new(&header_check_cipher_key), offer: Mutex::new(EphemeralOffer::NoiseXKInit(Box::new(( alice_noise_e_secret, - metadata.map(|md| md.to_vec()), + metadata.map(|md| ArrayVec::try_from(md).unwrap()), noise_es.clone(), Secret(alice_hk_secret.secret), )))), @@ -278,29 +285,18 @@ impl Context { /// wtth an active session this session is supplied, otherwise this parameter is None. The size /// of packets to be sent will not exceed the supplied mtu. /// - /// New sessions can be accepted or rejected at both the initial negotiation phase and the final - /// negotiation phase using the incoming session filter function. For the initial phase of Noise_XK - /// the function will be called with None as a parameter since we do not yet know the static identity - /// or meta-data associated with the connection attempt. In the final phase the function will be called - /// again with the static public identity blob of the initiating endpoint and optionally any meta-data - /// that was supplied. In both cases a return value of false causes abandonment of the session. - /// /// * `app` - Interface to application using ZSSP - /// * `incoming_session_filter` - Function to call to check whether new sessions should be accepted + /// * `check_allow_incoming_session` - Function to call to check whether an unidentified new session should be accepted /// * `send` - Function to call to send packets /// * `data_buf` - Buffer to receive decrypted and authenticated object data (an error is returned if too small) /// * `incoming_packet_buf` - Buffer containing incoming wire packet (receive() takes ownership) /// * `mtu` - Physical wire MTU for sending packets /// * `current_time` - Current monotonic time in milliseconds #[inline] - pub fn receive< - 'b, - SendFunction: FnMut(Option<&Arc>>, &mut [u8]), - PermitIncomingSession: FnMut(Option<&[u8]>, Option<&[u8]>) -> bool, - >( + pub fn receive<'b, SendFunction: FnMut(Option<&Arc>>, &mut [u8]), CheckAllowIncomingSession: FnMut() -> bool>( &self, app: &Application, - mut incoming_session_filter: PermitIncomingSession, + mut check_allow_incoming_session: CheckAllowIncomingSession, mut send: SendFunction, data_buf: &'b mut [u8], mut incoming_packet_buf: Application::IncomingPacketBuffer, @@ -330,7 +326,7 @@ impl Context { return self.receive_complete( app, &mut send, - &mut incoming_session_filter, + &mut check_allow_incoming_session, data_buf, counter, assembled_packet.as_ref(), @@ -351,7 +347,7 @@ impl Context { return self.receive_complete( app, &mut send, - &mut incoming_session_filter, + &mut check_allow_incoming_session, data_buf, counter, &[incoming_packet_buf], @@ -364,7 +360,7 @@ impl Context { ); } } else { - return Ok(ReceiveResult::Ignored); + return Err(Error::OutOfCounterWindow); } } else { if let Some(p) = self.sessions.read().unwrap().incomplete.get(&local_session_id).cloned() { @@ -389,7 +385,7 @@ impl Context { return self.receive_complete( app, &mut send, - &mut incoming_session_filter, + &mut check_allow_incoming_session, data_buf, counter, assembled_packet.as_ref(), @@ -405,7 +401,7 @@ impl Context { return self.receive_complete( app, &mut send, - &mut incoming_session_filter, + &mut check_allow_incoming_session, data_buf, counter, &[incoming_packet_buf], @@ -424,12 +420,12 @@ impl Context { fn receive_complete< 'b, SendFunction: FnMut(Option<&Arc>>, &mut [u8]), - PermitIncomingSession: FnMut(Option<&[u8]>, Option<&[u8]>) -> bool, + CheckAllowIncomingSession: FnMut() -> bool, >( &self, app: &Application, send: &mut SendFunction, - incoming_session_filter: &mut PermitIncomingSession, + check_allow_incoming_session: &mut CheckAllowIncomingSession, data_buf: &'b mut [u8], counter: u64, fragments: &[Application::IncomingPacketBuffer], @@ -511,7 +507,7 @@ impl Context { return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); } else { - return Ok(ReceiveResult::Ignored); + return Err(Error::OutOfCounterWindow); } } } @@ -551,9 +547,8 @@ impl Context { * to the current exchange. */ - // There shouldn't be a session yet on Bob's end, and this should be the first packet. if session.is_some() || counter != 1 { - return Ok(ReceiveResult::Ignored); + return Err(Error::OutOfCounterWindow); } let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; @@ -572,11 +567,12 @@ impl Context { return Err(Error::FailedAuthentication); } - if !incoming_session_filter(None, None) { + // 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 (already authenticated above). + // 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]); @@ -611,19 +607,23 @@ impl Context { } } - if sessions.incomplete.len() >= INCOMPLETE_SESSION_MAX_QUEUE_SIZE { + 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. + // entry. If we find one that is actually timed out, that always gets replaced. let mut newest = i64::MIN; - let mut newest_id = None; + let mut replace_id = None; + let cutoff_time = current_time - INCOMPLETE_SESSION_TIMEOUT; for (id, s) in sessions.incomplete.iter() { - if s.timestamp >= newest { + if s.timestamp <= cutoff_time { + replace_id = Some(*id); + break; + } else if s.timestamp >= newest { newest = s.timestamp; - newest_id = Some(*id); + replace_id = Some(*id); } } - let _ = sessions.incomplete.remove(newest_id.as_ref().unwrap()); + let _ = sessions.incomplete.remove(replace_id.as_ref().unwrap()); } sessions.incomplete.insert( @@ -688,149 +688,154 @@ impl Context { */ if counter != 1 { - return Ok(ReceiveResult::Ignored); - } else if let Some(session) = session { - match std::mem::replace(&mut *session.offer.lock().unwrap(), EphemeralOffer::None) { - EphemeralOffer::NoiseXKInit(mut boxed_offer) => { - let (alice_e_secret, metadata, noise_es, alice_hk_secret) = boxed_offer.as_mut(); + return Err(Error::OutOfCounterWindow); + } + + if let Some(session) = session { + let mut offer = session.offer.lock().unwrap(); + if let EphemeralOffer::NoiseXKInit(boxed_offer) = &*offer { + let (alice_e_secret, metadata, noise_es, alice_hk_secret) = boxed_offer.as_ref(); + 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( + noise_es.as_bytes(), + alice_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(), + &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)?; - 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( - noise_es.as_bytes(), - alice_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()); + // 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 + // it's an external add-on beyond the Noise spec. + let hk = pqc_kyber::decapsulate(&pkt.bob_hk_ciphertext, alice_hk_secret.as_bytes()) + .map_err(|_| Error::FailedAuthentication) + .map(|k| Secret(k))?; + let noise_es_ee_se_hk_psk = Secret(hmac_sha512( + &hmac_sha512( + noise_es_ee.as_bytes(), + app.get_local_s_keypair() + .agree(&bob_noise_e) + .ok_or(Error::FailedAuthentication)? + .as_bytes(), + ), + &hmac_sha512(session.psk.as_bytes(), hk.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(), - &message_nonce, - &pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START], - ), - ) { - return Err(Error::FailedAuthentication); - } + let noise_es_ee_se_hk_psk_hmac_key = + kbkdf::(noise_es_ee_se_hk_psk.as_bytes()); - // 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)?; + let reply_counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; + let reply_message_nonce = create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_ACK, reply_counter.get()); - // 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 - // it's an external add-on beyond the Noise spec. - let hk = pqc_kyber::decapsulate(&pkt.bob_hk_ciphertext, alice_hk_secret.as_bytes()) - .map_err(|_| Error::FailedAuthentication) - .map(|k| Secret(k))?; - let noise_es_ee_se_hk_psk = Secret(hmac_sha512( - &hmac_sha512( - noise_es_ee.as_bytes(), - app.get_local_s_keypair() - .agree(&bob_noise_e) - .ok_or(Error::FailedAuthentication)? - .as_bytes(), - ), - &hmac_sha512(session.psk.as_bytes(), hk.as_bytes()), - )); - - let noise_es_ee_se_hk_psk_hmac_key = kbkdf::( - noise_es_ee_se_hk_psk.as_bytes(), - ); - - let reply_counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; - let reply_message_nonce = create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_ACK, reply_counter.get()); - - // Create reply informing Bob of our static identity now that we've verified Bob and set - // up forward secrecy. Also return Bob's opaque note. - let mut reply_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; - reply_buffer[HEADER_SIZE] = SESSION_PROTOCOL_VERSION; - let mut reply_len = HEADER_SIZE + 1; - let mut reply_buffer_append = |b: &[u8]| { - let reply_len_new = reply_len + b.len(); - debug_assert!(reply_len_new <= MAX_NOISE_HANDSHAKE_SIZE); - reply_buffer[reply_len..reply_len_new].copy_from_slice(b); - reply_len = reply_len_new; - }; - let alice_s_public_blob = app.get_local_s_public_blob(); - assert!(alice_s_public_blob.len() <= (u16::MAX as usize)); - reply_buffer_append(&(alice_s_public_blob.len() as u16).to_le_bytes()); - reply_buffer_append(alice_s_public_blob); - if let Some(md) = metadata.as_ref() { - reply_buffer_append(&(md.len() as u16).to_le_bytes()); - reply_buffer_append(md.as_slice()); - } else { - 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(&SHA384::hash(hk.as_bytes())[..AES_CTR_NONCE_SIZE]); - ctr.crypt_in_place(&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. - let hmac_es_ee = hmac_sha384_2( - noise_es_ee_kex_hmac_key.as_bytes(), - &reply_message_nonce, - &reply_buffer[HEADER_SIZE..reply_len], - ); - reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee); - reply_len += HMAC_SHA384_SIZE; - - // Then attach the final HMAC permitting Bob to verify the authenticity of the whole - // key exchange. Bob won't be able to do this until he decrypts and parses Alice's - // identity, so the first HMAC is to let him authenticate that first. - let hmac_es_ee_se_hk_psk = hmac_sha384_2( - noise_es_ee_se_hk_psk_hmac_key.as_bytes(), - &reply_message_nonce, - &reply_buffer[HEADER_SIZE..reply_len], - ); - reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee_se_hk_psk); - reply_len += HMAC_SHA384_SIZE; - - // Learn Bob's session ID and the first session key. - { - let mut state = session.state.write().unwrap(); - let _ = state.remote_session_id.insert(bob_session_id); - let _ = state.keys[0].insert(SessionKey::new( - noise_es_ee_se_hk_psk, - current_time, - reply_counter.get(), - true, - false, - )); - state.current_key = 0; - } - - send_with_fragmentation( - |b| send(Some(&session), b), - &mut reply_buffer[..reply_len], - mtu, - PACKET_TYPE_ALICE_NOISE_XK_ACK, - Some(bob_session_id), - 0, - reply_counter.get(), - Some(&session.header_check_cipher), - )?; - - return Ok(ReceiveResult::Ok); + // Create reply informing Bob of our static identity now that we've verified Bob and set + // up forward secrecy. Also return Bob's opaque note. + let mut reply_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; + reply_buffer[HEADER_SIZE] = SESSION_PROTOCOL_VERSION; + let mut reply_len = HEADER_SIZE + 1; + let mut reply_buffer_append = |b: &[u8]| { + let reply_len_new = reply_len + b.len(); + debug_assert!(reply_len_new <= MAX_NOISE_HANDSHAKE_SIZE); + reply_buffer[reply_len..reply_len_new].copy_from_slice(b); + reply_len = reply_len_new; + }; + let alice_s_public_blob = app.get_local_s_public_blob(); + assert!(alice_s_public_blob.len() <= (u16::MAX as usize)); + reply_buffer_append(&(alice_s_public_blob.len() as u16).to_le_bytes()); + reply_buffer_append(alice_s_public_blob); + if let Some(md) = metadata.as_ref() { + reply_buffer_append(&(md.len() as u16).to_le_bytes()); + reply_buffer_append(md.as_ref()); } else { - return Err(Error::InvalidPacket); + 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(&SHA384::hash(hk.as_bytes())[..AES_CTR_NONCE_SIZE]); + ctr.crypt_in_place(&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. + let hmac_es_ee = hmac_sha384_2( + noise_es_ee_kex_hmac_key.as_bytes(), + &reply_message_nonce, + &reply_buffer[HEADER_SIZE..reply_len], + ); + reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee); + reply_len += HMAC_SHA384_SIZE; + + // Then attach the final HMAC permitting Bob to verify the authenticity of the whole + // key exchange. Bob won't be able to do this until he decrypts and parses Alice's + // identity, so the first HMAC is to let him authenticate that first. + let hmac_es_ee_se_hk_psk = hmac_sha384_2( + noise_es_ee_se_hk_psk_hmac_key.as_bytes(), + &reply_message_nonce, + &reply_buffer[HEADER_SIZE..reply_len], + ); + reply_buffer[reply_len..reply_len + HMAC_SHA384_SIZE].copy_from_slice(&hmac_es_ee_se_hk_psk); + reply_len += HMAC_SHA384_SIZE; + + // Clear the offer field since we're finished handling a response to our initial offer. + *offer = EphemeralOffer::None; + drop(offer); + + // Learn Bob's session ID and the first session key. + { + let mut state = session.state.write().unwrap(); + let _ = state.remote_session_id.insert(bob_session_id); + let _ = state.keys[0].insert(SessionKey::new( + noise_es_ee_se_hk_psk, + current_time, + reply_counter.get(), + true, + false, + )); + state.current_key = 0; + } + + send_with_fragmentation( + |b| send(Some(&session), b), + &mut reply_buffer[..reply_len], + mtu, + PACKET_TYPE_ALICE_NOISE_XK_ACK, + Some(bob_session_id), + 0, + reply_counter.get(), + Some(&session.header_check_cipher), + )?; + + return Ok(ReceiveResult::Ok); + } else { + return Err(Error::InvalidPacket); } - _ => return Ok(ReceiveResult::Ignored), + } else { + return Ok(ReceiveResult::Ignored); } } else { return Err(Error::SessionNotEstablished); @@ -1166,7 +1171,7 @@ fn send_with_fragmentation( } impl SessionKey { - fn new(key: Secret<64>, current_time: i64, current_counter: u64, confirmed: bool, role_is_bob: bool) -> Self { + fn new(key: Secret, current_time: i64, current_counter: u64, confirmed: bool, role_is_bob: bool) -> Self { let a2b = kbkdf::(key.as_bytes()); let b2a = kbkdf::(key.as_bytes()); let (receive_key, send_key) = if role_is_bob { @@ -1175,6 +1180,7 @@ impl SessionKey { (b2a, a2b) }; Self { + ratchet_key: kbkdf::(key.as_bytes()), receive_key, send_key, receive_cipher_pool: Mutex::new(Vec::with_capacity(2)),