diff --git a/Cargo.lock b/Cargo.lock index f9170ea..c58728d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,12 +43,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - [[package]] name = "block-buffer" version = "0.10.4" @@ -108,7 +102,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", "typenum", ] @@ -128,7 +121,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" dependencies = [ "const-oid", - "pem-rfc7468", "zeroize", ] @@ -139,25 +131,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] -[[package]] -name = "ecdsa" -version = "0.16.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - [[package]] name = "elliptic-curve" version = "0.13.5" @@ -171,8 +148,6 @@ dependencies = [ "generic-array", "group", "hkdf", - "pem-rfc7468", - "pkcs8", "rand_core", "sec1", "subtle", @@ -277,29 +252,8 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" dependencies = [ - "ecdsa", "elliptic-curve", "primeorder", - "sha2", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", ] [[package]] @@ -341,16 +295,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - [[package]] name = "sec1" version = "0.7.3" @@ -360,7 +304,6 @@ dependencies = [ "base16ct", "der", "generic-array", - "pkcs8", "subtle", "zeroize", ] @@ -376,26 +319,6 @@ dependencies = [ "digest", ] -[[package]] -name = "signature" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "spki" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" -dependencies = [ - "base64ct", - "der", -] - [[package]] name = "subtle" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index cd08fbc..e41d91d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,21 +11,24 @@ path = "src/lib.rs" doc = true [dependencies] -rand_core = "0.6.4" -zeroize = { version = "1.6.0", default-features = false} +rand_core = { version = "0.6.4" } +zeroize = { version = "1.6.0" } pqc_kyber = { version = "0.6.0", default-features = false, features = ["kyber1024", "std"], optional = true } -aes-gcm = { version = "0.10.2", optional = true} -aes = { version = "0.8.3", optional = true} -p384 = { version = "0.13.0", optional = true} -sha2 = { version = "0.10.7", optional = true} -hmac = { version = "0.12.1", optional = true} +aes-gcm = { version = "0.10.2", default-features = false, features = ["aes"], optional = true } +aes = { version = "0.8.3", default-features = false, 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 } [features] -default = ["debug", "aes-gcm", "p384", "sha2", "hmac", "pqc_kyber"] +default = ["debug", "aes-gcm", "p384", "hmac", "pqc_kyber"] aes = ["dep:aes"] aes-gcm = ["dep:aes-gcm", "aes"] sha2 = ["dep:sha2"] hmac = ["dep:hmac", "sha2"] logging = [] debug = ["logging"] + +[dev-dependencies] +rand_core = { version = "0.6.4", features = ["getrandom"] } diff --git a/README.md b/README.md index 95f2984..d164830 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ ZeroTier Secure Sessions Protocol ## Introduction +An in-depth guide to the full protocol specification can be found in the [protocol whitepaper](./whitepaper.pdf) provided in this repo. This implementation references it heavily. + 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. @@ -21,40 +23,3 @@ Further information can be found in the ZSSP whitepaper (pending official releas - **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/comparison.md b/comparison.md new file mode 100644 index 0000000..abcdd50 --- /dev/null +++ b/comparison.md @@ -0,0 +1,37 @@ + +## 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/examples/basic_test.rs b/examples/basic_test.rs index 6d98679..87335a1 100644 --- a/examples/basic_test.rs +++ b/examples/basic_test.rs @@ -6,34 +6,46 @@ * https://www.zerotier.com/ */ +use std::collections::HashMap; +use std::convert::Infallible; use std::iter::ExactSizeIterator; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::{mpsc, Arc}; use std::thread; use std::time::{Duration, Instant}; -use aes::Aes256; -use aes_gcm::Aes256Gcm; -use p384::{ecdh::EphemeralSecret, PublicKey}; use rand_core::OsRng; use rand_core::RngCore; -use sha2::Sha512; -use zssp_proto::crypto::secure_eq; -use zssp_proto::ratchet_state::RatchetState; -use zssp_proto::{Session, Settings, RATCHET_SIZE}; +use zssp_proto::application::{ + AcceptAction, ApplicationLayer, RatchetState, RatchetStates, RatchetUpdate, Settings, RATCHET_SIZE, CryptoLayer, +}; +use zssp_proto::crypto::P384KeyPair; +use zssp_proto::crypto_impl::{ + Aes256Crate, AesGcmCrate, RustKyber1024PrivateKey, P384CrateKeyPair, P384CratePublicKey, Sha512Crate, +}; +use zssp_proto::Session; const TEST_MTU: usize = 1500; struct TestApplication { time: Instant, name: &'static str, - ratchets: Mutex<[RatchetState; 2]>, + ratchets: Ratchets, } -#[allow(unused)] -impl zssp_proto::ApplicationLayer for &TestApplication { +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() } + } +} + +impl CryptoLayer for TestApplication { const SETTINGS: Settings = Settings { initial_offer_timeout: Settings::INITIAL_OFFER_TIMEOUT_MS, rekey_timeout: 60 * 1000, @@ -44,64 +56,76 @@ impl zssp_proto::ApplicationLayer for &TestApplication { fragment_assembly_timeout: Settings::FRAGMENT_ASSEMBLY_TIMEOUT_MS, }; type Rng = OsRng; - type Prp = Aes256; - type Aead = Aes256Gcm; - type Hash = Sha512; - type PublicKey = PublicKey; - type KeyPair = EphemeralSecret; - type Kem = [u8; pqc_kyber::KYBER_SECRETKEYBYTES]; + type Prp = Aes256Crate; + type Aead = AesGcmCrate; + type Hash = Sha512Crate; + type PublicKey = P384CratePublicKey; + type KeyPair = P384CrateKeyPair; + type Kem = RustKyber1024PrivateKey; - type DiskError = (); - type Data = (); + type SessionData = u128; +} +#[allow(unused)] +impl ApplicationLayer for &mut TestApplication { + type Crypto = TestApplication; + type StorageError = Infallible; + fn hello_requires_recognized_ratchet(&mut self) -> bool { + false + } - fn hello_requires_recognized_ratchet(&self) -> bool { + fn initiator_disallows_downgrade(&mut self, session: &Arc>) -> bool { true } - fn initiator_disallows_downgrade(&self, session: &Arc>) -> bool { - true - } - - fn check_accept_session(&self, remote_static_key: &Self::PublicKey, identity: &[u8]) -> (Option<(bool, Self::Data)>, bool) { - (Some((true, ())), true) - } - - fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE]) -> Result { - let ratchets = self.ratchets.lock().unwrap(); - for rs in ratchets.iter() { - if rs.nonempty().map_or(false, |rs| secure_eq(&rs.fingerprint, ratchet_fingerprint)) { - return Ok(rs.clone()); - } + fn check_accept_session(&mut self, remote_static_key: &P384CratePublicKey, identity: &[u8]) -> AcceptAction { + AcceptAction { + session_data: Some(1), + responder_disallows_downgrade: true, + responder_silently_rejects: false, } - Ok(RatchetState::Null) } - fn restore_by_identity(&self, remote_static_key: &Self::PublicKey, application_data: &Self::Data) -> Result<[RatchetState; 2], Self::DiskError> { - Ok(self.ratchets.lock().unwrap().clone()) + fn restore_by_fingerprint( + &mut self, + ratchet_fingerprint: &[u8; RATCHET_SIZE], + ) -> Result, Infallible> { + Ok(self.ratchets.rf_map.get(ratchet_fingerprint).cloned()) + } + + fn restore_by_identity( + &mut self, + remote_static_key: &P384CratePublicKey, + session_data: &u128, + ) -> Result, Infallible> { + Ok(self.ratchets.peer_map.get(session_data).cloned()) } fn save_ratchet_state( - &self, - remote_static_key: &Self::PublicKey, - application_data: &Self::Data, - pre_ratchet_states: [&RatchetState; 2], - new_ratchet_states: [&RatchetState; 2], - ) -> Result<(), Self::DiskError> { - let mut ratchets = self.ratchets.lock().unwrap(); - ratchets[0] = new_ratchet_states[0].clone(); - ratchets[1] = new_ratchet_states[1].clone(); - let chain_len = new_ratchet_states[0].chain_len(); - if chain_len > pre_ratchet_states[0].chain_len() { - println!("[{}] new ratchet #{}", self.name, chain_len); + &mut self, + remote_static_key: &P384CratePublicKey, + session_data: &u128, + update_data: RatchetUpdate<'_>, + ) -> Result<(), Infallible> { + self.ratchets.peer_map.insert(*session_data, update_data.to_states()); + + if let Some(rf) = update_data.added_fingerprint() { + self.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() { + self.ratchets.rf_map.remove(rf); + } + if let Some(rf) = update_data.deleted_fingerprint2() { + self.ratchets.rf_map.remove(rf); } Ok(()) } - fn time(&self) -> i64 { + fn time(&mut self) -> i64 { self.time.elapsed().as_millis() as i64 } - fn event_log(&self, event: zssp_proto::LogEvent) { + fn event_log(&mut self, event: zssp_proto::LogEvent) { println!(">[{}] {:?}", self.name, event); } } @@ -109,15 +133,15 @@ impl zssp_proto::ApplicationLayer for &TestApplication { fn alice_main( run: &AtomicBool, packet_success_rate: u32, - alice_app: &TestApplication, + mut alice_app: TestApplication, alice_out: mpsc::SyncSender>, alice_in: mpsc::Receiver>, recursive_out: mpsc::SyncSender>, - alice_keypair: EphemeralSecret, - bob_pubkey: PublicKey, + alice_keypair: P384CrateKeyPair, + bob_pubkey: P384CratePublicKey, ) { let startup_time = std::time::Instant::now(); - let context = zssp_proto::Context::<&TestApplication>::new(alice_keypair, OsRng); + let mut context = zssp_proto::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; @@ -128,7 +152,14 @@ fn alice_main( up = false; alice_session = Some( context - .open(alice_app, |b| alice_out.send(b).is_ok(), TEST_MTU, bob_pubkey.clone(), (), Vec::new()) + .open( + &mut alice_app, + |b| alice_out.send(b).is_ok(), + TEST_MTU, + bob_pubkey.clone(), + 0, + Vec::new(), + ) .unwrap(), ); println!("[alice] opening session"); @@ -142,7 +173,7 @@ fn alice_main( use zssp_proto::result::ReceiveOk::*; use zssp_proto::result::SessionEvent::*; match context.receive( - alice_app, + &mut alice_app, |b| alice_out.send(b).is_ok(), TEST_MTU, |_| Some((|b| alice_out.send(b).is_ok(), TEST_MTU)), @@ -160,9 +191,8 @@ fn alice_main( assert!(!data.is_empty()); //println!("[alice] received {}", data.len()); } - NewSession => panic!(), - Rejected => panic!(), Control => (), + _ => panic!(), }, Err(e) => { println!("[alice] ERROR {:?}", e); @@ -192,12 +222,13 @@ fn alice_main( 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.0000025) as u32 { + if OsRng.next_u32() <= ((u32::MAX as f64) * 0.000005) as u32 { alice_session = None; } if current_time >= next_service { - next_service = current_time + context.service(alice_app, |_| Some((|b| alice_out.send(b).is_ok(), TEST_MTU))); + next_service = + current_time + context.service(&mut alice_app, |_| Some((|b| alice_out.send(b).is_ok(), TEST_MTU))); } } } @@ -205,14 +236,14 @@ fn alice_main( fn bob_main( run: &AtomicBool, packet_success_rate: u32, - bob_app: &TestApplication, + mut bob_app: TestApplication, bob_out: mpsc::SyncSender>, bob_in: mpsc::Receiver>, recursive_out: mpsc::SyncSender>, - bob_keypair: EphemeralSecret, + bob_keypair: P384CrateKeyPair, ) { let startup_time = std::time::Instant::now(); - let context = zssp_proto::Context::<&TestApplication>::new(bob_keypair, OsRng); + let mut context = zssp_proto::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; @@ -229,7 +260,7 @@ fn bob_main( use zssp_proto::result::ReceiveOk::*; use zssp_proto::result::SessionEvent::*; match context.receive( - bob_app, + &mut bob_app, |b| bob_out.send(b).is_ok(), TEST_MTU, |_| Some((|b| bob_out.send(b).is_ok(), TEST_MTU)), @@ -238,7 +269,7 @@ fn bob_main( ) { Ok(Unassociated) => {} Ok(Session(s, event)) => match event { - NewSession => { + NewSession | NewDowngradedSession => { println!("[bob] new session, took {}s", current_time as f32 / 1000.0); let _ = bob_session.replace(s); } @@ -248,9 +279,8 @@ fn bob_main( transferred += data.len() as u64 * 2; // *2 because we are also sending this many bytes back context.send(&s, |b| bob_out.send(b).is_ok(), TEST_MTU, data).unwrap(); } - Established => panic!(), - Rejected => panic!(), Control => (), + _ => panic!(), }, Err(e) => { println!("[bob] ERROR {:?}", e); @@ -275,7 +305,7 @@ fn bob_main( } if current_time >= next_service { - next_service = current_time + context.service(bob_app, |_| Some((|b| bob_out.send(b).is_ok(), TEST_MTU))); + next_service = current_time + context.service(&mut bob_app, |_| Some((|b| bob_out.send(b).is_ok(), TEST_MTU))); } } } @@ -283,19 +313,18 @@ fn bob_main( fn core(time: u64, packet_success_rate: u32) { let run = &AtomicBool::new(true); - let shared_ratchet_states = RatchetState::new_from_otp::(b"password1"); - let alice_keypair = EphemeralSecret::random(&mut OsRng); + let alice_keypair = P384CrateKeyPair::generate(&mut OsRng); let alice_app = TestApplication { time: Instant::now(), name: "alice", - ratchets: Mutex::new(shared_ratchet_states.clone()), + ratchets: Ratchets::new(), }; - let bob_keypair = EphemeralSecret::random(&mut OsRng); + let bob_keypair = P384CrateKeyPair::generate(&mut OsRng); let bob_pubkey = bob_keypair.public_key(); let bob_app = TestApplication { time: Instant::now(), name: "bob", - ratchets: Mutex::new(shared_ratchet_states), + ratchets: Ratchets::new(), }; let (alice_out, bob_in) = mpsc::sync_channel::>(256); @@ -309,7 +338,7 @@ fn core(time: u64, packet_success_rate: u32) { alice_main( run, packet_success_rate, - &alice_app, + alice_app, alice_out, alice_in, bob_out, @@ -318,7 +347,17 @@ fn core(time: u64, packet_success_rate: u32) { ) }); } - ts.spawn(move || bob_main(run, packet_success_rate, &bob_app, bob_out, bob_in, alice_out, bob_keypair)); + 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)); diff --git a/rustfmt.toml b/rustfmt.toml index 3a3929c..9c9fedd 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 150 +max_width = 120 edition = "2021" newline_style = "Unix" struct_lit_width = 60 diff --git a/src/applicationlayer.rs b/src/application.rs similarity index 56% rename from src/applicationlayer.rs rename to src/application.rs index 3ea8922..ba1f018 100644 --- a/src/applicationlayer.rs +++ b/src/application.rs @@ -1,13 +1,14 @@ use rand_core::{CryptoRng, RngCore}; use std::sync::Arc; -use crate::crypto::{AeadAesGcm, HashSha512, KeyPairP384, PrivateKeyKyber1024, PrpAes256, PublicKeyP384}; -use crate::proto::RATCHET_SIZE; -use crate::ratchet_state::RatchetState; +use crate::crypto::{Aes256Prp, AesGcmAead, Kyber1024PrivateKey, P384KeyPair, P384PublicKey, Sha512Hash}; #[cfg(feature = "logging")] use crate::LogEvent; use crate::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. @@ -82,39 +83,73 @@ impl Default for Settings { } } -/// Trait to implement to integrate the session into an application. +/// Trait to implement to integrate ZSSP into an application. /// -/// Templating the session on this trait lets the code here be almost entirely transport, OS, -/// and use case independent. -pub trait ApplicationLayer: Sized { +/// This is a container trait for all of the cryptographic algorithms ZSSP will use, and all of the +/// generic types that ZSSP will attach to sessions. +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(); + /// The random number generator that ZSSP should use. + /// + /// FIPS compliance requires use of a FIPS certified implementation. type Rng: CryptoRng + RngCore; + /// The implementation of AES-256 that ZSSP should use. + /// We provide an optional implementation for this trait using the `aes` crate. + /// + /// FIPS compliance requires use of a FIPS certified implementation. + type Prp: Aes256Prp; + /// The implementation of AES-GCM-256 that ZSSP should use. + /// The efficiency and security of ZSSP is very closely tied to the efficiency and security of + /// this implementation. + /// We provide an optional implementation for this trait using the `aes-gcm` crate. + /// + /// FIPS compliance requires a FIPS certified implementation. + type Aead: AesGcmAead; + /// The implementation of SHA-512 and HMAC-SHA-512 that ZSSP should use. + /// We provide an optional implementation for this trait using the `sha2` crate. + /// + /// FIPS compliance requires use of a FIPS certified implementation. + type Hash: Sha512Hash; + /// The implementation of P-384 public keys that ZSSP should use. + /// We provide an optional implementation for this trait using the `p384` crate. + /// + /// FIPS compliance requires a FIPS certified implementation. + type PublicKey: P384PublicKey; + /// The implementation of P-384 private keys that ZSSP should use. + /// We provide an optional implementation for this trait using the `p384` crate. + /// + /// FIPS compliance requires use of a FIPS certified implementation. + type KeyPair: P384KeyPair; + /// The implementation of Kyber1024 that ZSSP should use. + /// We provide an optional implementation for this trait using the `pqc_kyber` crate. + /// + /// No implementation of Kyber1024 can be FIPS certified, but this is not required + /// for ZSSP to achieve FIPS compliance. + type Kem: Kyber1024PrivateKey; - type Prp: PrpAes256; - - type Aead: AeadAesGcm; - - type Hash: HashSha512; - - type PublicKey: PublicKeyP384; - type KeyPair: KeyPairP384; - type Kem: PrivateKeyKyber1024; - - type DiskError: std::fmt::Debug; - - /// Type for arbitrary opaque object for use by the application that is attached to - /// each session. - type Data; + /// An arbitrary opaque object for use by the application that is attached to each session. + type SessionData; +} +/// Trait to implement to integrate ZSSP into an application. +/// +/// Templating ZSSP on this trait lets the code here be almost entirely transport, OS, +/// and use case independent. +pub trait ApplicationLayer: Sized { + /// Specifies which concrete set of cryptography types will be used by this application. + type Crypto: CryptoLayer; + /// A user-defined error returned when the `ApplicationLayer` fails to access persistent storage + /// for a peer's ratchet states. + type StorageError: std::error::Error; /// 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(&self) -> i64; + fn time(&mut self) -> i64; /// 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 @@ -125,7 +160,7 @@ pub trait ApplicationLayer: Sized { /// If this function is configured to always return true, it means peers will not be able to /// connect to us unless they had a prior-established ratchet key with us. This is the best way /// for the paranoid to enforce a manual allow-list. - fn hello_requires_recognized_ratchet(&self) -> bool; + 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. @@ -141,33 +176,47 @@ pub trait ApplicationLayer: Sized { /// least one party is misconfigured and got their ratchet keys corrupted or lost, or Bob has /// been compromised and is being impersonated. An attacker must at least have Bob's private /// static key to be able to ask Alice to downgrade. - fn initiator_disallows_downgrade(&self, session: &Arc>) -> bool; + 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 returns (Some(_), _), 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(&self, remote_static_key: &Self::PublicKey, identity: &[u8]) -> (Option<(bool, Self::Data)>, bool); + /// 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 the ratchet key was found, the function should return `RestoreAction::RestoreRatchet`. This will - /// cause us to connect to Alice using the returned ratchet number and ratchet key. - /// - /// If the ratchet key could not be found, the application may choose between returning - /// `RatchetAction::DowngradeRatchet` or `RatchetAction::FailAuthentication`. - /// If `RatchetAction::DowngradeRatchet` is returned we will attempt to convince Alice to downgrade - /// to the empty ratchet key, restarting the ratchet chain. - /// If `RatchetAction::FailAuthentication` is returned Alice's connection will be silently dropped. - fn restore_by_fingerprint(&self, ratchet_fingerprint: &[u8; RATCHET_SIZE]) -> Result; - /// Lookup a specific ratchet state based on the identity of the peer being communicated with. + /// 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, Self::StorageError>; + /// 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. - fn restore_by_identity(&self, remote_static_key: &Self::PublicKey, application_data: &Self::Data) -> Result<[RatchetState; 2], Self::DiskError>; - /// Atomically save the given `new_ratchet_states` to persistent storage. - /// `pre_ratchet_states` contains what should be the previous contents of persistent storage. + /// + /// 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, Self::StorageError>; + /// 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 @@ -179,20 +228,37 @@ pub trait ApplicationLayer: Sized { /// fix is to reset both ratchet keys to empty. /// /// This function may also save state to volatile storage, in which case all peers which connect - /// to us will have to allow downgrade, i.e. `initiator_disallows_downgrade` returns false - /// and/or `check_accept_session` returns `(Some(true, _), _)`. + /// 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( - &self, - remote_static_key: &Self::PublicKey, - application_data: &Self::Data, - pre_ratchet_states: [&RatchetState; 2], - new_ratchet_states: [&RatchetState; 2], - ) -> Result<(), Self::DiskError>; + &mut self, + remote_static_key: &::PublicKey, + session_data: &::SessionData, + update_data: RatchetUpdate<'_>, + ) -> Result<(), Self::StorageError>; /// 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")] - fn event_log(&self, event: LogEvent); + fn event_log(&mut self, event: LogEvent<'_, Self::Crypto>); +} + +/// A collection of fields specifying how to complete the key exchange with a specific remote peer, +/// used by Bob, the responder, at the very last stage of the key exchange. +/// +/// Corresponds to the *Accept* callback of Transition Algorithm 4. +pub struct AcceptAction { + /// The data object to be attached to the session if we successfully connect. + /// If this field is None then we will not connect to this remote peer. + pub session_data: Option, + /// Whether or not we will accept a connection with the remote peer when they do not have a + /// ratchet key that we think they should have. + pub responder_disallows_downgrade: bool, + /// Whether or not to send an explicit rejection packet to the remote peer if we do not create + /// a session with them. + /// + /// This field will not be used if `session_data` is `Some` and the remote peer passes all other + /// authentication checks. + pub responder_silently_rejects: bool, } diff --git a/src/challenge.rs b/src/challenge.rs index 5162656..fa8ccd2 100644 --- a/src/challenge.rs +++ b/src/challenge.rs @@ -2,23 +2,24 @@ use std::hash::Hasher; use rand_core::{CryptoRng, RngCore}; -use crate::crypto::{secure_eq, HashSha512}; +use crate::crypto::{secure_eq, Sha512Hash}; use crate::proto::*; pub struct ChallengeContext { pub enabled: bool, counter: u64, - antireplay_window: [u64; COUNTER_WINDOW_MAX_OOO], + antireplay_window: [u64; CHALLENGE_COUNTER_WINDOW_MAX_OOO], salt: [u8; SALT_SIZE], } +/// Corresponds to Algorithm 11 found in Section 5. pub fn gen_null_response(rng: &mut Rng) -> [u8; CHALLENGE_SIZE] { let mut response = [0u8; CHALLENGE_SIZE]; response[POW_START..].copy_from_slice(&rng.next_u64().to_be_bytes()); response } - -pub fn respond_to_challenge_in_place( +/// Corresponds to Algorithm 13 found in Section 5. +pub fn respond_to_challenge_in_place( rng: &mut Rng, challenge: &[u8; CHALLENGE_SIZE], pre_response: &mut [u8; CHALLENGE_SIZE], @@ -47,7 +48,8 @@ impl ChallengeContext { salt, } } - pub fn process_hello( + /// Corresponds to Algorithm 12 found in Section 5. + pub fn process_hello( &mut self, addr: &impl std::hash::Hash, response: &[u8; CHALLENGE_SIZE], @@ -56,7 +58,10 @@ impl ChallengeContext { return Ok(false); } let c = u64::from_be_bytes(response[..COUNTER_SIZE].try_into().unwrap()); - if self.check_window(c) && secure_eq(&response[COUNTER_SIZE..POW_START], &self.create_mac::(c, addr)) && verify_pow::(response) { + if self.check_window(c) + && secure_eq(&response[COUNTER_SIZE..POW_START], &self.create_mac::(c, addr)) + && verify_pow::(response) + { self.update_window(c); Ok(true) } else { @@ -69,7 +74,7 @@ impl ChallengeContext { Err(challenge) } } - fn create_mac(&self, c: u64, addr: &impl std::hash::Hash) -> [u8; MAC_SIZE] { + fn create_mac(&self, c: u64, addr: &impl std::hash::Hash) -> [u8; MAC_SIZE] { let mut h = Hash::new(); let mut hasher = ShaHasher(&mut h); hasher.write(&c.to_be_bytes()); @@ -95,8 +100,8 @@ impl ChallengeContext { } /// Trick rust into letting us use a hasher that returns more than 64 bits. -struct ShaHasher<'a, ShaImpl: HashSha512>(&'a mut ShaImpl); -impl<'a, ShaImpl: HashSha512> Hasher for ShaHasher<'a, ShaImpl> { +struct ShaHasher<'a, ShaImpl: Sha512Hash>(&'a mut ShaImpl); +impl<'a, ShaImpl: Sha512Hash> Hasher for ShaHasher<'a, ShaImpl> { fn finish(&self) -> u64 { unimplemented!() } @@ -107,7 +112,7 @@ impl<'a, ShaImpl: HashSha512> Hasher for ShaHasher<'a, ShaImpl> { /// Check if the proof of work attached to the first message contains the correct number of leading /// zeros. -fn verify_pow(response: &[u8]) -> bool { +fn verify_pow(response: &[u8]) -> bool { if DIFFICULTY == 0 { return true; } diff --git a/src/context.rs b/src/context.rs index ab8a010..b5855d6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,19 +1,20 @@ use rand_core::RngCore; +use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::hash::Hash; use std::num::NonZeroU32; -use std::sync::{Arc, Mutex, Weak}; +use std::sync::{Arc, Weak}; -use crate::crypto::{AES_256_KEY_SIZE, AES_GCM_IV_SIZE}; +use crate::application::{ApplicationLayer, CryptoLayer}; +use crate::challenge::ChallengeContext; +use crate::crypto::{AES_256_KEY_SIZE, AES_GCM_NONCE_SIZE}; use crate::fragmentation::{send_with_fragmentation, DefragBuffer}; use crate::proto::*; -use crate::result::{byzantine_fault, ReceiveError, ReceiveOk, SendError, SessionEvent}; +use crate::result::{byzantine_fault, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent}; use crate::zeta::*; -use crate::ApplicationLayer; #[cfg(feature = "logging")] use crate::LogEvent::*; -use crate::{challenge::ChallengeContext, result::OpenError}; /// Macro to turn off logging at compile time. macro_rules! log { @@ -30,92 +31,93 @@ pub(crate) use log; /// 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(Arc>); -impl Clone for Context { +pub struct Context(Arc>); +impl Clone for Context { fn clone(&self) -> Self { Self(self.0.clone()) } } -pub(crate) type SessionMap = Mutex>>>; +pub(crate) type SessionMap = RefCell>>>; -pub(crate) struct ContextInner { - pub(crate) rng: Mutex, - pub(crate) s_secret: App::KeyPair, - pub(crate) session_map: SessionMap, - pub(crate) sessions: Mutex, Weak>>>, - pub(crate) b2_map: Mutex>>, +pub(crate) struct ContextInner { + pub(crate) rng: RefCell, + pub(crate) s_secret: Crypto::KeyPair, + pub(crate) session_map: SessionMap, + pub(crate) sessions: RefCell, Weak>>>, + pub(crate) b2_map: RefCell>>, - hello_defrag: Mutex, - challenge: Mutex, + hello_defrag: RefCell, + challenge: RefCell, } -fn to_aes_nonce(pn: &[u8; PACKET_NONCE_SIZE]) -> [u8; AES_GCM_IV_SIZE] { - let mut an = [0u8; AES_GCM_IV_SIZE]; +/// Corresponds to Figure 10 found in Section 4.3. +fn to_aes_nonce(pn: &[u8; PACKET_NONCE_SIZE]) -> [u8; AES_GCM_NONCE_SIZE] { + let mut an = [0u8; AES_GCM_NONCE_SIZE]; an[2..].copy_from_slice(pn); an } -fn to_packet_nonce(n: &[u8; AES_GCM_IV_SIZE]) -> &[u8; PACKET_NONCE_SIZE] { +/// Corresponds to Figure 14 found near Section 6. +fn to_packet_nonce(n: &[u8; AES_GCM_NONCE_SIZE]) -> &[u8; PACKET_NONCE_SIZE] { (&n[n.len() - PACKET_NONCE_SIZE..]).try_into().unwrap() } -#[allow(unused)] -impl Context { +impl Context { /// Create a new session context. - pub fn new(static_secret_key: App::KeyPair, mut rng: App::Rng) -> Self { + 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), + rng: RefCell::new(rng), s_secret: static_secret_key, - session_map: Mutex::new(HashMap::new()), - b2_map: Mutex::new(HashMap::new()), - hello_defrag: Mutex::new(DefragBuffer::new(None)), - challenge: Mutex::new(challenge), - sessions: Mutex::new(HashMap::new()), + session_map: RefCell::new(HashMap::new()), + b2_map: RefCell::new(HashMap::new()), + hello_defrag: RefCell::new(DefragBuffer::new(None)), + challenge: RefCell::new(challenge), + sessions: RefCell::new(HashMap::new()), })) } /// Enable the ZeroTier Challenge Protocol, to protect this machine from CPU exhaustion DDOS /// attacks. - pub fn enable_challenge(&self, enabled: bool) { - self.0.challenge.lock().unwrap().enabled = enabled; + pub fn enable_challenge(&mut self, enabled: bool) { + self.0.challenge.borrow_mut().enabled = enabled; } /// 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 - /// * `application_data` - Arbitrary data meaningful to the application to include with session + /// * `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, + pub fn open( + &mut self, app: App, send: impl FnMut(Vec) -> bool, mut mtu: usize, - static_remote_key: App::PublicKey, - application_data: App::Data, + static_remote_key: Crypto::PublicKey, + session_data: Crypto::SessionData, identity: Vec, - ) -> Result>, OpenError> { + ) -> Result>, OpenError> where App: ApplicationLayer{ mtu = mtu.max(MIN_TRANSPORT_MTU); + if identity.len() > IDENTITY_MAX_SIZE { + return Err(OpenError::IdentityTooLarge); + } let ctx = &self.0; // Process zeta layer. trans_to_a1( app, - &ctx, + ctx, static_remote_key, - application_data, + session_data, identity, |Packet(kid, nonce, payload): &Packet| { // Process fragmentation layer. - send_with_fragmentation::(send, mtu, *kid, to_packet_nonce(&nonce), payload, None); + send_with_fragmentation::(send, mtu, *kid, to_packet_nonce(&nonce), payload, None); }, ) } @@ -128,15 +130,15 @@ impl Context { /// * `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 /// * `raw_fragment` - Buffer containing incoming wire packet - pub fn receive<'a, SendFn: FnMut(Vec) -> bool>( - &self, - app: App, + pub fn receive) -> bool>( + &mut self, + mut app: App, send_unassociated_reply: impl FnMut(Vec) -> bool, mut send_unassociated_mtu: usize, - send_to: impl FnOnce(&Arc>) -> Option<(SendFn, usize)>, + send_to: impl FnOnce(&Arc>) -> Option<(SendFn, usize)>, remote_address: &impl Hash, raw_fragment: Vec, - ) -> Result, ReceiveError> { + ) -> Result, ReceiveError> where App: ApplicationLayer { use crate::result::FaultType::*; send_unassociated_mtu = send_unassociated_mtu.max(MIN_TRANSPORT_MTU); let ctx = &self.0; @@ -144,60 +146,70 @@ impl Context { // Multiplex session. let kid_recv = u32::from_be_bytes(raw_fragment[..KID_SIZE].try_into().unwrap()); if let Some(kid_recv) = NonZeroU32::new(kid_recv) { - let session = ctx.session_map.lock().unwrap().get(&kid_recv).map(|r| r.upgrade()); + let session = ctx.session_map.borrow_mut().get(&kid_recv).map(|r| r.upgrade()); if let Some(Some(session)) = session { // Process recv fragmentation layer. - let mut zeta = session.0.lock().unwrap(); - let result = zeta.defrag.received_fragment::(raw_fragment, app.time(), |n, frag_no, frag_count| { - let (p, c) = from_nonce(n); - if p != PACKET_TYPE_DATA { - log!(app, ReceivedRawFragment(p, c, frag_no, frag_count)); - } - if p == PACKET_TYPE_HANDSHAKE_RESPONSE { - if !matches!(&zeta.beta, ZsspAutomata::A1(_)) { - // A resent handshake response from Bob may have arrived out of order, - // after we already received one. - return Err(byzantine_fault!(OutOfSequence, false)); - } - if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD { - return Err(byzantine_fault!(ExpiredCounter, true)); - } - Ok(()) - } else if PACKET_TYPE_USES_COUNTER_RANGE.contains(&p) { - if !zeta.check_counter_window(c) { - // The counter window has finite memory and so will occasionally give - // false positives on very out-of-order packets. - return Err(byzantine_fault!(ExpiredCounter, false)); - } - Ok(()) - } else if p == PACKET_TYPE_HANDSHAKE_COMPLETION { - // The handshake completion packet could have been resent. - return Err(byzantine_fault!(InvalidPacket, false)); - } else { - return Err(byzantine_fault!(InvalidPacket, true)); - } - })?; + let mut zeta = session.0.borrow_mut(); + let result = + zeta.defrag + .received_fragment::(raw_fragment, app.time(), |n, frag_no, frag_count| { + let (p, c) = from_nonce(n); + if p != PACKET_TYPE_DATA { + log!(app, ReceivedRawFragment(p, c, frag_no, frag_count)); + } + if p == PACKET_TYPE_HANDSHAKE_RESPONSE { + if !matches!(&zeta.beta, ZetaAutomata::A1(_)) { + // A resent handshake response from Bob may have arrived out of order, + // after we already received one. + return Err(byzantine_fault!(OutOfSequence, false)); + } + if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD { + return Err(byzantine_fault!(ExpiredCounter, true)); + } + Ok(()) + } else if PACKET_TYPE_USES_COUNTER_RANGE.contains(&p) { + if !zeta.check_counter_window(c) { + // The counter window has finite memory and so will occasionally give + // false positives on very out-of-order packets. + return Err(byzantine_fault!(ExpiredCounter, false)); + } + Ok(()) + } else if p == PACKET_TYPE_HANDSHAKE_COMPLETION { + // The handshake completion packet could have been resent. + return Err(byzantine_fault!(InvalidPacket, false)); + } else { + return Err(byzantine_fault!(InvalidPacket, true)); + } + })?; if let Some((pn, mut assembled_packet)) = result { // Process recv zeta layer. - let send_associated = |Packet(kid, nonce, payload): &Packet, hk: Option<&[u8; AES_256_KEY_SIZE]>| { - if let Some((send_fragment, mut mtu)) = send_to(&session) { - mtu = mtu.max(MIN_TRANSPORT_MTU); - send_with_fragmentation::(send_fragment, mtu, *kid, to_packet_nonce(&nonce), payload, hk); - } - }; + let send_associated = + |Packet(kid, nonce, payload): &Packet, hk: Option<&[u8; AES_256_KEY_SIZE]>| { + if let Some((send_fragment, mut mtu)) = send_to(&session) { + mtu = mtu.max(MIN_TRANSPORT_MTU); + send_with_fragmentation::( + send_fragment, + mtu, + *kid, + to_packet_nonce(&nonce), + payload, + hk, + ); + } + }; let (p, _) = from_nonce(&pn); let ret = match p { PACKET_TYPE_DATA => { - received_payload_in_place(&mut zeta, kid_recv, to_aes_nonce(&pn), &mut assembled_packet)?; + received_payload_in_place::(&mut zeta, kid_recv, to_aes_nonce(&pn), &mut assembled_packet)?; SessionEvent::Data(assembled_packet) } PACKET_TYPE_HANDSHAKE_RESPONSE => { log!(app, ReceivedRawX2); - received_x2_trans( + let should_warn_missing_ratchet = received_x2_trans( &mut zeta, &session, - &app, + &mut app, &ctx, kid_recv, to_aes_nonce(&pn), @@ -205,12 +217,23 @@ impl Context { send_associated, )?; log!(app, X2IsAuthSentX3(&session)); - SessionEvent::Control + if should_warn_missing_ratchet { + SessionEvent::DowngradedRatchetKey + } else { + SessionEvent::Control + } } PACKET_TYPE_KEY_CONFIRM => { log!(app, ReceivedRawKeyConfirm); - let result = - received_c1_trans(&mut zeta, &app, &ctx.rng, kid_recv, to_aes_nonce(&pn), assembled_packet, send_associated)?; + let result = received_c1_trans( + &mut zeta, + &mut app, + &ctx.rng, + kid_recv, + to_aes_nonce(&pn), + assembled_packet, + send_associated, + )?; log!(app, KeyConfirmIsAuthSentAck(&session)); if result { SessionEvent::Established @@ -220,7 +243,14 @@ impl Context { } PACKET_TYPE_ACK => { log!(app, ReceivedRawAck); - received_c2_trans(&mut zeta, &app, &ctx.rng, kid_recv, to_aes_nonce(&pn), assembled_packet)?; + received_c2_trans( + &mut zeta, + &mut app, + &ctx.rng, + kid_recv, + to_aes_nonce(&pn), + assembled_packet, + )?; log!(app, AckIsAuth(&session)); SessionEvent::Control } @@ -229,7 +259,7 @@ impl Context { received_k1_trans( &mut zeta, &session, - &app, + &mut app, &ctx.rng, &ctx.session_map, &ctx.s_secret, @@ -243,13 +273,20 @@ impl Context { } PACKET_TYPE_REKEY_COMPLETE => { log!(app, ReceivedRawK2); - received_k2_trans(&mut zeta, &app, kid_recv, to_aes_nonce(&pn), assembled_packet, send_associated)?; + received_k2_trans( + &mut zeta, + &mut app, + kid_recv, + to_aes_nonce(&pn), + assembled_packet, + send_associated, + )?; log!(app, K2IsAuthSentKeyConfirm(&session)); SessionEvent::Control } PACKET_TYPE_SESSION_REJECTED => { log!(app, ReceivedRawD); - received_d_trans(&mut zeta, kid_recv, to_aes_nonce(&pn), assembled_packet)?; + received_d_trans::(&mut zeta, kid_recv, to_aes_nonce(&pn), assembled_packet)?; log!(app, DIsAuthClosedSession(&session)); SessionEvent::Rejected } @@ -261,34 +298,47 @@ impl Context { Ok(ReceiveOk::Unassociated) } } else { - let mut b2_map = ctx.b2_map.lock().unwrap(); + let mut b2_map = ctx.b2_map.borrow_mut(); if let Entry::Occupied(mut entry) = b2_map.entry(kid_recv) { let zeta = entry.get_mut(); // Process recv fragmentation layer. - let result = zeta.defrag.received_fragment::(raw_fragment, app.time(), |n, frag_no, frag_count| { - let (p, c) = from_nonce(n); - log!(app, ReceivedRawFragment(p, c, frag_no, frag_count)); - if p == PACKET_TYPE_HANDSHAKE_COMPLETION && c == 0 { - Ok(()) - } else { - Err(byzantine_fault!(InvalidPacket, true)) - } - })?; + let result = + zeta.defrag + .received_fragment::(raw_fragment, app.time(), |n, frag_no, frag_count| { + let (p, c) = from_nonce(n); + log!(app, ReceivedRawFragment(p, c, frag_no, frag_count)); + if p == PACKET_TYPE_HANDSHAKE_COMPLETION && c == 0 { + Ok(()) + } else { + Err(byzantine_fault!(InvalidPacket, true)) + } + })?; if let Some((_, assembled_packet)) = result { log!(app, ReceivedRawX3); let zeta = entry.remove(); - let session = received_x3_trans(zeta, &app, ctx, kid_recv, assembled_packet, |Packet(kid, nonce, payload), hk| { - send_with_fragmentation::( - send_unassociated_reply, - send_unassociated_mtu, - *kid, - to_packet_nonce(&nonce), - payload, - hk, - ); - })?; + let (session, should_warn_missing_ratchet) = received_x3_trans( + zeta, + &mut app, + ctx, + kid_recv, + assembled_packet, + |Packet(kid, nonce, payload), hk| { + send_with_fragmentation::( + send_unassociated_reply, + send_unassociated_mtu, + *kid, + to_packet_nonce(&nonce), + payload, + hk, + ); + }, + )?; log!(app, X3IsAuthSentKeyConfirm(&session)); - Ok(ReceiveOk::Session(session, SessionEvent::NewSession)) + Ok(ReceiveOk::Session(session, if should_warn_missing_ratchet { + SessionEvent::NewDowngradedSession + } else { + SessionEvent::NewSession + })) } else { Ok(ReceiveOk::Unassociated) } @@ -300,11 +350,10 @@ impl Context { } } else { // Process recv fragmentation layer. - let result = ctx - .hello_defrag - .lock() - .unwrap() - .received_fragment::(raw_fragment, app.time(), |n, frag_no, frag_count| { + let result = ctx.hello_defrag.borrow_mut().received_fragment::( + raw_fragment, + app.time(), + |n, frag_no, frag_count| { let (p, c) = from_nonce(n); log!(app, ReceivedRawFragment(p, c, frag_no, frag_count)); if p == PACKET_TYPE_HANDSHAKE_HELLO || p == PACKET_TYPE_CHALLENGE { @@ -312,25 +361,25 @@ impl Context { } else { Err(byzantine_fault!(InvalidPacket, true)) } - })?; + }, + )?; if let Some((n, mut assembled_packet)) = result { let (p, _) = from_nonce(&n); if p == PACKET_TYPE_HANDSHAKE_HELLO { log!(app, ReceivedRawX1); // Process recv challenge layer. let challenge_start = assembled_packet.len() - CHALLENGE_SIZE; - let result = ctx - .challenge - .lock() - .unwrap() - .process_hello::(remote_address, (&assembled_packet[challenge_start..]).try_into().unwrap()); + let result = ctx.challenge.borrow_mut().process_hello::( + remote_address, + (&assembled_packet[challenge_start..]).try_into().unwrap(), + ); if let Err(challenge) = result { log!(app, X1FailedChallengeSentNewChallenge); let mut challenge_packet = Vec::new(); challenge_packet.extend(&assembled_packet[..KID_SIZE]); challenge_packet.extend(&challenge); - let nonce = to_nonce(PACKET_TYPE_CHALLENGE, ctx.rng.lock().unwrap().next_u64()); - send_with_fragmentation::( + let nonce = to_nonce(PACKET_TYPE_CHALLENGE, ctx.rng.borrow_mut().next_u64()); + send_with_fragmentation::( send_unassociated_reply, send_unassociated_mtu, 0, @@ -346,16 +395,22 @@ impl Context { assembled_packet.truncate(challenge_start); // Process recv zeta layer. - received_x1_trans(&app, &ctx, to_aes_nonce(&n), assembled_packet, |Packet(kid, nonce, payload), hk| { - send_with_fragmentation::( - send_unassociated_reply, - send_unassociated_mtu, - *kid, - to_packet_nonce(&nonce), - payload, - Some(hk), - ); - })?; + received_x1_trans( + &mut app, + &ctx, + to_aes_nonce(&n), + assembled_packet, + |Packet(kid, nonce, payload), hk| { + send_with_fragmentation::( + send_unassociated_reply, + send_unassociated_mtu, + *kid, + to_packet_nonce(&nonce), + payload, + Some(hk), + ); + }, + )?; log!(app, X1IsAuthSentX2); Ok(ReceiveOk::Unassociated) } else if p == PACKET_TYPE_CHALLENGE { @@ -364,10 +419,16 @@ impl Context { if assembled_packet.len() != KID_SIZE + CHALLENGE_SIZE { return Err(byzantine_fault!(InvalidPacket, true)); } - if let Some(kid_recv) = NonZeroU32::new(u32::from_be_bytes(assembled_packet[..KID_SIZE].try_into().unwrap())) { - if let Some(Some(session)) = ctx.session_map.lock().unwrap().get(&kid_recv).map(|r| r.upgrade()) { - let mut zeta = session.0.lock().unwrap(); - respond_to_challenge(&mut zeta, &ctx.rng, &assembled_packet[KID_SIZE..].try_into().unwrap()); + if let Some(kid_recv) = + NonZeroU32::new(u32::from_be_bytes(assembled_packet[..KID_SIZE].try_into().unwrap())) + { + if let Some(Some(session)) = ctx.session_map.borrow_mut().get(&kid_recv).map(|r| r.upgrade()) { + let mut zeta = session.0.borrow_mut(); + respond_to_challenge::( + &mut zeta, + &ctx.rng, + &assembled_packet[KID_SIZE..].try_into().unwrap(), + ); log!(app, ChallengeIsAuth(&session)); return Ok(ReceiveOk::Unassociated); } @@ -382,18 +443,27 @@ impl Context { } } - /// Send data over the session. + /// Send data as fragments over the session to the remote peer. + /// + /// The session used here must have been created from this instance of a ZSSP context. /// /// * `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` - The MTU of the link, all packets passed to `send` will be at most `mtu` in length /// * `payload` - Data to send - pub fn send(&self, session: &Arc>, send: impl FnMut(Vec) -> bool, mut mtu: usize, payload: Vec) -> Result<(), SendError> { + pub fn send( + &mut self, + session: &Arc>, + send: impl FnMut(Vec) -> bool, + mut mtu: usize, + payload: Vec, + ) -> Result<(), SendError> { + debug_assert_eq!(session.0.borrow().ctx.as_ptr(), Arc::as_ptr(&self.0)); mtu = mtu.max(MIN_TRANSPORT_MTU); - let mut zeta = session.0.lock().unwrap(); - send_payload(&mut zeta, payload, |Packet(kid, nonce, payload), hk| { - send_with_fragmentation::(send, mtu, *kid, to_packet_nonce(nonce), &payload, hk); + let mut zeta = session.0.borrow_mut(); + send_payload::(&mut zeta, payload, |Packet(kid, nonce, payload), hk| { + send_with_fragmentation::(send, mtu, *kid, to_packet_nonce(nonce), &payload, hk); }) } @@ -401,35 +471,46 @@ impl Context { /// /// 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 few seconds are not - /// a problem. + /// a problem. It is completely fine to call this function more often than the returned interval. /// /// * `send_to` - Function to get a sender and an MTU to send something over an active session - pub fn service) -> bool>(&self, app: App, mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>) -> i64 { + pub fn service) -> bool>( + &mut self, + mut app: App, + mut send_to: impl FnMut(&Arc>) -> Option<(SendFn, usize)>, + ) -> i64 where App: ApplicationLayer { let ctx = &self.0; - let sessions = ctx.sessions.lock().unwrap(); + let sessions = ctx.sessions.borrow_mut(); let current_time = app.time(); let mut next_timer = i64::MAX; for (_, session) in sessions.iter() { if let Some(session) = session.upgrade() { - let mut zeta = session.0.lock().unwrap(); + let mut zeta = session.0.borrow_mut(); service( &mut zeta, &session, ctx, - &app, + &mut app, current_time, |Packet(kid, nonce, payload): &Packet, hk| { if let Some((send_fragment, mut mtu)) = send_to(&session) { mtu = mtu.max(MIN_TRANSPORT_MTU); - send_with_fragmentation::(send_fragment, mtu, *kid, to_packet_nonce(&nonce), payload, hk); + send_with_fragmentation::( + send_fragment, + mtu, + *kid, + to_packet_nonce(&nonce), + payload, + hk, + ); } }, ); next_timer = next_timer.min(zeta.next_timer()); - zeta.defrag.service::(current_time); + zeta.defrag.service(current_time); } } - ctx.hello_defrag.lock().unwrap().service::(current_time); - (App::SETTINGS.resend_time as i64).min(next_timer - current_time) + ctx.hello_defrag.borrow_mut().service(current_time); + (Crypto::SETTINGS.resend_time as i64).min(next_timer - current_time) } } diff --git a/src/crypto/aes.rs b/src/crypto/aes.rs index 5e8f41a..f7881c0 100644 --- a/src/crypto/aes.rs +++ b/src/crypto/aes.rs @@ -1,29 +1,45 @@ +/// The size of an AES block, which is 16 bytes, or 128 bits. pub const AES_256_BLOCK_SIZE: usize = 16; +/// The size of an AES-256 key, which is 32 bytes, or 256 bits. pub const AES_256_KEY_SIZE: usize = 32; +/// The size of an AES-GCM authentication tag, which is 16 bytes, or 128 bits. +/// Some implementations of AES-GCM allow use of smaller tags, but ZSSP will only accept 16 byte tags. pub const AES_GCM_TAG_SIZE: usize = 16; -pub const AES_GCM_IV_SIZE: usize = 12; +/// The size of an AES-GCM IV, or nonce, which is 12 bytes, or 96 bits. +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. -pub trait PrpAes256 { - /// Decrypt the given `block` of plaintext directly using the AES block cipher +pub trait Aes256Prp { + /// Encrypt 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`. + /// The ciphertext should be written directly back to `block`. fn encrypt_in_place(key: &[u8; AES_256_KEY_SIZE], block: &mut [u8; AES_256_BLOCK_SIZE]); /// 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`. + /// The plaintext should be written directly back to `block`. fn decrypt_in_place(key: &[u8; AES_256_KEY_SIZE], block: &mut [u8; AES_256_BLOCK_SIZE]); } -pub trait AeadAesGcm { - fn encrypt_in_place(key: &[u8; AES_256_KEY_SIZE], iv: [u8; AES_GCM_IV_SIZE], aad: Option<&[u8]>, buffer: &mut [u8]) -> [u8; AES_GCM_TAG_SIZE]; - fn decrypt_in_place( +/// A trait accessing AES-GCM-256 encryption and decryption as a set of pure-functions. +/// These should be trivial to implement for most implementations of AES-GCM. +pub trait AesGcmAead { + /// Encrypt the given `buffer` of plaintext using AES-GCM-256, with the given `key`, `iv` and `aad`. + /// The ciphertext should be written directly back to `buffer`, and the GCM tag should be returned. + fn encrypt_in_place( key: &[u8; AES_256_KEY_SIZE], - iv: [u8; AES_GCM_IV_SIZE], + nonce: &[u8; AES_GCM_NONCE_SIZE], aad: Option<&[u8]>, buffer: &mut [u8], - tag: [u8; AES_GCM_TAG_SIZE], + ) -> [u8; AES_GCM_TAG_SIZE]; + /// Decrypt the given `buffer` of ciphertext using AES-GCM-256, with the given `key`, `iv` and `aad`. + /// The ciphertext should be written directly back to `buffer`, and the GCM tag should be returned. + fn decrypt_in_place( + key: &[u8; AES_256_KEY_SIZE], + nonce: &[u8; AES_GCM_NONCE_SIZE], + aad: Option<&[u8]>, + buffer: &mut [u8], + tag: &[u8; AES_GCM_TAG_SIZE], ) -> bool; } diff --git a/src/crypto/kyber1024.rs b/src/crypto/kyber1024.rs index 1d6bc82..0cc898a 100644 --- a/src/crypto/kyber1024.rs +++ b/src/crypto/kyber1024.rs @@ -1,14 +1,37 @@ 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 PrivateKeyKyber1024: Sized + Send + Sync { +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]); - - fn encapsulate(rng: &mut Rng, public_key: &[u8; KYBER_PUBLIC_KEY_SIZE]) -> Option<([u8; KYBER_CIPHERTEXT_SIZE], [u8; KYBER_PLAINTEXT_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. + fn encapsulate( + rng: &mut Rng, + public_key: &[u8; KYBER_PUBLIC_KEY_SIZE], + ) -> Option<([u8; KYBER_CIPHERTEXT_SIZE], [u8; KYBER_PLAINTEXT_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. fn decapsulate(&self, ciphertext: &[u8; KYBER_CIPHERTEXT_SIZE]) -> Option<[u8; KYBER_PLAINTEXT_SIZE]>; } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 00ca2aa..16279f0 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -13,6 +13,7 @@ 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 rand_core; +pub use zeroize; /// Constant time byte slice equality. pub fn secure_eq + ?Sized, B: AsRef<[u8]> + ?Sized>(a: &A, b: &B) -> bool { diff --git a/src/crypto/p384.rs b/src/crypto/p384.rs index c7ed465..84acf23 100644 --- a/src/crypto/p384.rs +++ b/src/crypto/p384.rs @@ -1,42 +1,45 @@ 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 PublicKeyP384: Sized + Send + Sync { - /// Create a p384 public key from raw bytes. +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 P384 curve, - /// or if it breaks the P384 standard in any other way. + /// **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 49 byte NIST encoding of P384 public keys. + /// 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 KeyPairP384 { - type PublicKey: PublicKeyP384; - /// Randomly generate a new p384 keypair. - /// This function may use the provided RNG or it's own, - /// so long as the produced keys are cryptographically random. +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 49 byte NIST encoding of P384 public keys. + /// 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 `output`. /// /// **CRITICAL**: This function must return `None` 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 P384 ECDH key agreement, or better yet use a peer reviewed + /// 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. fn agree(&self, public_key: &Self::PublicKey) -> Option<[u8; P384_ECDH_SHARED_SECRET_SIZE]>; } diff --git a/src/crypto/sha512.rs b/src/crypto/sha512.rs index 0ff187e..b1468ca 100644 --- a/src/crypto/sha512.rs +++ b/src/crypto/sha512.rs @@ -1,15 +1,17 @@ +/// The size of a SHA-512 hash, which of course is 64 bytes, or 512 bits. pub const SHA512_HASH_SIZE: usize = 64; -/// Opaque SHA-512 implementation. -/// Does not need to be threadsafe. -pub trait HashSha512 { - /// Allocate memory on the stack or heap for Sha512. - /// An instance of Sha512 will only ever be held on the stack. +/// A SHA-512 and HMAC-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 hashing the input and write the final hash to output. + /// Finish streaming input and output the final hash. fn finish(self) -> [u8; SHA512_HASH_SIZE]; + /// Produce a HMAC-SHA-512 hash based on the given `key` and `data`. + /// This is a pure function and does not need to support streaming. fn hmac(key: &[u8], data: &[u8]) -> [u8; SHA512_HASH_SIZE]; } diff --git a/src/crypto_impl/aes_impl.rs b/src/crypto_impl/aes_impl.rs index 9a2edab..5160ef4 100644 --- a/src/crypto_impl/aes_impl.rs +++ b/src/crypto_impl/aes_impl.rs @@ -5,7 +5,9 @@ use aes_gcm::{Aes256Gcm, Key, Nonce, Tag}; use crate::crypto::*; -impl PrpAes256 for Aes256 { +/// The version and type of the aes crate that the `Aes256Prp` trait is implemented for. +pub type Aes256Crate = Aes256; +impl Aes256Prp for Aes256Crate { fn encrypt_in_place(key: &[u8; AES_256_KEY_SIZE], block: &mut [u8; AES_256_BLOCK_SIZE]) { let cipher = Aes256::new(GenericArray::from_slice(key)); cipher.encrypt_block(GenericArray::from_mut_slice(block)); @@ -17,12 +19,19 @@ impl PrpAes256 for Aes256 { } } -impl AeadAesGcm for Aes256Gcm { - fn encrypt_in_place(key: &[u8; AES_256_KEY_SIZE], iv: [u8; AES_GCM_IV_SIZE], aad: Option<&[u8]>, buffer: &mut [u8]) -> [u8; AES_GCM_TAG_SIZE] { +/// The version and type of the aes-gcm crate that the `Aes256Gcm` trait is implemented for. +pub type AesGcmCrate = Aes256Gcm; +impl AesGcmAead for AesGcmCrate { + fn encrypt_in_place( + key: &[u8; AES_256_KEY_SIZE], + iv: &[u8; AES_GCM_NONCE_SIZE], + aad: Option<&[u8]>, + buffer: &mut [u8], + ) -> [u8; AES_GCM_TAG_SIZE] { let key = Key::::from_slice(key); let mut cipher = Aes256Gcm::new(&key); cipher - .encrypt_in_place_detached(&Nonce::from(iv), aad.unwrap_or(&[]), buffer) + .encrypt_in_place_detached(&Nonce::from_slice(iv), aad.unwrap_or(&[]), buffer) .unwrap() .try_into() .unwrap() @@ -30,15 +39,20 @@ impl AeadAesGcm for Aes256Gcm { fn decrypt_in_place( key: &[u8; AES_256_KEY_SIZE], - iv: [u8; AES_GCM_IV_SIZE], + iv: &[u8; AES_GCM_NONCE_SIZE], aad: Option<&[u8]>, buffer: &mut [u8], - tag: [u8; AES_GCM_TAG_SIZE], + tag: &[u8; AES_GCM_TAG_SIZE], ) -> bool { let key = Key::::from_slice(key); let mut cipher = Aes256Gcm::new(&key); cipher - .decrypt_in_place_detached(&Nonce::from(iv), aad.unwrap_or(&[]), buffer, &Tag::from(tag)) + .decrypt_in_place_detached( + &Nonce::from_slice(iv), + aad.unwrap_or(&[]), + buffer, + &Tag::from_slice(tag), + ) .is_ok() } } diff --git a/src/crypto_impl/kyber1024.rs b/src/crypto_impl/kyber1024.rs index a17ba38..57cd5e7 100644 --- a/src/crypto_impl/kyber1024.rs +++ b/src/crypto_impl/kyber1024.rs @@ -1,18 +1,25 @@ use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroizing; use crate::crypto::*; -impl PrivateKeyKyber1024 for [u8; pqc_kyber::KYBER_SECRETKEYBYTES] { +/// 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 RustKyber1024PrivateKey = Zeroizing<[u8; pqc_kyber::KYBER_SECRETKEYBYTES]>; +impl Kyber1024PrivateKey for RustKyber1024PrivateKey { fn generate(rng: &mut Rng) -> (Self, [u8; KYBER_PUBLIC_KEY_SIZE]) { let keypair = pqc_kyber::keypair(rng); - (keypair.secret, keypair.public) + (Zeroizing::new(keypair.secret), keypair.public) } - fn encapsulate(rng: &mut Rng, public_key: &[u8; KYBER_PUBLIC_KEY_SIZE]) -> Option<([u8; KYBER_CIPHERTEXT_SIZE], [u8; KYBER_PLAINTEXT_SIZE])> { + fn encapsulate( + rng: &mut Rng, + public_key: &[u8; KYBER_PUBLIC_KEY_SIZE], + ) -> Option<([u8; KYBER_CIPHERTEXT_SIZE], [u8; KYBER_PLAINTEXT_SIZE])> { pqc_kyber::encapsulate(public_key, rng).ok() } fn decapsulate(&self, ciphertext: &[u8; KYBER_CIPHERTEXT_SIZE]) -> Option<[u8; KYBER_PLAINTEXT_SIZE]> { - pqc_kyber::decapsulate(ciphertext, self).ok() + pqc_kyber::decapsulate(ciphertext, self.as_ref()).ok() } } diff --git a/src/crypto_impl/mod.rs b/src/crypto_impl/mod.rs index 0a9dd5a..921af90 100644 --- a/src/crypto_impl/mod.rs +++ b/src/crypto_impl/mod.rs @@ -1,11 +1,19 @@ #[cfg(feature = "aes-gcm")] -pub mod aes_impl; +mod aes_impl; +#[cfg(feature = "aes-gcm")] +pub use aes_impl::*; #[cfg(feature = "pqc_kyber")] -pub mod kyber1024; +mod kyber1024; +#[cfg(feature = "pqc_kyber")] +pub use kyber1024::*; #[cfg(feature = "p384")] -pub mod p384_impl; +mod p384_impl; +#[cfg(feature = "p384")] +pub use p384_impl::*; #[cfg(feature = "sha2")] -pub mod sha512; +mod sha512; +#[cfg(feature = "sha2")] +pub use sha512::*; #[cfg(feature = "aes")] pub use aes; @@ -19,3 +27,38 @@ pub use p384; pub use pqc_kyber; #[cfg(feature = "sha2")] pub use sha2; + +/* +TODO: wrangle the feature flags so we can provide the default set of crypto implementations below. +use crate::application::{Settings, CryptoLayer}; +#[cfg(feature = "default")] +pub trait CrateCryptoLayer { + /// 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(); + + /// A user-defined error returned when the `ApplicationLayer` fails to access persistent storage + /// for a peer's ratchet states. + type StorageError: std::error::Error; + + /// An arbitrary opaque object for use by the application that is attached to each session. + type SessionData; +} + +use rand_core::OsRng; +#[cfg(feature = "default")] +impl CryptoLayer for Crypto { + type Rng = OsRng; + type Prp = Aes256Crate; + type Aead = AesGcmCrate; + type Hash = Sha512Crate; + type PublicKey = P384CratePublicKey; + type KeyPair = P384CrateKeyPair; + type Kem = RustKyber1024PrivateKey; + + type StorageError = Crypto::StorageError; + + type SessionData = Crypto::SessionData; +} + */ diff --git a/src/crypto_impl/p384_impl.rs b/src/crypto_impl/p384_impl.rs index ddf6e68..66cd470 100644 --- a/src/crypto_impl/p384_impl.rs +++ b/src/crypto_impl/p384_impl.rs @@ -3,7 +3,9 @@ use rand_core::{CryptoRng, RngCore}; use crate::crypto::*; -impl PublicKeyP384 for PublicKey { +/// The version and type of the p384 crate that the `P384PublicKey` trait is implemented for. +pub type P384CratePublicKey = PublicKey; +impl P384PublicKey for P384CratePublicKey { fn from_bytes(raw_key: &[u8; P384_PUBLIC_KEY_SIZE]) -> Option { PublicKey::from_sec1_bytes(raw_key).ok() } @@ -14,7 +16,9 @@ impl PublicKeyP384 for PublicKey { } } -impl KeyPairP384 for EphemeralSecret { +/// The version and type of the p384 crate that the `P384KeyPair` trait is implemented for. +pub type P384CrateKeyPair = EphemeralSecret; +impl P384KeyPair for P384CrateKeyPair { type PublicKey = PublicKey; fn generate(rng: &mut Rng) -> Self { @@ -26,6 +30,12 @@ impl KeyPairP384 for EphemeralSecret { } fn agree(&self, public_key: &Self::PublicKey) -> Option<[u8; P384_ECDH_SHARED_SECRET_SIZE]> { - Some(self.diffie_hellman(public_key).raw_secret_bytes().as_slice().try_into().unwrap()) + Some( + self.diffie_hellman(public_key) + .raw_secret_bytes() + .as_slice() + .try_into() + .unwrap(), + ) } } diff --git a/src/crypto_impl/sha512.rs b/src/crypto_impl/sha512.rs index 11e531c..1d2996c 100644 --- a/src/crypto_impl/sha512.rs +++ b/src/crypto_impl/sha512.rs @@ -3,7 +3,9 @@ use sha2::{Digest, Sha512}; use crate::crypto::*; -impl HashSha512 for Sha512 { +/// The version and type of the sha2 crate that the `Sha512Hash` trait is implemented for. +pub type Sha512Crate = Sha512; +impl Sha512Hash for Sha512Crate { fn new() -> Self { Digest::new() } diff --git a/src/fragmentation.rs b/src/fragmentation.rs index 64d3e7c..c028295 100644 --- a/src/fragmentation.rs +++ b/src/fragmentation.rs @@ -1,15 +1,21 @@ +use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; -use std::sync::Mutex; use zeroize::Zeroizing; -use crate::crypto::{PrpAes256, AES_256_KEY_SIZE}; +use crate::application::{CryptoLayer, ApplicationLayer}; +use crate::crypto::{Aes256Prp, AES_256_KEY_SIZE}; use crate::proto::*; use crate::result::{byzantine_fault, ReceiveError}; -use crate::ApplicationLayer; -fn create_fragment_header(kid_send: u32, fragment_count: usize, fragment_no: usize, n: &[u8; PACKET_NONCE_SIZE]) -> [u8; HEADER_SIZE] { +/// Corresponds to Figure 13 found in Section 6. +fn create_fragment_header( + kid_send: u32, + fragment_count: usize, + fragment_no: usize, + n: &[u8; PACKET_NONCE_SIZE], +) -> [u8; HEADER_SIZE] { debug_assert!(fragment_count > 0); debug_assert!(fragment_count <= MAX_FRAGMENTS); debug_assert!(fragment_no < MAX_FRAGMENTS); @@ -21,7 +27,8 @@ fn create_fragment_header(kid_send: u32, fragment_count: usize, fragment_no: usi header } -pub fn send_with_fragmentation( +/// Corresponds to the fragmentation algorithm described in Section 6. +pub fn send_with_fragmentation( mut send: impl FnMut(Vec) -> bool, mtu: usize, identifier: u32, @@ -45,7 +52,10 @@ pub fn send_with_fragmentation( fragment.extend(&packet[i..j]); if let Some(hk_send) = hk_send { - App::Prp::encrypt_in_place(hk_send, (&mut fragment[HEADER_AUTH_START..HEADER_AUTH_END]).try_into().unwrap()); + Crypto::Prp::encrypt_in_place( + hk_send, + (&mut fragment[HEADER_AUTH_START..HEADER_AUTH_END]).try_into().unwrap(), + ); } if !send(fragment) { return false; @@ -56,7 +66,7 @@ pub fn send_with_fragmentation( } pub struct DefragBuffer { - fragment_map: Mutex>, + fragment_map: RefCell>, hk_recv: Option>, } @@ -69,22 +79,28 @@ struct Buffer { impl DefragBuffer { pub fn new(hk_recv: Option>) -> Self { - Self { fragment_map: Mutex::new(HashMap::new()), hk_recv } + Self { fragment_map: RefCell::new(HashMap::new()), hk_recv } } + /// Corresponds to the authentication and defragmentation algorithm described in Section 6.1. pub fn received_fragment( &self, mut raw_fragment: Vec, current_time: i64, - vrfy: impl FnOnce(&[u8; PACKET_NONCE_SIZE], usize, usize) -> Result<(), ReceiveError>, - ) -> Result)>, ReceiveError> { + vrfy: impl FnOnce(&[u8; PACKET_NONCE_SIZE], usize, usize) -> Result<(), ReceiveError>, + ) -> Result)>, ReceiveError> { use crate::result::FaultType::*; - if raw_fragment.len() < HEADER_AUTH_END { + if raw_fragment.len() < MIN_PACKET_SIZE { return Err(byzantine_fault!(InvalidPacket, true)); } if let Some(hk_recv) = self.hk_recv.as_ref() { - App::Prp::decrypt_in_place(hk_recv, (&mut raw_fragment[HEADER_AUTH_START..HEADER_AUTH_END]).try_into().unwrap()) + ::Prp::decrypt_in_place( + hk_recv, + (&mut raw_fragment[HEADER_AUTH_START..HEADER_AUTH_END]) + .try_into() + .unwrap(), + ) } let fragment_no = raw_fragment[FRAGMENT_NO_IDX] as usize; @@ -99,8 +115,8 @@ impl DefragBuffer { return Err(e); } - let expiration_time = current_time + App::SETTINGS.fragment_assembly_timeout as i64; - let mut map = self.fragment_map.lock().unwrap(); + let expiration_time = current_time + App::Crypto::SETTINGS.fragment_assembly_timeout as i64; + let mut map = self.fragment_map.borrow_mut(); match map.entry(n) { Entry::Occupied(mut entry) => { let buffer = entry.get_mut(); @@ -141,8 +157,8 @@ impl DefragBuffer { } } - pub fn service(&self, current_time: i64) { - let mut map = self.fragment_map.lock().unwrap(); + pub fn service(&self, current_time: i64) { + let mut map = self.fragment_map.borrow_mut(); map.retain(|_, buffer| buffer.expiration_time < current_time); } } diff --git a/src/lib.rs b/src/lib.rs index bc794bd..9519d31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,28 +5,71 @@ * (c) ZeroTier, Inc. * https://www.zerotier.com/ */ -//#![warn(missing_docs, rust_2018_idioms)] -//! TODO: docs +//! ZeroTier Secure Sessions Protocol +//! ====== +//! +//! ## Introduction +//! +//! An in-depth guide to the full protocol specification can be found in the [protocol whitepaper](./whitepaper.pdf) provided in this repo. This implementation references it heavily. +//! +//! 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 [protocol whitepaper](./whitepaper.pdf). +//! +//! ## 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 +#![warn(missing_docs, rust_2018_idioms)] -mod applicationlayer; mod challenge; mod context; mod fragmentation; #[cfg(feature = "logging")] mod log_event; mod proto; +mod ratchet_state; mod symmetric_state; mod zeta; -#[macro_use] -pub mod result; +/// An abstraction over OS and use-case specific resources and queries. +/// This allows this library to be platform independent, but a user of this library must implement +/// the `ApplicationLayer` trait. +pub mod application; +/// A collection of implementation-independent traits for the various specific cryptographic +/// algorithms ZSSP depends on. +/// +/// Each trait is hyper-specific about the semantics of the algorithms and the lengths of their +/// inputs and outputs. +/// This is to enforced a basic sanity-check upon anyone trying to use their own implementations. +/// +/// The `crypto_impl` module contains implementations of these traits in terms of popular Rust +/// implementations of these algorithms. pub mod crypto; +/// A module containing optional implementations of the ZSSP `crypto` traits in terms of popular +/// Rust crates. Some of these crates are not thoroughly audited, so use at your own risk. +/// +/// Note that none of these crates are FIPS certified, meaning a build of ZSSP using them will not +/// be FIPS compliant. However lack of FIPS compliance by no means implies lack of security or lack +/// of confidence. +/// +/// This module contains the trait implementations as well as re-exports of those crates. pub mod crypto_impl; -pub mod ratchet_state; +/// The collection of major return types of this library. +pub mod result; -pub use applicationlayer::{ApplicationLayer, Settings}; pub use context::Context; #[cfg(feature = "logging")] pub use log_event::LogEvent; -pub use proto::{MIN_TRANSPORT_MTU, RATCHET_SIZE}; +pub use proto::MIN_TRANSPORT_MTU; pub use zeta::Session; diff --git a/src/log_event.rs b/src/log_event.rs index c95252f..ae01138 100644 --- a/src/log_event.rs +++ b/src/log_event.rs @@ -1,21 +1,23 @@ use std::sync::Arc; -use crate::{ApplicationLayer, Session}; +use crate::application::CryptoLayer; +use crate::Session; /// ZSSP events that might be interesting to log or aggregate into metrics. -pub enum LogEvent<'a, App: ApplicationLayer> { - ResentX1(&'a Arc>), - TimeoutX1(&'a Arc>), +#[allow(missing_docs)] +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>), + 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, @@ -23,24 +25,24 @@ pub enum LogEvent<'a, App: ApplicationLayer> { X1SucceededChallenge, X1IsAuthSentX2, ReceivedRawChallenge, - ChallengeIsAuth(&'a Arc>), + ChallengeIsAuth(&'a Arc>), ReceivedRawX2, - X2IsAuthSentX3(&'a Arc>), + X2IsAuthSentX3(&'a Arc>), ReceivedRawX3, - X3IsAuthSentKeyConfirm(&'a Arc>), + X3IsAuthSentKeyConfirm(&'a Arc>), ReceivedRawKeyConfirm, - KeyConfirmIsAuthSentAck(&'a Arc>), + KeyConfirmIsAuthSentAck(&'a Arc>), ReceivedRawAck, - AckIsAuth(&'a Arc>), + AckIsAuth(&'a Arc>), ReceivedRawK1, - K1IsAuthSentK2(&'a Arc>), + K1IsAuthSentK2(&'a Arc>), ReceivedRawK2, - K2IsAuthSentKeyConfirm(&'a Arc>), + K2IsAuthSentKeyConfirm(&'a Arc>), ReceivedRawD, - DIsAuthClosedSession(&'a Arc>), + DIsAuthClosedSession(&'a Arc>), } -impl<'a, App: ApplicationLayer> std::fmt::Debug for LogEvent<'a, App> { +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(), diff --git a/src/proto.rs b/src/proto.rs index 0b7a5fc..6a08419 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -1,8 +1,12 @@ -use crate::crypto::{AES_GCM_TAG_SIZE, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, P384_PUBLIC_KEY_SIZE, SHA512_HASH_SIZE}; +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; @@ -41,6 +45,8 @@ 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; /* Key exchange constants */ @@ -64,9 +70,12 @@ 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; -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"; -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"; +/// 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"; @@ -74,10 +83,21 @@ 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 INIT_COUNTER: u64 = 0; -pub(crate) const EXPIRE_AFTER_USES: u64 = 4294967295; +//pub(crate) const EXPIRE_AFTER_USES: u64 = (1 << 32) - 1; +pub(crate) const HARD_EXPIRATION: u64 = u64::MAX; +/// Determines the number of counters a session will remember. If a counter arrives over +/// this amount out of order relative to other received counters, it is likely to be +/// rejected on the basis that the session can't remember if this counter was replayed. +/// Increasing this value makes a session consume more memory. pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 64; +/// Maximum number of counter steps that the counter is allowed to skip ahead. +/// This cannot be changed away from 2^24 without changing the header nonce handling code. pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 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 */ @@ -93,15 +113,26 @@ 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; +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_RESPONSE_SIZE: usize = P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE; +pub(crate) const HANDSHAKE_RESPONSE_SIZE: usize = + P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE; pub(crate) const 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 KEY_CONFIRMATION_SIZE: usize = AES_GCM_TAG_SIZE; pub(crate) const ACKNOWLEDGEMENT_SIZE: usize = AES_GCM_TAG_SIZE; pub(crate) const SESSION_REJECTED_SIZE: usize = AES_GCM_TAG_SIZE; pub(crate) const REKEY_SIZE: usize = P384_PUBLIC_KEY_SIZE + KID_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_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; diff --git a/src/ratchet_state.rs b/src/ratchet_state.rs index 615a60f..b6bf51b 100644 --- a/src/ratchet_state.rs +++ b/src/ratchet_state.rs @@ -1,92 +1,230 @@ -use crate::crypto::{secure_eq, HashSha512}; -use crate::proto::*; -use std::ops::Deref; - -#[derive(Clone, PartialEq, Eq)] -pub enum RatchetState { - Null, - Empty, - NonEmpty(NonEmptyRatchetState), -} use zeroize::Zeroizing; -use RatchetState::*; + +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 { + key: Zeroizing<[u8; RATCHET_SIZE]>, + fingerprint: Option>, + 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 { - /// Helper function to create a new nonempty ratchet state from a raw ratchet key, fingerprint, - /// and the ratchet chain's current length. - pub fn new_nonempty(key: Zeroizing<[u8; RATCHET_SIZE]>, fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, chain_len: u64) -> Self { - NonEmpty(NonEmptyRatchetState { key, fingerprint, chain_len }) + /// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length. + pub fn new(key: Zeroizing<[u8; RATCHET_SIZE]>, fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, chain_len: u64) -> Self { + RatchetState { key, fingerprint: Some(fingerprint), chain_len } } - /// Creates the set of two ratchet states that ZSSP initializes a key exchange with when - /// communicating to a brand new peer. - pub fn new_initial_states() -> [RatchetState; 2] { - [RatchetState::Empty, RatchetState::Null] + /// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length. + /// + /// The caller should make sure any copies of these values are deleted from memory once they are + /// no longer needed. + 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, + } } - /// Creates a set of two ratchet states that ZSSP can initialize a key exchange with when - /// communicating to a brand new peer. - /// The peer must know the one-time-password and initialize their key exchange with it as well. - pub fn new_from_otp(otp: &[u8]) -> [RatchetState; 2] { + /// The ratchet key for this ratchet state. This is directly mixed into the master secret of a + /// session and so is very sensitive. All operations upon a ratchet key must be implemented + /// in constant time. The user should prefer to do nothing with the ratchet key besides copying + /// it to or from a storage device. + /// + /// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be + /// all zeros. + pub fn key(&self) -> &[u8; RATCHET_SIZE] { + &self.key + } + /// Ratchet keys and fingerprints are "chained together", where each set is derived from the + /// previous set. + /// + /// This function outputs the total length of that chain, as in the total number of previous + /// ratchet states that this ratchet state was derived from. + pub fn chain_len(&self) -> u64 { + self.chain_len + } + /// Creates a new "empty" ratchet state, where the ratchet fingerprint is the + /// empty string, the ratchet key is all zeros, and the chain length is 0. + /// + /// This value is the default value of `RatchetState`. + pub fn empty() -> Self { + RatchetState { + key: Zeroizing::new([0u8; RATCHET_SIZE]), + fingerprint: None, + chain_len: 0, + } + } + /// Creates a new ratchet state derived from a one-time-password. If both sides of a session use + /// the same one-time-password then they can use this ratchet state to connect with each other + /// for the first time. + pub fn new_from_otp(otp: &[u8]) -> RatchetState { let mut buffer = Vec::new(); buffer.push(1); buffer.extend(LABEL_OTP_TO_RATCHET); - buffer.push(0); - buffer.extend((2u16 * 512u16).to_be_bytes()); + buffer.push(0x00); + buffer.extend((1024u16).to_be_bytes()); let r1 = Hmac::hmac(otp, &buffer); buffer[0] = 2; let r2 = Hmac::hmac(otp, &buffer); - [ - Self::new_nonempty( - Zeroizing::new(r1[..RATCHET_SIZE].try_into().unwrap()), - Zeroizing::new(r2[..RATCHET_SIZE].try_into().unwrap()), - 1, - ), - RatchetState::Null, - ] + Self::new( + Zeroizing::new(r1[..RATCHET_SIZE].try_into().unwrap()), + Zeroizing::new(r2[..RATCHET_SIZE].try_into().unwrap()), + 1, + ) } - /// Returns true if this ratchet state is the null ratchet state. - pub fn is_null(&self) -> bool { - matches!(self, Null) - } - /// Returns true if this ratchet state is the empty ratchet state. The empty ratchet state has - /// a key of all zeros and the empty string as the ratchet fingerprint. + /// Returns true if this is the "empty" ratchet state, where the ratchet fingerprint is the + /// empty string, the ratchet key is all zeros, and the chain length is 0. pub fn is_empty(&self) -> bool { - matches!(self, Empty) + self.fingerprint.is_none() } - /// Retrieve a nonempty ratchet state if it exists. - pub fn nonempty(&self) -> Option<&NonEmptyRatchetState> { - match self { - NonEmpty(rs) => Some(rs), - _ => None, - } + /// Checks if the fingerprint of this ratchet state equals the fingerprint contained in argument + /// `rf`. Uses constant time equality. + pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { + self.fingerprint.as_ref().map_or(false, |rf0| secure_eq(rf0, rf)) } - /// Retrieve the ratchet chain length, or 0 if this ratchet state is null or empty. - pub fn chain_len(&self) -> u64 { - self.nonempty().map_or(0, |rs| rs.chain_len) - } - /// Retrieve the ratchet fingerprint if it exists. + /// The ratchet fingerprint for this ratchet state. + /// + /// If this returns `None` then the ratchet fingerprint is the empty string. + /// This is the "empty" ratchet state and the key will be all zeros. + /// + /// The ratchet fingerprint value is sensitive and should be hidden, + /// but the security of ZSSP can survive having this value leaked. + /// Operations on a ratchet fingerprint should be implemented in constant time, + /// but it is ok if they are not. pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { - self.nonempty().map(|rs| rs.fingerprint.deref()) + self.fingerprint.as_deref() } - /// Retrieve the ratchet key if it exists. - /// This function will return a key of all zeros if this ratchet state is the empty ratchet state. - pub fn key(&self) -> Option<&[u8; RATCHET_SIZE]> { - const ZERO_KEY: [u8; RATCHET_SIZE] = [0u8; RATCHET_SIZE]; - match self { - Null => None, - Empty => Some(&ZERO_KEY), - NonEmpty(rs) => Some(&rs.key), +} +impl Default for RatchetState { + fn default() -> Self { + Self::empty() + } +} + +/// An ordered pair of two 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 { + /// The first ratchet state from the pair. + pub state1: RatchetState, + /// The second ratchet state from the pair. It can, and usually will be `None`. + pub state2: Option, +} +impl RatchetStates { + /// Creates a new pair of ratchet states. The order of the arguments matters, and it should be + /// the same order that was originally given by an instance of the `RatchetUpdate` struct. + pub fn new(state1: RatchetState, state2: Option) -> Self { + Self { state1, state2 } + } + /// Creates a new initial pair of ratchet states, where the first ratchet state is the empty + /// ratchet state and the second is `None`. + /// + /// This value is the default value of `RatchetStates`. + pub fn new_initial_states() -> Self { + Self { state1: RatchetState::empty(), state2: None } + } + /// Creates a new initial pair of ratchet states from a one-time password. + /// The first ratchet state will be derived from this password, while the second will be `None`. + /// + /// If both sides of a session use the same one-time-password then they can use this pair to + /// connect with each other for the first time. This pair can be generated with this function, + /// saved to persistent storage, and eventually restored by the `ApplicationLayer` when we + /// attempt to form a session with the correct peer. + pub fn new_otp_states(otp: &[u8]) -> Self { + Self { + state1: RatchetState::new_from_otp::(otp), + state2: None, } } } -/// A ratchet key and fingerprint, -/// along with the length of the ratchet chain the keys were derived from. -#[derive(Clone, Eq)] -pub struct NonEmptyRatchetState { - pub key: Zeroizing<[u8; RATCHET_SIZE]>, - pub fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, - pub chain_len: u64, +impl Default for RatchetStates { + fn default() -> Self { + Self::new_initial_states() + } } -impl PartialEq for NonEmptyRatchetState { - fn eq(&self, other: &Self) -> bool { - secure_eq(&self.key, &other.key) && secure_eq(&self.fingerprint, &other.fingerprint) && self.chain_len == other.chain_len + +/// 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> { + /// Clones the `state1` and `state2` pair of ratchet states out of this struct and into a + /// `RatchetStates` instance. + pub fn to_states(&self) -> RatchetStates { + RatchetStates::new(self.state1.clone(), self.state2.cloned()) + } + /// Returns the new ratchet fingerprint that was added during this update, if any. + pub fn added_fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> { + if self.state1_was_just_added { + self.state1.fingerprint() + } else { + None + } + } + /// Returns the first ratchet fingerprint that was deleted during this update, if any. + pub fn deleted_fingerprint1(&self) -> Option<&[u8; RATCHET_SIZE]> { + if let Some(rs) = &self.deleted_state1 { + rs.fingerprint() + } else { + None + } + } + /// Returns the second fingerprint that was deleted during this update, if there was more than + /// one ratchet state deleted during this update. + /// + /// It is extremely rare that this function ever returns `Some`, but it is possible. + /// Users should explicitly test that their implementation is able to correctly delete more than + /// 1 ratchet state at a time, because it is unlikely to occur under normal conditions. + pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> { + if let Some(rs) = &self.deleted_state2 { + rs.fingerprint() + } else { + None + } } } diff --git a/src/result.rs b/src/result.rs index 9537b77..47da452 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,24 +1,25 @@ use std::sync::Arc; -use crate::{ApplicationLayer, Session}; +use crate::application::CryptoLayer; +use crate::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 { +pub enum OpenError { /// An invalid parameter was supplied to the function. InvalidPublicKey, + /// The given identity slice is too large to be fragmented. It must at most than 4096 bytes. + IdentityTooLarge, - RatchetIoError(IoError), + /// Looking up the ratchet state of the specified peer failed. + StorageError(StorageError), } /// 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 parameter was supplied to the function. - InvalidParameter, - /// The session has been marked as expired and refuses to send data. /// Several components of ZSSP can cause this to occur, but the most likely situation to be seen /// in practice is where rekeying repeatedly fails due to exceedingly bad network conditions. @@ -59,7 +60,7 @@ pub enum FaultType { /// An error that occurred during the receipt of a given packet. #[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub enum ReceiveError { +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. @@ -101,7 +102,7 @@ pub enum ReceiveError { /// One of the ratchet saving or lookup functions returned an error, so the packet had to be /// dropped. - RatchetIoError(IoError), + StorageError(StorageError), } macro_rules! byzantine_fault { @@ -120,13 +121,13 @@ pub(crate) use byzantine_fault; /// Result generated by the context packet receive function, with possible payloads. #[derive(Clone)] -pub enum ReceiveOk { +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), + 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. @@ -138,6 +139,16 @@ pub enum SessionEvent { /// 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, + /// The received packet was valid, and it contained the necessary keys to fully establish a new + /// session with Alice, the handshake initiator. + /// + /// However we had to downgrade our ratchet chain with this peer to a known state, + /// meaning either Alice corrupted their ratchet key storage, or someone is impersonating Alice. + /// The application should attempt to warn the user of this fact. + /// + /// 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. + 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. /// @@ -158,4 +169,9 @@ pub enum SessionEvent { Data(Vec), /// The received packet was some authentic protocol control packet. No action needs to be taken. Control, + /// In the process of establishing a session with Bob, the responder, + /// we had to downgrade our ratchet chain with this peer to a known state. + /// This means either Bob corrupted their ratchet key storage, or someone is impersonating Alice. + /// The application should attempt to warn the user of this fact. + DowngradedRatchetKey, } diff --git a/src/symmetric_state.rs b/src/symmetric_state.rs index 946e53a..e061efb 100644 --- a/src/symmetric_state.rs +++ b/src/symmetric_state.rs @@ -2,19 +2,19 @@ use std::marker::PhantomData; use zeroize::Zeroizing; -use crate::crypto::{AeadAesGcm, HashSha512, AES_256_KEY_SIZE, AES_GCM_IV_SIZE, AES_GCM_TAG_SIZE}; -use crate::proto::{HASHLEN, LABEL_KBKDF_CHAIN}; -use crate::ApplicationLayer; +use crate::application::CryptoLayer; +use crate::crypto::*; +use crate::proto::*; -pub struct SymmetricState { +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 App::Data>, + /// If anyone knows a better way to get rid of the "parameter `Crypto` is never used" error + /// please let me know. + _app: PhantomData Crypto::SessionData>, } -impl Clone for SymmetricState { +impl Clone for SymmetricState { fn clone(&self) -> Self { Self { k: self.k.clone(), @@ -25,14 +25,7 @@ impl Clone for SymmetricState { } } -const KBKDF_LABEL_START: usize = 1; -const KBKDF_LABEL_END: usize = KBKDF_LABEL_START + 4; -const KBKDF_CONTEXT_START: usize = KBKDF_LABEL_END + 1; -const KBKDF_LENGTH_START: usize = KBKDF_CONTEXT_START + HASHLEN; -const KBKDF_INPUT_SIZE: usize = KBKDF_LENGTH_START + 2; -const HASHLEN_BITS: usize = HASHLEN * 8; - -impl SymmetricState { +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 @@ -44,6 +37,7 @@ impl SymmetricState { /// * 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, input_key_material: &[u8], @@ -53,53 +47,63 @@ impl SymmetricState { output2: Option<&mut [u8; HASHLEN]>, output3: Option<&mut [u8; HASHLEN]>, ) { - let mut buffer = Zeroizing::new([0u8; KBKDF_INPUT_SIZE]); - let buffer: &mut [u8] = buffer.as_mut(); - buffer[0] = 1; - buffer[KBKDF_LABEL_START..KBKDF_LABEL_END].copy_from_slice(label); - buffer[KBKDF_LABEL_END] = 0x00; - buffer[KBKDF_CONTEXT_START..KBKDF_LENGTH_START].copy_from_slice(self.ck.as_ref()); - buffer[KBKDF_LENGTH_START..].copy_from_slice(&(num_outputs * HASHLEN_BITS as u16).to_be_bytes()); + let mut buffer = Zeroizing::new(Vec::new()); + buffer.push(1); + buffer.extend(label); + buffer.push(0x00); + buffer.extend(self.ck.as_ref()); + buffer.extend(&(num_outputs * 8 * HASHLEN as u16).to_be_bytes()); debug_assert!(num_outputs >= 1); - *output1 = App::Hash::hmac(input_key_material, &buffer); + *output1 = Crypto::Hash::hmac(input_key_material, &buffer); if let Some(output2) = output2 { debug_assert!(num_outputs >= 2); buffer[0] = 2; - *output2 = App::Hash::hmac(input_key_material, &buffer); + *output2 = Crypto::Hash::hmac(input_key_material, &buffer); } if let Some(output3) = output3 { debug_assert!(num_outputs >= 3); buffer[0] = 3; - *output3 = App::Hash::hmac(input_key_material, &buffer); + *output3 = Crypto::Hash::hmac(input_key_material, &buffer); } } - pub fn initialize(h: [u8; HASHLEN]) -> Self { + /// Corresponds to Noise `Initialize` on a SymmetricState. + pub fn initialize(h: &[u8; HASHLEN]) -> Self { Self { k: Zeroizing::default(), - ck: Zeroizing::new(h), - h, + ck: Zeroizing::new(*h), + h: *h, _app: PhantomData, } } + /// Corresponds to Noise `MixKey`. pub fn mix_key(&mut self, input_key_material: &[u8]) { let mut next_ck = [0u8; HASHLEN]; let mut temp_k = [0u8; HASHLEN]; - self.kbkdf(input_key_material, LABEL_KBKDF_CHAIN, 2, &mut next_ck, Some(&mut temp_k), None); + self.kbkdf( + 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 `MixHash`. pub fn mix_hash(&mut self, data: &[u8]) { - let mut hash = App::Hash::new(); + let mut hash = Crypto::Hash::new(); hash.update(&self.h); hash.update(data); self.h = hash.finish(); } + /// Corresponds to Noise `MixKeyAndHash`. pub fn mix_key_and_hash(&mut self, input_key_material: &[u8]) { let mut next_ck = [0u8; HASHLEN]; let mut temp_h = [0u8; HASHLEN]; @@ -118,19 +122,30 @@ impl SymmetricState { self.mix_hash(&temp_h); self.k.clone_from_slice(&temp_k[..AES_256_KEY_SIZE]); } - pub fn encrypt_and_hash_in_place(&mut self, iv: [u8; AES_GCM_IV_SIZE], plaintext_start: usize, buffer: &mut Vec) { - let tag = App::Aead::encrypt_in_place(&self.k, iv, Some(&self.h), &mut buffer[plaintext_start..]); + /// Corresponds to Noise `EncryptAndHash`. + pub fn encrypt_and_hash_in_place( + &mut self, + iv: [u8; AES_GCM_NONCE_SIZE], + plaintext_start: usize, + buffer: &mut Vec, + ) { + let tag = Crypto::Aead::encrypt_in_place(&self.k, &iv, Some(&self.h), &mut buffer[plaintext_start..]); buffer.extend(&tag); - let mut hash = App::Hash::new(); - hash.update(&buffer[plaintext_start..]); - self.h = hash.finish(); + self.mix_hash(&buffer[plaintext_start..]); } + /// Corresponds to Noise `DecryptAndHash`. #[must_use] - pub fn decrypt_and_hash_in_place(&mut self, iv: [u8; AES_GCM_IV_SIZE], buffer: &mut [u8], tag: [u8; AES_GCM_TAG_SIZE]) -> bool { - let mut hash = App::Hash::new(); + pub fn decrypt_and_hash_in_place( + &mut self, + iv: [u8; AES_GCM_NONCE_SIZE], + buffer: &mut [u8], + tag: [u8; AES_GCM_TAG_SIZE], + ) -> bool { + let mut hash = Crypto::Hash::new(); + hash.update(&self.h); hash.update(buffer); hash.update(&tag); - let ret = App::Aead::decrypt_in_place(&self.k, iv, Some(&self.h), buffer, tag); + let ret = Crypto::Aead::decrypt_in_place(&self.k, &iv, Some(&self.h), buffer, &tag); self.h = hash.finish(); ret } diff --git a/src/zeta.rs b/src/zeta.rs index 74026cd..7e8a25c 100644 --- a/src/zeta.rs +++ b/src/zeta.rs @@ -1,61 +1,44 @@ -use rand_core::RngCore; +use std::cell::RefCell; use std::collections::HashMap; use std::num::NonZeroU32; use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, Mutex, Weak}; +use std::sync::{Arc, Weak}; + +use rand_core::RngCore; use zeroize::Zeroizing; +use crate::application::{ApplicationLayer, RatchetState, RatchetStates, RatchetUpdate, CryptoLayer}; use crate::challenge::{gen_null_response, respond_to_challenge_in_place}; use crate::context::{log, ContextInner, SessionMap}; use crate::crypto::*; use crate::fragmentation::DefragBuffer; use crate::proto::*; -use crate::ratchet_state::RatchetState; use crate::result::{byzantine_fault, FaultType, OpenError, ReceiveError, SendError}; use crate::symmetric_state::SymmetricState; -use crate::ApplicationLayer; #[cfg(feature = "logging")] use crate::LogEvent::*; -/// 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. -pub(crate) fn to_nonce(packet_type: u8, counter: u64) -> [u8; AES_GCM_IV_SIZE] { - let mut ret = [0u8; AES_GCM_IV_SIZE]; - ret[3] = packet_type; - // Noise requires a big endian counter at the end of the Nonce - ret[4..].copy_from_slice(&counter.to_be_bytes()); - ret -} -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) struct Zeta { - ctx: Weak>, +/// Corresponds to the Zeta State Machine found in Section 4.1. +pub(crate) struct Zeta { + pub ctx: Weak>, /// An arbitrary application defined object associated with each session. - pub application_data: App::Data, + 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, - s_remote: App::PublicKey, + s_remote: Crypto::PublicKey, send_counter: u64, key_creation_counter: u64, key_index: bool, keys: [DuplexKey; 2], - ratchet_states: [RatchetState; 2], + ratchet_state1: RatchetState, + ratchet_state2: Option, pub hk_send: Zeroizing<[u8; AES_256_KEY_SIZE]>, resend_timer: i64, timeout_timer: i64, - pub beta: ZsspAutomata, + pub beta: ZetaAutomata, pub counter_antireplay_window: [u64; COUNTER_WINDOW_MAX_OOO], pub defrag: DefragBuffer, @@ -63,16 +46,16 @@ pub(crate) struct Zeta { /// ZeroTier Secure Session Protocol (ZSSP) Session. /// /// A FIPS/NIST compliant variant of Noise_XK with hybrid Kyber1024 PQ data forward secrecy. -pub struct Session(pub(crate) Mutex>); +pub struct Session(pub(crate) RefCell>); -pub(crate) struct StateB2 { - /// Can never be Null. +/// 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]>, - e_secret: App::KeyPair, - noise: SymmetricState, + e_secret: Crypto::KeyPair, + noise: SymmetricState, pub defrag: DefragBuffer, } @@ -89,21 +72,24 @@ pub(crate) struct Keys { kid: Option, } +/// Corresponds to the tuple of values the Transition Algorithms send to the remote peer in Section 4.3. #[derive(Clone)] -pub(crate) struct Packet(pub u32, pub [u8; AES_GCM_IV_SIZE], pub Vec); +pub(crate) struct Packet(pub u32, pub [u8; AES_GCM_NONCE_SIZE], pub Vec); +/// 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: App::KeyPair, - e1_secret: App::Kem, +pub(crate) struct StateA1 { + noise: SymmetricState, + e_secret: Crypto::KeyPair, + e1_secret: Crypto::Kem, identity: Vec, packet: Packet, } -pub(crate) enum ZsspAutomata { +/// Corresponds to the ZKE Automata found in Section 4.1 - Definition 2. +pub(crate) enum ZetaAutomata { Null, - A1(StateA1), + A1(StateA1), A3 { identity: Vec, packet: Packet, @@ -111,8 +97,8 @@ pub(crate) enum ZsspAutomata { S1, S2, R1 { - noise: SymmetricState, - e_secret: App::KeyPair, + noise: SymmetricState, + e_secret: Crypto::KeyPair, k1: Vec, }, R2 { @@ -120,24 +106,24 @@ pub(crate) enum ZsspAutomata { }, } -impl SymmetricState { - fn write_e(&mut self, rng: &Mutex, packet: &mut Vec) -> App::KeyPair { - let e_secret = App::KeyPair::generate(rng.lock().unwrap().deref_mut()); +impl SymmetricState { + fn write_e(&mut self, rng: &RefCell, packet: &mut Vec) -> Crypto::KeyPair { + let e_secret = Crypto::KeyPair::generate(rng.borrow_mut().deref_mut()); let pub_key = e_secret.public_key_bytes(); packet.extend(&pub_key); self.mix_hash(&pub_key); self.mix_key(&pub_key); e_secret } - fn read_e(&mut self, i: &mut usize, packet: &Vec) -> Option { + fn read_e(&mut self, i: &mut usize, packet: &Vec) -> Option { let j = *i + P384_PUBLIC_KEY_SIZE; let pub_key = &packet[*i..j]; self.mix_hash(pub_key); self.mix_key(pub_key); *i = j; - App::PublicKey::from_bytes((pub_key).try_into().unwrap()) + Crypto::PublicKey::from_bytes((pub_key).try_into().unwrap()) } - fn mix_dh(&mut self, secret: &App::KeyPair, remote: &App::PublicKey) -> Option<()> { + fn mix_dh(&mut self, secret: &Crypto::KeyPair, remote: &Crypto::PublicKey) -> Option<()> { if let Some(ecdh) = secret.agree(&remote).map(Zeroizing::new) { self.mix_key(ecdh.as_ref()); Some(()) @@ -147,6 +133,28 @@ impl SymmetricState { } } +/// 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())) +} /// Generate a random local key id that is currently unused. fn gen_kid(session_map: &HashMap, rng: &mut impl RngCore) -> NonZeroU32 { loop { @@ -157,8 +165,24 @@ fn gen_kid(session_map: &HashMap, rng: &mut impl RngCore) -> N } } } +fn remap( + session: &Arc>, + zeta: &Zeta, + rng: &RefCell, + session_map: &SessionMap, +) -> NonZeroU32 { + let mut session_map = session_map.borrow_mut(); + let weak = if let Some(Some(weak)) = zeta.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(), rng.borrow_mut().deref_mut()); + session_map.insert(new_kid_recv, weak); + new_kid_recv +} -impl Zeta { +impl Zeta { pub(crate) fn check_counter_window(&self, c: u64) -> bool { let slot = &self.counter_antireplay_window[c as usize % self.counter_antireplay_window.len()]; let adj_counter = c.saturating_add(1); @@ -185,10 +209,10 @@ impl Zeta { fn expire(&mut self) { self.resend_timer = i64::MAX; self.timeout_timer = i64::MAX; - self.beta = ZsspAutomata::Null; + self.beta = ZetaAutomata::Null; if let Some(ctx) = self.ctx.upgrade() { - let mut session_map = ctx.session_map.lock().unwrap(); - let mut sessions = ctx.sessions.lock().unwrap(); + let mut session_map = ctx.session_map.borrow_mut(); + let mut sessions = ctx.sessions.borrow_mut(); for key in &self.keys { if let Some(kid_recv) = key.recv.kid { if let Some(weak) = session_map.remove(&kid_recv) { @@ -201,19 +225,33 @@ impl Zeta { pub(crate) fn next_timer(&self) -> i64 { self.timeout_timer.min(self.resend_timer) } + fn get_counter(&mut self) -> Option<(u64, bool)> { + let c = self.send_counter; + if c >= HARD_EXPIRATION { + return None; + } + self.send_counter += 1; + Some(( + c, + c >= self + .key_creation_counter + .saturating_add(Crypto::SETTINGS.rekey_after_key_uses), + )) + } } fn create_a1_state( - rng: &Mutex, - s_remote: &App::PublicKey, + rng: &RefCell<::Rng>, + s_remote: &::PublicKey, kid_recv: NonZeroU32, - ratchet_states: &[RatchetState; 2], + ratchet_state1: &RatchetState, + ratchet_state2: Option<&RatchetState>, identity: Vec, -) -> Option> { +) -> Option> { // <- s // ... // -> e, es, e1 - let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); + let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); let mut x1 = Vec::new(); // Noise process prologue. let kid = kid_recv.get().to_be_bytes(); @@ -226,21 +264,22 @@ fn create_a1_state( noise.mix_dh(&e_secret, s_remote)?; // Process message pattern 1 e1 token. let i = x1.len(); - let (e1_secret, e1_public) = App::Kem::generate(rng.lock().unwrap().deref_mut()); + let (e1_secret, e1_public) = ::Kem::generate(rng.borrow_mut().deref_mut()); x1.extend(&e1_public); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 0), i, &mut x1); // Process message pattern 1 payload. let i = x1.len(); - for r in ratchet_states { - if let Some(rf) = r.fingerprint() { - x1.extend(rf); - } + if let Some(rf) = ratchet_state1.fingerprint() { + x1.extend(rf.as_ref()); + } + if let Some(Some(rf)) = ratchet_state2.map(|rs| rs.fingerprint()) { + x1.extend(rf.as_ref()); } noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), i, &mut x1); let c = u64::from_be_bytes(x1[x1.len() - 8..].try_into().unwrap()); - x1.extend(&gen_null_response(rng.lock().unwrap().deref_mut())); + x1.extend(&gen_null_response(rng.borrow_mut().deref_mut())); Some(StateA1 { noise, e_secret, @@ -249,22 +288,25 @@ fn create_a1_state( packet: Packet(0, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, c), x1), }) } +/// Corresponds to Transition Algorithm 1 found in Section 4.3. pub(crate) fn trans_to_a1( - app: App, - ctx: &Arc>, - s_remote: App::PublicKey, - application_data: App::Data, + mut app: App, + ctx: &Arc>, + s_remote: ::PublicKey, + session_data: ::SessionData, identity: Vec, send: impl FnOnce(&Packet), -) -> Result>, OpenError> { +) -> Result>, OpenError> { let ratchet_states = app - .restore_by_identity(&s_remote, &application_data) - .map_err(|e| OpenError::RatchetIoError(e))?; + .restore_by_identity(&s_remote, &session_data) + .map_err(|e| OpenError::StorageError(e))?; + let RatchetStates { state1, state2 } = ratchet_states.unwrap_or_default(); - let mut session_map = ctx.session_map.lock().unwrap(); - let kid_recv = gen_kid(session_map.deref(), ctx.rng.lock().unwrap().deref_mut()); + let mut session_map = ctx.session_map.borrow_mut(); + let kid_recv = gen_kid(session_map.deref(), ctx.rng.borrow_mut().deref_mut()); - let a1 = create_a1_state(&ctx.rng, &s_remote, kid_recv, &ratchet_states, identity).ok_or(OpenError::InvalidPublicKey)?; + let a1 = create_a1_state::(&ctx.rng, &s_remote, kid_recv, &state1, state2.as_ref(), identity) + .ok_or(OpenError::InvalidPublicKey)?; let packet = a1.packet.clone(); let (hk_recv, hk_send) = a1.noise.get_ask(LABEL_HEADER_KEY); @@ -272,48 +314,57 @@ pub(crate) fn trans_to_a1( let current_time = app.time(); let mut zeta = Zeta { ctx: Arc::downgrade(ctx), - application_data, + session_data, was_bob: false, s_remote, - send_counter: INIT_COUNTER, + send_counter: 0, key_creation_counter: 0, counter_antireplay_window: std::array::from_fn(|_| 0), defrag: DefragBuffer::new(Some(hk_recv)), key_index: true, keys: [DuplexKey::default(), DuplexKey::default()], - ratchet_states, + ratchet_state1: state1, + ratchet_state2: state2, hk_send, - resend_timer: current_time + App::SETTINGS.resend_time as i64, - timeout_timer: current_time + App::SETTINGS.initial_offer_timeout as i64, - beta: ZsspAutomata::A1(a1), + resend_timer: current_time + ::SETTINGS.resend_time as i64, + timeout_timer: current_time + ::SETTINGS.initial_offer_timeout as i64, + beta: ZetaAutomata::A1(a1), }; zeta.key_mut(true).recv.kid = Some(kid_recv); - let session = Arc::new(Session(Mutex::new(zeta))); + let session = Arc::new(Session(RefCell::new(zeta))); session_map.insert(kid_recv, Arc::downgrade(&session)); - ctx.sessions.lock().unwrap().insert(Arc::as_ptr(&session), Arc::downgrade(&session)); + ctx.sessions + .borrow_mut() + .insert(Arc::as_ptr(&session), Arc::downgrade(&session)); send(&packet); Ok(session) } -pub(crate) fn respond_to_challenge(zeta: &mut Zeta, rng: &Mutex, challenge: &[u8; CHALLENGE_SIZE]) { - if let ZsspAutomata::A1(StateA1 { packet: Packet(_, _, x1), .. }) = &mut zeta.beta { +/// Corresponds to Algorithm 13 found in Section 5. +pub(crate) fn respond_to_challenge( + zeta: &mut Zeta, + rng: &RefCell<::Rng>, + challenge: &[u8; CHALLENGE_SIZE], +) { + if let ZetaAutomata::A1(StateA1 { packet: Packet(_, _, x1), .. }) = &mut zeta.beta { let response_start = x1.len() - CHALLENGE_SIZE; - respond_to_challenge_in_place::( - rng.lock().unwrap().deref_mut(), + respond_to_challenge_in_place::<::Rng, ::Hash>( + rng.borrow_mut().deref_mut(), challenge, (&mut x1[response_start..]).try_into().unwrap(), ); } } +/// Corresponds to Transition Algorithm 2 found in Section 4.3. pub(crate) fn received_x1_trans( - app: &App, - ctx: &ContextInner, - n: [u8; AES_GCM_IV_SIZE], + app: &mut App, + ctx: &ContextInner, + n: [u8; AES_GCM_NONCE_SIZE], mut x1: Vec, send: impl FnOnce(&Packet, &[u8; AES_256_KEY_SIZE]), -) -> Result<(), ReceiveError> { +) -> Result<(), ReceiveError> { use FaultType::*; // <- s // ... @@ -322,21 +373,24 @@ pub(crate) fn received_x1_trans( if !(HANDSHAKE_HELLO_MIN_SIZE..=HANDSHAKE_HELLO_MAX_SIZE).contains(&x1.len()) { return Err(byzantine_fault!(InvalidPacket, true)); } - if &n[AES_GCM_IV_SIZE - 8..] != &x1[x1.len() - 8..] { + if &n[AES_GCM_NONCE_SIZE - 8..] != &x1[x1.len() - 8..] { return Err(byzantine_fault!(FailedAuth, true)); } - let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); + let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_XK); let mut i = 0; // Noise process prologue. let j = i + KID_SIZE; noise.mix_hash(&x1[i..j]); - let kid_send = NonZeroU32::new(u32::from_be_bytes(x1[i..j].try_into().unwrap())).ok_or(byzantine_fault!(InvalidPacket, true))?; + let kid_send = NonZeroU32::new(u32::from_be_bytes(x1[i..j].try_into().unwrap())) + .ok_or(byzantine_fault!(InvalidPacket, true))?; noise.mix_hash(&ctx.s_secret.public_key_bytes()); i = j; // Process message pattern 1 e token. let e_remote = noise.read_e(&mut i, &x1).ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 1 es token. - noise.mix_dh(&ctx.s_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(&ctx.s_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 1 e1 token. let j = i + KYBER_PUBLIC_KEY_SIZE; let k = j + AES_GCM_TAG_SIZE; @@ -355,58 +409,66 @@ pub(crate) fn received_x1_trans( return Err(byzantine_fault!(FailedAuth, true)); } - let mut ratchet_state = RatchetState::Null; + let mut ratchet_state = None; while i + RATCHET_SIZE <= j { - match app.restore_by_fingerprint((&x1[i..i + RATCHET_SIZE]).try_into().unwrap()) { - Ok(RatchetState::Null) | Ok(RatchetState::Empty) => {} - Ok(rs) => { - ratchet_state = rs; + let rf = (&x1[i..i + RATCHET_SIZE]).try_into().unwrap(); + match app.restore_by_fingerprint(rf) { + Ok(None) => {} + Ok(Some(rs)) => { + ratchet_state = Some(rs); break; } - Err(e) => return Err(ReceiveError::RatchetIoError(e)), + Err(e) => return Err(ReceiveError::StorageError(e)), } i += RATCHET_SIZE; } - if ratchet_state.is_null() { + let ratchet_state = if let Some(rs) = ratchet_state { + rs + } else { if app.hello_requires_recognized_ratchet() { return Err(byzantine_fault!(FailedAuth, true)); } - ratchet_state = RatchetState::Empty; - } + RatchetState::empty() + }; let (hk_send, hk_recv) = noise.get_ask(LABEL_HEADER_KEY); let mut x2 = Vec::new(); // Process message pattern 2 e token. let e_secret = noise.write_e(&ctx.rng, &mut x2); // Process message pattern 2 ee token. - noise.mix_dh(&e_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(&e_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 ekem1 token. let i = x2.len(); - let (ekem1, ekem1_secret) = App::Kem::encapsulate(ctx.rng.lock().unwrap().deref_mut(), (&x1[e1_start..e1_end]).try_into().unwrap()) - .map(|(ct, secret)| (ct, Zeroizing::new(secret))) - .ok_or(byzantine_fault!(FailedAuth, true))?; + let (ekem1, ekem1_secret) = ::Kem::encapsulate( + ctx.rng.borrow_mut().deref_mut(), + (&x1[e1_start..e1_end]).try_into().unwrap(), + ) + .map(|(ct, secret)| (ct, Zeroizing::new(secret))) + .ok_or(byzantine_fault!(FailedAuth, true))?; x2.extend(ekem1); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), i, &mut x2); noise.mix_key(ekem1_secret.as_ref()); drop(ekem1_secret); // Process message pattern 2 psk2 token. - noise.mix_key_and_hash(ratchet_state.key().unwrap()); + noise.mix_key_and_hash(ratchet_state.key()); // Process message pattern 2 payload. - let session_map = ctx.session_map.lock().unwrap(); - let kid_recv = gen_kid(session_map.deref(), ctx.rng.lock().unwrap().deref_mut()); + let session_map = ctx.session_map.borrow_mut(); + let kid_recv = gen_kid(session_map.deref(), ctx.rng.borrow_mut().deref_mut()); let i = x2.len(); x2.extend(kid_recv.get().to_be_bytes()); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, 0), i, &mut x2); let i = x2.len(); - let mut c = 0u64.to_be_bytes(); + 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); - ctx.b2_map.lock().unwrap().insert( + ctx.b2_map.borrow_mut().insert( kid_recv, StateB2 { ratchet_state, @@ -419,19 +481,23 @@ pub(crate) fn received_x1_trans( }, ); - send(&Packet(kid_send.get(), to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, c), x2), &hk_send); + send( + &Packet(kid_send.get(), to_nonce(PACKET_TYPE_HANDSHAKE_RESPONSE, c), x2), + &hk_send, + ); Ok(()) } +/// Corresponds to Transition Algorithm 3 found in Section 4.3. pub(crate) fn received_x2_trans( - zeta: &mut Zeta, - session: &Arc>, - app: &App, - ctx: &Arc>, + zeta: &mut Zeta, + session: &Arc>, + app: &mut App, + ctx: &Arc>, kid: NonZeroU32, - n: [u8; AES_GCM_IV_SIZE], + n: [u8; AES_GCM_NONCE_SIZE], mut x2: Vec, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), -) -> Result<(), ReceiveError> { +) -> Result> { use FaultType::*; // <- e, ee, ekem1, psk // -> s, se @@ -442,17 +508,20 @@ pub(crate) fn received_x2_trans( return Err(byzantine_fault!(UnknownLocalKeyId, true)); } let (_, c) = from_nonce(&n); - if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || &n[AES_GCM_IV_SIZE - 3..] != &x2[x2.len() - 3..] { + if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || &n[AES_GCM_NONCE_SIZE - 3..] != &x2[x2.len() - 3..] { return Err(byzantine_fault!(FailedAuth, true)); } + let mut should_warn_missing_ratchet = false; let result = (|| { - if let ZsspAutomata::A1(StateA1 { noise, e_secret, e1_secret, identity, .. }) = &zeta.beta { + if let ZetaAutomata::A1(StateA1 { noise, e_secret, e1_secret, identity, .. }) = &zeta.beta { let mut noise = noise.clone(); let mut i = 0; // Process message pattern 2 e token. let e_remote = noise.read_e(&mut i, &x2).ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 ee token. - noise.mix_dh(e_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(e_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 ekem1 token. let j = i + KYBER_CIPHERTEXT_SIZE; let k = j + AES_GCM_TAG_SIZE; @@ -478,7 +547,7 @@ pub(crate) fn received_x2_trans( 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 test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState)> { + let test_ratchet_key = |ratchet_key| -> Option<(NonZeroU32, SymmetricState)> { let mut noise = noise.clone(); let mut payload = payload.clone(); // Process message pattern 2 psk token. @@ -490,19 +559,15 @@ pub(crate) fn received_x2_trans( NonZeroU32::new(u32::from_be_bytes(payload)).map(|kid2| (kid2, noise)) }; // Check first key. - let mut ratchet_i = 0; - let mut result = None; - let mut chain_len = 0; - if let Some(key) = zeta.ratchet_states[0].key() { - chain_len = zeta.ratchet_states[0].chain_len(); - result = test_ratchet_key(key); - } + let mut ratchet_i = 1; + let mut chain_len = zeta.ratchet_state1.chain_len(); + let mut result = test_ratchet_key(zeta.ratchet_state1.key()); // Check second key. if result.is_none() { - ratchet_i = 1; - if let Some(key) = zeta.ratchet_states[1].key() { - chain_len = zeta.ratchet_states[1].chain_len(); - result = test_ratchet_key(key); + ratchet_i = 2; + if let Some(rs) = zeta.ratchet_state2.as_ref() { + chain_len = rs.chain_len(); + result = test_ratchet_key(rs.key()); } } // Check zero key. @@ -510,7 +575,7 @@ pub(crate) fn received_x2_trans( chain_len = 0; result = test_ratchet_key(&[0u8; RATCHET_SIZE]); if result.is_some() { - // TODO: add some kind of warning callback or signal. + should_warn_missing_ratchet = true; } } @@ -522,24 +587,35 @@ pub(crate) fn received_x2_trans( x3.extend(&ctx.s_secret.public_key_bytes()); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 1), i, &mut x3); // Process message pattern 3 se token. - noise.mix_dh(&ctx.s_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; - // Process message pattern 3 payload token. + noise + .mix_dh(&ctx.s_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; + // Process message pattern 3 payload. let i = x3.len(); x3.extend(identity); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 0), i, &mut x3); let (rk, rf) = noise.get_ask(LABEL_RATCHET_STATE); - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, chain_len + 1); + let new_ratchet_state = RatchetState::new(rk, rf, chain_len + 1); - let ratchet_to_preserve = &zeta.ratchet_states[ratchet_i]; + let (ratchet_to_preserve, ratchet_to_delete) = if ratchet_i == 1 { + (Some(&zeta.ratchet_state1), zeta.ratchet_state2.as_ref()) + } else { + (zeta.ratchet_state2.as_ref(), Some(&zeta.ratchet_state1)) + }; let result = app.save_ratchet_state( &zeta.s_remote, - &zeta.application_data, - [&zeta.ratchet_states[0], &zeta.ratchet_states[1]], - [&new_ratchet_state, ratchet_to_preserve], + &zeta.session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: ratchet_to_preserve, + state1_was_just_added: true, + deleted_state1: ratchet_to_delete, + deleted_state2: None, + }, ); if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); + return Err(ReceiveError::StorageError(e)); } let (kek_recv, kek_send) = noise.get_ask(LABEL_KEX_KEY); @@ -552,14 +628,14 @@ pub(crate) fn received_x2_trans( zeta.key_mut(true).send.nk = Some(nk_send); zeta.key_mut(true).recv.kek = Some(kek_recv); zeta.key_mut(true).recv.nk = Some(nk_recv); - zeta.ratchet_states[1] = zeta.ratchet_states[ratchet_i].clone(); - zeta.ratchet_states[0] = new_ratchet_state; + zeta.ratchet_state2 = Some(zeta.ratchet_state1.clone()); + zeta.ratchet_state1 = new_ratchet_state; let current_time = app.time(); zeta.key_creation_counter = zeta.send_counter; - zeta.resend_timer = current_time + App::SETTINGS.resend_time as i64; - zeta.timeout_timer = current_time + App::SETTINGS.initial_offer_timeout as i64; + zeta.resend_timer = current_time + ::SETTINGS.resend_time as i64; + zeta.timeout_timer = current_time + ::SETTINGS.initial_offer_timeout as i64; let packet = Packet(kid_send.get(), n, x3); - zeta.beta = ZsspAutomata::A3 { identity, packet: packet.clone() }; + zeta.beta = ZetaAutomata::A3 { identity, packet: packet.clone() }; Ok(packet) } else { @@ -567,23 +643,48 @@ pub(crate) fn received_x2_trans( } })(); match &result { - Err(ReceiveError::ByzantineFault { .. }) => timeout_trans(zeta, session, app, ctx, app.time(), send), + Err(ReceiveError::ByzantineFault { .. }) => { + let current_time = app.time(); + timeout_trans(zeta, session, app, ctx, current_time, send); + } Ok(packet) => send(packet, Some(&zeta.hk_send)), _ => {} } - result.map(|_| ()) + result.map(|_| should_warn_missing_ratchet) } +fn send_control( + zeta: &mut Zeta, + packet_type: u8, + mut payload: Vec, + send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), +) -> bool { + if let Some((c, _)) = zeta.get_counter() { + if let (Some(kek), Some(kid)) = (zeta.key_ref(false).send.kek.as_ref(), zeta.key_ref(false).send.kid) { + let nonce = to_nonce(packet_type, c); + let tag = ::Aead::encrypt_in_place(kek, &nonce, None, &mut payload); + payload.extend(tag); + + send(&Packet(kid.get(), nonce, payload), Some(&zeta.hk_send)); + true + } else { + false + } + } else { + false + } +} +/// Corresponds to Transition Algorithm 4 found in Section 4.3. pub(crate) fn received_x3_trans( - zeta: StateB2, - app: &App, - ctx: &Arc>, + zeta: StateB2, + app: &mut App, + ctx: &Arc>, kid: NonZeroU32, mut x3: Vec, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), -) -> Result>, ReceiveError> { +) -> Result<(Arc>, bool), ReceiveError> { use FaultType::*; // -> s, se - if x3.len() < HANDSHAKE_COMPLETION_MIN_SIZE { + if !(HANDSHAKE_COMPLETION_MIN_SIZE..=HANDSHAKE_COMPLETION_MAX_SIZE).contains(&x3.len()) { return Err(byzantine_fault!(InvalidPacket, true)); } if kid != zeta.kid_recv { @@ -599,10 +700,13 @@ pub(crate) fn received_x3_trans( if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 1), &mut x3[i..j], tag) { return Err(byzantine_fault!(FailedAuth, true)); } - let s_remote = App::PublicKey::from_bytes((&x3[i..j]).try_into().unwrap()).ok_or(byzantine_fault!(FailedAuth, true))?; + let s_remote = + ::PublicKey::from_bytes((&x3[i..j]).try_into().unwrap()).ok_or(byzantine_fault!(FailedAuth, true))?; i = k; // Process message pattern 3 se token. - noise.mix_dh(&zeta.e_secret, &s_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(&zeta.e_secret, &s_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 3 payload. let k = x3.len(); let j = k - AES_GCM_TAG_SIZE; @@ -614,32 +718,32 @@ pub(crate) fn received_x3_trans( let identity_end = j; let (kek_send, kek_recv) = noise.get_ask(LABEL_KEX_KEY); - let c = INIT_COUNTER; + let c = 0; - let (responder_disallows_downgrade, responder_silently_rejects) = app.check_accept_session(&s_remote, &x3[identity_start..identity_end]); + 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 = || { let mut d = Vec::::new(); let n = to_nonce(PACKET_TYPE_SESSION_REJECTED, c); - let tag = App::Aead::encrypt_in_place(&kek_send, n, None, &mut []); + let tag = ::Aead::encrypt_in_place(&kek_send, &n, None, &mut []); d.extend(&tag); // 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. Packet(zeta.kid_send.get(), n, d) }; - if let Some((responder_disallows_downgrade, application_data)) = responder_disallows_downgrade { - let result = app.restore_by_identity(&s_remote, &application_data); + if let Some(session_data) = session_data { + let result = app.restore_by_identity(&s_remote, &session_data); match result { - Ok(true_ratchet_states) => { - let mut has_match = false; - for rs in &true_ratchet_states { - if !rs.is_null() { - has_match |= &zeta.ratchet_state == rs; - } - } - if !has_match { - if !responder_disallows_downgrade && zeta.ratchet_state.is_empty() { - // TODO: add some kind of warning callback or signal. + 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(&create_reject(), Some(&zeta.hk_send)) @@ -650,20 +754,25 @@ pub(crate) fn received_x3_trans( let (rk, rf) = noise.get_ask(LABEL_RATCHET_STATE); // We must make sure the ratchet key is saved before we transition. - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, zeta.ratchet_state.chain_len() + 1); + let new_ratchet_state = RatchetState::new(rk, rf, zeta.ratchet_state.chain_len() + 1); let result = app.save_ratchet_state( &s_remote, - &application_data, - [&true_ratchet_states[0], &true_ratchet_states[1]], - [&new_ratchet_state, &RatchetState::Null], + &session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: None, + state1_was_just_added: true, + deleted_state1: Some(&state1), + deleted_state2: state2.as_ref(), + }, ); if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); + return Err(ReceiveError::StorageError(e)); } let mut c1 = Vec::new(); let n = to_nonce(PACKET_TYPE_KEY_CONFIRM, c); - let tag = App::Aead::encrypt_in_place(&kek_send, n, None, &mut []); + let tag = ::Aead::encrypt_in_place(&kek_send, &n, None, &mut []); c1.extend(&tag); let (nk1, nk2) = noise.split(); @@ -673,7 +782,7 @@ pub(crate) fn received_x3_trans( }; let current_time = app.time(); - let mut session_map = ctx.session_map.lock().unwrap(); + let mut session_map = ctx.session_map.borrow_mut(); 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 @@ -681,30 +790,33 @@ pub(crate) fn received_x3_trans( Occupied(_) => return Err(byzantine_fault!(OutOfSequence, false)), Vacant(entry) => entry, }; - let session = Arc::new(Session(Mutex::new(Zeta { + let session = Arc::new(Session(RefCell::new(Zeta { ctx: Arc::downgrade(ctx), - application_data, + session_data, was_bob: true, s_remote, - send_counter: INIT_COUNTER + 1, - key_creation_counter: INIT_COUNTER + 1, + send_counter: 1, + key_creation_counter: 1, key_index: false, keys: [keys, DuplexKey::default()], - ratchet_states: [new_ratchet_state, RatchetState::Null], + ratchet_state1: new_ratchet_state, + ratchet_state2: None, hk_send: zeta.hk_send.clone(), - resend_timer: current_time + App::SETTINGS.resend_time as i64, - timeout_timer: current_time + App::SETTINGS.rekey_timeout as i64, - beta: ZsspAutomata::S1, + resend_timer: current_time + ::SETTINGS.resend_time as i64, + timeout_timer: current_time + ::SETTINGS.rekey_timeout as i64, + beta: ZetaAutomata::S1, counter_antireplay_window: std::array::from_fn(|_| 0), defrag: zeta.defrag, }))); entry.insert(Arc::downgrade(&session)); - ctx.sessions.lock().unwrap().insert(Arc::as_ptr(&session), Arc::downgrade(&session)); + ctx.sessions + .borrow_mut() + .insert(Arc::as_ptr(&session), Arc::downgrade(&session)); send(&Packet(zeta.kid_send.get(), n, c1), Some(&zeta.hk_send)); - Ok(session) + Ok((session, should_warn_missing_ratchet)) } - Err(e) => Err(ReceiveError::RatchetIoError(e)), + Err(e) => Err(ReceiveError::StorageError(e)), } } else { if !responder_silently_rejects { @@ -713,15 +825,16 @@ pub(crate) fn received_x3_trans( Err(byzantine_fault!(FailedAuth, true)) } } +/// Corresponds to Transition Algorithm 5 found in Section 4.3. pub(crate) fn received_c1_trans( - zeta: &mut Zeta, - app: &App, - rng: &Mutex, + zeta: &mut Zeta, + app: &mut App, + rng: &RefCell<::Rng>, kid: NonZeroU32, - n: [u8; AES_GCM_IV_SIZE], + n: [u8; AES_GCM_NONCE_SIZE], c1: Vec, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), -) -> Result> { +) -> Result> { use FaultType::*; if c1.len() != KEY_CONFIRMATION_SIZE { @@ -737,9 +850,14 @@ pub(crate) fn received_c1_trans( return Err(byzantine_fault!(OutOfSequence, false)); }; - let specified_key = zeta.key_ref(is_other).recv.kek.as_ref().ok_or(byzantine_fault!(OutOfSequence, true))?; + let specified_key = zeta + .key_ref(is_other) + .recv + .kek + .as_ref() + .ok_or(byzantine_fault!(OutOfSequence, true))?; let tag = c1[..].try_into().unwrap(); - if !App::Aead::decrypt_in_place(specified_key, n, None, &mut [], tag) { + if !::Aead::decrypt_in_place(specified_key, &n, None, &mut [], tag) { return Err(byzantine_fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); @@ -747,51 +865,51 @@ pub(crate) fn received_c1_trans( return Err(byzantine_fault!(ExpiredCounter, true)); } - let just_establised = is_other && matches!(&zeta.beta, ZsspAutomata::A3 { .. }); + let just_establised = is_other && matches!(&zeta.beta, ZetaAutomata::A3 { .. }); if is_other { - if let ZsspAutomata::A3 { .. } | ZsspAutomata::R2 { .. } = &zeta.beta { - if !zeta.ratchet_states[1].is_null() { + if let ZetaAutomata::A3 { .. } | ZetaAutomata::R2 { .. } = &zeta.beta { + if zeta.ratchet_state2.is_some() { let result = app.save_ratchet_state( &zeta.s_remote, - &zeta.application_data, - [&zeta.ratchet_states[0], &zeta.ratchet_states[1]], - [&zeta.ratchet_states[0], &RatchetState::Null], + &zeta.session_data, + RatchetUpdate { + state1: &zeta.ratchet_state1, + state2: None, + state1_was_just_added: false, + deleted_state1: zeta.ratchet_state2.as_ref(), + deleted_state2: None, + }, ); if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); + return Err(ReceiveError::StorageError(e)); } } - zeta.ratchet_states[1] = RatchetState::Null; + zeta.ratchet_state2 = None; zeta.key_index ^= true; - zeta.timeout_timer = app.time() - + App::SETTINGS - .rekey_after_time - .saturating_sub(rng.lock().unwrap().next_u64() % App::SETTINGS.rekey_time_max_jitter) as i64; + let r = rng.borrow_mut().next_u64() % ::SETTINGS.rekey_time_max_jitter; + zeta.timeout_timer = app.time() + ::SETTINGS.rekey_after_time.saturating_sub(r) as i64; zeta.resend_timer = i64::MAX; - zeta.beta = ZsspAutomata::S2; + zeta.beta = ZetaAutomata::S2; } } - let mut c2 = Vec::new(); + let c2 = Vec::new(); + if !send_control::(zeta, PACKET_TYPE_ACK, c2, send) { + return Err(byzantine_fault!(OutOfSequence, true)); + } - let c = zeta.send_counter; - zeta.send_counter += 1; - let n = to_nonce(PACKET_TYPE_ACK, c); - let latest_confirmed_key = zeta.key_ref(false).send.kek.as_ref().ok_or(byzantine_fault!(OutOfSequence, true))?; - let tag = App::Aead::encrypt_in_place(latest_confirmed_key, n, None, &mut []); - c2.extend(&tag); - - send(&Packet(zeta.key_ref(false).send.kid.unwrap().get(), n, c2), Some(&zeta.hk_send)); 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( - zeta: &mut Zeta, - app: &App, - rng: &Mutex, + zeta: &mut Zeta, + app: &mut App, + rng: &RefCell<::Rng>, kid: NonZeroU32, - n: [u8; AES_GCM_IV_SIZE], + n: [u8; AES_GCM_NONCE_SIZE], c2: Vec, -) -> Result<(), ReceiveError> { +) -> Result<(), ReceiveError> { use FaultType::*; if c2.len() != ACKNOWLEDGEMENT_SIZE { @@ -801,13 +919,13 @@ pub(crate) fn received_c2_trans( // Some acknowledgement may have arrived extremely delayed. return Err(byzantine_fault!(UnknownLocalKeyId, false)); } - if !matches!(&zeta.beta, ZsspAutomata::S1) { + if !matches!(&zeta.beta, ZetaAutomata::S1) { // Some acknowledgement may have arrived extremely delayed. return Err(byzantine_fault!(OutOfSequence, false)); } let tag = c2[..].try_into().unwrap(); - if !App::Aead::decrypt_in_place(zeta.key_ref(false).recv.kek.as_ref().unwrap(), n, None, &mut [], tag) { + if !::Aead::decrypt_in_place(zeta.key_ref(false).recv.kek.as_ref().unwrap(), &n, None, &mut [], tag) { return Err(byzantine_fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); @@ -815,31 +933,31 @@ pub(crate) fn received_c2_trans( return Err(byzantine_fault!(ExpiredCounter, true)); } - zeta.timeout_timer = app.time() - + App::SETTINGS - .rekey_after_time - .saturating_sub(rng.lock().unwrap().next_u64() % App::SETTINGS.rekey_time_max_jitter) as i64; + let r = rng.borrow_mut().next_u64() % ::SETTINGS.rekey_time_max_jitter; + zeta.timeout_timer = app.time() + ::SETTINGS.rekey_after_time.saturating_sub(r) as i64; zeta.resend_timer = i64::MAX; - zeta.beta = ZsspAutomata::S2; + zeta.beta = ZetaAutomata::S2; Ok(()) } +/// Corresponds to the trivial Transition Algorithm described for processing D packets found in +/// Section 4.3. pub(crate) fn received_d_trans( - zeta: &mut Zeta, + zeta: &mut Zeta, kid: NonZeroU32, - n: [u8; AES_GCM_IV_SIZE], + n: [u8; AES_GCM_NONCE_SIZE], d: Vec, -) -> Result<(), ReceiveError> { +) -> Result<(), ReceiveError> { use FaultType::*; if d.len() != SESSION_REJECTED_SIZE { return Err(byzantine_fault!(InvalidPacket, true)); } - if Some(kid) != zeta.key_ref(true).recv.kid || !matches!(&zeta.beta, ZsspAutomata::A3 { .. }) { + if Some(kid) != zeta.key_ref(true).recv.kid || !matches!(&zeta.beta, ZetaAutomata::A3 { .. }) { return Err(byzantine_fault!(OutOfSequence, true)); } let tag = d[..].try_into().unwrap(); - if !App::Aead::decrypt_in_place(zeta.key_ref(true).recv.kek.as_ref().unwrap(), n, None, &mut [], tag) { + if !::Aead::decrypt_in_place(zeta.key_ref(true).recv.kek.as_ref().unwrap(), &n, None, &mut [], tag) { return Err(byzantine_fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); @@ -850,94 +968,85 @@ pub(crate) fn received_d_trans( zeta.expire(); Ok(()) } +/// Corresponds to the timer rules of the Zeta State Machine found in Section 4.1 - Definition 3. pub(crate) fn service( - zeta: &mut Zeta, - session: &Arc>, - ctx: &Arc>, - app: &App, + zeta: &mut Zeta, + session: &Arc>, + ctx: &Arc>, + app: &mut App, current_time: i64, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), ) { if zeta.timeout_timer <= current_time { timeout_trans(zeta, session, app, ctx, current_time, send); } else if zeta.resend_timer <= current_time { - zeta.resend_timer = current_time + App::SETTINGS.resend_time as i64; + // Corresponds to the resend timer rules found in Section 4.1 - Definition 3. + zeta.resend_timer = current_time + ::SETTINGS.resend_time as i64; - let (p, mut control_payload) = match &zeta.beta { - ZsspAutomata::Null => return, - ZsspAutomata::A1(StateA1 { packet, .. }) => { + let (p, control_payload) = match &zeta.beta { + ZetaAutomata::Null => return, + ZetaAutomata::A1(StateA1 { packet, .. }) => { log!(app, ResentX1(session)); return send(packet, None); } - ZsspAutomata::A3 { packet, .. } => { + ZetaAutomata::A3 { packet, .. } => { log!(app, ResentX3(session)); return send(packet, Some(&zeta.hk_send)); } - ZsspAutomata::S1 => { + ZetaAutomata::S1 => { log!(app, ResentKeyConfirm(session)); (PACKET_TYPE_KEY_CONFIRM, Vec::new()) } - ZsspAutomata::S2 => return, - ZsspAutomata::R1 { k1, .. } => { + ZetaAutomata::S2 => return, + ZetaAutomata::R1 { k1, .. } => { log!(app, ResentK1(session)); (PACKET_TYPE_REKEY_INIT, k1.clone()) } - ZsspAutomata::R2 { k2, .. } => { + ZetaAutomata::R2 { k2, .. } => { log!(app, ResentK2(session)); (PACKET_TYPE_REKEY_COMPLETE, k2.clone()) } }; - let c = zeta.send_counter; - zeta.send_counter += 1; - let n = to_nonce(p, c); - let tag = App::Aead::encrypt_in_place(zeta.key_ref(false).send.kek.as_ref().unwrap(), n, None, &mut control_payload); - control_payload.extend(&tag); - send( - &Packet(zeta.key_ref(false).send.kid.unwrap().get(), n, control_payload), - Some(&zeta.hk_send), - ); + + send_control::(zeta, p, control_payload, send); } } -fn remap(session: &Arc>, zeta: &Zeta, rng: &Mutex, session_map: &SessionMap) -> NonZeroU32 { - let mut session_map = session_map.lock().unwrap(); - let weak = if let Some(Some(weak)) = zeta.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(), rng.lock().unwrap().deref_mut()); - session_map.insert(new_kid_recv, weak); - new_kid_recv -} -#[allow(unused)] +/// Corresponds to the timeout timer Transition Algorithm described in Section 4.1 - Definition 3. fn timeout_trans( - zeta: &mut Zeta, - session: &Arc>, - app: &App, - ctx: &Arc>, + zeta: &mut Zeta, + session: &Arc>, + app: &mut App, + ctx: &Arc>, current_time: i64, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), ) { match &zeta.beta { - ZsspAutomata::Null => {} - ZsspAutomata::A1(StateA1 { identity, .. }) | ZsspAutomata::A3 { identity, .. } => { - if matches!(&zeta.beta, ZsspAutomata::A1(_)) { + ZetaAutomata::Null => {} + ZetaAutomata::A1(StateA1 { identity, .. }) | ZetaAutomata::A3 { identity, .. } => { + if matches!(&zeta.beta, ZetaAutomata::A1(_)) { log!(app, TimeoutX1(session)); } else { log!(app, TimeoutX3(session)); } let new_kid_recv = remap(session, &zeta, &ctx.rng, &ctx.session_map); - if let Some(a1) = create_a1_state(&ctx.rng, &zeta.s_remote, new_kid_recv, &zeta.ratchet_states, identity.clone()) { + if let Some(a1) = create_a1_state::( + &ctx.rng, + &zeta.s_remote, + new_kid_recv, + &zeta.ratchet_state1, + zeta.ratchet_state2.as_ref(), + identity.clone(), + ) { let (hk_recv, hk_send) = a1.noise.get_ask(LABEL_HEADER_KEY); let packet = a1.packet.clone(); zeta.hk_send = hk_send; *zeta.key_mut(true) = DuplexKey::default(); zeta.key_mut(true).recv.kid = Some(new_kid_recv); - zeta.resend_timer = current_time + App::SETTINGS.resend_time as i64; - zeta.timeout_timer = current_time + App::SETTINGS.initial_offer_timeout as i64; - zeta.beta = ZsspAutomata::A1(a1); + zeta.resend_timer = current_time + ::SETTINGS.resend_time as i64; + zeta.timeout_timer = current_time + ::SETTINGS.initial_offer_timeout as i64; + zeta.beta = ZetaAutomata::A1(a1); zeta.defrag = DefragBuffer::new(Some(hk_recv)); send(&packet, None); @@ -945,7 +1054,8 @@ fn timeout_trans( zeta.expire(); } } - ZsspAutomata::S2 => { + ZetaAutomata::S2 => { + // Corresponds to Transition Algorithm 6 found in Section 4.3. log!(app, StartedRekeyingSentK1(session)); let new_kid_recv = remap(session, &zeta, &ctx.rng, &ctx.session_map); // -> s @@ -958,7 +1068,7 @@ fn timeout_trans( noise.mix_hash(&ctx.s_secret.public_key_bytes()); noise.mix_hash(&zeta.s_remote.to_bytes()); // Process message pattern 1 psk0 token. - noise.mix_key_and_hash(zeta.ratchet_states[0].key().unwrap()); + noise.mix_key_and_hash(zeta.ratchet_state1.key()); // Process message pattern 1 e token. let e_secret = noise.write_e(&ctx.rng, &mut k1); // Process message pattern 1 es token. @@ -971,50 +1081,45 @@ fn timeout_trans( zeta.expire(); return; } - // Process message pattern 1 payload token. + // Process message pattern 1 payload. let i = k1.len(); k1.extend(&new_kid_recv.get().to_be_bytes()); noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_REKEY_INIT, 0), i, &mut k1); zeta.key_mut(true).recv.kid = Some(new_kid_recv); - zeta.timeout_timer = current_time + App::SETTINGS.rekey_timeout as i64; - zeta.resend_timer = current_time + App::SETTINGS.resend_time as i64; - zeta.beta = ZsspAutomata::R1 { noise, e_secret, k1: k1.clone() }; + zeta.timeout_timer = current_time + ::SETTINGS.rekey_timeout as i64; + zeta.resend_timer = current_time + ::SETTINGS.resend_time as i64; + zeta.beta = ZetaAutomata::R1 { noise, e_secret, k1: k1.clone() }; - let c = zeta.send_counter; - zeta.send_counter += 1; - let n = to_nonce(PACKET_TYPE_REKEY_INIT, c); - let tag = App::Aead::encrypt_in_place(zeta.key_ref(false).send.kek.as_ref().unwrap(), n, None, &mut k1); - k1.extend(&tag); - - send(&Packet(zeta.key_ref(false).send.kid.unwrap().get(), n, k1), Some(&zeta.hk_send)); + send_control::(zeta, PACKET_TYPE_REKEY_INIT, k1, send); } - ZsspAutomata::S1 { .. } => { + ZetaAutomata::S1 { .. } => { log!(app, TimeoutKeyConfirm(session)); zeta.expire(); } - ZsspAutomata::R1 { .. } => { + ZetaAutomata::R1 { .. } => { log!(app, TimeoutK1(session)); zeta.expire(); } - ZsspAutomata::R2 { .. } => { + ZetaAutomata::R2 { .. } => { log!(app, TimeoutK2(session)); zeta.expire(); } } } +/// Corresponds to Transition Algorithm 7 found in Section 4.3. pub(crate) fn received_k1_trans( - zeta: &mut Zeta, - session: &Arc>, - app: &App, - rng: &Mutex, - session_map: &SessionMap, - s_secret: &App::KeyPair, + zeta: &mut Zeta, + session: &Arc>, + app: &mut App, + rng: &RefCell<::Rng>, + session_map: &SessionMap, + s_secret: &::KeyPair, kid: NonZeroU32, - n: [u8; AES_GCM_IV_SIZE], + n: [u8; AES_GCM_NONCE_SIZE], mut k1: Vec, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), -) -> Result<(), ReceiveError> { +) -> Result<(), ReceiveError> { use FaultType::*; // -> s // <- s @@ -1029,8 +1134,8 @@ pub(crate) fn received_k1_trans( return Err(byzantine_fault!(UnknownLocalKeyId, false)); } let should_rekey_as_bob = match &zeta.beta { - ZsspAutomata::S2 { .. } => true, - ZsspAutomata::R1 { .. } => zeta.was_bob, + ZetaAutomata::S2 { .. } => true, + ZetaAutomata::R1 { .. } => zeta.was_bob, _ => false, }; if !should_rekey_as_bob { @@ -1040,7 +1145,13 @@ pub(crate) fn received_k1_trans( let i = k1.len() - AES_GCM_TAG_SIZE; let tag = k1[i..].try_into().unwrap(); - if !App::Aead::decrypt_in_place(zeta.key_ref(false).recv.kek.as_ref().unwrap(), n, None, &mut k1[..i], tag) { + if !::Aead::decrypt_in_place( + zeta.key_ref(false).recv.kek.as_ref().unwrap(), + &n, + None, + &mut k1[..i], + &tag, + ) { return Err(byzantine_fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); @@ -1051,18 +1162,22 @@ pub(crate) fn received_k1_trans( let result = (|| { let mut i = 0; - let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_KK); + let mut noise = SymmetricState::::initialize(PROTOCOL_NAME_NOISE_KK); // Noise process prologue. noise.mix_hash(&zeta.s_remote.to_bytes()); noise.mix_hash(&s_secret.public_key_bytes()); // Process message pattern 1 psk0 token. - noise.mix_key_and_hash(zeta.ratchet_states[0].key().unwrap()); + noise.mix_key_and_hash(zeta.ratchet_state1.key()); // Process message pattern 1 e token. let e_remote = noise.read_e(&mut i, &k1).ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 1 es token. - noise.mix_dh(s_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(s_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 1 ss token. - noise.mix_dh(s_secret, &zeta.s_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(s_secret, &zeta.s_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 1 payload. let j = i + KID_SIZE; let k = j + AES_GCM_TAG_SIZE; @@ -1070,15 +1185,20 @@ pub(crate) fn received_k1_trans( if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_REKEY_INIT, 0), &mut k1[i..j], tag) { return Err(byzantine_fault!(FailedAuth, true)); } - let kid_send = NonZeroU32::new(u32::from_be_bytes(k1[i..j].try_into().unwrap())).ok_or(byzantine_fault!(FailedAuth, true))?; + let kid_send = NonZeroU32::new(u32::from_be_bytes(k1[i..j].try_into().unwrap())) + .ok_or(byzantine_fault!(FailedAuth, true))?; let mut k2 = Vec::new(); // Process message pattern 2 e token. let e_secret = noise.write_e(rng, &mut k2); // Process message pattern 2 ee token. - noise.mix_dh(&e_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(&e_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 se token. - noise.mix_dh(&s_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(&s_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 payload. let i = k2.len(); let new_kid_recv = remap(session, &zeta, rng, session_map); @@ -1086,15 +1206,20 @@ pub(crate) fn received_k1_trans( noise.encrypt_and_hash_in_place(to_nonce(PACKET_TYPE_REKEY_COMPLETE, 0), i, &mut k2); let (rk, rf) = noise.get_ask(LABEL_RATCHET_STATE); - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, zeta.ratchet_states[0].chain_len() + 1); + let new_ratchet_state = RatchetState::new(rk, rf, zeta.ratchet_state1.chain_len() + 1); let result = app.save_ratchet_state( &zeta.s_remote, - &zeta.application_data, - [&zeta.ratchet_states[0], &zeta.ratchet_states[1]], - [&new_ratchet_state, &zeta.ratchet_states[0]], + &zeta.session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: Some(&zeta.ratchet_state1), + state1_was_just_added: true, + deleted_state1: zeta.ratchet_state2.as_ref(), + deleted_state2: None, + }, ); if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); + return Err(ReceiveError::StorageError(e)); } let (kek_send, kek_recv) = noise.get_ask(LABEL_KEX_KEY); let (nk_send, nk_recv) = noise.split(); @@ -1105,21 +1230,15 @@ pub(crate) fn received_k1_trans( zeta.key_mut(true).recv.kid = Some(new_kid_recv); zeta.key_mut(true).recv.kek = Some(kek_recv); zeta.key_mut(true).recv.nk = Some(nk_recv); - zeta.ratchet_states[1] = zeta.ratchet_states[0].clone(); - zeta.ratchet_states[0] = new_ratchet_state; + zeta.ratchet_state2 = Some(zeta.ratchet_state1.clone()); + zeta.ratchet_state1 = new_ratchet_state; let current_time = app.time(); zeta.key_creation_counter = zeta.send_counter; - zeta.timeout_timer = current_time + App::SETTINGS.rekey_timeout as i64; - zeta.resend_timer = current_time + App::SETTINGS.resend_time as i64; - zeta.beta = ZsspAutomata::R2 { k2: k2.clone() }; + zeta.timeout_timer = current_time + ::SETTINGS.rekey_timeout as i64; + zeta.resend_timer = current_time + ::SETTINGS.resend_time as i64; + zeta.beta = ZetaAutomata::R2 { k2: k2.clone() }; - let c = zeta.send_counter; - zeta.send_counter += 1; - let n = to_nonce(PACKET_TYPE_REKEY_COMPLETE, c); - let tag = App::Aead::encrypt_in_place(zeta.key_ref(false).send.kek.as_ref().unwrap(), n, None, &mut k2); - k2.extend(&tag); - - send(&Packet(zeta.key_ref(false).send.kid.unwrap().get(), n, k2), Some(&zeta.hk_send)); + send_control::(zeta, PACKET_TYPE_REKEY_COMPLETE, k2, send); Ok(()) })(); if matches!(result, Err(ReceiveError::ByzantineFault { .. })) { @@ -1127,14 +1246,15 @@ pub(crate) fn received_k1_trans( } result } +/// Corresponds to Transition Algorithm 8 found in Section 4.3. pub(crate) fn received_k2_trans( - zeta: &mut Zeta, - app: &App, + zeta: &mut Zeta, + app: &mut App, kid: NonZeroU32, - n: [u8; AES_GCM_IV_SIZE], + n: [u8; AES_GCM_NONCE_SIZE], mut k2: Vec, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), -) -> Result<(), ReceiveError> { +) -> Result<(), ReceiveError> { use FaultType::*; // <- e, ee, se if k2.len() != REKEY_SIZE { @@ -1144,14 +1264,20 @@ pub(crate) fn received_k2_trans( // Some rekey packet may have arrived extremely delayed. return Err(byzantine_fault!(UnknownLocalKeyId, false)); } - if !matches!(&zeta.beta, ZsspAutomata::R1 { .. }) { + if !matches!(&zeta.beta, ZetaAutomata::R1 { .. }) { // Some rekey packet may have arrived extremely delayed. return Err(byzantine_fault!(OutOfSequence, false)); } let i = k2.len() - AES_GCM_TAG_SIZE; let tag = k2[i..].try_into().unwrap(); - if !App::Aead::decrypt_in_place(zeta.key_ref(false).recv.kek.as_ref().unwrap(), n, None, &mut k2[..i], tag) { + if !::Aead::decrypt_in_place( + zeta.key_ref(false).recv.kek.as_ref().unwrap(), + &n, + None, + &mut k2[..i], + &tag, + ) { return Err(byzantine_fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); @@ -1160,15 +1286,19 @@ pub(crate) fn received_k2_trans( } k2.truncate(i); let result = (|| { - if let ZsspAutomata::R1 { noise, e_secret, .. } = &zeta.beta { + if let ZetaAutomata::R1 { noise, e_secret, .. } = &zeta.beta { let mut noise = noise.clone(); let mut i = 0; // Process message pattern 2 e token. let e_remote = noise.read_e(&mut i, &k2).ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 ee token. - noise.mix_dh(e_secret, &e_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(e_secret, &e_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 se token. - noise.mix_dh(e_secret, &zeta.s_remote).ok_or(byzantine_fault!(FailedAuth, true))?; + noise + .mix_dh(e_secret, &zeta.s_remote) + .ok_or(byzantine_fault!(FailedAuth, true))?; // Process message pattern 2 payload. let j = i + KID_SIZE; let k = j + AES_GCM_TAG_SIZE; @@ -1176,18 +1306,24 @@ pub(crate) fn received_k2_trans( if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_REKEY_COMPLETE, 0), &mut k2[i..j], tag) { return Err(byzantine_fault!(FailedAuth, true)); } - let kid_send = NonZeroU32::new(u32::from_be_bytes(k2[i..j].try_into().unwrap())).ok_or(byzantine_fault!(InvalidPacket, true))?; + let kid_send = NonZeroU32::new(u32::from_be_bytes(k2[i..j].try_into().unwrap())) + .ok_or(byzantine_fault!(InvalidPacket, true))?; let (rk, rf) = noise.get_ask(LABEL_RATCHET_STATE); - let new_ratchet_state = RatchetState::new_nonempty(rk, rf, zeta.ratchet_states[0].chain_len() + 1); + let new_ratchet_state = RatchetState::new(rk, rf, zeta.ratchet_state1.chain_len() + 1); let result = app.save_ratchet_state( &zeta.s_remote, - &zeta.application_data, - [&zeta.ratchet_states[0], &zeta.ratchet_states[1]], - [&new_ratchet_state, &RatchetState::Null], + &zeta.session_data, + RatchetUpdate { + state1: &new_ratchet_state, + state2: None, + state1_was_just_added: true, + deleted_state1: Some(&zeta.ratchet_state1), + deleted_state2: zeta.ratchet_state2.as_ref(), + }, ); if let Err(e) = result { - return Err(ReceiveError::RatchetIoError(e)); + return Err(ReceiveError::StorageError(e)); } let (kek_recv, kek_send) = noise.get_ask(LABEL_KEX_KEY); let (nk_recv, nk_send) = noise.split(); @@ -1197,22 +1333,16 @@ pub(crate) fn received_k2_trans( zeta.key_mut(true).send.nk = Some(nk_send); zeta.key_mut(true).recv.kek = Some(kek_recv); zeta.key_mut(true).recv.nk = Some(nk_recv); - zeta.ratchet_states[0] = new_ratchet_state; + zeta.ratchet_state1 = new_ratchet_state; zeta.key_index ^= true; let current_time = app.time(); zeta.key_creation_counter = zeta.send_counter; - zeta.timeout_timer = current_time + App::SETTINGS.rekey_timeout as i64; - zeta.resend_timer = current_time + App::SETTINGS.resend_time as i64; - zeta.beta = ZsspAutomata::S1; + zeta.timeout_timer = current_time + ::SETTINGS.rekey_timeout as i64; + zeta.resend_timer = current_time + ::SETTINGS.resend_time as i64; + zeta.beta = ZetaAutomata::S1; - let mut c1 = Vec::new(); - let c = zeta.send_counter; - zeta.send_counter += 1; - let n = to_nonce(PACKET_TYPE_KEY_CONFIRM, c); - let tag = App::Aead::encrypt_in_place(zeta.key_ref(false).send.kek.as_ref().unwrap(), n, None, &mut []); - c1.extend(&tag); - - send(&Packet(zeta.key_ref(false).send.kid.unwrap().get(), n, c1), Some(&zeta.hk_send)); + let c1 = Vec::new(); + send_control::(zeta, PACKET_TYPE_KEY_CONFIRM, c1, send); Ok(()) } else { unreachable!() @@ -1223,46 +1353,50 @@ pub(crate) fn received_k2_trans( } result } -pub(crate) fn send_payload( - zeta: &mut Zeta, +/// Corresponds to Algorithm 9 found in Section 4.3. +pub(crate) fn send_payload( + zeta: &mut Zeta, mut payload: Vec, send: impl FnOnce(&Packet, Option<&[u8; AES_256_KEY_SIZE]>), ) -> Result<(), SendError> { use SendError::*; - if matches!(&zeta.beta, ZsspAutomata::Null) { + if matches!(&zeta.beta, ZetaAutomata::Null) { return Err(SessionExpired); } if !matches!( &zeta.beta, - ZsspAutomata::S1 | ZsspAutomata::S2 | ZsspAutomata::R1 { .. } | ZsspAutomata::R2 { .. } + ZetaAutomata::S1 | ZetaAutomata::S2 | ZetaAutomata::R1 { .. } | ZetaAutomata::R2 { .. } ) { return Err(SessionNotEstablished); } - let c = zeta.send_counter; - zeta.send_counter += 1; - if c >= zeta.key_creation_counter + App::SETTINGS.rekey_after_key_uses { - if c >= zeta.key_creation_counter + EXPIRE_AFTER_USES { - zeta.expire(); - } else { + if let Some((c, should_rekey)) = zeta.get_counter() { + if should_rekey { // Cause timeout to occur next service interval. zeta.timeout_timer = i64::MIN; } + + let n = to_nonce(PACKET_TYPE_DATA, c); + let tag = Crypto::Aead::encrypt_in_place(zeta.key_ref(false).send.nk.as_ref().unwrap(), &n, None, &mut payload); + payload.extend(&tag); + + send( + &Packet(zeta.key_ref(false).send.kid.unwrap().get(), n, payload), + Some(&zeta.hk_send), + ); + Ok(()) + } else { + zeta.expire(); + Err(SessionExpired) } - - let n = to_nonce(PACKET_TYPE_DATA, c); - let tag = App::Aead::encrypt_in_place(zeta.key_ref(false).send.nk.as_ref().unwrap(), n, None, &mut payload); - payload.extend(&tag); - - send(&Packet(zeta.key_ref(false).send.kid.unwrap().get(), n, payload), Some(&zeta.hk_send)); - Ok(()) } +/// Corresponds to Algorithm 10 found in Section 4.3. pub(crate) fn received_payload_in_place( - zeta: &mut Zeta, + zeta: &mut Zeta, kid: NonZeroU32, - n: [u8; AES_GCM_IV_SIZE], + n: [u8; AES_GCM_NONCE_SIZE], payload: &mut Vec, -) -> Result<(), ReceiveError> { +) -> Result<(), ReceiveError> { use FaultType::*; if payload.len() < AES_GCM_TAG_SIZE { @@ -1279,9 +1413,10 @@ pub(crate) fn received_payload_in_place( }; let i = payload.len() - AES_GCM_TAG_SIZE; - let specified_key = zeta.key_ref(is_other).recv.nk.as_ref().ok_or(byzantine_fault!(OutOfSequence, true))?; + let specified_key = zeta.key_ref(is_other).recv.nk.as_ref(); + let specified_key = specified_key.ok_or(byzantine_fault!(OutOfSequence, true))?; let tag = payload[i..].try_into().unwrap(); - if !App::Aead::decrypt_in_place(specified_key, n, None, &mut payload[..i], tag) { + if !::Aead::decrypt_in_place(specified_key, &n, None, &mut payload[..i], &tag) { return Err(byzantine_fault!(FailedAuth, true)); } let (_, c) = from_nonce(&n); @@ -1295,16 +1430,16 @@ pub(crate) fn received_payload_in_place( Ok(()) } -impl Session { +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(&mut self) { - self.0.lock().unwrap().expire(); + self.0.borrow_mut().expire(); } } -impl Drop for Session { +impl Drop for Session { fn drop(&mut self) { self.expire(); } diff --git a/whitepaper.pdf b/whitepaper.pdf new file mode 100644 index 0000000..c9231b7 Binary files /dev/null and b/whitepaper.pdf differ