diff --git a/.gitignore b/.gitignore index 7dd2bd4..b3dabfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,4 @@ /target -/**/target -/**/Cargo.lock - -.DS_* -.Icon* -._* -*.o -*.so -*.dylib -*.dSYM -*.a -/.idea -/.nova -*.secret +perf*.data +perf*.old +*.svg diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e1e0058 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,315 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +dependencies = [ + "zeroize", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-oid" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "openssl-sys" +version = "0.9.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "elliptic-curve", + "primeorder", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "pqc_kyber" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5dd33c0b42d244b01ab4f6cabaeb03c3b875017780fb3903b53b9c91fb6663" +dependencies = [ + "rand_core", +] + +[[package]] +name = "primeorder" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zssp" +version = "0.0.3" +dependencies = [ + "arrayvec", + "hmac", + "openssl-sys", + "p384", + "pqc_kyber", + "rand_core", + "sha2", + "zeroize", +] diff --git a/Cargo.toml b/Cargo.toml index 74aa8d5..c7ec4f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,23 @@ name = "zssp" path = "src/lib.rs" doc = true +[profile.bench] +debug = true + [dependencies] -pqc_kyber = { version = "0.6.0", default-features = false, features = ["kyber1024", "std"] } -rand_core = "0.6.4" -hex-literal = "0.4.1" +rand_core = { version = "0.6.4" } +zeroize = { version = "1.6.0" } +arrayvec = { version = "0.7.4", default-features = false, features = ["std", "zeroize"] } + +pqc_kyber = { version = "0.7.0", default-features = false, features = ["kyber1024", "std"], optional = true } +p384 = { version = "0.13.0", default-features = false, features = ["ecdh"], optional = true } +sha2 = { version = "0.10.7", default-features = false, optional = true } +hmac = { version = "0.12.1", default-features = false, optional = true } +openssl-sys = { version = "0.9.91", default-features = false, optional = true } + +[features] +default = ["debug", "default-crypto"] +default-crypto = ["p384", "sha2", "pqc_kyber", "openssl-sys", "rand_core/getrandom"] +sha2 = ["dep:sha2", "dep:hmac"] +logging = [] +debug = ["logging"] diff --git a/README.md b/README.md index 39d3988..95f2984 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ Further information can be found in the ZSSP whitepaper (pending official releas * **Forward Secret Identity Hiding**: An attacker with the static private key of one or more parties cannot determine the identity of everyone they have previously communicated with. * **Quantum Forward Secret**: A quantum computer powerful enough to break Elliptic-curve cryptography is not sufficient in order to decrypt recordings of messages sent between parties. * **Ratcheted Forward Secrecy**: In order to break forward secrecy an attacker must record and break every single key exchange two parties perform, in order, starting from the first time they began communicating. Improves secrecy under weak or compromised RNG. +* **Silence is a Virtue**: A server running the protocol can be configured in such a way that it will not respond to an unauthenticated, anonymous or replayed message. * **Key-Compromise Impersonation**: The attacker has a memory image of a single party, and attempts to create a brand new session with that party, pretending to be someone else. * **Compromise-and-Impersonate**: The attacker has a memory image of a single party, and attempts to impersonate them on a brand new session with the other party. * **Single Key-Compromise MitM**: The attacker has a memory image of a single party, and attempts to become a Man-in-the-Middle between them and any other party. * **Double Key-Compromise MitM**: The attacker has a memory image of both parties, and attempts to become a Man-in-the-Middle between them. -* **Silence is a Virtue**: A server running the protocol can be configured in such a way that it will not respond to an unauthenticated, anonymous or replayed message. * **Supports Fragmentation**: Transmission data can be fragmented into smaller units to support jumbo-sized data or MTU discovery. * **FIPS Compliant**: The protocol uses FIPS approved cryptographic algorithms. * **Small Code Footprint**: The Codebase implementing the protocol can be easily audited by anyone on the internet. diff --git a/examples/basic_test.rs b/examples/basic_test.rs new file mode 100644 index 0000000..1c5534c --- /dev/null +++ b/examples/basic_test.rs @@ -0,0 +1,406 @@ +use std::collections::HashMap; +use std::iter::ExactSizeIterator; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; + +use rand_core::OsRng; +use rand_core::RngCore; + +use zssp::application::{ + AcceptAction, ApplicationLayer, CryptoLayer, IncomingSessionAction, RatchetState, RatchetStates, RatchetUpdate, + Settings, RATCHET_SIZE, +}; +use zssp::crypto::P384KeyPair; +use zssp::crypto_impl::*; +use zssp::result::ReceiveError; +use zssp::Session; + +const TEST_MTU: usize = 1500; + +struct TestApplication { + time: Instant, + name: &'static str, + ratchets: Mutex, +} + +struct Ratchets { + rf_map: HashMap<[u8; RATCHET_SIZE], RatchetState>, + peer_map: HashMap, +} +impl Ratchets { + fn new() -> Self { + Self { rf_map: HashMap::new(), peer_map: HashMap::new() } + } +} + +#[allow(unused)] +impl CryptoLayer for TestApplication { + const SETTINGS: Settings = Settings { + initial_offer_timeout: Settings::INITIAL_OFFER_TIMEOUT_MS, + rekey_timeout: 60 * 1000, + rekey_after_time: 3000, + rekey_time_max_jitter: 1000, + rekey_after_key_uses: Settings::REKEY_AFTER_KEY_USES, + resend_time: 250, + fragment_assembly_timeout: Settings::FRAGMENT_ASSEMBLY_TIMEOUT_MS, + }; + + type Rng = OsRng; + type PrpEnc = OpenSSLAes256Enc; + type PrpDec = OpenSSLAes256Dec; + type Aead = OpenSSLAesGcm; + type AeadPool = OpenSSLAesGcmPool; + type Hash = CrateSha512; + type Hmac = CrateHmacSha512; + type PublicKey = CrateP384PublicKey; + type KeyPair = CrateP384KeyPair; + type Kem = CrateKyber1024PrivateKey; + + type SessionData = u128; + + type IncomingPacketBuffer = Vec; +} +#[allow(unused)] +impl ApplicationLayer for &TestApplication { + type Crypto = TestApplication; + + fn incoming_session(&mut self) -> IncomingSessionAction { + IncomingSessionAction::Challenge + } + + fn hello_requires_recognized_ratchet(&mut self) -> bool { + false + } + + fn initiator_disallows_downgrade(&mut self, session: &Arc>) -> bool { + true + } + + fn check_accept_session( + &mut self, + remote_static_key: &CrateP384PublicKey, + identity: &[u8], + ) -> AcceptAction { + AcceptAction { + session_data: Some(1), + responder_disallows_downgrade: true, + responder_silently_rejects: false, + } + } + + fn restore_by_fingerprint(&mut self, ratchet_fingerprint: &[u8; RATCHET_SIZE]) -> Result, ()> { + let ratchets = self.ratchets.lock().unwrap(); + Ok(ratchets.rf_map.get(ratchet_fingerprint).cloned()) + } + + fn restore_by_identity( + &mut self, + remote_static_key: &CrateP384PublicKey, + session_data: &u128, + ) -> Result, ()> { + let ratchets = self.ratchets.lock().unwrap(); + Ok(ratchets.peer_map.get(session_data).cloned()) + } + + fn save_ratchet_state( + &mut self, + remote_static_key: &CrateP384PublicKey, + session_data: &u128, + update_data: RatchetUpdate<'_>, + ) -> Result<(), ()> { + let mut ratchets = self.ratchets.lock().unwrap(); + ratchets.peer_map.insert(*session_data, update_data.to_states()); + + if let Some(rf) = update_data.added_fingerprint() { + ratchets.rf_map.insert(*rf, update_data.state1.clone()); + println!("[{}] new ratchet #{}", self.name, update_data.state1.chain_len); + } + if let Some(rf) = update_data.deleted_fingerprint1() { + ratchets.rf_map.remove(rf); + } + if let Some(rf) = update_data.deleted_fingerprint2() { + ratchets.rf_map.remove(rf); + } + Ok(()) + } + + fn time(&mut self) -> i64 { + self.time.elapsed().as_millis() as i64 + } + + fn event_log(&mut self, event: zssp::LogEvent) { + println!(">[{}] {:?}", self.name, event); + } +} + +#[allow(unused)] +fn alice_main( + run: &AtomicBool, + packet_success_rate: u32, + alice_app: &TestApplication, + alice_out: mpsc::SyncSender>, + alice_in: mpsc::Receiver>, + recursive_out: mpsc::SyncSender>, + alice_keypair: CrateP384KeyPair, + bob_pubkey: CrateP384PublicKey, +) { + let startup_time = std::time::Instant::now(); + let context = zssp::Context::::new(alice_keypair, OsRng); + let mut next_service = startup_time.elapsed().as_millis() as i64 + 500; + let test_data = [1u8; TEST_MTU * 10]; + let mut up = false; + let mut alice_session = None; + + while run.load(Ordering::Relaxed) { + if alice_session.is_none() { + up = false; + alice_session = Some( + context + .open( + alice_app, + |b| alice_out.send(b.to_vec()).is_ok(), + TEST_MTU, + bob_pubkey.clone(), + 0, + &[], + ) + .unwrap(), + ); + println!("[alice] opening session"); + } + let current_time = startup_time.elapsed().as_millis() as i64; + loop { + let pkt = alice_in.try_recv(); + if let Ok(pkt) = pkt { + if OsRng.next_u32() <= packet_success_rate { + use zssp::result::ReceiveOk::*; + use zssp::result::SessionEvent::*; + let mut output_data = Vec::new(); + match context.receive( + alice_app, + |b| alice_out.send(b.to_vec()).is_ok(), + TEST_MTU, + |_| Some((|b: &mut [u8]| alice_out.send(b.to_vec()).is_ok(), TEST_MTU)), + &0, + pkt, + &mut output_data, + ) { + Ok(Unassociated) => { + //println!("[alice] ok"); + } + Ok(Session(_, event)) => match event { + Established => { + up = true; + } + Data => { + assert!(!output_data.is_empty()); + //println!("[alice] received {}", data.len()); + } + Control => (), + _ => panic!(), + }, + Err(e) => { + println!("[alice] ERROR {:?}", e); + if let ReceiveError::ByzantineFault { unnatural, .. } = e { + assert!(!unnatural) + } + } + } + //} else if OsRng.next_u32() | 1 > 0 { + // let _ = recursive_out.send(pkt); + } + } else { + break; + } + } + + if up { + context + .send( + alice_session.as_ref().unwrap(), + |b| alice_out.send(b.to_vec()).is_ok(), + &mut [0u8; TEST_MTU], + &test_data[..1400 + ((OsRng.next_u64() as usize) % (test_data.len() - 1400))], + ) + .unwrap(); + } else { + thread::sleep(Duration::from_millis(10)); + } + // TODO: we need to more comprehensively test if re-opening the session works + if OsRng.next_u32() <= ((u32::MAX as f64) * 0.0000005) as u32 { + alice_session = None; + } + + if current_time >= next_service { + next_service = current_time + + context.service(alice_app, |_| { + Some((|b: &mut [u8]| alice_out.send(b.to_vec()).is_ok(), TEST_MTU)) + }); + } + } +} + +#[allow(unused)] +fn bob_main( + run: &AtomicBool, + packet_success_rate: u32, + bob_app: &TestApplication, + bob_out: mpsc::SyncSender>, + bob_in: mpsc::Receiver>, + recursive_out: mpsc::SyncSender>, + bob_keypair: CrateP384KeyPair, +) { + let startup_time = std::time::Instant::now(); + let context = zssp::Context::::new(bob_keypair, OsRng); + let mut last_speed_metric = startup_time.elapsed().as_millis() as i64; + let mut next_service = last_speed_metric + 500; + let mut transferred = 0u64; + + let mut bob_session = None; + + while run.load(Ordering::Relaxed) { + let pkt = bob_in.recv_timeout(Duration::from_millis(100)); + let current_time = startup_time.elapsed().as_millis() as i64; + + if let Ok(pkt) = pkt { + if OsRng.next_u32() <= packet_success_rate { + use zssp::result::ReceiveOk::*; + use zssp::result::SessionEvent::*; + let mut output_data = Vec::new(); + match context.receive( + bob_app, + |b| bob_out.send(b.to_vec()).is_ok(), + TEST_MTU, + |_| Some((|b: &mut [u8]| bob_out.send(b.to_vec()).is_ok(), TEST_MTU)), + &0, + pkt, + &mut output_data, + ) { + Ok(Unassociated) => {} + Ok(Session(s, event)) => match event { + NewSession | NewDowngradedSession => { + println!("[bob] new session, took {}s", current_time as f32 / 1000.0); + let _ = bob_session.replace(s); + } + Data => { + assert!(!output_data.is_empty()); + //println!("[bob] received {}", output_data.len()); + transferred += output_data.len() as u64 * 2; // *2 because we are also sending this many bytes back + context + .send( + &s, + |b| bob_out.send(b.to_vec()).is_ok(), + &mut [0u8; TEST_MTU], + &output_data, + ) + .unwrap(); + } + Control => (), + _ => panic!(), + }, + Err(e) => { + println!("[bob] ERROR {:?}", e); + if let ReceiveError::ByzantineFault { unnatural, .. } = e { + assert!(!unnatural) + } + } + } + //} else if OsRng.next_u32() | 1 > 0 { + // let _ = recursive_out.try_send(pkt); + } + } + + let speed_metric_elapsed = current_time - last_speed_metric; + if speed_metric_elapsed >= 10000 { + last_speed_metric = current_time; + println!( + "[bob] throughput: {} MiB/sec (combined input and output)", + ((transferred as f64) / 1048576.0) / ((speed_metric_elapsed as f64) / 1000.0) + ); + transferred = 0; + } + + if current_time >= next_service { + next_service = current_time + + context.service(bob_app, |_| { + Some((|b: &mut [u8]| bob_out.send(b.to_vec()).is_ok(), TEST_MTU)) + }); + } + } +} + +fn core(time: u64, packet_success_rate: u32) { + let run = &AtomicBool::new(true); + + let alice_keypair = CrateP384KeyPair::generate(&mut OsRng); + let alice_app = TestApplication { + time: Instant::now(), + name: "alice", + ratchets: Mutex::new(Ratchets::new()), + }; + let bob_keypair = CrateP384KeyPair::generate(&mut OsRng); + let bob_pubkey = bob_keypair.public_key(); + let bob_app = TestApplication { + time: Instant::now(), + name: "bob", + ratchets: Mutex::new(Ratchets::new()), + }; + + let (alice_out, bob_in) = mpsc::sync_channel::>(256); + let (bob_out, alice_in) = mpsc::sync_channel::>(256); + + thread::scope(|ts| { + { + let alice_out = alice_out.clone(); + let bob_out = bob_out.clone(); + ts.spawn(move || { + alice_main( + run, + packet_success_rate, + &alice_app, + alice_out, + alice_in, + bob_out, + alice_keypair, + bob_pubkey, + ) + }); + } + ts.spawn(move || { + bob_main( + run, + packet_success_rate, + &bob_app, + bob_out, + bob_in, + alice_out, + bob_keypair, + ) + }); + + thread::sleep(Duration::from_secs(time)); + + run.store(false, Ordering::SeqCst); + println!("finished"); + }); +} + +fn main() { + let args = std::env::args(); + let packet_success_rate = if args.len() <= 1 { + let default_success_rate = 1.0; + ((u32::MAX as f64) * default_success_rate) as u32 + } else { + ((u32::MAX as f64) * f64::from_str(args.last().unwrap().as_str()).unwrap()) as u32 + }; + + core(60 * 60, packet_success_rate) +} + +#[test] +fn test_main() { + core(2, u32::MAX / 2) +} diff --git a/examples/benchmark.rs b/examples/benchmark.rs new file mode 100644 index 0000000..c570df9 --- /dev/null +++ b/examples/benchmark.rs @@ -0,0 +1,324 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; + +use arrayvec::ArrayVec; +use rand_core::OsRng; +use rand_core::RngCore; + +use zssp::application::{ + AcceptAction, ApplicationLayer, IncomingSessionAction, RatchetState, RatchetStates, RatchetUpdate, RATCHET_SIZE, +}; +use zssp::crypto::P384KeyPair; +use zssp::crypto_impl::*; +use zssp::result::ReceiveError; +use zssp::Session; + +const TEST_MTU: usize = 1500; + +struct TestApplication { + time: Instant, +} + +/// We have to pool allocations or else variations in the speed of the memory allocator will bias +/// our performance stats. +struct PooledVec(Vec); +static POOL: Mutex>> = Mutex::new(Vec::new()); +fn alloc(b: &[u8]) -> PooledVec { + let mut p = POOL.lock().unwrap(); + let mut v = p.pop().unwrap_or_default(); + v.extend(b); + PooledVec(v) +} +impl Drop for PooledVec { + fn drop(&mut self) { + let mut p = POOL.lock().unwrap(); + let mut v = Vec::new(); + std::mem::swap(&mut self.0, &mut v); + v.clear(); + p.push(v); + } +} +impl AsMut<[u8]> for PooledVec { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut() + } +} +impl AsRef<[u8]> for PooledVec { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[allow(unused)] +impl DefaultCrypto for TestApplication { + type SessionData = (); + type IncomingPacketBuffer = PooledVec; +} +#[allow(unused)] +impl ApplicationLayer for &TestApplication { + type Crypto = TestApplication; + + fn incoming_session(&mut self) -> IncomingSessionAction { + IncomingSessionAction::Allow + } + + fn hello_requires_recognized_ratchet(&mut self) -> bool { + false + } + + fn initiator_disallows_downgrade(&mut self, session: &Arc>) -> bool { + false + } + + fn check_accept_session( + &mut self, + remote_static_key: &CrateP384PublicKey, + identity: &[u8], + ) -> AcceptAction { + AcceptAction { + session_data: Some(()), + responder_disallows_downgrade: true, + responder_silently_rejects: false, + } + } + + fn restore_by_fingerprint(&mut self, ratchet_fingerprint: &[u8; RATCHET_SIZE]) -> Result, ()> { + Ok(None) + } + + fn restore_by_identity( + &mut self, + remote_static_key: &CrateP384PublicKey, + session_data: &(), + ) -> Result, ()> { + Ok(None) + } + + fn save_ratchet_state( + &mut self, + remote_static_key: &CrateP384PublicKey, + session_data: &(), + update_data: RatchetUpdate<'_>, + ) -> Result<(), ()> { + Ok(()) + } + + fn time(&mut self) -> i64 { + self.time.elapsed().as_millis() as i64 + } +} + +#[allow(unused)] +fn alice_main( + run: &AtomicBool, + alice_app: &TestApplication, + alice_out: mpsc::SyncSender, + alice_in: mpsc::Receiver, + alice_keypair: CrateP384KeyPair, + bob_pubkey: CrateP384PublicKey, +) { + let startup_time = std::time::Instant::now(); + let context = zssp::Context::::new(alice_keypair, OsRng); + let mut next_service = startup_time.elapsed().as_millis() as i64 + 500; + let test_data = [1u8; TEST_MTU * 10]; + let mut up = false; + let mut output_data = ArrayVec::::new(); + + let alice_session = Some( + context + .open( + alice_app, + |b| alice_out.send(alloc(b)).is_ok(), + TEST_MTU, + bob_pubkey.clone(), + (), + &[], + ) + .unwrap(), + ); + println!("[alice] opening session"); + while run.load(Ordering::Relaxed) { + let current_time = startup_time.elapsed().as_millis() as i64; + loop { + let pkt = alice_in.try_recv(); + if let Ok(pkt) = pkt { + use zssp::result::ReceiveOk::*; + use zssp::result::SessionEvent::*; + output_data.clear(); + match context.receive( + alice_app, + |b| alice_out.send(alloc(b)).is_ok(), + TEST_MTU, + |_| Some((|b: &mut [u8]| alice_out.send(alloc(b)).is_ok(), TEST_MTU)), + &0, + pkt, + &mut output_data, + ) { + Ok(Unassociated) => { + //println!("[alice] ok"); + } + Ok(Session(_, event)) => match event { + Established => { + up = true; + } + Data => { + assert!(!output_data.is_empty()); + //println!("[alice] received {}", data.len()); + } + Control => (), + _ => panic!(), + }, + Err(e) => { + println!("[alice] ERROR {:?}", e); + if let ReceiveError::ByzantineFault { unnatural, .. } = e { + assert!(!unnatural) + } + } + } + } else { + break; + } + } + + if up { + context + .send( + alice_session.as_ref().unwrap(), + |b| alice_out.send(alloc(b)).is_ok(), + &mut [0u8; TEST_MTU], + &test_data[..1400 + ((OsRng.next_u64() as usize) % (test_data.len() - 1400))], + ) + .unwrap(); + } else { + thread::sleep(Duration::from_millis(10)); + } + + if current_time >= next_service { + next_service = current_time + + context.service(alice_app, |_| { + Some((|b: &mut [u8]| alice_out.send(alloc(b)).is_ok(), TEST_MTU)) + }); + } + } +} + +#[allow(unused)] +fn bob_main( + run: &AtomicBool, + bob_app: &TestApplication, + bob_out: mpsc::SyncSender, + bob_in: mpsc::Receiver, + bob_keypair: CrateP384KeyPair, +) { + let startup_time = std::time::Instant::now(); + let context = zssp::Context::::new(bob_keypair, OsRng); + let mut last_speed_metric = startup_time.elapsed().as_millis() as i64; + let mut next_service = last_speed_metric + 500; + let mut transferred = 0u64; + let mut output_data = ArrayVec::::new(); + + let mut bob_session = None; + + while run.load(Ordering::Relaxed) { + let pkt = bob_in.recv_timeout(Duration::from_millis(100)); + let current_time = startup_time.elapsed().as_millis() as i64; + + if let Ok(pkt) = pkt { + use zssp::result::ReceiveOk::*; + use zssp::result::SessionEvent::*; + output_data.clear(); + match context.receive( + bob_app, + |b| bob_out.send(alloc(b)).is_ok(), + TEST_MTU, + |_| Some((|b: &mut [u8]| bob_out.send(alloc(b)).is_ok(), TEST_MTU)), + &0, + pkt, + &mut output_data, + ) { + Ok(Unassociated) => {} + Ok(Session(s, event)) => match event { + NewSession | NewDowngradedSession => { + println!("[bob] new session, took {}s", current_time as f32 / 1000.0); + let _ = bob_session.replace(s); + } + Data => { + assert!(!output_data.is_empty()); + //println!("[bob] received {}", output_data.len()); + transferred += output_data.len() as u64 * 2; // *2 because we are also sending this many bytes back + context + .send( + &s, + |b| bob_out.send(alloc(b)).is_ok(), + &mut [0u8; TEST_MTU], + &output_data, + ) + .unwrap(); + } + Control => (), + _ => panic!(), + }, + Err(e) => { + println!("[bob] ERROR {:?}", e); + if let ReceiveError::ByzantineFault { unnatural, .. } = e { + assert!(!unnatural) + } + } + } + } + + let speed_metric_elapsed = current_time - last_speed_metric; + if speed_metric_elapsed >= 10000 { + last_speed_metric = current_time; + println!( + "[bob] throughput: {} MiB/sec (combined input and output)", + ((transferred as f64) / 1048576.0) / ((speed_metric_elapsed as f64) / 1000.0) + ); + transferred = 0; + } + + if current_time >= next_service { + next_service = current_time + + context.service(bob_app, |_| { + Some((|b: &mut [u8]| bob_out.send(alloc(b)).is_ok(), TEST_MTU)) + }); + } + } +} + +fn core(time: u64) { + let run = &AtomicBool::new(true); + + let alice_keypair = CrateP384KeyPair::generate(&mut OsRng); + let alice_app = TestApplication { time: Instant::now() }; + let bob_keypair = CrateP384KeyPair::generate(&mut OsRng); + let bob_pubkey = bob_keypair.public_key(); + let bob_app = TestApplication { time: Instant::now() }; + + let (alice_out, bob_in) = mpsc::sync_channel::(256); + let (bob_out, alice_in) = mpsc::sync_channel::(256); + + thread::scope(|ts| { + { + let alice_out = alice_out.clone(); + ts.spawn(move || alice_main(run, &alice_app, alice_out, alice_in, alice_keypair, bob_pubkey)); + } + ts.spawn(move || bob_main(run, &bob_app, bob_out, bob_in, bob_keypair)); + + thread::sleep(Duration::from_secs(time)); + + run.store(false, Ordering::SeqCst); + println!("finished"); + }); +} + +fn main() { + core(20) +} + +#[test] +fn test_main() { + core(2) +} diff --git a/rustfmt.toml b/rustfmt.toml index 3a3929c..9c9fedd 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 150 +max_width = 120 edition = "2021" newline_style = "Unix" struct_lit_width = 60 diff --git a/src/antireplay.rs b/src/antireplay.rs new file mode 100644 index 0000000..81bc5a0 --- /dev/null +++ b/src/antireplay.rs @@ -0,0 +1,24 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +pub struct Window([AtomicU64; L]); + +impl Window { + pub fn new() -> Self { + Self(std::array::from_fn(|_| AtomicU64::new(0))) + } + /// Check the window without mutating state. + pub fn check(&self, counter: u64) -> bool { + let slot = &self.0[(counter as usize) % self.0.len()]; + let counter = counter.wrapping_add(1); + let prev_counter = slot.load(Ordering::Relaxed); + prev_counter < counter && counter.wrapping_sub(prev_counter) <= MAX + } + /// Update the window, returning true if the packet is still valid. + /// This should only be called after the packet is authenticated. + pub fn update(&self, counter: u64) -> bool { + let slot = &self.0[(counter as usize) % self.0.len()]; + let counter = counter.wrapping_add(1); + let prev_counter = slot.fetch_max(counter, Ordering::Relaxed); + prev_counter < counter && counter.wrapping_sub(prev_counter) <= MAX + } +} diff --git a/src/application.rs b/src/application.rs new file mode 100644 index 0000000..3246920 --- /dev/null +++ b/src/application.rs @@ -0,0 +1,274 @@ +use rand_core::{CryptoRng, RngCore}; +use std::sync::Arc; + +use crate::crypto::*; +use crate::zeta::Session; + +pub use crate::proto::RATCHET_SIZE; +pub use crate::ratchet_state::*; + +/// A container for a vast majority of the dynamic settings within ZSSP, including all time-based settings. +/// If the user wishes to measure time in units other than milliseconds for some reason, then they can +/// create an adjusted version of this struct with those units, and use it instead of the default. +pub struct Settings { + /// Timeout for how long Alice should wait for Bob to confirm that the Noise_XK handshake + /// was completed successfully. The handshake attempt will be assumed as failed and + /// restarted if Bob does not respond by this cut-off. + pub initial_offer_timeout: u64, + /// Timeout for how long ZSSP should wait before expiring and closing a session when it has + /// lingered in certain states for too long, primarily the rekeying states. + /// If a remote peer does not send the correct information to rekey a session before this + /// timeout then the session will close. + pub rekey_timeout: u64, + /// How long until rekeying should occur for each new session key. + pub rekey_after_time: u64, + /// Maximum random jitter to subtract from the rekey after time timer. + /// Must be greater than 0. + /// This prevents rekeying from occurring predictably on the hour, so traffic analysis is harder. + pub rekey_time_max_jitter: u64, + /// How many key uses may occur before the session starts attempting to rekey. + /// The session will forceably close at 2^32 key uses so it is recommended this value be smaller. + pub rekey_after_key_uses: u64, + /// Retry interval for outgoing connection initiation or rekey attempts. + /// + /// Retry attempts will be no more often than this, but the delay may end up being + /// slightly more in some cases based on the rate of calls to `service`. + pub resend_time: u64, + /// How long fragments are allowed to linger in the defragmentation buffer before they are dropped. + /// This implementation of a defrag buffer only bounds memory consumption based on this value. + pub fragment_assembly_timeout: u64, +} +impl Settings { + /// Default value for the `initial_offer_timeout`. + /// The default value is 10 seconds in ms. + pub const INITIAL_OFFER_TIMEOUT_MS: u64 = 10 * 1000; + /// Default value for the `rekey_timeout`. + /// The default value is 1 minute in ms. + pub const REKEY_TIMEOUT_MS: u64 = 60 * 1000; + /// Default value for the `rekey_after_time`. + /// The default value is 1 hour in ms. + pub const REKEY_AFTER_TIME_MS: u64 = 60 * 60 * 1000; + /// Default value for the `rekey_time_max_jitter`. + /// The default is 10 minutes in ms. + pub const REKEY_AFTER_TIME_MAX_JITTER_MS: u64 = 10 * 60 * 1000; + /// Default value for the `rekey_after_key_uses`. + /// The default is 2^30. + pub const REKEY_AFTER_KEY_USES: u64 = 1 << 30; + /// Default value for the `resend_time`. + /// The default is 1 second in ms. + pub const RESEND_TIME: u64 = 1000; + /// Default value for the `fragment_assembly_timeout`. + /// The default is 5 seconds in ms. + pub const FRAGMENT_ASSEMBLY_TIMEOUT_MS: u64 = 5 * 1000; + /// Create an instance of Settings with all default values. + /// These defaults are in units of milliseconds, so if these defaults are used, `App::time` + /// must return timestamps in unts of milliseconds as well. + pub const fn new_ms() -> Self { + Self { + initial_offer_timeout: Self::INITIAL_OFFER_TIMEOUT_MS, + rekey_timeout: Self::REKEY_TIMEOUT_MS, + rekey_after_time: Self::REKEY_AFTER_TIME_MS, + rekey_time_max_jitter: Self::REKEY_AFTER_TIME_MAX_JITTER_MS, + rekey_after_key_uses: Self::REKEY_AFTER_KEY_USES, + resend_time: Self::RESEND_TIME, + fragment_assembly_timeout: Self::FRAGMENT_ASSEMBLY_TIMEOUT_MS, + } + } +} +impl Default for Settings { + fn default() -> Self { + Self::new_ms() + } +} + +/// Trait to implement to integrate the session into an application. +/// +/// Templating the session on this trait lets the code here be almost entirely transport, OS, +/// and use case independent. +/// +/// The constants exposed in this trait can be redefined from their defaults to change rekey +/// and negotiation timeout behavior. Both sides of a ZSSP session **must** have these constants +/// set to the same values. Changing these constants is generally discouraged unless you know +/// what you are doing. +pub trait CryptoLayer: Sized { + /// These are constants that can be redefined from their defaults to change rekey + /// and negotiation timeout behavior. If two sides of a ZSSP session have different constants, + /// the protocol will tend to default to the smaller constants. + const SETTINGS: Settings = Settings::new_ms(); + + type Rng: CryptoRng + RngCore; + + /// The implementation of AES-256 Encryption that ZSSP should use. + /// + /// FIPS compliance requires use of a FIPS certified implementation. + type PrpEnc: Aes256Enc; + /// The implementation of AES-256 Decryption that ZSSP should use. + /// + /// FIPS compliance requires use of a FIPS certified implementation. + type PrpDec: Aes256Dec; + + type Aead: LowThroughputAesGcm; + type AeadPool: HighThroughputAesGcmPool; + + /// The implementation of SHA-512 that ZSSP should use. + /// + /// FIPS compliance requires use of a FIPS certified implementation. + type Hash: Sha512Hash; + type Hmac: Sha512Hmac; + /// The implementation of P-384 public keys that ZSSP should use. + /// + /// FIPS compliance requires a FIPS certified implementation. + type PublicKey: P384PublicKey; + /// The implementation of P-384 private keys that ZSSP should use. + /// + /// FIPS compliance requires use of a FIPS certified implementation. + type KeyPair: P384KeyPair; + /// The implementation of Kyber1024 that ZSSP should use. + /// + /// No implementation of Kyber1024 can be FIPS certified, but this is not required + /// for ZSSP to achieve FIPS compliance. + type Kem: Kyber1024PrivateKey; + + /// Type for arbitrary opaque object for use by the application that is attached to + /// each session. + type SessionData; + + /// Data type for incoming packet buffers. + /// + /// This can be something like `Vec` or `Box<[u8]>` or it can be something like a pooled + /// reusable buffer that automatically returns to its pool when ZSSP is done with it. ZSSP may + /// hold these for a short period of time when assembling fragmented packets on the receive + /// path. + type IncomingPacketBuffer: AsRef<[u8]> + AsMut<[u8]>; +} + +pub trait ApplicationLayer: Sized { + type Crypto: CryptoLayer; + + /// Should return the current time in milliseconds. Does not have to be monotonic, nor synced + /// with remote peers (although both of these properties would help reliability slightly). + /// Used to determine if any current handshakes should be resent or timed-out, or if a session + /// should rekey. + fn time(&mut self) -> i64; + + fn incoming_session(&mut self) -> IncomingSessionAction; + /// This function will be called whenever Alice's initial Hello packet contains the empty ratchet + /// fingerprint. Brand new peers will always connect to Bob with the empty ratchet, but from + /// then on they should be using non-empty ratchet states. + /// + /// If this returns false, we will attempt to connect to Alice with the empty ratchet state. + /// If this returns true, Alice's connection will be silently dropped. + /// If this function is configured to always return true, it means peers will not be able to + /// connect to us unless they had a prior-established ratchet key with us. This is the best way + /// for the paranoid to enforce a manual allow-list. + fn hello_requires_recognized_ratchet(&mut self) -> bool; + /// This function is called if we, as Alice, attempted to open a session with Bob using a + /// non-empty ratchet key, but Bob does not have this ratchet key and wants to downgrade + /// to the zero ratchet key. + /// + /// If it returns true Alice will downgrade their ratchet state to emtpy, potentially ending + /// their current ratchet chain. + /// If it returns false then we will consider Bob as having failed authentication, and this + /// packet will be dropped. The session will continue attempting to connect to Bob. + /// + /// This function must deterministically return either true or false for a given session. + /// + /// It is a bad sign that Bob has somehow forgotten Alice's ratchet key, it either means at + /// least one party is misconfigured and got their ratchet keys corrupted or lost, or Bob has + /// been compromised and is being impersonated. An attacker must at least have Bob's private + /// static key to be able to ask Alice to downgrade. + fn initiator_disallows_downgrade(&mut self, session: &Arc>) -> bool; + /// Function to accept sessions after final negotiation. + /// The second argument is the identity that the remote peer sent us. The application + /// must verify this identity is associated with the remote peer's static key. + /// To prevent desync, if this function specifies that we should connect, no other open session + /// with the same remote peer must exist. Drop or call expire on any pre-existing sessions + /// before returning. + fn check_accept_session( + &mut self, + remote_static_key: &::PublicKey, + identity: &[u8], + ) -> AcceptAction; + + /// Lookup a specific ratchet state based on its ratchet fingerprint. + /// This function will be called whenever Alice attempts to connect to us with a non-empty + /// ratchet fingerprint. + /// + /// If a ratchet state with a matching fingerprint could not be found, this function should + /// return `Ok(None)`. + fn restore_by_fingerprint(&mut self, ratchet_fingerprint: &[u8; RATCHET_SIZE]) -> Result, ()>; + /// Lookup the specific ratchet states based on the identity of the peer being communicated with. + /// This function will be called whenever Alice attempts to open a session, or Bob attempts + /// to verify Alice's identity. + /// + /// If the peer's ratchet states could not be could, this function should return + /// `RatchetState::new_initial_states()`. + /// + /// If a one-time-password has been pre-shared with this peer, `RatchetState::new_otp_states(...)` + /// should be pre-saved to the storage backend as if it is a normal ratchet state. + /// This is to ensure it can both be restored and eventually deleted when it is used. + /// + /// This function is not responsible for deciding whether or not to connect to this remote peer. + /// Filtering peers should be done by the caller to `Context::open` as well as by the + /// function `ApplicationLayer::check_accept_session`. + fn restore_by_identity( + &mut self, + remote_static_key: &::PublicKey, + session_data: &::SessionData, + ) -> Result, ()>; + /// Atomically commit the update specified by `update_data` to storage, or return an error if + /// the update could not be made. + /// The implementor is free to choose how to apply these updates to storage. + /// + /// If this returns `Err(IoError)`, the packet which triggered this function to be called will be + /// dropped, and no session state will be mutated, preserving synchronization. The remote peer + /// will eventually resend that packet and so this function will be called again. + /// + /// If persistent storage is supported, this function should not return until the ratchet state + /// is saved, otherwise it is possible, albeit unlikely, for a sudden restart of the local + /// machine to put our ratchet state out of sync with the remote peer. If this happens the only + /// fix is to reset both ratchet keys to empty. + /// + /// This function may also save state to volatile storage, in which case all peers which connect + /// to us will have to allow downgrade across the board. + /// Otherwise, when we restart, we will not be allowed to reconnect. + fn save_ratchet_state( + &mut self, + remote_static_key: &::PublicKey, + session_data: &::SessionData, + update_data: RatchetUpdate<'_>, + ) -> Result<(), ()>; + + /// Receives a stream of events that occur during an execution of ZSSP. + /// These are provided for debugging, logging or metrics purposes, and must be used for + /// nothing else. Do not base protocol-level decisions upon the events passed to this function. + #[cfg(feature = "logging")] + #[allow(unused)] + fn event_log(&mut self, event: crate::LogEvent<'_, Self::Crypto>) {} +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum IncomingSessionAction { + Allow, + Challenge, + Drop, +} + +/// A collection of fields specifying how to complete the key exchange with a specific remote peer, +/// used by Bob, the responder, at the very last stage of the key exchange. +/// +/// Corresponds to the *Accept* callback of Transition Algorithm 4. +pub struct AcceptAction { + /// The data object to be attached to the session if we successfully connect. + /// If this field is None then we will not connect to this remote peer. + pub session_data: Option, + /// Whether or not we will accept a connection with the remote peer when they do not have a + /// ratchet key that we think they should have. + pub responder_disallows_downgrade: bool, + /// Whether or not to send an explicit rejection packet to the remote peer if we do not create + /// a session with them. + /// + /// This field will not be used if `session_data` is `Some` and the remote peer passes all other + /// authentication checks. + pub responder_silently_rejects: bool, +} diff --git a/src/applicationlayer.rs b/src/applicationlayer.rs deleted file mode 100644 index 0485f7f..0000000 --- a/src/applicationlayer.rs +++ /dev/null @@ -1,196 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ -use std::sync::Arc; - -use crate::crypto::aes::{AesDec, AesEnc}; -use crate::crypto::aes_gcm::{AesGcmDec, AesGcmEnc}; -use crate::crypto::p384::{P384KeyPair, P384PublicKey}; -use crate::crypto::rand_core::{CryptoRng, RngCore}; -use crate::crypto::sha512::{HmacSha512, Sha512}; -use crate::RatchetState; -use crate::{log_event::LogEvent, Session, RATCHET_SIZE}; - -/// Trait to implement to integrate the session into an application. -/// -/// Templating the session on this trait lets the code here be almost entirely transport, OS, -/// and use case independent. -/// -/// The constants exposed in this trait can be redefined from their defaults to change rekey -/// and negotiation timeout behavior. Both sides of a ZSSP session **must** have these constants -/// set to the same values. Changing these constants is generally discouraged unless you know -/// what you are doing. -pub trait ApplicationLayer: Sized { - /// Retry interval for outgoing connection initiation or rekey attempts. - /// - /// Retry attempts will be no more often than this, but the delay may end up being - /// slightly more in some cases depending on where in the cycle the initial attempt - /// falls. - /// - /// Default value is 1 second. - const RETRY_INTERVAL_MS: i64 = 1000; - /// Timeout for how long Alice should wait for Bob to confirm that the Noise_XK handshake - /// was completed successfully. The handshake attempt will be assumed as failed and - /// restarted if Bob does not respond by this cut-off. - /// - /// Default is 10 seconds. - const INITIAL_OFFER_TIMEOUT_MS: i64 = 10 * 1000; - /// Timeout for how long ZSSP should wait before expiring and closing a session when it has - /// lingered in certain states for too long, primarily the rekeying states. - /// If a remote peer does not send the correct information to rekey a session before this - /// timeout then the session will close. - /// - /// Default is 1 minute. - const EXPIRATION_TIMEOUT_MS: i64 = 60 * 1000; - /// Start attempting to rekey after a key has been in use for this many milliseconds. - /// - /// Default is 1 hour. - const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; - /// Maximum random jitter to subtract from the rekey after time timer. - /// Must be greater than 0 and less than u32::MAX. - /// This prevents rekeying from occurring predictably on the hour, so traffic analysis is harder. - /// - /// Default is 10 minutes. - const REKEY_AFTER_TIME_MAX_JITTER_MS: i64 = 1000 * 60 * 10; - /// Rekey after this many key uses. - /// - /// The default is 1/4 the recommended NIST limit for AES-GCM. Unless you are transferring - /// a massive amount of data REKEY_AFTER_TIME_MS is probably going to kick in first. - const REKEY_AFTER_USES: u64 = 1073741824; - - /// Hard expiration of a key after this many uses. - /// - /// Attempting to encrypt more than this many messages with a key will cause a hard error - /// and prevent all encryption. - /// This should basically never occur in practice because of rekeying. - /// - /// Default value is 2^32 - 1, one less than NIST's recommended limit. - /// https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf - const EXPIRE_AFTER_USES: u64 = 4294967295; - - /// Determines how computationally difficult the proof of work is when Bob challenges Alice. - /// It is extremely computationally expensive on Bob to process Alice's initiation packet. So - /// Bob has the option to challenge Alice to prove ownership of address and to prove work before - /// they attempt process Alice's initiation packet. - /// The amount of computational work Alice has to prove increases exponentially with this value. - /// - /// This value must be between 0 and 32 (inclusive). - /// - /// Default is 13, which, on a modern processor, ensures Alice will have to do about as much - /// computational work as Bob will when they process Alice's initiation packet. - const PROOF_OF_WORK_BIT_DIFFICULTY: u32 = 13; - - type Rng: CryptoRng + RngCore; - - type PrpEnc: AesEnc; - type PrpDec: AesDec; - - type AeadEnc: AesGcmEnc; - type AeadDec: AesGcmDec; - - type Hash: Sha512; - type HmacHash: HmacSha512; - - type PublicKey: P384PublicKey; - type KeyPair: P384KeyPair; - - type IoError: std::fmt::Debug; - - /// Type for arbitrary opaque object for use by the application that is attached to - /// each session. - type Data; - - /// Data type for incoming packet buffers. - /// - /// This can be something like `Vec` or `Box<[u8]>` or it can be something like a pooled - /// reusable buffer that automatically returns to its pool when ZSSP is done with it. ZSSP may - /// hold these for a short period of time when assembling fragmented packets on the receive - /// path. - type IncomingPacketBuffer: AsRef<[u8]> + AsMut<[u8]>; - /// Data type for giving ZSSP temporary ownership of a buffer containing the local party's - /// identity. - /// It will be dropped as soon as the session is established. - type LocalIdentityBlob: AsRef<[u8]>; - - /// This function will be called whenever Alice's initial Hello packet contains the empty ratchet - /// fingerprint. Brand new peers will always connect to Bob with the empty ratchet, but from - /// then on they should be using non-empty ratchet states. - /// - /// If this returns false, we will attempt to connect to Alice with the empty ratchet state. - /// If this returns true, Alice's connection will be silently dropped. - /// If this function is configured to always return true, it means peers will not be able to - /// connect to us unless they had a prior-established ratchet key with us. This is the best way - /// for the paranoid to enforce a manual allow-list. - fn hello_requires_recognized_ratchet(&self, current_time: i64) -> bool; - /// This function is called if we, as Alice, attempted to open a session with Bob using a - /// non-empty ratchet key, but Bob does not have this ratchet key and wants to downgrade - /// to the zero ratchet key. - /// - /// If it returns true Alice will downgrade their ratchet state to emtpy, potentially ending - /// their current ratchet chain. - /// If it returns false then we will consider Bob as having failed authentication, and this - /// packet will be dropped. The session will continue attempting to connect to Bob. - /// - /// This function must deterministically return either true or false for a given session. - /// - /// It is a bad sign that Bob has somehow forgotten Alice's ratchet key, it either means at - /// least one party is misconfigured and got their ratchet keys corrupted or lost, or Bob has - /// been compromised and is being impersonated. An attacker must at least have Bob's private - /// static key to be able to ask Alice to downgrade. - fn initiator_disallows_downgrade(&self, session: &Arc>, current_time: i64) -> bool; - /// Lookup a specific ratchet state based on its ratchet fingerprint. - /// This function will be called whenever Alice attempts to connect to us with a non-empty - /// ratchet fingerprint. - /// - /// If the ratchet key was found, the function should return `RestoreAction::RestoreRatchet`. This will - /// cause us to connect to Alice using the returned ratchet number and ratchet key. - /// - /// If the ratchet key could not be found, the application may choose between returning - /// `RatchetAction::DowngradeRatchet` or `RatchetAction::FailAuthentication`. - /// If `RatchetAction::DowngradeRatchet` is returned we will attempt to convince Alice to downgrade - /// to the empty ratchet key, restarting the ratchet chain. - /// If `RatchetAction::FailAuthentication` is returned Alice's connection will be silently dropped. - fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE], current_time: i64) -> Result; - - /// Lookup a specific ratchet state based on the identity of the peer being communicated with. - /// This function will be called whenever Alice attempts to open a session, or Bob attempts - /// to verify Alice's identity. - fn restore_by_identity( - &self, - remote_static_key: &Self::PublicKey, - application_data: &Self::Data, - current_time: i64, - ) -> Result<[RatchetState; 2], Self::IoError>; - /// Atomically save the given `new_ratchet_states` to persistent storage. - /// `pre_ratchet_states` contains what should be the previous contents of persistent storage. - /// - /// If this returns `Err(IoError)`, the packet which triggered this function to be called will be - /// dropped, and no session state will be mutated, preserving synchronization. The remote peer - /// will eventually resend that packet and so this function will be called again. - /// - /// If persistent storage is supported, this function should not return until the ratchet state - /// is saved, otherwise it is possible, albeit unlikely, for a sudden restart of the local - /// machine to put our ratchet state out of sync with the remote peer. If this happens the only - /// fix is to reset both ratchet keys to empty. - /// - /// This function may also save state to volatile storage, in which case all peers which connect - /// to us will have to allow downgrade, i.e. `initiator_disallows_downgrade` returns false - /// and/or `check_accept_session` returns `(Some(true, _), _)`. - /// Otherwise, when we restart, we will not be allowed to reconnect. - fn save_ratchet_state( - &self, - remote_static_key: &Self::PublicKey, - application_data: &Self::Data, - pre_ratchet_states: [&RatchetState; 2], - new_ratchet_states: [&RatchetState; 2], - current_time: i64, - ) -> Result<(), Self::IoError>; - - #[allow(unused)] - #[inline] - fn event_log(&self, event: LogEvent, current_time: i64) {} -} diff --git a/src/challenge.rs b/src/challenge.rs new file mode 100644 index 0000000..09b14d4 --- /dev/null +++ b/src/challenge.rs @@ -0,0 +1,108 @@ +use std::hash::Hasher; +use std::sync::atomic::{AtomicU64, Ordering}; + +use rand_core::{CryptoRng, RngCore}; + +use crate::antireplay::Window; +use crate::crypto::*; +use crate::proto::*; + +pub struct ChallengeContext { + counter: AtomicU64, + antireplay_window: Window, + salt: [u8; SALT_SIZE], +} + +/// Corresponds to Algorithm 11 found in Section 5. +pub fn gen_null_response(rng: &mut impl RngCore) -> [u8; CHALLENGE_SIZE] { + let mut response = [0u8; CHALLENGE_SIZE]; + response[POW_START..].copy_from_slice(&rng.next_u64().to_ne_bytes()); + response +} +/// Corresponds to Algorithm 13 found in Section 5. +pub fn respond_to_challenge_in_place( + rng: &mut impl RngCore, + hash: &mut impl Sha512Hash, + challenge: &[u8; CHALLENGE_SIZE], + pre_response: &mut [u8; CHALLENGE_SIZE], +) { + if &challenge[POW_START..] == &pre_response[POW_START..] { + pre_response.copy_from_slice(challenge); + let mut pow = rng.next_u64(); + let mut work_buf = [0u8; SHA512_HASH_SIZE]; + loop { + pre_response[POW_START..].copy_from_slice(&pow.to_ne_bytes()); + if verify_pow(hash, pre_response, &mut work_buf) { + return; + } + pow = pow.wrapping_add(1); + } + } +} + +impl ChallengeContext { + pub fn new(rng: &mut Rng) -> Self { + let mut salt = [0u8; SALT_SIZE]; + rng.fill_bytes(&mut salt); + Self { + counter: AtomicU64::new(0), + antireplay_window: Window::new(), + salt, + } + } + /// Corresponds to Algorithm 12 found in Section 5. + pub fn process_hello( + &self, + hash: &mut impl Sha512Hash, + addr: &impl std::hash::Hash, + response: &[u8; CHALLENGE_SIZE], + ) -> Result<(), [u8; CHALLENGE_SIZE]> { + let c = u64::from_be_bytes(response[..COUNTER_SIZE].try_into().unwrap()); + let mut work_buf = [0u8; SHA512_HASH_SIZE]; + if self.antireplay_window.check(c) + && secure_eq(&response[COUNTER_SIZE..POW_START], &self.create_mac(hash, c, addr)) + && verify_pow(hash, response, &mut work_buf) + { + self.antireplay_window.update(c); + Ok(()) + } else { + let mut challenge = [0u8; CHALLENGE_SIZE]; + let d = self.counter.fetch_add(1, Ordering::Relaxed); + challenge[..COUNTER_SIZE].copy_from_slice(&d.to_be_bytes()); + challenge[COUNTER_SIZE..POW_START].copy_from_slice(&self.create_mac(hash, d, addr)); + challenge[POW_START..].copy_from_slice(&response[POW_START..]); + Err(challenge) + } + } + fn create_mac(&self, hash: &mut impl Sha512Hash, c: u64, addr: &impl std::hash::Hash) -> [u8; MAC_SIZE] { + let mut hasher = ShaHasher(hash); + hasher.write(&c.to_be_bytes()); + addr.hash(&mut hasher); + hasher.write(&self.salt); + drop(hasher); + + let mut mac = [0u8; SHA512_HASH_SIZE]; + hash.finish_and_reset(&mut mac); + mac[..MAC_SIZE].try_into().unwrap() + } +} + +/// Trick rust into letting us use a hasher that returns more than 64 bits. +struct ShaHasher<'a, ShaImpl: Sha512Hash>(&'a mut ShaImpl); +impl<'a, ShaImpl: Sha512Hash> Hasher for ShaHasher<'a, ShaImpl> { + fn finish(&self) -> u64 { + unimplemented!() + } + fn write(&mut self, bytes: &[u8]) { + self.0.update(bytes) + } +} + +/// Check if the proof of work attached to the first message contains the correct number of leading +/// zeros. +fn verify_pow(hash: &mut impl Sha512Hash, response: &[u8], work_buf: &mut [u8; SHA512_HASH_SIZE]) -> bool { + hash.update(response); + hash.finish_and_reset(work_buf); + let n = u32::from_be_bytes(work_buf[..4].try_into().unwrap()); + n.leading_zeros() >= DIFFICULTY +} diff --git a/src/crypto/aes.rs b/src/crypto/aes.rs index 842e1ed..7921c30 100644 --- a/src/crypto/aes.rs +++ b/src/crypto/aes.rs @@ -1,20 +1,88 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. -pub const AES_256_BLOCK_SIZE: usize = 16; pub const AES_256_KEY_SIZE: usize = 32; +pub const AES_256_BLOCK_SIZE: usize = 16; +pub const AES_GCM_TAG_SIZE: usize = 16; +pub const AES_GCM_NONCE_SIZE: usize = 12; -pub trait AesEnc: Send + Sync { +/// A trait for encrypting individual blocks of plaintext using AES-256. +/// It is used for header authentication, for which we have a standard model proof that our +/// algorithm is secure. +/// +/// Instances must securely delete their keys when dropped or reset. +pub trait Aes256Enc: Sized + Send + Sync { fn new(key: &[u8; AES_256_KEY_SIZE]) -> Self; - fn reset(&self, key: &[u8; AES_256_KEY_SIZE]); + /// Change the encryption key to `key` so that all future encryption is performed with it. + /// This function is very rarely called so it does not have to be particularly efficient. + fn reset(&mut self, key: &[u8; AES_256_KEY_SIZE]) { + *self = Self::new(key); + } + /// Decrypt the given `block` of plaintext directly using the AES block cipher + /// (i.e. AES-256 in zero-padding ECB mode). + /// The ciphertext should be written directly back out to `block`. fn encrypt_in_place(&self, block: &mut [u8; AES_256_BLOCK_SIZE]); } -pub trait AesDec: Send + Sync { +/// A trait for decrypting individual blocks of plaintext using AES-256. +/// +/// Instances must securely delete their keys when dropped or reset. +pub trait Aes256Dec: Sized + Send + Sync { fn new(key: &[u8; AES_256_KEY_SIZE]) -> Self; - fn reset(&self, key: &[u8; AES_256_KEY_SIZE]); + /// Change the decryption key to `key` so that all future decryption is performed with it. + /// This function is very rarely called so it does not have to be particularly efficient. + fn reset(&mut self, key: &[u8; AES_256_KEY_SIZE]) { + *self = Self::new(key); + } + /// Decrypt the given `block` of ciphertext directly using the AES 256 block cipher + /// (i.e. AES-256 in zero-padding ECB mode). + /// The plaintext should be written directly back out to `block`. fn decrypt_in_place(&self, block: &mut [u8; AES_256_BLOCK_SIZE]); } + +pub trait AesGcmEncContext { + fn encrypt(&mut self, input: &[u8], output: &mut [u8]); + + fn finish(self) -> [u8; AES_GCM_TAG_SIZE]; +} + +pub trait AesGcmDecContext { + fn decrypt_in_place(&mut self, data: &mut [u8]); + + #[must_use] + fn finish(self, tag: &[u8; AES_GCM_TAG_SIZE]) -> bool; +} + +pub trait HighThroughputAesGcmPool: Send + Sync { + type EncContext<'a>: AesGcmEncContext + where + Self: 'a; + type DecContext<'a>: AesGcmDecContext + where + Self: 'a; + + fn new(encrypt_key: &[u8; AES_256_KEY_SIZE], decrypt_key: &[u8; AES_256_KEY_SIZE]) -> Self; + + fn start_enc<'a>(&'a self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> Self::EncContext<'a>; + fn start_dec<'a>(&'a self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> Self::DecContext<'a>; +} + +pub trait LowThroughputAesGcm { + fn encrypt_in_place( + key: &[u8; AES_256_KEY_SIZE], + nonce: &[u8; AES_GCM_NONCE_SIZE], + aad: &[u8], + data: &mut [u8], + ) -> [u8; AES_GCM_TAG_SIZE]; + #[must_use] + fn decrypt_in_place( + key: &[u8; AES_256_KEY_SIZE], + nonce: &[u8; AES_GCM_NONCE_SIZE], + aad: &[u8], + data: &mut [u8], + tag: &[u8; AES_GCM_TAG_SIZE], + ) -> bool; +} diff --git a/src/crypto/aes_gcm.rs b/src/crypto/aes_gcm.rs deleted file mode 100644 index 250c587..0000000 --- a/src/crypto/aes_gcm.rs +++ /dev/null @@ -1,37 +0,0 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. - -pub const AES_GCM_TAG_SIZE: usize = 16; -pub const AES_GCM_IV_SIZE: usize = 12; -pub const AES_GCM_KEY_SIZE: usize = super::aes::AES_256_KEY_SIZE; - -/// Implementations of this trait does not have to be Send + Sync, -/// but if it is wrapped in a `Mutex` it must satisfy the requirements of Send + Sync. -pub trait AesGcmEnc { - fn new(key: &[u8; AES_GCM_KEY_SIZE]) -> Self; - - fn set_iv(&mut self, iv: &[u8; AES_GCM_IV_SIZE]); - - fn set_aad(&mut self, aad: &[u8]); - - fn encrypt(&mut self, input: &[u8], output: &mut [u8]); - - fn encrypt_in_place(&mut self, data: &mut [u8]); - - fn finish_encrypt(&mut self, output: &mut [u8; AES_GCM_TAG_SIZE]); -} - -/// Implementations of this trait does not have to be Send + Sync, -/// but if it is wrapped in a `Mutex` it must satisfy the requirements of Send + Sync. -pub trait AesGcmDec { - fn new(key: &[u8; AES_GCM_KEY_SIZE]) -> Self; - - fn set_iv(&mut self, iv: &[u8; AES_GCM_IV_SIZE]); - - fn set_aad(&mut self, aad: &[u8]); - - fn decrypt(&mut self, input: &[u8], output: &mut [u8]); - - fn decrypt_in_place(&mut self, data: &mut [u8]); - - fn finish_decrypt(&mut self, expected_tag: &[u8; AES_GCM_TAG_SIZE]) -> bool; -} diff --git a/src/crypto/kyber1024.rs b/src/crypto/kyber1024.rs new file mode 100644 index 0000000..74c9571 --- /dev/null +++ b/src/crypto/kyber1024.rs @@ -0,0 +1,44 @@ +use rand_core::{CryptoRng, RngCore}; + +/// The size of a Kyber1024 public key, which is 1568 bytes. +pub const KYBER_PUBLIC_KEY_SIZE: usize = 1568; +/// The size of a Kyber1024 KEM ciphertext, which is 1568 bytes. +pub const KYBER_CIPHERTEXT_SIZE: usize = 1568; +/// The size of a Kyber1024 KEM plaintext, which is 32 bytes. +pub const KYBER_PLAINTEXT_SIZE: usize = 32; + +/// Instances must securely delete the private key when dropped. +pub trait Kyber1024PrivateKey: Sized + Send + Sync { + /// Generate a Kyber1024 private key and public key pair, and return the raw bytes of the public + /// key. + /// The private key will be temporarily held in memory but the public key will be immediately + /// sent to the remote peer. + /// + /// This function may use the provided RNG or its own, so long as the output is cryptographically random. + fn generate(rng: &mut Rng) -> (Self, [u8; KYBER_PUBLIC_KEY_SIZE]); + /// Generate a Kyber1024 key encapsulation based on the given `public_key`, and return the + /// raw bytes of the generated ciphertext and plaintext. The ciphertext is immediately sent to + /// the remote peer and the plaintext is immediately hashed, both are quickly deleted. + /// + /// This function may use the provided RNG or its own, so long as the output is cryptographically random. + /// + /// **CRITICAL**: This must return `None` if the given `public_key` is invalid in any way + /// according to the Kyber1024 spec. + #[must_use] + fn encapsulate( + rng: &mut Rng, + public_key: &[u8; KYBER_PUBLIC_KEY_SIZE], + plaintext_out: &mut [u8; KYBER_PLAINTEXT_SIZE], + ) -> Option<[u8; KYBER_CIPHERTEXT_SIZE]>; + /// Decapsulate a Kyber1024 `ciphertext` received from the remote peer, retreiving + /// the raw bytes of the original plaintext. This plaintext is immediately hashed and deleted. + /// + /// **CRITICAL**: This must return `None` if the given `ciphertext` is invalid in any way + /// according to the Kyber1024 spec. + #[must_use] + fn decapsulate( + &self, + ciphertext: &[u8; KYBER_CIPHERTEXT_SIZE], + plaintext_out: &mut [u8; KYBER_PLAINTEXT_SIZE], + ) -> bool; +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 0c7f601..2b6ea78 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,12 +1,31 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. +mod aes; +pub use self::aes::*; -pub mod aes; -pub mod aes_gcm; -pub mod p384; -pub mod secret; -pub mod sha512; +mod p384; +pub use self::p384::*; + +mod sha512; +pub use sha512::*; + +mod kyber1024; +pub use kyber1024::*; // We re-export our dependencies so it is less of a headache for the implementor to use the same // exact version of them. -pub use pqc_kyber; +pub use arrayvec; pub use rand_core; +pub use zeroize; + +/// Constant time byte slice equality. +pub fn secure_eq + ?Sized, B: AsRef<[u8]> + ?Sized>(a: &A, b: &B) -> bool { + let (a, b) = (a.as_ref(), b.as_ref()); + if a.len() == b.len() { + let mut x = 0u8; + for (aa, bb) in a.iter().zip(b.iter()) { + x |= *aa ^ *bb; + } + x == 0 + } else { + false + } +} diff --git a/src/crypto/p384.rs b/src/crypto/p384.rs index 2a14067..67f780a 100644 --- a/src/crypto/p384.rs +++ b/src/crypto/p384.rs @@ -1,31 +1,46 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. - -use super::rand_core::{CryptoRng, RngCore}; +use rand_core::{CryptoRng, RngCore}; +/// The size in bytes of a P-384 public key when in compressed SEC1-encoded format. pub const P384_PUBLIC_KEY_SIZE: usize = 49; +/// The size in bytes of the raw output of ECDH between a P-384 public and private key. pub const P384_ECDH_SHARED_SECRET_SIZE: usize = 48; /// A NIST P-384 ECDH/ECDSA public key. pub trait P384PublicKey: Sized + Send + Sync { - /// Create a p384 public key from raw bytes. + /// Create a P-384 public key from raw bytes. + /// + /// **CRITICAL**: This function must return `None` if the input `raw_key` is not on the P-384 + /// curve, or if it breaks the P-384 spec in any other way. fn from_bytes(raw_key: &[u8; P384_PUBLIC_KEY_SIZE]) -> Option; /// Get the raw bytes that uniquely define the public key. - fn as_bytes(&self) -> &[u8; P384_PUBLIC_KEY_SIZE]; + /// + /// This must output the compressed SEC1 NIST encoding of P-384 public keys. + fn to_bytes(&self) -> [u8; P384_PUBLIC_KEY_SIZE]; } /// A NIST P-384 ECDH/ECDSA public/private key pair. -pub trait P384KeyPair: Send + Sync { +/// +/// Instances must securely delete the private key when dropped. +pub trait P384KeyPair { + /// The `PublicKeyP384` implementation which matches this `KeyPairP384` implementation. type PublicKey: P384PublicKey; - type Rng: RngCore + CryptoRng; - /// Randomly generate a new p384 keypair. - /// This function may use the provided RNG or it's own, - /// so long as the produced keys are cryptographically random. - fn generate(rng: &mut Self::Rng) -> Self; + /// Randomly generate a new P-384 keypair. + /// + /// This function may use the provided RNG or its own, so long as the output is cryptographically random. + fn generate(rng: &mut Rng) -> Self; /// Get the raw bytes that uniquely define the public key. - fn public_key_bytes(&self) -> &[u8; P384_PUBLIC_KEY_SIZE]; + /// + /// This must output the compressed SEC1 NIST encoding of P-384 public keys. + fn public_key_bytes(&self) -> [u8; P384_PUBLIC_KEY_SIZE]; - /// Perform ECDH key agreement, returning the raw (un-hashed!) ECDH secret. - fn agree(&self, other_public: &Self::PublicKey, output: &mut [u8; P384_ECDH_SHARED_SECRET_SIZE]) -> bool; + /// Perform ECDH key agreement, writing the raw (un-hashed!) ECDH secret to `ecdh_out`. + /// + /// **CRITICAL**: This function must return `false` if key agreement between this private key and + /// the input `public_key` key would result in an invalid, non-standard or predictable ECDH secret. + /// Please refer to the NIST spec for P-384 ECDH key agreement, or better yet use a peer reviewed + /// library that has already implemented this correctly. + #[must_use] + fn agree(&self, public_key: &Self::PublicKey, ecdh_out: &mut [u8; P384_ECDH_SHARED_SECRET_SIZE]) -> bool; } diff --git a/src/crypto/secret.rs b/src/crypto/secret.rs deleted file mode 100644 index 60399b3..0000000 --- a/src/crypto/secret.rs +++ /dev/null @@ -1,135 +0,0 @@ -// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. -use std::convert::TryInto; - -/// Constant time byte slice equality. -#[inline] -pub fn secure_eq + ?Sized, B: AsRef<[u8]> + ?Sized>(a: &A, b: &B) -> bool { - let (a, b) = (a.as_ref(), b.as_ref()); - if a.len() == b.len() { - let mut x = 0u8; - for (aa, bb) in a.iter().zip(b.iter()) { - x |= *aa ^ *bb; - } - x == 0 - } else { - false - } -} - -/// Container for secrets that clears them on drop. -/// -/// We can't be totally sure that things like libraries are doing this and it's -/// hard to get every use of a secret anywhere, but using this in our code at -/// least reduces the number of secrets that are left lying around in memory. -/// -/// This is generally a low-risk thing since it's process memory that's protected, -/// but it's still not a bad idea due to things like swap or obscure side channel -/// attacks that allow memory to be read. -#[derive(Clone)] -#[repr(transparent)] -pub struct Secret(pub [u8; L]); - -impl Secret { - /// Create a new all-zero secret. - #[inline(always)] - pub fn new() -> Self { - Self([0_u8; L]) - } - /// Copy bytes into secret, then delete the previous value, will panic if the slice does not match the size of this secret. - pub fn from_bytes_then_delete(b: &mut [u8]) -> Self { - let ret = Self(b.try_into().unwrap()); - b.fill(0); - ret - } - /// Moves bytes into secret, will panic if the slice does not match the size of this secret. - /// This is unsafe because it will not destroy the contents of its input. - /// # Safety - /// Make sure the contents of the input are securely deleted. - #[inline(always)] - pub unsafe fn from_bytes(b: &[u8]) -> Self { - Self(b.try_into().unwrap()) - } - - #[inline(always)] - pub fn as_ptr(&self) -> *const u8 { - self.0.as_ptr() - } - - #[inline(always)] - pub fn as_bytes(&self) -> &[u8; L] { - &self.0 - } - - /// Get the first N bytes of this secret as a fixed length array. - #[inline(always)] - pub fn first_n(&self) -> &[u8; N] { - assert!(N <= L); - unsafe { &*self.0.as_ptr().cast() } - } - - /// Clone the first N bytes of this secret as another secret. - #[inline(always)] - pub fn first_n_clone(&self) -> Secret { - Secret::(*self.first_n()) - } - - pub fn overwrite(&mut self, src: &Self) { - self.0.copy_from_slice(&src.0); - } - pub fn overwrite_first_n(&mut self, src: &Secret) { - let amount = N.min(L); - self.0[..amount].copy_from_slice(&src.0[..amount]); - } - - pub fn eq_bytes(&self, other: &[u8]) -> bool { - secure_eq(&self.0, other) - } -} - -impl Drop for Secret { - fn drop(&mut self) { - self.0.fill(0); - } -} - -impl Default for Secret { - #[inline(always)] - fn default() -> Self { - Self([0_u8; L]) - } -} - -impl AsRef<[u8]> for Secret { - #[inline(always)] - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl AsRef<[u8; L]> for Secret { - #[inline(always)] - fn as_ref(&self) -> &[u8; L] { - &self.0 - } -} - -impl AsMut<[u8]> for Secret { - #[inline(always)] - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl AsMut<[u8; L]> for Secret { - #[inline(always)] - fn as_mut(&mut self) -> &mut [u8; L] { - &mut self.0 - } -} - -impl PartialEq for Secret { - fn eq(&self, other: &Self) -> bool { - secure_eq(&self.0, &other.0) - } -} -impl Eq for Secret {} diff --git a/src/crypto/sha512.rs b/src/crypto/sha512.rs index c2161be..4e064d5 100644 --- a/src/crypto/sha512.rs +++ b/src/crypto/sha512.rs @@ -2,41 +2,27 @@ pub const SHA512_HASH_SIZE: usize = 64; -/// Opaque SHA-512 implementation. -/// Does not need to be threadsafe. -pub trait Sha512 { - /// Allocate memory on the stack or heap for Sha512. - /// An instance of Sha512 will only ever be held on the stack. +/// A SHA-512 implementation. +pub trait Sha512Hash { + /// Create a new instance of SHA-512 for streaming data to. fn new() -> Self; - - /// Reinitialize the internal state of the hash function for a fresh input. - fn reset(&mut self); - - fn update(&mut self, input: &[u8]); - /// Finish hashing the input and write the final hash to output. - /// - /// After this function is called, this instance of Sha512 will either be dropped - /// or `reset` will be called. - fn finish(&mut self, output: &mut [u8; SHA512_HASH_SIZE]); + /// Update the instance of SHA-512 with input `data`. + /// This must update the state of SHA-512 as if `data` was appended to the previous input. + fn update(&mut self, data: &[u8]); + /// Finish streaming input and output the final hash. + fn finish_and_reset(&mut self, output: &mut [u8; SHA512_HASH_SIZE]); } /// Opaque HMAC-SHA-512 implementation. /// Does not need to be threadsafe. -pub trait HmacSha512 { - /// Allocate memory on the stack or heap for HmacSha512. - /// An instance of HmacSha512 will only ever be held on the stack. +pub trait Sha512Hmac { + /// Allocate space on the stack or heap for repeated Hmac invocations. /// - /// `reset` will always be called before `update` on a new instance of HmacSha512, - /// to make sure there is always a set key. + /// Many FIPS compliant libraries, namely OpenSSL, require initializing an Hmac context on the + /// heap before operating on it. + /// If you are using a more sane library feel free to make this return an empty type. fn new() -> Self; - /// Reinitialize the internal state of the hash function for a fresh input. - /// The provided key should replace the previous Hmac key. - fn reset(&mut self, key: &[u8]); - - fn update(&mut self, input: &[u8]); - /// Finish hashing the input and write the final hash to output. - /// - /// After this function is called, this instance of HmacSha512 will either be dropped - /// or `reset` will be called. - fn finish(&mut self, output: &mut [u8; SHA512_HASH_SIZE]); + /// Pure function for computing a single HMAC Hash. Repeat invocations of this function should + /// have no effect on each other. + fn hash(&mut self, key: &[u8], full_input: &[u8], output: &mut [u8; SHA512_HASH_SIZE]); } diff --git a/src/crypto_impl/kyber1024.rs b/src/crypto_impl/kyber1024.rs new file mode 100644 index 0000000..ef553c2 --- /dev/null +++ b/src/crypto_impl/kyber1024.rs @@ -0,0 +1,39 @@ +use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroizing; + +use crate::crypto::*; + +/// A wrapper for a buffer the size of a pqc_kyber secret key. +/// The crate `pqc_kyber` is low level and operates directly on buffers of bytes. +pub type CrateKyber1024PrivateKey = Zeroizing<[u8; pqc_kyber::KYBER_SECRETKEYBYTES]>; +impl Kyber1024PrivateKey for CrateKyber1024PrivateKey { + fn generate(rng: &mut Rng) -> (Self, [u8; KYBER_PUBLIC_KEY_SIZE]) { + // According to the source code this can only fail if the RNG fails. + // Idk why rust allows RNG to fail. + let keypair = pqc_kyber::keypair(rng).unwrap(); + (Zeroizing::new(keypair.secret), keypair.public) + } + + fn encapsulate( + rng: &mut Rng, + public_key: &[u8; KYBER_PUBLIC_KEY_SIZE], + plaintext_out: &mut [u8; KYBER_PLAINTEXT_SIZE], + ) -> Option<[u8; KYBER_CIPHERTEXT_SIZE]> { + let ret; + (ret, *plaintext_out) = pqc_kyber::encapsulate(public_key, rng).ok()?; + Some(ret) + } + + fn decapsulate( + &self, + ciphertext: &[u8; KYBER_CIPHERTEXT_SIZE], + plaintext_out: &mut [u8; KYBER_PLAINTEXT_SIZE], + ) -> bool { + if let Ok(result) = pqc_kyber::decapsulate(ciphertext, self.as_ref()) { + *plaintext_out = result; + true + } else { + false + } + } +} diff --git a/src/crypto_impl/mod.rs b/src/crypto_impl/mod.rs new file mode 100644 index 0000000..8aa0ceb --- /dev/null +++ b/src/crypto_impl/mod.rs @@ -0,0 +1,51 @@ +#[cfg(feature = "pqc_kyber")] +mod kyber1024; +#[cfg(feature = "pqc_kyber")] +pub use kyber1024::*; +#[cfg(feature = "pqc_kyber")] +pub use pqc_kyber; + +#[cfg(feature = "p384")] +mod p384_impl; +#[cfg(feature = "p384")] +pub use p384; +#[cfg(feature = "p384")] +pub use p384_impl::*; + +#[cfg(feature = "sha2")] +mod sha512; +#[cfg(feature = "sha2")] +pub use hmac; +#[cfg(feature = "sha2")] +pub use sha2; +#[cfg(feature = "sha2")] +pub use sha512::*; + +#[cfg(feature = "openssl-sys")] +mod openssl; +#[cfg(feature = "openssl-sys")] +pub use openssl::*; +#[cfg(feature = "openssl-sys")] +pub use openssl_sys; + +#[cfg(feature = "default-crypto")] +pub trait DefaultCrypto { + type SessionData; + type IncomingPacketBuffer: AsMut<[u8]> + AsRef<[u8]>; +} +#[cfg(feature = "default-crypto")] +impl crate::application::CryptoLayer for C { + type Rng = rand_core::OsRng; + type PrpEnc = OpenSSLAes256Enc; + type PrpDec = OpenSSLAes256Dec; + type Aead = OpenSSLAesGcm; + type AeadPool = OpenSSLAesGcmPool; + type Hash = CrateSha512; + type Hmac = CrateHmacSha512; + type PublicKey = CrateP384PublicKey; + type KeyPair = CrateP384KeyPair; + type Kem = CrateKyber1024PrivateKey; + + type SessionData = C::SessionData; + type IncomingPacketBuffer = C::IncomingPacketBuffer; +} diff --git a/src/crypto_impl/openssl.rs b/src/crypto_impl/openssl.rs new file mode 100644 index 0000000..8a73e92 --- /dev/null +++ b/src/crypto_impl/openssl.rs @@ -0,0 +1,302 @@ +use std::{ + ptr::{self, NonNull}, + sync::{Mutex, MutexGuard}, +}; + +use openssl_sys::*; + +use crate::crypto::*; + +pub struct OpenSSLCtx(NonNull); +impl Drop for OpenSSLCtx { + fn drop(&mut self) { + unsafe { + EVP_CIPHER_CTX_free(self.0.as_ptr()); + } + } +} +impl OpenSSLCtx { + /// Creates a new context. + pub fn new() -> Option { + unsafe { Some(OpenSSLCtx(NonNull::new(EVP_CIPHER_CTX_new())?)) } + } + + pub unsafe fn cipher_init( + &self, + t: *const openssl_sys::EVP_CIPHER, + key: *const u8, + iv: *const u8, + ) -> bool { + let evp_f = if ENCRYPT { + EVP_EncryptInit_ex + } else { + EVP_DecryptInit_ex + }; + + // OpenSSL will usually leak a static amount of memory per cipher given here. + evp_f(self.0.as_ptr(), t, ptr::null_mut(), key, iv) > 0 + } + + pub unsafe fn update(&self, input: &[u8], output: *mut u8) -> bool { + let evp_f = if ENCRYPT { + EVP_EncryptUpdate + } else { + EVP_DecryptUpdate + }; + + let mut outlen = 0; + + evp_f( + self.0.as_ptr(), + output, + &mut outlen, + input.as_ptr(), + input.len() as c_int, + ) > 0 + } + + pub unsafe fn finalize(&self) -> bool { + let evp_f = if ENCRYPT { + EVP_EncryptFinal_ex + } else { + EVP_DecryptFinal_ex + }; + let mut outl = 0; + + evp_f(self.0.as_ptr(), ptr::null_mut(), &mut outl) > 0 + } + + pub unsafe fn get_tag(&self, tag: &mut [u8]) -> bool { + EVP_CIPHER_CTX_ctrl( + self.0.as_ptr(), + openssl_sys::EVP_CTRL_GCM_GET_TAG, + tag.len() as c_int, + tag.as_mut_ptr() as *mut _, + ) > 0 + } + #[allow(unused)] + pub unsafe fn set_tag(&self, tag: &[u8]) -> bool { + EVP_CIPHER_CTX_ctrl( + self.0.as_ptr(), + openssl_sys::EVP_CTRL_GCM_SET_TAG, + tag.len() as c_int, + tag.as_ptr() as *mut _, + ) > 0 + } + pub fn as_ptr(&self) -> *mut openssl_sys::EVP_CIPHER_CTX { + self.0.as_ptr() + } +} + +pub struct OpenSSLAes256Enc(Mutex); +unsafe impl Send for OpenSSLAes256Enc {} +unsafe impl Sync for OpenSSLAes256Enc {} + +impl Aes256Enc for OpenSSLAes256Enc { + fn new(key: &[u8; AES_256_KEY_SIZE]) -> Self { + let ctx = OpenSSLCtx::new().unwrap(); + unsafe { + let t = openssl_sys::EVP_aes_256_ecb(); + assert!(ctx.cipher_init::(t, key.as_ptr(), ptr::null())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + } + Self(Mutex::new(ctx)) + } + + fn reset(&mut self, key: &[u8; AES_256_KEY_SIZE]) { + let ctx = self.0.lock().unwrap(); + unsafe { + let t = openssl_sys::EVP_aes_256_ecb(); + assert!(ctx.cipher_init::(t, key.as_ptr(), ptr::null())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + } + } + + fn encrypt_in_place(&self, block: &mut [u8; AES_256_BLOCK_SIZE]) { + let ptr = block.as_mut_ptr(); + let ctx = self.0.lock().unwrap(); + unsafe { assert!(ctx.update::(block, ptr)) } + } +} +pub struct OpenSSLAes256Dec(Mutex); +unsafe impl Send for OpenSSLAes256Dec {} +unsafe impl Sync for OpenSSLAes256Dec {} + +impl Aes256Dec for OpenSSLAes256Dec { + fn new(key: &[u8; AES_256_KEY_SIZE]) -> Self { + let ctx = OpenSSLCtx::new().unwrap(); + unsafe { + let t = openssl_sys::EVP_aes_256_ecb(); + assert!(ctx.cipher_init::(t, key.as_ptr(), ptr::null())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + } + Self(Mutex::new(ctx)) + } + + fn reset(&mut self, key: &[u8; AES_256_KEY_SIZE]) { + let ctx = self.0.lock().unwrap(); + unsafe { + let t = openssl_sys::EVP_aes_256_ecb(); + assert!(ctx.cipher_init::(t, key.as_ptr(), ptr::null())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + } + } + + fn decrypt_in_place(&self, block: &mut [u8; AES_256_BLOCK_SIZE]) { + let ptr = block.as_mut_ptr(); + let ctx = self.0.lock().unwrap(); + unsafe { assert!(ctx.update::(block, ptr)) } + } +} + +pub struct OpenSSLAesGcmEnc<'a>(MutexGuard<'a, OpenSSLCtx>); +impl<'a> AesGcmEncContext for OpenSSLAesGcmEnc<'a> { + fn encrypt(&mut self, input: &[u8], output: &mut [u8]) { + unsafe { assert!(self.0.update::(input, output.as_mut_ptr())) }; + } + + fn finish(self) -> [u8; AES_GCM_TAG_SIZE] { + let mut output = [0u8; AES_GCM_TAG_SIZE]; + unsafe { + assert!(self.0.finalize::()); + assert!(self.0.get_tag(&mut output)); + } + output + } +} + +pub struct OpenSSLAesGcmDec<'a>(MutexGuard<'a, OpenSSLCtx>); +impl<'a> AesGcmDecContext for OpenSSLAesGcmDec<'a> { + fn decrypt_in_place(&mut self, data: &mut [u8]) { + let p = data.as_mut_ptr(); + unsafe { assert!(self.0.update::(data, p)) }; + } + + fn finish(self, tag: &[u8; AES_GCM_TAG_SIZE]) -> bool { + unsafe { self.0.set_tag(tag) && self.0.finalize::() } + } +} + +pub struct OpenSSLAesGcmPool { + enc: [Mutex; 8], + dec: [Mutex; 8], +} +unsafe impl Send for OpenSSLAesGcmPool {} +unsafe impl Sync for OpenSSLAesGcmPool {} + +impl HighThroughputAesGcmPool for OpenSSLAesGcmPool { + type EncContext<'a> = OpenSSLAesGcmEnc<'a>; + + type DecContext<'a> = OpenSSLAesGcmDec<'a>; + + fn new(encrypt_key: &[u8; AES_256_KEY_SIZE], decrypt_key: &[u8; AES_256_KEY_SIZE]) -> Self { + unsafe { + OpenSSLAesGcmPool { + enc: std::array::from_fn(|_| { + let ctx = OpenSSLCtx::new().unwrap(); + let t = openssl_sys::EVP_aes_256_gcm(); + assert!(ctx.cipher_init::(t, encrypt_key.as_ptr(), ptr::null())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + Mutex::new(ctx) + }), + dec: std::array::from_fn(|_| { + let ctx = OpenSSLCtx::new().unwrap(); + let t = openssl_sys::EVP_aes_256_gcm(); + assert!(ctx.cipher_init::(t, decrypt_key.as_ptr(), ptr::null())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + Mutex::new(ctx) + }), + } + } + } + + fn start_enc<'a>(&'a self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> OpenSSLAesGcmEnc { + let i = u64::from_be_bytes(nonce[4..].try_into().unwrap()); + let g = self.enc[(i as usize) % self.enc.len()].lock().unwrap(); + unsafe { + assert!(g.cipher_init::(ptr::null(), ptr::null(), nonce.as_ptr())); + } + OpenSSLAesGcmEnc(g) + } + + fn start_dec<'a>(&'a self, nonce: &[u8; AES_GCM_NONCE_SIZE]) -> OpenSSLAesGcmDec { + let i = u64::from_be_bytes(nonce[4..].try_into().unwrap()); + let g = self.dec[(i as usize) % self.enc.len()].lock().unwrap(); + unsafe { + assert!(g.cipher_init::(ptr::null(), ptr::null(), nonce.as_ptr())); + } + OpenSSLAesGcmDec(g) + } +} + +pub struct OpenSSLAesGcm; +impl LowThroughputAesGcm for OpenSSLAesGcm { + fn encrypt_in_place( + key: &[u8; AES_256_KEY_SIZE], + nonce: &[u8; AES_GCM_NONCE_SIZE], + aad: &[u8], + data: &mut [u8], + ) -> [u8; AES_GCM_TAG_SIZE] { + let mut output = [0u8; AES_GCM_TAG_SIZE]; + let ctx = OpenSSLCtx::new().unwrap(); + unsafe { + let t = openssl_sys::EVP_aes_256_gcm(); + assert!(ctx.cipher_init::(t, key.as_ptr(), nonce.as_ptr())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + + assert!(ctx.update::(aad, ptr::null_mut())); + let p = data.as_mut_ptr(); + assert!(ctx.update::(data, p)); + + assert!(ctx.finalize::()); + assert!(ctx.get_tag(&mut output)); + } + output + } + + fn decrypt_in_place( + key: &[u8; AES_256_KEY_SIZE], + nonce: &[u8; AES_GCM_NONCE_SIZE], + aad: &[u8], + data: &mut [u8], + tag: &[u8; AES_GCM_TAG_SIZE], + ) -> bool { + let ctx = OpenSSLCtx::new().unwrap(); + unsafe { + let t = openssl_sys::EVP_aes_256_gcm(); + assert!(ctx.cipher_init::(t, key.as_ptr(), nonce.as_ptr())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + + assert!(ctx.update::(aad, ptr::null_mut())); + let p = data.as_mut_ptr(); + assert!(ctx.update::(data, p)); + + ctx.set_tag(tag) && ctx.finalize::() + } + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn aes_128_ecb() { + let key = [1u8; 16]; + let ctx = OpenSSLCtx::new().unwrap(); + unsafe { + assert!(ctx.cipher_init::(openssl_sys::EVP_aes_128_ecb(), key.as_ptr(), ptr::null())); + openssl_sys::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); + assert_eq!(openssl_sys::EVP_CIPHER_CTX_get_block_size(ctx.as_ptr()) as usize, 16); + + let origin = [2u8; 16]; + let mut val = origin; + let p = val.as_mut_ptr(); + + assert!(ctx.update::(&val, p)); + assert!(ctx.cipher_init::(ptr::null(), key.as_ptr(), ptr::null())); + assert!(ctx.update::(&val, p)); + + assert_eq!(val, origin); + } + } +} diff --git a/src/crypto_impl/p384_impl.rs b/src/crypto_impl/p384_impl.rs new file mode 100644 index 0000000..17bfb1b --- /dev/null +++ b/src/crypto_impl/p384_impl.rs @@ -0,0 +1,39 @@ +use p384::{ecdh::EphemeralSecret, CompressedPoint, PublicKey}; +use rand_core::{CryptoRng, RngCore}; + +use crate::crypto::*; + +pub type CrateP384PublicKey = PublicKey; +impl P384PublicKey for CrateP384PublicKey { + fn from_bytes(raw_key: &[u8; P384_PUBLIC_KEY_SIZE]) -> Option { + PublicKey::from_sec1_bytes(raw_key).ok() + } + + fn to_bytes(&self) -> [u8; P384_PUBLIC_KEY_SIZE] { + let k = CompressedPoint::from(self); + k.as_slice().try_into().unwrap() + } +} + +pub type CrateP384KeyPair = EphemeralSecret; +impl P384KeyPair for CrateP384KeyPair { + type PublicKey = PublicKey; + + fn generate(rng: &mut Rng) -> Self { + EphemeralSecret::random(rng) + } + + fn public_key_bytes(&self) -> [u8; P384_PUBLIC_KEY_SIZE] { + CompressedPoint::from(self.public_key()).as_slice().try_into().unwrap() + } + + fn agree(&self, public_key: &Self::PublicKey, output: &mut [u8; P384_ECDH_SHARED_SECRET_SIZE]) -> bool { + *output = self + .diffie_hellman(public_key) + .raw_secret_bytes() + .as_slice() + .try_into() + .unwrap(); + true + } +} diff --git a/src/crypto_impl/sha512.rs b/src/crypto_impl/sha512.rs new file mode 100644 index 0000000..1a1bd4d --- /dev/null +++ b/src/crypto_impl/sha512.rs @@ -0,0 +1,34 @@ +use hmac::{Hmac, Mac}; +use sha2::{Digest, Sha512}; + +use crate::crypto::*; + +pub type CrateSha512 = Sha512; +impl Sha512Hash for CrateSha512 { + fn new() -> Self { + Digest::new() + } + + fn update(&mut self, data: &[u8]) { + Digest::update(self, data) + } + + fn finish_and_reset(&mut self, output: &mut [u8; SHA512_HASH_SIZE]) { + let mut hasher = Digest::new(); + std::mem::swap(self, &mut hasher); + *output = hasher.finalize().into(); + } +} + +pub struct CrateHmacSha512; +impl Sha512Hmac for CrateHmacSha512 { + fn new() -> Self { + CrateHmacSha512 + } + + fn hash(&mut self, key: &[u8], full_input: &[u8], output: &mut [u8; SHA512_HASH_SIZE]) { + let mut hm = Hmac::::new_from_slice(key).unwrap(); + hm.update(full_input); + *output = hm.finalize().into_bytes().into() + } +} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 63c31c0..0000000 --- a/src/error.rs +++ /dev/null @@ -1,110 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - -#[derive(Debug, PartialEq, Eq)] -pub enum OpenError { - /// An invalid parameter was supplied to the function. - InvalidPublicKey, - - /// Local identity blob is too large to send, even with fragmentation. - DataTooLarge, - - RatchetIoError(IoError), -} - -#[derive(Debug, PartialEq, Eq)] -pub enum SendError { - /// An invalid parameter was supplied to the function. - InvalidParameter, - - /// The session has been marked as expired and refuses to send data. - /// Several components of ZSSP can cause this to occur, but the most likely situation to be seen - /// in practice is where rekeying repeatedly fails due to exceedingly bad network conditions. - /// - /// The associated session will no longer send or receive data and must be immediately dropped. - SessionExpired, - - /// Attempt to send using a session without a shared symmetric key. - /// The caller should wait until the handshake has completed. - SessionNotEstablished, - - /// Data object is too large to send, even with fragmentation. - DataTooLarge, -} - -/// A type of fault occurred because we received a bad packet. -/// -/// An unauthenticated attacker can intentionally trigger any of these, so it is best to -/// treat these as raw user input that needs to be sanitize. -#[derive(Debug, PartialEq, Eq)] -pub enum FaultType { - /// The received packet was addressed to an unrecognized local session. - UnknownLocalKeyId, - - /// The received packet from the remote peer was not well formed. - InvalidPacket, - - /// Packet failed one or more authentication (MAC) checks. - FailedAuthentication, - - /// Packet counter was repeated or outside window of allowed counter values. - ExpiredCounter, - - /// Packet contained protocol control parameters that are disallowed at this point in - /// time by ZSSP. - OutOfSequence, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ReceiveError { - /// A type of fault that can occur because a remote peer sent us a bad packet. - /// Such packets will be ignored by ZSSP but a user of ZSSP might want to log - /// them for debugging or tracing. - /// - /// Because an unauthenticated remote peer can force these to occur with specific - /// contained information, it is recommended in production to either drop these - /// immediately, or log them safely to a local output stream and then drop them. - ByzantineFault { - /// The type of fault that has occurred. Be cautious if you choose to read this - /// value, as an attacker has control over it. - error: FaultType, - /// Some byzantine faults within ZSSP are naturally occurring, i.e. they can occur - /// between two well behaved and trusted parties executing the protocol. - /// This boolean is true if this is one of these faults. If you go to the file and - /// line number specified by this error you will find a comment describing - /// how and why exactly this fault can occur naturally. - /// - /// Faults that can occur because the underlying communication medium is lossy and - /// sequentially inconsistent (as in UDP) are considered naturally occurring. - /// However ZSSP considers faults that occur because data integrity has not been - /// persevered (i.e. bits have been flipped) to be unnatural. - /// ZSSP also considers collisions of what are supposed to be uniform random - /// numbers to be unnatural. - is_naturally_occurring: bool, - /// The file of this implementation of ZSSP from which this error was generated. - file: &'static str, - /// The line number of this implementation of ZSSP from which this error was - /// generated. As such this number uniquely identifies each possible fault that - /// can occur during ZSSP. Advanced user can use this information to debug more - /// complicated usages of ZSSP. - line: u32, - }, - - /// The caller supplied data buffer is too small to receive data from the remote peer. - /// An attacker can cause this to occur, so users should place a hard upper limit on - /// how large their supplied data buffers can be. - DataBufferTooSmall, - - /// Rekeying failed and session secret has reached its hard usage count limit. - /// The associated session will no longer function and has to be dropped. - MaxKeyLifetimeExceeded, - - /// One of the ratchet saving or lookup functions returned an error, so the packet had to be - /// dropped. - RatchetIoError(IoError), -} diff --git a/src/frag_cache.rs b/src/frag_cache.rs index ebb428e..5fdd804 100644 --- a/src/frag_cache.rs +++ b/src/frag_cache.rs @@ -1,15 +1,8 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - use std::collections::hash_map::RandomState; use std::hash::{BuildHasher, Hash, Hasher}; use std::mem::MaybeUninit; +use crate::crypto::AES_GCM_NONCE_SIZE; use crate::fragged::Assembled; use crate::proto::{MAX_FRAGMENTS, MAX_UNASSOCIATED_FRAGMENTS, MAX_UNASSOCIATED_PACKETS, MAX_UNASSOCIATED_PACKET_SIZE}; @@ -17,7 +10,7 @@ struct PacketMetadata { key: u64, frags_idx: u32, fragment_have: u64, - fragment_count: u32, + fragment_count: u8, packet_size: u32, creation_time: i64, } @@ -55,24 +48,27 @@ impl UnassociatedFragCache { /// Will check that aad is the same for all fragments. pub(crate) fn assemble( &mut self, - nonce: [u8; 10], + nonce: &[u8; AES_GCM_NONCE_SIZE], remote_address: impl Hash, fragment_size: usize, fragment: Fragment, - fragment_no: u8, - fragment_count: u8, - timeout: i64, + fragment_no: usize, + fragment_count: usize, + timeout_interval: i64, current_time: i64, ret_assembled: &mut Assembled, ) { debug_assert!(MAX_FRAGMENTS < MAX_UNASSOCIATED_FRAGMENTS); - if fragment_no >= fragment_count || (fragment_count as usize) > MAX_FRAGMENTS || fragment_size > MAX_UNASSOCIATED_PACKET_SIZE { + if fragment_no >= fragment_count + || fragment_count > MAX_FRAGMENTS + || fragment_size > MAX_UNASSOCIATED_PACKET_SIZE + { return; } let mut hasher = self.dos_salt.build_hasher(); remote_address.hash(&mut hasher); - hasher.write(&nonce); + hasher.write(nonce); let mut key = hasher.finish(); if key == 0 { key = 1; @@ -98,7 +94,7 @@ impl UnassociatedFragCache { } else if self.map[idx0].key == 0 || self.map[idx1].key == 0 { if (fragment_count as usize) > self.frags_unused_size { // There are not enough free fragment slots so attempt to expire a bunch of entries. - self.check_for_expiry(timeout, current_time); + self.check_for_expiry(timeout_interval, current_time); } if self.map[idx0].key == 0 { idx0 @@ -107,7 +103,7 @@ impl UnassociatedFragCache { } } else { // No room for a new entry so attempt to expire a bunch of entries. - self.check_for_expiry(timeout, current_time); + self.check_for_expiry(timeout_interval, current_time); if self.map[idx0].key == 0 { idx0 } else if self.map[idx1].key == 0 { @@ -121,11 +117,11 @@ impl UnassociatedFragCache { if self.map[idx].key == 0 { // This is a new entry so initialize it. if (fragment_count as usize) <= self.frags_unused_size { - let mut entry = &mut self.map[idx]; + let entry = &mut self.map[idx]; entry.key = key; entry.frags_idx = self.frags_first_unused as u32; entry.fragment_have = 0; - entry.fragment_count = fragment_count as u32; + entry.fragment_count = fragment_count as u8; entry.packet_size = 0; entry.creation_time = current_time; @@ -139,11 +135,14 @@ impl UnassociatedFragCache { return; } } - let mut entry = &mut self.map[idx]; + let entry = &mut self.map[idx]; let new_size = entry.packet_size + fragment_size as u32; let got = 1u64.wrapping_shl(fragment_no as u32); - if got & entry.fragment_have == 0 && fragment_count == entry.fragment_count as u8 && new_size <= MAX_UNASSOCIATED_PACKET_SIZE as u32 { + if got & entry.fragment_have == 0 + && fragment_count == entry.fragment_count as usize + && new_size <= MAX_UNASSOCIATED_PACKET_SIZE as u32 + { entry.packet_size = new_size; entry.fragment_have |= got; @@ -151,24 +150,11 @@ impl UnassociatedFragCache { self.frags[frag_idx].write(fragment); if entry.fragment_have == 1u64.wrapping_shl(fragment_count as u32) - 1 { - ret_assembled.empty(); - ret_assembled.1 = fragment_count as usize; + debug_assert!(ret_assembled.is_empty()); let start_idx = entry.frags_idx as usize; - // This is a ring buffer copy into ret_assembled. - // The fragments are moved into the `ret_assembled` container and returned. - // That container will drop them when it is dropped. - if start_idx + ret_assembled.1 <= self.frags.len() { - // Copy does not occur at the buffer's boundary - unsafe { - std::ptr::copy_nonoverlapping(&self.frags[start_idx], &mut ret_assembled.0[0], ret_assembled.1); - } - } else { - // Copy does occur at the buffer's boundary - let first_chunk_size = self.frags.len() - start_idx; - let second_chunk_size = ret_assembled.1 - first_chunk_size; - unsafe { - std::ptr::copy_nonoverlapping(&self.frags[start_idx], &mut ret_assembled.0[0], first_chunk_size); - std::ptr::copy_nonoverlapping(&self.frags[0], &mut ret_assembled.0[first_chunk_size], second_chunk_size); + unsafe { + for i in start_idx..start_idx + fragment_count { + ret_assembled.push(self.frags[i % self.frags.len()].assume_init_read()) } } self.invalidate::(idx); @@ -234,9 +220,20 @@ impl Drop for UnassociatedFragCache { } } -/* #[test] fn test_cache() { + use std::sync::Mutex; + fn xorshift64_random() -> u64 { + static XORSHIFT64_STATE: Mutex = Mutex::new(12); + let mut x = XORSHIFT64_STATE.lock().unwrap(); + *x ^= x.wrapping_shr(12); + *x ^= x.wrapping_shl(25); + *x ^= x.wrapping_shr(27); + let r = *x; + drop(x); + r.wrapping_mul(0x2545F4914F6CDD1Du64) + } + let mut cache = UnassociatedFragCache::new(); let mut assembled = Assembled::new(); @@ -245,8 +242,8 @@ fn test_cache() { let mut in_progress_fragments = 0; // A basic fuzzer for testing the cache. for i in 0..5000u32 { - let fragment_count = (random::xorshift64_random() as usize % MAX_FRAGMENTS) + 1; - let r = random::xorshift64_random() as u8; + let fragment_count = (xorshift64_random() as usize % MAX_FRAGMENTS) + 1; + let r = xorshift64_random() as u8; if r & 1 == 0 { let mut packet = Vec::new(); for j in 0..fragment_count { @@ -255,21 +252,38 @@ fn test_cache() { } in_progress.push((i, fragment_count as u8, packet)); } else { - assembled.empty(); - let drop = random::xorshift64_random() as usize % (2 * fragment_count); + assembled.clear(); + let drop = xorshift64_random() as usize % (2 * fragment_count); for j in 0..fragment_count { if drop != j { let fragment = vec![0, 1, 2, 3, 4, 5, 6, r]; // If the timeout is 1 we should be guaranteed to get our packet cached. - let mut nonce = [0; 10]; + let mut nonce = [0; 12]; nonce[..4].copy_from_slice(&i.to_be_bytes()); - cache.assemble(nonce, 0, fragment.len(), fragment, j as u8, fragment_count as u8, 1, time, &mut assembled); + cache.assemble( + &nonce, + 0, + fragment.len(), + fragment, + j, + fragment_count, + 1, + time, + &mut assembled, + ); time += 1; } } if drop >= fragment_count { - assert!(!assembled.is_empty(), "Packet was dropped from the cache when it shouldn't have"); - assert_eq!(assembled.as_ref().len(), fragment_count, "Cache returned the wrong packet"); + assert!( + !assembled.is_empty(), + "Packet was dropped from the cache when it shouldn't have" + ); + assert_eq!( + assembled.as_ref().len(), + fragment_count, + "Cache returned the wrong packet" + ); for j in 0..fragment_count { assert_eq!(assembled.as_ref()[j][7], r, "Cache returned a corrupted packet"); } @@ -279,16 +293,27 @@ fn test_cache() { } if r > 200 { if in_progress.len() > 0 { - let to_remain = (random::xorshift64_random() as usize % in_progress_fragments) + 16; + let to_remain = (xorshift64_random() as usize % in_progress_fragments) + 16; while in_progress_fragments > to_remain { - let (id, fragment_count, mut packet) = in_progress.swap_remove(random::xorshift64_random() as usize % in_progress.len()); - for _ in 0..((random::xorshift64_random() as usize % packet.len()) + 1) { - let (no, fragment) = packet.swap_remove(random::xorshift64_random() as usize % packet.len()); + let (id, fragment_count, mut packet) = + in_progress.swap_remove(xorshift64_random() as usize % in_progress.len()); + for _ in 0..((xorshift64_random() as usize % packet.len()) + 1) { + let (no, fragment) = packet.swap_remove(xorshift64_random() as usize % packet.len()); - assembled.empty(); - let mut nonce = [0; 10]; + assembled.clear(); + let mut nonce = [0; 12]; nonce[..4].copy_from_slice(&id.to_be_bytes()); - cache.assemble(nonce, 0, fragment.len(), fragment, no, fragment_count, 1000, time, &mut assembled); + cache.assemble( + &nonce, + 0, + fragment.len(), + fragment, + no as usize, + fragment_count as usize, + 1000, + time, + &mut assembled, + ); time += 1; in_progress_fragments -= 1; @@ -304,4 +329,3 @@ fn test_cache() { } } } - */ diff --git a/src/fragged.rs b/src/fragged.rs index a9eb2cc..b3c293a 100644 --- a/src/fragged.rs +++ b/src/fragged.rs @@ -1,58 +1,21 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - +use arrayvec::ArrayVec; use std::mem::{needs_drop, zeroed, MaybeUninit}; -use std::ptr::slice_from_raw_parts; -use crate::proto::MAX_FRAGMENTS; +use crate::crypto::AES_GCM_NONCE_SIZE; +use crate::proto::{MAX_FRAGMENTS, NONCE_SIZE_DIFF}; -pub(crate) struct Assembled(pub(crate) [MaybeUninit; MAX_FRAGMENTS], pub(crate) usize); - -impl Assembled { - pub(crate) fn new() -> Self { - Self(unsafe { MaybeUninit::<[MaybeUninit<_>; MAX_FRAGMENTS]>::uninit().assume_init() }, 0) - } - pub(crate) fn is_empty(&self) -> bool { - self.1 == 0 - } - pub(crate) fn empty(&mut self) { - for i in 0..self.1 { - unsafe { - self.0.get_unchecked_mut(i).assume_init_drop(); - } - } - self.1 = 0; - } -} -impl AsRef<[Fragment]> for Assembled { - #[inline(always)] - fn as_ref(&self) -> &[Fragment] { - unsafe { &*slice_from_raw_parts(self.0.as_ptr().cast::(), self.1) } - } -} -impl Drop for Assembled { - #[inline(always)] - fn drop(&mut self) { - self.empty() - } -} +pub type Assembled = ArrayVec; /// Fast packet defragmenter pub struct Fragged { - count: u32, - have: u64, nonce: [u8; 10], + count: u8, + have: u64, size: usize, frags: [MaybeUninit; MAX_FRAGMENTS], } impl Fragged { - #[inline(always)] pub fn new() -> Self { debug_assert!(MAX_FRAGMENTS <= 64); unsafe { zeroed() } @@ -64,31 +27,31 @@ impl Fragged { /// be reused to assemble another packet. /// /// Will check that aad is the same for all fragments. - #[inline] pub(crate) fn assemble( &mut self, - nonce: [u8; 10], + nonce: &[u8; AES_GCM_NONCE_SIZE], fragment: Fragment, - fragment_no: u8, - fragment_count: u8, + fragment_no: usize, + fragment_count: usize, ret_assembled: &mut Assembled, ) { - if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { + if fragment_no < fragment_count && fragment_count <= MAX_FRAGMENTS { + let nonce = nonce[NONCE_SIZE_DIFF..].try_into().unwrap(); // If the counter has changed, reset the structure to receive a new packet. if nonce != self.nonce { self.drop_in_place(); - self.count = fragment_count as u32; + self.count = fragment_count as u8; self.nonce = nonce; self.size = 0; } let got = 1u64.wrapping_shl(fragment_no as u32); - if got & self.have == 0 && self.count as u8 == fragment_count { + if got & self.have == 0 && self.count == fragment_count as u8 { self.have |= got; unsafe { self.frags.get_unchecked_mut(fragment_no as usize).write(fragment); } - if self.have == 1u64.wrapping_shl(self.count) - 1 { + if self.have == 1u64.wrapping_shl(self.count as u32) - 1 { self.have = 0; self.count = 0; self.nonce = [0; 10]; @@ -96,10 +59,10 @@ impl Fragged { // Setting 'have' to 0 resets the state of this object, and the fragments // are effectively moved into the Assembled<> container and returned. That // container will drop them when it is dropped. - ret_assembled.empty(); - ret_assembled.1 = fragment_count as usize; unsafe { - std::ptr::copy_nonoverlapping(&self.frags[0], &mut ret_assembled.0[0], ret_assembled.1); + for i in 0..fragment_count { + ret_assembled.push(self.frags[i].assume_init_read()); + } } } } @@ -107,7 +70,6 @@ impl Fragged { } /// Drops any remaining fragments and resets this object. - #[inline(always)] pub fn drop_in_place(&mut self) { if needs_drop::() { let mut have = self.have; @@ -129,7 +91,6 @@ impl Fragged { } impl Drop for Fragged { - #[inline(always)] fn drop(&mut self) { self.drop_in_place(); } diff --git a/src/handshake_cache.rs b/src/handshake_cache.rs index 25f219a..1425a9b 100644 --- a/src/handshake_cache.rs +++ b/src/handshake_cache.rs @@ -1,33 +1,25 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - use std::num::NonZeroU32; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; -use crate::zssp::NoiseXKBobHandshakeState; -use crate::{proto::MAX_UNASSOCIATED_HANDSHAKE_STATES, ApplicationLayer}; +use crate::zeta::StateB2; +use crate::{application::CryptoLayer, proto::MAX_UNASSOCIATED_HANDSHAKE_STATES}; -pub(crate) struct UnassociatedHandshakeCache { +pub(crate) struct UnassociatedHandshakeCache { has_pending: AtomicBool, // Allowed to be falsely positive cache: RwLock>, } /// SoA format -struct CacheInner { +struct CacheInner { local_ids: [Option; MAX_UNASSOCIATED_HANDSHAKE_STATES], timeouts: [i64; MAX_UNASSOCIATED_HANDSHAKE_STATES], - handshakes: [Option>>; MAX_UNASSOCIATED_HANDSHAKE_STATES], + handshakes: [Option>>; MAX_UNASSOCIATED_HANDSHAKE_STATES], } /// Linear-search cache for capping the memory consumption of handshake data. /// Designed specifically to have short and simple code that clearly bounds above /// memory consumption. -impl UnassociatedHandshakeCache { +impl UnassociatedHandshakeCache { pub(crate) fn new() -> Self { Self { has_pending: AtomicBool::new(false), @@ -38,7 +30,7 @@ impl UnassociatedHandshakeCache { }), } } - pub(crate) fn get(&self, local_id: NonZeroU32) -> Option>> { + pub(crate) fn get(&self, local_id: NonZeroU32) -> Option>> { let cache = self.cache.read().unwrap(); for (i, id) in cache.local_ids.iter().enumerate() { if *id == Some(local_id) { @@ -47,7 +39,7 @@ impl UnassociatedHandshakeCache { } None } - pub(crate) fn insert(&self, local_id: NonZeroU32, state: Arc>, current_time: i64) { + pub(crate) fn insert(&self, local_id: NonZeroU32, state: Arc>, current_time: i64) { let mut cache = self.cache.write().unwrap(); let mut idx = 0; for i in 0..cache.local_ids.len() { @@ -59,7 +51,7 @@ impl UnassociatedHandshakeCache { } } cache.local_ids[idx] = Some(local_id); - cache.timeouts[idx] = current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS); + cache.timeouts[idx] = current_time + Application::SETTINGS.fragment_assembly_timeout as i64; cache.handshakes[idx] = Some(state); self.has_pending.store(true, Ordering::Release); } diff --git a/src/indexed_heap.rs b/src/indexed_heap.rs index 8a0547c..c83b8ca 100644 --- a/src/indexed_heap.rs +++ b/src/indexed_heap.rs @@ -42,7 +42,6 @@ impl IndexedBinaryHeap { .first_mut() .map(|entry| (&mut entry.0, &entry.1, BinaryHeapIndex(entry.2, self.map[entry.2].1))) } - #[inline] fn swap(&mut self, a: usize, b: usize) { self.map[self.data[a].2].0 = b; self.map[self.data[b].2].0 = a; @@ -53,7 +52,8 @@ impl IndexedBinaryHeap { let child0_idx = parent_idx * 2 + 1; let child1_idx = child0_idx + 1; if child0_idx < self.data.len() { - let largest_child = if child1_idx < self.data.len() && self.data[child1_idx].1 > self.data[child0_idx].1 { + let largest_child = if child1_idx < self.data.len() && self.data[child1_idx].1 > self.data[child0_idx].1 + { child1_idx } else { child0_idx @@ -156,7 +156,8 @@ impl IndexedBinaryHeap { .map(|data_idx| std::mem::replace(&mut self.data[data_idx].0, new_item)) } pub fn get(&self, idx: BinaryHeapIndex) -> Option<(&T, &P)> { - self.deref_index(idx).map(|data_idx| (&self.data[data_idx].0, &self.data[data_idx].1)) + self.deref_index(idx) + .map(|data_idx| (&self.data[data_idx].0, &self.data[data_idx].1)) } pub fn get_mut(&mut self, idx: BinaryHeapIndex) -> Option<(&mut T, &P)> { self.deref_index(idx).map(|data_idx| { diff --git a/src/lib.rs b/src/lib.rs index 46d0713..caf7a4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,21 +6,24 @@ * https://www.zerotier.com/ */ pub mod crypto; +pub mod crypto_impl; -mod applicationlayer; +mod antireplay; +mod challenge; mod frag_cache; mod fragged; mod handshake_cache; mod indexed_heap; mod log_event; -mod proto; mod ratchet_state; mod symmetric_state; +mod zeta; mod zssp; -pub mod error; -pub use crate::applicationlayer::ApplicationLayer; -pub use crate::log_event::LogEvent; -pub use crate::proto::{MAX_IDENTITY_BLOB_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU, RATCHET_SIZE}; -pub use crate::ratchet_state::RatchetState; -pub use crate::zssp::{Context, ContextInner, IncomingSessionAction, ReceiveResult, Session, SessionEvent}; +pub mod application; +pub mod proto; +pub mod result; + +pub use crate::log_event::*; +pub use crate::zeta::*; +pub use crate::zssp::*; diff --git a/src/log_event.rs b/src/log_event.rs index 3c033f7..3671abd 100644 --- a/src/log_event.rs +++ b/src/log_event.rs @@ -1,74 +1,88 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ use std::sync::Arc; -use crate::{ApplicationLayer, Session}; +use crate::application::CryptoLayer; +use crate::zeta::Session; /// ZSSP events that might be interesting to log or aggregate into metrics. -pub enum LogEvent<'a, Application: ApplicationLayer> { - ServiceXK1Resend(&'a Arc>), - ServiceXK3Resend(&'a Arc>), - ServiceXKTimeout(&'a Arc>), - ServiceKKStart(&'a Arc>), - ServiceKK1Resend(&'a Arc>), - ServiceKK2Resend(&'a Arc>), - ServiceKKTimeout(&'a Arc>), - ServiceKeyConfirmResend(&'a Arc>), - ServiceKeyConfirmTimeout(&'a Arc>), - /// `(fragment_count, fragment_no, packet_type)` - ReceiveUnassociatedFragment(u8, u8, u8), - ReceiveUncheckedXK1, - ReceiveCheckXK1Challenge(bool), - ReceiveValidXK1, - ReceiveUncheckedDOSChallenge, - ReceiveValidDOSChallenge(&'a Arc>), - ReceiveUncheckedXK2, - ReceiveValidXK2(&'a Arc>), - ReceiveUncheckedXK3, - ReceiveValidXK3(&'a Application::Data), - ReceiveUncheckedKK1, - ReceiveValidKK1(&'a Arc>), - ReceiveUncheckedKK2, - ReceiveValidKK2(&'a Arc>), - ReceiveValidKeyConfirm(&'a Arc>), - ReceiveValidKeyDelete(&'a Arc>), +pub enum LogEvent<'a, Crypto: CryptoLayer> { + ResentX1(&'a Arc>), + TimeoutX1(&'a Arc>), + TimeoutX2, + ResentX3(&'a Arc>), + TimeoutX3(&'a Arc>), + ResentKeyConfirm(&'a Arc>), + TimeoutKeyConfirm(&'a Arc>), + StartedRekeyingSentK1(&'a Arc>), + ResentK1(&'a Arc>), + TimeoutK1(&'a Arc>), + ResentK2(&'a Arc>), + TimeoutK2(&'a Arc>), + /// `(packet_type, packet_counter, fragment_no, fragment_count)` + ReceivedRawFragment(u8, u64, usize, usize), + ReceivedRawX1, + X1FailedChallengeSentNewChallenge, + X1SucceededChallenge, + X1IsAuthSentX2, + ReceivedRawChallenge, + ChallengeIsAuth(&'a Arc>), + ReceivedRawX2, + X2IsAuthSentX3(&'a Arc>), + ReceivedRawX3, + X3IsAuthSentKeyConfirm(&'a Arc>), + ReceivedRawKeyConfirm, + KeyConfirmIsAuthSentAck(&'a Arc>), + ReceivedRawAck, + AckIsAuth(&'a Arc>), + ReceivedRawK1, + K1IsAuthSentK2(&'a Arc>), + ReceivedRawK2, + K2IsAuthSentKeyConfirm(&'a Arc>), + ReceivedRawD, + DIsAuthClosedSession(&'a Arc>), } -impl<'a, Application: ApplicationLayer> std::fmt::Debug for LogEvent<'a, Application> { + +impl<'a, Crypto: CryptoLayer> std::fmt::Debug for LogEvent<'a, Crypto> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use LogEvent::*; match self { - ServiceXK1Resend(_) => write!(f, "ServiceXK1Resend"), - ServiceXK3Resend(_) => write!(f, "ServiceXK3Resend"), - ServiceXKTimeout(_) => write!(f, "ServiceXKTimeout"), - ServiceKKStart(_) => write!(f, "ServiceKKStart"), - ServiceKK1Resend(_) => write!(f, "ServiceKK1Resend"), - ServiceKK2Resend(_) => write!(f, "ServiceKK2Resend"), - ServiceKKTimeout(_) => write!(f, "ServiceKKTimeout"), - ServiceKeyConfirmResend(_) => write!(f, "ServiceKeyConfirmResend"), - ServiceKeyConfirmTimeout(_) => write!(f, "ServiceKeyConfirmTimeout"), - ReceiveUnassociatedFragment(arg0, arg1, arg2) => { - f.debug_tuple("ReceiveUnassociatedFragment").field(arg0).field(arg1).field(arg2).finish() - } - ReceiveUncheckedXK1 => write!(f, "ReceiveUncheckedXK1"), - ReceiveCheckXK1Challenge(arg0) => f.debug_tuple("ReceiveCheckXK1Challenge").field(arg0).finish(), - ReceiveValidXK1 => write!(f, "ReceiveValidXK1"), - ReceiveUncheckedDOSChallenge => write!(f, "ReceiveUncheckedDOSChallenge"), - ReceiveValidDOSChallenge(_) => write!(f, "ReceiveValidDOSChallenge"), - ReceiveUncheckedXK2 => write!(f, "ReceiveUncheckedXK2"), - ReceiveValidXK2(_) => write!(f, "ReceiveValidXK2"), - ReceiveUncheckedXK3 => write!(f, "ReceiveUncheckedXK3"), - ReceiveValidXK3(_) => write!(f, "ReceiveValidXK3"), - ReceiveUncheckedKK1 => write!(f, "ReceiveUncheckedKK1"), - ReceiveValidKK1(_) => write!(f, "ReceiveValidKK1"), - ReceiveUncheckedKK2 => write!(f, "ReceiveUncheckedKK2"), - ReceiveValidKK2(_) => write!(f, "ReceiveValidKK2"), - ReceiveValidKeyConfirm(_) => write!(f, "ReceiveValidKeyConfirm"), - ReceiveValidKeyDelete(_) => write!(f, "ReceiveValidKeyDelete"), + Self::ResentX1(_) => f.debug_tuple("ResentX1").finish(), + Self::TimeoutX1(_) => f.debug_tuple("TimeoutX1").finish(), + Self::TimeoutX2 => write!(f, "TimeoutX2"), + Self::ResentX3(_) => f.debug_tuple("ResentX3").finish(), + Self::TimeoutX3(_) => f.debug_tuple("TimeoutX3").finish(), + Self::ResentKeyConfirm(_) => f.debug_tuple("ResentKeyConfirm").finish(), + Self::TimeoutKeyConfirm(_) => f.debug_tuple("TimeoutKeyConfirm").finish(), + Self::StartedRekeyingSentK1(_) => f.debug_tuple("StartedRekeyingSentK1").finish(), + Self::ResentK1(_) => f.debug_tuple("ResentK1").finish(), + Self::TimeoutK1(_) => f.debug_tuple("TimeoutK1").finish(), + Self::ResentK2(_) => f.debug_tuple("ResentK2").finish(), + Self::TimeoutK2(_) => f.debug_tuple("TimeoutK2").finish(), + Self::ReceivedRawFragment(arg0, arg1, arg2, arg3) => f + .debug_tuple("ReceivedRawFragment") + .field(arg0) + .field(arg1) + .field(arg2) + .field(arg3) + .finish(), + Self::ReceivedRawX1 => write!(f, "ReceivedRawX1"), + Self::X1FailedChallengeSentNewChallenge => write!(f, "X1FailedChallengeSentNewChallenge"), + Self::X1SucceededChallenge => write!(f, "X1SucceededChallenge"), + Self::X1IsAuthSentX2 => write!(f, "X1IsAuthSentX2"), + Self::ReceivedRawChallenge => write!(f, "ReceivedRawChallenge"), + Self::ChallengeIsAuth(_) => f.debug_tuple("ChallengeIsAuth").finish(), + Self::ReceivedRawX2 => write!(f, "ReceivedRawX2"), + Self::X2IsAuthSentX3(_) => f.debug_tuple("X2IsAuthSentX3").finish(), + Self::ReceivedRawX3 => write!(f, "ReceivedRawX3"), + Self::X3IsAuthSentKeyConfirm(_) => f.debug_tuple("X3IsAuthSentKeyConfirm").finish(), + Self::ReceivedRawKeyConfirm => write!(f, "ReceivedRawKeyConfirm"), + Self::KeyConfirmIsAuthSentAck(_) => f.debug_tuple("KeyConfirmIsAuthSentAck").finish(), + Self::ReceivedRawAck => write!(f, "ReceivedRawAck"), + Self::AckIsAuth(_) => f.debug_tuple("AckIsAuth").finish(), + Self::ReceivedRawK1 => write!(f, "ReceivedRawK1"), + Self::K1IsAuthSentK2(_) => f.debug_tuple("K1IsAuthSentK2").finish(), + Self::ReceivedRawK2 => write!(f, "ReceivedRawK2"), + Self::K2IsAuthSentKeyConfirm(_) => f.debug_tuple("K2IsAuthSentKeyConfirm").finish(), + Self::ReceivedRawD => write!(f, "ReceivedRawD"), + Self::DIsAuthClosedSession(_) => f.debug_tuple("DIsAuthClosedSession").finish(), } } } diff --git a/src/proto.rs b/src/proto.rs index 634e8d5..da39de7 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -1,131 +1,59 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ +use crate::crypto::*; -use std::hash::Hasher; -use std::mem::size_of; - -use crate::crypto::aes_gcm::AES_GCM_TAG_SIZE; -use crate::crypto::p384::P384_PUBLIC_KEY_SIZE; -use crate::crypto::pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_PUBLICKEYBYTES}; -use crate::crypto::sha512::{Sha512, SHA512_HASH_SIZE}; -use hex_literal::hex; +/* Common constants */ /// Minimum size of a valid physical ZSSP packet of any type. Anything smaller is discarded. pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; - /// Minimum physical MTU for ZSSP to function. +/// If an MTU is passed to ZSSP that is lower than this, it will be ignored and instead this value +/// will be used. pub const MIN_TRANSPORT_MTU: usize = 128; -pub const RATCHET_SIZE: usize = 32; +pub(crate) const KID_SIZE: usize = 4; -/// The application has the ability to attach a data payload to Alice's handshake. -/// It will be the first payload Bob receives from Alice. -/// The application also must attach a static public identity to their handshake. -/// The combined size of both in bytes must be at most this value. -/// -/// If not ZSSP will return `OpenError::DataTooLarge` and refuse to create a session object. -pub const MAX_IDENTITY_BLOB_SIZE: usize = NoiseXKPattern3::MAX_SIZE - NoiseXKPattern3::MIN_SIZE; +/* Challenge protocol constants */ -/// Initial value of 'h'. -/// echo -n 'Noise_XKhfs+psk2_P384+Kyber1024_AESGCM_SHA512' | shasum -a 512 -pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = - hex!("cd1f422196a5a614e24392cf34dcbf340ee61ad6ee6834274ff35fd42a7a5c44d04a045101555548a291778dd036b93ae21005a26c003213f57a5df9fb17f745"); -/// Initial value of 'ck' for rekeying. -/// echo -n 'Noise_KKpsk0_P384_AESGCM_SHA512' | shasum -a 512 -pub(crate) const INITIAL_H_REKEY: [u8; SHA512_HASH_SIZE] = - hex!("daeedd651ac9c5173f2eaaff996beebac6f3f1bfe9a70bb1cc54fa1fb2bf46260d71a3c4fb4d4ee36f654c31773a8a15e5d5be974a0668dc7db70f4e13ed172e"); +pub(crate) const SALT_SIZE: usize = 32; -pub(crate) const SESSION_ID_SIZE: usize = 4; +pub(crate) const COUNTER_SIZE: usize = 8; +pub(crate) const MAC_SIZE: usize = 16; +pub(crate) const POW_SIZE: usize = 8; +pub(crate) const POW_START: usize = COUNTER_SIZE + MAC_SIZE; -pub(crate) const PACKET_TYPE_NOISE_XK_PATTERN_1: u8 = 0; -pub(crate) const PACKET_TYPE_NOISE_XK_PATTERN_2: u8 = 1; -pub(crate) const PACKET_TYPE_NOISE_XK_PATTERN_3: u8 = 2; -pub(crate) const PACKET_TYPE_KEY_CONFIRM: u8 = 3; -pub(crate) const PACKET_TYPE_ACK: u8 = 4; -pub(crate) const PACKET_TYPE_NOISE_KK_PATTERN_1: u8 = 5; -pub(crate) const PACKET_TYPE_NOISE_KK_PATTERN_2: u8 = 6; -pub(crate) const PACKET_TYPE_SESSION_REJECTED: u8 = 7; -pub(crate) const PACKET_TYPE_DATA: u8 = 8; -pub(crate) const PACKET_TYPE_BOB_DOS_CHALLENGE: u8 = 9; -pub(crate) const PACKET_TYPE_RANGE_TRANSPORT: std::ops::Range = 3..9; +pub(crate) const CHALLENGE_SIZE: usize = COUNTER_SIZE + MAC_SIZE + POW_SIZE; +pub(crate) const DIFFICULTY: u32 = 13; -/// Noise asks that the counter be initialized to 0 but for out of order reasons we have -/// to start it at 1. -/// Since with unreliable transport the first counter could always end up dropped this is -/// functionally equivalent to initializing to 0. -pub(crate) const INIT_COUNTER: u64 = 0; -pub(crate) const LABEL_RATCHET_STATE: u8 = b'R'; -pub(crate) const LABEL_HEADER_KEY: u8 = b'H'; -pub(crate) const LABEL_KEX_KEY: u8 = b'K'; - -/// Size of keys used during derivation, mixing, etc. -pub(crate) const NOISE_HASHLEN: usize = SHA512_HASH_SIZE; +pub(crate) const HEADERED_CHALLENGE_SIZE: usize = CHALLENGE_SIZE + HEADER_SIZE + KID_SIZE; +/* Fragmentation constants */ +/* +Header: + [0..4] recipient key id +-- start AES(ck_es * h_e_e1_p) encrypted block -- + [5] fragment number (0..254) + [4] fragment count (1..255) +-- start packet nonce -- + [6] reserved zero + [7] packet type + [8..16] 64-bit counter +*/ pub(crate) const HEADER_SIZE: usize = 16; -pub(crate) const HEADER_PROTECT_ENC_START: usize = 4; -pub(crate) const HEADER_PROTECT_ENC_END: usize = 20; -pub(crate) const CHALLENGE_COUNTER_SIZE: usize = 8; -pub(crate) const CHALLENGE_MAC_SIZE: usize = 16; -pub(crate) const CHALLENGE_POW_SIZE: usize = 8; -pub(crate) const CHALLENGE_SALT_SIZE: usize = 32; +pub(crate) const PACKET_NONCE_SIZE: usize = 10; -pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_FRAGMENTS * MIN_TRANSPORT_MTU; -pub(crate) const CONTROL_PACKET_MAX_SIZE: usize = HEADER_SIZE + NoiseKKPattern1or2::SIZE + AES_GCM_TAG_SIZE; -pub(crate) const CONTROL_PACKET_MIN_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE; +pub(crate) const HEADER_AUTH_START: usize = 4; +pub(crate) const HEADER_AUTH_END: usize = 20; +pub(crate) const PACKET_NONCE_START: usize = HEADER_SIZE - PACKET_NONCE_SIZE; -/// Determines the number of counters a session will remember. If a counter arrives over -/// this amount out of order relative to other received counters, it is likely to be -/// rejected on the basis that the session can't remember if this counter was replayed. -/// Increasing this value makes a session consume more memory. -pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 64; -/// Maximum number of counter steps that the counter is allowed to skip ahead. -/// This cannot be changed away from 2^24 without changing the header nonce handling code. -pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 16777216; -/// Similar to `COUNTER_WINDOW_MAX_OOO`, except this governs the receive context challenge -/// counter rather than the session counter. -/// When Bob issues a challenge to Alice to mitigate DDOS, Bob will only accept Alice's -/// response once, and then its attached counter is added to the window. -pub(crate) const CHALLENGE_COUNTER_WINDOW_MAX_OOO: usize = 32; -/// We hard-expire the Noise counter long before we reach u64::MAX because of the ABA problem. -/// Over (1<<16) threads would have to attempt to increment the counter at the same time -/// to overflow it. -/// Having (1<<16) threads active at the same time would crash basically any system. -pub(crate) const THREAD_SAFE_COUNTER_HARD_EXPIRE: u64 = u64::MAX - (1 << 16); +pub(crate) const FRAGMENT_NO_IDX: usize = 4; +pub(crate) const FRAGMENT_COUNT_IDX: usize = 5; /// Maximum number of fragments a single packet may be split into. If a packet cannot fit /// into this number of fragments it will be dropped. -pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 -/// Maximum window over which session packets may be reordered to be defragmented and -/// reassembled. Out of order fragments may be dropped in favor of newer fragments. -/// Increasing this value makes a session consume more significantly more memory. -pub(crate) const SESSION_MAX_FRAGMENTS_OOO: usize = 32; +pub(crate) const MAX_FRAGMENTS: usize = 48; -/// The maximum number of unassociated packets that a receive context will cache. -/// Additional packets will either be dropped or cause a different packet to be dropped -/// from the cache. -/// Larger values consume more memory but provide better reliability and DDOS resistance. -pub(crate) const MAX_UNASSOCIATED_PACKETS: usize = 32; -/// The maximum number of fragments of unassociated packets that a receive context will -/// cache. -/// All unassociated fragments share the same buffer, when it fills up additional -/// fragments will be dropped or cause other fragments to be dropped from the cache. -/// Larger values consume more memory but provide better reliability and DDOS resistance. -pub(crate) const MAX_UNASSOCIATED_FRAGMENTS: usize = 32 * 32; -/// The maximum number of `NoiseXKBobHandshakeState` that a receive context will cache. -/// These are extremely large and since Alice has not been authenticated we put a hard -/// limit to how many we cache. -/// Larger values consume more memory but provide better reliability and DDOS resistance. -pub(crate) const MAX_UNASSOCIATED_HANDSHAKE_STATES: usize = 32; - -/// The maximum size a packet that is not associated to a session may be. -/// Excludes the size of headers for fragmentation. -pub(crate) const MAX_UNASSOCIATED_PACKET_SIZE: usize = NoiseXKPattern1::MAX_SIZE - HEADER_SIZE; +pub(crate) const NONCE_SIZE_DIFF: usize = AES_GCM_NONCE_SIZE - PACKET_NONCE_SIZE; +/* Key exchange constants */ /* XKhfs+psk2: <- s @@ -142,155 +70,113 @@ KKpsk0: -> psk, e, es, ss <- e, ee, se */ -/* -Header: - [0..4] recipient key id --- start AES(ck_es * h_e_e1_p) encrypted block -- - [4] fragment count (1..255) - [5] fragment number (0..254) - [6] reserved zero --- start AES-GCM Nonce -- - [7] packet type - [8..16] 64-bit counter or packet id -*/ -/// The first packet in Noise_XK exchange containing Alice's ephemeral keys, key id, -/// and a random symmetric key to protect header fragmentation fields for this session. -#[repr(C, packed)] -pub(crate) struct NoiseXKPattern1 { - pub header: [u8; HEADER_SIZE], - /// -- start prologue -- - pub alice_key_id: [u8; SESSION_ID_SIZE], - /// -- end prologue -- - pub noise_e: [u8; P384_PUBLIC_KEY_SIZE], - /// -- start AES-GCM(k_es) encrypted section - pub noise_e1: [u8; KYBER_PUBLICKEYBYTES], - /// -- end encrypted section - pub e1_gcm_tag: [u8; AES_GCM_TAG_SIZE], - pub payload: [u8; RATCHET_SIZE + RATCHET_SIZE + AES_GCM_TAG_SIZE + ChallengeResponse::SIZE], -} +pub(crate) const HASHLEN: usize = SHA512_HASH_SIZE; +/// The size in bytes of both a ratchet key and a ratchet fingerprint. +pub const RATCHET_SIZE: usize = 32; -#[repr(C, packed)] -pub(crate) struct ChallengeResponse { - pub challenge_counter: [u8; CHALLENGE_COUNTER_SIZE], - pub challenge_mac: [u8; CHALLENGE_MAC_SIZE], - pub challenge_pow: [u8; CHALLENGE_POW_SIZE], -} +/// Initial value of 'h'. +pub(crate) const PROTOCOL_NAME_NOISE_XK: &[u8; HASHLEN] = + b"Noise_XKhfs+psk2_P384+Kyber1024_AESGCM_SHA512\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; +/// Initial value of 'ck' for rekeying. +pub(crate) const PROTOCOL_NAME_NOISE_KK: &[u8; HASHLEN] = + b"Noise_KKpsk0_P384_AESGCM_SHA512\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; -impl NoiseXKPattern1 { - pub const PROLOGUE_START: usize = HEADER_SIZE; - pub const PROLOGUE_END: usize = Self::PROLOGUE_START + SESSION_ID_SIZE; - pub const E1_ENC_START: usize = Self::PROLOGUE_END + P384_PUBLIC_KEY_SIZE; - pub const E1_AUTH_START: usize = Self::E1_ENC_START + KYBER_PUBLICKEYBYTES; - pub const P_ENC_START: usize = Self::E1_AUTH_START + AES_GCM_TAG_SIZE; +pub(crate) const LABEL_OTP_TO_RATCHET: &[u8; 19] = b"ZSSP_OTP_TO_RATCHET"; +pub(crate) const LABEL_KBKDF_CHAIN: &[u8; 4] = b"ZSSP"; +pub(crate) const LABEL_RATCHET_STATE: &[u8; 4] = b"ASKR"; +pub(crate) const LABEL_HEADER_KEY: &[u8; 4] = b"ASKH"; +pub(crate) const LABEL_KEX_KEY: &[u8; 4] = b"ASKK"; - pub const MIN_SIZE: usize = Self::P_ENC_START + AES_GCM_TAG_SIZE + ChallengeResponse::SIZE; - pub const MAX_SIZE: usize = Self::MIN_SIZE + RATCHET_SIZE + RATCHET_SIZE; -} -impl ChallengeResponse { - pub const SIZE: usize = CHALLENGE_COUNTER_SIZE + CHALLENGE_MAC_SIZE + CHALLENGE_POW_SIZE; -} +pub(crate) const EXPIRE_AFTER_USES: u64 = 1 << 32 - 1; +pub(crate) const THREAD_SAFE_COUNTER_HARD_EXPIRE: u64 = u64::MAX - 1 << 16; +/// Determines the number of counters a session will remember. If a counter arrives over +/// this amount out of order relative to other received counters, it is likely to be +/// rejected on the basis that the session can't remember if this counter was replayed. +/// Increasing this value makes a session consume more memory. +pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 128; +/// Maximum number of counter steps that the counter is allowed to skip ahead. +/// This cannot be changed away from 2^24 without changing the header nonce handling code. +pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 1 << 24; +/// Similar to `COUNTER_WINDOW_MAX_OOO`, except this governs the receive context challenge +/// counter rather than the session counter. +/// When Bob issues a challenge to Alice to mitigate DDOS, Bob will only accept Alice's +/// response once, and then its attached counter is added to the window. +pub(crate) const CHALLENGE_COUNTER_WINDOW_MAX_OOO: usize = 32; -#[repr(C, packed)] -pub(crate) struct BobDOSChallenge { - pub header: [u8; HEADER_SIZE], - pub alice_key_id: [u8; SESSION_ID_SIZE], - pub challenge_counter: [u8; CHALLENGE_COUNTER_SIZE], - pub challenge_mac: [u8; CHALLENGE_MAC_SIZE], - pub prior_challenge_pow: [u8; CHALLENGE_POW_SIZE], -} +/* Packet constants */ -impl BobDOSChallenge { - pub const SIZE: usize = HEADER_SIZE + SESSION_ID_SIZE + CHALLENGE_COUNTER_SIZE + CHALLENGE_MAC_SIZE + CHALLENGE_POW_SIZE; -} +pub(crate) const PACKET_TYPE_HANDSHAKE_HELLO: u8 = 0; +pub(crate) const PACKET_TYPE_HANDSHAKE_RESPONSE: u8 = 1; +pub(crate) const PACKET_TYPE_HANDSHAKE_COMPLETION: u8 = 2; +pub(crate) const PACKET_TYPE_KEY_CONFIRM: u8 = 3; +pub(crate) const PACKET_TYPE_ACK: u8 = 4; +pub(crate) const PACKET_TYPE_REKEY_INIT: u8 = 5; +pub(crate) const PACKET_TYPE_REKEY_COMPLETE: u8 = 6; +pub(crate) const PACKET_TYPE_SESSION_REJECTED: u8 = 7; +pub(crate) const PACKET_TYPE_DATA: u8 = 8; +pub(crate) const PACKET_TYPE_CHALLENGE: u8 = 9; +pub(crate) const PACKET_TYPE_USES_COUNTER_RANGE: std::ops::Range = 3..9; -/// The response to NoiseXKPattern1 containing Bob's ephemeral keys. -#[repr(C, packed)] -pub(crate) struct NoiseXKPattern2 { - pub header: [u8; HEADER_SIZE], - pub noise_e: [u8; P384_PUBLIC_KEY_SIZE], - /// -- start AES-GCM(k_es_ee) encrypted section - pub noise_ekem1: [u8; KYBER_CIPHERTEXTBYTES], - /// -- end encrypted section - pub ekem1_gcm_tag: [u8; AES_GCM_TAG_SIZE], - /// -- start AES-GCM(k_es_ee_ekem1_psk) encrypted section - pub bob_key_id: [u8; SESSION_ID_SIZE], - /// -- end encrypted section - pub p_gcm_tag: [u8; AES_GCM_TAG_SIZE], -} +pub(crate) const HANDSHAKE_HELLO_MIN_SIZE: usize = + KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE; +pub(crate) const HANDSHAKE_HELLO_MAX_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + RATCHET_SIZE + RATCHET_SIZE; -impl NoiseXKPattern2 { - pub const EKEM1_ENC_START: usize = HEADER_SIZE + P384_PUBLIC_KEY_SIZE; - pub const EKEM1_AUTH_START: usize = Self::EKEM1_ENC_START + KYBER_CIPHERTEXTBYTES; - pub const P_ENC_START: usize = Self::EKEM1_AUTH_START + AES_GCM_TAG_SIZE; - pub const P_AUTH_START: usize = Self::P_ENC_START + SESSION_ID_SIZE; - pub const P_AUTH_END: usize = Self::P_AUTH_START + AES_GCM_TAG_SIZE; - pub const SIZE: usize = Self::P_AUTH_END; -} +pub(crate) const HANDSHAKE_HELLO_CHALLENGE_MIN_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + CHALLENGE_SIZE; +pub(crate) const HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE: usize = HANDSHAKE_HELLO_MAX_SIZE + CHALLENGE_SIZE; -/// Alice's final response containing her identity (she already knows Bob's) and meta-data. -/// While Alice's response does match what is described in this struct, -/// this struct is unused because it would contain variable length fields. -/// It is present here for documentation purposes. -#[repr(C, packed)] -pub(crate) struct NoiseXKPattern3 { - pub header: [u8; HEADER_SIZE], - /// -- start AES-GCM(k_es_ee_ekem1_psk) encrypted section - pub noise_s: [u8; P384_PUBLIC_KEY_SIZE], - /// -- end encrypted section - pub s_gcm_tag: [u8; AES_GCM_TAG_SIZE], - /// -- start AES-GCM(k_es_ee_ekem1_psk_se) encrypted section - pub alice_blob: [u8; 0], - /// -- end encrypted section - pub p_gcm_tag: [u8; AES_GCM_TAG_SIZE], -} -impl NoiseXKPattern3 { - pub const MIN_SIZE: usize = HEADER_SIZE + P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE; - pub const MAX_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE; -} +pub(crate) const HEADERED_HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE + HEADER_SIZE; -#[repr(C, packed)] -pub(crate) struct NoiseKKPattern1or2 { - pub header: [u8; HEADER_SIZE], - pub noise_e: [u8; P384_PUBLIC_KEY_SIZE], - pub key_id: [u8; SESSION_ID_SIZE], - pub gcm_tag: [u8; AES_GCM_TAG_SIZE], - pub kek_tag: [u8; AES_GCM_TAG_SIZE], -} -impl NoiseKKPattern1or2 { - pub const ENC_START: usize = HEADER_SIZE + P384_PUBLIC_KEY_SIZE; - pub const AUTH_START: usize = Self::ENC_START + SESSION_ID_SIZE; - pub const AUTH_END: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; - pub const SIZE: usize = Self::AUTH_END + AES_GCM_TAG_SIZE; -} +pub(crate) const HANDSHAKE_RESPONSE_SIZE: usize = + P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE; +pub(crate) const HEADERED_HANDSHAKE_RESPONSE_SIZE: usize = HANDSHAKE_RESPONSE_SIZE + HEADER_SIZE; -// Annotate only these structs as being compatible with byte_array_as_proto_buffer(). These structs -// are packed flat buffers containing only byte or byte array fields, making them safe to treat -// this way even on architectures that require type size aligned access. -pub(crate) trait ProtocolFlatBuffer {} -impl ProtocolFlatBuffer for NoiseXKPattern1 {} -impl ProtocolFlatBuffer for NoiseXKPattern2 {} -impl ProtocolFlatBuffer for NoiseKKPattern1or2 {} -impl ProtocolFlatBuffer for BobDOSChallenge {} -impl ProtocolFlatBuffer for ChallengeResponse {} +pub(crate) const HANDSHAKE_COMPLETION_MIN_SIZE: usize = P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + 0 + AES_GCM_TAG_SIZE; +pub(crate) const HANDSHAKE_COMPLETION_MAX_SIZE: usize = HANDSHAKE_COMPLETION_MIN_SIZE + IDENTITY_MAX_SIZE; -#[inline(always)] -pub(crate) fn byte_array_as_proto_buffer(b: &[u8]) -> &B { - assert!(b.len() >= size_of::()); - unsafe { &*b.as_ptr().cast() } -} +pub(crate) const HEADERED_HANDSHAKE_COMPLETION_MAX_SIZE: usize = HANDSHAKE_COMPLETION_MAX_SIZE + HEADER_SIZE; -#[inline(always)] -pub(crate) fn byte_array_as_proto_buffer_mut(b: &mut [u8]) -> &mut B { - assert!(b.len() >= size_of::()); - unsafe { &mut *b.as_mut_ptr().cast() } -} -/// Trick rust into letting us use a hasher that returns more than 64 bits. -pub(crate) struct ShaHasher<'a, ShaImpl: Sha512>(pub &'a mut ShaImpl); -impl<'a, ShaImpl: Sha512> Hasher for ShaHasher<'a, ShaImpl> { - fn finish(&self) -> u64 { - panic!() - } - fn write(&mut self, bytes: &[u8]) { - self.0.update(bytes) - } -} +pub(crate) const KEY_CONFIRMATION_SIZE: usize = AES_GCM_TAG_SIZE; +pub(crate) const HEADERED_KEY_CONFIRMATION_SIZE: usize = KEY_CONFIRMATION_SIZE + HEADER_SIZE; + +pub(crate) const ACKNOWLEDGEMENT_SIZE: usize = AES_GCM_TAG_SIZE; +pub(crate) const HEADERED_ACKNOWLEDGEMENT_SIZE: usize = ACKNOWLEDGEMENT_SIZE + HEADER_SIZE; + +pub(crate) const SESSION_REJECTED_SIZE: usize = AES_GCM_TAG_SIZE; +pub(crate) const HEADERED_SESSION_REJECTED_SIZE: usize = SESSION_REJECTED_SIZE + HEADER_SIZE; + +pub(crate) const REKEY_SIZE: usize = P384_PUBLIC_KEY_SIZE + KID_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE; +pub(crate) const HEADERED_REKEY_SIZE: usize = REKEY_SIZE + HEADER_SIZE; + +/// The application has the ability to attach a data payload to Alice's handshake. +/// It will be the first payload Bob receives from Alice. +/// The application also must attach a static public identity to their handshake. +/// The combined size of both in bytes must be at most this value. +/// +/// If not ZSSP will return `OpenError::DataTooLarge` and refuse to create a session object. +pub const IDENTITY_MAX_SIZE: usize = 4096; + +/* DOS mitigation constants */ + +/// The maximum number of `NoiseXKBobHandshakeState` that a receive context will cache. +/// These are extremely large and since Alice has not been authenticated we put a hard +/// limit to how many we cache. +/// Larger values consume more memory but provide better reliability and DDOS resistance. +pub(crate) const MAX_UNASSOCIATED_HANDSHAKE_STATES: usize = 32; + +/// The maximum number of unassociated packets that a receive context will cache. +/// Additional packets will either be dropped or cause a different packet to be dropped +/// from the cache. +/// Larger values consume more memory but provide better reliability and DDOS resistance. +pub(crate) const MAX_UNASSOCIATED_PACKETS: usize = 32; +/// The maximum number of fragments of unassociated packets that a receive context will +/// cache. +/// All unassociated fragments share the same buffer, when it fills up additional +/// fragments will be dropped or cause other fragments to be dropped from the cache. +/// Larger values consume more memory but provide better reliability and DDOS resistance. +pub(crate) const MAX_UNASSOCIATED_FRAGMENTS: usize = 32 * 32; + +/// The maximum size a packet that is not associated to a session may be. +/// Excludes the size of headers for fragmentation. +pub(crate) const MAX_UNASSOCIATED_PACKET_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE; + +pub(crate) const SESSION_MAX_FRAGMENTS_OOO: usize = 64; diff --git a/src/ratchet_state.rs b/src/ratchet_state.rs index fddbc57..72a4ca9 100644 --- a/src/ratchet_state.rs +++ b/src/ratchet_state.rs @@ -1,70 +1,165 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ +use arrayvec::ArrayVec; +use zeroize::Zeroizing; -use std::num::NonZeroU64; - -use crate::crypto::secret::Secret; -use crate::RATCHET_SIZE; - -#[derive(Clone, PartialEq, Eq)] -pub enum RatchetState { - Null, - Empty, - NonEmpty(NonEmptyRatchetState), -} -use RatchetState::*; -impl RatchetState { - #[inline] - pub fn new_nonempty(key: Secret, fingerprint: Secret, chain_len: NonZeroU64) -> Self { - NonEmpty(NonEmptyRatchetState { key, fingerprint, chain_len }) - } - #[inline] - pub fn new_initial_states() -> [RatchetState; 2] { - [RatchetState::Empty, RatchetState::Null] - } - #[inline] - pub fn is_null(&self) -> bool { - matches!(self, Null) - } - #[inline] - pub fn is_empty(&self) -> bool { - matches!(self, Empty) - } - #[inline] - pub fn nonempty(&self) -> Option<&NonEmptyRatchetState> { - match self { - NonEmpty(rs) => Some(rs), - _ => None, - } - } - #[inline] - pub fn chain_len(&self) -> u64 { - self.nonempty().map_or(0, |rs| rs.chain_len.get()) - } - #[inline] - pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - self.nonempty().map(|rs| rs.fingerprint.as_ref()) - } - #[inline] - pub fn key(&self) -> Option<&[u8; RATCHET_SIZE]> { - const ZERO_KEY: [u8; RATCHET_SIZE] = [0u8; RATCHET_SIZE]; - match self { - Null => None, - Empty => Some(&ZERO_KEY), - NonEmpty(rs) => Some(rs.key.as_ref()), - } - } -} +use crate::crypto::*; +use crate::proto::*; /// A ratchet key and fingerprint, /// along with the length of the ratchet chain the keys were derived from. -#[derive(Clone, PartialEq, Eq)] -pub struct NonEmptyRatchetState { - pub key: Secret, - pub fingerprint: Secret, - pub chain_len: NonZeroU64, +/// +/// Implements constant time equality. +/// The hash implementation only uses the ratchet fingerprint. +/// Any operation involving the ratchet key must take constant time. +/// +/// Corresponds to the Ratchet Key and Ratchet Fingerprint described in Section 3. +#[derive(Clone, Eq)] +pub struct RatchetState { + pub key: Zeroizing<[u8; RATCHET_SIZE]>, + pub fingerprint: Option>, + pub chain_len: u64, +} +impl PartialEq for RatchetState { + fn eq(&self, other: &Self) -> bool { + let ret = match (self.fingerprint.as_ref(), other.fingerprint.as_ref()) { + (Some(rf1), Some(rf2)) => secure_eq(rf1, rf2), + (None, None) => true, + _ => false, + }; + ret & secure_eq(&self.key, &other.key) & (self.chain_len == other.chain_len) + } +} +impl std::hash::Hash for RatchetState { + fn hash(&self, state: &mut H) { + if let Some(rf) = &self.fingerprint { + state.write_u64(u64::from_ne_bytes(rf[..8].try_into().unwrap())) + } + } +} +impl RatchetState { + pub fn new(key: Zeroizing<[u8; RATCHET_SIZE]>, fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, chain_len: u64) -> Self { + RatchetState { key, fingerprint: Some(fingerprint), chain_len } + } + pub fn new_raw(key: [u8; RATCHET_SIZE], fingerprint: [u8; RATCHET_SIZE], chain_len: u64) -> Self { + RatchetState { + key: Zeroizing::new(key), + fingerprint: Some(Zeroizing::new(fingerprint)), + chain_len, + } + } + pub fn empty() -> Self { + RatchetState { + key: Zeroizing::new([0u8; RATCHET_SIZE]), + fingerprint: None, + chain_len: 0, + } + } + pub fn new_from_otp(otp: &[u8]) -> RatchetState { + let mut buffer = ArrayVec::::new(); + buffer.push(1); + buffer.extend(*LABEL_OTP_TO_RATCHET); + buffer.push(0x00); + buffer.extend((1024u16).to_be_bytes()); + + let mut hmac = Hmac::new(); + let mut output = Zeroizing::new([0u8; HASHLEN]); + hmac.hash(otp, &buffer, &mut output); + let rk = Zeroizing::new(output[..RATCHET_SIZE].try_into().unwrap()); + buffer[0] = 2; + hmac.hash(otp, &buffer, &mut output); + let rf = Zeroizing::new(output[..RATCHET_SIZE].try_into().unwrap()); + + Self::new(rk, rf, 1) + } + pub fn is_empty(&self) -> bool { + self.fingerprint.is_none() + } + pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { + self.fingerprint.as_ref().map_or(false, |rf0| secure_eq(rf0, rf)) + } + pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { + self.fingerprint.as_deref() + } +} +impl Default for RatchetState { + fn default() -> Self { + Self::empty() + } +} + +/// A pair of ratchet states. +/// It is expected that an instance of this object will be saved to a storage device per-peer, +/// and be restore-able via the `ApplicationLayer` trait. +/// +/// This corresponds to the possible values of abstract variables `rf` and `rk` found in Section 4.3. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct RatchetStates { + pub state1: RatchetState, + pub state2: Option, +} +impl RatchetStates { + pub fn new(state1: RatchetState, state2: Option) -> Self { + Self { state1, state2 } + } + pub fn new_initial_states() -> Self { + Self { state1: RatchetState::empty(), state2: None } + } + pub fn new_otp_states(otp: &[u8]) -> Self { + Self { + state1: RatchetState::new_from_otp::(otp), + state2: None, + } + } +} +impl Default for RatchetStates { + fn default() -> Self { + Self::new_initial_states() + } +} + +/// A set of references to ratchet states specifying how a remote peer's persistent +/// storage should be updated. +/// +/// There should be only up to two ratchet states saved to storage at a time per peer. +/// Every time a new ratchet state is generated, a previous ratchet state will be deleted. +/// +/// These are sensitive values should they ought to be securely stored. +#[derive(Clone)] +pub struct RatchetUpdate<'a> { + /// The ratchet key and fingerprint to store in the first slot. + pub state1: &'a RatchetState, + /// The ratchet key and fingerprint to store in the second slot. + pub state2: Option<&'a RatchetState>, + /// Whether `state1` is a brand new ratchet state, or if it was previously saved. + pub state1_was_just_added: bool, + /// A previous ratchet key and fingerprint that now must be deleted from storage. + /// This will have been a previously given value of `state1` or `state2`. + pub deleted_state1: Option<&'a RatchetState>, + /// A previous ratchet key and fingerprint that now must be deleted from storage. + /// It is extremely rare that this field is occupied. + pub deleted_state2: Option<&'a RatchetState>, +} +impl<'a> RatchetUpdate<'a> { + pub fn to_states(&self) -> RatchetStates { + RatchetStates::new(self.state1.clone(), self.state2.cloned()) + } + pub fn added_fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { + if self.state1_was_just_added { + self.state1.fingerprint() + } else { + None + } + } + pub fn deleted_fingerprint1(&self) -> Option<&[u8; RATCHET_SIZE]> { + if let Some(rs) = &self.deleted_state1 { + rs.fingerprint() + } else { + None + } + } + pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> { + if let Some(rs) = &self.deleted_state2 { + rs.fingerprint() + } else { + None + } + } } diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000..126b903 --- /dev/null +++ b/src/result.rs @@ -0,0 +1,171 @@ +use std::sync::Arc; + +use crate::application::CryptoLayer; +use crate::zeta::Session; + +/// An error that can occur when attempting to open a session. +/// Depending on the error type trying again may not work. +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum OpenError { + /// An invalid parameter was supplied to the function. + InvalidPublicKey, + + IdentityTooLarge, + + RatchetStorageError, +} + +/// An error that can occur when attempting to send data over a session. +/// Depending on the error type trying again may not work. +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum SendError { + /// An invalid mtu was supplied to the function. The MTU can be no smaller than 128 bytes. + MtuTooSmall, + + /// The session has been marked as expired and refuses to send data. + /// Several components of ZSSP can cause this to occur, but the most likely situation to be seen + /// in practice is where rekeying repeatedly fails due to exceedingly bad network conditions. + /// + /// The user can also explicitly cause this to occur by manually calling `expire` on a session. + /// + /// The associated session will no longer send or receive data and must be immediately dropped. + SessionExpired, + + /// Attempt to send using a session without a shared symmetric key. + /// The caller should wait until the handshake has completed. + SessionNotEstablished, + + /// Data object is too large to send, even with fragmentation. + DataTooLarge, +} + +/// A type of fault occurred because we received a bad packet. +/// +/// An unauthenticated attacker can intentionally trigger any of these, so it is best to +/// treat these as raw user input that needs to be sanitize. +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum FaultType { + /// The received packet was addressed to an unrecognized local session. + UnknownLocalKeyId, + + /// The received packet from the remote peer was not well formed. + InvalidPacket, + + /// Packet failed one or more authentication (MAC) checks. + FailedAuth, + + /// Packet counter was repeated or outside window of allowed counter values. + ExpiredCounter, + + /// Packet contained protocol control parameters that are disallowed at this point in + /// time by ZSSP. + OutOfSequence, +} + +/// An error that occurred during the receipt of a given packet. +#[derive(Debug)] +pub enum ReceiveError { + /// A type of fault that can occur because a remote peer sent us a bad packet. + /// Such packets will be ignored by ZSSP but a user of ZSSP might want to log + /// them for debugging or tracing. + /// + /// Because an unauthenticated remote peer can force these to occur with specific + /// contained information, it is recommended in production to either drop these + /// immediately, or log them safely to a local output stream and then drop them. + ByzantineFault { + /// The type of fault that has occurred. Be cautious if you choose to read this + /// value, as an attacker has control over it. + error: FaultType, + /// Some byzantine faults within ZSSP are naturally occurring, i.e. they can occur + /// between two well behaved and trusted parties executing the protocol. + /// This boolean is false if this is one of these faults. If you go to the file and + /// line number specified by this error you will find a comment describing + /// how and why exactly this fault can occur naturally. + /// + /// Faults that can occur because the underlying communication medium is lossy and + /// sequentially inconsistent (as in UDP) are considered naturally occurring. + /// However ZSSP considers faults that occur because data integrity has not been + /// persevered (i.e. bits have been flipped) to be unnatural. + /// ZSSP also considers collisions of what are supposed to be uniform random + /// numbers to be unnatural. + unnatural: bool, + /// The file of this implementation of ZSSP from which this error was generated. + #[cfg(feature = "debug")] + file: &'static str, + /// The line number of this implementation of ZSSP from which this error was + /// generated. As such this number uniquely identifies each possible fault that + /// can occur during ZSSP. Advanced user can use this information to debug more + /// complicated usages of ZSSP. + #[cfg(feature = "debug")] + line: u32, + }, + + /// Rekeying failed and session secret has reached its hard usage count limit. + /// The associated session will no longer function and has to be dropped. + MaxKeyLifetimeExceeded, + + Rejected, + /// One of the ratchet saving or lookup functions returned an error, so the packet had to be + /// dropped. + RatchetStorageError, + + IoError(std::io::Error), +} + +macro_rules! fault { + ($name:expr, $unnatural:ident) => { + ReceiveError::ByzantineFault { + #[cfg(feature = "debug")] + file: file!(), + #[cfg(feature = "debug")] + line: line!(), + error: $name, + unnatural: $unnatural, + } + }; +} +pub(crate) use fault; + +/// Result generated by the context packet receive function, with possible payloads. +#[derive(Clone)] +pub enum ReceiveOk { + /// Packet superficially appeared valid but is not associated with a session yet. + /// This can occur because the packet was only a fragment of a larger packet, + /// or if it was a control packet that does not go through full Noise authentication. + Unassociated, + /// Packet was authentic and belongs to this specific session. + Session(Arc>, SessionEvent), +} +/// Something that can occur to an associated session when a packet is received successfully, +/// including receiving a payload of decrypted, authenticated data. +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum SessionEvent { + /// The received packet was valid, and it contained the necessary keys to fully establish a new + /// session with Alice, the handshake initiator. + /// + /// If the session Arc returned is dropped, the session with this peer will be immediately + /// terminated. Save the session Arc to some long lived datastructure to keep it alive. + NewSession, + NewDowngradedSession, + /// When Alice calls `Context::open`, a session will be created, but Bob will not yet have + /// received this session. They will have to successfully complete a handshake first. + /// + /// Alice will receive this return value when the received packet confirms both parties + /// have completed the initial handshake and now have a shared session with each other. + /// If according to the upper protocol, Bob is the first party to send data, it is possible for + /// Alice to start receiving data from Bob before this value is returned. + /// + /// This return value can only occur once per session, only for session objects that were + /// created with `Context::open`. + Established, + /// Bob explicitly refused to establish a session with Alice, and sent us an error code. + /// The application should immediately drop this session as Bob will not allow us to connect. + /// + /// This return value cannot occur after a session is fully established. + Rejected, + /// The received packet was valid and a data payload was decoded and authenticated. + Data, + /// The received packet was some authentic protocol control packet. No action needs to be taken. + Control, + DowngradedRatchetKey, +} diff --git a/src/symmetric_state.rs b/src/symmetric_state.rs index 6f6ac6f..c9ea5a8 100644 --- a/src/symmetric_state.rs +++ b/src/symmetric_state.rs @@ -1,120 +1,31 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ -use crate::crypto::aes::AES_256_KEY_SIZE; -use crate::crypto::secret::Secret; -use crate::crypto::sha512::HmacSha512; +use std::marker::PhantomData; -use crate::proto::NOISE_HASHLEN; +use zeroize::Zeroizing; -#[derive(Clone)] -pub(crate) struct SymmetricState { - chaining_key: Secret, - token_counter: u8, +use crate::application::CryptoLayer; +use crate::crypto::*; +use crate::proto::*; + +pub struct SymmetricState { + k: Zeroizing<[u8; AES_256_KEY_SIZE]>, + ck: Zeroizing<[u8; HASHLEN]>, + h: [u8; HASHLEN], + /// If anyone knows a better way to get rid of the "parameter `App` is never used" error please + /// let me know. + _app: PhantomData Crypto::SessionData>, +} +impl Clone for SymmetricState { + fn clone(&self) -> Self { + Self { + k: self.k.clone(), + ck: self.ck.clone(), + h: self.h.clone(), + _app: PhantomData, + } + } } -impl SymmetricState { - pub(crate) fn new(h: [u8; NOISE_HASHLEN]) -> Self { - Self { chaining_key: Secret(h), token_counter: b'P' } - } - /// Corresponds to Noise `MixKey`. - pub(crate) fn mix_key(&mut self, hm: &mut impl HmacSha512, input_key_material: &[u8]) { - let mut next_ck = Secret::new(); - - self.kbkdf(hm, input_key_material, self.label(), 2, next_ck.as_mut(), None, None); - self.token_counter += 1; - - self.chaining_key.overwrite(&next_ck); - // We don't need a key at this step of Noise, so generating that key and calling - // `InitializeKey` would be completely pointless. - } - /// Corresponds to Noise `MixKey` followed by `InitializeKey`. - #[inline(always)] - pub(crate) fn mix_key_initialize_key(&mut self, hm: &mut impl HmacSha512, input_key_material: &[u8]) -> Secret { - let mut next_ck = Secret::new(); - let mut temp_k = [0u8; NOISE_HASHLEN]; - - self.kbkdf(hm, input_key_material, self.label(), 2, next_ck.as_mut(), Some(&mut temp_k), None); - self.token_counter += 1; - - self.chaining_key.overwrite(&next_ck); - Secret::from_bytes_then_delete(&mut temp_k[..AES_256_KEY_SIZE]) - } - /// Corresponds to Noise `MixKeyAndHash`. - pub(crate) fn mix_key_and_hash(&mut self, hm: &mut impl HmacSha512, input_key_material: &[u8]) -> [u8; NOISE_HASHLEN] { - let mut next_ck = Secret::new(); - let mut temp_h = [0u8; NOISE_HASHLEN]; - - self.kbkdf(hm, input_key_material, self.label(), 3, next_ck.as_mut(), Some(&mut temp_h), None); - self.token_counter += 1; - - self.chaining_key.overwrite(&next_ck); - temp_h - } - /// Corresponds to Noise `MixKeyAndHash` followed by `InitializeKey`. - pub(crate) fn mix_key_and_hash_initialize_key( - &mut self, - hm: &mut impl HmacSha512, - input_key_material: &[u8], - ) -> ([u8; NOISE_HASHLEN], Secret) { - let mut next_ck = Secret::new(); - let mut temp_h = [0u8; NOISE_HASHLEN]; - let mut temp_k = [0u8; NOISE_HASHLEN]; - - self.kbkdf( - hm, - input_key_material, - self.label(), - 3, - next_ck.as_mut(), - Some(&mut temp_h), - Some(&mut temp_k), - ); - self.token_counter += 1; - - self.chaining_key.overwrite(&next_ck); - (temp_h, Secret::from_bytes_then_delete(&mut temp_k[..AES_256_KEY_SIZE])) - } - /// Get an additional symmetric key (ASK) that is a collision resistant hash of the transcript, - /// is forward secrect and is cryptographically independent from all other produced keys. - /// Based on Noise's unstable ASK mechanism, using KBKDF instead of HKDF. - /// https://github.com/noiseprotocol/noise_wiki/wiki/Additional-Symmetric-Keys. - #[inline(always)] - pub(crate) fn get_ask2( - &self, - hm: &mut impl HmacSha512, - label: u8, - noise_h: &[u8; NOISE_HASHLEN], - ) -> (Secret, Secret) { - let mut temp_k1 = [0u8; NOISE_HASHLEN]; - let mut temp_k2 = [0u8; NOISE_HASHLEN]; - self.kbkdf(hm, noise_h, [b'A', b'S', b'K', label], 2, &mut temp_k1, Some(&mut temp_k2), None); - ( - Secret::from_bytes_then_delete(&mut temp_k1[..AES_256_KEY_SIZE]), - Secret::from_bytes_then_delete(&mut temp_k2[..AES_256_KEY_SIZE]), - ) - } - /// Corresponds to Noise `Split`. - #[inline(always)] - pub(crate) fn split(self, hm: &mut impl HmacSha512) -> (Secret, Secret) { - let mut temp_k1 = [0u8; NOISE_HASHLEN]; - let mut temp_k2 = [0u8; NOISE_HASHLEN]; - self.kbkdf(hm, &[], self.label(), 2, &mut temp_k1, Some(&mut temp_k2), None); - // Normally KBKDF would not truncate to derive the correct length of AES keys, - // but Noise specifies that the AES keys be truncated from NOISE_HASHLEN to AES_256_KEY_SIZE. - ( - Secret::from_bytes_then_delete(&mut temp_k1[..AES_256_KEY_SIZE]), - Secret::from_bytes_then_delete(&mut temp_k2[..AES_256_KEY_SIZE]), - ) - } - #[inline(always)] - fn label(&self) -> [u8; 4] { - [b'Z', b'S', b'S', self.token_counter] - } +impl SymmetricState { /// HMAC-SHA512 key derivation based on KBKDF Counter Mode: /// https://csrc.nist.gov/publications/detail/sp/800-108/rev-1/final. /// Cryptographically this isn't meaningfully different from @@ -126,37 +37,180 @@ impl SymmetricState { /// * L = `num_outputs*512u16` /// We have intentionally made every input small and fixed size to avoid unnecessary complexity /// and data representation ambiguity. - #[inline(always)] + /// Corresponds to Noise `HKDF`. fn kbkdf( &self, - hm: &mut impl HmacSha512, + hmac: &mut Crypto::Hmac, input_key_material: &[u8], - label: [u8; 4], + label: &[u8; 4], num_outputs: u16, - output1: &mut [u8; NOISE_HASHLEN], - output2: Option<&mut [u8; NOISE_HASHLEN]>, - output3: Option<&mut [u8; NOISE_HASHLEN]>, + output1: &mut [u8; HASHLEN], + output2: Option<&mut [u8; HASHLEN]>, + output3: Option<&mut [u8; HASHLEN]>, ) { - let l = &(num_outputs * 512u16).to_be_bytes(); + const LABEL_START: usize = 1; + const LABEL_END: usize = 5; + const CONTEXT_START: usize = 6; + const LEN_START: usize = 70; + const LEN_END: usize = 72; + let mut buffer = Zeroizing::new([0u8; LEN_END]); + buffer[0] = 1; + buffer[LABEL_START..LABEL_END].copy_from_slice(label); + buffer[LABEL_END] = 0x00; + buffer[CONTEXT_START..LEN_START].copy_from_slice(self.ck.as_ref()); + buffer[LEN_START..LEN_END].copy_from_slice(&(num_outputs * 8 * HASHLEN as u16).to_be_bytes()); + + debug_assert!(num_outputs >= 1); + hmac.hash(input_key_material, buffer.as_ref(), output1); - hm.reset(input_key_material); - hm.update(&[1, label[0], label[1], label[2], label[3], 0x00]); - hm.update(self.chaining_key.as_ref()); - hm.update(l); - hm.finish(output1); if let Some(output2) = output2 { - hm.reset(input_key_material); - hm.update(&[2, label[0], label[1], label[2], label[3], 0x00]); - hm.update(self.chaining_key.as_ref()); - hm.update(l); - hm.finish(output2); + debug_assert!(num_outputs >= 2); + buffer[0] = 2; + hmac.hash(input_key_material, buffer.as_ref(), output2); } + if let Some(output3) = output3 { - hm.reset(input_key_material); - hm.update(&[3, label[0], label[1], label[2], label[3], 0x00]); - hm.update(self.chaining_key.as_ref()); - hm.update(l); - hm.finish(output3); + debug_assert!(num_outputs >= 3); + buffer[0] = 3; + hmac.hash(input_key_material, buffer.as_ref(), output3); } } + + /// Corresponds to Noise `Initialize` on a SymmetricState. + pub fn initialize(h: &[u8; HASHLEN]) -> Self { + Self { + k: Zeroizing::default(), + ck: Zeroizing::new(*h), + h: *h, + _app: PhantomData, + } + } + /// Corresponds to Noise `MixKey`. + pub fn mix_key(&mut self, hmac: &mut Crypto::Hmac, input_key_material: &[u8]) { + let mut next_ck = Zeroizing::new([0u8; HASHLEN]); + let mut temp_k = Zeroizing::new([0u8; HASHLEN]); + + self.kbkdf( + hmac, + input_key_material, + LABEL_KBKDF_CHAIN, + 2, + &mut next_ck, + Some(&mut temp_k), + None, + ); + + *self.ck = *next_ck; + self.k.clone_from_slice(&temp_k[..AES_256_KEY_SIZE]); + } + /// Corresponds to Noise `MixKey`. + pub fn mix_key_no_init(&mut self, hmac: &mut Crypto::Hmac, input_key_material: &[u8]) { + let mut next_ck = Zeroizing::new([0u8; HASHLEN]); + + self.kbkdf(hmac, input_key_material, LABEL_KBKDF_CHAIN, 2, &mut next_ck, None, None); + + *self.ck = *next_ck; + } + /// Corresponds to Noise `MixHash`. + pub fn mix_hash(&mut self, hash: &mut Crypto::Hash, data: &[u8]) { + hash.update(&self.h); + hash.update(data); + hash.finish_and_reset(&mut self.h); + } + /// Corresponds to Noise `MixKeyAndHash`. + pub fn mix_key_and_hash(&mut self, hash: &mut Crypto::Hash, hmac: &mut Crypto::Hmac, input_key_material: &[u8]) { + let mut next_ck = Zeroizing::new([0u8; HASHLEN]); + let mut temp_h = [0u8; HASHLEN]; + let mut temp_k = Zeroizing::new([0u8; HASHLEN]); + + self.kbkdf( + hmac, + input_key_material, + LABEL_KBKDF_CHAIN, + 3, + &mut next_ck, + Some(&mut temp_h), + Some(&mut temp_k), + ); + + *self.ck = *next_ck; + self.mix_hash(hash, &temp_h); + self.k.clone_from_slice(&temp_k[..AES_256_KEY_SIZE]); + } + /// Corresponds to Noise `MixKeyAndHash`. + pub fn mix_key_and_hash_no_init( + &mut self, + hash: &mut Crypto::Hash, + hmac: &mut Crypto::Hmac, + input_key_material: &[u8], + ) { + let mut next_ck = Zeroizing::new([0u8; HASHLEN]); + let mut temp_h = [0u8; HASHLEN]; + + self.kbkdf( + hmac, + input_key_material, + LABEL_KBKDF_CHAIN, + 3, + &mut next_ck, + Some(&mut temp_h), + None, + ); + + *self.ck = *next_ck; + self.mix_hash(hash, &temp_h); + } + /// Corresponds to Noise `EncryptAndHash`. + #[must_use] + pub fn encrypt_and_hash_in_place( + &mut self, + hash: &mut Crypto::Hash, + iv: [u8; AES_GCM_NONCE_SIZE], + data: &mut [u8], + ) -> [u8; AES_GCM_TAG_SIZE] { + let tag = Crypto::Aead::encrypt_in_place(&self.k, &iv, &self.h, data); + hash.update(&self.h); + hash.update(data); + hash.update(&tag); + hash.finish_and_reset(&mut self.h); + tag + } + /// Corresponds to Noise `DecryptAndHash`. + #[must_use] + pub fn decrypt_and_hash_in_place( + &mut self, + hash: &mut Crypto::Hash, + iv: [u8; AES_GCM_NONCE_SIZE], + data: &mut [u8], + tag: [u8; AES_GCM_TAG_SIZE], + ) -> bool { + hash.update(&self.h); + hash.update(data); + hash.update(&tag); + let is_auth = Crypto::Aead::decrypt_in_place(&self.k, &iv, &self.h, data, tag.as_ref().try_into().unwrap()); + hash.finish_and_reset(&mut self.h); + is_auth + } + /// Corresponds to Noise `Split`. + pub fn split(self, hmac: &mut Crypto::Hmac, key1: &mut [u8; HASHLEN], key2: &mut [u8; HASHLEN]) { + self.kbkdf(hmac, &[], LABEL_KBKDF_CHAIN, 2, key1, Some(key2), None); + } + /// Get an additional symmetric key (ASK) that is a collision resistant hash of the transcript, + /// is forward secrect and is cryptographically independent from all other produced keys. + /// Based on Noise's unstable ASK mechanism, using KBKDF instead of HKDF. + /// https://github.com/noiseprotocol/noise_wiki/wiki/Additional-Symmetric-Keys. + pub fn get_ask( + &self, + hmac: &mut Crypto::Hmac, + label: &[u8; 4], + key1: &mut [u8; HASHLEN], + key2: &mut [u8; HASHLEN], + ) { + self.kbkdf(hmac, &self.h, label, 2, key1, Some(key2), None); + } + /// Used for internally debugging a key exchange. + #[allow(unused)] + pub(crate) fn finger(&self) -> (u8, u8, u8) { + (self.k[0], self.ck[0], self.h[0]) + } } diff --git a/src/zeta.rs b/src/zeta.rs new file mode 100644 index 0000000..8af5e23 --- /dev/null +++ b/src/zeta.rs @@ -0,0 +1,1833 @@ +use std::cmp::Reverse; +use std::collections::HashMap; +use std::io::Write; +use std::num::NonZeroU32; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, Weak}; + +use arrayvec::ArrayVec; +use rand_core::RngCore; +use zeroize::Zeroizing; + +use crate::antireplay::Window; +use crate::application::*; +use crate::challenge::{gen_null_response, respond_to_challenge_in_place}; +use crate::crypto::*; +use crate::fragged::Fragged; +use crate::indexed_heap::BinaryHeapIndex; +use crate::proto::*; +use crate::ratchet_state::{RatchetState, RatchetStates}; +use crate::result::{fault, FaultType, OpenError, ReceiveError, SendError}; +use crate::symmetric_state::SymmetricState; +use crate::zssp::{log, ContextInner, SessionQueue}; +#[cfg(feature = "logging")] +use crate::LogEvent::*; + +/// Corresponds to the Zeta State Machine found in Section 4.1. +pub struct Session { + ctx: Weak>, + /// An arbitrary application defined object associated with each session. + pub session_data: Crypto::SessionData, + /// Is true if the local peer acted as Bob, the responder in the initial key exchange. + pub was_bob: bool, + queue_idx: BinaryHeapIndex, + + pub(crate) s_remote: Crypto::PublicKey, + send_counter: AtomicU64, + session_has_expired: AtomicBool, + + pub window: Window, + pub(crate) defrag: [Mutex>; SESSION_MAX_FRAGMENTS_OOO], + + /// `session_queue -> state_machine_lock -> state -> session_map` + state_machine_lock: Mutex<()>, + /// `session_queue -> state_machine_lock -> state -> session_map` + pub(crate) state: RwLock>, + + /// Pre-computed rekeying value. + noise_kk_ss: Zeroizing<[u8; P384_ECDH_SHARED_SECRET_SIZE]>, +} +pub(crate) struct MutableState { + ratchet_state1: RatchetState, + ratchet_state2: Option, + + pub(crate) hk_send: Crypto::PrpEnc, + pub(crate) hk_recv: Crypto::PrpDec, + key_creation_counter: u64, + key_index: bool, + keys: [DuplexKey; 2], + + resend_timer: AtomicI64, + timeout_timer: i64, + pub(crate) beta: ZetaAutomata, +} + +/// Corresponds to State B_2 of the Zeta State Machine found in Section 4.1 - Definition 3. +pub(crate) struct StateB2 { + ratchet_state: RatchetState, + kid_send: NonZeroU32, + pub kid_recv: NonZeroU32, + pub hk_send: Zeroizing<[u8; AES_256_KEY_SIZE]>, + pub hk_recv: Zeroizing<[u8; AES_256_KEY_SIZE]>, + e_secret: Crypto::KeyPair, + noise: SymmetricState, + pub defrag: Mutex>, +} + +pub(crate) struct DuplexKey { + send: Keys, + recv: Keys, + nk: Option, +} + +#[derive(Default)] +pub(crate) struct Keys { + kek: Option>, + kid: Option, +} + +/// Corresponds to State A_1 of the Zeta State Machine found in Section 4.1. +#[derive(Clone)] +pub(crate) struct StateA1 { + noise: SymmetricState, + e_secret: Crypto::KeyPair, + e1_secret: Crypto::Kem, + identity: ArrayVec, + x1: ArrayVec, +} + +pub(crate) struct StateA3 { + identity: ArrayVec, + x3: ArrayVec, +} + +/// Corresponds to the ZKE Automata found in Section 4.1 - Definition 2. +pub(crate) enum ZetaAutomata { + Null, + A1(Box>), + A3(Box), + S1, + S2, + R1 { + noise: SymmetricState, + e_secret: Crypto::KeyPair, + k1: ArrayVec, + }, + R2 { + k2: ArrayVec, + }, +} + +impl Default for DuplexKey { + fn default() -> Self { + Self { send: Default::default(), recv: Default::default(), nk: None } + } +} +impl DuplexKey { + fn replace_nk(&mut self, nk_send: &[u8; HASHLEN], nk_recv: &[u8; HASHLEN]) { + let nk_send = (&nk_send[..AES_256_KEY_SIZE]).try_into().unwrap(); + let nk_recv = (&nk_recv[..AES_256_KEY_SIZE]).try_into().unwrap(); + self.nk = Some(Crypto::AeadPool::new(nk_send, nk_recv)) + } +} +impl Keys { + fn replace_kek(&mut self, kek: &[u8; HASHLEN]) { + // We want to give rust the best chance of implementing this in a way that does + // not leak the key on the stack. + let old_kek = self.kek.get_or_insert(Zeroizing::new([0u8; AES_256_KEY_SIZE])); + old_kek.copy_from_slice(&kek[..AES_256_KEY_SIZE]); + } +} + +impl MutableState { + fn key_ref(&self, is_next: bool) -> &DuplexKey { + &self.keys[(self.key_index ^ is_next) as usize] + } + fn key_mut(&mut self, is_next: bool) -> &mut DuplexKey { + &mut self.keys[(self.key_index ^ is_next) as usize] + } +} + +impl SymmetricState { + #[must_use] + fn write_e_no_init( + &mut self, + hash: &mut Crypto::Hash, + hmac: &mut Crypto::Hmac, + rng: &Mutex, + packet: &mut ArrayVec, + ) -> Crypto::KeyPair { + let e_secret = Crypto::KeyPair::generate(rng.lock().unwrap().deref_mut()); + let pub_key = e_secret.public_key_bytes(); + packet.extend(pub_key); + self.mix_hash(hash, &pub_key); + self.mix_key_no_init(hmac, &pub_key); + e_secret + } + #[must_use] + fn read_e_no_init( + &mut self, + hash: &mut Crypto::Hash, + hmac: &mut Crypto::Hmac, + i: &mut usize, + packet: &[u8], + ) -> Option { + let j = *i + P384_PUBLIC_KEY_SIZE; + let pub_key = &packet[*i..j]; + self.mix_hash(hash, pub_key); + self.mix_key_no_init(hmac, pub_key); + *i = j; + Crypto::PublicKey::from_bytes((pub_key).try_into().unwrap()) + } + #[must_use] + fn mix_dh(&mut self, hmac: &mut Crypto::Hmac, secret: &Crypto::KeyPair, remote: &Crypto::PublicKey) -> Option<()> { + let mut ecdh_secret = Zeroizing::new([0u8; P384_ECDH_SHARED_SECRET_SIZE]); + if secret.agree(&remote, &mut ecdh_secret) { + self.mix_key(hmac, ecdh_secret.as_ref()); + Some(()) + } else { + None + } + } + #[must_use] + fn mix_dh_no_init( + &mut self, + hmac: &mut Crypto::Hmac, + secret: &Crypto::KeyPair, + remote: &Crypto::PublicKey, + ) -> Option<()> { + let mut ecdh_secret = Zeroizing::new([0u8; P384_ECDH_SHARED_SECRET_SIZE]); + if secret.agree(&remote, &mut ecdh_secret) { + self.mix_key_no_init(hmac, ecdh_secret.as_ref()); + Some(()) + } else { + None + } + } +} + +/// Create a 96-bit AES-GCM nonce. +/// +/// The primary information that we want to be contained here is the counter and the +/// packet type. The former makes this unique and the latter's inclusion authenticates +/// it as effectively AAD. Other elements of the header are either not authenticated, +/// like fragmentation info, or their authentication is implied via key exchange like +/// the key id. +/// +/// Corresponds to Figure 10 found in Section 4.3. +pub(crate) fn to_nonce(packet_type: u8, counter: u64) -> [u8; AES_GCM_NONCE_SIZE] { + let mut ret = [0u8; AES_GCM_NONCE_SIZE]; + ret[3] = packet_type; + // Noise requires a big endian counter at the end of the Nonce + ret[4..].copy_from_slice(&counter.to_be_bytes()); + ret +} +/// Corresponds to Figure 10 and Figure 14 found in Section 4.3. +pub(crate) fn from_nonce(n: &[u8]) -> (u8, u64) { + assert!(n.len() >= PACKET_NONCE_SIZE); + let c_start = n.len() - 8; + (n[c_start - 1], u64::from_be_bytes(n[c_start..].try_into().unwrap())) +} +pub(crate) fn set_header(packet: &mut [u8], kid_send: u32, nonce: &[u8; AES_GCM_NONCE_SIZE]) { + packet[..KID_SIZE].copy_from_slice(&kid_send.to_ne_bytes()); + packet[PACKET_NONCE_START..HEADER_SIZE].copy_from_slice(&nonce[NONCE_SIZE_DIFF..]); +} +fn create_ratchet_state( + hmac: &mut Crypto::Hmac, + noise: &SymmetricState, + pre_chain_len: u64, +) -> RatchetState { + let mut rk = Zeroizing::new([0u8; HASHLEN]); + let mut rf = Zeroizing::new([0u8; HASHLEN]); + noise.get_ask(hmac, LABEL_RATCHET_STATE, &mut rk, &mut rf); + RatchetState::new( + Zeroizing::new(rk[..RATCHET_SIZE].try_into().unwrap()), + Zeroizing::new(rf[..RATCHET_SIZE].try_into().unwrap()), + pre_chain_len + 1, + ) +} +fn get_counter(session: &Session, state: &MutableState) -> Option<(u64, bool)> { + if session.session_has_expired.load(Ordering::Relaxed) { + None + } else { + let c = session.send_counter.fetch_add(1, Ordering::Relaxed); + if c > THREAD_SAFE_COUNTER_HARD_EXPIRE { + session.session_has_expired.store(true, Ordering::SeqCst); + } + if c > state.key_creation_counter + EXPIRE_AFTER_USES { + session.session_has_expired.store(true, Ordering::SeqCst); + return None; + } + let rekey_at = state.key_creation_counter + Crypto::SETTINGS.rekey_after_key_uses; + Some((c, c > rekey_at)) + } +} + +/// Generate a random local key id that is currently unused. +fn gen_kid(session_map: &HashMap, rng: &mut impl RngCore) -> NonZeroU32 { + loop { + if let Some(kid) = NonZeroU32::new(rng.next_u32()) { + if !session_map.contains_key(&kid) { + return kid; + } + } + } +} +fn remap( + ctx: &Arc>, + session: &Arc>, + state: &MutableState, +) -> NonZeroU32 { + let mut session_map = ctx.session_map.write().unwrap(); + let weak = if let Some(Some(weak)) = state.key_ref(true).recv.kid.as_ref().map(|kid| session_map.remove(kid)) { + weak + } else { + Arc::downgrade(&session) + }; + let new_kid_recv = gen_kid(session_map.deref(), ctx.rng.lock().unwrap().deref_mut()); + session_map.insert(new_kid_recv, weak); + new_kid_recv +} + +fn create_a1_state( + hash: &mut Crypto::Hash, + hmac: &mut Crypto::Hmac, + rng: &Mutex, + s_remote: &Crypto::PublicKey, + kid_recv: NonZeroU32, + ratchet_state1: &RatchetState, + ratchet_state2: Option<&RatchetState>, + identity: &[u8], +) -> Option>> { + // <- s + // ... + // -> e, es, e1 + let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); + let mut x1 = ArrayVec::::new(); + x1.extend([0u8; HEADER_SIZE]); + // Noise process prologue. + let kid = kid_recv.get().to_ne_bytes(); + x1.extend(kid); + noise.mix_hash(hash, &kid); + noise.mix_hash(hash, &s_remote.to_bytes()); + // Process message pattern 1 e token. + let e_secret = noise.write_e_no_init(hash, hmac, rng, &mut x1); + // Process message pattern 1 es token. + noise.mix_dh(hmac, &e_secret, s_remote)?; + // Process message pattern 1 e1 token. + let i = x1.len(); + let (e1_secret, e1_public) = Crypto::Kem::generate(rng.lock().unwrap().deref_mut()); + x1.extend(e1_public); + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 0), &mut x1[i..]); + x1.extend(tag); + // Process message pattern 1 payload. + let i = x1.len(); + if let Some(rf) = ratchet_state1.fingerprint() { + x1.try_extend_from_slice(rf).unwrap(); + } + if let Some(Some(rf)) = ratchet_state2.map(|rs| rs.fingerprint()) { + x1.try_extend_from_slice(rf).unwrap(); + } + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), &mut x1[i..]); + x1.extend(tag); + + let c = u64::from_be_bytes(x1[x1.len() - 8..].try_into().unwrap()); + + // Process challenge + x1.extend(gen_null_response(rng.lock().unwrap().deref_mut())); + + set_header(&mut x1, 0, &to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, c)); + + let identity = identity.try_into().unwrap(); + Some(Box::new(StateA1 { noise, e_secret, e1_secret, identity, x1 })) +} +/// Corresponds to Transition Algorithm 1 found in Section 4.3. +pub(crate) fn trans_to_a1>( + mut app: App, + ctx: &Arc>, + s_remote: Crypto::PublicKey, + session_data: Crypto::SessionData, + identity: &[u8], + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Result>, OpenError> { + let RatchetStates { state1, state2 } = app + .restore_by_identity(&s_remote, &session_data) + .map_err(|_| OpenError::RatchetStorageError)? + .unwrap_or_default(); + + let mut session_queue = ctx.session_queue.lock().unwrap(); + let mut session_map = ctx.session_map.write().unwrap(); + let kid_recv = gen_kid(session_map.deref(), ctx.rng.lock().unwrap().deref_mut()); + + let hash = &mut Crypto::Hash::new(); + let hmac = &mut Crypto::Hmac::new(); + let a1 = create_a1_state( + hash, + hmac, + &ctx.rng, + &s_remote, + kid_recv, + &state1, + state2.as_ref(), + identity, + ) + .ok_or(OpenError::InvalidPublicKey)?; + + let mut noise_kk_ss = Zeroizing::new([0u8; P384_ECDH_SHARED_SECRET_SIZE]); + if !ctx.s_secret.agree(&s_remote, &mut noise_kk_ss) { + return Err(OpenError::InvalidPublicKey); + } + + let mut hk_recv = Zeroizing::new([0u8; HASHLEN]); + let mut hk_send = Zeroizing::new([0u8; HASHLEN]); + a1.noise.get_ask(hmac, LABEL_HEADER_KEY, &mut hk_recv, &mut hk_send); + + let mut x1 = a1.x1.clone(); + + let current_time = app.time(); + let queue_idx = session_queue.reserve_index(); + let resend_timer = current_time + Crypto::SETTINGS.resend_time as i64; + let session = Arc::new(Session { + ctx: Arc::downgrade(ctx), + session_data, + was_bob: false, + queue_idx, + s_remote, + send_counter: AtomicU64::new(0), + session_has_expired: AtomicBool::new(false), + window: Window::new(), + state_machine_lock: Mutex::new(()), + state: RwLock::new(MutableState { + ratchet_state1: state1.clone(), + ratchet_state2: state2.clone(), + hk_send: Crypto::PrpEnc::new((&hk_send[..AES_256_KEY_SIZE]).try_into().unwrap()), + hk_recv: Crypto::PrpDec::new((&hk_recv[..AES_256_KEY_SIZE]).try_into().unwrap()), + key_creation_counter: 0, + key_index: true, + keys: [DuplexKey::default(), DuplexKey::default()], + resend_timer: AtomicI64::new(resend_timer), + timeout_timer: current_time + Crypto::SETTINGS.initial_offer_timeout as i64, + beta: ZetaAutomata::A1(a1), + }), + noise_kk_ss: noise_kk_ss.clone(), + defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), + }); + { + let mut state = session.state.write().unwrap(); + state.key_mut(true).recv.kid = Some(kid_recv); + } + + session_map.insert(kid_recv, Arc::downgrade(&session)); + session_queue.push_reserved(queue_idx, Arc::downgrade(&session), Reverse(resend_timer)); + + send(&mut x1, None); + + Ok(session) +} +/// Corresponds to Algorithm 13 found in Section 5. +pub(crate) fn respond_to_challenge( + ctx: &Arc>, + session: &Session, + challenge: &[u8; CHALLENGE_SIZE], +) { + let mut state = session.state.write().unwrap(); + if let ZetaAutomata::A1(a1) = &mut state.beta { + let response_start = a1.x1.len() - CHALLENGE_SIZE; + let mut rng = ctx.rng.lock().unwrap(); + let response = (&mut a1.x1[response_start..]).try_into().unwrap(); + respond_to_challenge_in_place(rng.deref_mut(), &mut Crypto::Hash::new(), challenge, response); + } +} +/// Corresponds to Transition Algorithm 2 found in Section 4.3. +pub(crate) fn received_x1_trans>( + app: &mut App, + ctx: &ContextInner, + hash: &mut Crypto::Hash, + n: &[u8; AES_GCM_NONCE_SIZE], + x1: &mut [u8], + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Result<(), ReceiveError> { + use FaultType::*; + // <- s + // ... + // -> e, es, e1 + // <- e, ee, ekem1, psk + if !(HANDSHAKE_HELLO_MIN_SIZE..=HANDSHAKE_HELLO_MAX_SIZE).contains(&x1.len()) { + return Err(fault!(InvalidPacket, true)); + } + + if &n[AES_GCM_NONCE_SIZE - 8..] != &x1[x1.len() - 8..] { + return Err(fault!(FailedAuth, true)); + } + let hmac = &mut Crypto::Hmac::new(); + let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); + let mut i = 0; + // Noise process prologue. + let j = i + KID_SIZE; + noise.mix_hash(hash, &x1[i..j]); + let kid_send = + NonZeroU32::new(u32::from_ne_bytes(x1[i..j].try_into().unwrap())).ok_or(fault!(InvalidPacket, true))?; + noise.mix_hash(hash, &ctx.s_secret.public_key_bytes()); + i = j; + // Process message pattern 1 e token. + let e_remote = noise + .read_e_no_init(hash, hmac, &mut i, &x1) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 1 es token. + noise + .mix_dh(hmac, &ctx.s_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 1 e1 token. + let j = i + KYBER_PUBLIC_KEY_SIZE; + let k = j + AES_GCM_TAG_SIZE; + let tag = x1[j..k].try_into().unwrap(); + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 0), &mut x1[i..j], tag) { + return Err(fault!(FailedAuth, true)); + } + let e1_start = i; + let e1_end = j; + i = k; + // Process message pattern 1 payload. + let k = x1.len(); + let j = k - AES_GCM_TAG_SIZE; + let tag = x1[j..k].try_into().unwrap(); + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), &mut x1[i..j], tag) { + return Err(fault!(FailedAuth, true)); + } + + let mut ratchet_state = None; + while i + RATCHET_SIZE <= j { + match app.restore_by_fingerprint((&x1[i..i + RATCHET_SIZE]).try_into().unwrap()) { + Ok(None) => {} + Ok(Some(rs)) => { + ratchet_state = Some(rs); + break; + } + Err(_) => return Err(ReceiveError::RatchetStorageError), + } + i += RATCHET_SIZE; + } + let ratchet_state = if let Some(rs) = ratchet_state { + rs + } else { + if app.hello_requires_recognized_ratchet() { + return Err(fault!(FailedAuth, true)); + } + RatchetState::empty() + }; + + let mut hk_recv = Zeroizing::new([0u8; HASHLEN]); + let mut hk_send = Zeroizing::new([0u8; HASHLEN]); + noise.get_ask(hmac, LABEL_HEADER_KEY, &mut hk_send, &mut hk_recv); + + let mut x2 = ArrayVec::::new(); + x2.extend([0u8; HEADER_SIZE]); + // Process message pattern 2 e token. + let e_secret = noise.write_e_no_init(hash, hmac, &ctx.rng, &mut x2); + // Process message pattern 2 ee token. + noise + .mix_dh(hmac, &e_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 ekem1 token. + { + let i = x2.len(); + let mut ekem1_secret = Zeroizing::new([0u8; KYBER_PLAINTEXT_SIZE]); + let ekem1 = Crypto::Kem::encapsulate( + ctx.rng.lock().unwrap().deref_mut(), + (&x1[e1_start..e1_end]).try_into().unwrap(), + &mut ekem1_secret, + ) + .ok_or(fault!(FailedAuth, true))?; + x2.extend(ekem1); + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), &mut x2[i..]); + x2.extend(tag); + noise.mix_key_no_init(hmac, ekem1_secret.as_ref()); + } + // Process message pattern 2 psk2 token. + noise.mix_key_and_hash(hash, hmac, ratchet_state.key.as_ref()); + // Process message pattern 2 payload. + let kid_recv = gen_kid( + ctx.session_map.read().unwrap().deref(), + ctx.rng.lock().unwrap().deref_mut(), + ); + + let i = x2.len(); + x2.extend(kid_recv.get().to_ne_bytes()); + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), &mut x2[i..]); + x2.extend(tag); + + let i = x2.len(); + let mut c = [0u8; 8]; + c[5] = x2[i - 3]; + c[6] = x2[i - 2]; + c[7] = x2[i - 1]; + let c = u64::from_be_bytes(c); + + set_header(&mut x2, kid_send.get(), &to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, c)); + + ctx.unassociated_handshake_states.insert( + kid_recv, + Arc::new(StateB2 { + ratchet_state, + kid_send, + kid_recv, + hk_send: Zeroizing::new(hk_send[..AES_256_KEY_SIZE].try_into().unwrap()), + hk_recv: Zeroizing::new(hk_recv[..AES_256_KEY_SIZE].try_into().unwrap()), + e_secret, + noise, + defrag: Mutex::new(Fragged::new()), + }), + app.time(), + ); + + send( + &mut x2, + Some(&Crypto::PrpEnc::new(&hk_send[..AES_256_KEY_SIZE].try_into().unwrap())), + ); + Ok(()) +} +/// Corresponds to Transition Algorithm 3 found in Section 4.3. +pub(crate) fn received_x2_trans>( + app: &mut App, + ctx: &Arc>, + session: &Arc>, + kid: NonZeroU32, + n: &[u8; AES_GCM_NONCE_SIZE], + x2: &mut [u8], + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Result { + use FaultType::*; + // <- e, ee, ekem1, psk + // -> s, se + if HANDSHAKE_RESPONSE_SIZE != x2.len() { + return Err(fault!(InvalidPacket, true)); + } + + let kex_lock = session.state_machine_lock.lock().unwrap(); + let state = session.state.read().unwrap(); + let hash = &mut Crypto::Hash::new(); + let hmac = &mut Crypto::Hmac::new(); + + if Some(kid) != state.key_ref(true).recv.kid { + return Err(fault!(UnknownLocalKeyId, true)); + } + let (_, c) = from_nonce(n); + if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || &n[AES_GCM_NONCE_SIZE - 3..] != &x2[x2.len() - 3..] { + return Err(fault!(FailedAuth, true)); + } + let mut should_warn_missing_ratchet = false; + let mut result = (|| { + let a1 = if let ZetaAutomata::A1(a1) = &state.beta { + a1 + } else { + return Err(fault!(FailedAuth, true)); + }; + let mut noise = a1.noise.clone(); + let mut i = 0; + // Process message pattern 2 e token. + let e_remote = noise + .read_e_no_init(hash, hmac, &mut i, &x2) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 ee token. + noise + .mix_dh(hmac, &a1.e_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 ekem1 token. + let j = i + KYBER_CIPHERTEXT_SIZE; + let k = j + AES_GCM_TAG_SIZE; + let tag = x2[j..k].try_into().unwrap(); + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), &mut x2[i..j], tag) { + return Err(fault!(FailedAuth, true)); + } + let mut ekem1_secret = Zeroizing::new([0u8; KYBER_PLAINTEXT_SIZE]); + if !a1 + .e1_secret + .decapsulate((&x2[i..j]).try_into().unwrap(), &mut ekem1_secret) + { + return Err(fault!(FailedAuth, true)); + } + noise.mix_key_no_init(hmac, ekem1_secret.as_ref()); + drop(ekem1_secret); + i = k; + // We attempt to decrypt the payload at most three times. First two times with + // the ratchet key Alice remembers, and final time with a ratchet + // key of zero if Alice allows ratchet downgrades. + // The following code is not constant time, meaning we leak to an + // attacker whether or not we downgraded. + // We don't currently consider this sensitive enough information to hide. + let j = i + KID_SIZE; + let k = j + AES_GCM_TAG_SIZE; + let payload: [u8; KID_SIZE] = x2[i..j].try_into().unwrap(); + let tag = x2[j..k].try_into().unwrap(); + // Check for which ratchet key Bob wants to use. + let mut test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState)> { + let mut noise = noise.clone(); + let mut payload = payload.clone(); + // Process message pattern 2 psk token. + noise.mix_key_and_hash(hash, hmac, ratchet_key); + // Process message pattern 2 payload. + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), &mut payload, tag) { + return None; + } + NonZeroU32::new(u32::from_ne_bytes(payload)).map(|kid2| (kid2, noise)) + }; + // Check first key. + let mut ratchet_i = 1; + let mut chain_len = state.ratchet_state1.chain_len; + let mut result = test_ratchet_key(state.ratchet_state1.key.as_ref()); + // Check second key. + if result.is_none() { + ratchet_i = 2; + if let Some(rs) = state.ratchet_state2.as_ref() { + chain_len = rs.chain_len; + result = test_ratchet_key(rs.key.as_ref()); + } + } + // Check zero key. + if result.is_none() && !app.initiator_disallows_downgrade(session) { + chain_len = 0; + result = test_ratchet_key(&[0u8; RATCHET_SIZE]); + if result.is_some() { + should_warn_missing_ratchet = true; + } + } + + let (kid_send, mut noise) = result.ok_or(fault!(FailedAuth, true))?; + + let mut x3 = ArrayVec::::new(); + x3.extend([0u8; HEADER_SIZE]); + // Process message pattern 3 s token. + let i = x3.len(); + x3.extend(ctx.s_secret.public_key_bytes()); + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 1), &mut x3[i..]); + x3.extend(tag); + // Process message pattern 3 se token. + noise + .mix_dh(hmac, &ctx.s_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 3 payload. + let i = x3.len(); + x3.try_extend_from_slice(&a1.identity).unwrap(); + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 0), &mut x3[i..]); + x3.extend(tag); + + let new_ratchet_state = create_ratchet_state(hmac, &mut noise, chain_len); + + let (ratchet_to_preserve, ratchet_to_delete) = if ratchet_i == 1 { + (Some(&state.ratchet_state1), state.ratchet_state2.as_ref()) + } else { + (state.ratchet_state2.as_ref(), Some(&state.ratchet_state1)) + }; + app.save_ratchet_state( + &session.s_remote, + &session.session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: ratchet_to_preserve, + state1_was_just_added: true, + deleted_state1: ratchet_to_delete, + deleted_state2: None, + }, + ) + .map_err(|_| ReceiveError::RatchetStorageError)?; + + let mut kek_recv = Zeroizing::new([0u8; HASHLEN]); + let mut kek_send = Zeroizing::new([0u8; HASHLEN]); + let mut nk_recv = Zeroizing::new([0u8; HASHLEN]); + let mut nk_send = Zeroizing::new([0u8; HASHLEN]); + noise.get_ask(hmac, LABEL_KEX_KEY, &mut kek_recv, &mut kek_send); + noise.split(hmac, &mut nk_recv, &mut nk_send); + + let nonce = to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 0); + set_header(&mut x3, kid_send.get(), &nonce); + + drop(state); + let resend_timer = { + let mut state = session.state.write().unwrap(); + state.key_mut(true).send.kid = Some(kid_send); + state.key_mut(true).send.replace_kek(&kek_send); + state.key_mut(true).recv.replace_kek(&kek_recv); + state.key_mut(true).replace_nk(&nk_send, &nk_recv); + state.ratchet_state2 = Some(state.ratchet_state1.clone()); + state.ratchet_state1 = new_ratchet_state.clone(); + let current_time = app.time(); + state.key_creation_counter = session.send_counter.load(Ordering::Relaxed); + let resend_timer = current_time + Crypto::SETTINGS.resend_time as i64; + state.resend_timer = AtomicI64::new(resend_timer); + state.timeout_timer = current_time + Crypto::SETTINGS.initial_offer_timeout as i64; + let a1 = if let ZetaAutomata::A1(a1) = &state.beta { + a1 + } else { + // This return is unreachable. + return Err(fault!(FailedAuth, true)); + }; + state.beta = ZetaAutomata::A3(Box::new(StateA3 { identity: a1.identity.clone(), x3: x3.clone() })); + resend_timer + }; + drop(kex_lock); + ctx.session_queue + .lock() + .unwrap() + .change_priority(session.queue_idx, Reverse(resend_timer)); + + Ok(x3) + })(); + + match result { + Err(ReceiveError::ByzantineFault { .. }) => { + let kex_lock = session.state_machine_lock.lock().unwrap(); + let state = session.state.read().unwrap(); + let current_time = app.time(); + timeout_trans(app, ctx, session, kex_lock, state, current_time, send); + } + Ok(ref mut packet) => send(packet, Some(&session.state.read().unwrap().hk_send)), + _ => {} + } + result.map(|_| should_warn_missing_ratchet) +} +fn send_control( + session: &Arc>, + state: &MutableState, + packet_type: u8, + mut payload: ArrayVec, + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> bool { + if let Some((c, _)) = get_counter(session, &state) { + if let (Some(kek), Some(kid)) = (state.key_ref(false).send.kek.as_ref(), state.key_ref(false).send.kid) { + let nonce = to_nonce(packet_type, c); + let tag = Crypto::Aead::encrypt_in_place(kek, &nonce, &[], &mut payload[HEADER_SIZE..]); + payload.extend(tag); + set_header(&mut payload, kid.get(), &nonce); + send(&mut payload, Some(&state.hk_send)); + true + } else { + false + } + } else { + false + } +} +/// Corresponds to Transition Algorithm 4 found in Section 4.3. +pub(crate) fn received_x3_trans>( + app: &mut App, + ctx: &Arc>, + zeta: Arc>, + kid: NonZeroU32, + x3: &mut [u8], + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Result<(Arc>, bool), ReceiveError> { + use FaultType::*; + // -> s, se + if x3.len() < HANDSHAKE_COMPLETION_MIN_SIZE { + return Err(fault!(InvalidPacket, true)); + } + if kid != zeta.kid_recv { + return Err(fault!(UnknownLocalKeyId, true)); + } + let hash = &mut Crypto::Hash::new(); + let hmac = &mut Crypto::Hmac::new(); + + let mut noise = zeta.noise.clone(); + let mut i = 0; + // Process message pattern 3 s token. + let j = i + P384_PUBLIC_KEY_SIZE; + let k = j + AES_GCM_TAG_SIZE; + let tag = x3[j..k].try_into().unwrap(); + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 1), &mut x3[i..j], tag) { + return Err(fault!(FailedAuth, true)); + } + let s_remote = Crypto::PublicKey::from_bytes((&x3[i..j]).try_into().unwrap()).ok_or(fault!(FailedAuth, true))?; + i = k; + // Process message pattern 3 se token. + noise + .mix_dh(hmac, &zeta.e_secret, &s_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 3 payload. + let k = x3.len(); + let j = k - AES_GCM_TAG_SIZE; + let tag = x3[j..k].try_into().unwrap(); + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 0), &mut x3[i..j], tag) { + return Err(fault!(FailedAuth, true)); + } + let identity_start = i; + let identity_end = j; + + let mut kek_recv = Zeroizing::new([0u8; HASHLEN]); + let mut kek_send = Zeroizing::new([0u8; HASHLEN]); + noise.get_ask(hmac, LABEL_KEX_KEY, &mut kek_send, &mut kek_recv); + let c = 0; + + let action = app.check_accept_session(&s_remote, &x3[identity_start..identity_end]); + let responder_disallows_downgrade = action.responder_disallows_downgrade; + let responder_silently_rejects = action.responder_silently_rejects; + let session_data = action.session_data; + let create_reject = || { + // We just used a counter with this key, but we are not storing + // the fact we used it in memory. This is currently ok because the + // handshake is being dropped, so nonce reuse can't happen. + let mut d = ArrayVec::::new(); + d.extend([0u8; HEADER_SIZE]); + let nonce = to_nonce(PACKET_TYPE_SESSION_REJECTED, c); + let kek_send = (&kek_send[..AES_256_KEY_SIZE]).try_into().unwrap(); + d.extend(Crypto::Aead::encrypt_in_place(kek_send, &nonce, &[], &mut [])); + set_header(&mut d, zeta.kid_send.get(), &nonce); + d + }; + if let Some(session_data) = session_data { + let result = app.restore_by_identity(&s_remote, &session_data); + match result { + Ok(rss) => { + let RatchetStates { state1, state2 } = rss.unwrap_or_default(); + let mut should_warn_missing_ratchet = false; + + if (&zeta.ratchet_state != &state1) & (Some(&zeta.ratchet_state) != state2.as_ref()) { + if !responder_disallows_downgrade && zeta.ratchet_state.fingerprint().is_none() { + should_warn_missing_ratchet = true; + } else { + if !responder_silently_rejects { + send(&mut create_reject(), Some(&Crypto::PrpEnc::new(&zeta.hk_send))) + } + return Err(fault!(FailedAuth, true)); + } + } + + let mut noise_kk_ss = Zeroizing::new([0u8; P384_ECDH_SHARED_SECRET_SIZE]); + if !ctx.s_secret.agree(&s_remote, &mut noise_kk_ss) { + return Err(fault!(FailedAuth, true)); + } + + let new_ratchet_state = create_ratchet_state(hmac, &mut noise, zeta.ratchet_state.chain_len); + let mut nk_recv = Zeroizing::new([0u8; HASHLEN]); + let mut nk_send = Zeroizing::new([0u8; HASHLEN]); + noise.split(hmac, &mut nk_send, &mut nk_recv); + + // We must make sure the ratchet key is saved before we transition. + app.save_ratchet_state( + &s_remote, + &session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: None, + state1_was_just_added: true, + deleted_state1: Some(&state1), + deleted_state2: state2.as_ref(), + }, + ) + .map_err(|_| ReceiveError::RatchetStorageError)?; + + let session = { + let mut session_map = ctx.session_map.write().unwrap(); + use std::collections::hash_map::Entry::*; + let entry = match session_map.entry(zeta.kid_recv) { + // We could have issued the kid that we initially offered Alice to someone else + // before Alice was able to respond. It is unlikely but possible. + Occupied(_) => return Err(fault!(OutOfSequence, false)), + Vacant(entry) => entry, + }; + let mut session_queue = ctx.session_queue.lock().unwrap(); + let queue_idx = session_queue.reserve_index(); + let current_time = app.time(); + let resend_timer = current_time + Crypto::SETTINGS.resend_time as i64; + let session = Arc::new(Session { + ctx: Arc::downgrade(ctx), + session_data, + was_bob: true, + s_remote, + send_counter: AtomicU64::new(c + 1), + session_has_expired: AtomicBool::new(false), + state_machine_lock: Mutex::new(()), + state: RwLock::new(MutableState { + ratchet_state1: new_ratchet_state.clone(), + ratchet_state2: None, + hk_send: Crypto::PrpEnc::new(&zeta.hk_send), + hk_recv: Crypto::PrpDec::new(&zeta.hk_recv), + key_creation_counter: c + 1, + key_index: false, + keys: [DuplexKey::default(), DuplexKey::default()], + resend_timer: AtomicI64::new(resend_timer), + timeout_timer: current_time + Crypto::SETTINGS.rekey_timeout as i64, + beta: ZetaAutomata::S1, + }), + window: Window::new(), + queue_idx, + noise_kk_ss: noise_kk_ss.clone(), + defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), + }); + { + let mut state = session.state.write().unwrap(); + state.key_mut(false).replace_nk(&nk_send, &nk_recv); + state.key_mut(false).recv.kid = Some(zeta.kid_recv); + state.key_mut(false).recv.replace_kek(&kek_recv); + state.key_mut(false).send.kid = Some(zeta.kid_send); + state.key_mut(false).send.replace_kek(&kek_send); + } + + session_queue.push_reserved(queue_idx, Arc::downgrade(&session), Reverse(resend_timer)); + entry.insert(Arc::downgrade(&session)); + + session + }; + let state = session.state.read().unwrap(); + let mut c1 = ArrayVec::::new(); + c1.extend([0u8; HEADER_SIZE]); + send_control(&session, &state, PACKET_TYPE_KEY_CONFIRM, c1, send); + drop(state); + + Ok((session, should_warn_missing_ratchet)) + } + Err(()) => Err(ReceiveError::RatchetStorageError), + } + } else { + if !responder_silently_rejects { + send(&mut create_reject(), Some(&Crypto::PrpEnc::new(&zeta.hk_send))) + } + Err(ReceiveError::Rejected) + } +} +/// Corresponds to Transition Algorithm 5 found in Section 4.3. +pub(crate) fn received_c1_trans>( + app: &mut App, + ctx: &Arc>, + session: &Arc>, + kid: NonZeroU32, + n: &[u8; AES_GCM_NONCE_SIZE], + c1: &[u8], + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Result { + use FaultType::*; + + if c1.len() != KEY_CONFIRMATION_SIZE { + return Err(fault!(InvalidPacket, true)); + } + + let kex_lock = session.state_machine_lock.lock().unwrap(); + let mut state = session.state.read().unwrap(); + + let is_other = if Some(kid) == state.key_ref(true).recv.kid { + true + } else if Some(kid) == state.key_ref(false).recv.kid { + false + } else { + // Some key confirmation may have arrived extremely delayed. + // It is unlikely but possible. + return Err(fault!(OutOfSequence, false)); + }; + + let specified_key = state.key_ref(is_other).recv.kek.as_ref(); + let specified_key = specified_key.ok_or(fault!(OutOfSequence, true))?; + let tag = c1[..].try_into().unwrap(); + if !Crypto::Aead::decrypt_in_place(specified_key, n, &[], &mut [], tag) { + return Err(fault!(FailedAuth, true)); + } + let (_, c) = from_nonce(n); + if !session.window.update(c) { + return Err(fault!(ExpiredCounter, true)); + } + + let just_establised = is_other && matches!(&state.beta, ZetaAutomata::A3 { .. }); + if is_other { + if let ZetaAutomata::A3 { .. } | ZetaAutomata::R2 { .. } = &state.beta { + if state.ratchet_state2.is_some() { + app.save_ratchet_state( + &session.s_remote, + &session.session_data, + RatchetUpdate { + state1: &state.ratchet_state1, + state2: None, + state1_was_just_added: false, + deleted_state1: state.ratchet_state2.as_ref(), + deleted_state2: None, + }, + ) + .map_err(|_| ReceiveError::RatchetStorageError)?; + } + drop(state); + let timeout_timer = { + let mut state = session.state.write().unwrap(); + state.ratchet_state2 = None; + state.key_index ^= true; + let jitter = ctx.rng.lock().unwrap().next_u64() % Crypto::SETTINGS.rekey_time_max_jitter; + state.timeout_timer = app.time() + Crypto::SETTINGS.rekey_after_time.saturating_sub(jitter) as i64; + state.resend_timer = AtomicI64::new(i64::MAX); + state.beta = ZetaAutomata::S2; + state.timeout_timer + }; + drop(kex_lock); + ctx.session_queue + .lock() + .unwrap() + .change_priority(session.queue_idx, Reverse(timeout_timer)); + state = session.state.read().unwrap(); + } + } + + let mut c2 = ArrayVec::::new(); + c2.extend([0u8; HEADER_SIZE]); + if !send_control(session, &state, PACKET_TYPE_ACK, c2, send) { + return Err(fault!(OutOfSequence, true)); + } + + Ok(just_establised) +} +/// Corresponds to the trivial Transition Algorithm described for processing C_2 packets found in +/// Section 4.3. +pub(crate) fn received_c2_trans>( + app: &mut App, + ctx: &Arc>, + session: &Arc>, + kid: NonZeroU32, + n: &[u8; AES_GCM_NONCE_SIZE], + c2: &[u8], +) -> Result<(), ReceiveError> { + use FaultType::*; + + if c2.len() != ACKNOWLEDGEMENT_SIZE { + return Err(fault!(InvalidPacket, true)); + } + + let kex_lock = session.state_machine_lock.lock().unwrap(); + let state = session.state.read().unwrap(); + + if Some(kid) != state.key_ref(false).recv.kid { + // Some acknowledgement may have arrived extremely delayed. + return Err(fault!(UnknownLocalKeyId, false)); + } + if !matches!(&state.beta, ZetaAutomata::S1) { + // Some acknowledgement may have arrived extremely delayed. + return Err(fault!(OutOfSequence, false)); + } + + let tag = c2[..].try_into().unwrap(); + if !Crypto::Aead::decrypt_in_place(state.key_ref(false).recv.kek.as_ref().unwrap(), n, &[], &mut [], tag) { + return Err(fault!(FailedAuth, true)); + } + let (_, c) = from_nonce(n); + if !session.window.update(c) { + return Err(fault!(ExpiredCounter, true)); + } + drop(state); + let timeout_timer = { + let mut state = session.state.write().unwrap(); + let jitter = ctx.rng.lock().unwrap().next_u64() % Crypto::SETTINGS.rekey_time_max_jitter; + state.timeout_timer = app.time() + Crypto::SETTINGS.rekey_after_time.saturating_sub(jitter) as i64; + state.resend_timer = AtomicI64::new(i64::MAX); + state.beta = ZetaAutomata::S2; + state.timeout_timer + }; + drop(kex_lock); + ctx.session_queue + .lock() + .unwrap() + .change_priority(session.queue_idx, Reverse(timeout_timer)); + Ok(()) +} +/// Corresponds to the trivial Transition Algorithm described for processing D packets found in +/// Section 4.3. +pub(crate) fn received_d_trans( + session: &Arc>, + kid: NonZeroU32, + n: &[u8; AES_GCM_NONCE_SIZE], + d: &[u8], +) -> Result<(), ReceiveError> { + use FaultType::*; + + if d.len() != SESSION_REJECTED_SIZE { + return Err(fault!(InvalidPacket, true)); + } + + let kex_lock = session.state_machine_lock.lock().unwrap(); + let state = session.state.read().unwrap(); + + if Some(kid) != state.key_ref(true).recv.kid || !matches!(&state.beta, ZetaAutomata::A3 { .. }) { + return Err(fault!(OutOfSequence, true)); + } + + let tag = d[..].try_into().unwrap(); + if !Crypto::Aead::decrypt_in_place(state.key_ref(true).recv.kek.as_ref().unwrap(), n, &[], &mut [], tag) { + return Err(fault!(FailedAuth, true)); + } + let (_, c) = from_nonce(n); + if !session.window.update(c) { + return Err(fault!(ExpiredCounter, true)); + } + + drop(state); + drop(kex_lock); + session.expire(); + Ok(()) +} +// Corresponds to the timeout timer Transition Algorithm described in Section 4.1 - Definition 3. +fn timeout_trans>( + app: &mut App, + ctx: &Arc>, + session: &Arc>, + kex_lock: MutexGuard<'_, ()>, + state: RwLockReadGuard<'_, MutableState>, + current_time: i64, + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Option { + match &state.beta { + ZetaAutomata::Null => None, + ZetaAutomata::A1(_) | ZetaAutomata::A3 { .. } => { + let identity = match &state.beta { + ZetaAutomata::A1(a1) => &a1.identity, + ZetaAutomata::A3(a3) => &a3.identity, + _ => unreachable!(), + }; + if matches!(&state.beta, ZetaAutomata::A1(_)) { + log!(app, TimeoutX1(session)); + } else { + log!(app, TimeoutX3(session)); + } + let new_kid_recv = remap(ctx, session, &state); + + let hash = &mut Crypto::Hash::new(); + let hmac = &mut Crypto::Hmac::new(); + if let Some(a1) = create_a1_state( + hash, + hmac, + &ctx.rng, + &session.s_remote, + new_kid_recv, + &state.ratchet_state1, + state.ratchet_state2.as_ref(), + identity, + ) { + let mut hk_recv = Zeroizing::new([0u8; HASHLEN]); + let mut hk_send = Zeroizing::new([0u8; HASHLEN]); + a1.noise.get_ask(hmac, LABEL_HEADER_KEY, &mut hk_recv, &mut hk_send); + let mut x1 = a1.x1.clone(); + + drop(state); + let resend_timer = { + let mut state = session.state.write().unwrap(); + state.hk_recv.reset((&hk_recv[..AES_256_KEY_SIZE]).try_into().unwrap()); + state.hk_send.reset((&hk_send[..AES_256_KEY_SIZE]).try_into().unwrap()); + *state.key_mut(true) = DuplexKey::default(); + state.key_mut(true).recv.kid = Some(new_kid_recv); + let resend_timer = current_time + Crypto::SETTINGS.resend_time as i64; + state.resend_timer = AtomicI64::new(resend_timer); + state.timeout_timer = current_time + Crypto::SETTINGS.initial_offer_timeout as i64; + state.beta = ZetaAutomata::A1(a1); + resend_timer + }; + drop(kex_lock); + + send(&mut x1, None); + Some(resend_timer) + } else { + None + } + } + ZetaAutomata::S2 => { + // Corresponds to Transition Algorithm 6 found in Section 4.3. + log!(app, StartedRekeyingSentK1(session)); + let new_kid_recv = remap(ctx, session, &state); + // -> s + // <- s + // ... + // -> psk, e, es, ss + let mut noise = SymmetricState::initialize(PROTOCOL_NAME_NOISE_KK); + let hash = &mut Crypto::Hash::new(); + let hmac = &mut Crypto::Hmac::new(); + let mut k1 = ArrayVec::::new(); + k1.extend([0u8; HEADER_SIZE]); + // Noise process prologue. + noise.mix_hash(hash, &ctx.s_secret.public_key_bytes()); + noise.mix_hash(hash, &session.s_remote.to_bytes()); + // Process message pattern 1 psk0 token. + noise.mix_key_and_hash_no_init(hash, hmac, state.ratchet_state1.key.as_ref()); + // Process message pattern 1 e token. + let e_secret = noise.write_e_no_init(hash, hmac, &ctx.rng, &mut k1); + // Process message pattern 1 es token. + if noise.mix_dh_no_init(hmac, &e_secret, &session.s_remote).is_none() { + return None; + } + // Process message pattern 1 ss token. + noise.mix_key(hmac, session.noise_kk_ss.as_ref()); + // Process message pattern 1 payload. + let i = k1.len(); + k1.extend(new_kid_recv.get().to_ne_bytes()); + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_REKEY_INIT, 0), &mut k1[i..]); + k1.extend(tag); + + drop(state); + let resend_timer = { + let mut state = session.state.write().unwrap(); + state.key_mut(true).recv.kid = Some(new_kid_recv); + state.timeout_timer = current_time + Crypto::SETTINGS.rekey_timeout as i64; + let resend_timer = current_time + Crypto::SETTINGS.resend_time as i64; + state.resend_timer = AtomicI64::new(resend_timer); + state.beta = ZetaAutomata::R1 { noise, e_secret, k1: k1.clone() }; + resend_timer + }; + drop(kex_lock); + let state = session.state.read().unwrap(); + + send_control(session, &state, PACKET_TYPE_REKEY_INIT, k1, send); + Some(resend_timer) + } + ZetaAutomata::S1 { .. } => { + log!(app, TimeoutKeyConfirm(session)); + None + } + ZetaAutomata::R1 { .. } => { + log!(app, TimeoutK1(session)); + None + } + ZetaAutomata::R2 { .. } => { + log!(app, TimeoutK2(session)); + None + } + } +} +/// Corresponds to the timer rules of the Zeta State Machine found in Section 4.1 - Definition 3. +pub(crate) fn process_timers>( + app: &mut App, + ctx: &Arc>, + session: &Arc>, + current_time: i64, + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Option { + let kex_lock = session.state_machine_lock.lock().unwrap(); + let state = session.state.read().unwrap(); + if state.timeout_timer <= current_time { + // Corresponds to the timeout timer Transition Algorithm described in Section 4.1 - Definition 3. + timeout_trans(app, ctx, session, kex_lock, state, current_time, send) + } else { + let ts = state.resend_timer.load(Ordering::Relaxed); + let resend_next = current_time + Crypto::SETTINGS.resend_time as i64; + if ts <= current_time && state.resend_timer.fetch_max(resend_next, Ordering::Relaxed) == ts { + // Corresponds to the resend timer rules found in Section 4.1 - Definition 3. + + let (packet_type, control_payload) = match &state.beta { + ZetaAutomata::Null => return None, + ZetaAutomata::A1(a1) => { + log!(app, ResentX1(session)); + send(&mut a1.x1.clone(), None); + return Some(resend_next); + } + ZetaAutomata::A3(a3) => { + log!(app, ResentX3(session)); + send(&mut a3.x3.clone(), Some(&state.hk_send)); + return Some(resend_next); + } + ZetaAutomata::S1 => { + log!(app, ResentKeyConfirm(session)); + let mut c1 = ArrayVec::new(); + c1.extend([0u8; HEADER_SIZE]); + (PACKET_TYPE_KEY_CONFIRM, c1) + } + ZetaAutomata::S2 => return Some(state.timeout_timer), + ZetaAutomata::R1 { k1, .. } => { + log!(app, ResentK1(session)); + (PACKET_TYPE_REKEY_INIT, k1.clone()) + } + ZetaAutomata::R2 { k2, .. } => { + log!(app, ResentK2(session)); + (PACKET_TYPE_REKEY_COMPLETE, k2.clone()) + } + }; + + send_control(session, &state, packet_type, control_payload, send); + Some(resend_next) + } else { + Some(ts) + } + } +} +/// Corresponds to Transition Algorithm 7 found in Section 4.3. +pub(crate) fn received_k1_trans>( + app: &mut App, + ctx: &Arc>, + session: &Arc>, + kid: NonZeroU32, + n: &[u8; AES_GCM_NONCE_SIZE], + k1: &mut [u8], + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Result<(), ReceiveError> { + use FaultType::*; + // -> s + // <- s + // ... + // -> psk, e, es, ss + // <- e, ee, se + if k1.len() != REKEY_SIZE { + return Err(fault!(InvalidPacket, true)); + } + + let kex_lock = session.state_machine_lock.lock().unwrap(); + let state = session.state.read().unwrap(); + + if Some(kid) != state.key_ref(false).recv.kid { + // Some rekey packet may have arrived extremely delayed. + return Err(fault!(UnknownLocalKeyId, false)); + } + let should_rekey_as_bob = match &state.beta { + ZetaAutomata::S2 { .. } => true, + ZetaAutomata::R1 { .. } => session.was_bob, + _ => false, + }; + if !should_rekey_as_bob { + // Some rekey packet may have arrived extremely delayed. + return Err(fault!(OutOfSequence, false)); + } + + let i = k1.len() - AES_GCM_TAG_SIZE; + let tag = k1[i..].try_into().unwrap(); + let kek_recv = state.key_ref(false).recv.kek.as_ref().unwrap(); + if !Crypto::Aead::decrypt_in_place(kek_recv, n, &[], &mut k1[..i], &tag) { + return Err(fault!(FailedAuth, true)); + } + let (_, c) = from_nonce(n); + if !session.window.update(c) { + return Err(fault!(ExpiredCounter, true)); + } + + let result = (|| { + let mut i = 0; + let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_KK); + let hash = &mut Crypto::Hash::new(); + let hmac = &mut Crypto::Hmac::new(); + // Noise process prologue. + noise.mix_hash(hash, &session.s_remote.to_bytes()); + noise.mix_hash(hash, &ctx.s_secret.public_key_bytes()); + // Process message pattern 1 psk0 token. + noise.mix_key_and_hash_no_init(hash, hmac, state.ratchet_state1.key.as_ref()); + // Process message pattern 1 e token. + let e_remote = noise + .read_e_no_init(hash, hmac, &mut i, &k1) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 1 es token. + noise + .mix_dh_no_init(hmac, &ctx.s_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 1 ss token. + noise.mix_key(hmac, session.noise_kk_ss.as_ref()); + // Process message pattern 1 payload. + let j = i + KID_SIZE; + let k = j + AES_GCM_TAG_SIZE; + let tag = k1[j..k].try_into().unwrap(); + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_REKEY_INIT, 0), &mut k1[i..j], tag) { + return Err(fault!(FailedAuth, true)); + } + let kid_send = + NonZeroU32::new(u32::from_ne_bytes(k1[i..j].try_into().unwrap())).ok_or(fault!(FailedAuth, true))?; + + let mut k2 = ArrayVec::::new(); + k2.extend([0u8; HEADER_SIZE]); + // Process message pattern 2 e token. + let e_secret = noise.write_e_no_init(hash, hmac, &ctx.rng, &mut k2); + // Process message pattern 2 ee token. + noise + .mix_dh_no_init(hmac, &e_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 se token. + noise + .mix_dh(hmac, &ctx.s_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 payload. + let i = k2.len(); + let new_kid_recv = remap(ctx, session, &state); + k2.extend(new_kid_recv.get().to_ne_bytes()); + let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_REKEY_COMPLETE, 0), &mut k2[i..]); + k2.extend(tag); + + let new_ratchet_state = create_ratchet_state(hmac, &noise, state.ratchet_state1.chain_len); + app.save_ratchet_state( + &session.s_remote, + &session.session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: Some(&state.ratchet_state1), + state1_was_just_added: true, + deleted_state1: state.ratchet_state2.as_ref(), + deleted_state2: None, + }, + ) + .map_err(|_| ReceiveError::RatchetStorageError)?; + + let mut kek_recv = Zeroizing::new([0u8; HASHLEN]); + let mut kek_send = Zeroizing::new([0u8; HASHLEN]); + let mut nk_recv = Zeroizing::new([0u8; HASHLEN]); + let mut nk_send = Zeroizing::new([0u8; HASHLEN]); + noise.get_ask(hmac, LABEL_KEX_KEY, &mut kek_send, &mut kek_recv); + noise.split(hmac, &mut nk_send, &mut nk_recv); + + drop(state); + let resend_timer = { + let mut state = session.state.write().unwrap(); + state.key_mut(true).replace_nk(&nk_send, &nk_recv); + state.key_mut(true).send.kid = Some(kid_send); + state.key_mut(true).send.replace_kek(&kek_send); + state.key_mut(true).recv.kid = Some(new_kid_recv); + state.key_mut(true).recv.replace_kek(&kek_recv); + state.ratchet_state2 = Some(state.ratchet_state1.clone()); + state.ratchet_state1 = new_ratchet_state.clone(); + let current_time = app.time(); + state.key_creation_counter = session.send_counter.load(Ordering::Relaxed); + let resend_timer = current_time + Crypto::SETTINGS.resend_time as i64; + state.timeout_timer = current_time + Crypto::SETTINGS.rekey_timeout as i64; + state.resend_timer = AtomicI64::new(resend_timer); + state.beta = ZetaAutomata::R2 { k2: k2.clone() }; + resend_timer + }; + drop(kex_lock); + ctx.session_queue + .lock() + .unwrap() + .change_priority(session.queue_idx, Reverse(resend_timer)); + let state = session.state.read().unwrap(); + + if !send_control(session, &state, PACKET_TYPE_REKEY_COMPLETE, k2, send) { + return Err(fault!(OutOfSequence, true)); + } + + Ok(()) + })(); + + if matches!(result, Err(ReceiveError::ByzantineFault { .. })) { + session.expire(); + } + result +} +/// Corresponds to Transition Algorithm 8 found in Section 4.3. +pub(crate) fn received_k2_trans>( + app: &mut App, + ctx: &Arc>, + session: &Arc>, + kid: NonZeroU32, + n: &[u8; AES_GCM_NONCE_SIZE], + k2: &mut [u8], + send: impl FnOnce(&mut [u8], Option<&Crypto::PrpEnc>), +) -> Result<(), ReceiveError> { + use FaultType::*; + // <- e, ee, se + if k2.len() != REKEY_SIZE { + return Err(fault!(InvalidPacket, true)); + } + + let kex_lock = session.state_machine_lock.lock().unwrap(); + let state = session.state.read().unwrap(); + + if Some(kid) != state.key_ref(false).recv.kid { + // Some rekey packet may have arrived extremely delayed. + return Err(fault!(UnknownLocalKeyId, false)); + } + if !matches!(&state.beta, ZetaAutomata::R1 { .. }) { + // Some rekey packet may have arrived extremely delayed. + return Err(fault!(OutOfSequence, false)); + } + + let i = k2.len() - AES_GCM_TAG_SIZE; + let tag = k2[i..].try_into().unwrap(); + let kek_recv = state.key_ref(false).recv.kek.as_ref().unwrap(); + if !Crypto::Aead::decrypt_in_place(kek_recv, n, &[], &mut k2[..i], &tag) { + return Err(fault!(FailedAuth, true)); + } + let (_, c) = from_nonce(n); + if !session.window.update(c) { + return Err(fault!(ExpiredCounter, true)); + } + let result = (|| { + if let ZetaAutomata::R1 { noise, e_secret, .. } = &state.beta { + let mut noise = noise.clone(); + let mut i = 0; + let hash = &mut Crypto::Hash::new(); + let hmac = &mut Crypto::Hmac::new(); + // Process message pattern 2 e token. + let e_remote = noise + .read_e_no_init(hash, hmac, &mut i, &k2) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 ee token. + noise + .mix_dh_no_init(hmac, e_secret, &e_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 se token. + noise + .mix_dh(hmac, e_secret, &session.s_remote) + .ok_or(fault!(FailedAuth, true))?; + // Process message pattern 2 payload. + let j = i + KID_SIZE; + let k = j + AES_GCM_TAG_SIZE; + let tag = k2[j..k].try_into().unwrap(); + if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_REKEY_COMPLETE, 0), &mut k2[i..j], tag) { + return Err(fault!(FailedAuth, true)); + } + let kid_send = + NonZeroU32::new(u32::from_ne_bytes(k2[i..j].try_into().unwrap())).ok_or(fault!(InvalidPacket, true))?; + + let new_ratchet_state = create_ratchet_state(hmac, &noise, state.ratchet_state1.chain_len); + app.save_ratchet_state( + &session.s_remote, + &session.session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: None, + state1_was_just_added: true, + deleted_state1: Some(&state.ratchet_state1), + deleted_state2: state.ratchet_state2.as_ref(), + }, + ) + .map_err(|_| ReceiveError::RatchetStorageError)?; + + let mut kek_recv = Zeroizing::new([0u8; HASHLEN]); + let mut kek_send = Zeroizing::new([0u8; HASHLEN]); + let mut nk_recv = Zeroizing::new([0u8; HASHLEN]); + let mut nk_send = Zeroizing::new([0u8; HASHLEN]); + noise.get_ask(hmac, LABEL_KEX_KEY, &mut kek_recv, &mut kek_send); + noise.split(hmac, &mut nk_recv, &mut nk_send); + + drop(state); + let resend_timer = { + let mut state = session.state.write().unwrap(); + state.key_mut(true).replace_nk(&nk_send, &nk_recv); + state.key_mut(true).send.kid = Some(kid_send); + state.key_mut(true).send.replace_kek(&kek_send); + state.key_mut(true).recv.replace_kek(&kek_recv); + state.ratchet_state1 = new_ratchet_state.clone(); + state.key_index ^= true; + let current_time = app.time(); + state.key_creation_counter = session.send_counter.load(Ordering::Relaxed); + let resend_timer = current_time + Crypto::SETTINGS.resend_time as i64; + state.timeout_timer = current_time + Crypto::SETTINGS.rekey_timeout as i64; + state.resend_timer = AtomicI64::new(resend_timer); + state.beta = ZetaAutomata::S1; + resend_timer + }; + drop(kex_lock); + ctx.session_queue + .lock() + .unwrap() + .change_priority(session.queue_idx, Reverse(resend_timer)); + let state = session.state.read().unwrap(); + + let mut c1 = ArrayVec::::new(); + c1.extend([0u8; HEADER_SIZE]); + if !send_control(&session, &state, PACKET_TYPE_KEY_CONFIRM, c1, send) { + return Err(fault!(OutOfSequence, true)); + } + + Ok(()) + } else { + unreachable!() + } + })(); + + if matches!(result, Err(ReceiveError::ByzantineFault { .. })) { + session.expire(); + } + result +} +/// Corresponds to Algorithm 9 found in Section 4.3. +pub(crate) fn send_payload( + ctx: &Arc>, + session: &Arc>, + payload: &[u8], + mut send: impl FnMut(&mut [u8]) -> bool, + mtu_sized_buffer: &mut [u8], +) -> Result<(), SendError> { + use SendError::*; + let mtu = mtu_sized_buffer.len(); + if mtu < MIN_TRANSPORT_MTU { + return Err(MtuTooSmall); + } + + let state = session.state.read().unwrap(); + let (c, should_rekey) = get_counter(session, &state).ok_or(SessionExpired)?; + let nonce = to_nonce(PACKET_TYPE_DATA, c); + + let key = state.key_ref(false); + let kid_send = key.send.kid.ok_or(SessionNotEstablished)?.get().to_ne_bytes(); + let mut cipher = key.nk.as_ref().ok_or(SessionNotEstablished)?.start_enc(&nonce); + + debug_assert!(matches!( + &state.beta, + ZetaAutomata::S1 | ZetaAutomata::S2 | ZetaAutomata::R1 { .. } | ZetaAutomata::R2 { .. } + )); + + let payload_mtu = mtu - HEADER_SIZE; + debug_assert!(payload_mtu >= 4); + let tagged_payload_len = payload.len() + AES_GCM_TAG_SIZE; + let fragment_count = tagged_payload_len.saturating_add(payload_mtu - 1) / payload_mtu; // Ceiling div. + let fragment_base_size = tagged_payload_len / fragment_count; + let fragment_size_remainder = tagged_payload_len % fragment_count; + + let mut header = [0u8; HEADER_SIZE]; + header[..KID_SIZE].copy_from_slice(&kid_send); + header[FRAGMENT_COUNT_IDX] = fragment_count as u8; + header[PACKET_NONCE_START..].copy_from_slice(&nonce[NONCE_SIZE_DIFF..]); + + let mut i = 0; + for fragment_no in 0..fragment_count - 1 { + let fragment_len = fragment_base_size + (fragment_no < fragment_size_remainder) as usize; + let j = i + fragment_len; + + mtu_sized_buffer[..HEADER_SIZE].copy_from_slice(&header); + mtu_sized_buffer[FRAGMENT_NO_IDX] = fragment_no as u8; + let fragment_start = &mut mtu_sized_buffer[HEADER_SIZE..HEADER_SIZE + fragment_len]; + cipher.encrypt(&payload[i..j], fragment_start); + + let header_auth = &mut mtu_sized_buffer[HEADER_AUTH_START..HEADER_AUTH_END]; + state.hk_send.encrypt_in_place(header_auth.try_into().unwrap()); + + if !send(&mut mtu_sized_buffer[..HEADER_SIZE + fragment_len]) { + return Ok(()); + } + i = j; + } + let fragment_no = fragment_count - 1; + let payload_rem = payload.len() - i; + let fragment_len = payload_rem + AES_GCM_TAG_SIZE; + debug_assert_eq!(fragment_len, fragment_base_size); + + mtu_sized_buffer[..HEADER_SIZE].copy_from_slice(&header); + mtu_sized_buffer[FRAGMENT_NO_IDX] = fragment_no as u8; + let fragment_start = &mut mtu_sized_buffer[HEADER_SIZE..HEADER_SIZE + payload_rem]; + cipher.encrypt(&payload[i..], fragment_start); + mtu_sized_buffer[HEADER_SIZE + payload_rem..HEADER_SIZE + fragment_len].copy_from_slice(&cipher.finish()); + + let header_auth = &mut mtu_sized_buffer[HEADER_AUTH_START..HEADER_AUTH_END]; + state.hk_send.encrypt_in_place(header_auth.try_into().unwrap()); + + if !send(&mut mtu_sized_buffer[..HEADER_SIZE + fragment_len]) { + return Ok(()); + } + + drop(state); + + if should_rekey { + let mut state = session.state.write().unwrap(); + state.timeout_timer = i64::MIN; + drop(state); + ctx.session_queue + .lock() + .unwrap() + .change_priority(session.queue_idx, Reverse(i64::MIN)); + } + Ok(()) +} +/// Corresponds to Algorithm 10 found in Section 4.3. +pub(crate) fn receive_payload_in_place( + session: &Arc>, + state: RwLockReadGuard<'_, MutableState>, + kid: NonZeroU32, + nonce: &[u8; AES_GCM_NONCE_SIZE], + fragments: &mut [Crypto::IncomingPacketBuffer], + mut output_buffer: impl Write, +) -> Result<(), ReceiveError> { + use FaultType::*; + debug_assert!(!fragments.is_empty()); + + let specified_key = if Some(kid) == state.keys[0].recv.kid { + state.keys[0].nk.as_ref() + } else if Some(kid) == state.keys[1].recv.kid { + state.keys[1].nk.as_ref() + } else { + // Should be unreachable unless we are leaking kids somewhere. + return Err(fault!(UnknownLocalKeyId, true)); + }; + + let mut cipher = specified_key.ok_or(fault!(OutOfSequence, true))?.start_dec(nonce); + let (_, c) = from_nonce(nonce); + + // NOTE: This only works because we check the size of every received fragment in the receive + // function, otherwise this could panic. + for i in 0..fragments.len() - 1 { + let fragment = &mut fragments[i].as_mut()[HEADER_SIZE..]; + debug_assert!(fragment.len() >= AES_GCM_TAG_SIZE); + cipher.decrypt_in_place(fragment); + } + let fragment = &mut fragments[fragments.len() - 1].as_mut()[HEADER_SIZE..]; + debug_assert!(fragment.len() >= AES_GCM_TAG_SIZE); + let tag_idx = fragment.len() - AES_GCM_TAG_SIZE; + cipher.decrypt_in_place(&mut fragment[..tag_idx]); + + if !cipher.finish((&fragment[tag_idx..]).try_into().unwrap()) { + return Err(fault!(FailedAuth, true)); + } + + if !session.window.update(c) { + // This error is marked as not happening naturally, but it could occur if something about + // the transport protocol is duplicating packets. + return Err(fault!(ExpiredCounter, true)); + } + + for fragment in fragments { + let result = output_buffer.write(&fragment.as_ref()[HEADER_SIZE..]); + if let Err(e) = result { + return Err(ReceiveError::IoError(e)); + } + } + + Ok(()) +} + +impl Drop for Session { + fn drop(&mut self) { + self.expire(); + } +} +impl Session { + /// Mark a session as expired. This will make it impossible for this session to successfully + /// receive or send data or control packets. It is recommended to simply `drop` the session + /// instead, but this can provide some reassurance in complex shared ownership situations. + pub fn expire(&self) { + if let Some(ctx) = self.ctx.upgrade() { + self.expire_inner(Some(&ctx), Some(&mut ctx.session_queue.lock().unwrap())); + } else { + self.expire_inner(None, None); + } + } + /// Allows us to expire sessions with the correct locking order, preventing deadlock. + pub(crate) fn expire_inner( + &self, + ctx: Option<&Arc>>, + session_queue: Option<&mut SessionQueue>, + ) { + let _kex_lock = self.state_machine_lock.lock().unwrap(); + let mut state = self.state.write().unwrap(); + let mut kids_to_remove = None; + if !matches!(&state.beta, ZetaAutomata::Null) { + self.session_has_expired.store(true, Ordering::Relaxed); + kids_to_remove = Some([state.keys[0].recv.kid, state.keys[1].recv.kid]); + state.keys = [DuplexKey::default(), DuplexKey::default()]; + state.resend_timer = AtomicI64::new(i64::MAX); + state.timeout_timer = i64::MAX; + state.beta = ZetaAutomata::Null; + } + if let Some(session_queue) = session_queue { + session_queue.remove(self.queue_idx); + } + if let (Some(ctx), Some(kids_to_remove)) = (ctx, kids_to_remove) { + let mut session_map = ctx.session_map.write().unwrap(); + for kid_recv in kids_to_remove.iter().flatten() { + session_map.remove(kid_recv); + } + } + } + /// + ///// The current ratchet state of this session. + ///// The returned values are sensitive and should be securely erased before being dropped. + pub fn ratchet_states(&self) -> RatchetStates { + let state = self.state.read().unwrap(); + RatchetStates::new(state.ratchet_state1.clone(), state.ratchet_state2.clone()) + } + /// The current ratchet count of this session. + pub fn ratchet_count(&self) -> u64 { + self.state.read().unwrap().ratchet_state1.chain_len + } + /// Check whether this session is established. + pub fn established(&self) -> bool { + let state = self.state.read().unwrap(); + !matches!( + &state.beta, + ZetaAutomata::A1(_) | ZetaAutomata::A3 { .. } | ZetaAutomata::Null + ) + } + /// The static public key of the remote peer. + pub fn remote_static_key(&self) -> &Crypto::PublicKey { + &self.s_remote + } +} diff --git a/src/zssp.rs b/src/zssp.rs index 897391b..91f8e66 100644 --- a/src/zssp.rs +++ b/src/zssp.rs @@ -1,38 +1,34 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this -* file, You can obtain one at https://mozilla.org/MPL/2.0/. -* -* (c) ZeroTier, Inc. -* https://www.zerotier.com/ -*/ -// ZSSP: ZeroTier Secure Session Protocol -// FIPS compliant Noise_XK with Jedi powers (Kyber1024) and built-in attack-resistant large payload (fragmentation) support. - use std::cmp::Reverse; use std::collections::HashMap; use std::hash::Hash; -use std::num::{NonZeroU32, NonZeroU64}; -use std::ops::DerefMut; -use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; +use std::io::Write; +use std::num::NonZeroU32; +use std::sync::{Arc, Mutex, RwLock, Weak}; -use crate::crypto::aes::{AesDec, AesEnc}; -use crate::crypto::aes_gcm::{AesGcmDec, AesGcmEnc, AES_GCM_IV_SIZE, AES_GCM_KEY_SIZE, AES_GCM_TAG_SIZE}; -use crate::crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE, P384_PUBLIC_KEY_SIZE}; -use crate::crypto::pqc_kyber::KYBER_SECRETKEYBYTES; -use crate::crypto::rand_core::RngCore; -use crate::crypto::secret::{secure_eq, Secret}; -use crate::crypto::sha512::{HmacSha512, Sha512}; +use arrayvec::ArrayVec; +use rand_core::RngCore; -use crate::error::{FaultType, OpenError, ReceiveError, SendError}; +use crate::application::*; +use crate::challenge::ChallengeContext; +use crate::crypto::*; use crate::frag_cache::UnassociatedFragCache; -use crate::fragged::{Assembled, Fragged}; +use crate::fragged::Assembled; use crate::handshake_cache::UnassociatedHandshakeCache; -use crate::indexed_heap::{BinaryHeapIndex, IndexedBinaryHeap}; -use crate::log_event::LogEvent; +use crate::indexed_heap::IndexedBinaryHeap; use crate::proto::*; -use crate::symmetric_state::SymmetricState; -use crate::{applicationlayer::*, RatchetState}; +use crate::result::{fault, FaultType, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent}; +use crate::zeta::*; +#[cfg(feature = "logging")] +use crate::LogEvent::*; + +/// Macro to turn off logging at compile time. +macro_rules! log { + ($app:expr, $event:expr) => { + #[cfg(feature = "logging")] + $app.event_log($event); + }; +} +pub(crate) use log; /// Session context for local application. /// @@ -40,416 +36,92 @@ use crate::{applicationlayer::*, RatchetState}; /// defragment incoming packets that are not yet associated with a session. /// /// Internally this is just a clonable Arc, so it can be safely shared with multiple threads. -pub struct Context(pub Arc>); -impl Clone for Context { +pub struct Context(pub Arc>); +impl Clone for Context { fn clone(&self) -> Self { Self(self.0.clone()) } } -pub struct ContextInner { - static_keypair: Application::KeyPair, - unassociated_defrag_cache: Mutex>, - unassociated_handshake_states: UnassociatedHandshakeCache, + +pub(crate) type SessionMap = RwLock>>>; +pub(crate) type SessionQueue = IndexedBinaryHeap>, Reverse>; +pub struct ContextInner { + pub rng: Mutex, + pub(crate) s_secret: Crypto::KeyPair, /// `session_queue -> state_machine_lock -> state -> session_map` - session_queue: Mutex>, Reverse>>, - session_map: RwLock>, bool)>>, - challenge_counter: AtomicU64, - challenge_antireplay_window: [AtomicU64; CHALLENGE_COUNTER_WINDOW_MAX_OOO], - challenge_salt: [u8; CHALLENGE_SALT_SIZE], - rng: Mutex, + pub(crate) session_queue: Mutex>, + /// `session_queue -> state_machine_lock -> state -> session_map` + pub(crate) session_map: SessionMap, + pub(crate) unassociated_defrag_cache: Mutex>, + pub(crate) unassociated_handshake_states: UnassociatedHandshakeCache, + + pub(crate) challenge: ChallengeContext, } -/// Result generated by the context packet receive function, with possible payloads. -pub enum ReceiveResult<'b, Application: ApplicationLayer> { - /// Packet superficially appeared valid but is not associated with a session yet. - /// This can occur because the packet was only a fragment of a larger packet, - /// or if it was a control packet that does not go through full Noise authentication. - Unassociated, - /// Packet was authentic and belongs to this specific session. - Session(Arc>, SessionEvent<'b>), - /// Packet was a part of a handshake, and while it superficially appeared valid the application - /// explicitly rejected it. - /// Relates to callbacks `check_allow_incoming_session`, `hello_requires_recognized_ratchet` - /// and `check_accept_session`. - Rejected, +fn parse_fragment_header(incoming_fragment: &[u8]) -> Result<(usize, usize, [u8; AES_GCM_NONCE_SIZE]), ReceiveError> { + let fragment_no = incoming_fragment[FRAGMENT_NO_IDX] as usize; + let fragment_count = incoming_fragment[FRAGMENT_COUNT_IDX] as usize; + if fragment_no >= fragment_count || fragment_count > MAX_FRAGMENTS { + return Err(fault!(FaultType::InvalidPacket, true)); + } + let mut nonce = [0u8; AES_GCM_NONCE_SIZE]; + nonce[2..].copy_from_slice(&incoming_fragment[PACKET_NONCE_START..HEADER_SIZE]); + Ok((fragment_no, fragment_count, nonce)) } -#[derive(Debug, PartialEq, Eq)] -pub enum SessionEvent<'b> { - /// The received packet was valid, and it contained the necessary keys to fully establish a new - /// session with Alice, the handshake initiator. - /// - /// If the session Arc returned is dropped, the session with this peer will be immediately - /// terminated. Save the session Arc to some long lived datastructure to keep it alive. - NewSession, - /// When Alice calls `Context::open`, a session will be created, but Bob will not yet have - /// received this session. They will have to successfully complete a handshake first. - /// - /// Alice will receive this return value when the received packet confirms both parties - /// have completed the initial handshake and now have a shared session with each other. - /// If according to the upper protocol, Bob is the first party to send data, it is possible for - /// Alice to start receiving data from Bob before this value is returned. - /// - /// This return value can only occur once per session, only for session objects that were - /// created with `Context::open`. - Established, - /// Bob explicitly refused to establish a session with Alice, and sent us an error code. - /// The application should immediately drop this session as Bob will not allow us to connect. - /// - /// This return value cannot occur after a session is fully established. - Rejected, - /// The received packet was valid and a data payload was decoded and authenticated. - Data(&'b mut [u8]), - /// The received packet was some authentic protocol control packet. No action needs to be taken. - Control, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum IncomingSessionAction { - Allow, - Challenge, - Drop, -} - -/// ZeroTier Secure Session Protocol (ZSSP) Session +/// Fragments and sends the packet, destroying it in the process. /// -/// A FIPS/NIST compliant variant of Noise_XK with hybrid Kyber1024 PQ data forward secrecy. -pub struct Session { - /// An arbitrary application defined object associated with each session. - pub application_data: Application::Data, - /// The receive context associated with this session, - /// only this context can receive messages from the remote peer. - context: Weak>, - /// Handle into the session queue for changing the update timer. - queue_idx: BinaryHeapIndex, +/// Corresponds to the fragmentation algorithm described in Section 6. +fn send_with_fragmentation( + mut send: impl FnMut(&mut [u8]) -> bool, + mtu: usize, + headered_packet: &mut [u8], + hk_send: Option<&PrpEnc>, +) -> bool { + let payload_len = headered_packet.len() - HEADER_SIZE; + let payload_mtu = mtu - HEADER_SIZE; + debug_assert!(payload_mtu >= 4); + let fragment_count = payload_len.saturating_add(payload_mtu - 1) / payload_mtu; // Ceiling div. + let fragment_base_size = payload_len / fragment_count; + let fragment_size_remainder = payload_len % fragment_count; - remote_static_key: Application::PublicKey, - send_counter: AtomicU64, - /// This bool signals to all threads to stop incrementing the counter and instead error out. - session_has_expired: AtomicBool, - /// The following is a ring buffer of previously seen counter values, where we use the counter's - /// value as the index of the head of the ring buffer. - counter_antireplay_window: [AtomicU64; COUNTER_WINDOW_MAX_OOO], - /// Enforces atomicity of state machine transitions. - /// There is a standard locking sequence, - /// it goes `session_queue -> state_machine_lock -> state -> session_map`. - /// Any lock can be skipped but they must be locked in that order. - state_machine_lock: Mutex<()>, - state: RwLock>, - defrag: [Mutex>; SESSION_MAX_FRAGMENTS_OOO], - header_send_cipher: Application::PrpEnc, - header_receive_cipher: Application::PrpDec, - kex_send_cipher: Mutex>, - kex_receive_cipher: Mutex>, - /// Pre-computed rekeying values. - noise_kk_ss: Secret, - noise_kk_local_init_h: [u8; NOISE_HASHLEN], - noise_kk_remote_init_h: [u8; NOISE_HASHLEN], - was_bob: bool, -} -/// `AesGcm` is not threadsafe, but it is threadsafe when inside a `Mutex`. -unsafe impl Send for Session {} -unsafe impl Sync for Session {} + let mut header: [u8; HEADER_SIZE] = headered_packet[..HEADER_SIZE].try_into().unwrap(); + header[FRAGMENT_COUNT_IDX] = fragment_count as u8; -/// Session state may only be mutated during atomic transitions of the offer state machine. -struct SessionMutableState { - ratchet_states: [RatchetState; 2], - /// For OOO rekeying reliability we allow our version of Noise CipherState to hold the last two - /// session keys, instead of just the most recent one. - cipher_states: [Option>; 2], - /// This is the index of `noise_cipher_state` that contains the most recent key. - /// It will be attached to fragment headers to help with OOO transport. - current_key: usize, - /// This defines the exact state of the offer state machine we are in. - outgoing_offer: OfferStateMachine, -} + let mut i = HEADER_SIZE; + for fragment_no in 0..fragment_count { + let j = i + fragment_base_size + (fragment_no < fragment_size_remainder) as usize; + let fragment = &mut headered_packet[i - HEADER_SIZE..j]; -/// These offer enums form a state machine. -/// Documented below are the only legal transitions for this state machine. -/// A session is initialized with an `outgoing_offer` of either NoiseXKPattern1 or Normal. -enum OfferStateMachine { - Normal { - timeout: i64, - }, // -> NoiseKKPattern1, NoiseKKPattern2 - /// This state uses a lot of memory so we put it on the heap. - NoiseXKPattern1or3(Box>), // -> Normal - NoiseKKPattern1 { - next_retry_time: AtomicI64, - timeout: i64, - new_key_id: NonZeroU32, - noise_e_secret: Application::KeyPair, - noise_message: [u8; NoiseKKPattern1or2::SIZE], - noise_ck: SymmetricState, - noise_h_pskep: [u8; NOISE_HASHLEN], - }, // -> NoiseKKPattern2, KeyConfirm - NoiseKKPattern2 { - next_retry_time: AtomicI64, - timeout: i64, - noise_message: [u8; NoiseKKPattern1or2::SIZE], - kex_send_key: Secret, - }, // -> Normal - KeyConfirm { - next_retry_time: AtomicI64, - timeout: i64, - }, // -> Normal -} + fragment[..HEADER_SIZE].copy_from_slice(&header); + fragment[FRAGMENT_NO_IDX] = fragment_no as u8; -pub(crate) struct NoiseXKBobHandshakeState { - /// Can never be Null. - ratchet_state: RatchetState, - remote_key_id: NonZeroU32, - local_key_id: NonZeroU32, - header_receive_key: Secret, - header_send_key: Secret, - noise_h_ee1peekem1pskp: [u8; NOISE_HASHLEN], - noise_e_secret: Application::KeyPair, - noise_ck_eseeekem1psk: SymmetricState, - noise_k_eseeekem1psk: Secret, - noise_pattern3_defrag: Mutex>, -} - -struct NoiseXKAliceHandshake { - next_retry_time: AtomicI64, - timeout: i64, - /// A secure random number put in the header of Alice's fragments to identify them. - /// If a DDOS attacker could guess this they could block Alice starting the handshake. - local_key_id: NonZeroU32, - alice_identity_blob: Application::LocalIdentityBlob, - offer: NoiseXKAliceHandshakeState, -} - -enum NoiseXKAliceHandshakeState { - NoiseXKPattern1 { - noise_h_ee1p: [u8; NOISE_HASHLEN], - noise_e_secret: Application::KeyPair, - noise_e1_secret: Secret, - noise_ck_es: SymmetricState, - /// ZSSP assumes an unreliable, out-of-order physical transport environment, so for that - /// reason we have to resend key offers. - noise_message: [u8; NoiseXKPattern1::MAX_SIZE], - noise_message_len: usize, - message_id: u64, - }, - NoiseXKPattern3 { - noise_message: [u8; NoiseXKPattern3::MAX_SIZE], - noise_message_len: usize, - }, -} - -struct SessionKey { - remote_key_id: NonZeroU32, - local_key_id: NonZeroU32, - /// Pool of reusable sending ciphers. - receive_cipher_pool: [Mutex; 8], - /// Pool of reusable receiving ciphers. - send_cipher_pool: [Mutex; 8], - /// Rekey at or after this counter. - rekey_at_counter: u64, - /// Hard error when this counter value is reached or exceeded. - expire_at_counter: u64, -} - -macro_rules! byzantine_fault { - ($name:expr, $is_natural:ident) => { - ReceiveError::ByzantineFault { - file: file!(), - line: line!(), - error: $name, - is_naturally_occurring: $is_natural, + if let Some(hk_send) = hk_send { + hk_send.encrypt_in_place((&mut fragment[HEADER_AUTH_START..HEADER_AUTH_END]).try_into().unwrap()); } - }; + if !send(fragment) { + return false; + } + i = j; + } + true } -impl Context { +impl Context { /// Create a new session context. - pub fn new(static_keypair: Application::KeyPair, mut rng: Application::Rng) -> Self { - debug_assert!(Application::REKEY_AFTER_TIME_MAX_JITTER_MS > 0, "Invalid protocol constant"); - let mut challenge_salt = [0u8; CHALLENGE_SALT_SIZE]; - rng.fill_bytes(&mut challenge_salt); + pub fn new(static_secret_key: Crypto::KeyPair, mut rng: Crypto::Rng) -> Self { + let challenge = ChallengeContext::new(&mut rng); Self(Arc::new(ContextInner { - static_keypair, + rng: Mutex::new(rng), + s_secret: static_secret_key, + session_map: RwLock::new(HashMap::new()), + challenge, + session_queue: Mutex::new(IndexedBinaryHeap::new()), unassociated_defrag_cache: Mutex::new(UnassociatedFragCache::new()), unassociated_handshake_states: UnassociatedHandshakeCache::new(), - session_map: RwLock::new(HashMap::new()), - session_queue: Mutex::new(IndexedBinaryHeap::new()), - challenge_counter: AtomicU64::new(INIT_COUNTER), - challenge_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), - challenge_salt, - rng: Mutex::new(rng), })) } - /// Perform periodic background service and cleanup tasks. - /// - /// This returns the number of milliseconds until it should be called again. The caller should - /// try to satisfy this but small variations in timing of up to +/- a second or two are not - /// a problem. - /// - /// * `send_to` - Function to get a sender and an MTU to send something over an active session - /// * `current_time` - Current time in milliseconds. Does not have to be monotonic, nor synced - /// with remote peers (although both of these properties would help reliability slightly). - /// Used to determine if any current handshakes should be resent or timed-out, or if a session - /// should rekey. - #[inline] - pub fn service bool>( - &self, - app: &Application, - mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, - current_time: i64, - ) -> i64 { - let retry_next = current_time.saturating_add(Application::RETRY_INTERVAL_MS); - let mut next_service_time = 2 * Application::RETRY_INTERVAL_MS; - - let mut session_queue = self.0.session_queue.lock().unwrap(); - // This update system takes heavy advantage of the fact that sessions only need to be updated - // either roughly every second or roughly every hour. That big gap allows for minor optimizations. - // If the gap changes (unlikely) this code may need to be rewritten. - while let Some((session, timer, queue_idx)) = session_queue.peek() { - if timer.0 >= current_time { - next_service_time = next_service_time.min(timer.0 - current_time); - break; - } - let session = match session.upgrade() { - Some(s) => s, - _ => { - session_queue.remove(queue_idx); - continue; - } - }; - let state = session.state.read().unwrap(); - use OfferStateMachine::*; - let next_timer = match &state.outgoing_offer { - Normal { timeout, .. } => { - if *timeout <= current_time { - drop(state); - if let Some((send, _)) = send_to(&session) { - let result = initiate_rekey(&self.0, &session, send, current_time); - if result.is_ok() { - app.event_log(LogEvent::ServiceKKStart(&session), current_time); - } - result.unwrap_or(retry_next) - } else { - retry_next - } - } else { - *timeout - } - } - // If there's an outstanding attempt to open a session, retransmit this - // periodically in case the initial packet doesn't make it. - NoiseXKPattern1or3(handshake_state) => { - if let Some(ts) = process_timer(&handshake_state.next_retry_time, Application::RETRY_INTERVAL_MS, current_time) { - ts - } else { - // We have to eventually time out NoiseXKPattern3 because of unreliable network conditions. - if handshake_state.timeout <= current_time { - drop(state); - let _kex_lock = session.state_machine_lock.lock().unwrap(); - let mut state = session.state.write().unwrap(); - let ratchet_state = state.ratchet_states.clone(); - // Since we dropped the lock we must re-check if we are in the correct state. - if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { - if handshake_state.timeout <= current_time { - app.event_log(LogEvent::ServiceXKTimeout(&session), current_time); - if !handshake_state.reinitialize( - &session, - &ratchet_state, - &mut self.0.session_map.write().unwrap(), - &mut self.0.rng.lock().unwrap(), - current_time, - ) { - session.expire_inner(&self.0, &mut session_queue); - } - } - } - } else if let Some((mut send, mut mtu)) = send_to(&session) { - mtu = mtu.max(MIN_TRANSPORT_MTU); - match &handshake_state.offer { - NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } => { - app.event_log(LogEvent::ServiceXK1Resend(&session), current_time); - // We are in state NoiseXKPattern1 so resend noise_pattern1. - send_with_fragmentation( - &mut send, - mtu, - &mut noise_message.clone()[..*noise_message_len], - PACKET_TYPE_NOISE_XK_PATTERN_1, - None, - *message_id, - None::<&Application::PrpEnc>, - ); - } - NoiseXKAliceHandshakeState::NoiseXKPattern3 { noise_message, noise_message_len, .. } => { - app.event_log(LogEvent::ServiceXK3Resend(&session), current_time); - send_with_fragmentation( - &mut send, - mtu, - &mut noise_message.clone()[..*noise_message_len], - PACKET_TYPE_NOISE_XK_PATTERN_3, - state.cipher_states[0].as_ref().map(|k| k.remote_key_id), - 0, - Some(&session.header_send_cipher), - ); - } - } - } - retry_next - } - } - NoiseKKPattern1 { next_retry_time, timeout, noise_message, .. } | NoiseKKPattern2 { next_retry_time, timeout, noise_message, .. } => { - if let Some(ts) = process_timer(next_retry_time, Application::RETRY_INTERVAL_MS, current_time) { - ts - } else { - if *timeout <= current_time { - app.event_log(LogEvent::ServiceKKTimeout(&session), current_time); - next_retry_time.store(i64::MAX, Ordering::Relaxed); - session.expire_inner(&self.0, &mut session_queue); - } else { - let packet_type = if let NoiseKKPattern1 { .. } = &state.outgoing_offer { - app.event_log(LogEvent::ServiceKK1Resend(&session), current_time); - PACKET_TYPE_NOISE_KK_PATTERN_1 - } else { - app.event_log(LogEvent::ServiceKK2Resend(&session), current_time); - PACKET_TYPE_NOISE_KK_PATTERN_2 - }; - if let Some((send, _)) = send_to(&session) { - let _ = session.send_control(&state, send, packet_type, noise_message); - } - } - retry_next - } - } - KeyConfirm { next_retry_time, timeout, .. } => { - if let Some(ts) = process_timer(next_retry_time, Application::RETRY_INTERVAL_MS, current_time) { - ts - } else { - if *timeout <= current_time { - app.event_log(LogEvent::ServiceKeyConfirmTimeout(&session), current_time); - next_retry_time.store(i64::MAX, Ordering::Relaxed); - session.expire_inner(&self.0, &mut session_queue); - } else { - app.event_log(LogEvent::ServiceKeyConfirmResend(&session), current_time); - if let Some((send, _)) = send_to(&session) { - let _ = session.send_control(&state, send, PACKET_TYPE_KEY_CONFIRM, &[]); - } - } - retry_next - } - } - }; - session_queue.change_priority(queue_idx, Reverse(next_timer)); - } - drop(session_queue); - - self.0 - .unassociated_defrag_cache - .lock() - .unwrap() - .check_for_expiry(Application::INITIAL_OFFER_TIMEOUT_MS, current_time); - self.0.unassociated_handshake_states.service(current_time); - - next_service_time - } - /// Create a new session and send initial packet(s) to other side. /// /// This will return SendError::DataTooLarge if the combined size of the metadata and the local @@ -459,109 +131,35 @@ impl Context { /// * `send` - Function to be called to send one or more initial packets to the remote being /// contacted /// * `mtu` - MTU for initial packets - /// * `remote_static_key` - Remote side's static public NIST P-384 key - /// * `application_data` - Arbitrary data meaningful to the application to include with session + /// * `static_remote_key` - Remote side's static public NIST P-384 key + /// * `session_data` - Arbitrary data meaningful to the application to include with session /// object - /// * `ratchet_state` - The last saved and confirmed ratchet state associated with this remote - /// peer, or None if we do not have one. - /// * `local_identity_blob` - Payload to be sent to Bob that contains the information necessary + /// * `identity` - Payload to be sent to Bob that contains the information necessary /// for the upper protocol to authenticate and approve of Alice's identity. - /// * `current_time` - Current time in milliseconds. Does not have to be monotonic, nor synced - /// with the remote peer. Used to determine when this offer should be resent. - #[inline] - pub fn open( + pub fn open>( &self, - app: &Application, - mut send: impl FnMut(&mut [u8]) -> bool, + app: App, + send: impl FnMut(&mut [u8]) -> bool, mut mtu: usize, - remote_static_key: Application::PublicKey, - application_data: Application::Data, - local_identity_blob: Application::LocalIdentityBlob, - current_time: i64, - ) -> Result>, OpenError> { + static_remote_key: Crypto::PublicKey, + session_data: Crypto::SessionData, + identity: &[u8], + ) -> Result>, OpenError> { mtu = mtu.max(MIN_TRANSPORT_MTU); - if local_identity_blob.as_ref().len() > MAX_IDENTITY_BLOB_SIZE { - return Err(OpenError::DataTooLarge); - } - let result = app.restore_by_identity(&remote_static_key, &application_data, current_time); - match result { - Ok(ratchet_states) => { - let sha512 = &mut Application::Hash::new(); - - let mut noise_kk_ss = Secret::new(); - if !self.0.static_keypair.agree(&remote_static_key, noise_kk_ss.as_mut()) { - return Err(OpenError::InvalidPublicKey); - } - let noise_kk_local_init_h = mix_hash(sha512, &INITIAL_H_REKEY, self.0.static_keypair.public_key_bytes()); - let noise_kk_local_init_h = mix_hash(sha512, &noise_kk_local_init_h, remote_static_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &INITIAL_H_REKEY, remote_static_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &noise_kk_remote_init_h, self.0.static_keypair.public_key_bytes()); - - let mut session_queue = self.0.session_queue.lock().unwrap(); - let mut session_map = self.0.session_map.write().unwrap(); - let local_key_id = generate_key_id(&session_map, &mut self.0.rng.lock().unwrap()); - // Begin Noise XKhfs+psk2. - let (offer, a2b_header_key, b2a_header_key) = - NoiseXKAliceHandshake::::initialize(local_key_id, &remote_static_key, &ratchet_states, &mut self.0.rng.lock().unwrap())?; - let handshake_state = Box::new(NoiseXKAliceHandshake { - next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - timeout: current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS), - local_key_id, - alice_identity_blob: local_identity_blob, - offer, - }); - if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, message_id, .. } = &handshake_state.offer { - send_with_fragmentation( - &mut send, - mtu, - &mut noise_message.clone()[..*noise_message_len], - PACKET_TYPE_NOISE_XK_PATTERN_1, - None, - *message_id, - None::<&Application::PrpEnc>, - ); - } - - let queue_idx = session_queue.reserve_index(); - let session = Arc::new(Session { - context: Arc::downgrade(&self.0), - queue_idx, - application_data, - remote_static_key, - send_counter: AtomicU64::new(INIT_COUNTER), - session_has_expired: AtomicBool::new(false), - counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), - state_machine_lock: Mutex::new(()), - state: RwLock::new(SessionMutableState { - ratchet_states: ratchet_states.clone(), - cipher_states: [None, None], - // Points at 1 until the first key is confirmed. - current_key: 1, - outgoing_offer: OfferStateMachine::NoiseXKPattern1or3(handshake_state), - }), - header_send_cipher: Application::PrpEnc::new(a2b_header_key.as_ref()), - header_receive_cipher: Application::PrpDec::new(b2a_header_key.as_ref()), - kex_receive_cipher: Mutex::new(None), - kex_send_cipher: Mutex::new(None), - noise_kk_ss: noise_kk_ss.clone(), - noise_kk_local_init_h, - noise_kk_remote_init_h, - defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), - was_bob: false, - }); - session_map.insert(local_key_id, (Arc::downgrade(&session), false)); - session_queue.push_reserved( - queue_idx, - Arc::downgrade(&session), - Reverse(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - ); - - Ok(session) - } - Err(e) => { - Err(OpenError::RatchetIoError(e)) - } + if identity.len() > IDENTITY_MAX_SIZE { + return Err(OpenError::IdentityTooLarge); } + // Process zeta layer. + trans_to_a1( + app, + &self.0, + static_remote_key, + session_data, + identity, + |packet, hk_send| { + send_with_fragmentation(send, mtu, packet, hk_send); + }, + ) } /// Receive, authenticate, decrypt, and process a physical wire packet. @@ -573,1081 +171,418 @@ impl Context { /// The check_accept_session function is called at the end of negotiation for an incoming /// session with the caller's static public blob. It must return the P-384 static public key /// extracted from the supplied blob and application data. A return of Some() accepts the - /// session and will always result in a new session ReceiveResult being returned. + /// session and will always result in a new session ReceiveOk being returned. /// /// * `app` - Interface to application using ZSSP - /// * `check_allow_incoming_session` - Function to call to check whether an unidentified new - /// session should be accepted - /// * `check_accept_session` - Function to accept sessions after final negotiation. - /// The second argument is the identity blob that the remote peer sent us. The application - /// must verify this identity is associated with the remote peer's static key. - /// The third argument is true if the remote peer connected to us with a recognized ratchet fingerprint. /// * `send_unassociated_reply` - Function to send reply packets directly when no session exists /// * `send_unassociated_mtu` - MTU for unassociated replies /// * `send_to` - Function to get senders for existing sessions, permitting MTU and path lookup /// * `remote_address` - Whatever the remote address is, as long as you can Hash it - /// * `data_buf` - Buffer to receive decrypted and authenticated object data (an error is - /// returned if too small) - /// * `incoming_physical_packet_buf` - Buffer containing incoming wire packet - /// (receive() takes ownership) - /// * `current_time` - Current time in milliseconds. Does not have to be monotonic, nor synced - /// with the remote peer. Used to check the state of local offers we may currently have or want - /// to put in-flight. - #[inline] - pub fn receive<'a, SendFn: FnMut(&mut [u8]) -> bool>( + /// * `incoming_fragment_buf` - Buffer containing incoming wire packet (the context takes ownership) + /// * `output_buffer` - Buffer to receive decrypted and authenticated object data + pub fn receive<'a, App: ApplicationLayer, SendFn: FnMut(&mut [u8]) -> bool>( &self, - app: &Application, - check_allow_incoming_session: impl FnOnce() -> IncomingSessionAction, - check_accept_session: impl FnOnce(&Application::PublicKey, &[u8], u64) -> (Option<(bool, Application::Data)>, bool), + mut app: App, mut send_unassociated_reply: impl FnMut(&mut [u8]) -> bool, mut send_unassociated_mtu: usize, - mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, + mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, remote_address: &impl Hash, - data_buf: &'a mut [u8], - mut incoming_physical_packet_buf: Application::IncomingPacketBuffer, - current_time: i64, - ) -> Result, ReceiveError> { + mut incoming_fragment_buf: Crypto::IncomingPacketBuffer, + output_buffer: impl Write, + ) -> Result, ReceiveError> { + use crate::result::FaultType::*; + let ctx = &self.0; send_unassociated_mtu = send_unassociated_mtu.max(MIN_TRANSPORT_MTU); - let incoming_physical_packet: &mut [u8] = incoming_physical_packet_buf.as_mut(); - let incoming_physical_packet_len = incoming_physical_packet.len(); - if incoming_physical_packet_len < MIN_PACKET_SIZE { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); + let incoming_fragment: &mut [u8] = incoming_fragment_buf.as_mut(); + if incoming_fragment.len() < MIN_PACKET_SIZE { + return Err(fault!(FaultType::InvalidPacket, false)); } - // The first section parses the header and looks up relevant state information. If it's a DATA - // or NOP packet it gets handled right here, otherwise we pull out a set of variables and - // continue to the logic that handles KEX and session control packets. + let mut fragment_buffer = Assembled::new(); - let mut assembled_packet = Assembled::new(); // needs to outlive the block below - let mut incoming = None; - let (session, packet_type, fragments) = { - let mut local_key_id = [0u8; SESSION_ID_SIZE]; - local_key_id.copy_from_slice(&incoming_physical_packet[0..SESSION_ID_SIZE]); - // `from_ne_bytes` because this id was generated locally. - if let Some(local_key_id) = NonZeroU32::new(u32::from_ne_bytes(local_key_id)) { - let session_map = self.0.session_map.read().unwrap(); - if let Some((Some(session), key_index)) = session_map.get(&local_key_id).map(|r| (r.0.upgrade(), r.1 as usize)) { - drop(session_map); - session.header_receive_cipher.decrypt_in_place( - (&mut incoming_physical_packet[HEADER_PROTECT_ENC_START..HEADER_PROTECT_ENC_END]) - .try_into() - .unwrap(), + let kid_recv = incoming_fragment[0..KID_SIZE].try_into().unwrap(); + if let Some(kid_recv) = NonZeroU32::new(u32::from_ne_bytes(kid_recv)) { + let session = ctx.session_map.read().unwrap().get(&kid_recv).map(|r| r.upgrade()); + if let Some(Some(session)) = session { + let state = session.state.read().unwrap(); + let header_auth = &mut incoming_fragment[HEADER_AUTH_START..HEADER_AUTH_END]; + state.hk_recv.decrypt_in_place(header_auth.try_into().unwrap()); + + let (fragment_no, fragment_count, nonce) = parse_fragment_header(incoming_fragment)?; + let (packet_type, incoming_counter) = from_nonce(&nonce); + if packet_type != PACKET_TYPE_DATA { + log!( + app, + ReceivedRawFragment(packet_type, incoming_counter, fragment_no, fragment_count) ); - let (fragment_count, fragment_no, packet_type, incoming_counter, header_nonce) = parse_packet_header(incoming_physical_packet); - // Handle replay protection. - if PACKET_TYPE_RANGE_TRANSPORT.contains(&packet_type) { + } + + { + //vrfy + if packet_type == PACKET_TYPE_HANDSHAKE_RESPONSE { + if !matches!(&state.beta, ZetaAutomata::A1(_)) { + // A resent handshake response from Bob may have arrived out of order, + // after we already received one. + return Err(fault!(OutOfSequence, false)); + } + if incoming_counter >= COUNTER_WINDOW_MAX_SKIP_AHEAD { + return Err(fault!(ExpiredCounter, true)); + } + } else if PACKET_TYPE_USES_COUNTER_RANGE.contains(&packet_type) { // For DOS resistant reply-protection we need to check that the given counter is // in the window of valid counters immediately. // But for packets larger than 1 fragment we can't actually record the // counter as received until we've authenticated the packet. // So we check the counter window twice, and only update it the second time // after the packet has been authenticated. - if !session.check_receive_window(incoming_counter) { + if !session.window.check(incoming_counter) { // This can occur naturally if packets arrive way out of order, or // if they are duplicates. // This can also be naturally triggered if Bob has just successfully // received the first session key and is reject all of Alice's resends. // This can also occur if a session was manually expired, but not // dropped, and the remote party is still sending us data. - return Err(byzantine_fault!(FaultType::ExpiredCounter, true)); + return Err(fault!(ExpiredCounter, false)); } - if packet_type != PACKET_TYPE_DATA { - // This is a control packet. - if fragment_count != 1 || fragment_no > 0 { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - return receive_control_fragment( - self, - session, - app, - send_to, - packet_type, - incoming_counter, - incoming_physical_packet_buf.as_mut(), - current_time, - ); - } - } else if packet_type == PACKET_TYPE_NOISE_XK_PATTERN_2 { - // We need to reject fragments marked with this type if they are sent out - // of sequence, since an attacker is able to replay them. - match &session.state.read().unwrap().outgoing_offer { - OfferStateMachine::NoiseXKPattern1or3(handshake_state) => match &handshake_state.offer { - NoiseXKAliceHandshakeState::NoiseXKPattern1 { .. } => { - if incoming_counter >= COUNTER_WINDOW_MAX_SKIP_AHEAD { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - } - // This error can occur naturally if Bob's initial reply to Alice had a - // resend that was delayed massively and arrived out of order. - NoiseXKAliceHandshakeState::NoiseXKPattern3 { .. } => return Err(byzantine_fault!(FaultType::OutOfSequence, true)), - }, - _ => return Err(byzantine_fault!(FaultType::OutOfSequence, false)), - }; - } else if packet_type == PACKET_TYPE_NOISE_XK_PATTERN_3 { + } else if packet_type == PACKET_TYPE_HANDSHAKE_COMPLETION { // This can be triggered if Bob successfully received a session key and // needs to reject all of Alice's resends of PACKET_TYPE_NOISE_XK_PATTERN_3. - return Err(byzantine_fault!(FaultType::OutOfSequence, true)); + return Err(fault!(InvalidPacket, false)); } else { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); + return Err(fault!(InvalidPacket, true)); } - // Handle defragmentation. + } + + // Handle defragmentation. + let ret = if packet_type == PACKET_TYPE_DATA { let fragments = if fragment_count > 1 { let idx = incoming_counter as usize % session.defrag.len(); session.defrag[idx].lock().unwrap().assemble( - header_nonce, - incoming_physical_packet_buf, + &nonce, + incoming_fragment_buf, fragment_no, fragment_count, - &mut assembled_packet, + &mut fragment_buffer, ); - if assembled_packet.is_empty() { + if fragment_buffer.is_empty() { + return Ok(ReceiveOk::Unassociated); + } else { // We have not yet authenticated the sender so we do not report // receiving a packet from them. - return Ok(ReceiveResult::Unassociated); - } else { - assembled_packet.as_ref() + fragment_buffer.as_mut() } } else { - std::array::from_ref(&incoming_physical_packet_buf) + std::slice::from_mut(&mut incoming_fragment_buf) }; - // Handle DATA in the fastest path when we have a session. - if packet_type == PACKET_TYPE_DATA { - let state = session.state.read().unwrap(); - // The error here can occur because the other party is using a brand new - // session key that we have not received yet. - let key = state.cipher_states[key_index] - .as_ref() - .ok_or(byzantine_fault!(FaultType::OutOfSequence, true))?; - let mut c = key.get_receive_cipher(incoming_counter); - c.set_iv(&create_message_nonce(packet_type, incoming_counter)); - let mut data_len = 0; + receive_payload_in_place(&session, state, kid_recv, &nonce, fragments, output_buffer)?; - // Decrypt fragments 0..N-1 where N is the number of fragments. - for f in fragments[..(fragments.len() - 1)].iter() { - let f: &[u8] = f.as_ref(); - debug_assert!(f.len() >= HEADER_SIZE); - let current_frag_data_start = data_len; - data_len += f.len() - HEADER_SIZE; - if data_len > data_buf.len() { - return Err(ReceiveError::DataBufferTooSmall); - } - c.decrypt(&f[HEADER_SIZE..], &mut data_buf[current_frag_data_start..data_len]); - } - - // Decrypt final fragment (or only fragment if not fragmented) - let current_frag_data_start = data_len; - let last_fragment = fragments.last().unwrap().as_ref(); - if last_fragment.len() < (HEADER_SIZE + AES_GCM_TAG_SIZE) { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - data_len += last_fragment.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE); - if data_len > data_buf.len() { - return Err(ReceiveError::DataBufferTooSmall); - } - let payload_end = last_fragment.len() - AES_GCM_TAG_SIZE; - c.decrypt(&last_fragment[HEADER_SIZE..payload_end], &mut data_buf[current_frag_data_start..data_len]); - - let aead_authentication_ok = c.finish_decrypt(&last_fragment[payload_end..].try_into().unwrap()); - drop(c); - drop(state); - - if !aead_authentication_ok { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - if !session.update_receive_window(incoming_counter) { - // This can be naturally triggered because Bob has just - // successfully received a session key and needs to reject - // all of Alice's resends. - // This can also occur naturally if some part of the outer - // system is duplicating the packets being sent to us. - // We are safely deduplicating them here. - return Err(byzantine_fault!(FaultType::ExpiredCounter, true)); - } - // Packet fully authenticated - return Ok(ReceiveResult::Session(session, SessionEvent::Data(&mut data_buf[..data_len]))); - } else if packet_type == PACKET_TYPE_NOISE_XK_PATTERN_2 { - (Some(session), packet_type, fragments) - } else { - unreachable!() - } + SessionEvent::Data } else { - drop(session_map); - // Check for and handle PACKET_TYPE_ALICE_NOISE_XK_PATTERN_3 - incoming = self.0.unassociated_handshake_states.get(local_key_id); - if let Some(incoming) = incoming.as_ref() { - Application::PrpDec::new(incoming.header_receive_key.as_ref()).decrypt_in_place( - (&mut incoming_physical_packet[HEADER_PROTECT_ENC_START..HEADER_PROTECT_ENC_END]) - .try_into() - .unwrap(), + drop(state); + let mut buffer = ArrayVec::::new(); + let assembled_packet = if fragment_count > 1 { + let idx = incoming_counter as usize % session.defrag.len(); + session.defrag[idx].lock().unwrap().assemble( + &nonce, + incoming_fragment_buf, + fragment_no, + fragment_count, + &mut fragment_buffer, ); - let (fragment_count, fragment_no, packet_type, _, header_nonce) = parse_packet_header(incoming_physical_packet); - app.event_log( - LogEvent::ReceiveUnassociatedFragment(fragment_count, fragment_no, packet_type), - current_time, - ); - if packet_type != PACKET_TYPE_NOISE_XK_PATTERN_3 { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - let fragments = if fragment_count > 1 { - incoming.noise_pattern3_defrag.lock().unwrap().assemble( - header_nonce, - incoming_physical_packet_buf, - fragment_no, - fragment_count, - &mut assembled_packet, - ); - if !assembled_packet.is_empty() { - assembled_packet.as_ref() - } else { - return Ok(ReceiveResult::Unassociated); + if fragment_buffer.is_empty() { + return Ok(ReceiveOk::Unassociated); + } else { + for fragment in fragment_buffer.as_ref() { + buffer + .try_extend_from_slice(&fragment.as_ref()[HEADER_SIZE..]) + .map_err(|_| fault!(InvalidPacket, true))?; } - } else { - std::array::from_ref(&incoming_physical_packet_buf) - }; - // We must guarantee that this incoming handshake is processed once and only - // once. This prevents catastrophic nonce reuse caused by multithreading. - if self.0.unassociated_handshake_states.remove(local_key_id) { - (None, PACKET_TYPE_NOISE_XK_PATTERN_3, fragments) - } else { - return Ok(ReceiveResult::Unassociated); + // We have not yet authenticated the sender so we do not report + // receiving a packet from them. + buffer.as_mut() } } else { - // This can occur naturally because either Bob's incoming_sessions cache got - // full so Alice's incoming session was dropped, or the session this packet - // was for was dropped by the application. - return Err(byzantine_fault!(FaultType::UnknownLocalKeyId, true)); + &mut incoming_fragment_buf.as_mut()[HEADER_SIZE..] + }; + + let send_associated = |packet: &mut [u8], hk_send: Option<&Crypto::PrpEnc>| { + if let Some((send_fragment, mut mtu)) = send_to(&session) { + mtu = mtu.max(MIN_TRANSPORT_MTU); + send_with_fragmentation(send_fragment, mtu, packet, hk_send); + } + }; + match packet_type { + PACKET_TYPE_HANDSHAKE_RESPONSE => { + log!(app, ReceivedRawX2); + let should_warn_missing_ratchet = received_x2_trans( + &mut app, + ctx, + &session, + kid_recv, + &nonce, + assembled_packet, + send_associated, + )?; + log!(app, X2IsAuthSentX3(&session)); + if should_warn_missing_ratchet { + SessionEvent::DowngradedRatchetKey + } else { + SessionEvent::Control + } + } + PACKET_TYPE_KEY_CONFIRM => { + log!(app, ReceivedRawKeyConfirm); + let just_established = received_c1_trans( + &mut app, + ctx, + &session, + kid_recv, + &nonce, + assembled_packet, + send_associated, + )?; + log!(app, KeyConfirmIsAuthSentAck(&session)); + if just_established { + SessionEvent::Established + } else { + SessionEvent::Control + } + } + PACKET_TYPE_ACK => { + log!(app, ReceivedRawAck); + received_c2_trans(&mut app, ctx, &session, kid_recv, &nonce, assembled_packet)?; + log!(app, AckIsAuth(&session)); + SessionEvent::Control + } + PACKET_TYPE_REKEY_INIT => { + log!(app, ReceivedRawK1); + received_k1_trans( + &mut app, + ctx, + &session, + kid_recv, + &nonce, + assembled_packet, + send_associated, + )?; + log!(app, K1IsAuthSentK2(&session)); + SessionEvent::Control + } + PACKET_TYPE_REKEY_COMPLETE => { + log!(app, ReceivedRawK2); + received_k2_trans( + &mut app, + ctx, + &session, + kid_recv, + &nonce, + assembled_packet, + send_associated, + )?; + log!(app, K2IsAuthSentKeyConfirm(&session)); + SessionEvent::Control + } + PACKET_TYPE_SESSION_REJECTED => { + log!(app, ReceivedRawD); + received_d_trans(&session, kid_recv, &nonce, assembled_packet)?; + log!(app, DIsAuthClosedSession(&session)); + SessionEvent::Rejected + } + _ => return Err(fault!(InvalidPacket, true)), // This is unreachable. } + }; + Ok(ReceiveOk::Session(session, ret)) + } else { + // Check for and handle PACKET_TYPE_ALICE_NOISE_XK_PATTERN_3 + let zeta = self.0.unassociated_handshake_states.get(kid_recv); + if let Some(zeta) = zeta { + Crypto::PrpDec::new(&zeta.hk_recv).decrypt_in_place( + (&mut incoming_fragment[HEADER_AUTH_START..HEADER_AUTH_END]) + .try_into() + .unwrap(), + ); + + let (fragment_no, fragment_count, nonce) = parse_fragment_header(incoming_fragment)?; + let (packet_type, incoming_counter) = from_nonce(&nonce); + log!( + app, + ReceivedRawFragment(packet_type, incoming_counter, fragment_no, fragment_count) + ); + + { + //vrfy + if packet_type != PACKET_TYPE_HANDSHAKE_COMPLETION || incoming_counter != 0 { + return Err(fault!(InvalidPacket, true)); + } + } + + let mut buffer = ArrayVec::::new(); + let assembled_packet = if fragment_count > 1 { + zeta.defrag.lock().unwrap().assemble( + &nonce, + incoming_fragment_buf, + fragment_no, + fragment_count, + &mut fragment_buffer, + ); + if fragment_buffer.is_empty() { + return Ok(ReceiveOk::Unassociated); + } else { + for fragment in fragment_buffer.as_ref() { + buffer + .try_extend_from_slice(&fragment.as_ref()[HEADER_SIZE..]) + .map_err(|_| fault!(InvalidPacket, true))?; + } + buffer.as_mut() + } + } else { + &mut incoming_fragment_buf.as_mut()[HEADER_SIZE..] + }; + // We must guarantee that this incoming handshake is processed once and only + // once. This prevents catastrophic nonce reuse caused by multithreading. + if !self.0.unassociated_handshake_states.remove(kid_recv) { + return Ok(ReceiveOk::Unassociated); + } + + log!(app, ReceivedRawX3); + let (session, should_warn_missing_ratchet) = + received_x3_trans(&mut app, ctx, zeta, kid_recv, assembled_packet, |packet, hk_send| { + send_with_fragmentation(send_unassociated_reply, send_unassociated_mtu, packet, hk_send); + })?; + log!(app, X3IsAuthSentKeyConfirm(&session)); + Ok(ReceiveOk::Session( + session, + if should_warn_missing_ratchet { + SessionEvent::NewDowngradedSession + } else { + SessionEvent::NewSession + }, + )) + } else { + // This can occur naturally because either Bob's incoming_sessions cache got + // full so Alice's incoming session was dropped, or the session this packet + // was for was dropped by the application. + return Err(fault!(UnknownLocalKeyId, false)); + } + } + } else { + let (fragment_no, fragment_count, nonce) = parse_fragment_header(incoming_fragment)?; + let (packet_type, _c) = from_nonce(&nonce); + log!(app, ReceivedRawFragment(packet_type, _c, fragment_no, fragment_count)); + + { + //vrfy + if packet_type != PACKET_TYPE_HANDSHAKE_HELLO && packet_type != PACKET_TYPE_CHALLENGE { + return Err(fault!(InvalidPacket, true)); + } + } + + let mut buffer = ArrayVec::::new(); + let assembled_packet = if fragment_count > 1 { + self.0.unassociated_defrag_cache.lock().unwrap().assemble( + &nonce, + remote_address, + incoming_fragment.len() - HEADER_SIZE, + incoming_fragment_buf, + fragment_no, + fragment_count, + Crypto::SETTINGS.resend_time as i64, + app.time(), + &mut fragment_buffer, + ); + if fragment_buffer.is_empty() { + return Ok(ReceiveOk::Unassociated); + } else { + for fragment in fragment_buffer.as_ref() { + buffer + .try_extend_from_slice(&fragment.as_ref()[HEADER_SIZE..]) + .map_err(|_| fault!(InvalidPacket, true))?; + } + buffer.as_mut() } } else { - let (fragment_count, fragment_no, packet_type, _, header_nonce) = parse_packet_header(incoming_physical_packet); - app.event_log( - LogEvent::ReceiveUnassociatedFragment(fragment_count, fragment_no, packet_type), - current_time, - ); - if packet_type != PACKET_TYPE_NOISE_XK_PATTERN_1 && packet_type != PACKET_TYPE_BOB_DOS_CHALLENGE { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - let fragments = if fragment_count > 1 { - self.0.unassociated_defrag_cache.lock().unwrap().assemble( - header_nonce, - remote_address, - incoming_physical_packet_len - HEADER_SIZE, - incoming_physical_packet_buf, - fragment_no, - fragment_count, - Application::RETRY_INTERVAL_MS, - current_time, - &mut assembled_packet, - ); - if !assembled_packet.is_empty() { - assembled_packet.as_ref() - } else { - return Ok(ReceiveResult::Unassociated); - } - } else { - std::array::from_ref(&incoming_physical_packet_buf) - }; - (None, packet_type, fragments) - } - }; + &mut incoming_fragment_buf.as_mut()[HEADER_SIZE..] + }; - debug_assert!(!fragments.is_empty()); - debug_assert!(incoming.is_none() || session.is_none()); + if packet_type == PACKET_TYPE_HANDSHAKE_HELLO { + log!(app, ReceivedRawX1); - let message = &mut [0u8; MAX_NOISE_HANDSHAKE_SIZE]; - let message_size = assemble_fragments_into::(fragments, message)?; - if message_size < MIN_PACKET_SIZE { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - - use OfferStateMachine::*; - match packet_type { - PACKET_TYPE_NOISE_XK_PATTERN_1 => { - // Alice (remote) --> Bob (local) - // -> e, es, e1 - app.event_log(LogEvent::ReceiveUncheckedXK1, current_time); - - if session.is_some() || incoming.is_some() { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - if !(NoiseXKPattern1::MIN_SIZE..=NoiseXKPattern1::MAX_SIZE).contains(&message_size) { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - // The message id must be the first 8 bytes of the gcm tag. - // This forces the message id to be authenticated along with the entire message. - let p_auth_end = message_size - ChallengeResponse::SIZE; - if message[8..16] != message[p_auth_end - 8..p_auth_end] { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - let p_size = p_auth_end - NoiseXKPattern1::P_ENC_START - AES_GCM_TAG_SIZE; - let total_ratchet_fingerprints = p_size / RATCHET_SIZE; - if p_size % RATCHET_SIZE != 0 || total_ratchet_fingerprints > 2 { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - - let noise_pattern1: &NoiseXKPattern1 = byte_array_as_proto_buffer(message); - if let Some(remote_key_id) = NonZeroU32::new(u32::from_ne_bytes(noise_pattern1.alice_key_id)) { - let sha512 = &mut Application::Hash::new(); - // Let application filter incoming connection attempts by whatever criteria it wants. - // This should ideally prevent ZSSP from wasting time on DDOS attacks. - match check_allow_incoming_session() { - IncomingSessionAction::Allow => {} - IncomingSessionAction::Challenge => { - let response: &ChallengeResponse = byte_array_as_proto_buffer(&message[p_auth_end..message_size]); - let mut counter = 0u64.to_ne_bytes(); - counter.copy_from_slice(&response.challenge_counter); - let counter = u64::from_be_bytes(counter); - - sha512.reset(); - let mut hasher = ShaHasher(sha512); - let mut output = [0u8; NOISE_HASHLEN]; - hasher.0.update(&response.challenge_counter); - remote_address.hash(&mut hasher); - hasher.0.update(&self.0.challenge_salt); - hasher.0.finish(&mut output); - let is_valid = self.check_challenge_window(counter) - && secure_eq(&output[..CHALLENGE_MAC_SIZE], &response.challenge_mac) - && verify_pow::(hasher.0, &message[p_auth_end..message_size]) - && self.update_challenge_window(counter); - app.event_log(LogEvent::ReceiveCheckXK1Challenge(is_valid), current_time); - if !is_valid { - // Alice failed the challenge so issue them a new challenge. - let mut challenge_buffer = [0u8; BobDOSChallenge::SIZE]; - let challenge: &mut BobDOSChallenge = byte_array_as_proto_buffer_mut(&mut challenge_buffer); - challenge.alice_key_id = remote_key_id.get().to_ne_bytes(); - // We attach a monotonically increasing counter value to the challenge - // so it cannot be replayed. - let counter = self.0.challenge_counter.fetch_add(1, Ordering::Relaxed); - challenge.challenge_counter = counter.to_be_bytes(); - - hasher.0.reset(); - hasher.0.update(&counter.to_be_bytes()); - remote_address.hash(&mut hasher); - hasher.0.update(&self.0.challenge_salt); - hasher.0.finish(&mut output); - challenge.challenge_mac.copy_from_slice(&output[..CHALLENGE_MAC_SIZE]); - challenge.prior_challenge_pow = response.challenge_pow; - // We haven't decrypted any of Alice's packet so we don't know the - // header protection cipher. - // For DOS resistance Alice will not accept unencrypted headers directly - // into their session defrag buffer, so we have to send them this reply - // through their incoming sessions cache. - send_with_fragmentation( - &mut send_unassociated_reply, - send_unassociated_mtu, - &mut challenge_buffer, - PACKET_TYPE_BOB_DOS_CHALLENGE, - None, - self.0.rng.lock().unwrap().next_u64(), - None::<&Application::PrpEnc>, - ); - return Ok(ReceiveResult::Unassociated); - } - // Alice succeeded at the challenge so continue to decryption. - } - IncomingSessionAction::Drop => return Ok(ReceiveResult::Rejected), - } - - // Noise process handshake prologue. - let noise_h = mix_hash( - sha512, - &INITIAL_H, - &message[NoiseXKPattern1::PROLOGUE_START..NoiseXKPattern1::PROLOGUE_END], - ); - let noise_h = mix_hash(sha512, &noise_h, self.0.static_keypair.public_key_bytes()); - // Noise process pattern1 e token. - let mut noise_ck = SymmetricState::new(INITIAL_H); - let hmac = &mut Application::HmacHash::new(); - let mut noise_es = Secret::new(); - let noise_e_pattern1 = from_bytes_agreement::(&noise_pattern1.noise_e, &self.0.static_keypair, noise_es.as_mut()) - .ok_or(byzantine_fault!(FaultType::FailedAuthentication, false))?; - let noise_h_e = mix_hash(sha512, &noise_h, &noise_pattern1.noise_e); - noise_ck.mix_key(hmac, &noise_pattern1.noise_e); - // Noise process pattern1 es token. - let noise_k_es = noise_ck.mix_key_initialize_key(hmac, noise_es.as_ref()); - drop(noise_es); - // Noise process pattern1 e1 token. - let (is_auth, noise_h_ee1) = decrypt_and_hash::( - sha512, - &noise_k_es, - &noise_h_e, - packet_type, - 0, - &mut message[NoiseXKPattern1::E1_ENC_START..NoiseXKPattern1::P_ENC_START], - ); - if !is_auth { - // This could occur naturally if Alice's ApplicationLayer is dynamically - // changing their mtu, which in bad network conditions could clobber their - // resent KEX packet. - // Or maybe Alice randomly generated the same temporary id twice in a row. - // Since these situations are super unlikely to occur we still mark this error - // as unnatural. - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - // Noise process pattern1 payload. - let (is_auth, noise_h_ee1p) = decrypt_and_hash::( - sha512, - &noise_k_es, - &noise_h_ee1, - packet_type, - 1, - &mut message[NoiseXKPattern1::P_ENC_START..p_auth_end], - ); - drop(noise_k_es); - if !is_auth { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - let (header_b2a_key, header_a2b_key) = noise_ck.get_ask2(hmac, LABEL_HEADER_KEY, &noise_h_ee1p); - // Get ratchet key. - let noise_pattern1: &NoiseXKPattern1 = byte_array_as_proto_buffer(message); - let mut ratchet_state = RatchetState::Null; - for i in 0..total_ratchet_fingerprints { - match app.restore_by_fingerprint( - (&noise_pattern1.payload[i * RATCHET_SIZE..(i + 1) * RATCHET_SIZE]).try_into().unwrap(), - current_time, - ) { - Ok(RatchetState::Null) | Ok(RatchetState::Empty) => {} - Ok(rs) => { - ratchet_state = rs; - break; - } - Err(e) => return Err(ReceiveError::RatchetIoError(e)), - } - } - if ratchet_state.is_null() { - if app.hello_requires_recognized_ratchet(current_time) { - return Ok(ReceiveResult::Rejected); - } - ratchet_state = RatchetState::Empty; - } - - // Start of Noise XKhfs+psk2 pattern2. - let mut message2 = [0u8; NoiseXKPattern2::SIZE]; - let noise_pattern2: &mut NoiseXKPattern2 = byte_array_as_proto_buffer_mut(&mut message2); - // Noise process pattern2 e token. - let noise_e_pattern2_secret = Application::KeyPair::generate(&mut self.0.rng.lock().unwrap()); - noise_pattern2.noise_e = *noise_e_pattern2_secret.public_key_bytes(); - let noise_h_ee1pe = mix_hash(sha512, &noise_h_ee1p, &noise_pattern2.noise_e); - noise_ck.mix_key(hmac, &noise_pattern2.noise_e); - // Noise process pattern2 ee token. - let mut noise_ee = Secret::new(); - if !noise_e_pattern2_secret.agree(&noise_e_pattern1, noise_ee.as_mut()) { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - let noise_k_esee = noise_ck.mix_key_initialize_key(hmac, noise_ee.as_ref()); - drop(noise_ee); - // Noise process pattern2 ekem1 token. - let (noise_ekem1, noise_ekem1_secret) = pqc_kyber::encapsulate(&noise_pattern1.noise_e1, self.0.rng.lock().unwrap().deref_mut()) - .map_err(|_| byzantine_fault!(FaultType::FailedAuthentication, false)) - .map(|(ct, ekem1)| (ct, Secret(ekem1)))?; - // Alice fully authenticated. - noise_pattern2.noise_ekem1 = noise_ekem1; - let noise_h_ee1peekem1 = encrypt_and_hash::( - sha512, - &noise_k_esee, - &noise_h_ee1pe, - PACKET_TYPE_NOISE_XK_PATTERN_2, - 0, - &mut message2[NoiseXKPattern2::EKEM1_ENC_START..NoiseXKPattern2::P_ENC_START], - ); - drop(noise_k_esee); - noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); - drop(noise_ekem1_secret); - // Noise process pattern2 psk token. - let ratchet_key = ratchet_state.key().unwrap(); - let (temp_h, noise_k_eseeekem1psk) = noise_ck.mix_key_and_hash_initialize_key(hmac, ratchet_key); - let noise_h_ee1peekem1psk = mix_hash(sha512, &noise_h_ee1peekem1, &temp_h); - // Noise process pattern2 payload. - // We try to prevent the id we generate from colliding with another session but - // because we might have handshakes in flight it's impossible to 100% prevent. - // In those exceedingly rare cases we have to drop Alice's session and start over. - let local_key_id = generate_key_id(&self.0.session_map.read().unwrap(), &mut self.0.rng.lock().unwrap()); - let noise_pattern2: &mut NoiseXKPattern2 = byte_array_as_proto_buffer_mut(&mut message2); - noise_pattern2.bob_key_id = local_key_id.get().to_ne_bytes(); - - let noise_h_ee1peekem1pskp = encrypt_and_hash::( - sha512, - &noise_k_eseeekem1psk, - &noise_h_ee1peekem1psk, - PACKET_TYPE_NOISE_XK_PATTERN_2, - 0, - &mut message2[NoiseXKPattern2::P_ENC_START..NoiseXKPattern2::P_AUTH_END], - ); - - app.event_log(LogEvent::ReceiveValidXK1, current_time); - let handshake = Arc::new(NoiseXKBobHandshakeState { - local_key_id, - remote_key_id, - ratchet_state, - noise_h_ee1peekem1pskp, - noise_ck_eseeekem1psk: noise_ck.clone(), - noise_k_eseeekem1psk: noise_k_eseeekem1psk.clone(), - noise_e_secret: noise_e_pattern2_secret, - header_receive_key: header_a2b_key.clone(), - header_send_key: header_b2a_key.clone(), - noise_pattern3_defrag: Mutex::new(Fragged::new()), - }); - self.0.unassociated_handshake_states.insert(local_key_id, handshake, current_time); - - // We put a copy of the gcm tag in the header so Alice can tell this packet apart - // from any other pattern 1 packet we send, without having to make Bob maintain state. - let mut pattern2_id = 0u64.to_ne_bytes(); - pattern2_id[5] = message2[NoiseXKPattern2::P_AUTH_END - 3]; - pattern2_id[6] = message2[NoiseXKPattern2::P_AUTH_END - 2]; - pattern2_id[7] = message2[NoiseXKPattern2::P_AUTH_END - 1]; - send_with_fragmentation( - &mut send_unassociated_reply, - send_unassociated_mtu, - &mut message2, - PACKET_TYPE_NOISE_XK_PATTERN_2, - Some(remote_key_id), - u64::from_be_bytes(pattern2_id), - Some(&Application::PrpEnc::new(header_b2a_key.first_n::())), - ); - - return Ok(ReceiveResult::Unassociated); - } else { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - } - PACKET_TYPE_BOB_DOS_CHALLENGE => { - let message = &mut message[..message_size]; - app.event_log(LogEvent::ReceiveUncheckedDOSChallenge, current_time); - - // We expect Bob to only send this to us through our unassociated defrag cache. - if incoming.is_some() || session.is_some() { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - if message.len() != BobDOSChallenge::SIZE { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - let challenge: &BobDOSChallenge = byte_array_as_proto_buffer(message); - - if let Some(local_key_id) = NonZeroU32::new(u32::from_ne_bytes(challenge.alice_key_id)) { - if let Some(session) = self.0.session_map.read().unwrap().get(&local_key_id).and_then(|s| s.0.upgrade()) { - // We don't need to hold the kex lock because we are not transitioning state. - let mut state = session.state.write().unwrap(); - if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { - if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { noise_message, noise_message_len, .. } = &mut handshake_state.offer { - let response_raw = &mut noise_message[*noise_message_len - ChallengeResponse::SIZE..]; - - let response: &mut ChallengeResponse = byte_array_as_proto_buffer_mut(response_raw); - // Only people who know what Alice's prior pow was can convince us to - // compute a new pow. - if challenge.prior_challenge_pow != response.challenge_pow { - // This can occur if Bob sends us multiple challenges and they - // arrive OOO. - return Err(byzantine_fault!(FaultType::FailedAuthentication, true)); - } - response.challenge_counter.copy_from_slice(&challenge.challenge_counter); - response.challenge_mac.copy_from_slice(&challenge.challenge_mac); - let mut pow = self.0.rng.lock().unwrap().next_u64(); - let sha512 = &mut Application::Hash::new(); - loop { - let response: &mut ChallengeResponse = byte_array_as_proto_buffer_mut(response_raw); - response.challenge_pow.copy_from_slice(&pow.to_be_bytes()); - if verify_pow::(sha512, response_raw) { - break; - } - pow = pow.wrapping_add(1); - } - - app.event_log(LogEvent::ReceiveValidDOSChallenge(&session), current_time); - return Ok(ReceiveResult::Unassociated); - } else { - // This could happen if Bob challenges Alice, but their challenge packet - // gets massively delayed. - return Err(byzantine_fault!(FaultType::OutOfSequence, true)); - } - } else { - // This could happen if Bob challenges Alice, but their challenge packet - // gets massively delayed. - return Err(byzantine_fault!(FaultType::OutOfSequence, true)); - } - } else { - // This can occur naturally if Alice's session was dropped. - return Err(byzantine_fault!(FaultType::UnknownLocalKeyId, true)); - } - } else { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - } - PACKET_TYPE_NOISE_XK_PATTERN_2 => { - // Bob (remote) --> Alice (local) - // <- e, ee, ekem1, psk - let message = &mut message[..message_size]; - app.event_log(LogEvent::ReceiveUncheckedXK2, current_time); - - if incoming.is_some() { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - if message.len() != NoiseXKPattern2::SIZE { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - - let session = session.ok_or(byzantine_fault!(FaultType::UnknownLocalKeyId, false))?; - let kex_lock = session.state_machine_lock.lock().unwrap(); - let state = session.state.read().unwrap(); - - if let NoiseXKPattern1or3(handshake_state) = &state.outgoing_offer { - if let NoiseXKAliceHandshakeState::NoiseXKPattern1 { - noise_h_ee1p, noise_e_secret, noise_e1_secret, noise_ck_es, .. - } = &handshake_state.offer - { - let noise_pattern2: &NoiseXKPattern2 = byte_array_as_proto_buffer(message); - // Authenticate header counter. - if noise_pattern2.header[13..16] != noise_pattern2.p_gcm_tag[13..16] { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - - // Noise process pattern2 e token. - let mut noise_ee = Secret::new(); - if let Some(noise_e_pattern2) = - from_bytes_agreement::(&noise_pattern2.noise_e, noise_e_secret, noise_ee.as_mut()) - { - let sha512 = &mut Application::Hash::new(); - let hmac = &mut Application::HmacHash::new(); - let mut noise_ck = noise_ck_es.clone(); - let noise_h_ee1pe = mix_hash(sha512, noise_h_ee1p, noise_e_pattern2.as_bytes()); - noise_ck.mix_key(hmac, noise_e_pattern2.as_bytes()); - // Noise process pattern2 ee token. - let noise_k_esee = noise_ck.mix_key_initialize_key(hmac, noise_ee.as_ref()); - drop(noise_ee); - // Noise process pattern2 ekem1 token. - let (is_auth, noise_h_ee1peekem1) = decrypt_and_hash::( - sha512, - &noise_k_esee, - &noise_h_ee1pe, - packet_type, - 0, - &mut message[NoiseXKPattern2::EKEM1_ENC_START..NoiseXKPattern2::P_ENC_START], - ); - let noise_pattern2: &NoiseXKPattern2 = byte_array_as_proto_buffer(message); - let noise_ekem1_secret = pqc_kyber::decapsulate(&noise_pattern2.noise_ekem1, noise_e1_secret.as_ref()).map(Secret); - if let Some(Ok(noise_ekem1_secret)) = is_auth.then_some(noise_ekem1_secret) { - noise_ck.mix_key(hmac, noise_ekem1_secret.as_ref()); - drop(noise_ekem1_secret); - - // We attempt to decrypt the payload at most three times. First two times with - // the ratchet key Alice remembers, and final time with a ratchet - // key of zero if Alice allows ratchet downgrades. - // The following code is not constant time, meaning we leak to an - // attacker whether or not we downgraded. - // We don't currently consider this sensitive enough information to hide. - let mut test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState, Secret, [u8; 64])> { - // Check for which ratchet key Bob wants to use. - let mut noise_ck = noise_ck.clone(); - let mut payload = [0u8; NoiseXKPattern2::P_AUTH_END - NoiseXKPattern2::P_ENC_START]; - payload.copy_from_slice(&message[NoiseXKPattern2::P_ENC_START..NoiseXKPattern2::P_AUTH_END]); - // Noise process pattern2 psk token. - let (temp_h, noise_k_eseeekem1psk) = noise_ck.mix_key_and_hash_initialize_key(hmac, ratchet_key); - let noise_h_ee1peekem1psk = mix_hash(sha512, &noise_h_ee1peekem1, &temp_h); - // Noise process pattern2 payload. - let (is_auth, noise_h_ee1peekem1pskp) = decrypt_and_hash::( - sha512, - &noise_k_eseeekem1psk, - &noise_h_ee1peekem1psk, - packet_type, - 0, - &mut payload, - ); - if is_auth { - let key_id = NonZeroU32::new(u32::from_ne_bytes( - (&payload[..NoiseXKPattern2::P_AUTH_START - NoiseXKPattern2::P_ENC_START]) - .try_into() - .unwrap(), - )); - key_id.map(|kid| (kid, noise_ck, noise_k_eseeekem1psk, noise_h_ee1peekem1pskp)) - } else { - None - } - }; - // Check first key. - let mut ratchet_i = 0; - let mut result = None; - let mut chain_len = 0; - if let Some(key) = state.ratchet_states[0].key() { - chain_len = state.ratchet_states[0].chain_len(); - result = test_ratchet_key(key); - } - // Check second key. - if result.is_none() { - ratchet_i = 1; - if let Some(key) = state.ratchet_states[1].key() { - chain_len = state.ratchet_states[1].chain_len(); - result = test_ratchet_key(key); - } - } - // Check zero key. - if result.is_none() && !app.initiator_disallows_downgrade(&session, current_time) { - chain_len = 0; - result = test_ratchet_key(&[0u8; RATCHET_SIZE]); - if result.is_some() { - // TODO: add some kind of warning callback or signal. - } - } - - if let Some((remote_key_id, mut noise_ck, noise_k_eseeekem1psk, noise_h_ee1peekem1pskp)) = result { - // Start of Noise XKhfs+psk2 pattern3. - let mut message3 = [0u8; NoiseXKPattern3::MAX_SIZE]; - // Noise process pattern3 s token. - let mut noise_se = Secret::new(); - if self.0.static_keypair.agree(&noise_e_pattern2, noise_se.as_mut()) { - let payload = handshake_state.alice_identity_blob.as_ref(); - // Packet fully authenticated. - let s_enc_start = HEADER_SIZE; - let s_auth_start = s_enc_start + P384_PUBLIC_KEY_SIZE; - let p_enc_start = s_auth_start + AES_GCM_TAG_SIZE; - let p_auth_start = p_enc_start + payload.len(); - let p_auth_end = p_auth_start + AES_GCM_TAG_SIZE; - let message3_len = p_auth_end; - - message3[s_enc_start..s_auth_start].copy_from_slice(self.0.static_keypair.public_key_bytes()); - let noise_h_ee1peekem1pskps = encrypt_and_hash::( - sha512, - &noise_k_eseeekem1psk, - &noise_h_ee1peekem1pskp, - PACKET_TYPE_NOISE_XK_PATTERN_3, - 1, - &mut message3[s_enc_start..p_enc_start], - ); - drop(noise_k_eseeekem1psk); - // Noise process pattern3 se token. - let noise_k_eseeekem1pskse = noise_ck.mix_key_initialize_key(hmac, noise_se.as_ref()); - drop(noise_se); - // Noise process pattern3 payload token. - message3[p_enc_start..p_auth_start].copy_from_slice(payload); - let noise_h_ee1peekem1pskpsp = encrypt_and_hash::( - sha512, - &noise_k_eseeekem1pskse, - &noise_h_ee1peekem1pskps, - PACKET_TYPE_NOISE_XK_PATTERN_3, - 0, - &mut message3[p_enc_start..p_auth_end], - ); - drop(noise_k_eseeekem1pskse); - // Alice finished Noise XKhfs+psk2 handshake. - // Transition offer state machine to the NoiseXKPattern3 state. - let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(chain_len + 1).unwrap()); - - let ratchet_to_preserve = &state.ratchet_states[ratchet_i]; - let result = app.save_ratchet_state( - &session.remote_static_key, - &session.application_data, - [&state.ratchet_states[0], &state.ratchet_states[1]], - [&new_ratchet_state, ratchet_to_preserve], - current_time, - ); - if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); - } - - let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_ee1peekem1pskpsp); - - let local_key_id = handshake_state.local_key_id; - drop(state); - let mut state = session.state.write().unwrap(); - session - .kex_send_cipher - .lock() - .unwrap() - .replace(Application::AeadEnc::new(kex_key_a2b.as_ref())); - session - .kex_receive_cipher - .lock() - .unwrap() - .replace(Application::AeadDec::new(kex_key_b2a.as_ref())); - state.ratchet_states[1] = state.ratchet_states[ratchet_i].clone(); - state.ratchet_states[0] = new_ratchet_state; - - state.cipher_states[0].replace(SessionKey::new( - hmac, - noise_ck, - local_key_id, - remote_key_id, - INIT_COUNTER, - false, - )); - debug_assert!(state.cipher_states[1].is_none()); - if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { - handshake_state.next_retry_time = - AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)); - handshake_state.timeout = current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS); - handshake_state.offer = NoiseXKAliceHandshakeState::NoiseXKPattern3 { - noise_message: message3, - noise_message_len: p_auth_end, - }; - } - drop(state); - drop(kex_lock); - - if let Some((mut send, mut mtu)) = send_to(&session) { - mtu = mtu.max(MIN_TRANSPORT_MTU); - send_with_fragmentation( - &mut send, - mtu, - &mut message3[..message3_len], - PACKET_TYPE_NOISE_XK_PATTERN_3, - Some(remote_key_id), - 0, - Some(&session.header_send_cipher), - ); - } - app.event_log(LogEvent::ReceiveValidXK2(&session), current_time); - return Ok(ReceiveResult::Session(session, SessionEvent::Control)); - } - } - } - } - // Bob failed authentication so we must restart our offer according to Noise. - // We restart the offer instead of dropping the session to defend against DOS. - drop(state); - let mut state = session.state.write().unwrap(); - let ratchet_state = state.ratchet_states.clone(); - if let NoiseXKPattern1or3(handshake_state) = &mut state.outgoing_offer { - if !handshake_state.reinitialize( - &session, - &ratchet_state, - &mut self.0.session_map.write().unwrap(), - &mut self.0.rng.lock().unwrap(), - current_time, - ) { - session.expire() - } - } - drop(state); - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } else { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - } else { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - } - PACKET_TYPE_NOISE_XK_PATTERN_3 => { - // Alice (remote) --> Bob (local) - // -> s, se - let message = &mut message[..message_size]; - app.event_log(LogEvent::ReceiveUncheckedXK3, current_time); - - if session.is_some() { - return Err(byzantine_fault!(FaultType::OutOfSequence, false)); - } - if message.len() < NoiseXKPattern3::MIN_SIZE || message.len() > NoiseXKPattern3::MAX_SIZE { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - // The code above guarantees to us that each `incoming` handshake state that reaches - // this point will be strictly unique, even for the same remote peer. - // This property is strictly necessary to prevent catastrophic nonce reuse due to - // two session being created with the same set of keys. - let handshake_state = incoming.ok_or(byzantine_fault!(FaultType::UnknownLocalKeyId, false))?; - let s_enc_start = HEADER_SIZE; - - let s_auth_start = s_enc_start + P384_PUBLIC_KEY_SIZE; - let p_enc_start = s_auth_start + AES_GCM_TAG_SIZE; - let p_auth_end = message.len(); - let p_auth_start = p_auth_end - AES_GCM_TAG_SIZE; - - if !(p_enc_start <= p_auth_start) { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - // Do not read from the message before this point, otherwise an array out of bounds - // error is possible. - // Noise process pattern3 s token. - let sha512 = &mut Application::Hash::new(); - let hmac = &mut Application::HmacHash::new(); - let (is_auth, noise_h_ee1peekem1pskps) = decrypt_and_hash::( - sha512, - &handshake_state.noise_k_eseeekem1psk, - &handshake_state.noise_h_ee1peekem1pskp, - packet_type, - 1, - &mut message[s_enc_start..p_enc_start], - ); - if !is_auth { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - // Noise process pattern3 se token. - let mut noise_se = Secret::new(); - if let Some(remote_s_public_key) = - from_bytes_agreement::(&message[s_enc_start..s_auth_start], &handshake_state.noise_e_secret, noise_se.as_mut()) + if !(HANDSHAKE_HELLO_CHALLENGE_MIN_SIZE..=HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE) + .contains(&assembled_packet.len()) { - let mut noise_ck = handshake_state.noise_ck_eseeekem1psk.clone(); - let noise_k_eseeekem1pskse = noise_ck.mix_key_initialize_key(hmac, noise_se.as_ref()); - drop(noise_se); - // Noise process pattern3 payload. - let (is_auth, noise_h_ee1peekem1pskpsp) = decrypt_and_hash::( - sha512, - &noise_k_eseeekem1pskse, - &noise_h_ee1peekem1pskps, - packet_type, - 0, - &mut message[p_enc_start..p_auth_end], - ); - drop(noise_k_eseeekem1pskse); - if !is_auth { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - // Bob finished Noise XKhfs+psk2 handshake. - let header_send_cipher = Application::PrpEnc::new(handshake_state.header_send_key.as_ref()); - let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_ee1peekem1pskpsp); - let mut send_reject = || { - // We just used a counter with this key, but we are not storing - // the fact we used it in memory. This is currently ok because the - // handshake is being dropped, so nonce reuse can't happen. - let (mut fragment, len) = encrypt_control( - &mut Application::AeadEnc::new(kex_key_b2a.as_ref()), - &header_send_cipher, - PACKET_TYPE_SESSION_REJECTED, - INIT_COUNTER, - handshake_state.remote_key_id.get(), - &[], - ); - send_unassociated_reply(&mut fragment[..len]); - }; - - let (responder_disallows_downgrade, responder_silently_rejects) = check_accept_session( - &remote_s_public_key, - &message[p_enc_start..p_auth_start], - handshake_state.ratchet_state.chain_len(), - ); - if let Some((responder_disallows_downgrade, application_data)) = responder_disallows_downgrade { - let result = app.restore_by_identity(&remote_s_public_key, &application_data, current_time); - match result { - Ok(true_ratchet_states) => { - let mut has_match = false; - for rs in &true_ratchet_states { - if !rs.is_null() { - has_match |= &handshake_state.ratchet_state == rs; - } - } - if !has_match { - if !responder_disallows_downgrade && handshake_state.ratchet_state.is_empty() { - // TODO: add some kind of warning callback or signal. - } else { - if !responder_silently_rejects { - send_reject(); - } - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - } - - let mut noise_kk_ss = Secret::new(); - if !self.0.static_keypair.agree(&remote_s_public_key, noise_kk_ss.as_mut()) { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); - } - let noise_kk_local_init_h = mix_hash(sha512, &INITIAL_H_REKEY, self.0.static_keypair.public_key_bytes()); - let noise_kk_local_init_h = mix_hash(sha512, &noise_kk_local_init_h, remote_s_public_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &INITIAL_H_REKEY, remote_s_public_key.as_bytes()); - let noise_kk_remote_init_h = mix_hash(sha512, &noise_kk_remote_init_h, self.0.static_keypair.public_key_bytes()); - - let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_ee1peekem1pskpsp); - // We must make sure the ratchet key is saved before we transition. - let new_ratchet_state = - RatchetState::new_nonempty(rk, rf, NonZeroU64::new(handshake_state.ratchet_state.chain_len() + 1).unwrap()); - let result = app.save_ratchet_state( - &remote_s_public_key, - &application_data, - [&true_ratchet_states[0], &true_ratchet_states[1]], - [&new_ratchet_state, &RatchetState::Null], - current_time, - ); - if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); - } - - - let mut session_queue = self.0.session_queue.lock().unwrap(); - let queue_idx = session_queue.reserve_index(); - let session = Arc::new(Session { - context: Arc::downgrade(&self.0), - queue_idx, - application_data, - remote_static_key: remote_s_public_key, - send_counter: AtomicU64::new(INIT_COUNTER), - session_has_expired: AtomicBool::new(false), - counter_antireplay_window: std::array::from_fn(|_| AtomicU64::new(0)), - state_machine_lock: Mutex::new(()), - state: RwLock::new(SessionMutableState { - ratchet_states: [new_ratchet_state.clone(), RatchetState::Null], - cipher_states: [ - Some(SessionKey::new( - hmac, - noise_ck, - handshake_state.local_key_id, - handshake_state.remote_key_id, - INIT_COUNTER, - true, - )), - None, - ], - current_key: 0, - outgoing_offer: KeyConfirm { - next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - timeout: current_time.saturating_add(Application::EXPIRATION_TIMEOUT_MS), - }, - }), - header_receive_cipher: Application::PrpDec::new(handshake_state.header_receive_key.as_ref()), - header_send_cipher, - kex_send_cipher: Mutex::new(Some(Application::AeadEnc::new(kex_key_b2a.as_ref()))), - kex_receive_cipher: Mutex::new(Some(Application::AeadDec::new(kex_key_a2b.as_ref()))), - noise_kk_ss, - noise_kk_local_init_h, - noise_kk_remote_init_h, - defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), - was_bob: true, - }); - let timer = Reverse(current_time.saturating_add(Application::RETRY_INTERVAL_MS)); - session_queue.push_reserved(queue_idx, Arc::downgrade(&session), timer); - drop(session_queue); - // There is the miniscule possibility this key id is already - // in use, in which case we have to drop this session like - // nothing ever happened. - let mut session_map = self.0.session_map.write().unwrap(); - if let std::collections::hash_map::Entry::Vacant(e) = session_map.entry(handshake_state.local_key_id) { - e.insert((Arc::downgrade(&session), false)); - drop(session_map); - let _ = session.send_control(&session.state.read().unwrap(), send_unassociated_reply, PACKET_TYPE_KEY_CONFIRM, &[]); - - app.event_log(LogEvent::ReceiveValidXK3(&session.application_data), current_time); - return Ok(ReceiveResult::Session(session, SessionEvent::NewSession)); - } else { - // This can occur if we accidentally generate a key id collision. - // There is an extremely short amount of time during which - // another session can steal this session's id, we'll have to - // restart the handshake in this case. - return Err(byzantine_fault!(FaultType::UnknownLocalKeyId, true)); - } - } - Err(e) => { - return Err(ReceiveError::RatchetIoError(e)); - } - } - } else { - if !responder_silently_rejects { - send_reject(); - } - return Ok(ReceiveResult::Rejected); - } - } else { - return Err(byzantine_fault!(FaultType::FailedAuthentication, false)); + return Err(fault!(InvalidPacket, true)); } + // Process recv challenge layer. + let challenge_start = assembled_packet.len() - CHALLENGE_SIZE; + let hash = &mut Crypto::Hash::new(); + match app.incoming_session() { + IncomingSessionAction::Allow => {} + IncomingSessionAction::Challenge => { + let result = ctx.challenge.process_hello( + hash, + remote_address, + (&assembled_packet[challenge_start..]).try_into().unwrap(), + ); + if let Err(challenge) = result { + log!(app, X1FailedChallengeSentNewChallenge); + let mut challenge_packet = ArrayVec::::new(); + challenge_packet.extend([0u8; HEADER_SIZE]); + challenge_packet + .try_extend_from_slice(&assembled_packet[..KID_SIZE]) + .unwrap(); + challenge_packet.extend(challenge); + let nonce = to_nonce(PACKET_TYPE_CHALLENGE, ctx.rng.lock().unwrap().next_u64()); + challenge_packet[FRAGMENT_COUNT_IDX] = 1; + challenge_packet[PACKET_NONCE_START..HEADER_SIZE] + .copy_from_slice(&nonce[..PACKET_NONCE_SIZE]); + set_header(&mut challenge_packet, 0, &nonce); + + send_unassociated_reply(&mut challenge_packet); + // If we issue a challenge the first hello packet will always fail. + return Err(fault!(FailedAuth, false)); + } else { + log!(app, X1SucceededChallenge); + } + } + IncomingSessionAction::Drop => return Err(ReceiveError::Rejected), + } + + // Process recv zeta layer. + received_x1_trans( + &mut app, + ctx, + hash, + &nonce, + &mut assembled_packet[..challenge_start], + |packet, hk_send| { + send_with_fragmentation(send_unassociated_reply, send_unassociated_mtu, packet, hk_send); + }, + )?; + log!(app, X1IsAuthSentX2); + + Ok(ReceiveOk::Unassociated) + } else if packet_type == PACKET_TYPE_CHALLENGE { + log!(app, ReceivedRawChallenge); + // Process recv challenge layer. + if assembled_packet.len() != KID_SIZE + CHALLENGE_SIZE { + return Err(fault!(InvalidPacket, true)); + } + if let Some(kid_recv) = + NonZeroU32::new(u32::from_ne_bytes(assembled_packet[..KID_SIZE].try_into().unwrap())) + { + if let Some(Some(session)) = ctx.session_map.read().unwrap().get(&kid_recv).map(|r| r.upgrade()) { + respond_to_challenge(ctx, &session, &assembled_packet[KID_SIZE..].try_into().unwrap()); + log!(app, ChallengeIsAuth(&session)); + return Ok(ReceiveOk::Unassociated); + } + } + Err(fault!(UnknownLocalKeyId, true)) + } else { + Err(fault!(InvalidPacket, true)) } - _ => return Err(byzantine_fault!(FaultType::InvalidPacket, false)), } } - /// Helper function for sending the empty string over the session. Useful for keep-alives. - /// - /// * `session` - The session to send to - /// * `send` - Function to call to send physical packet(s); the buffer passed to `send` is a - /// slice of `data` - /// * `current_time` - Current time in milliseconds - #[inline] - pub fn send_empty(&self, session: &Arc>, send: impl FnMut(&mut [u8]) -> bool, current_time: i64) -> Result<(), SendError> { - self.send(session, send, &mut [0u8; MIN_TRANSPORT_MTU], &[], current_time) - } /// Send data over the session. /// /// * `session` - The session to send to @@ -1655,1072 +590,70 @@ impl Context { /// slice of `data` /// * `mtu_sized_buffer` - A writable work buffer whose size equals the MTU /// * `data` - Data to send - /// * `current_time` - Current time in milliseconds - #[inline] pub fn send( &self, - session: &Arc>, - mut send: impl FnMut(&mut [u8]) -> bool, + session: &Arc>, + send: impl FnMut(&mut [u8]) -> bool, mtu_sized_buffer: &mut [u8], - mut data: &[u8], - current_time: i64, + data: &[u8], ) -> Result<(), SendError> { - if mtu_sized_buffer.len() < MIN_TRANSPORT_MTU { - return Err(SendError::InvalidParameter); - } - let state = session.state.read().unwrap(); - let key = state.cipher_states[state.current_key].as_ref().ok_or(SendError::SessionNotEstablished)?; - let counter = session.get_next_outgoing_counter()?; + send_payload(&self.0, session, data, send, mtu_sized_buffer) + } - let mut c = key.get_send_cipher(counter)?; - c.set_iv(&create_message_nonce(PACKET_TYPE_DATA, counter)); - - let fragment_max_chunk_size = mtu_sized_buffer.len() - HEADER_SIZE; - let fragment_count = (data.len() + AES_GCM_TAG_SIZE + (fragment_max_chunk_size - 1)) / fragment_max_chunk_size; - if fragment_count > MAX_FRAGMENTS { - return Err(SendError::DataTooLarge); - } - let last_fragment_no = fragment_count - 1; - - for fragment_no in 0..fragment_count { - let chunk_size = fragment_max_chunk_size.min(data.len()); - let mut fragment_size = chunk_size + HEADER_SIZE; - - set_packet_header( - mtu_sized_buffer, - fragment_count as u8, - fragment_no as u8, - PACKET_TYPE_DATA, - key.remote_key_id.get(), - counter, - ); - - c.encrypt(&data[..chunk_size], &mut mtu_sized_buffer[HEADER_SIZE..fragment_size]); - data = &data[chunk_size..]; - - if fragment_no == last_fragment_no { - debug_assert!(data.is_empty()); - let tagged_fragment_size = fragment_size + AES_GCM_TAG_SIZE; - c.finish_encrypt((&mut mtu_sized_buffer[fragment_size..tagged_fragment_size]).try_into().unwrap()); - fragment_size = tagged_fragment_size; - } - - session.header_send_cipher.encrypt_in_place( - (&mut mtu_sized_buffer[HEADER_PROTECT_ENC_START..HEADER_PROTECT_ENC_END]) - .try_into() - .unwrap(), - ); - if !send(&mut mtu_sized_buffer[..fragment_size]) { + /// Perform periodic background service and cleanup tasks. + /// + /// This returns the number of milliseconds until it should be called again. The caller should + /// try to satisfy this but small variations in timing of up to +/- a second or two are not + /// a problem. + /// + /// * `app` - Interface to application using ZSSP + /// * `send_to` - Function to get a sender and an MTU to send something over an active session + pub fn service, SendFn: FnMut(&mut [u8]) -> bool>( + &self, + mut app: App, + mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, + ) -> i64 { + let ctx = &self.0; + let mut session_queue = ctx.session_queue.lock().unwrap(); + let current_time = app.time(); + let mut next_service_time = current_time + Crypto::SETTINGS.fragment_assembly_timeout as i64; + // This update system takes heavy advantage of the fact that sessions only need to be updated + // either roughly every second or roughly every hour. That big gap allows for minor optimizations. + // If the gap changes (unlikely) this code may need to be rewritten. + while let Some((session, Reverse(timer), queue_idx)) = session_queue.peek() { + if *timer >= current_time { + next_service_time = next_service_time.min(*timer); break; } - } - drop(c); - if counter >= key.rekey_at_counter { - if let OfferStateMachine::Normal { .. } = &state.outgoing_offer { - drop(state); - if let Ok(timer) = initiate_rekey(&self.0, session, send, current_time) { - self.0.session_queue.lock().unwrap().change_priority(session.queue_idx, Reverse(timer)); + let session = match session.upgrade() { + Some(s) => s, + _ => { + session_queue.remove(queue_idx); + continue; } - } - } - Ok(()) - } - /// Update the challenge window, returning true if the challenge is still valid. - #[inline(always)] - fn check_challenge_window(&self, counter: u64) -> bool { - let slot = &self.0.challenge_antireplay_window[(counter as usize) % self.0.challenge_antireplay_window.len()]; - let counter = counter.wrapping_add(1); - let prev_counter = slot.load(Ordering::Relaxed); - prev_counter < counter - } - /// Update the challenge window, returning true if the challenge is still valid. - #[inline(always)] - fn update_challenge_window(&self, counter: u64) -> bool { - let slot = &self.0.challenge_antireplay_window[(counter as usize) % self.0.challenge_antireplay_window.len()]; - let counter = counter.wrapping_add(1); - let prev_counter = slot.fetch_max(counter, Ordering::Relaxed); - prev_counter < counter - } -} -/// Initiate the rekeying protocol. This session will now begin attempting to rekey this session -/// with its peer, if it was not already. -fn initiate_rekey( - context: &Arc>, - session: &Arc>, - send: impl FnOnce(&mut [u8]) -> bool, - current_time: i64, -) -> Result { - let mut message = [0u8; NoiseKKPattern1or2::SIZE]; - - let kex_lock = session.state_machine_lock.lock().unwrap(); - let state = session.state.read().unwrap(); - // We may only attempt to rekey if we are not already doing so. - match &state.outgoing_offer { - OfferStateMachine::Normal { .. } => (), - _ => return Err(()), - } - let sha512 = &mut Application::Hash::new(); - let hmac = &mut Application::HmacHash::new(); - // Start of Noise KKpsk0 pattern1. - // Noise process pattern1 psk0 token. - let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_states[0].key().unwrap()); - let noise_h_psk = mix_hash(sha512, &session.noise_kk_local_init_h, &noise_temp_h); - // Noise process pattern1 e token. - let noise_e_secret = Application::KeyPair::generate(&mut context.rng.lock().unwrap()); - let noise_h_pske = mix_hash(sha512, &noise_h_psk, noise_e_secret.public_key_bytes()); - noise_ck.mix_key(hmac, noise_e_secret.public_key_bytes()); - - let noise_pattern1: &mut NoiseKKPattern1or2 = byte_array_as_proto_buffer_mut(&mut message); - noise_pattern1.noise_e = *noise_e_secret.public_key_bytes(); - // Noise process pattern1 es token. - let mut noise_es = Secret::new(); - if !noise_e_secret.agree(&session.remote_static_key, noise_es.as_mut()) { - return Err(()); - } - noise_ck.mix_key(hmac, noise_es.as_ref()); - drop(noise_es); - // Noise process pattern1 ss token. - let noise_k_pskesss = noise_ck.mix_key_initialize_key(hmac, session.noise_kk_ss.as_ref()); - // Noise process pattern1 payload token. - let mut session_map = context.session_map.write().unwrap(); - let new_key_id = generate_key_id(&session_map, &mut context.rng.lock().unwrap()); - let next_key_index = state.current_key ^ 1; - session_map.insert(new_key_id, (Arc::downgrade(session), next_key_index > 0)); - drop(session_map); - - let noise_pattern1: &mut NoiseKKPattern1or2 = byte_array_as_proto_buffer_mut(&mut message); - noise_pattern1.key_id = new_key_id.get().to_ne_bytes(); - let noise_h_pskep = encrypt_and_hash::( - sha512, - &noise_k_pskesss, - &noise_h_pske, - PACKET_TYPE_NOISE_KK_PATTERN_1, - 0, - &mut message[NoiseKKPattern1or2::ENC_START..NoiseKKPattern1or2::AUTH_END], - ); - drop(noise_k_pskesss); - - drop(state); - let mut state = session.state.write().unwrap(); - state.outgoing_offer = OfferStateMachine::NoiseKKPattern1 { - next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - timeout: current_time.saturating_add(Application::EXPIRATION_TIMEOUT_MS), - new_key_id, - noise_e_secret, - noise_message: message.clone(), - noise_h_pskep, - noise_ck: noise_ck.clone(), - }; - drop(state); - drop(kex_lock); - let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_NOISE_KK_PATTERN_1, &message); - Ok(current_time.saturating_add(Application::RETRY_INTERVAL_MS)) -} -fn receive_control_fragment<'a, Application: ApplicationLayer, SendFn: FnMut(&mut [u8]) -> bool>( - context: &Context, - session: Arc>, - app: &Application, - mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, - packet_type: u8, - counter: u64, - fragment: &mut [u8], - current_time: i64, -) -> Result, ReceiveError> { - let state = session.state.read().unwrap(); - let mut c = session.kex_receive_cipher.lock().unwrap(); - let message = decrypt_control( - c.as_mut().ok_or(byzantine_fault!(FaultType::OutOfSequence, false))?, - packet_type, - counter, - fragment, - )?; - drop(c); - session.update_receive_window(counter); - use OfferStateMachine::*; - return match packet_type { - PACKET_TYPE_SESSION_REJECTED => match &state.outgoing_offer { - NoiseXKPattern1or3(_) => { - drop(state); - Ok(ReceiveResult::Session(session, SessionEvent::Rejected)) - } - _ => Err(byzantine_fault!(FaultType::OutOfSequence, false)), - }, - PACKET_TYPE_KEY_CONFIRM => { - drop(state); - app.event_log(LogEvent::ReceiveValidKeyConfirm(&session), current_time); - let kex_lock = session.state_machine_lock.lock().unwrap(); - let mut state = session.state.write().unwrap(); - // We only want to stop sending NoiseKKPattern2 offers when the latest derived - // key is confirmed. And we only want to do that once. - let (used_latest_key, try_delete, ret) = match &state.outgoing_offer { - NoiseKKPattern2 { .. } => (true, true, SessionEvent::Control), - NoiseXKPattern1or3(handshake_state) => { - if let NoiseXKAliceHandshakeState::NoiseXKPattern3 { .. } = &handshake_state.offer { - (true, true, SessionEvent::Established) - } else { - (false, false, SessionEvent::Control) - } - } - _ => (true, false, SessionEvent::Control), }; - if try_delete { - let result = if !state.ratchet_states[1].is_null() { - app.save_ratchet_state( - &session.remote_static_key, - &session.application_data, - [&state.ratchet_states[0], &state.ratchet_states[1]], - [&state.ratchet_states[0], &RatchetState::Null], - current_time, - ) - } else { - Ok(()) - }; - if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); + let result = process_timers(&mut app, ctx, &session, current_time, |packet, hk_send| { + if let Some((send_fragment, mut mtu)) = send_to(&session) { + mtu = mtu.max(MIN_TRANSPORT_MTU); + send_with_fragmentation(send_fragment, mtu, packet, hk_send); } - if let NoiseKKPattern2 { kex_send_key, .. } = &state.outgoing_offer { - session - .kex_send_cipher - .lock() - .unwrap() - .replace(Application::AeadEnc::new(kex_send_key.as_ref())); - } - state.ratchet_states[1] = RatchetState::Null; - state.current_key ^= 1; - state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); - } - drop(state); - drop(kex_lock); - if used_latest_key { - if let Some((send, _)) = send_to(&session) { - let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_ACK, &[]); - } - } - Ok(ReceiveResult::Session(session, ret)) - } - PACKET_TYPE_ACK => { - drop(state); - app.event_log(LogEvent::ReceiveValidKeyDelete(&session), current_time); - let kex_lock = session.state_machine_lock.lock().unwrap(); - let mut state = session.state.write().unwrap(); - // Check if we should end any current offers and transition back to Normal state - if let KeyConfirm { .. } = &state.outgoing_offer { - state.outgoing_offer = new_normal_state(context.0.rng.lock().unwrap().next_u64(), current_time); - } - drop(state); - drop(kex_lock); - Ok(ReceiveResult::Session(session, SessionEvent::Control)) - } - PACKET_TYPE_NOISE_KK_PATTERN_1 => { - app.event_log(LogEvent::ReceiveUncheckedKK1, current_time); - let message = &mut message[..NoiseKKPattern1or2::SIZE]; - let noise_pattern1: &NoiseKKPattern1or2 = byte_array_as_proto_buffer(message); - - drop(state); - let kex_lock = session.state_machine_lock.lock().unwrap(); - let state = session.state.read().unwrap(); - // We need the following operation to be atomic with the change of offer type - let (should_rekey_as_bob, chosen_id) = match &state.outgoing_offer { - // Check rekey rate limits. - Normal { .. } => (true, None), - // In the following situation, both parties are in state NoiseKKPattern1, - // we need to deterministically allow only one of them to transition to - // NoiseKKPattern2. - NoiseKKPattern1 { new_key_id, .. } => (session.was_bob, Some(*new_key_id)), - _ => (false, None), - }; - if !should_rekey_as_bob { - // This can be triggered if both parties attempt rekeying simultaneously, or if the - // remote party sent us a duplicate rekey request. - // The code above handles this case and only lets one party through to rekeying. - drop(state); - drop(kex_lock); - return Ok(ReceiveResult::Session(session, SessionEvent::Control)); - } - // Noise process pattern1 psk0 token. - let sha512 = &mut Application::Hash::new(); - let hmac = &mut Application::HmacHash::new(); - let mut noise_ck = SymmetricState::new(INITIAL_H_REKEY); - let noise_temp_h = noise_ck.mix_key_and_hash(hmac, state.ratchet_states[0].key().unwrap()); - let noise_h_psk = mix_hash(sha512, &session.noise_kk_remote_init_h, &noise_temp_h); - // Noise process pattern1 e token. - // Get public key validation out of the way early - let mut noise_es = Secret::new(); - let mut noise_ee = Secret::new(); - let mut noise_se = Secret::new(); - if let Some(alice_e) = from_bytes_agreement::(&noise_pattern1.noise_e, &context.0.static_keypair, noise_es.as_mut()) { - let bob_e_secret = Application::KeyPair::generate(&mut context.0.rng.lock().unwrap()); - if bob_e_secret.agree(&alice_e, noise_ee.as_mut()) && bob_e_secret.agree(&session.remote_static_key, noise_se.as_mut()) { - let noise_h_pske = mix_hash(sha512, &noise_h_psk, alice_e.as_bytes()); - noise_ck.mix_key(hmac, alice_e.as_bytes()); - // Noise process pattern1 es token. - noise_ck.mix_key(hmac, noise_es.as_ref()); - drop(noise_es); - // Noise process pattern1 ss token. - let noise_k_pskesss = noise_ck.mix_key_initialize_key(hmac, session.noise_kk_ss.as_ref()); - - // Noise process pattern1 payload. - let (is_auth, noise_h_pskep) = decrypt_and_hash::( - sha512, - &noise_k_pskesss, - &noise_h_pske, - packet_type, - 0, - &mut message[NoiseKKPattern1or2::ENC_START..NoiseKKPattern1or2::AUTH_END], - ); - let noise_pattern1: &NoiseKKPattern1or2 = byte_array_as_proto_buffer(message); - if let (true, Some(remote_key_id)) = (is_auth, NonZeroU32::new(u32::from_ne_bytes(noise_pattern1.key_id))) { - // Alice fully authenticated. - // Start of Noise KKpsk0 pattern2. - // Noise process pattern2 e token. - let noise_h_pskepe = mix_hash(sha512, &noise_h_pskep, bob_e_secret.public_key_bytes()); - noise_ck.mix_key(hmac, bob_e_secret.public_key_bytes()); - // Noise process pattern2 ee token. - noise_ck.mix_key(hmac, noise_ee.as_ref()); - drop(noise_ee); - // Noise process pattern2 se token. - let noise_k_pskessseese = noise_ck.mix_key_initialize_key(hmac, noise_se.as_ref()); - drop(noise_se); - // Noise process pattern2 payload. - let mut message2 = [0u8; NoiseKKPattern1or2::SIZE]; - let noise_pattern2: &mut NoiseKKPattern1or2 = byte_array_as_proto_buffer_mut(&mut message2); - noise_pattern2.noise_e = *bob_e_secret.public_key_bytes(); - let mut session_map = context.0.session_map.write().unwrap(); - // If we already generated a new key id mapping reuse it. - let new_key_id = chosen_id.unwrap_or_else(|| generate_key_id(&session_map, &mut context.0.rng.lock().unwrap())); - noise_pattern2.key_id = new_key_id.get().to_ne_bytes(); - - let noise_h_pskepep = encrypt_and_hash::( - sha512, - &noise_k_pskessseese, - &noise_h_pskepe, - PACKET_TYPE_NOISE_KK_PATTERN_2, - 0, - &mut message2[NoiseKKPattern1or2::ENC_START..NoiseKKPattern1or2::AUTH_END], - ); - drop(noise_k_pskessseese); - // Bob finished Noise KKpsk0 handshake. - let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, NonZeroU64::new(state.ratchet_states[0].chain_len() + 1).unwrap()); - let result = app.save_ratchet_state( - &session.remote_static_key, - &session.application_data, - [&state.ratchet_states[0], &state.ratchet_states[1]], - [&new_ratchet_state, &state.ratchet_states[0]], - current_time, - ); - if let Err(e) = result { - drop(state); - drop(kex_lock); - return Err(ReceiveError::RatchetIoError(e)); - } - let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_pskepep); - // The new "Bob" doesn't know yet if Alice has received the new key, so the - // new key is recorded as the "alt" (key_index ^ 1) but the current key is - // not advanced yet. - let next_key_index = state.current_key ^ 1; - session_map.insert(new_key_id, (Arc::downgrade(&session), next_key_index > 0)); - if let Some(pre_id) = state.cipher_states[next_key_index].as_ref().map(|k| k.local_key_id) { - session_map.remove(&pre_id); - } - drop(session_map); - drop(state); - let mut state = session.state.write().unwrap(); - let current_counter = session.send_counter.load(Ordering::Relaxed); - session - .kex_receive_cipher - .lock() - .unwrap() - .replace(Application::AeadDec::new(kex_key_a2b.as_ref())); - state.ratchet_states[1] = state.ratchet_states[0].clone(); - state.ratchet_states[0] = new_ratchet_state.clone(); - - state.cipher_states[next_key_index].replace(SessionKey::new( - hmac, - noise_ck, - new_key_id, - remote_key_id, - current_counter, - true, - )); - let timer = current_time.saturating_add(Application::RETRY_INTERVAL_MS); - state.outgoing_offer = NoiseKKPattern2 { - next_retry_time: AtomicI64::new(timer), - timeout: current_time.saturating_add(Application::EXPIRATION_TIMEOUT_MS), - noise_message: message2, - kex_send_key: kex_key_b2a.clone(), - }; - drop(state); - drop(kex_lock); - context.0.session_queue.lock().unwrap().change_priority(session.queue_idx, Reverse(timer)); - - if let Some((send, _)) = send_to(&session) { - let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_NOISE_KK_PATTERN_2, &message2); - } - app.event_log(LogEvent::ReceiveValidKK1(&session), current_time); - return Ok(ReceiveResult::Session(session, SessionEvent::Control)); - } - } - } - Err(byzantine_fault!(FaultType::FailedAuthentication, false)) - } - PACKET_TYPE_NOISE_KK_PATTERN_2 => { - app.event_log(LogEvent::ReceiveUncheckedKK2, current_time); - let message = &mut message[..NoiseKKPattern1or2::SIZE]; - let noise_pattern2: &NoiseKKPattern1or2 = byte_array_as_proto_buffer(message); - - drop(state); - let kex_lock = session.state_machine_lock.lock().unwrap(); - let state = session.state.read().unwrap(); - if let NoiseKKPattern1 { new_key_id, noise_e_secret, noise_ck, noise_h_pskep, .. } = &state.outgoing_offer { - // Noise process pattern2 e token. - let mut noise_ee = Secret::new(); - let mut noise_se = Secret::new(); - if let Some(bob_e) = from_bytes_agreement::(&noise_pattern2.noise_e, noise_e_secret, noise_ee.as_mut()) { - if context.0.static_keypair.agree(&bob_e, noise_se.as_mut()) { - let sha512 = &mut Application::Hash::new(); - let hmac = &mut Application::HmacHash::new(); - let mut noise_ck = noise_ck.clone(); - let noise_h_pskepe = mix_hash(sha512, noise_h_pskep, bob_e.as_bytes()); - noise_ck.mix_key(hmac, bob_e.as_bytes()); - // Noise process pattern2 ee token. - noise_ck.mix_key(hmac, noise_ee.as_ref()); - drop(noise_ee); - // Noise process pattern2 se token. - let noise_k_pskessseese = noise_ck.mix_key_initialize_key(hmac, noise_se.as_ref()); - drop(noise_se); - // Noise process pattern2 payload. - let (is_auth, noise_h_pskepep) = decrypt_and_hash::( - sha512, - &noise_k_pskessseese, - &noise_h_pskepe, - packet_type, - 0, - &mut message[NoiseKKPattern1or2::ENC_START..NoiseKKPattern1or2::AUTH_END], - ); - let noise_pattern2: &NoiseKKPattern1or2 = byte_array_as_proto_buffer(message); - if let (true, Some(remote_key_id)) = (is_auth, NonZeroU32::new(u32::from_ne_bytes(noise_pattern2.key_id))) { - // Bob fully authenticated. - // Alice finished Noise KKpsk0 handshake. - let (rk, rf) = noise_ck.get_ask2(hmac, LABEL_RATCHET_STATE, &noise_h_pskepep); - let new_ratchet_state = - RatchetState::new_nonempty(rk, rf, NonZeroU64::new(state.ratchet_states[0].chain_len() + 1).unwrap()); - let result = app.save_ratchet_state( - &session.remote_static_key, - &session.application_data, - [&state.ratchet_states[0], &state.ratchet_states[1]], - [&new_ratchet_state, &RatchetState::Null], - current_time, - ); - if let Err(e) = result { - drop(state); - drop(kex_lock); - return Err(ReceiveError::RatchetIoError(e)); - } - let (kex_key_b2a, kex_key_a2b) = noise_ck.get_ask2(hmac, LABEL_KEX_KEY, &noise_h_pskepep); - - let new_key_id = *new_key_id; - drop(state); - let mut state = session.state.write().unwrap(); - let next_key_index = state.current_key ^ 1; - state.current_key = next_key_index; - if let Some(key) = state.cipher_states[next_key_index].as_ref() { - context.0.session_map.write().unwrap().remove(&key.local_key_id); - } - session - .kex_receive_cipher - .lock() - .unwrap() - .replace(Application::AeadDec::new(kex_key_b2a.as_ref())); - session - .kex_send_cipher - .lock() - .unwrap() - .replace(Application::AeadEnc::new(kex_key_a2b.as_ref())); - state.ratchet_states[1] = RatchetState::Null; - state.ratchet_states[0] = new_ratchet_state.clone(); - - state.cipher_states[next_key_index].replace(SessionKey::new( - hmac, - noise_ck, - new_key_id, - remote_key_id, - session.send_counter.load(Ordering::Relaxed), - false, - )); - state.outgoing_offer = KeyConfirm { - next_retry_time: AtomicI64::new(current_time.saturating_add(Application::RETRY_INTERVAL_MS)), - timeout: current_time.saturating_add(Application::EXPIRATION_TIMEOUT_MS), - }; - drop(state); - drop(kex_lock); - // Let Bob know we got the key. - if let Some((send, _)) = send_to(&session) { - let _ = session.send_control(&session.state.read().unwrap(), send, PACKET_TYPE_KEY_CONFIRM, &[]); - } - app.event_log(LogEvent::ReceiveValidKK2(&session), current_time); - return Ok(ReceiveResult::Session(session, SessionEvent::Control)); - } - } - } - // Bob failed authentication so according to Noise we must terminate this - // handshake. - // This should not happen in practice since this packet will have already passed - // authentication under the current key. - session.expire(); - Err(byzantine_fault!(FaultType::FailedAuthentication, false)) + }); + if let Some(next_timer) = result { + next_service_time = next_service_time.min(next_timer); + session_queue.change_priority(queue_idx, Reverse(next_timer)); } else { - drop(state); - drop(kex_lock); - Ok(ReceiveResult::Session(session, SessionEvent::Control)) + session.expire_inner(Some(ctx), Some(&mut session_queue)); } } - _ => Err(byzantine_fault!(FaultType::InvalidPacket, false)), - }; -} + drop(session_queue); -impl Session { - /// This can only fail with `MaxKeyLifetimeExceeded` or `SessionNotEstablished`. - #[inline] - fn send_control( - &self, - state: &SessionMutableState, - send: impl FnOnce(&mut [u8]) -> bool, - packet_type: u8, - packet: &[u8], - ) -> Result<(), SendError> { - let key = state.cipher_states[state.current_key].as_ref().ok_or(SendError::SessionNotEstablished)?; - let counter = self.get_next_outgoing_counter()?; - let mut c = self.kex_send_cipher.lock().unwrap(); - let (mut fragment, len) = encrypt_control( - c.as_mut().ok_or(SendError::SessionNotEstablished)?, - &self.header_send_cipher, - packet_type, - counter, - key.remote_key_id.get(), - packet, - ); - send(&mut fragment[..len]); - Ok(()) - } - /// Check whether this session is established. - #[inline] - pub fn established(&self) -> bool { - let state = self.state.read().unwrap(); - !matches!(&state.outgoing_offer, OfferStateMachine::NoiseXKPattern1or3(_)) - } - /// The static public key of the remote peer. - #[inline] - pub fn remote_s_public_key(&self) -> &Application::PublicKey { - &self.remote_static_key - } - /// The current ratchet state of this session. - /// The returned values are sensitive and should be securely erased before being dropped. - #[inline] - pub fn ratchet_states(&self) -> [RatchetState; 2] { - let state = self.state.read().unwrap(); - state.ratchet_states.clone() - } - /// The current ratchet count of this session. - #[inline] - pub fn ratchet_count(&self) -> u64 { - self.state.read().unwrap().ratchet_states[0].chain_len() - } - /// Mark a session as expired. This will make it impossible for this session to successfully - /// receive or send data. It is recommended to simply `drop` the session instead, but this can - /// provide some reassurance in complex shared ownership situations. - pub fn expire(&self) { - if let Some(context) = self.context.upgrade() { - self.expire_inner(&context, &mut context.session_queue.lock().unwrap()); - } - } - fn expire_inner( - &self, - context: &Arc>, - session_queue: &mut IndexedBinaryHeap>, Reverse>, - ) { - // Prevent this session from being updated. - session_queue.remove(self.queue_idx); - self.session_has_expired.store(true, Ordering::Relaxed); - let _kex_lock = self.state_machine_lock.lock().unwrap(); - let state = self.state.read().unwrap(); - let mut session_map = context.session_map.write().unwrap(); - for key in &state.cipher_states { - if let Some(pre_id) = key.as_ref().map(|k| k.local_key_id) { - session_map.remove(&pre_id); - } - } - use OfferStateMachine::*; - let id = match &state.outgoing_offer { - NoiseXKPattern1or3(handshake_state) => handshake_state.local_key_id, - NoiseKKPattern1 { new_key_id, .. } => *new_key_id, - _ => return, - }; - session_map.remove(&id); - } + self.0 + .unassociated_defrag_cache + .lock() + .unwrap() + .check_for_expiry(Crypto::SETTINGS.fragment_assembly_timeout as i64, current_time); + self.0.unassociated_handshake_states.service(current_time); - /// Get the next outgoing counter value. - #[inline(always)] - fn get_next_outgoing_counter(&self) -> Result { - if self.session_has_expired.load(Ordering::Relaxed) { - Err(SendError::SessionExpired) - } else { - let counter = self.send_counter.fetch_add(1, Ordering::Relaxed); - if counter > THREAD_SAFE_COUNTER_HARD_EXPIRE { - // Because this thread sets the flag itself it will never be able to increment the - // counter again. - // For that reason the other atomic orderings can be `Relaxed`. - self.session_has_expired.store(true, Ordering::SeqCst) - } - Ok(counter) - } - } - /// Check the receive window without mutating state. - #[inline(always)] - fn check_receive_window(&self, counter: u64) -> bool { - let slot = &self.counter_antireplay_window[(counter as usize) % self.counter_antireplay_window.len()]; - let counter = counter.wrapping_add(1); - let prev_counter = slot.load(Ordering::Relaxed); - prev_counter < counter && counter.wrapping_sub(prev_counter) <= COUNTER_WINDOW_MAX_SKIP_AHEAD - } - /// Update the receive window, returning true if the packet is still valid. - /// This should only be called after the packet is authenticated. - #[inline(always)] - fn update_receive_window(&self, counter: u64) -> bool { - let slot = &self.counter_antireplay_window[(counter as usize) % self.counter_antireplay_window.len()]; - let counter = counter.wrapping_add(1); - let prev_counter = slot.fetch_max(counter, Ordering::Relaxed); - prev_counter < counter && counter.wrapping_sub(prev_counter) <= COUNTER_WINDOW_MAX_SKIP_AHEAD + next_service_time - current_time } } -impl Drop for Session { - fn drop(&mut self) { - if let Some(context) = self.context.upgrade() { - self.expire_inner(&context, &mut context.session_queue.lock().unwrap()); - } - } -} - -impl NoiseXKAliceHandshake { - /// Can only fail with `OpenError::InvalidPublicKey` because of remote_s_public_key. - /// Corresponds to Noise `Initialize`. - #[inline] - fn initialize( - local_key_id: NonZeroU32, - remote_s_public_key: &Application::PublicKey, - ratchet_state: &[RatchetState; 2], - rng: &mut Application::Rng, - ) -> Result< - ( - NoiseXKAliceHandshakeState, - Secret, - Secret, - ), - OpenError, - > { - let mut message = [0u8; NoiseXKPattern1::MAX_SIZE]; - let sha512 = &mut Application::Hash::new(); - let hmac = &mut Application::HmacHash::new(); - // Start of Noise XKhfs+psk2 pattern1. - let noise_pattern1: &mut NoiseXKPattern1 = byte_array_as_proto_buffer_mut(&mut message); - let noise_e_secret = Application::KeyPair::generate(rng); - let noise_e1_secret = pqc_kyber::keypair(rng); - noise_pattern1.alice_key_id = local_key_id.get().to_ne_bytes(); - noise_pattern1.noise_e = *noise_e_secret.public_key_bytes(); - noise_pattern1.noise_e1 = noise_e1_secret.public; - // Noise process prologue. - let noise_h = mix_hash( - sha512, - &INITIAL_H, - &message[NoiseXKPattern1::PROLOGUE_START..NoiseXKPattern1::PROLOGUE_END], - ); - let noise_h = mix_hash(sha512, &noise_h, remote_s_public_key.as_bytes()); - // Noise process pattern1 e token. - let mut noise_ck = SymmetricState::new(INITIAL_H); - let noise_h_e = mix_hash(sha512, &noise_h, noise_e_secret.public_key_bytes()); - noise_ck.mix_key(hmac, noise_e_secret.public_key_bytes()); - // Noise process pattern1 es token. - let mut noise_es = Secret::new(); - if !noise_e_secret.agree(remote_s_public_key, noise_es.as_mut()) { - return Err(OpenError::InvalidPublicKey); - } - let noise_k_es = noise_ck.mix_key_initialize_key(hmac, noise_es.as_ref()); - drop(noise_es); - // Noise process pattern1 e1 token. - let noise_h_ee1 = encrypt_and_hash::( - sha512, - &noise_k_es, - &noise_h_e, - PACKET_TYPE_NOISE_XK_PATTERN_1, - 0, - &mut message[NoiseXKPattern1::E1_ENC_START..NoiseXKPattern1::P_ENC_START], - ); - // Noise process pattern1 payload. - let noise_pattern1: &mut NoiseXKPattern1 = byte_array_as_proto_buffer_mut(&mut message); - let mut idx = 0; - for rs in ratchet_state { - if let Some(rf) = rs.fingerprint() { - let next_idx = idx + RATCHET_SIZE; - noise_pattern1.payload[idx..next_idx].copy_from_slice(rf); - idx = next_idx; - } - } - let p_auth_end = NoiseXKPattern1::P_ENC_START + idx + AES_GCM_TAG_SIZE; - let noise_message_len = p_auth_end + ChallengeResponse::SIZE; - - let noise_h_ee1p = encrypt_and_hash::( - sha512, - &noise_k_es, - &noise_h_ee1, - PACKET_TYPE_NOISE_XK_PATTERN_1, - 1, - &mut message[NoiseXKPattern1::P_ENC_START..p_auth_end], - ); - drop(noise_k_es); - let (header_b2a_key, header_a2b_key) = noise_ck.get_ask2(hmac, LABEL_HEADER_KEY, &noise_h_ee1p); - let message_id = u64::from_be_bytes(message[p_auth_end - 8..p_auth_end].try_into().unwrap()); - - message[noise_message_len - CHALLENGE_POW_SIZE..noise_message_len].copy_from_slice(&rng.next_u64().to_ne_bytes()); - Ok(( - NoiseXKAliceHandshakeState::NoiseXKPattern1 { - noise_h_ee1p, - noise_e_secret, - noise_e1_secret: Secret(noise_e1_secret.secret), - noise_ck_es: noise_ck, - noise_message_len, - noise_message: message, - message_id, - }, - header_a2b_key, - header_b2a_key, - )) - } - /// Should not fail unless Bob's public key is adversarial. - fn reinitialize( - &mut self, - session: &Arc>, - ratchet_state: &[RatchetState; 2], - session_map: &mut HashMap>, bool)>, - rng: &mut Application::Rng, - current_time: i64, - ) -> bool { - let local_key_id = generate_key_id(session_map, rng); - if let Ok((offer, a2b_header_key, b2a_header_key)) = Self::initialize(local_key_id, &session.remote_static_key, ratchet_state, rng) { - self.timeout = current_time.saturating_add(Application::INITIAL_OFFER_TIMEOUT_MS); - session_map.remove(&self.local_key_id); - session_map.insert(local_key_id, (Arc::downgrade(session), false)); - self.local_key_id = local_key_id; - self.offer = offer; - session.header_send_cipher.reset(a2b_header_key.as_ref()); - session.header_receive_cipher.reset(b2a_header_key.as_ref()); - true - } else { - false - } - } -} - -/// Create the normal state of the offer state machine, with the correct timestamps. -fn new_normal_state(rand: u64, current_time: i64) -> OfferStateMachine { - OfferStateMachine::Normal { - timeout: current_time - .saturating_add(Application::REKEY_AFTER_TIME_MS) - .saturating_sub(rand as i64 % Application::REKEY_AFTER_TIME_MAX_JITTER_MS), - } -} -/// Get a timestamp of when this timer should trigger next, or None if it should trigger now. -fn process_timer(timer: &AtomicI64, wait_time: i64, current_time: i64) -> Option { - let ts = timer.load(Ordering::Relaxed); - if ts <= current_time && timer.fetch_max(ts.saturating_add(wait_time), Ordering::Relaxed) == ts { - None - } else { - Some(ts) - } -} - -/// Corresponds to Noise `EncryptAndHash`. -#[inline] -fn encrypt_and_hash( - sha512: &mut Application::Hash, - noise_k: &Secret, - noise_h: &[u8; NOISE_HASHLEN], - packet_type: u8, - noise_k_uses: u64, - message: &mut [u8], -) -> [u8; NOISE_HASHLEN] { - let auth_start = message.len() - AES_GCM_TAG_SIZE; - let mut gcm = Application::AeadEnc::new(noise_k.as_ref()); - // Encrypt and add authentication tag. - gcm.set_iv(&create_message_nonce(packet_type, INIT_COUNTER + noise_k_uses)); - gcm.set_aad(noise_h); - if auth_start > 0 { - gcm.encrypt_in_place(&mut message[..auth_start]); - } - gcm.finish_encrypt((&mut message[auth_start..]).try_into().unwrap()); - mix_hash(sha512, noise_h, message) -} -/// Corresponds to Noise `DecryptAndHash`. -#[inline] -fn decrypt_and_hash( - sha512: &mut Application::Hash, - noise_k: &Secret, - noise_h: &[u8; NOISE_HASHLEN], - packet_type: u8, - noise_k_uses: u64, - message: &mut [u8], -) -> (bool, [u8; NOISE_HASHLEN]) { - let auth_start = message.len() - AES_GCM_TAG_SIZE; - let noise_h_c = mix_hash(sha512, noise_h, message); - let mut gcm = Application::AeadDec::new(noise_k.as_ref()); - gcm.set_iv(&create_message_nonce(packet_type, INIT_COUNTER + noise_k_uses)); - gcm.set_aad(noise_h); - if auth_start > 0 { - gcm.decrypt_in_place(&mut message[..auth_start]); - } - (gcm.finish_decrypt((&message[auth_start..]).try_into().unwrap()), noise_h_c) -} -/// Encrypt a standardized control packet. -#[inline] -fn encrypt_control( - c: &mut impl AesGcmEnc, - header_cipher: &impl AesEnc, - packet_type: u8, - counter: u64, - remote_key_id: u32, - packet: &[u8], -) -> ([u8; CONTROL_PACKET_MAX_SIZE], usize) { - let mut fragment = [0u8; CONTROL_PACKET_MAX_SIZE]; - let fragment_len = packet.len() + HEADER_SIZE + AES_GCM_TAG_SIZE; - - c.set_iv(&create_message_nonce(packet_type, counter)); - if !packet.is_empty(){ - fragment[HEADER_SIZE..fragment_len - AES_GCM_TAG_SIZE].copy_from_slice(packet); - c.encrypt_in_place(&mut fragment[HEADER_SIZE..fragment_len - AES_GCM_TAG_SIZE]); - } - c.finish_encrypt((&mut fragment[fragment_len - AES_GCM_TAG_SIZE..fragment_len]).try_into().unwrap()); - set_packet_header(&mut fragment, 1, 0, packet_type, remote_key_id, counter); - header_cipher.encrypt_in_place((&mut fragment[HEADER_PROTECT_ENC_START..HEADER_PROTECT_ENC_END]).try_into().unwrap()); - (fragment, fragment_len) -} -#[inline] -fn decrypt_control<'a, IoError>(c: &mut impl AesGcmDec, packet_type: u8, counter: u64, fragment: &'a mut [u8]) -> Result<&'a mut [u8], ReceiveError> { - let fragment_len = fragment.len(); - if !(CONTROL_PACKET_MIN_SIZE..=CONTROL_PACKET_MAX_SIZE).contains(&fragment_len) { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - c.set_iv(&create_message_nonce(packet_type, counter)); - c.decrypt_in_place(&mut fragment[HEADER_SIZE..fragment_len - AES_GCM_TAG_SIZE]); - if !c.finish_decrypt((&fragment[fragment_len - AES_GCM_TAG_SIZE..fragment_len]).try_into().unwrap()) { - // This can occur naturally if one of the remote peers resent a - // control packet that got delayed and arrived out of order. - return Err(byzantine_fault!(FaultType::FailedAuthentication, true)); - } - Ok(&mut fragment[HEADER_SIZE..fragment_len - AES_GCM_TAG_SIZE]) -} - -#[inline(always)] -fn set_packet_header(packet: &mut [u8], fragment_count: u8, fragment_no: u8, packet_type: u8, remote_key_id: u32, counter_or_id: u64) { - debug_assert!(packet.len() >= MIN_PACKET_SIZE); - debug_assert!(fragment_count > 0); - debug_assert!(fragment_count <= MAX_FRAGMENTS as u8); - debug_assert!(fragment_no < MAX_FRAGMENTS as u8); - debug_assert_eq!((packet_type << 1) >> 1, packet_type); - // [0..4] recipient key id - // -- start AES(ck_es * h_e_e1_p) encrypted block -- - // [4] fragment count (1..255) - // [5] fragment number (0..254) - // [6] reserved zero - // -- start of AES-GCM Nonce -- - // [7] packet type - // [8..16] 64-bit counter or packet id (big endian) - packet[4..16].copy_from_slice(&create_message_nonce(packet_type, counter_or_id)); - packet[0..4].copy_from_slice(&remote_key_id.to_ne_bytes()); - packet[4] = fragment_count; - packet[5] = fragment_no; - packet[6] = 0; -} -/// Create a 96-bit AES-GCM nonce. -/// -/// The primary information that we want to be contained here is the counter and the -/// packet type. The former makes this unique and the latter's inclusion authenticates -/// it as effectively AAD. Other elements of the header are either not authenticated, -/// like fragmentation info, or their authentication is implied via key exchange like -/// the key id. -#[inline(always)] -fn create_message_nonce(packet_type: u8, counter: u64) -> [u8; AES_GCM_IV_SIZE] { - let mut ret = [0u8; AES_GCM_IV_SIZE]; - ret[3] = packet_type; - // Noise requires a big endian counter at the end of the Nonce - ret[4..].copy_from_slice(&counter.to_be_bytes()); - ret -} -/// returns `(fragment_count, fragment_no, packet_type, counter, header_nonce)`. -#[inline(always)] -fn parse_packet_header(packet: &[u8]) -> (u8, u8, u8, u64, [u8; 10]) { - let mut header_nonce = [0; 10]; - let mut counter = 0u64.to_ne_bytes(); - header_nonce.copy_from_slice(&packet[6..16]); - counter.copy_from_slice(&packet[8..16]); - // We intentionally ignore the version number for future revisions. - (packet[4], packet[5], packet[7], u64::from_be_bytes(counter), header_nonce) -} - -/// Break a packet into fragments and send them all. -/// -/// The contents of packet[] are mangled during this operation, so it should be discarded after. -/// This is only used for key exchange and control packets. For data packets this is done inline -/// for better performance with encryption and fragmentation happening at the same time. -fn send_with_fragmentation( - send: &mut impl FnMut(&mut [u8]) -> bool, - mtu: usize, - packet: &mut [u8], - packet_type: u8, - remote_key_id: Option, - counter_or_id: u64, - header_cipher: Option<&impl AesEnc>, -) -> bool { - let packet_len = packet.len(); - let fragment_count = (packet_len.saturating_add(mtu - 1)) / mtu; // integer ceiling divide - debug_assert!(fragment_count <= MAX_FRAGMENTS); - let mut fragment_start = 0; - let mut fragment_end = packet_len.min(mtu); - let mut fragment_no = 0; - loop { - let fragment = &mut packet[fragment_start..fragment_end]; - set_packet_header( - fragment, - fragment_count as u8, - fragment_no as u8, - packet_type, - remote_key_id.map_or(0, |n| n.get()), - counter_or_id, - ); - if let Some(hcc) = header_cipher { - hcc.encrypt_in_place((&mut fragment[HEADER_PROTECT_ENC_START..HEADER_PROTECT_ENC_END]).try_into().unwrap()); - } - if !send(fragment) { - return false; - } - fragment_no += 1; - if fragment_no < fragment_count { - fragment_start = fragment_end - HEADER_SIZE; - fragment_end = (fragment_start.saturating_add(mtu)).min(packet_len); - } else { - break; - } - } - true -} - -/// Assemble a series of fragments into a buffer and return the length of the assembled packet in -/// bytes. -/// -/// This is also only used for key exchange and control packets. For data packets decryption and -/// assembly happen in one pass for better performance. -fn assemble_fragments_into(fragments: &[A::IncomingPacketBuffer], d: &mut [u8]) -> Result> { - let mut l = 0; - for i in 0..fragments.len() { - let mut ff = fragments[i].as_ref(); - if i > 0 { - ff = &ff[HEADER_SIZE..]; - } - let j = l + ff.len(); - if j > d.len() { - return Err(byzantine_fault!(FaultType::InvalidPacket, false)); - } - d[l..j].copy_from_slice(ff); - l = j; - } - Ok(l) -} -/// Generate a random local key id that is currently unused. -fn generate_key_id( - session_map: &HashMap>, bool)>, - rng: &mut Application::Rng, -) -> NonZeroU32 { - loop { - if let Some(local_key_id) = NonZeroU32::new(rng.next_u32()) { - if !session_map.contains_key(&local_key_id) { - return local_key_id; - } - } - } -} - -impl SessionKey { - #[inline(always)] - fn new( - hmac: &mut Application::HmacHash, - ck: SymmetricState, - local_key_id: NonZeroU32, - remote_key_id: NonZeroU32, - current_counter: u64, - is_bob: bool, - ) -> Self { - let (b2a, a2b) = ck.split(hmac); - let (receive_key, send_key) = if is_bob { - (&a2b, &b2a) - } else { - (&b2a, &a2b) - }; - let send_cipher_pool = std::array::from_fn(|_| Mutex::new(Application::AeadEnc::new(send_key.as_ref()))); - let receive_cipher_pool = std::array::from_fn(|_| Mutex::new(Application::AeadDec::new(receive_key.as_ref()))); - Self { - local_key_id, - remote_key_id, - send_cipher_pool, - receive_cipher_pool, - rekey_at_counter: current_counter.saturating_add(Application::REKEY_AFTER_USES), - expire_at_counter: current_counter.saturating_add(Application::EXPIRE_AFTER_USES), - } - } - - #[inline(always)] - fn get_send_cipher(&self, counter: u64) -> Result, SendError> { - if counter < self.expire_at_counter { - Ok(self.send_cipher_pool[(counter as usize) % self.send_cipher_pool.len()].lock().unwrap()) - } else { - Err(SendError::SessionExpired) - } - } - - #[inline(always)] - fn get_receive_cipher(&self, counter: u64) -> MutexGuard { - let idx = (counter as usize) % self.receive_cipher_pool.len(); - self.receive_cipher_pool[idx].lock().unwrap() - } -} - -/// MixHash to update 'h' during negotiation. -#[inline(always)] -fn mix_hash(hasher: &mut impl Sha512, h: &[u8; NOISE_HASHLEN], m: &[u8]) -> [u8; NOISE_HASHLEN] { - let mut output = [0u8; NOISE_HASHLEN]; - hasher.reset(); - hasher.update(h); - hasher.update(m); - hasher.finish(&mut output); - output -} -/// Check if the proof of work attached to the first message contains the correct number of leading -/// zeros. -#[inline(always)] -fn verify_pow(hasher: &mut Application::Hash, response: &[u8]) -> bool { - if Application::PROOF_OF_WORK_BIT_DIFFICULTY == 0 { - return true; - } - hasher.reset(); - hasher.update(response); - let mut output = [0u8; NOISE_HASHLEN]; - hasher.finish(&mut output); - let n = u32::from_be_bytes(output[..4].try_into().unwrap()); - n.leading_zeros() >= Application::PROOF_OF_WORK_BIT_DIFFICULTY -} -#[inline(always)] -fn from_bytes_agreement( - public: &[u8], - private: &Application::KeyPair, - output: &mut [u8; P384_ECDH_SHARED_SECRET_SIZE], -) -> Option { - Application::PublicKey::from_bytes(public.try_into().unwrap()).and_then(|e| private.agree(&e, output).then_some(e)) -}