diff --git a/zerotier-network-hypervisor/src/vl1/careof.rs b/zerotier-network-hypervisor/src/vl1/careof.rs new file mode 100644 index 000000000..ae6857c31 --- /dev/null +++ b/zerotier-network-hypervisor/src/vl1/careof.rs @@ -0,0 +1,71 @@ +// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. + +use std::io::Write; + +use crate::vl1::identity::Identity; +use crate::vl1::protocol::IDENTITY_FINGERPRINT_SIZE; + +use zerotier_core_crypto::varint; + +/// A signed bundle of identity fingerprints of nodes through which a node might be reached (e.g. roots). +/// +/// This can be sent by nodes to indicate which other nodes they wish to have used to reach them. Typically +/// these would be roots. It prevents a misbehaving or malicious root from pretending to host a node. +#[derive(Clone, PartialEq, Eq)] +pub struct CareOf { + pub timestamp: i64, + pub fingerprints: Vec<[u8; IDENTITY_FINGERPRINT_SIZE]>, + pub signature: Vec, +} + +impl CareOf { + pub fn new(timestamp: i64) -> Self { + Self { timestamp, fingerprints: Vec::new(), signature: Vec::new() } + } + + pub fn add_care_of(&mut self, id: &Identity) { + self.fingerprints.push(id.fingerprint); + } + + fn to_bytes_internal(&self, include_signature: bool) -> Vec { + let mut v: Vec = Vec::with_capacity(128 + (self.fingerprints.len() * 48)); + let _ = varint::write(&mut v, self.timestamp as u64); + let _ = varint::write(&mut v, self.fingerprints.len() as u64); + for f in self.fingerprints.iter() { + let _ = v.write_all(f); + } + let _ = varint::write(&mut v, 0); // reserved for future use + if include_signature { + let _ = varint::write(&mut v, self.signature.len() as u64); + let _ = v.write_all(self.signature.as_slice()); + } + v + } + + #[inline(always)] + pub fn to_bytes(&self) -> Vec { + self.to_bytes_internal(true) + } + + /// Sort, deduplicate, and sign this care-of packet. + /// + /// The supplied identitiy must contain its secret keys. False is returned if there is an error. + pub fn sign(&mut self, signer: &Identity) -> bool { + self.fingerprints.sort_unstable(); + self.fingerprints.dedup(); + if let Some(sig) = signer.sign(self.to_bytes_internal(false).as_slice(), false) { + self.signature = sig; + true + } else { + false + } + } + + pub fn verify(&self, signer: &Identity) -> bool { + signer.verify(self.to_bytes_internal(false).as_slice(), self.signature.as_slice()) + } + + pub fn contains(&self, id: &Identity) -> bool { + self.fingerprints.binary_search(&id.fingerprint).is_ok() + } +} diff --git a/zerotier-network-hypervisor/src/vl1/identity.rs b/zerotier-network-hypervisor/src/vl1/identity.rs index 1321c7376..8ea2c27ce 100644 --- a/zerotier-network-hypervisor/src/vl1/identity.rs +++ b/zerotier-network-hypervisor/src/vl1/identity.rs @@ -261,18 +261,15 @@ impl Identity { /// /// If both sides have NIST P-384 keys then key agreement is performed using both Curve25519 and /// NIST P-384 and the result is HMAC(Curve25519 secret, NIST P-384 secret). - /// - /// Nothing actually uses a 512-bit secret directly, but if the base secret is 512 bits then - /// no entropy is lost when deriving smaller secrets with a KDF. - pub fn agree(&self, other: &Identity) -> Option> { + pub fn agree(&self, other: &Identity) -> Option> { if let Some(secret) = self.secret.as_ref() { - let c25519_secret = Secret(SHA512::hash(&secret.c25519.agree(&other.c25519).0)); + let c25519_secret: Secret<48> = Secret((&SHA512::hash(&secret.c25519.agree(&other.c25519).0)[..48]).try_into().unwrap()); // FIPS note: FIPS-compliant exchange algorithms must be the last algorithms in any HKDF chain // for the final result to be technically FIPS compliant. Non-FIPS algorithm secrets are considered // a salt in the HMAC(salt, key) HKDF construction. if secret.p384.is_some() && other.p384.is_some() { - secret.p384.as_ref().unwrap().ecdh.agree(&other.p384.as_ref().unwrap().ecdh).map(|p384_secret| Secret(hmac_sha512(&c25519_secret.0, &p384_secret.0))) + secret.p384.as_ref().unwrap().ecdh.agree(&other.p384.as_ref().unwrap().ecdh).map(|p384_secret| Secret(hmac_sha384(&c25519_secret.0, &p384_secret.0))) } else { Some(c25519_secret) } diff --git a/zerotier-network-hypervisor/src/vl1/mod.rs b/zerotier-network-hypervisor/src/vl1/mod.rs index 80c9409e0..f17a10e5c 100644 --- a/zerotier-network-hypervisor/src/vl1/mod.rs +++ b/zerotier-network-hypervisor/src/vl1/mod.rs @@ -1,6 +1,7 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. mod address; +mod careof; mod dictionary; mod endpoint; mod fragmentedpacket; diff --git a/zerotier-network-hypervisor/src/vl1/node.rs b/zerotier-network-hypervisor/src/vl1/node.rs index 7b3dc6dbc..de8baab8f 100644 --- a/zerotier-network-hypervisor/src/vl1/node.rs +++ b/zerotier-network-hypervisor/src/vl1/node.rs @@ -12,6 +12,7 @@ use parking_lot::{Mutex, RwLock}; use crate::error::InvalidParameterError; use crate::util::debug_event; use crate::util::gate::IntervalGate; +use crate::vl1::careof::CareOf; use crate::vl1::path::{Path, PathServiceResult}; use crate::vl1::peer::Peer; use crate::vl1::protocol::*; @@ -133,8 +134,9 @@ struct BackgroundTaskIntervals { } struct RootInfo { - roots: HashMap>, Vec>, sets: HashMap, + roots: HashMap>, Vec>, + care_of: Vec, sets_modified: bool, online: bool, } @@ -255,8 +257,9 @@ impl Node { paths: parking_lot::RwLock::new(HashMap::new()), peers: parking_lot::RwLock::new(HashMap::new()), roots: RwLock::new(RootInfo { - roots: HashMap::new(), sets: HashMap::new(), + roots: HashMap::new(), + care_of: Vec::new(), sets_modified: false, online: false, }), @@ -415,7 +418,19 @@ impl Node { old_root_identities.sort_unstable(); new_root_identities.sort_unstable(); if !old_root_identities.eq(&new_root_identities) { - self.roots.write().roots = new_roots; + let mut care_of = CareOf::new(si.time_clock()); + for id in new_root_identities.iter() { + care_of.add_care_of(id); + } + assert!(care_of.sign(&self.identity)); + let care_of = care_of.to_bytes(); + + { + let mut roots = self.roots.write(); + roots.roots = new_roots; + roots.care_of = care_of; + } + si.event(Event::UpdatedRoots(old_root_identities, new_root_identities)); } } @@ -621,7 +636,11 @@ impl Node { self.roots.read().sets.values().cloned().collect() } - pub fn canonical_path(&self, ep: &Endpoint, local_socket: &SI::LocalSocket, local_interface: &SI::LocalInterface, time_ticks: i64) -> Arc> { + pub(crate) fn care_of_bytes(&self) -> Vec { + self.roots.read().care_of.clone() + } + + pub(crate) fn canonical_path(&self, ep: &Endpoint, local_socket: &SI::LocalSocket, local_interface: &SI::LocalInterface, time_ticks: i64) -> Arc> { if let Some(path) = self.paths.read().get(&PathKey::Ref(ep, local_socket)) { return path.clone(); } diff --git a/zerotier-network-hypervisor/src/vl1/peer.rs b/zerotier-network-hypervisor/src/vl1/peer.rs index 9d5400cc2..6bf8e5d0d 100644 --- a/zerotier-network-hypervisor/src/vl1/peer.rs +++ b/zerotier-network-hypervisor/src/vl1/peer.rs @@ -55,6 +55,7 @@ pub struct Peer { pub(crate) last_hello_reply_time_ticks: AtomicI64, last_forward_time_ticks: AtomicI64, create_time_ticks: i64, + random_ticks_offset: u64, // Counter for assigning sequential message IDs. message_id_counter: AtomicU64, @@ -165,6 +166,7 @@ impl Peer { last_forward_time_ticks: AtomicI64::new(crate::util::NEVER_HAPPENED_TICKS), last_hello_reply_time_ticks: AtomicI64::new(crate::util::NEVER_HAPPENED_TICKS), create_time_ticks: time_ticks, + random_ticks_offset: next_u64_secure(), message_id_counter: AtomicU64::new(((time_clock as u64) / 100).wrapping_shl(28) ^ next_u64_secure().wrapping_shr(36)), remote_version: AtomicU64::new(0), remote_protocol_version: AtomicU8::new(0), @@ -172,6 +174,27 @@ impl Peer { }) } + /// Get the remote version of this peer: major, minor, revision, and build. + /// Returns None if it's not yet known. + pub fn version(&self) -> Option<[u16; 4]> { + let rv = self.remote_version.load(Ordering::Relaxed); + if rv != 0 { + Some([(rv >> 48) as u16, (rv >> 32) as u16, (rv >> 16) as u16, rv as u16]) + } else { + None + } + } + + /// Get the remote protocol version of this peer or None if not yet known. + pub fn protocol_version(&self) -> Option { + let pv = self.remote_protocol_version.load(Ordering::Relaxed); + if pv != 0 { + Some(pv) + } else { + None + } + } + /// Get the next message ID for sending a message to this peer. #[inline(always)] pub(crate) fn next_message_id(&self) -> MessageId { @@ -201,32 +224,50 @@ impl Peer { return None; } - /// Get the remote version of this peer: major, minor, revision, and build. - /// Returns None if it's not yet known. - pub fn version(&self) -> Option<[u16; 4]> { - let rv = self.remote_version.load(Ordering::Relaxed); - if rv != 0 { - Some([(rv >> 48) as u16, (rv >> 32) as u16, (rv >> 16) as u16, rv as u16]) - } else { - None - } - } - - /// Get the remote protocol version of this peer or None if not yet known. - pub fn protocol_version(&self) -> Option { - let pv = self.remote_protocol_version.load(Ordering::Relaxed); - if pv != 0 { - Some(pv) - } else { - None - } - } - /// Sort a list of paths by quality or priority, with best paths first. fn prioritize_paths(paths: &mut Vec>) { paths.sort_unstable_by(|a, b| a.last_receive_time_ticks.cmp(&b.last_receive_time_ticks).reverse()); } + pub(crate) fn learn_path(&self, si: &SI, new_path: &Arc>, time_ticks: i64) { + let mut paths = self.paths.lock(); + + // If this is an IpUdp endpoint, scan the existing paths and replace any that come from + // the same IP address but a different port. This prevents the accumulation of duplicate + // paths to the same peer over different ports. + match &new_path.endpoint { + Endpoint::IpUdp(new_ip) => { + for pi in paths.iter_mut() { + if let Some(p) = pi.path.upgrade() { + match &p.endpoint { + Endpoint::IpUdp(existing_ip) => { + if existing_ip.ip_bytes().eq(new_ip.ip_bytes()) { + debug_event!(si, "[vl1] {} replacing path {} with {} (same IP, different port)", self.identity.address.to_string(), p.endpoint.to_string(), new_path.endpoint.to_string()); + pi.path = Arc::downgrade(new_path); + pi.canonical_instance_id = new_path.canonical.canonical_instance_id(); + pi.last_receive_time_ticks = time_ticks; + Self::prioritize_paths(&mut paths); + return; + } + } + _ => {} + } + } + } + } + _ => {} + } + + // Otherwise learn new path. + debug_event!(si, "[vl1] {} learned new path: {}", self.identity.address.to_string(), new_path.endpoint.to_string()); + paths.push(PeerPath:: { + path: Arc::downgrade(new_path), + canonical_instance_id: new_path.canonical.canonical_instance_id(), + last_receive_time_ticks: time_ticks, + }); + Self::prioritize_paths(&mut paths); + } + /// Called every SERVICE_INTERVAL_MS by the background service loop in Node. pub(crate) fn service(&self, _: &SI, _: &Node, time_ticks: i64) -> bool { { @@ -278,9 +319,9 @@ impl Peer { if let Ok(mut verb) = payload.u8_at(0) { let extended_authentication = (verb & packet_constants::VERB_FLAG_EXTENDED_AUTHENTICATION) != 0; if extended_authentication { - if payload.len() >= SHA512_HASH_SIZE { - let actual_end_of_payload = payload.len() - SHA512_HASH_SIZE; - let mut hmac = HMACSHA512::new(self.identity_symmetric_key.packet_hmac_key.as_bytes()); + if payload.len() >= SHA384_HASH_SIZE { + let actual_end_of_payload = payload.len() - SHA384_HASH_SIZE; + let mut hmac = HMACSHA384::new(self.identity_symmetric_key.packet_hmac_key.as_bytes()); hmac.update(&node.identity.fingerprint); hmac.update(&self.identity.fingerprint); hmac.update(&message_id.to_ne_bytes()); @@ -331,7 +372,7 @@ impl Peer { //VERB_VL1_NOP => {} verbs::VL1_HELLO => self.handle_incoming_hello(si, node, time_ticks, source_path, &payload).await, verbs::VL1_ERROR => self.handle_incoming_error(si, ph, node, time_ticks, source_path, forward_secrecy, extended_authentication, &payload).await, - verbs::VL1_OK => self.handle_incoming_ok(si, ph, node, time_ticks, source_path, path_is_known, forward_secrecy, extended_authentication, &payload).await, + verbs::VL1_OK => self.handle_incoming_ok(si, ph, node, time_ticks, source_path, packet_header.hops(), path_is_known, forward_secrecy, extended_authentication, &payload).await, verbs::VL1_WHOIS => self.handle_incoming_whois(si, node, time_ticks, source_path, &payload).await, verbs::VL1_RENDEZVOUS => self.handle_incoming_rendezvous(si, node, time_ticks, source_path, &payload).await, verbs::VL1_ECHO => self.handle_incoming_echo(si, node, time_ticks, source_path, &payload).await, @@ -463,7 +504,7 @@ impl Peer { hello_fixed_headers.version_major = VERSION_MAJOR; hello_fixed_headers.version_minor = VERSION_MINOR; hello_fixed_headers.version_revision = (VERSION_REVISION as u16).to_be_bytes(); - hello_fixed_headers.timestamp = si.time_clock().to_be_bytes(); + hello_fixed_headers.timestamp = (time_ticks as u64).wrapping_add(self.random_ticks_offset).to_be_bytes(); } assert_eq!(packet.len(), 41); @@ -471,8 +512,16 @@ impl Peer { // Full identity of this node. assert!(node.identity.marshal_with_options(&mut packet, Identity::ALGORITHM_ALL, false).is_ok()); - // Append two reserved bytes, currently always zero. - assert!(packet.append_padding(0, 2).is_ok()); + // Create session meta-data. + let mut session_metadata = Dictionary::new(); + session_metadata.set_bytes(session_metadata::INSTANCE_ID, node.instance_id.to_vec()); + session_metadata.set_bytes(session_metadata::CARE_OF, node.care_of_bytes()); + session_metadata.set_bytes(session_metadata::SENT_TO, destination.to_buffer::<{ Endpoint::MAX_MARSHAL_SIZE }>().unwrap().as_bytes().to_vec()); + let session_metadata = session_metadata.to_bytes(); + + // Prefix encrypted session metadata with its size (in cleartext). + assert!(session_metadata.len() <= 0xffff); // sanity check, should be impossible + assert!(packet.append_u16(session_metadata.len() as u16).is_ok()); // Append a 16-byte AES-CTR nonce. LEGACY: for compatibility the last two bytes of this nonce // are in fact an encryption of two zeroes with Salsa20/12, which old nodes will interpret as @@ -483,22 +532,7 @@ impl Peer { Salsa::<12>::new(&self.identity_symmetric_key.key.0[0..32], &salsa_iv).crypt(&crate::util::ZEROES[..2], &mut nonce[14..]); assert!(packet.append_bytes_fixed(&nonce).is_ok()); - // Add session meta-data, which is encrypted using plain AES-CTR. No authentication (AEAD) is needed - // because the whole packet is authenticated. While the data in this section is not necessarily critical - // to protect, encrypting it is a defense in depth measure. - let mut session_metadata = Dictionary::new(); - session_metadata.set_bytes(session_metadata::INSTANCE_ID, node.instance_id.to_vec()); - session_metadata.set_u64(session_metadata::TIME_TICKS, time_ticks as u64); - session_metadata.set_bytes(session_metadata::SENT_TO, destination.to_buffer::<{ Endpoint::MAX_MARSHAL_SIZE }>().unwrap().as_bytes().to_vec()); - let session_metadata = session_metadata.to_bytes(); - - // Prefix encrypted session metadata with its size (in cleartext). - assert!(session_metadata.len() <= 0xffff); // sanity check, should be impossible - assert!(packet.append_u16(session_metadata.len() as u16).is_ok()); - - // Write session meta-data in encrypted form. A derived key is made using the message ID as a salt - // because this only ever uses the permanent identity key. In V2 we don't want to get near any - // key usage boundaries unless forward secrecy is off for some reason. + // Write session meta-data in encrypted form. nonce[12] &= 0x7f; // mask off the MSB of the 32-bit counter part of the CTR nonce for compatibility with AES libraries that don't wrap let salted_key = Secret(hmac_sha384(&message_id.to_ne_bytes(), self.identity_symmetric_key.hello_private_section_key.as_bytes())); let mut aes = AesCtr::new(&salted_key.as_bytes()[0..32]); @@ -506,12 +540,12 @@ impl Peer { aes.crypt(session_metadata.as_slice(), packet.append_bytes_get_mut(session_metadata.len()).unwrap()); // Set fragment flag if the packet will need to be fragmented. - if (packet.len() + SHA512_HASH_SIZE) > max_fragment_size { + if (packet.len() + SHA384_HASH_SIZE) > max_fragment_size { set_packet_fragment_flag(&mut packet); } - // Seal packet with HMAC-SHA512 extended authentication. - let mut hmac = HMACSHA512::new(self.identity_symmetric_key.packet_hmac_key.as_bytes()); + // Seal packet with HMAC-SHA384 extended authentication. + let mut hmac = HMACSHA384::new(self.identity_symmetric_key.packet_hmac_key.as_bytes()); hmac.update(&self.identity.fingerprint); hmac.update(&node.identity.fingerprint); hmac.update(&message_id.to_ne_bytes()); @@ -561,55 +595,34 @@ impl Peer { } #[allow(unused)] - async fn handle_incoming_ok(&self, si: &SI, ph: &PH, node: &Node, time_ticks: i64, source_path: &Arc>, path_is_known: bool, forward_secrecy: bool, extended_authentication: bool, payload: &PacketBuffer) { + async fn handle_incoming_ok( + &self, + si: &SI, + ph: &PH, + node: &Node, + time_ticks: i64, + source_path: &Arc>, + hops: u8, + path_is_known: bool, + forward_secrecy: bool, + extended_authentication: bool, + payload: &PacketBuffer, + ) { let mut cursor: usize = 1; if let Ok(ok_header) = payload.read_struct::(&mut cursor) { let in_re_message_id = u64::from_ne_bytes(ok_header.in_re_message_id); let current_packet_id_counter = self.message_id_counter.load(Ordering::Relaxed); if current_packet_id_counter.wrapping_sub(in_re_message_id) <= PACKET_RESPONSE_COUNTER_DELTA_MAX { - // TODO: learn new paths - /* - if !path_is_known { - let mut paths = self.paths.lock(); - paths.retain_mut(|p| { - if let Some(path) = p.path.upgrade() { - match (&path.endpoint, &source_path.endpoint) { - (Endpoint::IpUdp(current_address), Endpoint::IpUdp(source_address)) => { - if current_address.ip_bytes().eq(source_address.ip_bytes()) { - // In UDP mode, replace paths that come from the same IP but a different port. This helps avoid - // creating endless duplicate paths in NAT traversal scenarios if a NAT changes its port. - p.path = Arc::downgrade(source_path); - p.path_internal_instance_id = source_path.internal_instance_id; - p.last_receive_time_ticks = time_ticks; - path_is_known = true; - true - } else { - true - } - } - _ => true, - } - } else { - false - } - }); - if !path_is_known { - paths.push(PeerPath:: { - path: Arc::downgrade(source_path), - path_internal_instance_id: source_path.internal_instance_id, - last_receive_time_ticks: time_ticks, - }); - } - Self::prioritize_paths(&mut paths); - } - */ - match ok_header.in_re_verb { verbs::VL1_HELLO => { - // TODO + if hops == 0 && !path_is_known { + self.learn_path(si, source_path, time_ticks); + } self.last_hello_reply_time_ticks.store(time_ticks, Ordering::Relaxed); } + verbs::VL1_WHOIS => {} + _ => { ph.handle_ok(self, &source_path, forward_secrecy, extended_authentication, ok_header.in_re_verb, in_re_message_id, payload, &mut cursor).await; } diff --git a/zerotier-network-hypervisor/src/vl1/protocol.rs b/zerotier-network-hypervisor/src/vl1/protocol.rs index 708e22a68..74de1cd85 100644 --- a/zerotier-network-hypervisor/src/vl1/protocol.rs +++ b/zerotier-network-hypervisor/src/vl1/protocol.rs @@ -234,8 +234,8 @@ pub mod security_constants { pub mod session_metadata { pub const INSTANCE_ID: &'static str = "i"; - pub const TIME_TICKS: &'static str = "t"; pub const SENT_TO: &'static str = "d"; + pub const CARE_OF: &'static str = "c"; } /// Maximum number of packet hops allowed by the protocol. diff --git a/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs b/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs index 014c13766..d33601e1c 100644 --- a/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs +++ b/zerotier-network-hypervisor/src/vl1/symmetricsecret.rs @@ -14,13 +14,13 @@ use crate::vl1::protocol::*; /// This contains the key and several sub-keys and ciphers keyed with sub-keys. pub(crate) struct SymmetricSecret { /// Master key from which other keys are derived. - pub key: Secret<64>, + pub key: Secret<48>, /// Key for private fields in HELLO packets. pub hello_private_section_key: Secret<48>, /// Key used for HMAC extended validation on packets like HELLO. - pub packet_hmac_key: Secret<64>, + pub packet_hmac_key: Secret<48>, /// Pool of keyed AES-GMAC-SIV engines (pooled to avoid AES re-init every time). pub aes_gmac_siv: Pool, @@ -28,11 +28,10 @@ pub(crate) struct SymmetricSecret { impl SymmetricSecret { /// Create a new symmetric secret, deriving all sub-keys and such. - pub fn new(key: Secret<64>) -> SymmetricSecret { + pub fn new(key: Secret<48>) -> SymmetricSecret { let hello_private_section_key = zt_kbkdf_hmac_sha384(&key.0, security_constants::KBKDF_KEY_USAGE_LABEL_HELLO_PRIVATE_SECTION); - let packet_hmac_key = zt_kbkdf_hmac_sha512(&key.0, security_constants::KBKDF_KEY_USAGE_LABEL_PACKET_HMAC); - let aes_factory = - AesGmacSivPoolFactory(zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0).first_n(), zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1).first_n()); + let packet_hmac_key = zt_kbkdf_hmac_sha384(&key.0, security_constants::KBKDF_KEY_USAGE_LABEL_PACKET_HMAC); + let aes_factory = AesGmacSivPoolFactory(zt_kbkdf_hmac_sha384(&key.0, security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0).first_n(), zt_kbkdf_hmac_sha384(&key.0[..48], security_constants::KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1).first_n()); SymmetricSecret { key, hello_private_section_key,