diff --git a/performance/.gitignore b/performance/.gitignore new file mode 100644 index 0000000..b3dabfd --- /dev/null +++ b/performance/.gitignore @@ -0,0 +1,4 @@ +/target +perf*.data +perf*.old +*.svg diff --git a/performance/Cargo.lock b/performance/Cargo.lock new file mode 100644 index 0000000..e1e0058 --- /dev/null +++ b/performance/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/performance/Cargo.toml b/performance/Cargo.toml new file mode 100644 index 0000000..c7ec4f9 --- /dev/null +++ b/performance/Cargo.toml @@ -0,0 +1,32 @@ +[package] +authors = ["ZeroTier, Inc. ", "Adam Ierymenko ", "Monica Moniot "] +edition = "2021" +license = "MPL-2.0" +name = "zssp" +version = "0.0.3" + +[lib] +name = "zssp" +path = "src/lib.rs" +doc = true + +[profile.bench] +debug = true + +[dependencies] +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/performance/README.md b/performance/README.md new file mode 100644 index 0000000..95f2984 --- /dev/null +++ b/performance/README.md @@ -0,0 +1,60 @@ +ZeroTier Secure Sessions Protocol +====== + +## Introduction + +ZeroTier Secure Socket Protocol (ZSSP) is a [Noise](http://noiseprotocol.org) protocol implementation using NIST/FIPS/CfSC compliant cryptographic primitives plus post-quantum forward secrecy via [Kyber1024](https://pq-crystals.org/kyber/). It also includes built-in support for fragmentation and defragmentation of large messages with strong resistance against denial of service attacks targeted against the fragmentation protocol. + +Specifically ZSSP implements the [Noise XK](http://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental) interactive handshake pattern which provides strong forward secrecy not only for data but for the identities of the two participants in the session. The XK pattern was chosen instead of the more popular IK pattern used in popular Noise implementations like Wireguard due to ZeroTier identities being long lived and potentially tied to the real world identity of the user. As a result a Noise pattern providing identity forward secrecy was considered preferable as it offers some level of deniability for recorded traffic even after secret key compromise. + +Hybrid post-quantum forward secrecy using Kyber1024 is performed alongside Noise with the result being mixed in alongside an optional pre-shared key at the end of session negotiation. + +ZSSP is designed for use in ZeroTier but is payload-agnostic and could easily be adapted for use in other projects. + +Further information can be found in the ZSSP whitepaper (pending official release). + +## Cryptographic Primitives Used + + - **NIST P-384 ECDH**: Elliptic curve key exchange during initial handshake and for periodic re-keying during the session + - **Kyber1024**: Quantum attack resistant lattice-based key exchange during initial handshake + - **SHA-512**: Used to construct KBKDF, also used in a proof of work and IP ownership DOS mitigation scheme + - **KBKDF**: Key mixing, sub-key derivation + - **AES-256**: 128-bit PRP for AES-256-GCM and for authenticated encryption of header to harden fragmentation against DOS (see section on header protection) + - **AES-256-GCM**: Authenticated encryption + +## Security Properties + +| | Persistent ZSSP | Opportunistic ZSSP| WireGuard | ZeroTier Legacy Transport | +| --- | --- | --- | --- | --- | +|**Construction**|Noise\_XKhfs+psk2|Noise\_XKhfs+psk2|Noise\_IKpsk2|Static Diffie-Helman| +|**Perfect Forward Secrecy**|Yes|Yes|Yes|No| +|**Forward Secret Identity Hiding**|Yes|Yes|No|No| +|**Quantum Forward Secret**|Yes|Yes|No|No| +|**Ratcheted Forward Secrecy**|Yes|Yes|No|No| +|**Silence is a Virtue**|Yes|No|Yes|No| +|**Key-Compromise Impersonation**|Resistant|Resistant|Resistant|Vulnerable| +|**Compromise-and-Impersonate**|Resistant|Detectable|Vulnerable|Vulnerable| +|**Single Key-Compromise MitM**|Resistant|Resistant|Resistant|Vulnerable| +|**Double Key-Compromise MitM**|Resistant|Detectable|Vulnerable|Vulnerable| +|**DOS Mitigation**|Yes|Yes|Yes|No| +|**Supports Fragmentation**|Yes|Yes|No|Yes| +|**FIPS Compliant**|Yes|Yes|No|No| +|**Small Code Footprint**|Yes|Yes|Yes|No| +|**RTT**|2|2|1|1| + +### Definitions + +* **Construction**: The mathematical construction the protocol is based upon. +* **Perfect Forward Secrecy**: An attacker with the static private keys of both party cannot decrypt recordings of messages sent between those parties. +* **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. +* **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. +* **RTT**: "Round-Trip-Time" - How many round trips from initiator to responder it takes to establish a session. diff --git a/performance/examples/basic_test.rs b/performance/examples/basic_test.rs new file mode 100644 index 0000000..1c5534c --- /dev/null +++ b/performance/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/performance/examples/benchmark.rs b/performance/examples/benchmark.rs new file mode 100644 index 0000000..c570df9 --- /dev/null +++ b/performance/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/performance/rustfmt.toml b/performance/rustfmt.toml new file mode 100644 index 0000000..9c9fedd --- /dev/null +++ b/performance/rustfmt.toml @@ -0,0 +1,8 @@ +max_width = 120 +edition = "2021" +newline_style = "Unix" +struct_lit_width = 60 +tab_spaces = 4 +use_small_heuristics = "Default" +single_line_if_else_max_width = 0 +use_try_shorthand = true diff --git a/performance/src/antireplay.rs b/performance/src/antireplay.rs new file mode 100644 index 0000000..81bc5a0 --- /dev/null +++ b/performance/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/performance/src/application.rs b/performance/src/application.rs new file mode 100644 index 0000000..3246920 --- /dev/null +++ b/performance/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/performance/src/challenge.rs b/performance/src/challenge.rs new file mode 100644 index 0000000..09b14d4 --- /dev/null +++ b/performance/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/performance/src/crypto/aes.rs b/performance/src/crypto/aes.rs new file mode 100644 index 0000000..7921c30 --- /dev/null +++ b/performance/src/crypto/aes.rs @@ -0,0 +1,88 @@ +// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. + +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; + +/// 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; + + /// 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]); +} + +/// 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; + + /// 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/performance/src/crypto/kyber1024.rs b/performance/src/crypto/kyber1024.rs new file mode 100644 index 0000000..74c9571 --- /dev/null +++ b/performance/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/performance/src/crypto/mod.rs b/performance/src/crypto/mod.rs new file mode 100644 index 0000000..2b6ea78 --- /dev/null +++ b/performance/src/crypto/mod.rs @@ -0,0 +1,31 @@ +mod aes; +pub use self::aes::*; + +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 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/performance/src/crypto/p384.rs b/performance/src/crypto/p384.rs new file mode 100644 index 0000000..67f780a --- /dev/null +++ b/performance/src/crypto/p384.rs @@ -0,0 +1,46 @@ +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 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. + /// + /// 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. +/// +/// Instances must securely delete the private key when dropped. +pub trait P384KeyPair { + /// The `PublicKeyP384` implementation which matches this `KeyPairP384` implementation. + type PublicKey: P384PublicKey; + /// 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. + /// + /// 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, 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/performance/src/crypto/sha512.rs b/performance/src/crypto/sha512.rs new file mode 100644 index 0000000..4e064d5 --- /dev/null +++ b/performance/src/crypto/sha512.rs @@ -0,0 +1,28 @@ +// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md. + +pub const SHA512_HASH_SIZE: usize = 64; + +/// A SHA-512 implementation. +pub trait Sha512Hash { + /// Create a new instance of SHA-512 for streaming data to. + fn new() -> Self; + /// 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 Sha512Hmac { + /// Allocate space on the stack or heap for repeated Hmac invocations. + /// + /// 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; + /// 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/performance/src/crypto_impl/kyber1024.rs b/performance/src/crypto_impl/kyber1024.rs new file mode 100644 index 0000000..ef553c2 --- /dev/null +++ b/performance/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/performance/src/crypto_impl/mod.rs b/performance/src/crypto_impl/mod.rs new file mode 100644 index 0000000..8aa0ceb --- /dev/null +++ b/performance/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/performance/src/crypto_impl/openssl.rs b/performance/src/crypto_impl/openssl.rs new file mode 100644 index 0000000..8a73e92 --- /dev/null +++ b/performance/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/performance/src/crypto_impl/p384_impl.rs b/performance/src/crypto_impl/p384_impl.rs new file mode 100644 index 0000000..17bfb1b --- /dev/null +++ b/performance/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/performance/src/crypto_impl/sha512.rs b/performance/src/crypto_impl/sha512.rs new file mode 100644 index 0000000..1a1bd4d --- /dev/null +++ b/performance/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/performance/src/frag_cache.rs b/performance/src/frag_cache.rs new file mode 100644 index 0000000..5fdd804 --- /dev/null +++ b/performance/src/frag_cache.rs @@ -0,0 +1,331 @@ +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}; + +struct PacketMetadata { + key: u64, + frags_idx: u32, + fragment_have: u64, + fragment_count: u8, + packet_size: u32, + creation_time: i64, +} + +pub(crate) struct UnassociatedFragCache { + dos_salt: RandomState, + frags_first_unused: usize, + frags_unused_size: usize, + map: [PacketMetadata; MAX_UNASSOCIATED_PACKETS], + frags: [MaybeUninit; MAX_UNASSOCIATED_FRAGMENTS], + map_idx: [u32; MAX_UNASSOCIATED_FRAGMENTS], +} +/// A combination of a hash table cache and a ring buffer for unassociated fragments. +/// Designed specifically to be extremely DDOS resistant. +/// This datastructure takes raw unauthenticated fragments straight from the network. +impl UnassociatedFragCache { + pub(crate) fn new() -> Self { + Self { + dos_salt: RandomState::new(), + frags_first_unused: 0, + frags_unused_size: MAX_UNASSOCIATED_FRAGMENTS, + map: std::array::from_fn(|_| PacketMetadata { + key: 0, + frags_idx: 0, + fragment_have: 0, + fragment_count: 0, + packet_size: 0, + creation_time: 0, + }), + frags: std::array::from_fn(|_| MaybeUninit::zeroed()), + map_idx: std::array::from_fn(|_| u32::MAX), + } + } + /// Add a fragment and return an assembled packet container if all fragments have been received. + /// Will check that aad is the same for all fragments. + pub(crate) fn assemble( + &mut self, + nonce: &[u8; AES_GCM_NONCE_SIZE], + remote_address: impl Hash, + fragment_size: usize, + fragment: Fragment, + 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 > 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); + let mut key = hasher.finish(); + if key == 0 { + key = 1; + } + + let map_len = self.map.len(); + let idx0 = (key as usize) % map_len; + let mut idx1 = (key as usize) / map_len % (map_len - 1); + if idx0 == idx1 { + idx1 = map_len - 1; + } + + // Open hash lookup of just 2 slots. + // To DOS, an adversary would either need to volumetrically spam the defrag table to keep most slots full + // or replay Alice's packet header from a spoofed physical path before Alice's packet is fully processed. + // Volumetric spam is quite difficult since without the `dos_salt` value an adversary cannot + // control which slots their fragments index to. And since Alice's packet header has a randomly + // generated counter value replaying it in time requires extreme amounts of network control. + let idx = if self.map[idx0].key == key { + idx0 + } else if self.map[idx1].key == key { + idx1 + } 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_interval, current_time); + } + if self.map[idx0].key == 0 { + idx0 + } else { + idx1 + } + } else { + // No room for a new entry so attempt to expire a bunch of entries. + self.check_for_expiry(timeout_interval, current_time); + if self.map[idx0].key == 0 { + idx0 + } else if self.map[idx1].key == 0 { + idx1 + } else { + // Give up and drop the fragment. + return; + } + }; + + if self.map[idx].key == 0 { + // This is a new entry so initialize it. + if (fragment_count as usize) <= self.frags_unused_size { + 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 u8; + entry.packet_size = 0; + entry.creation_time = current_time; + + for _ in 0..(entry.fragment_count as usize) { + self.map_idx[self.frags_first_unused] = idx as u32; + self.frags_first_unused = (self.frags_first_unused + 1) % self.frags.len(); + self.frags_unused_size -= 1; + } + } else { + // If there are not enough free fragment slots by this point we just drop the fragment. + return; + } + } + 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 usize + && new_size <= MAX_UNASSOCIATED_PACKET_SIZE as u32 + { + entry.packet_size = new_size; + entry.fragment_have |= got; + + let frag_idx = (entry.frags_idx as usize + fragment_no as usize) % self.frags.len(); + self.frags[frag_idx].write(fragment); + + if entry.fragment_have == 1u64.wrapping_shl(fragment_count as u32) - 1 { + debug_assert!(ret_assembled.is_empty()); + let start_idx = entry.frags_idx as usize; + 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); + } + } + } + pub(crate) fn check_for_expiry(&mut self, timeout: i64, current_time: i64) { + while self.frags_unused_size < self.frags.len() { + // Check if we can drop the entry at the start of the ring buffer. + let frag_idx = (self.frags_first_unused + self.frags_unused_size) % self.frags.len(); + let map_idx = self.map_idx[frag_idx] as usize; + debug_assert!(map_idx < self.map.len()); + + let entry = &mut self.map[map_idx]; + if entry.creation_time + timeout < current_time { + self.invalidate::(map_idx); + } else { + break; + } + } + } + + fn invalidate(&mut self, idx: usize) { + let entry = &mut self.map[idx]; + let start_idx = entry.frags_idx as usize; + for fragment_no in 0..(entry.fragment_count as usize) { + let frag_idx = (start_idx + fragment_no) % self.frags.len(); + self.map_idx[frag_idx] = u32::MAX; + // DROP is only false when we have moved the fragments out of this entry, and so we can't free them + // Otherwise we need to manually drop all of the fragments that this entry owns. + if DROP && entry.fragment_have & 1u64.wrapping_shl(fragment_no as u32) > 0 { + unsafe { self.frags[frag_idx].assume_init_drop() }; + } + } + entry.key = 0; + entry.frags_idx = 0; + entry.fragment_have = 0; + entry.fragment_count = 0; + entry.packet_size = 0; + entry.creation_time = 0; + let mut frags_first_used = (self.frags_first_unused + self.frags_unused_size) % self.frags.len(); + if frags_first_used == start_idx { + // `frags_unused_size` is pointing to the slot we just emptied. + // Move `frags_unused_size` to point at the first non-empty slot. + while self.frags_unused_size < self.frags.len() { + if self.map_idx[frags_first_used] == u32::MAX { + self.frags_unused_size += 1; + frags_first_used = (self.frags_first_unused + self.frags_unused_size) % self.frags.len(); + } else { + break; + } + } + } + } +} +impl Drop for UnassociatedFragCache { + fn drop(&mut self) { + for i in 0..self.map.len() { + if self.map[i].key != 0 { + self.invalidate::(i); + } + } + } +} + +#[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(); + + let mut time = 1; + let mut in_progress = Vec::new(); + let mut in_progress_fragments = 0; + // A basic fuzzer for testing the cache. + for i in 0..5000u32 { + 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 { + packet.push((j as u8, vec![0, 1, 2, 3, 4, 5, 6, r])); + in_progress_fragments += 1; + } + in_progress.push((i, fragment_count as u8, packet)); + } else { + 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; 12]; + nonce[..4].copy_from_slice(&i.to_be_bytes()); + 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" + ); + for j in 0..fragment_count { + assert_eq!(assembled.as_ref()[j][7], r, "Cache returned a corrupted packet"); + } + } else { + assert!(assembled.is_empty(), "Cache returned an incomplete packet"); + } + } + if r > 200 { + if in_progress.len() > 0 { + 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(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.clear(); + let mut nonce = [0; 12]; + nonce[..4].copy_from_slice(&id.to_be_bytes()); + cache.assemble( + &nonce, + 0, + fragment.len(), + fragment, + no as usize, + fragment_count as usize, + 1000, + time, + &mut assembled, + ); + time += 1; + in_progress_fragments -= 1; + + if packet.len() > 0 { + assert!(assembled.is_empty(), "Cache returned an incomplete packet"); + } + } + if packet.len() > 0 { + in_progress.push((id, fragment_count, packet)); + } + } + } + } + } +} diff --git a/performance/src/fragged.rs b/performance/src/fragged.rs new file mode 100644 index 0000000..b3c293a --- /dev/null +++ b/performance/src/fragged.rs @@ -0,0 +1,97 @@ +use arrayvec::ArrayVec; +use std::mem::{needs_drop, zeroed, MaybeUninit}; + +use crate::crypto::AES_GCM_NONCE_SIZE; +use crate::proto::{MAX_FRAGMENTS, NONCE_SIZE_DIFF}; + +pub type Assembled = ArrayVec; + +/// Fast packet defragmenter +pub struct Fragged { + nonce: [u8; 10], + count: u8, + have: u64, + size: usize, + frags: [MaybeUninit; MAX_FRAGMENTS], +} + +impl Fragged { + pub fn new() -> Self { + debug_assert!(MAX_FRAGMENTS <= 64); + unsafe { zeroed() } + } + + /// Add a fragment and return an assembled packet container if all fragments have been received. + /// + /// When a fully assembled packet is returned the internal state is reset and this object can + /// be reused to assemble another packet. + /// + /// Will check that aad is the same for all fragments. + pub(crate) fn assemble( + &mut self, + nonce: &[u8; AES_GCM_NONCE_SIZE], + fragment: Fragment, + fragment_no: usize, + fragment_count: usize, + ret_assembled: &mut Assembled, + ) { + 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 u8; + self.nonce = nonce; + self.size = 0; + } + + let got = 1u64.wrapping_shl(fragment_no as u32); + 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 as u32) - 1 { + self.have = 0; + self.count = 0; + self.nonce = [0; 10]; + self.size = 0; + // 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. + unsafe { + for i in 0..fragment_count { + ret_assembled.push(self.frags[i].assume_init_read()); + } + } + } + } + } + } + + /// Drops any remaining fragments and resets this object. + pub fn drop_in_place(&mut self) { + if needs_drop::() { + let mut have = self.have; + let mut i = 0; + while have != 0 { + if (have & 1) != 0 { + debug_assert!(i < MAX_FRAGMENTS); + unsafe { self.frags.get_unchecked_mut(i).assume_init_drop() }; + } + have = have.wrapping_shr(1); + i += 1; + } + } + self.have = 0; + self.count = 0; + self.nonce = [0; 10]; + self.size = 0; + } +} + +impl Drop for Fragged { + fn drop(&mut self) { + self.drop_in_place(); + } +} diff --git a/performance/src/handshake_cache.rs b/performance/src/handshake_cache.rs new file mode 100644 index 0000000..1425a9b --- /dev/null +++ b/performance/src/handshake_cache.rs @@ -0,0 +1,93 @@ +use std::num::NonZeroU32; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, RwLock}; + +use crate::zeta::StateB2; +use crate::{application::CryptoLayer, proto::MAX_UNASSOCIATED_HANDSHAKE_STATES}; + +pub(crate) struct UnassociatedHandshakeCache { + has_pending: AtomicBool, // Allowed to be falsely positive + cache: RwLock>, +} +/// SoA format +struct CacheInner { + local_ids: [Option; MAX_UNASSOCIATED_HANDSHAKE_STATES], + timeouts: [i64; 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 { + pub(crate) fn new() -> Self { + Self { + has_pending: AtomicBool::new(false), + cache: RwLock::new(CacheInner { + local_ids: std::array::from_fn(|_| None), + timeouts: std::array::from_fn(|_| 0), + handshakes: std::array::from_fn(|_| None), + }), + } + } + 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) { + return cache.handshakes[i].clone(); + } + } + None + } + 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() { + if cache.local_ids[i].is_none() || cache.timeouts[i] < current_time { + idx = i; + break; + } else if cache.local_ids[i] == Some(local_id) { + return; + } + } + cache.local_ids[idx] = Some(local_id); + cache.timeouts[idx] = current_time + Application::SETTINGS.fragment_assembly_timeout as i64; + cache.handshakes[idx] = Some(state); + self.has_pending.store(true, Ordering::Release); + } + pub(crate) fn remove(&self, local_id: NonZeroU32) -> bool { + let mut cache = self.cache.write().unwrap(); + for (i, id) in cache.local_ids.iter().enumerate() { + if *id == Some(local_id) { + cache.local_ids[i] = None; + cache.timeouts[i] = 0; + cache.handshakes[i] = None; + return true; + } + } + false + } + pub(crate) fn service(&self, current_time: i64) { + // Only check for expiration if we have a pending packet. + // This check is allowed to have false positives for simplicity's sake. + if self.has_pending.swap(false, Ordering::Acquire) { + // Check for packet expiration + let mut cache = self.cache.write().unwrap(); + let mut has_pending = false; + for i in 0..cache.local_ids.len() { + if cache.local_ids[i].is_some() { + if cache.timeouts[i] < current_time { + cache.local_ids[i] = None; + cache.timeouts[i] = 0; + cache.handshakes[i] = None; + } else { + has_pending = true; + } + } + } + if has_pending { + self.has_pending.store(true, Ordering::Release); + } + } + } +} diff --git a/performance/src/indexed_heap.rs b/performance/src/indexed_heap.rs new file mode 100644 index 0000000..c83b8ca --- /dev/null +++ b/performance/src/indexed_heap.rs @@ -0,0 +1,212 @@ +#[derive(Eq, PartialEq, Hash, Clone, Copy)] +pub struct BinaryHeapIndex(usize, u64); + +const RESERVED_MARKER: u64 = 1; +const EMPTY_MARKER: u64 = 0; + +/// A simple Priority Queue built from a binary heap and a generational array. +/// Entries in the queue are accessed and updated by their generational index. +/// This allows for extremely simple memory management and fast queue updates. +pub struct IndexedBinaryHeap { + generation: u64, + free_list_head: usize, + data: Vec<(T, P, usize)>, + map: Vec<(usize, u64)>, +} + +#[allow(unused)] +impl IndexedBinaryHeap { + pub fn new() -> Self { + Self { + generation: 1, + free_list_head: usize::MAX, + data: Vec::new(), + map: Vec::new(), + } + } + pub fn with_capacity(capacity: usize) -> Self { + Self { + generation: 1, + free_list_head: usize::MAX, + data: Vec::with_capacity(capacity), + map: Vec::with_capacity(capacity), + } + } + pub fn peek(&self) -> Option<(&T, &P, BinaryHeapIndex)> { + self.data + .first() + .map(|entry| (&entry.0, &entry.1, BinaryHeapIndex(entry.2, self.map[entry.2].1))) + } + pub fn peek_mut(&mut self) -> Option<(&mut T, &P, BinaryHeapIndex)> { + self.data + .first_mut() + .map(|entry| (&mut entry.0, &entry.1, BinaryHeapIndex(entry.2, self.map[entry.2].1))) + } + fn swap(&mut self, a: usize, b: usize) { + self.map[self.data[a].2].0 = b; + self.map[self.data[b].2].0 = a; + self.data.swap(a, b); + } + fn bubble_down(&mut self, mut parent_idx: usize) { + loop { + 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 + { + child1_idx + } else { + child0_idx + }; + if self.data[largest_child].1 > self.data[parent_idx].1 { + self.swap(parent_idx, largest_child); + parent_idx = largest_child; + } else { + break; + } + } else { + break; + } + } + } + fn bubble_up(&mut self, mut child_idx: usize) { + while child_idx > 0 { + let parent_idx = (child_idx - 1) / 2; + if self.data[child_idx].1 > self.data[parent_idx].1 { + self.swap(parent_idx, child_idx); + child_idx = parent_idx; + } else { + break; + } + } + } + fn remove_idx(&mut self, data_idx: usize) -> (T, P) { + self.swap(data_idx, self.data.len() - 1); + let ret = self.data.pop().unwrap(); + self.map[ret.2] = (self.free_list_head, EMPTY_MARKER); + self.free_list_head = ret.2; + + self.bubble_down(data_idx); + + (ret.0, ret.1) + } + fn deref_index(&self, idx: BinaryHeapIndex) -> Option { + (idx.0 < self.map.len() && self.map[idx.0].1 == idx.1).then(|| self.map[idx.0].0) + } + pub fn pop(&mut self) -> Option<(T, P)> { + (!self.data.is_empty()).then(|| self.remove_idx(0)) + } + /// Add an item to the queue and get back a generational index which allows for quick updating + /// of this item and its priority. + pub fn push(&mut self, item: T, priority: P) -> BinaryHeapIndex { + let idx = self.reserve_index(); + self.push_reserved(idx, item, priority); + idx + } + /// Reserve a generational index. It will not have an associated item until + /// `push_reserved` is called. + /// If the index is dropped without having been associated with a queue item it will be leaked, + /// causing the queue to consume a few bytes more of memory than it should. + pub fn reserve_index(&mut self) -> BinaryHeapIndex { + self.generation += 1; + if self.free_list_head != usize::MAX { + let pre_head = self.free_list_head; + self.free_list_head = self.map[pre_head].0; + self.map[pre_head] = (0, RESERVED_MARKER); + BinaryHeapIndex(pre_head, self.generation) + } else { + self.map.push((0, RESERVED_MARKER)); + BinaryHeapIndex(self.map.len() - 1, self.generation) + } + } + /// Add an item to the queue with the specific reserved index. + /// If this index already has an associated item this will return false. + pub fn push_reserved(&mut self, idx: BinaryHeapIndex, item: T, priority: P) -> bool { + if idx.0 < self.map.len() && self.map[idx.0].1 == RESERVED_MARKER { + let data_idx = self.data.len(); + self.map[idx.0] = (data_idx, idx.1); + + self.data.push((item, priority, idx.0)); + self.bubble_up(data_idx); + true + } else { + false + } + } + pub fn change_priority(&mut self, idx: BinaryHeapIndex, new_priority: P) -> Option

