A bunch of reorg, and add a safety valve to nuke the send key in ZSSP if it is hard-expired.

This commit is contained in:
Adam Ierymenko 2022-09-13 17:27:58 -04:00
parent 54d6fba6c5
commit 2649ce7571
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
13 changed files with 252 additions and 116 deletions

View file

@ -11,3 +11,5 @@ path = "src/main.rs"
zerotier-crypto = { path = "../crypto" }
zerotier-utils = { path = "../utils" }
zerotier-network-hypervisor = { path = "../network-hypervisor" }
serde = { version = "^1", features = ["derive"], default-features = false }
serde_json = { version = "^1", features = ["std"], default-features = false }

1
controller/src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod model;

127
controller/src/model.rs Normal file
View file

@ -0,0 +1,127 @@
// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
use std::hash::Hash;
use serde::{Deserialize, Serialize};
use zerotier_network_hypervisor::vl1::Address;
use zerotier_network_hypervisor::vl1::InetAddress;
use zerotier_network_hypervisor::vl2::NetworkId;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ObjectType {
#[serde(rename = "network")]
Network,
#[serde(rename = "member")]
Member,
}
impl ObjectType {
fn network() -> ObjectType {
Self::Network
}
fn member() -> ObjectType {
Self::Member
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ipv4AssignMode {
pub zt: bool,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ipv6AssignMode {
pub zt: bool,
pub rfc4193: bool,
#[serde(rename = "6plane")]
pub _6plane: bool,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IpAssignmentPool {
#[serde(rename = "ipRangeStart")]
ip_range_start: InetAddress,
#[serde(rename = "ipRangeEnd")]
ip_range_end: InetAddress,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Network {
pub id: NetworkId,
pub name: String,
#[serde(rename = "creationTime")]
pub creation_time: i64,
#[serde(rename = "multicastLimit")]
pub multicast_limit: u64,
#[serde(rename = "enableBroadcast")]
pub enable_broadcast: bool,
#[serde(rename = "v4AssignMode")]
pub v4_assign_mode: Ipv4AssignMode,
#[serde(rename = "v6AssignMode")]
pub v6_assign_mode: Ipv6AssignMode,
#[serde(rename = "ipAssignmentPools")]
pub ip_assignment_pools: Vec<IpAssignmentPool>,
#[serde(rename = "rulesSource")]
pub rules_source: String,
pub mtu: u16,
pub private: bool,
#[serde(default = "ObjectType::network")]
pub objtype: ObjectType,
}
impl Hash for Network {
#[inline(always)]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state)
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Member {
#[serde(rename = "networkId")]
pub network_id: NetworkId,
pub address: Address,
pub name: String,
#[serde(rename = "creationTime")]
pub creation_time: i64,
#[serde(rename = "ipAssignments")]
pub ip_assignments: Vec<InetAddress>,
#[serde(rename = "noAutoAssignIps")]
pub no_auto_assign_ips: bool,
#[serde(rename = "vMajor")]
pub version_major: u16,
#[serde(rename = "vMinor")]
pub version_minor: u16,
#[serde(rename = "vRev")]
pub version_revision: u16,
#[serde(rename = "vProto")]
pub version_protocol: u16,
pub authorized: bool,
#[serde(rename = "activeBridge")]
pub bridge: bool,
#[serde(rename = "ssoExempt")]
pub sso_exempt: bool,
#[serde(default = "ObjectType::member")]
pub objtype: ObjectType,
}
impl Hash for Member {
#[inline(always)]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.network_id.hash(state);
self.address.hash(state);
}
}

View file

@ -20,12 +20,13 @@ extern "C" {
pub struct Secret<const L: usize>(pub [u8; L]);
impl<const L: usize> Secret<L> {
/// Create a new all-zero secret.
#[inline(always)]
pub fn new() -> Self {
Self([0_u8; L])
}
/// Copy bytes into secret, will panic if size does not match.
/// Copy bytes into secret, will panic if the slice does not match the size of this secret.
#[inline(always)]
pub fn from_bytes(b: &[u8]) -> Self {
Self(b.try_into().unwrap())
@ -36,16 +37,26 @@ impl<const L: usize> Secret<L> {
&self.0
}
/// Get the first N bytes of this secret as a fixed length array.
#[inline(always)]
pub fn first_n<const N: usize>(&self) -> &[u8; N] {
assert!(N <= L);
unsafe { &*self.0.as_ptr().cast() }
}
/// Clone the first N bytes of this secret as another secret.
#[inline(always)]
pub fn first_n_clone<const N: usize>(&self) -> Secret<N> {
Secret::<N>(self.first_n().clone())
}
/// Destroy the contents of this secret, ignoring normal Rust mutability constraints.
///
/// This can be used to force a secret to be forgotten under e.g. key lifetime exceeded or error conditions.
#[inline(always)]
pub fn nuke(&self) {
unsafe { OPENSSL_cleanse(std::mem::transmute(self.0.as_ptr().cast::<c_void>()), L) };
}
}
impl<const L: usize> Drop for Secret<L> {

View file

@ -1604,6 +1604,11 @@ impl SessionKey {
.pop()
.unwrap_or_else(|| Box::new(AesGcm::new(self.send_key.as_bytes(), true))))
} else {
// Not only do we return an error, but we also destroy the key.
let mut scp = self.send_cipher_pool.lock();
scp.clear();
self.send_key.nuke();
Err(Error::MaxKeyLifetimeExceeded)
}
}

View file

@ -26,27 +26,3 @@ macro_rules! debug_event {
#[allow(unused_imports)]
pub(crate) use debug_event;
/// Obtain a view into a byte array cast as another byte array.
#[inline(always)]
pub(crate) fn byte_array_range<const A: usize, const START: usize, const LEN: usize>(a: &[u8; A]) -> &[u8; LEN] {
assert!((START + LEN) <= A);
unsafe { &*a.as_ptr().add(START).cast::<[u8; LEN]>() }
}
/// View a flat (Copy) object as a byte array.
#[inline(always)]
pub(crate) fn flat_object_as_bytes<T: Copy>(t: &T) -> &[u8] {
unsafe { &*std::ptr::slice_from_raw_parts((t as *const T).cast::<u8>(), std::mem::size_of::<T>()) }
}
/// Trait that annotates a type as being alignment neutral, such as a packed struct of all bytes and byte arrays.
pub(crate) unsafe trait AlignmentNeutral: Copy {}
/// View a byte array as a flat (Copy) object.
/// To be safe this can only be used with annotated alignment-neutral structs.
#[inline(always)]
pub(crate) fn bytes_as_flat_object<T: Copy + AlignmentNeutral>(b: &[u8]) -> &T {
assert!(b.len() >= std::mem::size_of::<T>());
unsafe { &*b.as_ptr().cast() }
}

View file

@ -15,9 +15,9 @@ use zerotier_crypto::secret::Secret;
use zerotier_crypto::x25519::*;
use zerotier_utils::hex;
use zerotier_utils::memory::{as_byte_array, as_flat_object};
use crate::error::{InvalidFormatError, InvalidParameterError};
use crate::util::{bytes_as_flat_object, flat_object_as_bytes, AlignmentNeutral};
use crate::vl1::protocol::{ADDRESS_SIZE, ADDRESS_SIZE_STRING, IDENTITY_FINGERPRINT_SIZE, IDENTITY_POW_THRESHOLD};
use crate::vl1::Address;
@ -394,7 +394,7 @@ impl Identity {
pub fn to_public_bytes(&self) -> IdentityBytes {
if let Some(p384) = self.p384.as_ref() {
IdentityBytes::X25519P384Public(
flat_object_as_bytes(&packed::V1 {
as_byte_array(&packed::V1 {
v0: packed::V0 {
address: self.address.to_bytes(),
key_type: 0,
@ -410,12 +410,11 @@ impl Identity {
ecdsa_self_signature: p384.ecdsa_self_signature,
ed25519_self_signature: p384.ed25519_self_signature,
})
.try_into()
.unwrap(),
.clone(),
)
} else {
IdentityBytes::X25519Public(
flat_object_as_bytes(&packed::V0 {
as_byte_array(&packed::V0 {
address: self.address.to_bytes(),
key_type: 0,
x25519: self.x25519,
@ -424,8 +423,7 @@ impl Identity {
reserved: 0x03,
ext_len: [0; 2],
})
.try_into()
.unwrap(),
.clone(),
)
}
}
@ -434,7 +432,7 @@ impl Identity {
self.secret.as_ref().map(|s| {
if let Some(p384) = s.p384.as_ref() {
IdentityBytes::X25519P384Secret(
flat_object_as_bytes(&packed::V1S {
as_byte_array(&packed::V1S {
v0s: packed::V0S {
address: self.address.to_bytes(),
key_type: 0,
@ -454,12 +452,11 @@ impl Identity {
ecdh_secret: p384.ecdh.secret_key_bytes().0.clone(),
ecdsa_secret: p384.ecdsa.secret_key_bytes().0.clone(),
})
.try_into()
.unwrap(),
.clone(),
)
} else {
IdentityBytes::X25519Secret(
flat_object_as_bytes(&packed::V0S {
as_byte_array(&packed::V0S {
address: self.address.to_bytes(),
key_type: 0,
x25519: self.x25519,
@ -470,8 +467,7 @@ impl Identity {
reserved: 0x03,
ext_len: [0; 2],
})
.try_into()
.unwrap(),
.clone(),
)
}
})
@ -483,7 +479,7 @@ impl Identity {
pub fn from_bytes(bytes: &IdentityBytes) -> Option<Self> {
let mut id = match bytes {
IdentityBytes::X25519Public(b) => {
let b: &packed::V0 = bytes_as_flat_object(b);
let b: &packed::V0 = as_flat_object(b);
if b.key_type == 0 && b.secret_length == 0 && b.reserved == 0x03 && u16::from_be_bytes(b.ext_len) == 0 {
Some(Self {
address: Address::from_bytes_fixed(&b.address)?,
@ -498,7 +494,7 @@ impl Identity {
}
}
IdentityBytes::X25519Secret(b) => {
let b: &packed::V0S = bytes_as_flat_object(b);
let b: &packed::V0S = as_flat_object(b);
if b.key_type == 0
&& b.secret_length == (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8
&& b.reserved == 0x03
@ -521,7 +517,7 @@ impl Identity {
}
}
IdentityBytes::X25519P384Public(b) => {
let b: &packed::V1 = bytes_as_flat_object(b);
let b: &packed::V1 = as_flat_object(b);
if b.v0.key_type == 0
&& b.v0.secret_length == 0
&& b.v0.reserved == 0x03
@ -546,7 +542,7 @@ impl Identity {
}
}
IdentityBytes::X25519P384Secret(b) => {
let b: &packed::V1S = bytes_as_flat_object(b);
let b: &packed::V1S = as_flat_object(b);
if b.v0s.key_type == 0
&& b.v0s.secret_length == (C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE) as u8
&& b.v0s.reserved == 0x03
@ -587,7 +583,7 @@ impl Identity {
pub fn read_bytes<R: Read>(r: &mut R) -> std::io::Result<Self> {
let mut buf = [0_u8; 512];
r.read_exact(&mut buf[..Self::BYTE_LENGTH_X25519_PUBLIC])?;
let x25519_public = bytes_as_flat_object::<packed::V0>(&buf);
let x25519_public: &packed::V0 = as_flat_object(&buf);
let ext_len = u16::from_be_bytes(x25519_public.ext_len) as usize;
let obj_len = if x25519_public.secret_length == 0 {
let obj_len = ext_len + Self::BYTE_LENGTH_X25519_PUBLIC;
@ -910,11 +906,6 @@ mod packed {
pub ecdh_secret: [u8; P384_SECRET_KEY_SIZE],
pub ecdsa_secret: [u8; P384_SECRET_KEY_SIZE],
}
unsafe impl AlignmentNeutral for V0 {}
unsafe impl AlignmentNeutral for V0S {}
unsafe impl AlignmentNeutral for V1 {}
unsafe impl AlignmentNeutral for V1S {}
}
/// Identity rendered as a flat byte array.

View file

@ -11,9 +11,9 @@ use zerotier_crypto::poly1305;
use zerotier_crypto::random::next_u64_secure;
use zerotier_crypto::salsa::Salsa;
use zerotier_crypto::secret::Secret;
use zerotier_utils::memory::array_range;
use crate::util::buffer::BufferReader;
use crate::util::byte_array_range;
use crate::util::debug_event;
use crate::util::marshalable::Marshalable;
use crate::vl1::address::Address;
@ -142,7 +142,7 @@ fn try_aead_decrypt(
// AES-GMAC-SIV encrypts the packet ID too as part of its computation of a single
// opaque 128-bit tag, so to get the original packet ID we have to grab it from the
// decrypted tag.
Some(u64::from_ne_bytes(*byte_array_range::<16, 0, 8>(tag)))
Some(u64::from_ne_bytes(*array_range::<u8, 16, 0, 8>(tag)))
} else {
None
}
@ -446,11 +446,11 @@ impl<SI: SystemInterface> Peer<SI> {
aes_gmac_siv.encrypt_second_pass_in_place(payload);
let tag = aes_gmac_siv.encrypt_second_pass_finish();
let header = packet.struct_mut_at::<v1::PacketHeader>(0).unwrap();
header.id = *byte_array_range::<16, 0, 8>(tag);
header.id = *array_range::<u8, 16, 0, 8>(tag);
header.dest = self.identity.address.to_bytes();
header.src = node.identity.address.to_bytes();
header.flags_cipher_hops = flags_cipher_hops;
header.mac = *byte_array_range::<16, 8, 8>(tag);
header.mac = *array_range::<u8, 16, 8, 8>(tag);
} else {
return false;
}

View file

@ -12,6 +12,7 @@ path = "src/main.rs"
[dependencies]
zerotier-network-hypervisor = { path = "../network-hypervisor" }
zerotier-crypto = { path = "../crypto" }
zerotier-utils = { path = "../utils" }
async-trait = "^0"
num-traits = "^0"
tokio = { version = "^1", features = ["fs", "io-util", "io-std", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "sync", "time"], default-features = false }

View file

@ -12,6 +12,7 @@ use zerotier_network_hypervisor::vl2::*;
use zerotier_network_hypervisor::*;
use zerotier_crypto::random;
use zerotier_utils::{ms_monotonic, ms_since_epoch};
use tokio::time::Duration;
@ -19,7 +20,6 @@ use crate::datadir::DataDir;
use crate::localinterface::LocalInterface;
use crate::localsocket::LocalSocket;
use crate::udp::*;
use crate::utils::{ms_monotonic, ms_since_epoch};
/// Interval between scans of system network interfaces to update port bindings.
const UDP_UPDATE_BINDINGS_INTERVAL_MS: Duration = Duration::from_millis(5000);

View file

@ -2,13 +2,10 @@
use std::path::Path;
use std::str::FromStr;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use serde::de::DeserializeOwned;
use serde::Serialize;
use lazy_static::lazy_static;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
@ -19,20 +16,32 @@ use zerotier_network_hypervisor::vl1::Identity;
/// Default sanity limit parameter for read_limit() used throughout the service.
pub const DEFAULT_FILE_IO_READ_LIMIT: usize = 1048576;
lazy_static! {
static ref STARTUP_INSTANT: Instant = Instant::now();
/// Convenience function to read up to limit bytes from a file.
///
/// If the file is larger than limit, the excess is not read.
pub async fn read_limit<P: AsRef<Path>>(path: P, limit: usize) -> std::io::Result<Vec<u8>> {
let mut f = File::open(path).await?;
let bytes = f.metadata().await?.len().min(limit as u64) as usize;
let mut v: Vec<u8> = Vec::with_capacity(bytes);
v.resize(bytes, 0);
f.read_exact(v.as_mut_slice()).await?;
Ok(v)
}
/// Get milliseconds since unix epoch.
#[inline(always)]
pub fn ms_since_epoch() -> i64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as i64
}
/// Get milliseconds since an arbitrary time in the past, guaranteed to monotonically increase.
#[inline(always)]
pub fn ms_monotonic() -> i64 {
Instant::now().duration_since(*STARTUP_INSTANT).as_millis() as i64
/// Set permissions on a file or directory to be most restrictive (visible only to the service's user).
#[cfg(unix)]
pub fn fs_restrict_permissions<P: AsRef<Path>>(path: P) -> bool {
unsafe {
let c_path = std::ffi::CString::new(path.as_ref().to_str().unwrap()).unwrap();
libc::chmod(
c_path.as_ptr(),
if path.as_ref().is_dir() {
0o700
} else {
0o600
},
) == 0
}
}
/// Returns true if the string starts with [yY1tT] or false for [nN0fF].
@ -143,34 +152,6 @@ pub fn to_json_pretty<O: serde::Serialize>(o: &O) -> String {
}
}
/// Convenience function to read up to limit bytes from a file.
///
/// If the file is larger than limit, the excess is not read.
pub async fn read_limit<P: AsRef<Path>>(path: P, limit: usize) -> std::io::Result<Vec<u8>> {
let mut f = File::open(path).await?;
let bytes = f.metadata().await?.len().min(limit as u64) as usize;
let mut v: Vec<u8> = Vec::with_capacity(bytes);
v.resize(bytes, 0);
f.read_exact(v.as_mut_slice()).await?;
Ok(v)
}
/// Set permissions on a file or directory to be most restrictive (visible only to the service's user).
#[cfg(unix)]
pub fn fs_restrict_permissions<P: AsRef<Path>>(path: P) -> bool {
unsafe {
let c_path = std::ffi::CString::new(path.as_ref().to_str().unwrap()).unwrap();
libc::chmod(
c_path.as_ptr(),
if path.as_ref().is_dir() {
0o700
} else {
0o600
},
) == 0
}
}
/// Read an identity as either a literal or from a file.
pub async fn parse_cli_identity(input: &str, validate: bool) -> Result<Identity, String> {
let parse_func = |s: &str| {
@ -201,22 +182,3 @@ pub async fn parse_cli_identity(input: &str, validate: bool) -> Result<Identity,
//pub fn c_strerror() -> String {
// unsafe { std::ffi::CStr::from_ptr(libc::strerror(*libc::__error()).cast()).to_string_lossy().to_string() }
//}
#[cfg(test)]
mod tests {
use crate::utils::ms_monotonic;
use std::time::Duration;
#[test]
fn monotonic_clock_sanity_check() {
let start = ms_monotonic();
std::thread::sleep(Duration::from_millis(500));
let end = ms_monotonic();
// per docs:
//
// The thread may sleep longer than the duration specified due to scheduling specifics or
// platform-dependent functionality. It will never sleep less.
//
assert!((end - start).abs() >= 500);
}
}

View file

@ -1,3 +1,5 @@
// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
pub mod arrayvec;
pub mod gatherarray;
pub mod hex;
@ -5,3 +7,43 @@ pub mod memory;
pub mod pool;
pub mod ringbuffermap;
pub mod varint;
/// Get milliseconds since unix epoch.
pub fn ms_since_epoch() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64
}
/// Get milliseconds since an arbitrary time in the past, guaranteed to monotonically increase.
pub fn ms_monotonic() -> i64 {
static STARTUP_INSTANT: parking_lot::RwLock<Option<std::time::Instant>> = parking_lot::RwLock::new(None);
let si = *STARTUP_INSTANT.read();
let instant_zero = if let Some(si) = si {
si
} else {
*STARTUP_INSTANT.write().get_or_insert(std::time::Instant::now())
};
std::time::Instant::now().duration_since(instant_zero).as_millis() as i64
}
#[cfg(test)]
mod tests {
use super::ms_monotonic;
use std::time::Duration;
#[test]
fn monotonic_clock_sanity_check() {
let start = ms_monotonic();
std::thread::sleep(Duration::from_millis(500));
let end = ms_monotonic();
// per docs:
//
// The thread may sleep longer than the duration specified due to scheduling specifics or
// platform-dependent functionality. It will never sleep less.
//
assert!((end - start).abs() >= 500);
assert!((end - start).abs() < 750);
}
}

View file

@ -2,6 +2,7 @@
use std::mem::size_of;
// Version for architectures that definitely don't care about unaligned memory access.
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
#[allow(unused)]
mod fast_int_memory_access {
@ -66,6 +67,7 @@ mod fast_int_memory_access {
}
}
// Version for architectures that might care about unaligned memory access.
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
#[allow(unused)]
mod fast_int_memory_access {
@ -122,10 +124,26 @@ mod fast_int_memory_access {
pub use fast_int_memory_access::*;
/// Obtain a view into an array cast as another array.
/// This will panic if the template parameters would result in out of bounds access.
#[inline(always)]
pub fn array_range<T: Copy, const S: usize, const START: usize, const LEN: usize>(a: &[T; S]) -> &[T; LEN] {
assert!((START + LEN) <= S);
unsafe { &*a.as_ptr().add(START).cast::<[T; LEN]>() }
}
/// Get a reference to a raw object as a byte array.
/// The template parameter S must equal the size of the object in bytes or this will panic.
#[inline(always)]
pub fn as_byte_array<T: Copy, const S: usize>(o: &T) -> &[u8; S] {
assert_eq!(S, size_of::<T>());
unsafe { &*(o as *const T).cast::<[u8; S]>() }
unsafe { &*(o as *const T).cast() }
}
/// Get a byte array as a flat object.
///
/// WARNING: while this is technically safe, care must be taken if the object requires aligned access.
pub fn as_flat_object<T: Copy, const S: usize>(b: &[u8; S]) -> &T {
assert!(std::mem::size_of::<T>() <= S);
unsafe { &*b.as_ptr().cast() }
}