More controller work and some ZSSP cleanup.

This commit is contained in:
Adam Ierymenko 2022-10-27 15:03:23 -04:00
parent 8a50427833
commit 9e6617b324
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
9 changed files with 275 additions and 144 deletions

View file

@ -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;

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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
}
}

View file

@ -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,19 +28,18 @@ 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);
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]);
}
@ -60,25 +60,12 @@ impl Tag {
if for_sign {
let _ = v.write_all(&[0x7f; 8]);
}
return Some(v);
}
return None;
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
},
},

View file

@ -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>,