More cleanup and one more tweak to ephemeral keys. Add a ratchet counter to prevent replay attacks.

This commit is contained in:
Adam Ierymenko 2022-01-13 13:01:44 -05:00
parent c39f38d818
commit 07cfd12620
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
5 changed files with 136 additions and 103 deletions

View file

@ -9,6 +9,10 @@
#[macro_use]
extern crate lazy_static;
pub const VERSION_MAJOR: u8 = 1;
pub const VERSION_MINOR: u8 = 99;
pub const VERSION_REVISION: u8 = 1;
pub mod util;
pub mod error;
pub mod vl1;
@ -26,11 +30,6 @@ pub type PacketBufferFactory = crate::util::buffer::PooledBufferFactory<{ crate:
/// Source for instances of PacketBuffer
pub type PacketBufferPool = crate::util::pool::Pool<crate::util::buffer::Buffer<{ crate::vl1::protocol::PACKET_SIZE_MAX }>, crate::PacketBufferFactory>;
pub const VERSION_MAJOR: u8 = 1;
pub const VERSION_MINOR: u8 = 99;
pub const VERSION_REVISION: u8 = 1;
pub const VERSION_STR: &'static str = "1.99.1";
/*
* Protocol versions
*
@ -60,13 +59,13 @@ pub const VERSION_STR: &'static str = "1.99.1";
* 10 - 1.4.0 ... 1.4.6
* + Contained early pre-alpha versions of multipath, which are deprecated
* 11 - 1.6.0 ... 2.0.0
* + Supports AES-GMAC-SIV symmetric crypto, backported from v2 tree.
* + Supports and prefers AES-GMAC-SIV symmetric crypto, backported.
*
* 20 - 2.0.0 ... CURRENT
* + Forward secrecy with cryptographic ratchet! Finally!!!
* + New identity format including both x25519 and NIST P-521 keys.
* + AES-GMAC-SIV, a FIPS-compliant SIV construction using AES.
* + 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
* + HELLO and OK(HELLO) use a dictionary for better extensibilit.
*/
pub const VERSION_PROTO: u8 = 20;

View file

@ -27,7 +27,7 @@ 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.
/// A communication endpoint on the network where a ZeroTier node can be reached.
///
/// Currently only a few of these are supported. The rest are reserved for future use.
#[derive(Clone, PartialEq, Eq)]

View file

@ -6,6 +6,8 @@
* https://www.zerotier.com/
*/
use std::fmt::{Debug, Display};
use std::error::Error;
use std::sync::atomic::{AtomicU32, Ordering};
use std::io::Write;
use std::convert::TryInto;
@ -23,11 +25,41 @@ use crate::vl1::Address;
use crate::vl1::protocol::*;
use crate::vl1::symmetricsecret::SymmetricSecret;
const EPHEMERAL_PUBLIC_FLAG_HAVE_RATCHET_STATE: u8 = 0x01;
const EPHEMERAL_PUBLIC_FLAG_HAVE_RATCHET_STATE_HMAC: u8 = 0x01;
pub const ALGORITHM_C25519: u8 = 0x01;
pub const ALGORITHM_NISTP521ECDH: u8 = 0x02;
pub const ALGORITHM_SIDHP751: u8 = 0x04;
pub enum EphemeralKeyAgreementError {
OldPublic,
StateMismatch,
InvalidData,
NoCompatibleAlgorithms
}
impl Display for EphemeralKeyAgreementError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EphemeralKeyAgreementError::OldPublic => f.write_str("old (replayed?) public key data from remote"),
EphemeralKeyAgreementError::StateMismatch => f.write_str("ratchet state mismatch"),
EphemeralKeyAgreementError::InvalidData => f.write_str("invalid public key data"),
EphemeralKeyAgreementError::NoCompatibleAlgorithms => f.write_str("no compatible algorithms in public key data")
}
}
}
impl Debug for EphemeralKeyAgreementError {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { <Self as Display>::fmt(self, f) }
}
impl Error for EphemeralKeyAgreementError {}
/// A set of ephemeral secret key pairs. Multiple algorithms are used.
pub(crate) struct EphemeralKeyPairSet {
previous_ratchet_state: Option<[u8; 16]>, // Previous state of ratchet on which this agreement should build
previous_ratchet_count: u64, // Previous ratchet count, next ratchet should be this + 1
state_hmac: Option<[u8; 48]>, // HMAC of previous ratchet count, if there was a previous state
c25519: C25519KeyPair, // Hipster DJB cryptography
p521: P521KeyPair, // US Federal Government cryptography
sidhp751: Option<SIDHEphemeralKeyPair>, // Post-quantum moon math cryptography (not used in every ratchet tick)
@ -39,15 +71,16 @@ impl EphemeralKeyPairSet {
/// This contains key pairs for the asymmetric key agreement algorithms used and a
/// timestamp used to enforce TTL.
pub fn new(local_address: Address, remote_address: Address, previous_ephemeral_secret: Option<&EphemeralSymmetricSecret>) -> Self {
let (sidhp751, previous_ratchet_state) = previous_ephemeral_secret.map_or_else(|| {
let (sidhp751, previous_ratchet_count, state_hmac) = previous_ephemeral_secret.map_or_else(|| {
(
Some(SIDHEphemeralKeyPair::generate(local_address, remote_address)),
0,
None
)
}, |previous_ephemeral_secret| {
(
if previous_ephemeral_secret.ratchet_state[0] == 0 {
// We include SIDH with a probability of 1/256, which for a 5 minute re-key interval
if (previous_ephemeral_secret.ratchet_count & 0xff) == 0 {
// We include SIDH every 256 ratchets, which for a 5 minute re-key interval
// means SIDH will be included about every 24 hours. SIDH is slower and is intended
// to guard against long term warehousing for eventual cracking with a QC, so this
// should be good enough for that threat model.
@ -55,11 +88,13 @@ impl EphemeralKeyPairSet {
} else {
None
},
Some(previous_ephemeral_secret.ratchet_state.clone())
previous_ephemeral_secret.ratchet_count,
Some(SHA384::hmac(previous_ephemeral_secret.secret.ephemeral_ratchet_state_key.as_bytes(), &previous_ephemeral_secret.ratchet_count.to_be_bytes()))
)
});
EphemeralKeyPairSet {
previous_ratchet_state,
previous_ratchet_count,
state_hmac,
c25519: C25519KeyPair::generate(true),
p521: P521KeyPair::generate(true).expect("NIST P-521 key pair generation failed"),
sidhp751,
@ -73,19 +108,21 @@ impl EphemeralKeyPairSet {
pub fn public_bytes(&self) -> Vec<u8> {
let mut b: Vec<u8> = Vec::with_capacity(SHA384_HASH_SIZE + 8 + C25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + SIDH_P751_PUBLIC_KEY_SIZE);
if self.previous_ratchet_state.is_some() {
b.push(EPHEMERAL_PUBLIC_FLAG_HAVE_RATCHET_STATE);
let _ = b.write_all(self.previous_ratchet_state.as_ref().unwrap());
let _ = varint::write(&mut b, self.previous_ratchet_count);
if self.state_hmac.is_some() {
b.push(EPHEMERAL_PUBLIC_FLAG_HAVE_RATCHET_STATE_HMAC);
let _ = b.write_all(self.state_hmac.as_ref().unwrap());
} else {
b.push(0);
}
b.push(EphemeralKeyAgreementAlgorithm::C25519 as u8);
b.push(ALGORITHM_C25519);
let _ = varint::write(&mut b, C25519_PUBLIC_KEY_SIZE as u64);
let _ = b.write_all(&self.c25519.public_bytes());
let _ = self.sidhp751.as_ref().map(|sidhp751| {
b.push(EphemeralKeyAgreementAlgorithm::SIDHP751 as u8);
b.push(ALGORITHM_SIDHP751);
let _ = varint::write(&mut b, (SIDH_P751_PUBLIC_KEY_SIZE + 1) as u64);
b.push(sidhp751.role());
let pk = match &sidhp751 {
@ -100,7 +137,7 @@ impl EphemeralKeyPairSet {
// FIPS point of view. Final key must be HKDF(salt, a FIPS-compliant algorithm secret). There is zero
// actual security implication to the order.
b.push(EphemeralKeyAgreementAlgorithm::NISTP521ECDH as u8);
b.push(ALGORITHM_NISTP521ECDH);
let _ = varint::write(&mut b, P521_PUBLIC_KEY_SIZE as u64);
let _ = b.write_all(self.p521.public_key_bytes());
@ -115,41 +152,61 @@ impl EphemeralKeyPairSet {
/// the ratchet sequence, or rather a key derived from it for this purpose.
///
/// Since ephemeral secrets should only be used once, this consumes the object.
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(|| {
pub fn agree(self, time_ticks: i64, static_secret: &SymmetricSecret, previous_ephemeral_secret: Option<&EphemeralSymmetricSecret>, other_public_bytes: &[u8]) -> Result<EphemeralSymmetricSecret, EphemeralKeyAgreementError> {
let (mut key, mut ratchet_count, mut c25519_ratchet_count, mut sidhp751_ratchet_count, mut nistp521_ratchet_count) = previous_ephemeral_secret.map_or_else(|| {
(
static_secret.ephemeral_ratchet_key.clone(),
0,
0,
0,
0
)
}, |previous_ephemeral_secret| {
(
previous_ephemeral_secret.secret.ephemeral_ratchet_key.clone(),
previous_ephemeral_secret.ratchet_count,
previous_ephemeral_secret.c25519_ratchet_count,
previous_ephemeral_secret.sidhp751_ratchet_count,
previous_ephemeral_secret.nistp521_ratchet_count
)
});
let mut it_happened = false;
let mut fips_compliant_exchange = false; // ends up true if last algorithm was FIPS compliant
let mut it_happened = false; // set to true if at least one exchange occurred
let mut fips_compliant_exchange = false; // is true in the end if the last algorithm was FIPS-compliant
let mut other_public_bytes = other_public_bytes;
// Check that the other side's ratchet state matches ours. If not the ratchet must restart.
if other_public_bytes.is_empty() {
return None;
// If the other side's ratchet counter is less than ours it means this may be a replayed
// public key (or duplicate packet) and should be ignored. If it's greater it's a state
// mismatch since there's no other way it could be from the future.
let other_ratchet_count = varint::read(&mut other_public_bytes);
if other_ratchet_count.is_err() {
return Err(EphemeralKeyAgreementError::InvalidData);
}
if (other_public_bytes[0] & EPHEMERAL_PUBLIC_FLAG_HAVE_RATCHET_STATE) == 0 {
if previous_ephemeral_secret.is_some() {
return None;
let other_ratchet_count = other_ratchet_count.unwrap().0;
if other_ratchet_count < ratchet_count {
return Err(EphemeralKeyAgreementError::OldPublic);
} else if other_ratchet_count > ratchet_count {
return Err(EphemeralKeyAgreementError::StateMismatch);
}
// Now check the other side's HMAC of the ratchet state to fully verify that the ratchet
// is aligned properly.
if other_public_bytes.is_empty() {
return Err(EphemeralKeyAgreementError::InvalidData);
}
if (other_public_bytes[0] & EPHEMERAL_PUBLIC_FLAG_HAVE_RATCHET_STATE_HMAC) != 0 {
if other_public_bytes.len() < 49 {
return Err(EphemeralKeyAgreementError::InvalidData);
}
if self.state_hmac.as_ref().map_or(true, |state_hmac| state_hmac != &other_public_bytes[1..49]) {
return Err(EphemeralKeyAgreementError::StateMismatch);
}
other_public_bytes = &other_public_bytes[49..];
} else {
if self.state_hmac.is_some() {
return Err(EphemeralKeyAgreementError::OldPublic);
}
other_public_bytes = &other_public_bytes[1..];
} else {
if other_public_bytes.len() < 17 || previous_ephemeral_secret.map_or(false, |previous_ephemeral_secret| other_public_bytes[1..17] != previous_ephemeral_secret.ratchet_state) {
return None;
}
other_public_bytes = &other_public_bytes[17..];
}
while !other_public_bytes.is_empty() {
@ -157,15 +214,15 @@ impl EphemeralKeyPairSet {
other_public_bytes = &other_public_bytes[1..];
let key_len = varint::read(&mut other_public_bytes);
if key_len.is_err() {
return None;
return Err(EphemeralKeyAgreementError::InvalidData);
}
let key_len = key_len.unwrap().0 as usize;
match cipher.try_into() {
match cipher {
Ok(EphemeralKeyAgreementAlgorithm::C25519) => {
ALGORITHM_C25519 => {
if other_public_bytes.len() < C25519_PUBLIC_KEY_SIZE || key_len != C25519_PUBLIC_KEY_SIZE {
return None;
return Err(EphemeralKeyAgreementError::InvalidData);
}
let c25519_secret = self.c25519.agree(&other_public_bytes[0..C25519_PUBLIC_KEY_SIZE]);
@ -177,9 +234,9 @@ impl EphemeralKeyPairSet {
c25519_ratchet_count += 1;
},
Ok(EphemeralKeyAgreementAlgorithm::SIDHP751) => {
ALGORITHM_SIDHP751 => {
if other_public_bytes.len() < (SIDH_P751_PUBLIC_KEY_SIZE + 1) || key_len != (SIDH_P751_PUBLIC_KEY_SIZE + 1) {
return None;
return Err(EphemeralKeyAgreementError::InvalidData);
}
let _ = match self.sidhp751.as_ref() {
@ -207,20 +264,20 @@ impl EphemeralKeyPairSet {
other_public_bytes = &other_public_bytes[(SIDH_P751_PUBLIC_KEY_SIZE + 1)..];
},
Ok(EphemeralKeyAgreementAlgorithm::NISTP521ECDH) => {
ALGORITHM_NISTP751ECDH => {
if other_public_bytes.len() < P521_PUBLIC_KEY_SIZE || key_len != P521_PUBLIC_KEY_SIZE {
return None;
return Err(EphemeralKeyAgreementError::InvalidData);
}
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;
return Err(EphemeralKeyAgreementError::InvalidData);
}
let p521_key = self.p521.agree(p521_public.as_ref().unwrap());
if p521_key.is_none() {
return None;
return Err(EphemeralKeyAgreementError::InvalidData);
}
key.0 = SHA384::hmac(&key.0, &p521_key.unwrap().0);
@ -229,9 +286,9 @@ impl EphemeralKeyPairSet {
nistp521_ratchet_count += 1;
},
Err(_) => {
_ => {
if other_public_bytes.len() < key_len {
return None;
return Err(EphemeralKeyAgreementError::InvalidData);
}
other_public_bytes = &other_public_bytes[key_len..];
}
@ -240,10 +297,10 @@ impl EphemeralKeyPairSet {
}
return if it_happened {
let rs = zt_kbkdf_hmac_sha384(&key.0, KBKDF_KEY_USAGE_LABEL_EPHEMERAL_RATCHET_STATE_ID, 0, 0);
Some(EphemeralSymmetricSecret {
ratchet_count += 1;
Ok(EphemeralSymmetricSecret {
secret: SymmetricSecret::new(key),
ratchet_state: (&rs.0[0..16]).try_into().unwrap(),
ratchet_count,
rekey_time: time_ticks + EPHEMERAL_SECRET_REKEY_AFTER_TIME,
expire_time: time_ticks + EPHEMERAL_SECRET_REJECT_AFTER_TIME,
c25519_ratchet_count,
@ -254,17 +311,17 @@ impl EphemeralKeyPairSet {
fips_compliant_exchange
})
} else {
None
Err(EphemeralKeyAgreementError::NoCompatibleAlgorithms)
};
}
}
/// Symmetric secret representing a step in the ephemeral keying ratchet.
/// An ephemeral symmetric secret with usage timers and counters.
pub(crate) struct EphemeralSymmetricSecret {
/// Current ephemeral secret key.
pub secret: SymmetricSecret,
/// An identifier used to check negotiation of the ratchet.
ratchet_state: [u8; 16],
/// Total number of ratchets that has occurred.
ratchet_count: u64,
/// Time at or after which we should start trying to re-key.
rekey_time: i64,
/// Time after which this key is no longer valid.
@ -280,7 +337,7 @@ pub(crate) struct EphemeralSymmetricSecret {
/// Number of times this secret has been used to decrypt.
decrypt_uses: AtomicU32,
/// True if most recent key exchange was NIST/FIPS compliant.
fips_compliant_exchange: bool,
pub fips_compliant_exchange: bool,
}
impl EphemeralSymmetricSecret {
@ -307,6 +364,7 @@ impl EphemeralSymmetricSecret {
}
}
/// Internal container for SIDH key pairs tracking whether this is the "alice" or "bob" side in the SIDH protocol.
enum SIDHEphemeralKeyPair {
Alice(SIDHPublicKeyAlice, SIDHSecretKeyAlice),
Bob(SIDHPublicKeyBob, SIDHSecretKeyBob)
@ -364,10 +422,10 @@ mod tests {
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.agree(t, &static_secret, prev_alice_key.as_ref(), bob_public.as_slice());
let bob_key = bob.agree(t, &static_secret, prev_bob_key.as_ref(), alice_public.as_slice());
assert!(alice_key.is_ok());
assert!(bob_key.is_ok());
let alice_key = alice_key.unwrap();
let bob_key = bob_key.unwrap();
assert_eq!(&alice_key.secret.key.0, &bob_key.secret.key.0);
@ -379,10 +437,12 @@ mod tests {
}
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.ratchet_count, ratchets as u64);
assert_eq!(last_bob_key.ratchet_count, ratchets as u64);
assert_eq!(last_alice_key.c25519_ratchet_count, ratchets as u64);
assert_eq!(last_bob_key.c25519_ratchet_count, ratchets as u64);
assert_eq!(last_alice_key.nistp521_ratchet_count, ratchets as u64);
assert_eq!(last_bob_key.nistp521_ratchet_count, ratchets as u64);
assert_eq!(last_alice_key.sidhp751_ratchet_count, last_bob_key.sidhp751_ratchet_count);
assert!(last_alice_key.sidhp751_ratchet_count >= 1);
}

View file

@ -42,7 +42,7 @@ pub const KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1: u8 = b'1';
pub const KBKDF_KEY_USAGE_LABEL_EPHEMERAL_RATCHET_NEXT_KEY: u8 = b'e';
/// KBKDF usage label for generating the ratchet state ID (which is not actually a key).
pub const KBKDF_KEY_USAGE_LABEL_EPHEMERAL_RATCHET_STATE_ID: u8 = b'E';
pub const KBKDF_KEY_USAGE_LABEL_EPHEMERAL_RATCHET_STATE_KEY: u8 = b'E';
/// Try to re-key ephemeral keys after this time.
pub const EPHEMERAL_SECRET_REKEY_AFTER_TIME: i64 = 300000; // 5 minutes
@ -181,37 +181,6 @@ pub const IDENTITY_V0_POW_THRESHOLD: u8 = 17;
/// This is lower than the V0 threshold, causing the V0 part of V1 identities to verify on old nodes.
pub const IDENTITY_V1_POW_THRESHOLD: u8 = 9;
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum EphemeralKeyAgreementAlgorithm {
C25519 = 1,
SIDHP751 = 2,
NISTP521ECDH = 3
}
impl EphemeralKeyAgreementAlgorithm {
pub fn is_fips_compliant(&self) -> bool {
match self {
Self::NISTP521ECDH => true,
_ => false
}
}
}
impl TryFrom<u8> for EphemeralKeyAgreementAlgorithm {
type Error = ();
#[inline(always)]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::C25519),
2 => Ok(Self::SIDHP751),
3 => Ok(Self::NISTP521ECDH),
_ => Err(())
}
}
}
/// Compress a packet and return true if compressed.
/// The 'dest' buffer must be empty (will panic otherwise). A return value of false indicates an error or
/// that the data was not compressible. The state of the destination buffer is undefined on a return

View file

@ -38,6 +38,9 @@ pub(crate) struct SymmetricSecret {
/// A key used as input to the ephemeral key ratcheting mechanism.
pub ephemeral_ratchet_key: Secret<SHA384_HASH_SIZE>,
/// A key used to verify the state of the ephemeral ratchet.
pub ephemeral_ratchet_state_key: Secret<SHA384_HASH_SIZE>,
/// A pool of reusable keyed and initialized AES-GMAC-SIV ciphers.
pub aes_gmac_siv: Pool<AesGmacSiv, AesGmacSivPoolFactory>,
}
@ -54,6 +57,7 @@ impl SymmetricSecret {
pub fn new(base_key: Secret<SHA384_HASH_SIZE>) -> SymmetricSecret {
let usage_packet_hmac = zt_kbkdf_hmac_sha384(&base_key.0, KBKDF_KEY_USAGE_LABEL_PACKET_HMAC, 0, 0);
let usage_ephemeral_ratchet = zt_kbkdf_hmac_sha384(&base_key.0, KBKDF_KEY_USAGE_LABEL_EPHEMERAL_RATCHET_NEXT_KEY, 0, 0);
let usage_ephemeral_ratchet_state = zt_kbkdf_hmac_sha384(&base_key.0, KBKDF_KEY_USAGE_LABEL_EPHEMERAL_RATCHET_STATE_KEY, 0, 0);
let aes_factory = AesGmacSivPoolFactory(
zt_kbkdf_hmac_sha384(&base_key.0, KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0, 0, 0),
zt_kbkdf_hmac_sha384(&base_key.0, KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1, 0, 0));
@ -61,6 +65,7 @@ impl SymmetricSecret {
key: base_key,
packet_hmac_key: usage_packet_hmac,
ephemeral_ratchet_key: usage_ephemeral_ratchet,
ephemeral_ratchet_state_key: usage_ephemeral_ratchet_state,
aes_gmac_siv: Pool::new(2, aes_factory),
}
}