mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-07 21:13:44 +02:00
commit
2ab9e5d40b
15 changed files with 1131 additions and 945 deletions
|
@ -1,6 +1,7 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crypto",
|
"crypto",
|
||||||
|
"zssp",
|
||||||
"network-hypervisor",
|
"network-hypervisor",
|
||||||
"controller",
|
"controller",
|
||||||
"service",
|
"service",
|
||||||
|
|
|
@ -3,5 +3,3 @@
|
||||||
------
|
------
|
||||||
|
|
||||||
Most of this library is just glue to provide a simple safe API around things like OpenSSL or OS-specific crypto APIs.
|
Most of this library is just glue to provide a simple safe API around things like OpenSSL or OS-specific crypto APIs.
|
||||||
|
|
||||||
It also contains [ZSSP](ZSSP.md), the V2 ZeroTier Secure Session Protocol.
|
|
||||||
|
|
|
@ -298,6 +298,9 @@ mod fruit_flavored {
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
mod openssl_aes {
|
mod openssl_aes {
|
||||||
use crate::secret::Secret;
|
use crate::secret::Secret;
|
||||||
|
use foreign_types::ForeignTypeRef;
|
||||||
|
use openssl::cipher::CipherRef;
|
||||||
|
use openssl::cipher_ctx::{CipherCtx, CipherCtxRef};
|
||||||
use openssl::symm::{Cipher, Crypter, Mode};
|
use openssl::symm::{Cipher, Crypter, Mode};
|
||||||
use std::cell::UnsafeCell;
|
use std::cell::UnsafeCell;
|
||||||
use std::mem::MaybeUninit;
|
use std::mem::MaybeUninit;
|
||||||
|
@ -385,7 +388,7 @@ mod openssl_aes {
|
||||||
unsafe impl Send for Aes {}
|
unsafe impl Send for Aes {}
|
||||||
unsafe impl Sync for Aes {}
|
unsafe impl Sync for Aes {}
|
||||||
|
|
||||||
pub struct AesGcm(Secret<32>, usize, Option<Crypter>, bool);
|
pub struct AesGcm(Secret<32>, usize, CipherCtx, bool);
|
||||||
|
|
||||||
impl AesGcm {
|
impl AesGcm {
|
||||||
/// Construct a new AES-GCM cipher.
|
/// Construct a new AES-GCM cipher.
|
||||||
|
@ -395,7 +398,7 @@ mod openssl_aes {
|
||||||
match k.len() {
|
match k.len() {
|
||||||
16 | 24 | 32 => {
|
16 | 24 | 32 => {
|
||||||
s.0[..k.len()].copy_from_slice(k);
|
s.0[..k.len()].copy_from_slice(k);
|
||||||
Self(s, k.len(), None, encrypt)
|
Self(s, k.len(), CipherCtx::new().unwrap(), encrypt)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
panic!("AES supports 128, 192, or 256 bits keys");
|
panic!("AES supports 128, 192, or 256 bits keys");
|
||||||
|
@ -408,58 +411,64 @@ mod openssl_aes {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn reset_init_gcm(&mut self, iv: &[u8]) {
|
pub fn reset_init_gcm(&mut self, iv: &[u8]) {
|
||||||
assert_eq!(iv.len(), 12);
|
assert_eq!(iv.len(), 12);
|
||||||
let mut c = Crypter::new(
|
let t = aes_gcm_by_key_size(self.1);
|
||||||
aes_gcm_by_key_size(self.1),
|
let key = &self.0 .0[..self.1];
|
||||||
if self.3 {
|
{
|
||||||
Mode::Encrypt
|
let f = match self.3 {
|
||||||
} else {
|
true => CipherCtxRef::encrypt_init,
|
||||||
Mode::Decrypt
|
false => CipherCtxRef::decrypt_init,
|
||||||
},
|
};
|
||||||
&self.0 .0[..self.1],
|
|
||||||
Some(iv),
|
f(
|
||||||
)
|
&mut self.2,
|
||||||
.unwrap();
|
Some(unsafe { CipherRef::from_ptr(t.as_ptr() as *mut _) }),
|
||||||
c.pad(false);
|
None,
|
||||||
//let _ = c.set_tag_len(16);
|
None,
|
||||||
let _ = self.2.replace(c);
|
).unwrap();
|
||||||
|
|
||||||
|
self.2.set_key_length(key.len()).unwrap();
|
||||||
|
|
||||||
|
if let Some(iv_len) = t.iv_len() {
|
||||||
|
if iv.len() != iv_len {
|
||||||
|
self.2.set_iv_length(iv.len()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(&mut self.2, None, Some(key), Some(iv)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.2.set_padding(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn aad(&mut self, aad: &[u8]) {
|
pub fn aad(&mut self, aad: &[u8]) {
|
||||||
assert!(self.2.as_mut().unwrap().aad_update(aad).is_ok());
|
self.2.cipher_update(aad, None).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt or decrypt (same operation with CTR mode)
|
/// Encrypt or decrypt (same operation with CTR mode)
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) {
|
pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) {
|
||||||
assert!(self.2.as_mut().unwrap().update(input, output).is_ok());
|
self.2.cipher_update(input, Some(output)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt or decrypt in place (same operation with CTR mode)
|
/// Encrypt or decrypt in place (same operation with CTR mode)
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn crypt_in_place(&mut self, data: &mut [u8]) {
|
pub fn crypt_in_place(&mut self, data: &mut [u8]) {
|
||||||
assert!(self
|
self.2.cipher_update(unsafe { &*std::slice::from_raw_parts(data.as_ptr(), data.len()) }, Some(data)).unwrap();
|
||||||
.2
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.update(unsafe { &*std::slice::from_raw_parts(data.as_ptr(), data.len()) }, data)
|
|
||||||
.is_ok());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn finish_encrypt(&mut self) -> [u8; 16] {
|
pub fn finish_encrypt(&mut self) -> [u8; 16] {
|
||||||
let mut tag = [0_u8; 16];
|
let mut tag = [0_u8; 16];
|
||||||
let mut c = self.2.take().unwrap();
|
self.2.cipher_final(&mut tag).unwrap();
|
||||||
assert!(c.finalize(&mut tag).is_ok());
|
self.2.tag(&mut tag).unwrap();
|
||||||
assert!(c.get_tag(&mut tag).is_ok());
|
|
||||||
tag
|
tag
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn finish_decrypt(&mut self, expected_tag: &[u8]) -> bool {
|
pub fn finish_decrypt(&mut self, expected_tag: &[u8]) -> bool {
|
||||||
let mut c = self.2.take().unwrap();
|
if self.2.set_tag(expected_tag).is_ok() {
|
||||||
if c.set_tag(expected_tag).is_ok() {
|
let result = self.2.cipher_final(&mut []).is_ok();
|
||||||
let result = c.finalize(&mut []).is_ok();
|
|
||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
|
@ -10,6 +10,5 @@ pub mod salsa;
|
||||||
pub mod secret;
|
pub mod secret;
|
||||||
pub mod verified;
|
pub mod verified;
|
||||||
pub mod x25519;
|
pub mod x25519;
|
||||||
pub mod zssp;
|
|
||||||
|
|
||||||
pub const ZEROES: [u8; 64] = [0_u8; 64];
|
pub const ZEROES: [u8; 64] = [0_u8; 64];
|
||||||
|
|
|
@ -33,6 +33,28 @@ pub fn write<W: Write>(w: &mut W, v: u64) -> std::io::Result<()> {
|
||||||
w.write_all(&b[0..i])
|
w.write_all(&b[0..i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dencode up to 10 bytes as a varint.
|
||||||
|
///
|
||||||
|
/// if the supplied byte slice does not contain a valid varint encoding this will return None.
|
||||||
|
/// if the supplied byte slice is shorter than expected this will return None.
|
||||||
|
pub fn decode(b: &[u8]) -> Option<(u64, usize)> {
|
||||||
|
let mut v = 0_u64;
|
||||||
|
let mut pos = 0;
|
||||||
|
let mut i = 0_usize;
|
||||||
|
while i < b.len() && i < VARINT_MAX_SIZE_BYTES {
|
||||||
|
let b = b[i];
|
||||||
|
i += 1;
|
||||||
|
if b <= 0x7f {
|
||||||
|
v |= (b as u64).wrapping_shl(pos);
|
||||||
|
pos += 7;
|
||||||
|
} else {
|
||||||
|
v |= ((b & 0x7f) as u64).wrapping_shl(pos);
|
||||||
|
return Some((v, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
/// Read a variable length integer, returning the value and the number of bytes written.
|
/// Read a variable length integer, returning the value and the number of bytes written.
|
||||||
pub fn read<R: Read>(r: &mut R) -> std::io::Result<(u64, usize)> {
|
pub fn read<R: Read>(r: &mut R) -> std::io::Result<(u64, usize)> {
|
||||||
let mut v = 0_u64;
|
let mut v = 0_u64;
|
||||||
|
|
35
zssp/Cargo.toml
Normal file
35
zssp/Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[package]
|
||||||
|
authors = ["ZeroTier, Inc. <contact@zerotier.com>", "Adam Ierymenko <adam.ierymenko@zerotier.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
name = "zerotier-zssp"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
zerotier-utils = { path = "../utils" }
|
||||||
|
zerotier-crypto = { path = "../crypto" }
|
||||||
|
pqc_kyber = { path = "../third_party/kyber", features = ["kyber1024", "reference"], default-features = false }
|
||||||
|
#ed25519-dalek = { version = "1.0.1", features = ["std", "u64_backend"], default-features = false }
|
||||||
|
#foreign-types = "0.3.1"
|
||||||
|
#lazy_static = "^1"
|
||||||
|
#poly1305 = { version = "0.8.0", features = [], default-features = false }
|
||||||
|
#pqc_kyber = { path = "../third_party/kyber", features = ["kyber1024", "reference"], default-features = false }
|
||||||
|
#pqc_kyber = { version = "^0", features = ["kyber1024", "reference"], default-features = false }
|
||||||
|
#rand_core = "0.5.1"
|
||||||
|
#rand_core_062 = { package = "rand_core", version = "0.6.2" }
|
||||||
|
#subtle = "2.4.1"
|
||||||
|
#x25519-dalek = { version = "1.2.0", features = ["std", "u64_backend"], default-features = false }
|
||||||
|
|
||||||
|
#[target."cfg(windows)".dependencies]
|
||||||
|
#openssl = { version = "^0", features = ["vendored"], default-features = false }
|
||||||
|
#winapi = { version = "^0", features = ["handleapi", "ws2ipdef", "ws2tcpip"] }
|
||||||
|
|
||||||
|
#[target."cfg(not(windows))".dependencies]
|
||||||
|
#openssl = { version = "^0", features = [], default-features = false }
|
||||||
|
#libc = "^0"
|
||||||
|
#signal-hook = "^0"
|
||||||
|
|
||||||
|
#[dev-dependencies]
|
||||||
|
#criterion = "0.3"
|
||||||
|
#sha2 = "^0"
|
||||||
|
#hex-literal = "^0"
|
19
zssp/changes.txt
Normal file
19
zssp/changes.txt
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
zssp has been moved into it's own crate.
|
||||||
|
|
||||||
|
zssp has been cut up into several files, only the new zssp.rs file contains the critical security path.
|
||||||
|
|
||||||
|
Standardized the naming conventions for security variables throughout zssp.
|
||||||
|
|
||||||
|
Implemented a safer version of write_all for zssp to use. This has 3 benefits: it completely prevents unknown io errors, making error handling easier and self-documenting; it completely prevents src from being truncated in dest, putting in an extra barrier to prevent catastrophic key truncation; and it has slightly less performance overhead than a write_all.
|
||||||
|
|
||||||
|
Implemented a safer version of read_exact for zssp to use. This has similar benefits to the previous change.
|
||||||
|
|
||||||
|
Refactored most buffer logic to use safe_read_exact and safe_write_all, the resulting code is less verbose and easier to analyze: Because of this refactor the buffer overrun below was caught.
|
||||||
|
|
||||||
|
Fixed a buffer overrun panic when decoding alice_ratchet_key_fingerprint
|
||||||
|
|
||||||
|
Renamed variables and added extra intermediate values so encoding and decoding are more obviously symmetric.
|
||||||
|
|
||||||
|
Added multiple comments.
|
||||||
|
|
||||||
|
Removed Box<EphemeralOffer>, EphemeralOffer is now passed out by reference instead of returned up the stack.
|
1
zssp/rustfmt.toml
Symbolic link
1
zssp/rustfmt.toml
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../rustfmt.toml
|
72
zssp/src/app_layer.rs
Normal file
72
zssp/src/app_layer.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use zerotier_crypto::{p384::{P384KeyPair, P384PublicKey}, secret::Secret};
|
||||||
|
|
||||||
|
use crate::{zssp::{Session, ReceiveContext}, ints::SessionId};
|
||||||
|
|
||||||
|
|
||||||
|
/// Trait to implement to integrate the session into an application.
|
||||||
|
///
|
||||||
|
/// Templating the session on this trait lets the code here be almost entirely transport, OS,
|
||||||
|
/// and use case independent.
|
||||||
|
pub trait ApplicationLayer: Sized {
|
||||||
|
/// Arbitrary opaque object associated with a session, such as a connection state object.
|
||||||
|
type SessionUserData;
|
||||||
|
|
||||||
|
/// Arbitrary object that dereferences to the session, such as Arc<Session<Self>>.
|
||||||
|
type SessionRef: Deref<Target = Session<Self>>;
|
||||||
|
|
||||||
|
/// A buffer containing data read from the network that can be cached.
|
||||||
|
///
|
||||||
|
/// This can be e.g. a pooled buffer that automatically returns itself to the pool when dropped.
|
||||||
|
/// It can also just be a Vec<u8> or Box<[u8]> or something like that.
|
||||||
|
type IncomingPacketBuffer: AsRef<[u8]>;
|
||||||
|
|
||||||
|
/// Remote physical address on whatever transport this session is using.
|
||||||
|
type RemoteAddress;
|
||||||
|
|
||||||
|
/// Rate limit for attempts to rekey existing sessions in milliseconds (default: 2000).
|
||||||
|
const REKEY_RATE_LIMIT_MS: i64 = 2000;
|
||||||
|
|
||||||
|
/// Get a reference to this host's static public key blob.
|
||||||
|
///
|
||||||
|
/// This must contain a NIST P-384 public key but can contain other information. In ZeroTier this
|
||||||
|
/// is a byte serialized identity. It could just be a naked NIST P-384 key if that's all you need.
|
||||||
|
fn get_local_s_public_blob(&self) -> &[u8];
|
||||||
|
|
||||||
|
/// Get SHA384(this host's static public key blob).
|
||||||
|
///
|
||||||
|
/// This allows us to avoid computing SHA384(public key blob) over and over again.
|
||||||
|
fn get_local_s_public_blob_hash(&self) -> &[u8; 48];
|
||||||
|
|
||||||
|
/// Get a reference to this hosts' static public key's NIST P-384 secret key pair.
|
||||||
|
///
|
||||||
|
/// This must return the NIST P-384 public key that is contained within the static public key blob.
|
||||||
|
fn get_local_s_keypair(&self) -> &P384KeyPair;
|
||||||
|
|
||||||
|
/// Extract the NIST P-384 ECC public key component from a static public key blob or return None on failure.
|
||||||
|
///
|
||||||
|
/// This is called to parse the static public key blob from the other end and extract its NIST P-384 public
|
||||||
|
/// key. SECURITY NOTE: the information supplied here is from the wire so care must be taken to parse it
|
||||||
|
/// safely and fail on any error or corruption.
|
||||||
|
fn extract_s_public_from_raw(static_public: &[u8]) -> Option<P384PublicKey>;
|
||||||
|
|
||||||
|
/// Look up a local session by local session ID or return None if not found.
|
||||||
|
fn lookup_session(&self, local_session_id: SessionId) -> Option<Self::SessionRef>;
|
||||||
|
|
||||||
|
/// Rate limit and check an attempted new session (called before accept_new_session).
|
||||||
|
fn check_new_session(&self, rc: &ReceiveContext<Self>, remote_address: &Self::RemoteAddress) -> bool;
|
||||||
|
|
||||||
|
/// Check whether a new session should be accepted.
|
||||||
|
///
|
||||||
|
/// On success a tuple of local session ID, static secret, and associated object is returned. The
|
||||||
|
/// static secret is whatever results from agreement between the local and remote static public
|
||||||
|
/// keys.
|
||||||
|
fn accept_new_session(
|
||||||
|
&self,
|
||||||
|
receive_context: &ReceiveContext<Self>,
|
||||||
|
remote_address: &Self::RemoteAddress,
|
||||||
|
remote_static_public: &[u8],
|
||||||
|
remote_metadata: &[u8],
|
||||||
|
) -> Option<(SessionId, Secret<64>, Self::SessionUserData)>;
|
||||||
|
}
|
108
zssp/src/constants.rs
Normal file
108
zssp/src/constants.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
|
||||||
|
/// Minimum size of a valid physical ZSSP packet or packet fragment.
|
||||||
|
pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE;
|
||||||
|
|
||||||
|
/// Minimum physical MTU for ZSSP to function.
|
||||||
|
pub const MIN_TRANSPORT_MTU: usize = 1280;
|
||||||
|
|
||||||
|
/// Minimum recommended interval between calls to service() on each session, in milliseconds.
|
||||||
|
pub const SERVICE_INTERVAL: u64 = 10000;
|
||||||
|
|
||||||
|
/// Setting this to true enables kyber1024 post-quantum forward secrecy.
|
||||||
|
///
|
||||||
|
/// Kyber1024 is used for data forward secrecy but not authentication. Authentication would
|
||||||
|
/// require Kyber1024 in identities, which would make them huge, and isn't needed for our
|
||||||
|
/// threat model which is data warehousing today to decrypt tomorrow. Breaking authentication
|
||||||
|
/// is only relevant today, not in some mid to far future where a QC that can break 384-bit ECC
|
||||||
|
/// exists.
|
||||||
|
///
|
||||||
|
/// This is normally enabled but could be disabled at build time for e.g. very small devices.
|
||||||
|
/// It might not even be necessary there to disable it since it's not that big and is usually
|
||||||
|
/// faster than NIST P-384 ECDH.
|
||||||
|
pub(crate) const JEDI: bool = true;
|
||||||
|
|
||||||
|
/// Maximum number of fragments for data packets.
|
||||||
|
pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63
|
||||||
|
|
||||||
|
/// Maximum number of fragments for key exchange packets (can be smaller to save memory, only a few needed)
|
||||||
|
pub(crate) const KEY_EXCHANGE_MAX_FRAGMENTS: usize = 2; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc.
|
||||||
|
|
||||||
|
/// Start attempting to rekey after a key has been used to send packets this many times.
|
||||||
|
///
|
||||||
|
/// This is 1/4 the NIST recommended maximum and 1/8 the absolute limit where u32 wraps.
|
||||||
|
/// As such it should leave plenty of margin against nearing key reuse bounds w/AES-GCM.
|
||||||
|
pub(crate) const REKEY_AFTER_USES: u64 = 536870912;
|
||||||
|
|
||||||
|
/// Maximum random jitter to add to rekey-after usage count.
|
||||||
|
pub(crate) const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576;
|
||||||
|
|
||||||
|
/// Hard expiration after this many uses.
|
||||||
|
///
|
||||||
|
/// Use of the key beyond this point is prohibited. If we reach this number of key uses
|
||||||
|
/// the key will be destroyed in memory and the session will cease to function. A hard
|
||||||
|
/// error is also generated.
|
||||||
|
pub(crate) const EXPIRE_AFTER_USES: u64 = (u32::MAX - 1024) as u64;
|
||||||
|
|
||||||
|
/// Start attempting to rekey after a key has been in use for this many milliseconds.
|
||||||
|
pub(crate) const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour
|
||||||
|
|
||||||
|
/// Maximum random jitter to add to rekey-after time.
|
||||||
|
pub(crate) const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 minutes
|
||||||
|
|
||||||
|
/// Version 0: AES-256-GCM + NIST P-384 + optional Kyber1024 PQ forward secrecy
|
||||||
|
pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00;
|
||||||
|
|
||||||
|
/// Secondary key type: none, use only P-384 for forward secrecy.
|
||||||
|
pub(crate) const E1_TYPE_NONE: u8 = 0;
|
||||||
|
|
||||||
|
/// Secondary key type: Kyber1024, PQ forward secrecy enabled.
|
||||||
|
pub(crate) const E1_TYPE_KYBER1024: u8 = 1;
|
||||||
|
|
||||||
|
/// Size of packet header
|
||||||
|
pub(crate) const HEADER_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// Size of AES-GCM keys (256 bits)
|
||||||
|
pub(crate) const AES_KEY_SIZE: usize = 32;
|
||||||
|
|
||||||
|
/// Size of AES-GCM MAC tags
|
||||||
|
pub(crate) const AES_GCM_TAG_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// Size of HMAC-SHA384 MAC tags
|
||||||
|
pub(crate) const HMAC_SIZE: usize = 48;
|
||||||
|
|
||||||
|
/// Size of a session ID, which behaves a bit like a TCP port number.
|
||||||
|
///
|
||||||
|
/// This is large since some ZeroTier nodes handle huge numbers of links, like roots and controllers.
|
||||||
|
pub(crate) const SESSION_ID_SIZE: usize = 6;
|
||||||
|
|
||||||
|
/// Number of session keys to hold at a given time (current, previous, next).
|
||||||
|
pub(crate) const KEY_HISTORY_SIZE: usize = 3;
|
||||||
|
|
||||||
|
// Packet types can range from 0 to 15 (4 bits) -- 0-3 are defined and 4-15 are reserved for future use
|
||||||
|
pub(crate) const PACKET_TYPE_DATA: u8 = 0;
|
||||||
|
pub(crate) const PACKET_TYPE_NOP: u8 = 1;
|
||||||
|
pub(crate) const PACKET_TYPE_INITIAL_KEY_OFFER: u8 = 2; // "alice"
|
||||||
|
pub(crate) const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3; // "bob"
|
||||||
|
|
||||||
|
// Key usage labels for sub-key derivation using NIST-style KBKDF (basically just HMAC KDF).
|
||||||
|
pub(crate) const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'M'; // HMAC-SHA384 authentication for key exchanges
|
||||||
|
pub(crate) const KBKDF_KEY_USAGE_LABEL_HEADER_CHECK: u8 = b'H'; // AES-based header check code generation
|
||||||
|
pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-GCM in A->B direction
|
||||||
|
pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction
|
||||||
|
pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHETING: u8 = b'R'; // Key input for next ephemeral ratcheting
|
||||||
|
|
||||||
|
// AES key size for header check code generation
|
||||||
|
pub(crate) const HEADER_CHECK_AES_KEY_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// Aribitrary starting value for master key derivation.
|
||||||
|
///
|
||||||
|
/// It doesn't matter very much what this is but it's good for it to be unique. It should
|
||||||
|
/// be changed if this code is changed in any cryptographically meaningful way like changing
|
||||||
|
/// the primary algorithm from NIST P-384 or the transport cipher from AES-GCM.
|
||||||
|
pub(crate) const INITIAL_KEY: [u8; 64] = [
|
||||||
|
// macOS command line to generate:
|
||||||
|
// echo -n 'ZSSP_Noise_IKpsk2_NISTP384_?KYBER1024_AESGCM_SHA512' | shasum -a 512 | cut -d ' ' -f 1 | xxd -r -p | xxd -i
|
||||||
|
0x35, 0x6a, 0x75, 0xc0, 0xbf, 0xbe, 0xc3, 0x59, 0x70, 0x94, 0x50, 0x69, 0x4c, 0xa2, 0x08, 0x40, 0xc7, 0xdf, 0x67, 0xa8, 0x68, 0x52,
|
||||||
|
0x6e, 0xd5, 0xdd, 0x77, 0xec, 0x59, 0x6f, 0x8e, 0xa1, 0x99, 0xb4, 0x32, 0x85, 0xaf, 0x7f, 0x0d, 0xa9, 0x6c, 0x01, 0xfb, 0x72, 0x46,
|
||||||
|
0xc0, 0x09, 0x58, 0xb8, 0xe0, 0xa8, 0xcf, 0xb1, 0x58, 0x04, 0x6e, 0x32, 0xba, 0xa8, 0xb8, 0xf9, 0x0a, 0xa4, 0xbf, 0x36,
|
||||||
|
];
|
119
zssp/src/ints.rs
Normal file
119
zssp/src/ints.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
|
||||||
|
use std::{sync::atomic::{AtomicU64, Ordering}};
|
||||||
|
|
||||||
|
use zerotier_crypto::random;
|
||||||
|
use zerotier_utils::memory;
|
||||||
|
|
||||||
|
|
||||||
|
/// "Canonical header" for generating 96-bit AES-GCM nonce and for inclusion in HMACs.
|
||||||
|
///
|
||||||
|
/// This is basically the actual header but with fragment count and fragment total set to zero.
|
||||||
|
/// Fragmentation is not considered when authenticating the entire packet. A separate header
|
||||||
|
/// check code is used to make fragmentation itself more robust, but that's outside the scope
|
||||||
|
/// of AEAD authentication.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub(crate) struct CanonicalHeader(pub u64, pub u32);
|
||||||
|
impl CanonicalHeader {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn make(session_id: SessionId, packet_type: u8, counter: u32) -> Self {
|
||||||
|
CanonicalHeader(
|
||||||
|
(u64::from(session_id) | (packet_type as u64).wrapping_shl(48)).to_le(),
|
||||||
|
counter.to_le(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn as_bytes(&self) -> &[u8; 12] {
|
||||||
|
memory::as_byte_array(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 48-bit session ID (most significant 16 bits of u64 are unused)
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct SessionId(pub(crate) u64);
|
||||||
|
|
||||||
|
impl SessionId {
|
||||||
|
/// The nil session ID used in messages initiating a new session.
|
||||||
|
///
|
||||||
|
/// This is all 1's so that ZeroTier can easily tell the difference between ZSSP init packets
|
||||||
|
/// and ZeroTier V1 packets.
|
||||||
|
pub const NIL: SessionId = SessionId(0xffffffffffff);
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn new_from_u64(i: u64) -> Option<SessionId> {
|
||||||
|
if i < Self::NIL.0 {
|
||||||
|
Some(Self(i))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn new_random() -> Self {
|
||||||
|
Self(random::next_u64_secure() % Self::NIL.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SessionId> for u64 {
|
||||||
|
#[inline(always)]
|
||||||
|
fn from(sid: SessionId) -> Self {
|
||||||
|
sid.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Outgoing packet counter with strictly ordered atomic semantics.
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub(crate) struct Counter(AtomicU64);
|
||||||
|
|
||||||
|
impl Counter {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
// Using a random value has no security implication. Zero would be fine. This just
|
||||||
|
// helps randomize packet contents a bit.
|
||||||
|
Self(AtomicU64::new(random::next_u32_secure() as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the value most recently used to send a packet.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn previous(&self) -> CounterValue {
|
||||||
|
CounterValue(self.0.load(Ordering::SeqCst))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a counter value for the next packet being sent.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn next(&self) -> CounterValue {
|
||||||
|
CounterValue(self.0.fetch_add(1, Ordering::SeqCst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value of the outgoing packet counter.
|
||||||
|
///
|
||||||
|
/// The used portion of the packet counter is the least significant 32 bits, but the internal
|
||||||
|
/// counter state is kept as a 64-bit integer. This makes it easier to correctly handle
|
||||||
|
/// key expiration after usage limits are reached without complicated logic to handle 32-bit
|
||||||
|
/// wrapping. Usage limits are below 2^32 so the actual 32-bit counter will not wrap for a
|
||||||
|
/// given shared secret key.
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub(crate) struct CounterValue(pub u64);
|
||||||
|
|
||||||
|
impl CounterValue {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn to_u32(&self) -> u32 {
|
||||||
|
self.0 as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Was this side the one who sent the first offer (Alice) or countered (Bob).
|
||||||
|
/// Note that role is not fixed. Either side can take either role. It's just who
|
||||||
|
/// initiated first.
|
||||||
|
pub enum Role {
|
||||||
|
Alice,
|
||||||
|
Bob,
|
||||||
|
}
|
10
zssp/src/lib.rs
Normal file
10
zssp/src/lib.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
mod zssp;
|
||||||
|
mod app_layer;
|
||||||
|
mod ints;
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub mod constants;
|
||||||
|
pub use zssp::{Error, ReceiveResult, ReceiveContext, Session};
|
||||||
|
pub use app_layer::ApplicationLayer;
|
||||||
|
pub use ints::{SessionId, Role};
|
226
zssp/src/tests.rs
Normal file
226
zssp/src/tests.rs
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::LinkedList;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use zerotier_crypto::hash::SHA384;
|
||||||
|
use zerotier_crypto::p384::{P384KeyPair, P384PublicKey};
|
||||||
|
use zerotier_crypto::random;
|
||||||
|
use zerotier_crypto::secret::Secret;
|
||||||
|
use zerotier_utils::hex;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use crate::*;
|
||||||
|
use constants::*;
|
||||||
|
|
||||||
|
struct TestHost {
|
||||||
|
local_s: P384KeyPair,
|
||||||
|
local_s_hash: [u8; 48],
|
||||||
|
psk: Secret<64>,
|
||||||
|
session: Mutex<Option<Arc<Session<Box<TestHost>>>>>,
|
||||||
|
session_id_counter: Mutex<u64>,
|
||||||
|
queue: Mutex<LinkedList<Vec<u8>>>,
|
||||||
|
key_id: Mutex<[u8; 16]>,
|
||||||
|
this_name: &'static str,
|
||||||
|
other_name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestHost {
|
||||||
|
fn new(psk: Secret<64>, this_name: &'static str, other_name: &'static str) -> Self {
|
||||||
|
let local_s = P384KeyPair::generate();
|
||||||
|
let local_s_hash = SHA384::hash(local_s.public_key_bytes());
|
||||||
|
Self {
|
||||||
|
local_s,
|
||||||
|
local_s_hash,
|
||||||
|
psk,
|
||||||
|
session: Mutex::new(None),
|
||||||
|
session_id_counter: Mutex::new(1),
|
||||||
|
queue: Mutex::new(LinkedList::new()),
|
||||||
|
key_id: Mutex::new([0; 16]),
|
||||||
|
this_name,
|
||||||
|
other_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationLayer for Box<TestHost> {
|
||||||
|
type SessionUserData = u32;
|
||||||
|
type SessionRef = Arc<Session<Box<TestHost>>>;
|
||||||
|
type IncomingPacketBuffer = Vec<u8>;
|
||||||
|
type RemoteAddress = u32;
|
||||||
|
|
||||||
|
const REKEY_RATE_LIMIT_MS: i64 = 0;
|
||||||
|
|
||||||
|
fn get_local_s_public_blob(&self) -> &[u8] {
|
||||||
|
self.local_s.public_key_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_local_s_public_blob_hash(&self) -> &[u8; 48] {
|
||||||
|
&self.local_s_hash
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_local_s_keypair(&self) -> &P384KeyPair {
|
||||||
|
&self.local_s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_s_public_from_raw(static_public: &[u8]) -> Option<P384PublicKey> {
|
||||||
|
P384PublicKey::from_bytes(static_public)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_session(&self, local_session_id: SessionId) -> Option<Self::SessionRef> {
|
||||||
|
self.session.lock().unwrap().as_ref().and_then(|s| {
|
||||||
|
if s.id == local_session_id {
|
||||||
|
Some(s.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_new_session(&self, _: &ReceiveContext<Self>, _: &Self::RemoteAddress) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_new_session(
|
||||||
|
&self,
|
||||||
|
_: &ReceiveContext<Self>,
|
||||||
|
_: &u32,
|
||||||
|
_: &[u8],
|
||||||
|
_: &[u8],
|
||||||
|
) -> Option<(SessionId, Secret<64>, Self::SessionUserData)> {
|
||||||
|
loop {
|
||||||
|
let mut new_id = self.session_id_counter.lock().unwrap();
|
||||||
|
*new_id += 1;
|
||||||
|
return Some((SessionId::new_from_u64(*new_id).unwrap(), self.psk.clone(), 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[test]
|
||||||
|
fn establish_session() {
|
||||||
|
let mut data_buf = [0_u8; (1280 - 32) * MAX_FRAGMENTS];
|
||||||
|
let mut mtu_buffer = [0_u8; 1280];
|
||||||
|
let mut psk: Secret<64> = Secret::default();
|
||||||
|
random::fill_bytes_secure(&mut psk.0);
|
||||||
|
|
||||||
|
let alice_host = Box::new(TestHost::new(psk.clone(), "alice", "bob"));
|
||||||
|
let bob_host = Box::new(TestHost::new(psk.clone(), "bob", "alice"));
|
||||||
|
let alice_rc: Box<ReceiveContext<Box<TestHost>>> = Box::new(ReceiveContext::new(&alice_host));
|
||||||
|
let bob_rc: Box<ReceiveContext<Box<TestHost>>> = Box::new(ReceiveContext::new(&bob_host));
|
||||||
|
|
||||||
|
//println!("zssp: size of session (bytes): {}", std::mem::size_of::<Session<Box<TestHost>>>());
|
||||||
|
|
||||||
|
let _ = alice_host.session.lock().unwrap().insert(Arc::new(
|
||||||
|
Session::start_new(
|
||||||
|
&alice_host,
|
||||||
|
|data| bob_host.queue.lock().unwrap().push_front(data.to_vec()),
|
||||||
|
SessionId::new_random(),
|
||||||
|
bob_host.local_s.public_key_bytes(),
|
||||||
|
&[],
|
||||||
|
&psk,
|
||||||
|
1,
|
||||||
|
mtu_buffer.len(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut ts = 0;
|
||||||
|
for test_loop in 0..256 {
|
||||||
|
for host in [&alice_host, &bob_host] {
|
||||||
|
let send_to_other = |data: &mut [u8]| {
|
||||||
|
if std::ptr::eq(host, &alice_host) {
|
||||||
|
bob_host.queue.lock().unwrap().push_front(data.to_vec());
|
||||||
|
} else {
|
||||||
|
alice_host.queue.lock().unwrap().push_front(data.to_vec());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let rc = if std::ptr::eq(host, &alice_host) {
|
||||||
|
&alice_rc
|
||||||
|
} else {
|
||||||
|
&bob_rc
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(qi) = host.queue.lock().unwrap().pop_back() {
|
||||||
|
let qi_len = qi.len();
|
||||||
|
ts += 1;
|
||||||
|
let r = rc.receive(host, &0, send_to_other, &mut data_buf, qi, mtu_buffer.len(), ts);
|
||||||
|
if r.is_ok() {
|
||||||
|
let r = r.unwrap();
|
||||||
|
match r {
|
||||||
|
ReceiveResult::Ok => {
|
||||||
|
//println!("zssp: {} => {} ({}): Ok", host.other_name, host.this_name, qi_len);
|
||||||
|
}
|
||||||
|
ReceiveResult::OkData(data) => {
|
||||||
|
//println!("zssp: {} => {} ({}): OkData length=={}", host.other_name, host.this_name, qi_len, data.len());
|
||||||
|
assert!(!data.iter().any(|x| *x != 0x12));
|
||||||
|
}
|
||||||
|
ReceiveResult::OkNewSession(new_session) => {
|
||||||
|
println!(
|
||||||
|
"zssp: {} => {} ({}): OkNewSession ({})",
|
||||||
|
host.other_name,
|
||||||
|
host.this_name,
|
||||||
|
qi_len,
|
||||||
|
u64::from(new_session.id)
|
||||||
|
);
|
||||||
|
let mut hs = host.session.lock().unwrap();
|
||||||
|
assert!(hs.is_none());
|
||||||
|
let _ = hs.insert(Arc::new(new_session));
|
||||||
|
}
|
||||||
|
ReceiveResult::Ignored => {
|
||||||
|
println!("zssp: {} => {} ({}): Ignored", host.other_name, host.this_name, qi_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"zssp: {} => {} ({}): error: {}",
|
||||||
|
host.other_name,
|
||||||
|
host.this_name,
|
||||||
|
qi_len,
|
||||||
|
r.err().unwrap().to_string()
|
||||||
|
);
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data_buf.fill(0x12);
|
||||||
|
if let Some(session) = host.session.lock().unwrap().as_ref().cloned() {
|
||||||
|
if session.established() {
|
||||||
|
{
|
||||||
|
let mut key_id = host.key_id.lock().unwrap();
|
||||||
|
let security_info = session.status().unwrap();
|
||||||
|
if !security_info.0.eq(key_id.as_ref()) {
|
||||||
|
*key_id = security_info.0;
|
||||||
|
println!(
|
||||||
|
"zssp: new key at {}: fingerprint {} ratchet {} kyber {}",
|
||||||
|
host.this_name,
|
||||||
|
hex::to_string(key_id.as_ref()),
|
||||||
|
security_info.2,
|
||||||
|
security_info.3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _ in 0..4 {
|
||||||
|
assert!(session
|
||||||
|
.send(
|
||||||
|
send_to_other,
|
||||||
|
&mut mtu_buffer,
|
||||||
|
&data_buf[..((random::xorshift64_random() as usize) % data_buf.len())]
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
}
|
||||||
|
if (test_loop % 8) == 0 && test_loop >= 8 && host.this_name.eq("alice") {
|
||||||
|
session.service(host, send_to_other, &[], mtu_buffer.len(), test_loop as i64, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue