mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-07-23 18:52:50 +02:00
342 lines
15 KiB
Rust
342 lines
15 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*
|
|
* (c)2021 ZeroTier, Inc.
|
|
* https://www.zerotier.com/
|
|
*/
|
|
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
use std::io::Write;
|
|
use std::convert::TryInto;
|
|
|
|
use zerotier_core_crypto::c25519::{C25519KeyPair, C25519_PUBLIC_KEY_SIZE};
|
|
use zerotier_core_crypto::hash::{SHA384_HASH_SIZE, SHA384};
|
|
use zerotier_core_crypto::p521::{P521KeyPair, P521_PUBLIC_KEY_SIZE, P521PublicKey};
|
|
use zerotier_core_crypto::random::SecureRandom;
|
|
use zerotier_core_crypto::secret::Secret;
|
|
use zerotier_core_crypto::sidhp751::{SIDHPublicKeyAlice, SIDHPublicKeyBob, SIDHSecretKeyAlice, SIDHSecretKeyBob, SIDH_P751_PUBLIC_KEY_SIZE};
|
|
use zerotier_core_crypto::varint;
|
|
|
|
use crate::vl1::Address;
|
|
use crate::vl1::protocol::*;
|
|
use crate::vl1::symmetricsecret::SymmetricSecret;
|
|
|
|
/// A set of ephemeral secret key pairs. Multiple algorithms are used.
|
|
pub struct EphemeralKeyPairSet {
|
|
previous_ratchet_state: Option<[u8; 16]>, // First 128 bits of SHA384(previous ratchet secret)
|
|
c25519: C25519KeyPair, // Hipster DJB cryptography
|
|
p521: P521KeyPair, // US federal government cryptography
|
|
sidhp751: Option<SIDHEphemeralKeyPair>, // Post-quantum moon math cryptography
|
|
}
|
|
|
|
impl EphemeralKeyPairSet {
|
|
/// Create a new ephemeral set of secret/public key pairs.
|
|
///
|
|
/// This contains key pairs for the asymmetric key agreement algorithms used and a
|
|
/// timestamp used to enforce TTL.
|
|
///
|
|
/// SIDH is only used once per ratchet sequence because it's much more CPU intensive
|
|
/// than ECDH. The threat model for SIDH is forward secrecy on the order of 5-15 years
|
|
/// from now when a quantum computer capable of attacking elliptic curve may exist,
|
|
/// it's incredibly unlikely that a p2p link would ever persist that long.
|
|
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(|| {
|
|
(
|
|
Some(SIDHEphemeralKeyPair::generate(local_address, remote_address)),
|
|
None
|
|
)
|
|
}, |previous_ephemeral_secret| {
|
|
(
|
|
None,
|
|
Some(previous_ephemeral_secret.ratchet_state.clone())
|
|
)
|
|
});
|
|
EphemeralKeyPairSet {
|
|
previous_ratchet_state,
|
|
c25519: C25519KeyPair::generate(true),
|
|
p521: P521KeyPair::generate(true).expect("NIST P-521 key pair generation failed"),
|
|
sidhp751,
|
|
}
|
|
}
|
|
|
|
/// Create a public version of this ephemeral secret to share with our counterparty.
|
|
///
|
|
/// Note that the public key bundle is NOT self-signed or otherwise self-authenticating. It must
|
|
/// be transmitted over an authenticated channel.
|
|
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_none() {
|
|
b.push(0); // no flags
|
|
} else {
|
|
b.push(1); // flag 0x01: previous ephemeral secret hash included
|
|
let _ = b.write_all(self.previous_ratchet_state.as_ref().unwrap());
|
|
}
|
|
|
|
b.push(EphemeralKeyAgreementAlgorithm::C25519 as u8);
|
|
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);
|
|
let _ = varint::write(&mut b, (SIDH_P751_PUBLIC_KEY_SIZE + 1) as u64);
|
|
b.push(sidhp751.role());
|
|
let pk = match &sidhp751 {
|
|
SIDHEphemeralKeyPair::Alice(a, _) => a.to_bytes(),
|
|
SIDHEphemeralKeyPair::Bob(b, _) => b.to_bytes()
|
|
};
|
|
let _ = b.write_all(&pk);
|
|
});
|
|
|
|
// FIPS note: any FIPS compliant ciphers must be last or the exchange will not be FIPS compliant. That's
|
|
// because we chain/ratchet using KHDF and non-FIPS ciphers are considered "salt" inputs for HKDF from a
|
|
// 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);
|
|
let _ = varint::write(&mut b, P521_PUBLIC_KEY_SIZE as u64);
|
|
let _ = b.write_all(self.p521.public_key_bytes());
|
|
|
|
b
|
|
}
|
|
|
|
/// Perform ephemeral key agreement.
|
|
///
|
|
/// None is returned if the public key data is malformed, no algorithms overlap, etc.
|
|
///
|
|
/// Input is the previous session key. The long-lived identity key exchange key starts
|
|
/// 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(|| {
|
|
(
|
|
static_secret.next_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.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 other_public_bytes = other_public_bytes;
|
|
|
|
// Make sure the state of the ratchet matches on both ends. Otherwise it must restart.
|
|
if other_public_bytes.is_empty() {
|
|
return None;
|
|
}
|
|
if (other_public_bytes[0] & 1) == 0 {
|
|
if previous_ephemeral_secret.is_some() {
|
|
return None;
|
|
}
|
|
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].ne(&previous_ephemeral_secret.ratchet_state)) {
|
|
return None;
|
|
}
|
|
other_public_bytes = &other_public_bytes[17..];
|
|
}
|
|
|
|
while !other_public_bytes.is_empty() {
|
|
let cipher = other_public_bytes[0];
|
|
other_public_bytes = &other_public_bytes[1..];
|
|
let key_len = varint::read(&mut other_public_bytes);
|
|
if key_len.is_err() {
|
|
return None;
|
|
}
|
|
let key_len = key_len.unwrap().0 as usize;
|
|
|
|
match cipher.try_into() {
|
|
|
|
Ok(EphemeralKeyAgreementAlgorithm::C25519) => {
|
|
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;
|
|
c25519_ratchet_count += 1;
|
|
},
|
|
|
|
Ok(EphemeralKeyAgreementAlgorithm::SIDHP751) => {
|
|
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)]))))
|
|
}
|
|
},
|
|
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)]))))
|
|
}
|
|
},
|
|
None => None,
|
|
}.map(|sidh_secret| {
|
|
key.0 = SHA384::hmac(&key.0, &sidh_secret.0);
|
|
it_happened = true;
|
|
fips_compliant_exchange = false;
|
|
sidhp751_ratchet_count += 1;
|
|
});
|
|
other_public_bytes = &other_public_bytes[(SIDH_P751_PUBLIC_KEY_SIZE + 1)..];
|
|
},
|
|
|
|
Ok(EphemeralKeyAgreementAlgorithm::NistP521ECDH) => {
|
|
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;
|
|
nistp521_ratchet_count += 1;
|
|
},
|
|
|
|
Err(_) => {
|
|
if other_public_bytes.len() < key_len {
|
|
return None;
|
|
}
|
|
other_public_bytes = &other_public_bytes[key_len..];
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return if it_happened {
|
|
let ratchet_state = SHA384::hash(&key.0)[0..16].try_into().unwrap();
|
|
Some(EphemeralSymmetricSecret {
|
|
secret: SymmetricSecret::new(key),
|
|
ratchet_state,
|
|
rekey_time: time_ticks + EPHEMERAL_SECRET_REKEY_AFTER_TIME,
|
|
expire_time: time_ticks + EPHEMERAL_SECRET_REJECT_AFTER_TIME,
|
|
c25519_ratchet_count,
|
|
sidhp751_ratchet_count,
|
|
nistp521_ratchet_count,
|
|
encrypt_uses: AtomicU32::new(0),
|
|
decrypt_uses: AtomicU32::new(0),
|
|
fips_compliant_exchange
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Symmetric secret representing a step in the ephemeral keying ratchet.
|
|
pub struct EphemeralSymmetricSecret {
|
|
pub secret: SymmetricSecret,
|
|
ratchet_state: [u8; 16],
|
|
rekey_time: i64,
|
|
expire_time: i64,
|
|
c25519_ratchet_count: u64,
|
|
sidhp751_ratchet_count: u64,
|
|
nistp521_ratchet_count: u64,
|
|
encrypt_uses: AtomicU32,
|
|
decrypt_uses: AtomicU32,
|
|
fips_compliant_exchange: bool,
|
|
}
|
|
|
|
impl EphemeralSymmetricSecret {
|
|
#[inline(always)]
|
|
pub fn use_secret_to_encrypt(&self) -> &SymmetricSecret {
|
|
let _ = self.encrypt_uses.fetch_add(1, Ordering::Relaxed);
|
|
&self.secret
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn use_secret_to_decrypt(&self) -> &SymmetricSecret {
|
|
let _ = self.decrypt_uses.fetch_add(1, Ordering::Relaxed);
|
|
&self.secret
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
enum SIDHEphemeralKeyPair {
|
|
Alice(SIDHPublicKeyAlice, SIDHSecretKeyAlice),
|
|
Bob(SIDHPublicKeyBob, SIDHSecretKeyBob)
|
|
}
|
|
|
|
impl SIDHEphemeralKeyPair {
|
|
/// Generate a SIDH key pair.
|
|
///
|
|
/// SIDH is weird. A key exchange must involve one participant taking a role
|
|
/// canonically called Alice and the other wearing the Bob hat, because math.
|
|
///
|
|
/// If our local address is less than the remote address, we take the Alice role.
|
|
/// Otherwise if it's greater or equal we take the Bob role.
|
|
///
|
|
/// Everything works as long as the two sides take opposite roles. There is no
|
|
/// security implication in one side always taking one role.
|
|
pub fn generate(local_address: Address, remote_address: Address) -> SIDHEphemeralKeyPair {
|
|
let mut rng = SecureRandom::get();
|
|
if local_address < remote_address {
|
|
let (p, s) = zerotier_core_crypto::sidhp751::generate_alice_keypair(&mut rng);
|
|
SIDHEphemeralKeyPair::Alice(p, s)
|
|
} else {
|
|
let (p, s) = zerotier_core_crypto::sidhp751::generate_bob_keypair(&mut rng);
|
|
SIDHEphemeralKeyPair::Bob(p, s)
|
|
}
|
|
}
|
|
|
|
/// Returns 0 if Alice, 1 if Bob.
|
|
#[inline(always)]
|
|
pub fn role(&self) -> u8 {
|
|
match self {
|
|
Self::Alice(_, _) => 0,
|
|
Self::Bob(_, _) => 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::vl1::ephemeral::EphemeralKeyPairSet;
|
|
use crate::vl1::Address;
|
|
use crate::vl1::symmetricsecret::SymmetricSecret;
|
|
use zerotier_core_crypto::secret::Secret;
|
|
|
|
#[test]
|
|
fn ephemeral_agreement() {
|
|
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();
|
|
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));
|
|
}
|
|
}
|