Merge pull request #2 from zerotier/dev

Merging stable version of dev with upgraded API and Docs
This commit is contained in:
Monica Moniot
2023-08-09 14:53:32 -04:00
committed by GitHub
28 changed files with 1662 additions and 1026 deletions
Generated
-77
View File
@@ -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"
+11 -8
View File
@@ -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"] }
+2 -37
View File
@@ -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.
+37
View File
@@ -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.
+115 -76
View File
@@ -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<u128, RatchetStates>,
}
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<Session<TestApplication>>) -> bool {
true
}
fn initiator_disallows_downgrade(&self, session: &Arc<Session<Self>>) -> 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<RatchetState, Self::DiskError> {
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<TestApplication> {
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<Option<RatchetState>, Infallible> {
Ok(self.ratchets.rf_map.get(ratchet_fingerprint).cloned())
}
fn restore_by_identity(
&mut self,
remote_static_key: &P384CratePublicKey,
session_data: &u128,
) -> Result<Option<RatchetStates>, 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<Self>) {
fn event_log(&mut self, event: zssp_proto::LogEvent<TestApplication>) {
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<Vec<u8>>,
alice_in: mpsc::Receiver<Vec<u8>>,
recursive_out: mpsc::SyncSender<Vec<u8>>,
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::<TestApplication>::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<Vec<u8>>,
bob_in: mpsc::Receiver<Vec<u8>>,
recursive_out: mpsc::SyncSender<Vec<u8>>,
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::<TestApplication>::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::<Sha512>(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::<Vec<u8>>(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));
+1 -1
View File
@@ -1,4 +1,4 @@
max_width = 150
max_width = 120
edition = "2021"
newline_style = "Unix"
struct_lit_width = 60
+116 -50
View File
@@ -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<Self::Rng, PublicKey = Self::PublicKey>;
/// 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<Self::Rng>;
type Prp: PrpAes256;
type Aead: AeadAesGcm;
type Hash: HashSha512;
type PublicKey: PublicKeyP384;
type KeyPair: KeyPairP384<Self::Rng, PublicKey = Self::PublicKey>;
type Kem: PrivateKeyKyber1024<Self::Rng>;
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<Session<Self>>) -> bool;
fn initiator_disallows_downgrade(&mut self, session: &Arc<Session<Self::Crypto>>) -> 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: &<Self::Crypto as CryptoLayer>::PublicKey, identity: &[u8]) -> AcceptAction<Self::Crypto>;
/// 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<RatchetState, Self::DiskError>;
/// 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<Option<RatchetState>, 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: &<Self::Crypto as CryptoLayer>::PublicKey,
session_data: &<Self::Crypto as CryptoLayer>::SessionData,
) -> Result<Option<RatchetStates>, 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: &<Self::Crypto as CryptoLayer>::PublicKey,
session_data: &<Self::Crypto as CryptoLayer>::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<Self>);
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<Crypto: CryptoLayer> {
/// 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<Crypto::SessionData>,
/// 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,
}
+15 -10
View File
@@ -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: RngCore + CryptoRng>(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<Rng: RngCore + CryptoRng, Hash: HashSha512>(
/// Corresponds to Algorithm 13 found in Section 5.
pub fn respond_to_challenge_in_place<Rng: RngCore + CryptoRng, Hash: Sha512Hash>(
rng: &mut Rng,
challenge: &[u8; CHALLENGE_SIZE],
pre_response: &mut [u8; CHALLENGE_SIZE],
@@ -47,7 +48,8 @@ impl ChallengeContext {
salt,
}
}
pub fn process_hello<Hash: HashSha512>(
/// Corresponds to Algorithm 12 found in Section 5.
pub fn process_hello<Hash: Sha512Hash>(
&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::<Hash>(c, addr)) && verify_pow::<Hash>(response) {
if self.check_window(c)
&& secure_eq(&response[COUNTER_SIZE..POW_START], &self.create_mac::<Hash>(c, addr))
&& verify_pow::<Hash>(response)
{
self.update_window(c);
Ok(true)
} else {
@@ -69,7 +74,7 @@ impl ChallengeContext {
Err(challenge)
}
}
fn create_mac<Hash: HashSha512>(&self, c: u64, addr: &impl std::hash::Hash) -> [u8; MAC_SIZE] {
fn create_mac<Hash: Sha512Hash>(&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<Hash: HashSha512>(response: &[u8]) -> bool {
fn verify_pow<Hash: Sha512Hash>(response: &[u8]) -> bool {
if DIFFICULTY == 0 {
return true;
}
+237 -156
View File
File diff suppressed because it is too large Load Diff
+26 -10
View File
@@ -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;
}
+27 -4
View File
@@ -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<Rng: RngCore + CryptoRng>: Sized + Send + Sync {
pub trait Kyber1024PrivateKey<Rng: RngCore + CryptoRng>: 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]>;
}
+1
View File
@@ -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<A: AsRef<[u8]> + ?Sized, B: AsRef<[u8]> + ?Sized>(a: &A, b: &B) -> bool {
+15 -12
View File
@@ -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<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 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<Rng: RngCore + CryptoRng> {
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<Rng: RngCore + CryptoRng> {
/// 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]>;
}
+9 -7
View File
@@ -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];
}
+21 -7
View File
@@ -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::<Aes256Gcm>::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::<Aes256Gcm>::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()
}
}
+11 -4
View File
@@ -1,18 +1,25 @@
use rand_core::{CryptoRng, RngCore};
use zeroize::Zeroizing;
use crate::crypto::*;
impl<Rng: RngCore + CryptoRng> PrivateKeyKyber1024<Rng> 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<Rng: RngCore + CryptoRng> Kyber1024PrivateKey<Rng> 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()
}
}
+47 -4
View File
@@ -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<Crypto: CrateCryptoLayer> 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;
}
*/
+13 -3
View File
@@ -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<Self> {
PublicKey::from_sec1_bytes(raw_key).ok()
}
@@ -14,7 +16,9 @@ impl PublicKeyP384 for PublicKey {
}
}
impl<Rng: RngCore + CryptoRng> KeyPairP384<Rng> for EphemeralSecret {
/// The version and type of the p384 crate that the `P384KeyPair` trait is implemented for.
pub type P384CrateKeyPair = EphemeralSecret;
impl<Rng: RngCore + CryptoRng> P384KeyPair<Rng> for P384CrateKeyPair {
type PublicKey = PublicKey;
fn generate(rng: &mut Rng) -> Self {
@@ -26,6 +30,12 @@ impl<Rng: RngCore + CryptoRng> KeyPairP384<Rng> 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(),
)
}
}
+3 -1
View File
@@ -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()
}
+32 -16
View File
@@ -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<App: ApplicationLayer>(
/// Corresponds to the fragmentation algorithm described in Section 6.
pub fn send_with_fragmentation<Crypto: CryptoLayer>(
mut send: impl FnMut(Vec<u8>) -> bool,
mtu: usize,
identifier: u32,
@@ -45,7 +52,10 @@ pub fn send_with_fragmentation<App: ApplicationLayer>(
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<App: ApplicationLayer>(
}
pub struct DefragBuffer {
fragment_map: Mutex<HashMap<[u8; PACKET_NONCE_SIZE], Buffer>>,
fragment_map: RefCell<HashMap<[u8; PACKET_NONCE_SIZE], Buffer>>,
hk_recv: Option<Zeroizing<[u8; AES_256_KEY_SIZE]>>,
}
@@ -69,22 +79,28 @@ struct Buffer {
impl DefragBuffer {
pub fn new(hk_recv: Option<Zeroizing<[u8; AES_256_KEY_SIZE]>>) -> 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<App: ApplicationLayer>(
&self,
mut raw_fragment: Vec<u8>,
current_time: i64,
vrfy: impl FnOnce(&[u8; PACKET_NONCE_SIZE], usize, usize) -> Result<(), ReceiveError<App::DiskError>>,
) -> Result<Option<([u8; PACKET_NONCE_SIZE], Vec<u8>)>, ReceiveError<App::DiskError>> {
vrfy: impl FnOnce(&[u8; PACKET_NONCE_SIZE], usize, usize) -> Result<(), ReceiveError<App::StorageError>>,
) -> Result<Option<([u8; PACKET_NONCE_SIZE], Vec<u8>)>, ReceiveError<App::StorageError>> {
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())
<App::Crypto as CryptoLayer>::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<App: ApplicationLayer>(&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);
}
}

Some files were not shown because too many files have changed in this diff Show More