{ + self.deref_index(idx).map(|data_idx| { + let c = if self.data[data_idx].1 < new_priority { + 1 + } else if self.data[data_idx].1 > new_priority { + 2 + } else { + 3 + }; + let old_priority = std::mem::replace(&mut self.data[data_idx].1, new_priority); + if c == 1 { + self.bubble_up(data_idx); + } else if c == 2 { + self.bubble_down(data_idx); + } + old_priority + }) + } + pub fn change_item(&mut self, idx: BinaryHeapIndex, new_item: T) -> Option { + self.deref_index(idx) + .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)) + } + pub fn get_mut(&mut self, idx: BinaryHeapIndex) -> Option<(&mut T, &P)> { + self.deref_index(idx).map(|data_idx| { + let entry = &mut self.data[data_idx]; + (&mut entry.0, &entry.1) + }) + } + /// Remove this index and its associated item from the queue, returning the item if it exists. + /// This can also be used to remove reserved indices from the queue. + pub fn remove(&mut self, idx: BinaryHeapIndex) -> Option<(T, P)> { + if idx.0 < self.map.len() { + if self.map[idx.0].1 == RESERVED_MARKER { + self.map[idx.0] = (self.free_list_head, EMPTY_MARKER); + self.free_list_head = idx.0; + None + } else if self.map[idx.0].1 == idx.1 { + Some(self.remove_idx(self.map[idx.0].0)) + } else { + None + } + } else { + None + } + } + pub fn clear(&mut self) { + self.free_list_head = usize::MAX; + self.map.clear(); + self.data.clear(); + } +} + +#[test] +fn test() { + let mut queue = IndexedBinaryHeap::new(); + let r0 = queue.push(1234, 1234); + for i in 0..100 { + queue.push(2 * i, 2 * i); + } + let r1 = queue.push(1234, 12); + assert_eq!(queue.remove(r0), Some((1234, 1234))); + for i in (0..100).rev() { + queue.push(2 * i + 1, 2 * i + 1); + } + assert_eq!(queue.change_priority(r1, 1234), Some(12)); + assert_eq!(queue.remove(r0), None); + let mut last = usize::MAX; + while let Some((i, j)) = queue.pop() { + assert_eq!(i, j); + assert!(i <= last); + last = i; + } +} diff --git a/performance/src/lib.rs b/performance/src/lib.rs new file mode 100644 index 0000000..caf7a4d --- /dev/null +++ b/performance/src/lib.rs @@ -0,0 +1,29 @@ +/* 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/ + */ +pub mod crypto; +pub mod crypto_impl; + +mod antireplay; +mod challenge; +mod frag_cache; +mod fragged; +mod handshake_cache; +mod indexed_heap; +mod log_event; +mod ratchet_state; +mod symmetric_state; +mod zeta; +mod zssp; + +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/performance/src/log_event.rs b/performance/src/log_event.rs new file mode 100644 index 0000000..3671abd --- /dev/null +++ b/performance/src/log_event.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; + +use crate::application::CryptoLayer; +use crate::zeta::Session; + +/// ZSSP events that might be interesting to log or aggregate into metrics. +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, Crypto: CryptoLayer> std::fmt::Debug for LogEvent<'a, Crypto> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + 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/performance/src/proto.rs b/performance/src/proto.rs new file mode 100644 index 0000000..da39de7 --- /dev/null +++ b/performance/src/proto.rs @@ -0,0 +1,182 @@ +use crate::crypto::*; + +/* 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(crate) const KID_SIZE: usize = 4; + +/* Challenge protocol constants */ + +pub(crate) const SALT_SIZE: usize = 32; + +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 CHALLENGE_SIZE: usize = COUNTER_SIZE + MAC_SIZE + POW_SIZE; +pub(crate) const DIFFICULTY: u32 = 13; + +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 PACKET_NONCE_SIZE: usize = 10; + +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; + +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; + +pub(crate) const NONCE_SIZE_DIFF: usize = AES_GCM_NONCE_SIZE - PACKET_NONCE_SIZE; + +/* Key exchange constants */ +/* +XKhfs+psk2: + <- s + ... + -> e, es, e1 + <- e, ee, ekem1, psk + -> s, se +*/ +/* +KKpsk0: + -> s + <- s + ... + -> psk, e, es, ss + <- e, ee, se +*/ +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; + +/// 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"; + +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(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; + +/* Packet constants */ + +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; + +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; + +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; + +pub(crate) const HEADERED_HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE + HEADER_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; + +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; + +pub(crate) const HEADERED_HANDSHAKE_COMPLETION_MAX_SIZE: usize = HANDSHAKE_COMPLETION_MAX_SIZE + HEADER_SIZE; + +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/performance/src/ratchet_state.rs b/performance/src/ratchet_state.rs new file mode 100644 index 0000000..72a4ca9 --- /dev/null +++ b/performance/src/ratchet_state.rs @@ -0,0 +1,165 @@ +use arrayvec::ArrayVec; +use zeroize::Zeroizing; + +use crate::crypto::*; +use crate::proto::*; +/// A ratchet key and fingerprint, +/// along with the length of the ratchet chain the keys were derived from. +/// +/// 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/performance/src/result.rs b/performance/src/result.rs new file mode 100644 index 0000000..126b903 --- /dev/null +++ b/performance/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/performance/src/symmetric_state.rs b/performance/src/symmetric_state.rs new file mode 100644 index 0000000..c9ea5a8 --- /dev/null +++ b/performance/src/symmetric_state.rs @@ -0,0 +1,216 @@ +use std::marker::PhantomData; + +use zeroize::Zeroizing; + +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 { + /// 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 + /// `HKDF(self.chaining_key, input_key_material)` but this is how NIST rolls. + /// These are the values we have assigned to the 4 variables involved in their KDF: + /// * K_IN = `input_key_material` + /// * Label = `label` + /// * Context = `self.chaining_key` + /// * L = `num_outputs*512u16` + /// We have intentionally made every input small and fixed size to avoid unnecessary complexity + /// and data representation ambiguity. + /// Corresponds to Noise `HKDF`. + fn kbkdf( + &self, + hmac: &mut Crypto::Hmac, + input_key_material: &[u8], + label: &[u8; 4], + num_outputs: u16, + output1: &mut [u8; HASHLEN], + output2: Option<&mut [u8; HASHLEN]>, + output3: Option<&mut [u8; HASHLEN]>, + ) { + 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); + + if let Some(output2) = output2 { + debug_assert!(num_outputs >= 2); + buffer[0] = 2; + hmac.hash(input_key_material, buffer.as_ref(), output2); + } + + if let Some(output3) = 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/performance/src/zeta.rs b/performance/src/zeta.rs new file mode 100644 index 0000000..5ff2992 --- /dev/null +++ b/performance/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: &Session, + 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/performance/src/zssp.rs b/performance/src/zssp.rs new file mode 100644 index 0000000..ad55015 --- /dev/null +++ b/performance/src/zssp.rs @@ -0,0 +1,659 @@ +use std::cmp::Reverse; +use std::collections::HashMap; +use std::hash::Hash; +use std::io::Write; +use std::num::NonZeroU32; +use std::sync::{Arc, Mutex, RwLock, Weak}; + +use arrayvec::ArrayVec; +use rand_core::RngCore; + +use crate::application::*; +use crate::challenge::ChallengeContext; +use crate::crypto::*; +use crate::frag_cache::UnassociatedFragCache; +use crate::fragged::Assembled; +use crate::handshake_cache::UnassociatedHandshakeCache; +use crate::indexed_heap::IndexedBinaryHeap; +use crate::proto::*; +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. +/// +/// Each application using ZSSP must create an instance of this to own sessions and +/// defragment incoming packets that are not yet associated with a session. +/// +/// 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 { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +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` + 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, +} + +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)) +} + +/// Fragments and sends the packet, destroying it in the process. +/// +/// 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; + + let mut header: [u8; HEADER_SIZE] = headered_packet[..HEADER_SIZE].try_into().unwrap(); + header[FRAGMENT_COUNT_IDX] = fragment_count as u8; + + 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]; + + fragment[..HEADER_SIZE].copy_from_slice(&header); + fragment[FRAGMENT_NO_IDX] = fragment_no as u8; + + 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 { + /// Create a new session context. + pub fn new(static_secret_key: Crypto::KeyPair, mut rng: Crypto::Rng) -> Self { + let challenge = ChallengeContext::new(&mut rng); + Self(Arc::new(ContextInner { + 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(), + })) + } + + /// 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 + /// static public blob (as retrieved from the application layer) exceed MAX_INIT_PAYLOAD_SIZE. + /// + /// * `app` - Application layer instance + /// * `send` - Function to be called to send one or more initial packets to the remote being + /// contacted + /// * `mtu` - MTU for initial packets + /// * `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 + /// * `identity` - Payload to be sent to Bob that contains the information necessary + /// for the upper protocol to authenticate and approve of Alice's identity. + pub fn open>( + &self, + app: App, + send: impl FnMut(&mut [u8]) -> bool, + mut mtu: usize, + static_remote_key: Crypto::PublicKey, + session_data: Crypto::SessionData, + identity: &[u8], + ) -> Result>, OpenError> { + mtu = mtu.max(MIN_TRANSPORT_MTU); + 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. + /// + /// The check_allow_incoming_session function is called when an initial Noise_XK init message is + /// received. This is before anything is known about the caller. A return value of true proceeds + /// with negotiation. False drops the packet and ignores the inbound attempt. + /// + /// 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 ReceiveOk being returned. + /// + /// * `app` - Interface to application using ZSSP + /// * `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 + /// * `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, + 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)>, + remote_address: &impl Hash, + 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_fragment: &mut [u8] = incoming_fragment_buf.as_mut(); + if incoming_fragment.len() < MIN_PACKET_SIZE { + return Err(fault!(FaultType::InvalidPacket, false)); + } + + let mut fragment_buffer = Assembled::new(); + + 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) + ); + } + + { + //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.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(fault!(ExpiredCounter, false)); + } + } 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(fault!(InvalidPacket, false)); + } else { + return Err(fault!(InvalidPacket, true)); + } + } + + // 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( + &nonce, + incoming_fragment_buf, + fragment_no, + fragment_count, + &mut fragment_buffer, + ); + 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. + fragment_buffer.as_mut() + } + } else { + std::slice::from_mut(&mut incoming_fragment_buf) + }; + + receive_payload_in_place(&session, state, kid_recv, &nonce, fragments, output_buffer)?; + + SessionEvent::Data + } else { + 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, + ); + 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))?; + } + // We have not yet authenticated the sender so we do not report + // receiving a packet from them. + buffer.as_mut() + } + } else { + &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 { + &mut incoming_fragment_buf.as_mut()[HEADER_SIZE..] + }; + + if packet_type == PACKET_TYPE_HANDSHAKE_HELLO { + log!(app, ReceivedRawX1); + + if !(HANDSHAKE_HELLO_CHALLENGE_MIN_SIZE..=HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE) + .contains(&assembled_packet.len()) + { + 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)) + } + } + } + /// Send data over the session. + /// + /// * `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` + /// * `mtu_sized_buffer` - A writable work buffer whose size equals the MTU + /// * `data` - Data to send + pub fn send( + &self, + session: &Session, + send: impl FnMut(&mut [u8]) -> bool, + mtu_sized_buffer: &mut [u8], + data: &[u8], + ) -> Result<(), SendError> { + send_payload(&self.0, session, data, send, mtu_sized_buffer) + } + + /// 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; + } + let session = match session.upgrade() { + Some(s) => s, + _ => { + session_queue.remove(queue_idx); + continue; + } + }; + 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 Some(next_timer) = result { + next_service_time = next_service_time.min(next_timer); + session_queue.change_priority(queue_idx, Reverse(next_timer)); + } else { + session.expire_inner(Some(ctx), Some(&mut session_queue)); + } + } + drop(session_queue); + + 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); + + next_service_time - current_time + } +}