Add CareOf, which will be used with new roots in the near future. Also tweak some stuff like key sizes. There is no need for 512-bit keys when nothing uses the last 128 bits.

This commit is contained in:
Adam Ierymenko 2022-06-24 08:08:41 -04:00
parent 1df6843e94
commit 1da011b75e
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
7 changed files with 203 additions and 103 deletions

View file

@ -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<u8>,
}
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<u8> {
let mut v: Vec<u8> = 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<u8> {
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()
}
}

View file

@ -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<Secret<64>> {
pub fn agree(&self, other: &Identity) -> Option<Secret<48>> {
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)
}

View file

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

View file

@ -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<SI: SystemInterface> {
roots: HashMap<Arc<Peer<SI>>, Vec<Endpoint>>,
sets: HashMap<String, RootSet>,
roots: HashMap<Arc<Peer<SI>>, Vec<Endpoint>>,
care_of: Vec<u8>,
sets_modified: bool,
online: bool,
}
@ -255,8 +257,9 @@ impl<SI: SystemInterface> Node<SI> {
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<SI: SystemInterface> Node<SI> {
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<SI: SystemInterface> Node<SI> {
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<Path<SI>> {
pub(crate) fn care_of_bytes(&self) -> Vec<u8> {
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<Path<SI>> {
if let Some(path) = self.paths.read().get(&PathKey::Ref(ep, local_socket)) {
return path.clone();
}

View file

@ -55,6 +55,7 @@ pub struct Peer<SI: SystemInterface> {
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<SI: SystemInterface> Peer<SI> {
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<SI: SystemInterface> Peer<SI> {
})
}
/// 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<u8> {
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<SI: SystemInterface> Peer<SI> {
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<u8> {
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<PeerPath<SI>>) {
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<Path<SI>>, 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::<SI> {
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<SI>, time_ticks: i64) -> bool {
{
@ -278,9 +319,9 @@ impl<SI: SystemInterface> Peer<SI> {
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<SI: SystemInterface> Peer<SI> {
//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<SI: SystemInterface> Peer<SI> {
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<SI: SystemInterface> Peer<SI> {
// 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<SI: SystemInterface> Peer<SI> {
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<SI: SystemInterface> Peer<SI> {
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<SI: SystemInterface> Peer<SI> {
}
#[allow(unused)]
async fn handle_incoming_ok<PH: InnerProtocolInterface>(&self, si: &SI, ph: &PH, node: &Node<SI>, time_ticks: i64, source_path: &Arc<Path<SI>>, path_is_known: bool, forward_secrecy: bool, extended_authentication: bool, payload: &PacketBuffer) {
async fn handle_incoming_ok<PH: InnerProtocolInterface>(
&self,
si: &SI,
ph: &PH,
node: &Node<SI>,
time_ticks: i64,
source_path: &Arc<Path<SI>>,
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::<message_component_structs::OkHeader>(&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::<SI> {
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;
}

View file

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

View file

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