mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-07 21:13:44 +02:00
More controller work and some ZSSP cleanup.
This commit is contained in:
parent
8a50427833
commit
9e6617b324
9 changed files with 275 additions and 144 deletions
|
@ -11,6 +11,7 @@ use zerotier_network_hypervisor::protocol::{PacketBuffer, DEFAULT_MULTICAST_LIMI
|
|||
use zerotier_network_hypervisor::vl1::{HostSystem, Identity, InnerProtocol, Node, PacketHandlerResult, Path, PathFilter, Peer};
|
||||
use zerotier_network_hypervisor::vl2;
|
||||
use zerotier_network_hypervisor::vl2::networkconfig::*;
|
||||
use zerotier_network_hypervisor::vl2::v1::Revocation;
|
||||
use zerotier_network_hypervisor::vl2::NetworkId;
|
||||
use zerotier_utils::blob::Blob;
|
||||
use zerotier_utils::buffer::OutOfBoundsError;
|
||||
|
@ -160,6 +161,9 @@ impl Controller {
|
|||
/// This is the central function of the controller that looks up members, checks their
|
||||
/// permissions, and generates a network config and other credentials (or not).
|
||||
///
|
||||
/// This may also return revocations. If it does these should be sent along with or right after
|
||||
/// the network config. This is for V1 nodes only, since V2 has another mechanism.
|
||||
///
|
||||
/// An error is only returned if a database or other unusual error occurs. Otherwise a rejection
|
||||
/// reason is returned with None or an acceptance reason with a network configuration is returned.
|
||||
async fn get_network_config(
|
||||
|
@ -167,10 +171,10 @@ impl Controller {
|
|||
source_identity: &Identity,
|
||||
network_id: NetworkId,
|
||||
now: i64,
|
||||
) -> Result<(AuthorizationResult, Option<NetworkConfig>), Box<dyn Error + Send + Sync>> {
|
||||
) -> Result<(AuthorizationResult, Option<NetworkConfig>, Option<Vec<vl2::v1::Revocation>>), Box<dyn Error + Send + Sync>> {
|
||||
let network = self.database.get_network(network_id).await?;
|
||||
if network.is_none() {
|
||||
return Ok((AuthorizationResult::Rejected, None));
|
||||
return Ok((AuthorizationResult::Rejected, None, None));
|
||||
}
|
||||
let network = network.unwrap();
|
||||
|
||||
|
@ -182,7 +186,7 @@ impl Controller {
|
|||
if let Some(member) = member.as_mut() {
|
||||
if let Some(pinned_identity) = member.identity.as_ref() {
|
||||
if !pinned_identity.eq(&source_identity) {
|
||||
return Ok((AuthorizationResult::RejectedIdentityMismatch, None));
|
||||
return Ok((AuthorizationResult::RejectedIdentityMismatch, None, None));
|
||||
} else if source_identity.is_upgraded_from(pinned_identity) {
|
||||
let _ = member.identity.replace(source_identity.clone_without_secret());
|
||||
member_changed = true;
|
||||
|
@ -206,7 +210,7 @@ impl Controller {
|
|||
let _ = member.insert(Member::new_with_identity(source_identity.clone(), network_id));
|
||||
member_changed = true;
|
||||
} else {
|
||||
return Ok((AuthorizationResult::Rejected, None));
|
||||
return Ok((AuthorizationResult::Rejected, None, None));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,7 +239,9 @@ impl Controller {
|
|||
assert!(!authorization_result.approved());
|
||||
}
|
||||
|
||||
let nc: Option<NetworkConfig> = if authorization_result.approved() {
|
||||
let mut network_config = None;
|
||||
let mut revocations = None;
|
||||
if authorization_result.approved() {
|
||||
// We should not be able to make it here if this is still false.
|
||||
assert!(member_authorized);
|
||||
|
||||
|
@ -260,13 +266,6 @@ impl Controller {
|
|||
nc.dns = network.dns;
|
||||
|
||||
if network.min_supported_version.unwrap_or(0) < (protocol::PROTOCOL_VERSION_V2 as u32) {
|
||||
// Get a list of all network members that were deauthorized but are still within the time window.
|
||||
// These will be issued revocations to remind the node not to speak to them until they fall off.
|
||||
let deauthed_members_still_in_window = self
|
||||
.database
|
||||
.list_members_deauthorized_after(network.id, now - credential_ttl)
|
||||
.await;
|
||||
|
||||
if let Some(com) =
|
||||
vl2::v1::CertificateOfMembership::new(&self.local_identity, network_id, &source_identity, now, credential_ttl)
|
||||
{
|
||||
|
@ -281,39 +280,60 @@ impl Controller {
|
|||
coo.add_ip(ip);
|
||||
}
|
||||
if !coo.sign(&self.local_identity, &source_identity) {
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None));
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None, None));
|
||||
}
|
||||
v1cred.certificates_of_ownership.push(coo);
|
||||
|
||||
for (id, value) in member.tags.iter() {
|
||||
let tag = vl2::v1::Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, now);
|
||||
if tag.is_none() {
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None));
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None, None));
|
||||
}
|
||||
let _ = v1cred.tags.insert(*id, tag.unwrap());
|
||||
}
|
||||
|
||||
nc.v1_credentials = Some(v1cred);
|
||||
|
||||
// Staple a bunch of revocations for anyone deauthed that still might be in the window.
|
||||
if let Ok(deauthed_members_still_in_window) = self
|
||||
.database
|
||||
.list_members_deauthorized_after(network.id, now - credential_ttl)
|
||||
.await
|
||||
{
|
||||
if !deauthed_members_still_in_window.is_empty() {
|
||||
let mut revs = Vec::with_capacity(deauthed_members_still_in_window.len());
|
||||
for dm in deauthed_members_still_in_window.iter() {
|
||||
if let Some(rev) = Revocation::new(
|
||||
network_id,
|
||||
now,
|
||||
*dm,
|
||||
source_identity.address,
|
||||
&self.local_identity,
|
||||
vl2::v1::CredentialType::CertificateOfMembership,
|
||||
false,
|
||||
) {
|
||||
revs.push(rev);
|
||||
}
|
||||
}
|
||||
revocations = Some(revs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None));
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None, None));
|
||||
}
|
||||
} else {
|
||||
// TODO: create V2 type credential for V2-only networks
|
||||
// TODO: populate node info for V2 networks
|
||||
}
|
||||
|
||||
// TODO: revocations!
|
||||
|
||||
Some(nc)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
network_config = Some(nc);
|
||||
}
|
||||
|
||||
if member_changed {
|
||||
self.database.save_member(member).await?;
|
||||
}
|
||||
|
||||
Ok((authorization_result, nc))
|
||||
Ok((authorization_result, network_config, revocations))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,11 +406,11 @@ impl InnerProtocol for Controller {
|
|||
let now = ms_since_epoch();
|
||||
|
||||
let (result, config) = match self2.get_network_config(&peer.identity, network_id, now).await {
|
||||
Result::Ok((result, Some(config))) => {
|
||||
Result::Ok((result, Some(config), revocations)) => {
|
||||
self2.send_network_config(peer.as_ref(), &config, Some(message_id));
|
||||
(result, Some(config))
|
||||
}
|
||||
Result::Ok((result, None)) => (result, None),
|
||||
Result::Ok((result, None, _)) => (result, None),
|
||||
Result::Err(_) => {
|
||||
// TODO: log invalid request or internal error
|
||||
return;
|
||||
|
|
|
@ -24,7 +24,7 @@ use zerotier_utils::varint;
|
|||
pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE;
|
||||
|
||||
/// Minimum wire MTU for ZSSP to function normally.
|
||||
pub const MIN_MTU: usize = 1280;
|
||||
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;
|
||||
|
@ -34,7 +34,7 @@ pub const SERVICE_INTERVAL: u64 = 10000;
|
|||
/// 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-future where a QC that can break 384-bit ECC
|
||||
/// 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.
|
||||
|
@ -42,9 +42,16 @@ pub const SERVICE_INTERVAL: u64 = 10000;
|
|||
/// faster than NIST P-384 ECDH.
|
||||
const JEDI: bool = true;
|
||||
|
||||
/// Maximum number of fragments for data packets.
|
||||
const MAX_FRAGMENTS: usize = 48; // protocol max: 63
|
||||
|
||||
/// Maximum number of fragments for key exchange packets (can be smaller to save memory, only a few needed)
|
||||
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.
|
||||
const REKEY_AFTER_USES: u64 = 536870912;
|
||||
|
||||
/// Maximum random jitter to add to rekey-after usage count.
|
||||
|
@ -52,44 +59,43 @@ const REKEY_AFTER_USES_MAX_JITTER: u32 = 1048576;
|
|||
|
||||
/// Hard expiration after this many uses.
|
||||
///
|
||||
/// Use of the key beyond this point is prohibited. This is the point where u32 wraps minus
|
||||
/// a little bit of margin. We should never get here under ordinary circumstances.
|
||||
/// 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.
|
||||
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.
|
||||
const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
/// Maximum random jitter to add to rekey-after time.
|
||||
const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10;
|
||||
const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 minutes
|
||||
|
||||
/// Version 0: NIST P-384 forward secrecy and authentication with optional Kyber1024 forward secrecy (but not authentication)
|
||||
/// Version 0: AES-256-GCM + NIST P-384 + optional Kyber1024 PQ forward secrecy
|
||||
const SESSION_PROTOCOL_VERSION: u8 = 0x00;
|
||||
|
||||
/// No additional keys included for hybrid exchange, just normal Noise_IK with P-384.
|
||||
/// Secondary key type: none, use only P-384 for forward secrecy.
|
||||
const E1_TYPE_NONE: u8 = 0;
|
||||
|
||||
/// Kyber1024 key (alice) or ciphertext (bob) included.
|
||||
/// Secondary key type: Kyber1024, PQ forward secrecy enabled.
|
||||
const E1_TYPE_KYBER1024: u8 = 1;
|
||||
|
||||
/// Maximum number of fragments for data packets.
|
||||
const MAX_FRAGMENTS: usize = 48; // protocol max: 63
|
||||
|
||||
/// Maximum number of fragments for key exchange packets (can be smaller to save memory, only a few needed)
|
||||
const KEY_EXCHANGE_MAX_FRAGMENTS: usize = 2; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc.
|
||||
|
||||
/// Size of packet header
|
||||
const HEADER_SIZE: usize = 16;
|
||||
|
||||
/// Size of AES-GCM MAC tags
|
||||
const AES_GCM_TAG_SIZE: usize = 16;
|
||||
|
||||
/// Size of HMAC-SHA384
|
||||
/// Size of HMAC-SHA384 MAC tags
|
||||
const HMAC_SIZE: usize = 48;
|
||||
|
||||
/// Size of a session ID, which is a bit like a TCP port number.
|
||||
/// 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.
|
||||
const SESSION_ID_SIZE: usize = 6;
|
||||
|
||||
/// Number of session keys to hold at a given time.
|
||||
///
|
||||
/// This provides room for a current, previous, and next key.
|
||||
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
|
||||
|
@ -98,7 +104,7 @@ const PACKET_TYPE_NOP: u8 = 1;
|
|||
const PACKET_TYPE_KEY_OFFER: u8 = 2; // "alice"
|
||||
const PACKET_TYPE_KEY_COUNTER_OFFER: u8 = 3; // "bob"
|
||||
|
||||
// Key usage labels for sub-key derivation using kbkdf (HMAC).
|
||||
// Key usage labels for sub-key derivation using NIST-style KBKDF (basically just HMAC KDF).
|
||||
const KBKDF_KEY_USAGE_LABEL_HMAC: u8 = b'M';
|
||||
const KBKDF_KEY_USAGE_LABEL_HEADER_CHECK: u8 = b'H';
|
||||
const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A';
|
||||
|
@ -109,7 +115,7 @@ const KBKDF_KEY_USAGE_LABEL_RATCHETING: u8 = b'R';
|
|||
///
|
||||
/// 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.
|
||||
/// the primary algorithm from NIST P-384 or the transport cipher from AES-GCM.
|
||||
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
|
||||
|
@ -191,6 +197,7 @@ impl std::fmt::Debug for Error {
|
|||
}
|
||||
}
|
||||
|
||||
/// Result generated by the packet receive function, with possible payloads.
|
||||
pub enum ReceiveResult<'a, H: Host> {
|
||||
/// Packet is valid, no action needs to be taken.
|
||||
Ok,
|
||||
|
@ -235,7 +242,7 @@ impl SessionId {
|
|||
|
||||
#[inline]
|
||||
pub fn new_random() -> Self {
|
||||
Self(random::xorshift64_random() % (Self::NIL.0 - 1))
|
||||
Self(random::next_u64_secure() % (Self::NIL.0 - 1))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,9 +253,22 @@ impl From<SessionId> for u64 {
|
|||
}
|
||||
}
|
||||
|
||||
/// State information to associate with receiving contexts such as sockets or remote paths/endpoints.
|
||||
///
|
||||
/// This holds the data structures used to defragment incoming packets that are not associated with an
|
||||
/// existing session, which would be new attempts to create sessions. Typically one of these is associated
|
||||
/// with a single listen socket or other inbound endpoint.
|
||||
pub struct ReceiveContext<H: Host> {
|
||||
initial_offer_defrag: Mutex<RingBufferMap<u32, GatherArray<H::IncomingPacketBuffer, KEY_EXCHANGE_MAX_FRAGMENTS>, 1024, 128>>,
|
||||
incoming_init_header_check_cipher: Aes,
|
||||
}
|
||||
|
||||
/// 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 Host: Sized {
|
||||
/// Arbitrary object that can be associated with sessions.
|
||||
/// Arbitrary opaque object associated with a session, such as a connection state object.
|
||||
type AssociatedObject;
|
||||
|
||||
/// Arbitrary object that dereferences to the session, such as Arc<Session<Self>>.
|
||||
|
@ -257,29 +277,39 @@ pub trait Host: Sized {
|
|||
/// 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 address to allow it to be passed back to functions like rate_limit_new_session().
|
||||
/// Remote physical address on whatever transport this session is using.
|
||||
type RemoteAddress;
|
||||
|
||||
/// Rate limit for attempts to rekey existing sessions.
|
||||
const REKEY_RATE_LIMIT_MS: i64;
|
||||
/// 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.
|
||||
/// 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(&self) -> &[u8];
|
||||
|
||||
/// Get SHA384(this host's static public key blob), included here so we don't have to calculate it each time.
|
||||
/// 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_hash(&self) -> &[u8; 48];
|
||||
|
||||
/// Get a reference to this hosts' static public key's NIST P-384 secret key pair
|
||||
/// 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_p384(&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_p384_static(static_public: &[u8]) -> Option<P384PublicKey>;
|
||||
|
||||
/// Look up a local session by local ID.
|
||||
/// Look up a local session by local session ID or return None if not found.
|
||||
fn session_lookup(&self, local_session_id: SessionId) -> Option<Self::SessionRef>;
|
||||
|
||||
/// Rate limit and check an attempted new session (called before accept_new_session).
|
||||
|
@ -322,15 +352,6 @@ struct SessionMutableState {
|
|||
last_remote_offer: i64,
|
||||
}
|
||||
|
||||
/// State information to associate with receiving contexts such as sockets or remote paths/endpoints.
|
||||
///
|
||||
/// This holds the data structures used to defragment incoming packets that are not associated with an
|
||||
/// existing session, which would be new attempts to create sessions.
|
||||
pub struct ReceiveContext<H: Host> {
|
||||
initial_offer_defrag: Mutex<RingBufferMap<u32, GatherArray<H::IncomingPacketBuffer, KEY_EXCHANGE_MAX_FRAGMENTS>, 1024, 128>>,
|
||||
incoming_init_header_check_cipher: Aes,
|
||||
}
|
||||
|
||||
impl<H: Host> Session<H> {
|
||||
/// Create a new session and send the first key offer message.
|
||||
///
|
||||
|
@ -408,7 +429,7 @@ impl<H: Host> Session<H> {
|
|||
mtu_buffer: &mut [u8],
|
||||
mut data: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
debug_assert!(mtu_buffer.len() >= MIN_MTU);
|
||||
debug_assert!(mtu_buffer.len() >= MIN_TRANSPORT_MTU);
|
||||
let state = self.state.read().unwrap();
|
||||
if let Some(remote_session_id) = state.remote_session_id {
|
||||
if let Some(key) = state.keys[state.key_ptr].as_ref() {
|
||||
|
@ -987,7 +1008,7 @@ impl<H: Host> ReceiveContext<H> {
|
|||
};
|
||||
|
||||
// Create reply packet.
|
||||
const REPLY_BUF_LEN: usize = MIN_MTU * KEY_EXCHANGE_MAX_FRAGMENTS;
|
||||
const REPLY_BUF_LEN: usize = MIN_TRANSPORT_MTU * KEY_EXCHANGE_MAX_FRAGMENTS;
|
||||
let mut reply_buf = [0_u8; REPLY_BUF_LEN];
|
||||
let reply_counter = session.send_counter.next();
|
||||
let mut reply_len = {
|
||||
|
@ -1296,7 +1317,7 @@ fn create_initial_offer<SendFunction: FnMut(&mut [u8])>(
|
|||
|
||||
let id: [u8; 16] = random::get_bytes_secure();
|
||||
|
||||
const PACKET_BUF_SIZE: usize = MIN_MTU * KEY_EXCHANGE_MAX_FRAGMENTS;
|
||||
const PACKET_BUF_SIZE: usize = MIN_TRANSPORT_MTU * KEY_EXCHANGE_MAX_FRAGMENTS;
|
||||
let mut packet_buf = [0_u8; PACKET_BUF_SIZE];
|
||||
let mut packet_len = {
|
||||
let mut p = &mut packet_buf[HEADER_SIZE..];
|
||||
|
@ -1390,7 +1411,7 @@ fn create_packet_header(
|
|||
let fragment_count = ((packet_len as f32) / (mtu - HEADER_SIZE) as f32).ceil() as usize;
|
||||
|
||||
debug_assert!(header.len() >= HEADER_SIZE);
|
||||
debug_assert!(mtu >= MIN_MTU);
|
||||
debug_assert!(mtu >= MIN_TRANSPORT_MTU);
|
||||
debug_assert!(packet_len >= MIN_PACKET_SIZE);
|
||||
debug_assert!(fragment_count > 0);
|
||||
debug_assert!(packet_type <= 0x0f); // packet type is 4 bits
|
||||
|
|
|
@ -16,66 +16,79 @@ use zerotier_utils::dictionary::Dictionary;
|
|||
use zerotier_utils::error::InvalidParameterError;
|
||||
use zerotier_utils::marshalable::Marshalable;
|
||||
|
||||
/// Credentials that must be sent to V1 nodes to allow access.
|
||||
///
|
||||
/// These are also handed out to V2 nodes to use when communicating with V1 nodes on
|
||||
/// networks that support older protocol versions.
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct V1Credentials {
|
||||
pub certificate_of_membership: CertificateOfMembership,
|
||||
pub certificates_of_ownership: Vec<CertificateOfOwnership>,
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
#[serde(default)]
|
||||
pub tags: HashMap<u32, Tag>,
|
||||
}
|
||||
|
||||
/// Network configuration object sent to nodes by network controllers.
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct NetworkConfig {
|
||||
/// Network ID
|
||||
pub network_id: NetworkId,
|
||||
|
||||
/// Short address of node to which this config was issued
|
||||
pub issued_to: Address,
|
||||
|
||||
/// Human-readable network name
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
|
||||
/// A human-readable message for members of this network (V2 only)
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
#[serde(default)]
|
||||
pub motd: String,
|
||||
|
||||
/// True if network has access control (the default)
|
||||
pub private: bool,
|
||||
|
||||
/// Network configuration timestamp
|
||||
pub timestamp: i64,
|
||||
|
||||
/// TTL for credentials on this network (or window size for V1 nodes)
|
||||
pub credential_ttl: i64,
|
||||
|
||||
/// Network configuration revision number
|
||||
pub revision: u64,
|
||||
|
||||
/// L2 Ethernet MTU for this network.
|
||||
pub mtu: u16,
|
||||
|
||||
/// Suggested horizon limit for multicast (not a hard limit, but 0 disables multicast)
|
||||
pub multicast_limit: u32,
|
||||
|
||||
/// ZeroTier-assigned L3 routes for this node.
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
#[serde(default)]
|
||||
pub routes: HashSet<IpRoute>,
|
||||
|
||||
/// ZeroTier-assigned static IP addresses for this node.
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
#[serde(default)]
|
||||
pub static_ips: HashSet<InetAddress>,
|
||||
|
||||
/// Network flow rules (low level).
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
#[serde(default)]
|
||||
pub rules: Vec<Rule>,
|
||||
|
||||
/// DNS resolvers available to be auto-configured on the host.
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
#[serde(default)]
|
||||
pub dns: HashMap<String, HashSet<InetAddress>>,
|
||||
|
||||
/// V1 certificate of membership and other exchange-able credentials, may be absent on V2-only networks.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub v1_credentials: Option<V1Credentials>,
|
||||
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
#[serde(default)]
|
||||
pub banned: HashSet<Address>, // v2 only
|
||||
/// Information about specific nodes such as names, services, etc. (V2 only)
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
#[serde(default)]
|
||||
pub node_info: HashMap<Address, NodeInfo>, // v2 only
|
||||
pub node_info: HashMap<Address, NodeInfo>,
|
||||
|
||||
/// URL to ZeroTier Central instance that is controlling the controller that issued this (if any).
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
#[serde(default)]
|
||||
pub central_url: String,
|
||||
|
||||
/// SSO / third party auth information (if enabled).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub sso: Option<SSOAuthConfiguration>,
|
||||
|
@ -99,7 +112,6 @@ impl NetworkConfig {
|
|||
rules: Vec::new(),
|
||||
dns: HashMap::new(),
|
||||
v1_credentials: None,
|
||||
banned: HashSet::new(),
|
||||
node_info: HashMap::new(),
|
||||
central_url: String::new(),
|
||||
sso: None,
|
||||
|
@ -179,7 +191,7 @@ impl NetworkConfig {
|
|||
if let Some(v1cred) = self.v1_credentials.as_ref() {
|
||||
d.set_bytes(
|
||||
proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP,
|
||||
v1cred.certificate_of_membership.to_bytes()?,
|
||||
v1cred.certificate_of_membership.to_bytes()?.as_bytes().to_vec(),
|
||||
);
|
||||
|
||||
if !v1cred.certificates_of_ownership.is_empty() {
|
||||
|
@ -191,11 +203,11 @@ impl NetworkConfig {
|
|||
}
|
||||
|
||||
if !v1cred.tags.is_empty() {
|
||||
let mut certs = Vec::with_capacity(v1cred.tags.len() * 256);
|
||||
let mut tags = Vec::with_capacity(v1cred.tags.len() * 256);
|
||||
for (_, t) in v1cred.tags.iter() {
|
||||
let _ = certs.write_all(t.to_bytes(controller_identity.address)?.as_slice());
|
||||
let _ = tags.write_all(t.to_bytes(controller_identity.address).as_ref());
|
||||
}
|
||||
d.set_bytes(proto_v1_field_name::network_config::TAGS, certs);
|
||||
d.set_bytes(proto_v1_field_name::network_config::TAGS, tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -420,6 +432,19 @@ pub struct SSOAuthConfiguration {
|
|||
pub client_id: String,
|
||||
}
|
||||
|
||||
/// Credentials that must be sent to V1 nodes to allow access.
|
||||
///
|
||||
/// These are also handed out to V2 nodes to use when communicating with V1 nodes on
|
||||
/// networks that support older protocol versions.
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct V1Credentials {
|
||||
pub certificate_of_membership: CertificateOfMembership,
|
||||
pub certificates_of_ownership: Vec<CertificateOfOwnership>,
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
#[serde(default)]
|
||||
pub tags: HashMap<u32, Tag>,
|
||||
}
|
||||
|
||||
/// Information about nodes on the network that can be included in a network config.
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct NodeInfo {
|
||||
|
|
|
@ -91,9 +91,9 @@ impl CertificateOfMembership {
|
|||
}
|
||||
|
||||
/// Get this certificate of membership in byte encoded format.
|
||||
pub fn to_bytes(&self) -> Option<Vec<u8>> {
|
||||
pub fn to_bytes(&self) -> Option<ArrayVec<u8, 384>> {
|
||||
if self.signature.len() == 96 {
|
||||
let mut v: Vec<u8> = Vec::with_capacity(3 + 168 + 5 + 96);
|
||||
let mut v = ArrayVec::new();
|
||||
v.push(1); // version byte from v1 protocol
|
||||
v.push(0);
|
||||
v.push(7); // 7 qualifiers, big-endian 16-bit
|
||||
|
|
|
@ -62,7 +62,7 @@ impl CertificateOfOwnership {
|
|||
let _ = self.things.insert(Thing::Mac(mac));
|
||||
}
|
||||
|
||||
fn internal_v1_proto_to_bytes(&self, for_sign: bool, signed_by: Address) -> Option<Vec<u8>> {
|
||||
fn internal_to_bytes(&self, for_sign: bool, signed_by: Address) -> Option<Vec<u8>> {
|
||||
if self.things.len() > 0xffff || self.signature.len() != 96 {
|
||||
return None;
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ impl CertificateOfOwnership {
|
|||
|
||||
#[inline(always)]
|
||||
pub fn to_bytes(&self, signed_by: Address) -> Option<Vec<u8>> {
|
||||
self.internal_v1_proto_to_bytes(false, signed_by)
|
||||
self.internal_to_bytes(false, signed_by)
|
||||
}
|
||||
|
||||
/// Decode a V1 legacy format certificate of ownership in byte format.
|
||||
|
@ -169,7 +169,7 @@ impl CertificateOfOwnership {
|
|||
/// Sign certificate of ownership for use by V1 nodes.
|
||||
pub fn sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool {
|
||||
self.issued_to = issued_to.address;
|
||||
if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) {
|
||||
if let Some(to_sign) = self.internal_to_bytes(true, issuer.address) {
|
||||
if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) {
|
||||
self.signature = signature;
|
||||
return true;
|
||||
|
|
|
@ -3,6 +3,16 @@ mod certificateofownership;
|
|||
mod revocation;
|
||||
mod tag;
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum CredentialType {
|
||||
Null = 0u8,
|
||||
CertificateOfMembership = 1,
|
||||
Capability = 2,
|
||||
Tag = 3,
|
||||
CertificateOfOwnership = 4,
|
||||
Revocation = 5,
|
||||
}
|
||||
|
||||
pub use certificateofmembership::CertificateOfMembership;
|
||||
pub use certificateofownership::{CertificateOfOwnership, Thing};
|
||||
pub use revocation::Revocation;
|
||||
|
|
|
@ -1,15 +1,79 @@
|
|||
use std::io::Write;
|
||||
|
||||
use zerotier_crypto::random;
|
||||
use zerotier_utils::arrayvec::ArrayVec;
|
||||
use zerotier_utils::blob::Blob;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::vl1::{Address, Identity};
|
||||
use crate::vl2::v1::CredentialType;
|
||||
use crate::vl2::NetworkId;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Revocation {
|
||||
pub id: u32,
|
||||
pub network_id: NetworkId,
|
||||
pub threshold: i64,
|
||||
pub target: Address,
|
||||
pub issued_to: Address,
|
||||
pub signature: ArrayVec<u8, { Identity::MAX_SIGNATURE_SIZE }>,
|
||||
pub version: u8,
|
||||
pub signature: Blob<96>,
|
||||
pub type_being_revoked: u8,
|
||||
pub fast_propagate: bool,
|
||||
}
|
||||
|
||||
impl Revocation {
|
||||
pub fn new(
|
||||
network_id: NetworkId,
|
||||
threshold: i64,
|
||||
target: Address,
|
||||
issued_to: Address,
|
||||
signer: &Identity,
|
||||
type_being_revoked: CredentialType,
|
||||
fast_propagate: bool,
|
||||
) -> Option<Self> {
|
||||
let mut r = Self {
|
||||
id: random::xorshift64_random() as u32, // arbitrary
|
||||
network_id,
|
||||
threshold,
|
||||
target,
|
||||
issued_to,
|
||||
signature: Blob::default(),
|
||||
type_being_revoked: type_being_revoked as u8,
|
||||
fast_propagate,
|
||||
};
|
||||
if let Some(sig) = signer.sign(r.internal_to_bytes(true, signer.address).as_bytes(), true) {
|
||||
r.signature.as_mut().copy_from_slice(sig.as_bytes());
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_to_bytes(&self, for_sign: bool, signed_by: Address) -> ArrayVec<u8, 256> {
|
||||
let mut v = ArrayVec::new();
|
||||
if for_sign {
|
||||
let _ = v.write_all(&[0x7f; 8]);
|
||||
}
|
||||
|
||||
let _ = v.write_all(&[0; 4]);
|
||||
let _ = v.write_all(&self.id.to_be_bytes());
|
||||
let _ = v.write_all(&self.network_id.to_bytes());
|
||||
let _ = v.write_all(&[0; 8]);
|
||||
let _ = v.write_all(&self.threshold.to_be_bytes());
|
||||
let _ = v.write_all(&(self.fast_propagate as u64).to_be_bytes()); // 0x1 is the flag for this
|
||||
let _ = v.write_all(&self.target.to_bytes());
|
||||
let _ = v.write_all(&signed_by.to_bytes());
|
||||
v.push(self.type_being_revoked);
|
||||
|
||||
if for_sign {
|
||||
let _ = v.write_all(&[0x7f; 8]);
|
||||
} else {
|
||||
v.push(1); // ed25519 signature
|
||||
let _ = v.write_all(&[0u8, 96u8]);
|
||||
let _ = v.write_all(self.signature.as_bytes());
|
||||
}
|
||||
|
||||
v
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::vl2::NetworkId;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use zerotier_utils::arrayvec::ArrayVec;
|
||||
use zerotier_utils::blob::Blob;
|
||||
use zerotier_utils::error::InvalidParameterError;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
@ -16,7 +17,7 @@ pub struct Tag {
|
|||
pub issued_to: Address,
|
||||
pub id: u32,
|
||||
pub value: u32,
|
||||
pub signature: ArrayVec<u8, { Identity::MAX_SIGNATURE_SIZE }>,
|
||||
pub signature: Blob<96>,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
|
@ -27,58 +28,44 @@ impl Tag {
|
|||
issued_to: issued_to.address,
|
||||
id,
|
||||
value,
|
||||
signature: ArrayVec::new(),
|
||||
signature: Blob::default(),
|
||||
};
|
||||
let to_sign = tag.internal_v1_proto_to_bytes(true, issuer.address)?;
|
||||
if let Some(signature) = issuer.sign(to_sign.as_slice(), true) {
|
||||
tag.signature = signature;
|
||||
let to_sign = tag.internal_to_bytes(true, issuer.address);
|
||||
if let Some(signature) = issuer.sign(to_sign.as_ref(), true) {
|
||||
tag.signature.as_mut().copy_from_slice(signature.as_bytes());
|
||||
return Some(tag);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn internal_v1_proto_to_bytes(&self, for_sign: bool, signed_by: Address) -> Option<Vec<u8>> {
|
||||
if self.signature.len() == 96 {
|
||||
let mut v = Vec::with_capacity(256);
|
||||
if for_sign {
|
||||
let _ = v.write_all(&[0x7f; 8]);
|
||||
}
|
||||
let _ = v.write_all(&self.network_id.to_bytes());
|
||||
let _ = v.write_all(&self.timestamp.to_be_bytes());
|
||||
let _ = v.write_all(&self.id.to_be_bytes());
|
||||
let _ = v.write_all(&self.value.to_be_bytes());
|
||||
let _ = v.write_all(&self.issued_to.to_bytes());
|
||||
let _ = v.write_all(&signed_by.to_bytes());
|
||||
if !for_sign {
|
||||
v.push(1);
|
||||
v.push(0);
|
||||
v.push(96); // size of legacy signatures, 16-bit
|
||||
let _ = v.write_all(self.signature.as_bytes());
|
||||
}
|
||||
v.push(0);
|
||||
v.push(0);
|
||||
if for_sign {
|
||||
let _ = v.write_all(&[0x7f; 8]);
|
||||
}
|
||||
return Some(v);
|
||||
fn internal_to_bytes(&self, for_sign: bool, signed_by: Address) -> ArrayVec<u8, 256> {
|
||||
let mut v = ArrayVec::new();
|
||||
if for_sign {
|
||||
let _ = v.write_all(&[0x7f; 8]);
|
||||
}
|
||||
return None;
|
||||
let _ = v.write_all(&self.network_id.to_bytes());
|
||||
let _ = v.write_all(&self.timestamp.to_be_bytes());
|
||||
let _ = v.write_all(&self.id.to_be_bytes());
|
||||
let _ = v.write_all(&self.value.to_be_bytes());
|
||||
let _ = v.write_all(&self.issued_to.to_bytes());
|
||||
let _ = v.write_all(&signed_by.to_bytes());
|
||||
if !for_sign {
|
||||
v.push(1);
|
||||
v.push(0);
|
||||
v.push(96); // size of legacy signatures, 16-bit
|
||||
let _ = v.write_all(self.signature.as_bytes());
|
||||
}
|
||||
v.push(0);
|
||||
v.push(0);
|
||||
if for_sign {
|
||||
let _ = v.write_all(&[0x7f; 8]);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_bytes(&self, signed_by: Address) -> Option<Vec<u8>> {
|
||||
self.internal_v1_proto_to_bytes(false, signed_by)
|
||||
}
|
||||
|
||||
pub fn sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool {
|
||||
self.issued_to = issued_to.address;
|
||||
if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) {
|
||||
if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) {
|
||||
self.signature = signature;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
pub fn to_bytes(&self, signed_by: Address) -> ArrayVec<u8, 256> {
|
||||
self.internal_to_bytes(false, signed_by)
|
||||
}
|
||||
|
||||
pub fn from_bytes(b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> {
|
||||
|
@ -94,8 +81,8 @@ impl Tag {
|
|||
id: u32::from_be_bytes(b[16..20].try_into().unwrap()),
|
||||
value: u32::from_be_bytes(b[20..24].try_into().unwrap()),
|
||||
signature: {
|
||||
let mut s = ArrayVec::new();
|
||||
s.push_slice(&b[37..133]);
|
||||
let mut s = Blob::default();
|
||||
s.as_mut().copy_from_slice(&b[37..133]);
|
||||
s
|
||||
},
|
||||
},
|
||||
|
|
|
@ -17,7 +17,6 @@ impl<T> std::fmt::Display for OutOfCapacityError<T> {
|
|||
}
|
||||
|
||||
impl<T: std::fmt::Debug> ::std::error::Error for OutOfCapacityError<T> {
|
||||
#[inline(always)]
|
||||
fn description(self: &Self) -> &str {
|
||||
"ArrayVec out of space"
|
||||
}
|
||||
|
@ -47,6 +46,7 @@ impl<T: PartialEq, const C: usize> PartialEq for ArrayVec<T, C> {
|
|||
impl<T: Eq, const C: usize> Eq for ArrayVec<T, C> {}
|
||||
|
||||
impl<T: Clone, const C: usize> Clone for ArrayVec<T, C> {
|
||||
#[inline]
|
||||
fn clone(&self) -> Self {
|
||||
debug_assert!(self.s <= C);
|
||||
Self {
|
||||
|
@ -63,7 +63,7 @@ impl<T: Clone, const C: usize> Clone for ArrayVec<T, C> {
|
|||
}
|
||||
|
||||
impl<T: Clone, const C: usize, const S: usize> From<[T; S]> for ArrayVec<T, C> {
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
fn from(v: [T; S]) -> Self {
|
||||
if S <= C {
|
||||
let mut tmp = Self::new();
|
||||
|
@ -78,7 +78,7 @@ impl<T: Clone, const C: usize, const S: usize> From<[T; S]> for ArrayVec<T, C> {
|
|||
}
|
||||
|
||||
impl<const C: usize> Write for ArrayVec<u8, C> {
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
for i in buf.iter() {
|
||||
if self.try_push(*i).is_err() {
|
||||
|
@ -140,7 +140,7 @@ impl<T, const C: usize> ArrayVec<T, C> {
|
|||
Self { s: 0, a: unsafe { MaybeUninit::uninit().assume_init() } }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn push(&mut self, v: T) {
|
||||
let i = self.s;
|
||||
if i < C {
|
||||
|
@ -151,7 +151,7 @@ impl<T, const C: usize> ArrayVec<T, C> {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn try_push(&mut self, v: T) -> Result<(), OutOfCapacityError<T>> {
|
||||
if self.s < C {
|
||||
let i = self.s;
|
||||
|
@ -178,7 +178,7 @@ impl<T, const C: usize> ArrayVec<T, C> {
|
|||
self.s
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
if self.s > 0 {
|
||||
let i = self.s - 1;
|
||||
|
@ -235,6 +235,7 @@ impl<T, const C: usize> AsMut<[T]> for ArrayVec<T, C> {
|
|||
}
|
||||
|
||||
impl<T: Serialize, const L: usize> Serialize for ArrayVec<T, L> {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
|
@ -253,10 +254,12 @@ struct ArrayVecVisitor<'de, T: Deserialize<'de>, const L: usize>(std::marker::Ph
|
|||
impl<'de, T: Deserialize<'de>, const L: usize> serde::de::Visitor<'de> for ArrayVecVisitor<'de, T, L> {
|
||||
type Value = ArrayVec<T, L>;
|
||||
|
||||
#[inline]
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str(format!("array of up to {} elements", L).as_str())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
|
@ -270,6 +273,7 @@ impl<'de, T: Deserialize<'de>, const L: usize> serde::de::Visitor<'de> for Array
|
|||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de> + 'de, const L: usize> Deserialize<'de> for ArrayVec<T, L> {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<ArrayVec<T, L>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
|
|
Loading…
Add table
Reference in a new issue