From 9e6617b324d48b84fb1ecc07d7e591b8f508cdbb Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Thu, 27 Oct 2022 15:03:23 -0400 Subject: [PATCH] More controller work and some ZSSP cleanup. --- controller/src/controller.rs | 68 +++++++----- crypto/src/zssp.rs | 101 +++++++++++------- network-hypervisor/src/vl2/networkconfig.rs | 69 ++++++++---- .../src/vl2/v1/certificateofmembership.rs | 4 +- .../src/vl2/v1/certificateofownership.rs | 6 +- network-hypervisor/src/vl2/v1/mod.rs | 10 ++ network-hypervisor/src/vl2/v1/revocation.rs | 68 +++++++++++- network-hypervisor/src/vl2/v1/tag.rs | 77 ++++++------- utils/src/arrayvec.rs | 16 +-- 9 files changed, 275 insertions(+), 144 deletions(-) diff --git a/controller/src/controller.rs b/controller/src/controller.rs index e2feba9ce..dccb557fe 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -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), Box> { + ) -> Result<(AuthorizationResult, Option, Option>), Box> { 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 = 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; diff --git a/crypto/src/zssp.rs b/crypto/src/zssp.rs index 2b6b8f7af..c3918df5d 100644 --- a/crypto/src/zssp.rs +++ b/crypto/src/zssp.rs @@ -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 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 { + initial_offer_defrag: Mutex, 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>. @@ -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 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; - /// 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; /// 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 { - initial_offer_defrag: Mutex, 1024, 128>>, - incoming_init_header_check_cipher: Aes, -} - impl Session { /// Create a new session and send the first key offer message. /// @@ -408,7 +429,7 @@ impl Session { 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 ReceiveContext { }; // 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( 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 diff --git a/network-hypervisor/src/vl2/networkconfig.rs b/network-hypervisor/src/vl2/networkconfig.rs index f8b484111..75d1107db 100644 --- a/network-hypervisor/src/vl2/networkconfig.rs +++ b/network-hypervisor/src/vl2/networkconfig.rs @@ -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, - #[serde(skip_serializing_if = "HashMap::is_empty")] - #[serde(default)] - pub tags: HashMap, -} - /// 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, + + /// ZeroTier-assigned static IP addresses for this node. #[serde(skip_serializing_if = "HashSet::is_empty")] #[serde(default)] pub static_ips: HashSet, + + /// Network flow rules (low level). #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub rules: Vec, + + /// DNS resolvers available to be auto-configured on the host. #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(default)] pub dns: HashMap>, + /// 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, - #[serde(skip_serializing_if = "HashSet::is_empty")] - #[serde(default)] - pub banned: HashSet
, // 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, // v2 only + pub node_info: HashMap, + /// 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, @@ -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, + #[serde(skip_serializing_if = "HashMap::is_empty")] + #[serde(default)] + pub tags: HashMap, +} + /// Information about nodes on the network that can be included in a network config. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct NodeInfo { diff --git a/network-hypervisor/src/vl2/v1/certificateofmembership.rs b/network-hypervisor/src/vl2/v1/certificateofmembership.rs index a99ee2eff..bb12cf545 100644 --- a/network-hypervisor/src/vl2/v1/certificateofmembership.rs +++ b/network-hypervisor/src/vl2/v1/certificateofmembership.rs @@ -91,9 +91,9 @@ impl CertificateOfMembership { } /// Get this certificate of membership in byte encoded format. - pub fn to_bytes(&self) -> Option> { + pub fn to_bytes(&self) -> Option> { if self.signature.len() == 96 { - let mut v: Vec = 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 diff --git a/network-hypervisor/src/vl2/v1/certificateofownership.rs b/network-hypervisor/src/vl2/v1/certificateofownership.rs index 9334d8376..bbdc6e61f 100644 --- a/network-hypervisor/src/vl2/v1/certificateofownership.rs +++ b/network-hypervisor/src/vl2/v1/certificateofownership.rs @@ -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> { + fn internal_to_bytes(&self, for_sign: bool, signed_by: Address) -> Option> { 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> { - 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; diff --git a/network-hypervisor/src/vl2/v1/mod.rs b/network-hypervisor/src/vl2/v1/mod.rs index 3576f0844..9813a08e8 100644 --- a/network-hypervisor/src/vl2/v1/mod.rs +++ b/network-hypervisor/src/vl2/v1/mod.rs @@ -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; diff --git a/network-hypervisor/src/vl2/v1/revocation.rs b/network-hypervisor/src/vl2/v1/revocation.rs index 14ecd71fb..be7f4f33e 100644 --- a/network-hypervisor/src/vl2/v1/revocation.rs +++ b/network-hypervisor/src/vl2/v1/revocation.rs @@ -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, - 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 { + 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 { + 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 + } } diff --git a/network-hypervisor/src/vl2/v1/tag.rs b/network-hypervisor/src/vl2/v1/tag.rs index b056f9436..4d0082ab3 100644 --- a/network-hypervisor/src/vl2/v1/tag.rs +++ b/network-hypervisor/src/vl2/v1/tag.rs @@ -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, + 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> { - 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 { + 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> { - 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 { + 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 }, }, diff --git a/utils/src/arrayvec.rs b/utils/src/arrayvec.rs index 30daa3da0..a76535bf5 100644 --- a/utils/src/arrayvec.rs +++ b/utils/src/arrayvec.rs @@ -17,7 +17,6 @@ impl std::fmt::Display for OutOfCapacityError { } impl ::std::error::Error for OutOfCapacityError { - #[inline(always)] fn description(self: &Self) -> &str { "ArrayVec out of space" } @@ -47,6 +46,7 @@ impl PartialEq for ArrayVec { impl Eq for ArrayVec {} impl Clone for ArrayVec { + #[inline] fn clone(&self) -> Self { debug_assert!(self.s <= C); Self { @@ -63,7 +63,7 @@ impl Clone for ArrayVec { } impl From<[T; S]> for ArrayVec { - #[inline(always)] + #[inline] fn from(v: [T; S]) -> Self { if S <= C { let mut tmp = Self::new(); @@ -78,7 +78,7 @@ impl From<[T; S]> for ArrayVec { } impl Write for ArrayVec { - #[inline(always)] + #[inline] fn write(&mut self, buf: &[u8]) -> std::io::Result { for i in buf.iter() { if self.try_push(*i).is_err() { @@ -140,7 +140,7 @@ impl ArrayVec { 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 ArrayVec { } } - #[inline(always)] + #[inline] pub fn try_push(&mut self, v: T) -> Result<(), OutOfCapacityError> { if self.s < C { let i = self.s; @@ -178,7 +178,7 @@ impl ArrayVec { self.s } - #[inline(always)] + #[inline] pub fn pop(&mut self) -> Option { if self.s > 0 { let i = self.s - 1; @@ -235,6 +235,7 @@ impl AsMut<[T]> for ArrayVec { } impl Serialize for ArrayVec { + #[inline] fn serialize(&self, serializer: S) -> Result 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; + #[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(self, mut seq: A) -> Result 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 { + #[inline] fn deserialize(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>,