Some cleanup and more testing for ephemeral ratchet.

This commit is contained in:
Adam Ierymenko 2022-01-12 18:13:59 -05:00
parent 5fb5c76694
commit c39f38d818
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
5 changed files with 84 additions and 26 deletions

View file

@ -62,14 +62,11 @@ pub const VERSION_STR: &'static str = "1.99.1";
* 11 - 1.6.0 ... 2.0.0
* + Supports AES-GMAC-SIV symmetric crypto, backported from v2 tree.
* 20 - 2.0.0 ... CURRENT
* + New more WAN-efficient P2P-assisted multicast algorithm
* + HELLO and OK(HELLO) include an extra HMAC to harden authentication
* + HELLO and OK(HELLO) carry meta-data in a dictionary that's encrypted
* + Forward secrecy, key lifetime management
* + Old planet/moon stuff is DEAD! Independent roots are easier.
* + AES encryption with the SIV construction AES-GMAC-SIV
* + New combined Curve25519/NIST P-384 identity type (type 1)
* + Short probe packets to reduce probe bandwidth
* + More aggressive NAT traversal techniques for IPv4 symmetric NATs
* + New combined Curve25519/NIST P-384 identity
*/
pub const VERSION_PROTO: u8 = 20;

View file

@ -8,6 +8,7 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use zerotier_core_crypto::hash::SHA384_HASH_SIZE;
use crate::vl1::{Address, MAC};
@ -26,18 +27,44 @@ pub const TYPE_HTTP: u8 = 8;
pub const TYPE_WEBRTC: u8 = 9;
pub const TYPE_ZEROTIER_ENCAP: u8 = 10;
/// A communication endpoint on the network where some ZeroTier node can be reached.
///
/// Currently only a few of these are supported. The rest are reserved for future use.
#[derive(Clone, PartialEq, Eq)]
pub enum Endpoint {
/// A null endpoint.
Nil,
/// Via another node using unencapsulated relaying (e.g. via a root)
/// Hash is a full hash of the identity for strong verification.
ZeroTier(Address, [u8; SHA384_HASH_SIZE]),
/// Direct L2 Ethernet
Ethernet(MAC),
/// Direct L2 Ethernet over WiFi-Direct (P2P WiFi)
WifiDirect(MAC),
/// Local bluetooth
Bluetooth(MAC),
/// Raw IP without a UDP or other header
Ip(InetAddress),
/// Raw UDP, the default and usually preferred transport mode
IpUdp(InetAddress),
/// Raw TCP with each packet prefixed by a varint size
IpTcp(InetAddress),
/// HTTP streaming
Http(String),
/// WebRTC data channel
WebRTC(Vec<u8>),
/// Via another node using inner encapsulation via VERB_ENCAP.
/// Hash is a full hash of the identity for strong verification.
ZeroTierEncap(Address, [u8; SHA384_HASH_SIZE]),
}
@ -47,6 +74,7 @@ impl Default for Endpoint {
}
impl Endpoint {
/// Get the IP address (and port if applicable) if this is an IP-based transport.
#[inline(always)]
pub fn ip(&self) -> Option<(&InetAddress, u8)> {
match self {
@ -73,6 +101,9 @@ impl Endpoint {
}
}
#[inline(always)]
pub fn is_nil(&self) -> bool { matches!(self, Endpoint::Nil) }
pub fn marshal<const BL: usize>(&self, buf: &mut Buffer<BL>) -> std::io::Result<()> {
match self {
Endpoint::Nil => {
@ -239,6 +270,7 @@ impl Hash for Endpoint {
impl Ord for Endpoint {
fn cmp(&self, other: &Self) -> Ordering {
// Manually implement Ord to ensure that sort order is known and consistent.
match (self, other) {
(Endpoint::Nil, Endpoint::Nil) => Ordering::Equal,
(Endpoint::ZeroTier(a, ah), Endpoint::ZeroTier(b, bh)) => a.cmp(b).then_with(|| ah.cmp(bh)),
@ -274,7 +306,7 @@ impl ToString for Endpoint {
Endpoint::IpTcp(ip) => format!("tcp:{}", ip.to_string()),
Endpoint::Http(url) => url.clone(),
Endpoint::WebRTC(offer) => format!("webrtc:{}", base64::encode_config(offer.as_slice(), base64::URL_SAFE_NO_PAD)),
Endpoint::ZeroTierEncap(a, ah) => format!("zte:{}-{}", a.to_string(), base64::encode_config(ah, base64::URL_SAFE_NO_PAD)),
Endpoint::ZeroTierEncap(a, ah) => format!("ztzt:{}-{}", a.to_string(), base64::encode_config(ah, base64::URL_SAFE_NO_PAD)),
}
}
}

View file

@ -118,14 +118,14 @@ impl EphemeralKeyPairSet {
pub fn agree(self, time_ticks: i64, static_secret: &SymmetricSecret, previous_ephemeral_secret: Option<&EphemeralSymmetricSecret>, other_public_bytes: &[u8]) -> Option<EphemeralSymmetricSecret> {
let (mut key, mut c25519_ratchet_count, mut sidhp751_ratchet_count, mut nistp521_ratchet_count) = previous_ephemeral_secret.map_or_else(|| {
(
static_secret.next_ephemeral_ratchet_key.clone(),
static_secret.ephemeral_ratchet_key.clone(),
0,
0,
0
)
}, |previous_ephemeral_secret| {
(
Secret(SHA384::hmac(&static_secret.next_ephemeral_ratchet_key.0, &previous_ephemeral_secret.secret.next_ephemeral_ratchet_key.0)),
previous_ephemeral_secret.secret.ephemeral_ratchet_key.clone(),
previous_ephemeral_secret.c25519_ratchet_count,
previous_ephemeral_secret.sidhp751_ratchet_count,
previous_ephemeral_secret.nistp521_ratchet_count
@ -167,8 +167,10 @@ impl EphemeralKeyPairSet {
if other_public_bytes.len() < C25519_PUBLIC_KEY_SIZE || key_len != C25519_PUBLIC_KEY_SIZE {
return None;
}
let c25519_secret = self.c25519.agree(&other_public_bytes[0..C25519_PUBLIC_KEY_SIZE]);
other_public_bytes = &other_public_bytes[C25519_PUBLIC_KEY_SIZE..];
key.0 = SHA384::hmac(&key.0, &c25519_secret.0);
it_happened = true;
fips_compliant_exchange = false;
@ -179,19 +181,20 @@ impl EphemeralKeyPairSet {
if other_public_bytes.len() < (SIDH_P751_PUBLIC_KEY_SIZE + 1) || key_len != (SIDH_P751_PUBLIC_KEY_SIZE + 1) {
return None;
}
let _ = match self.sidhp751.as_ref() {
Some(SIDHEphemeralKeyPair::Alice(_, seck)) => {
if other_public_bytes[0] != 0 { // Alice can't agree with Alice
None
} else {
Some(Secret(seck.shared_secret(&SIDHPublicKeyBob::from_bytes(&other_public_bytes[1..(SIDH_P751_PUBLIC_KEY_SIZE + 1)]))))
} else {
None
}
},
Some(SIDHEphemeralKeyPair::Bob(_, seck)) => {
if other_public_bytes[0] != 1 { // Bob can't agree with Bob
None
} else {
Some(Secret(seck.shared_secret(&SIDHPublicKeyAlice::from_bytes(&other_public_bytes[1..(SIDH_P751_PUBLIC_KEY_SIZE + 1)]))))
} else {
None
}
},
None => None,
@ -208,15 +211,18 @@ impl EphemeralKeyPairSet {
if other_public_bytes.len() < P521_PUBLIC_KEY_SIZE || key_len != P521_PUBLIC_KEY_SIZE {
return None;
}
let p521_public = P521PublicKey::from_bytes(&other_public_bytes[0..P521_PUBLIC_KEY_SIZE]);
other_public_bytes = &other_public_bytes[P521_PUBLIC_KEY_SIZE..];
if p521_public.is_none() {
return None;
}
let p521_key = self.p521.agree(p521_public.as_ref().unwrap());
if p521_key.is_none() {
return None;
}
key.0 = SHA384::hmac(&key.0, &p521_key.unwrap().0);
it_happened = true;
fips_compliant_exchange = true;
@ -257,7 +263,7 @@ impl EphemeralKeyPairSet {
pub(crate) struct EphemeralSymmetricSecret {
/// Current ephemeral secret key.
pub secret: SymmetricSecret,
/// First 16 bytes of SHA384(current ephemeral secret).
/// An identifier used to check negotiation of the ratchet.
ratchet_state: [u8; 16],
/// Time at or after which we should start trying to re-key.
rekey_time: i64,
@ -290,10 +296,12 @@ impl EphemeralSymmetricSecret {
&self.secret
}
#[inline(always)]
pub fn should_rekey(&self, time_ticks: i64) -> bool {
time_ticks >= self.rekey_time || self.encrypt_uses.load(Ordering::Relaxed).max(self.decrypt_uses.load(Ordering::Relaxed)) >= EPHEMERAL_SECRET_REKEY_AFTER_USES
}
#[inline(always)]
pub fn expired(&self, time_ticks: i64) -> bool {
time_ticks >= self.expire_time || self.encrypt_uses.load(Ordering::Relaxed).max(self.decrypt_uses.load(Ordering::Relaxed)) >= EPHEMERAL_SECRET_REJECT_AFTER_USES
}
@ -344,15 +352,38 @@ mod tests {
use zerotier_core_crypto::secret::Secret;
#[test]
fn ephemeral_agreement() {
fn ratchet() {
let static_secret = SymmetricSecret::new(Secret([1_u8; 48]));
let alice = EphemeralKeyPairSet::new(Address::from_u64(0xdeadbeef00).unwrap(), Address::from_u64(0xbeefdead00).unwrap(), None);
let bob = EphemeralKeyPairSet::new(Address::from_u64(0xbeefdead00).unwrap(), Address::from_u64(0xdeadbeef00).unwrap(), None);
let alice_public_bytes = alice.public_bytes();
let bob_public_bytes = bob.public_bytes();
let alice_key = alice.agree(2, &static_secret, None, bob_public_bytes.as_slice()).unwrap();
let bob_key = bob.agree(2, &static_secret, None, alice_public_bytes.as_slice()).unwrap();
let alice_address = Address::from_u64(0xdeadbeef00).unwrap();
let bob_address = Address::from_u64(0xbeefdead00).unwrap();
let mut alice = EphemeralKeyPairSet::new(alice_address, bob_address, None);
let mut bob = EphemeralKeyPairSet::new(bob_address, alice_address, None);
let mut prev_alice_key = None;
let mut prev_bob_key = None;
let ratchets = 16;
for t in 1..ratchets+1 {
let alice_public = alice.public_bytes();
let bob_public = bob.public_bytes();
let alice_key = alice.agree(1, &static_secret, prev_alice_key.as_ref(), bob_public.as_slice());
let bob_key = bob.agree(1, &static_secret, prev_bob_key.as_ref(), alice_public.as_slice());
assert!(alice_key.is_some());
assert!(bob_key.is_some());
let alice_key = alice_key.unwrap();
let bob_key = bob_key.unwrap();
assert_eq!(&alice_key.secret.key.0, &bob_key.secret.key.0);
//println!("ephemeral_agreement secret: {}", zerotier_core_crypto::hex::to_string(&alice_key.secret.key.0));
//println!("alice: c25519={} p521={} sidh={} | bob: c25519={} p521={} sidh={}", alice_key.c25519_ratchet_count, alice_key.nistp521_ratchet_count, alice_key.sidhp751_ratchet_count, bob_key.c25519_ratchet_count, bob_key.nistp521_ratchet_count, bob_key.sidhp751_ratchet_count);
alice = EphemeralKeyPairSet::new(alice_address, bob_address, Some(&alice_key));
bob = EphemeralKeyPairSet::new(bob_address, alice_address, Some(&bob_key));
prev_alice_key = Some(alice_key);
prev_bob_key = Some(bob_key);
}
let last_alice_key = prev_alice_key.unwrap();
let last_bob_key = prev_bob_key.unwrap();
assert_eq!(last_alice_key.c25519_ratchet_count, ratchets);
assert_eq!(last_bob_key.c25519_ratchet_count, ratchets);
assert_eq!(last_alice_key.nistp521_ratchet_count, ratchets);
assert_eq!(last_bob_key.nistp521_ratchet_count, ratchets);
assert_eq!(last_alice_key.sidhp751_ratchet_count, last_bob_key.sidhp751_ratchet_count);
assert!(last_alice_key.sidhp751_ratchet_count >= 1);
}
}

View file

@ -12,9 +12,9 @@ use std::convert::TryInto;
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::mem::MaybeUninit;
use std::ptr::{slice_from_raw_parts, slice_from_raw_parts_mut};
use std::str::FromStr;
use highway::HighwayHash;
use lazy_static::lazy_static;

View file

@ -6,8 +6,6 @@
* https://www.zerotier.com/
*/
use parking_lot::Mutex;
use zerotier_core_crypto::aes_gmac_siv::AesGmacSiv;
use zerotier_core_crypto::hash::SHA384_HASH_SIZE;
use zerotier_core_crypto::kbkdf::zt_kbkdf_hmac_sha384;
@ -38,7 +36,7 @@ pub(crate) struct SymmetricSecret {
pub packet_hmac_key: Secret<SHA384_HASH_SIZE>,
/// A key used as input to the ephemeral key ratcheting mechanism.
pub next_ephemeral_ratchet_key: Secret<SHA384_HASH_SIZE>,
pub ephemeral_ratchet_key: Secret<SHA384_HASH_SIZE>,
/// A pool of reusable keyed and initialized AES-GMAC-SIV ciphers.
pub aes_gmac_siv: Pool<AesGmacSiv, AesGmacSivPoolFactory>,
@ -62,7 +60,7 @@ impl SymmetricSecret {
SymmetricSecret {
key: base_key,
packet_hmac_key: usage_packet_hmac,
next_ephemeral_ratchet_key: usage_ephemeral_ratchet,
ephemeral_ratchet_key: usage_ephemeral_ratchet,
aes_gmac_siv: Pool::new(2, aes_factory),
}
}