mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-04-26 17:03:43 +02:00
Move Noise to the crypto package, and tests.
This commit is contained in:
parent
5ea5488590
commit
f3903144e4
7 changed files with 262 additions and 68 deletions
|
@ -1,6 +1,7 @@
|
||||||
#unstable_features = true
|
#unstable_features = true
|
||||||
max_width = 256
|
max_width = 256
|
||||||
use_small_heuristics = "Max"
|
#use_small_heuristics = "Max"
|
||||||
|
use_small_heuristics = "Default"
|
||||||
tab_spaces = 4
|
tab_spaces = 4
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
@ -15,6 +15,8 @@ openssl = { version = "^0", features = [], default-features = false }
|
||||||
lazy_static = "^1"
|
lazy_static = "^1"
|
||||||
foreign-types = "0.3.1"
|
foreign-types = "0.3.1"
|
||||||
poly1305 = { version = "0.7.2", features = [], default-features = false }
|
poly1305 = { version = "0.7.2", features = [], default-features = false }
|
||||||
|
parking_lot = { version = "^0", features = [], default-features = false }
|
||||||
|
pqc_kyber = { path = "../third_party/kyber", features = ["kyber512", "reference"], default-features = false }
|
||||||
|
|
||||||
[target."cfg(not(any(target_os = \"macos\", target_os = \"ios\")))".dependencies]
|
[target."cfg(not(any(target_os = \"macos\", target_os = \"ios\")))".dependencies]
|
||||||
openssl = "^0"
|
openssl = "^0"
|
||||||
|
|
|
@ -22,26 +22,15 @@ mod fruit_flavored {
|
||||||
const kCCOptionECBMode: i32 = 2;
|
const kCCOptionECBMode: i32 = 2;
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn CCCryptorCreateWithMode(
|
fn CCCryptorCreateWithMode(op: i32, mode: i32, alg: i32, padding: i32, iv: *const c_void, key: *const c_void, key_len: usize, tweak: *const c_void, tweak_len: usize, num_rounds: c_int, options: i32, cryyptor_ref: *mut *mut c_void) -> i32;
|
||||||
op: i32,
|
|
||||||
mode: i32,
|
|
||||||
alg: i32,
|
|
||||||
padding: i32,
|
|
||||||
iv: *const c_void,
|
|
||||||
key: *const c_void,
|
|
||||||
key_len: usize,
|
|
||||||
tweak: *const c_void,
|
|
||||||
tweak_len: usize,
|
|
||||||
num_rounds: c_int,
|
|
||||||
options: i32,
|
|
||||||
cryyptor_ref: *mut *mut c_void,
|
|
||||||
) -> i32;
|
|
||||||
fn CCCryptorUpdate(cryptor_ref: *mut c_void, data_in: *const c_void, data_in_len: usize, data_out: *mut c_void, data_out_len: usize, data_out_written: *mut usize) -> i32;
|
fn CCCryptorUpdate(cryptor_ref: *mut c_void, data_in: *const c_void, data_in_len: usize, data_out: *mut c_void, data_out_len: usize, data_out_written: *mut usize) -> i32;
|
||||||
//fn CCCryptorReset(cryptor_ref: *mut c_void, iv: *const c_void) -> i32;
|
//fn CCCryptorReset(cryptor_ref: *mut c_void, iv: *const c_void) -> i32;
|
||||||
fn CCCryptorRelease(cryptor_ref: *mut c_void) -> i32;
|
fn CCCryptorRelease(cryptor_ref: *mut c_void) -> i32;
|
||||||
fn CCCryptorGCMSetIV(cryptor_ref: *mut c_void, iv: *const c_void, iv_len: usize) -> i32;
|
fn CCCryptorGCMSetIV(cryptor_ref: *mut c_void, iv: *const c_void, iv_len: usize) -> i32;
|
||||||
fn CCCryptorGCMAddAAD(cryptor_ref: *mut c_void, aad: *const c_void, len: usize) -> i32;
|
fn CCCryptorGCMAddAAD(cryptor_ref: *mut c_void, aad: *const c_void, len: usize) -> i32;
|
||||||
fn CCCryptorGCMFinalize(cryptor_ref: *mut c_void, tag: *mut c_void, tag_len: usize) -> i32;
|
fn CCCryptorGCMEncrypt(cryptor_ref: *mut c_void, data_in: *const c_void, data_in_len: usize, data_out: *mut c_void) -> i32;
|
||||||
|
fn CCCryptorGCMDecrypt(cryptor_ref: *mut c_void, data_in: *const c_void, data_in_len: usize, data_out: *mut c_void) -> i32;
|
||||||
|
fn CCCryptorGCMFinal(cryptor_ref: *mut c_void, tag: *mut c_void, tag_len: *mut usize) -> i32;
|
||||||
fn CCCryptorGCMReset(cryptor_ref: *mut c_void) -> i32;
|
fn CCCryptorGCMReset(cryptor_ref: *mut c_void) -> i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +101,7 @@ mod fruit_flavored {
|
||||||
unsafe impl Send for Aes {}
|
unsafe impl Send for Aes {}
|
||||||
unsafe impl Sync for Aes {}
|
unsafe impl Sync for Aes {}
|
||||||
|
|
||||||
pub struct AesGcm(*mut c_void);
|
pub struct AesGcm(*mut c_void, bool);
|
||||||
|
|
||||||
impl Drop for AesGcm {
|
impl Drop for AesGcm {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
@ -130,11 +119,15 @@ mod fruit_flavored {
|
||||||
let mut ptr: *mut c_void = null_mut();
|
let mut ptr: *mut c_void = null_mut();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CCCryptorCreateWithMode(
|
CCCryptorCreateWithMode(
|
||||||
if encrypt { kCCEncrypt } else { kCCDecrypt },
|
if encrypt {
|
||||||
|
kCCEncrypt
|
||||||
|
} else {
|
||||||
|
kCCDecrypt
|
||||||
|
},
|
||||||
kCCModeGCM,
|
kCCModeGCM,
|
||||||
kCCAlgorithmAES,
|
kCCAlgorithmAES,
|
||||||
0,
|
0,
|
||||||
[0_u64; 2].as_ptr().cast(),
|
null(),
|
||||||
k.as_ptr().cast(),
|
k.as_ptr().cast(),
|
||||||
k.len(),
|
k.len(),
|
||||||
null(),
|
null(),
|
||||||
|
@ -145,7 +138,7 @@ mod fruit_flavored {
|
||||||
),
|
),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
AesGcm(ptr)
|
AesGcm(ptr, encrypt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,7 +161,7 @@ mod fruit_flavored {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn aad(&mut self, aad: &[u8]) {
|
pub fn aad(&mut self, aad: &[u8]) {
|
||||||
unsafe {
|
unsafe {
|
||||||
CCCryptorGCMAddAAD(self.0, aad.as_ptr().cast(), aad.len());
|
assert_eq!(CCCryptorGCMAddAAD(self.0, aad.as_ptr().cast(), aad.len()), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,26 +169,32 @@ mod fruit_flavored {
|
||||||
pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) {
|
pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) {
|
||||||
unsafe {
|
unsafe {
|
||||||
assert_eq!(input.len(), output.len());
|
assert_eq!(input.len(), output.len());
|
||||||
let mut data_out_written: usize = 0;
|
if self.1 {
|
||||||
CCCryptorUpdate(self.0, input.as_ptr().cast(), input.len(), output.as_mut_ptr().cast(), output.len(), &mut data_out_written);
|
assert_eq!(CCCryptorGCMEncrypt(self.0, input.as_ptr().cast(), input.len(), output.as_mut_ptr().cast()), 0);
|
||||||
assert_eq!(data_out_written, input.len());
|
} else {
|
||||||
|
assert_eq!(CCCryptorGCMDecrypt(self.0, input.as_ptr().cast(), input.len(), output.as_mut_ptr().cast()), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn crypt_in_place(&mut self, data: &mut [u8]) {
|
pub fn crypt_in_place(&mut self, data: &mut [u8]) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut data_out_written: usize = 0;
|
if self.1 {
|
||||||
CCCryptorUpdate(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast(), data.len(), &mut data_out_written);
|
assert_eq!(CCCryptorGCMEncrypt(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast()), 0);
|
||||||
assert_eq!(data_out_written, data.len());
|
} else {
|
||||||
|
assert_eq!(CCCryptorGCMDecrypt(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast()), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn finish(&mut self) -> [u8; 16] {
|
pub fn finish(&mut self) -> [u8; 16] {
|
||||||
let mut tag = [0_u8; 16];
|
let mut tag = 0_u128.to_ne_bytes();
|
||||||
unsafe {
|
unsafe {
|
||||||
if CCCryptorGCMFinalize(self.0, tag.as_mut_ptr().cast(), 16) != 0 {
|
let mut tag_len = 16;
|
||||||
|
if CCCryptorGCMFinal(self.0, tag.as_mut_ptr().cast(), &mut tag_len) != 0 {
|
||||||
|
debug_assert!(false);
|
||||||
tag.fill(0);
|
tag.fill(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,7 +326,17 @@ mod openssl {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn init(&mut self, iv: &[u8]) {
|
pub fn init(&mut self, iv: &[u8]) {
|
||||||
assert_eq!(iv.len(), 12);
|
assert_eq!(iv.len(), 12);
|
||||||
let mut c = Crypter::new(aes_gcm_by_key_size(self.1), if self.3 { Mode::Encrypt } else { Mode::Decrypt }, &self.0 .0[..self.1], Some(iv)).unwrap();
|
let mut c = Crypter::new(
|
||||||
|
aes_gcm_by_key_size(self.1),
|
||||||
|
if self.3 {
|
||||||
|
Mode::Encrypt
|
||||||
|
} else {
|
||||||
|
Mode::Decrypt
|
||||||
|
},
|
||||||
|
&self.0 .0[..self.1],
|
||||||
|
Some(iv),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
c.pad(false);
|
c.pad(false);
|
||||||
let _ = self.2.replace(c);
|
let _ = self.2.replace(c);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub mod aes_gmac_siv;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod hex;
|
pub mod hex;
|
||||||
pub mod kbkdf;
|
pub mod kbkdf;
|
||||||
|
pub mod noise;
|
||||||
pub mod p384;
|
pub mod p384;
|
||||||
pub mod poly1305;
|
pub mod poly1305;
|
||||||
pub mod random;
|
pub mod random;
|
||||||
|
@ -13,4 +14,6 @@ pub mod secret;
|
||||||
pub mod varint;
|
pub mod varint;
|
||||||
pub mod x25519;
|
pub mod x25519;
|
||||||
|
|
||||||
|
pub use pqc_kyber;
|
||||||
|
|
||||||
pub const ZEROES: [u8; 16] = [0_u8; 16];
|
pub const ZEROES: [u8; 16] = [0_u8; 16];
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use zerotier_core_crypto::aes::{Aes, AesGcm};
|
use crate::aes::{Aes, AesGcm};
|
||||||
use zerotier_core_crypto::hash::{hmac_sha384, hmac_sha512, SHA384, SHA512};
|
use crate::hash::{hmac_sha384, hmac_sha512, SHA384, SHA512};
|
||||||
use zerotier_core_crypto::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE};
|
use crate::p384::{P384KeyPair, P384PublicKey, P384_PUBLIC_KEY_SIZE};
|
||||||
use zerotier_core_crypto::random;
|
use crate::random;
|
||||||
use zerotier_core_crypto::secret::Secret;
|
use crate::secret::Secret;
|
||||||
|
|
||||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||||
|
|
||||||
|
@ -163,7 +163,6 @@ impl Obfuscator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub enum ReceiveResult<'a, O> {
|
pub enum ReceiveResult<'a, O> {
|
||||||
/// Packet is valid and contained a data payload.
|
/// Packet is valid and contained a data payload.
|
||||||
OkData(&'a [u8]),
|
OkData(&'a [u8]),
|
||||||
|
@ -242,7 +241,7 @@ pub struct Session<O> {
|
||||||
remote_s_public_hash: [u8; 48],
|
remote_s_public_hash: [u8; 48],
|
||||||
psk: Secret<64>,
|
psk: Secret<64>,
|
||||||
ss: Secret<48>,
|
ss: Secret<48>,
|
||||||
outgoing_obfuscator: Obfuscator,
|
pub(self) outgoing_obfuscator: Obfuscator,
|
||||||
state: RwLock<State>,
|
state: RwLock<State>,
|
||||||
remote_s_public_p384: [u8; P384_PUBLIC_KEY_SIZE],
|
remote_s_public_p384: [u8; P384_PUBLIC_KEY_SIZE],
|
||||||
|
|
||||||
|
@ -262,13 +261,23 @@ impl<O> Session<O> {
|
||||||
///
|
///
|
||||||
/// This must be checked often enough to ensure that the hard key usage limit is not reached, which in the
|
/// This must be checked often enough to ensure that the hard key usage limit is not reached, which in the
|
||||||
/// usual UDP use case means once every ~3TiB of traffic.
|
/// usual UDP use case means once every ~3TiB of traffic.
|
||||||
#[allow(unused)]
|
|
||||||
pub fn rekey_check<'a, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(&self, buffer: &'a mut [u8; MAX_PACKET_SIZE], local_s_public: &[u8; STATIC_PUBLIC_SIZE], current_time: i64, force: bool, jedi: bool) -> Option<&'a [u8]> {
|
pub fn rekey_check<'a, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(&self, buffer: &'a mut [u8; MAX_PACKET_SIZE], local_s_public: &[u8; STATIC_PUBLIC_SIZE], current_time: i64, force: bool, jedi: bool) -> Option<&'a [u8]> {
|
||||||
let state = self.state.upgradable_read();
|
let state = self.state.upgradable_read();
|
||||||
if let Some(key) = state.keys[0].as_ref() {
|
if let Some(key) = state.keys[0].as_ref() {
|
||||||
if force || (key.lifetime.should_rekey(self.send_counter.current(), current_time) && state.offer.as_ref().map_or(true, |o| (current_time - o.creation_time) > OFFER_RATE_LIMIT_MS)) {
|
if force || (key.lifetime.should_rekey(self.send_counter.current(), current_time) && state.offer.as_ref().map_or(true, |o| (current_time - o.creation_time) > OFFER_RATE_LIMIT_MS)) {
|
||||||
if let Some(remote_s_public_p384) = P384PublicKey::from_bytes(&self.remote_s_public_p384) {
|
if let Some(remote_s_public_p384) = P384PublicKey::from_bytes(&self.remote_s_public_p384) {
|
||||||
if let Some((offer, psize)) = EphemeralOffer::create_alice_offer(buffer, self.send_counter.next(), self.id, state.remote_session_id, local_s_public, &remote_s_public_p384, &self.ss, &self.outgoing_obfuscator, current_time, jedi) {
|
if let Some((offer, psize)) = EphemeralOffer::create_alice_offer(
|
||||||
|
buffer,
|
||||||
|
self.send_counter.next(),
|
||||||
|
self.id,
|
||||||
|
state.remote_session_id,
|
||||||
|
local_s_public,
|
||||||
|
&remote_s_public_p384,
|
||||||
|
&self.ss,
|
||||||
|
&self.outgoing_obfuscator,
|
||||||
|
current_time,
|
||||||
|
jedi,
|
||||||
|
) {
|
||||||
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
||||||
let _ = state.offer.replace(offer);
|
let _ = state.offer.replace(offer);
|
||||||
return Some(&buffer[..psize]);
|
return Some(&buffer[..psize]);
|
||||||
|
@ -281,7 +290,6 @@ impl<O> Session<O> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new session and return this plus an outgoing packet to send to the other end.
|
/// Create a new session and return this plus an outgoing packet to send to the other end.
|
||||||
#[allow(unused)]
|
|
||||||
pub fn new_session<'a, O, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
|
pub fn new_session<'a, O, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
|
||||||
buffer: &'a mut [u8; MAX_PACKET_SIZE],
|
buffer: &'a mut [u8; MAX_PACKET_SIZE],
|
||||||
local_session_id: SessionId,
|
local_session_id: SessionId,
|
||||||
|
@ -325,7 +333,6 @@ pub fn new_session<'a, O, const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE
|
||||||
/// Receive a packet from the network and take the appropriate action.
|
/// Receive a packet from the network and take the appropriate action.
|
||||||
///
|
///
|
||||||
/// Check ReceiveResult to see if it includes data or a reply packet.
|
/// Check ReceiveResult to see if it includes data or a reply packet.
|
||||||
#[allow(unused)]
|
|
||||||
pub fn receive<
|
pub fn receive<
|
||||||
'a,
|
'a,
|
||||||
ExtractP384PublicKeyFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<P384PublicKey>,
|
ExtractP384PublicKeyFunction: FnOnce(&[u8; STATIC_PUBLIC_SIZE]) -> Option<P384PublicKey>,
|
||||||
|
@ -419,19 +426,24 @@ pub fn receive<
|
||||||
return Err(Error::InvalidPacket);
|
return Err(Error::InvalidPacket);
|
||||||
}
|
}
|
||||||
let payload_end = incoming_packet.len() - (AES_GCM_TAG_SIZE + HMAC_SIZE);
|
let payload_end = incoming_packet.len() - (AES_GCM_TAG_SIZE + HMAC_SIZE);
|
||||||
|
let aes_gcm_tag_end = incoming_packet.len() - HMAC_SIZE;
|
||||||
|
|
||||||
match packet_type {
|
match packet_type {
|
||||||
PACKET_TYPE_KEY_OFFER => {
|
PACKET_TYPE_KEY_OFFER => {
|
||||||
// alice (remote) -> bob (local)
|
// alice (remote) -> bob (local)
|
||||||
|
|
||||||
let (alice_e0_public, e0s) = P384PublicKey::from_bytes(&buffer[HEADER_SIZE..HEADER_SIZE + P384_PUBLIC_KEY_SIZE]).and_then(|pk| local_s_keypair_p384.agree(&pk).map(move |s| (pk, s))).ok_or(Error::FailedAuthentication)?;
|
let (alice_e0_public, e0s) = P384PublicKey::from_bytes(&buffer[HEADER_SIZE..HEADER_SIZE + P384_PUBLIC_KEY_SIZE])
|
||||||
|
.and_then(|pk| local_s_keypair_p384.agree(&pk).map(move |s| (pk, s)))
|
||||||
|
.ok_or(Error::FailedAuthentication)?;
|
||||||
|
|
||||||
let key = Secret(hmac_sha512(&hmac_sha512(&KEY_DERIVATION_CHAIN_STARTING_SALT, alice_e0_public.as_bytes()), e0s.as_bytes()));
|
let key = Secret(hmac_sha512(&hmac_sha512(&KEY_DERIVATION_CHAIN_STARTING_SALT, alice_e0_public.as_bytes()), e0s.as_bytes()));
|
||||||
|
|
||||||
|
let original_ciphertext = buffer.clone();
|
||||||
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<32>(), false);
|
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<32>(), false);
|
||||||
c.init(&get_aes_gcm_nonce(buffer));
|
c.init(&get_aes_gcm_nonce(buffer));
|
||||||
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end]);
|
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end]);
|
||||||
if !c.finish().eq(&buffer[payload_end..(payload_end + AES_GCM_TAG_SIZE)]) {
|
let c = c.finish();
|
||||||
|
if !c.eq(&buffer[payload_end..aes_gcm_tag_end]) {
|
||||||
return Err(Error::FailedAuthentication);
|
return Err(Error::FailedAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,7 +461,7 @@ pub fn receive<
|
||||||
|
|
||||||
let key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes()));
|
let key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes()));
|
||||||
|
|
||||||
if !hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &buffer[..(payload_end + AES_GCM_TAG_SIZE)]).eq(&buffer[(payload_end + AES_GCM_TAG_SIZE)..(payload_end + AES_GCM_TAG_SIZE + HMAC_SIZE)]) {
|
if !hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &original_ciphertext[..aes_gcm_tag_end]).eq(&buffer[aes_gcm_tag_end..incoming_packet.len()]) {
|
||||||
return Err(Error::FailedAuthentication);
|
return Err(Error::FailedAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,7 +471,7 @@ pub fn receive<
|
||||||
let e0e0 = bob_e0_keypair.agree(&alice_e0_public).ok_or(Error::FailedAuthentication)?;
|
let e0e0 = bob_e0_keypair.agree(&alice_e0_public).ok_or(Error::FailedAuthentication)?;
|
||||||
let se0 = bob_e0_keypair.agree(&alice_s_public_p384).ok_or(Error::FailedAuthentication)?;
|
let se0 = bob_e0_keypair.agree(&alice_s_public_p384).ok_or(Error::FailedAuthentication)?;
|
||||||
|
|
||||||
let new_session = if let Some(session) = session.as_ref() {
|
let new_session = if session.is_some() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
if let Some((local_session_id, psk, associated_object)) = new_session_auth(&alice_s_public) {
|
if let Some((local_session_id, psk, associated_object)) = new_session_auth(&alice_s_public) {
|
||||||
|
@ -489,7 +501,10 @@ pub fn receive<
|
||||||
// NIST/FIPS allows HKDF with HMAC(salt, key) and salt is allowed to be anything. This way if the PSK is not
|
// NIST/FIPS allows HKDF with HMAC(salt, key) and salt is allowed to be anything. This way if the PSK is not
|
||||||
// FIPS compliant the compliance of the entire key derivation is not invalidated. Both inputs are secrets of
|
// FIPS compliant the compliance of the entire key derivation is not invalidated. Both inputs are secrets of
|
||||||
// fixed size so this shouldn't matter cryptographically.
|
// fixed size so this shouldn't matter cryptographically.
|
||||||
let key = Secret(hmac_sha512(session.psk.as_bytes(), &hmac_sha512(&hmac_sha512(&hmac_sha512(key.as_bytes(), bob_e0_keypair.public_key_bytes()), e0e0.as_bytes()), se0.as_bytes())));
|
let key = Secret(hmac_sha512(
|
||||||
|
session.psk.as_bytes(),
|
||||||
|
&hmac_sha512(&hmac_sha512(&hmac_sha512(key.as_bytes(), bob_e0_keypair.public_key_bytes()), e0e0.as_bytes()), se0.as_bytes()),
|
||||||
|
));
|
||||||
|
|
||||||
// At this point we've completed Noise_IK key derivation with NIST P-384 ECDH, but see final step below...
|
// At this point we've completed Noise_IK key derivation with NIST P-384 ECDH, but see final step below...
|
||||||
|
|
||||||
|
@ -509,7 +524,8 @@ pub fn receive<
|
||||||
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::<32>(), true);
|
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::<32>(), true);
|
||||||
c.init(&get_aes_gcm_nonce(buffer));
|
c.init(&get_aes_gcm_nonce(buffer));
|
||||||
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..reply_size]);
|
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..reply_size]);
|
||||||
buffer[reply_size..(reply_size + AES_GCM_TAG_SIZE)].copy_from_slice(&c.finish());
|
let c = c.finish();
|
||||||
|
buffer[reply_size..(reply_size + AES_GCM_TAG_SIZE)].copy_from_slice(&c);
|
||||||
reply_size += AES_GCM_TAG_SIZE;
|
reply_size += AES_GCM_TAG_SIZE;
|
||||||
|
|
||||||
// Normal Noise_IK is done, but we have one more step: mix in the Kyber shared secret (or all zeroes if Kyber is
|
// Normal Noise_IK is done, but we have one more step: mix in the Kyber shared secret (or all zeroes if Kyber is
|
||||||
|
@ -543,19 +559,26 @@ pub fn receive<
|
||||||
if let Some(session) = session {
|
if let Some(session) = session {
|
||||||
let state = session.state.upgradable_read();
|
let state = session.state.upgradable_read();
|
||||||
if let Some(offer) = state.offer.as_ref() {
|
if let Some(offer) = state.offer.as_ref() {
|
||||||
let (bob_e0_public, e0e0) = P384PublicKey::from_bytes(&buffer[HEADER_SIZE..(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)]).and_then(|pk| offer.alice_e0_keypair.agree(&pk).map(move |s| (pk, s))).ok_or(Error::FailedAuthentication)?;
|
let (bob_e0_public, e0e0) = P384PublicKey::from_bytes(&buffer[HEADER_SIZE..(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)])
|
||||||
|
.and_then(|pk| offer.alice_e0_keypair.agree(&pk).map(move |s| (pk, s)))
|
||||||
|
.ok_or(Error::FailedAuthentication)?;
|
||||||
let se0 = local_s_keypair_p384.agree(&bob_e0_public).ok_or(Error::FailedAuthentication)?;
|
let se0 = local_s_keypair_p384.agree(&bob_e0_public).ok_or(Error::FailedAuthentication)?;
|
||||||
|
|
||||||
let key = Secret(hmac_sha512(session.psk.as_bytes(), &hmac_sha512(&hmac_sha512(&hmac_sha512(offer.key.as_bytes(), bob_e0_public.as_bytes()), e0e0.as_bytes()), se0.as_bytes())));
|
let key = Secret(hmac_sha512(
|
||||||
|
session.psk.as_bytes(),
|
||||||
|
&hmac_sha512(&hmac_sha512(&hmac_sha512(offer.key.as_bytes(), bob_e0_public.as_bytes()), e0e0.as_bytes()), se0.as_bytes()),
|
||||||
|
));
|
||||||
|
|
||||||
|
let original_ciphertext = buffer.clone();
|
||||||
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::<32>(), false);
|
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE).first_n::<32>(), false);
|
||||||
c.init(&get_aes_gcm_nonce(buffer));
|
c.init(&get_aes_gcm_nonce(buffer));
|
||||||
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end]);
|
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end]);
|
||||||
if !c.finish().eq(&buffer[payload_end..(payload_end + AES_GCM_TAG_SIZE)]) {
|
let c = c.finish();
|
||||||
|
if !c.eq(&buffer[payload_end..aes_gcm_tag_end]) {
|
||||||
return Err(Error::FailedAuthentication);
|
return Err(Error::FailedAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alice has now completed Noise_IK for P-384, now for the hybrid part.
|
// Alice has now completed Noise_IK for P-384 and verified with GCM auth, now for the hybrid add-on.
|
||||||
|
|
||||||
let (bob_session_id, bob_e1_public) = parse_KEY_COUNTER_OFFER_after_header(&buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end])?;
|
let (bob_session_id, bob_e1_public) = parse_KEY_COUNTER_OFFER_after_header(&buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..payload_end])?;
|
||||||
|
|
||||||
|
@ -571,7 +594,7 @@ pub fn receive<
|
||||||
|
|
||||||
let key = Secret(hmac_sha512(e1e1.as_bytes(), key.as_bytes()));
|
let key = Secret(hmac_sha512(e1e1.as_bytes(), key.as_bytes()));
|
||||||
|
|
||||||
if !hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &buffer[..(payload_end + AES_GCM_TAG_SIZE)]).eq(&buffer[(payload_end + AES_GCM_TAG_SIZE)..(payload_end + AES_GCM_TAG_SIZE + HMAC_SIZE)]) {
|
if !hmac_sha384(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_HMAC).first_n::<48>(), &original_ciphertext[..aes_gcm_tag_end]).eq(&buffer[aes_gcm_tag_end..incoming_packet.len()]) {
|
||||||
return Err(Error::FailedAuthentication);
|
return Err(Error::FailedAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -669,12 +692,11 @@ impl KeyLifetime {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn expired(&self, counter: CounterValue) -> bool {
|
fn expired(&self, counter: CounterValue) -> bool {
|
||||||
counter.0 < self.hard_expire_at_counter
|
counter.0 >= self.hard_expire_at_counter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ephemeral offer sent with KEY_OFFER and rememebered so state can be reconstructed on COUNTER_OFFER.
|
/// Ephemeral offer sent with KEY_OFFER and rememebered so state can be reconstructed on COUNTER_OFFER.
|
||||||
#[allow(unused)]
|
|
||||||
struct EphemeralOffer {
|
struct EphemeralOffer {
|
||||||
creation_time: i64,
|
creation_time: i64,
|
||||||
key: Secret<64>,
|
key: Secret<64>,
|
||||||
|
@ -709,10 +731,12 @@ impl EphemeralOffer {
|
||||||
|
|
||||||
let mut packet_size = assemble_KEY_OFFER(buffer, counter, bob_session_id, alice_e0_keypair.public_key(), alice_session_id, alice_s_public, alice_e1_keypair.as_ref().map(|s| &s.public));
|
let mut packet_size = assemble_KEY_OFFER(buffer, counter, bob_session_id, alice_e0_keypair.public_key(), alice_session_id, alice_s_public, alice_e1_keypair.as_ref().map(|s| &s.public));
|
||||||
|
|
||||||
|
debug_assert!(packet_size <= MAX_PACKET_SIZE);
|
||||||
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<32>(), true);
|
let mut c = AesGcm::new(kbkdf512(key.as_bytes(), KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB).first_n::<32>(), true);
|
||||||
c.init(&get_aes_gcm_nonce(buffer));
|
c.init(&get_aes_gcm_nonce(buffer));
|
||||||
c.crypt_in_place(&mut buffer[HEADER_SIZE + P384_PUBLIC_KEY_SIZE..packet_size]);
|
c.crypt_in_place(&mut buffer[(HEADER_SIZE + P384_PUBLIC_KEY_SIZE)..packet_size]);
|
||||||
buffer[packet_size..packet_size + AES_GCM_TAG_SIZE].copy_from_slice(&c.finish());
|
let c = c.finish();
|
||||||
|
buffer[packet_size..packet_size + AES_GCM_TAG_SIZE].copy_from_slice(&c);
|
||||||
packet_size += AES_GCM_TAG_SIZE;
|
packet_size += AES_GCM_TAG_SIZE;
|
||||||
|
|
||||||
let key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes()));
|
let key = Secret(hmac_sha512(key.as_bytes(), ss.as_bytes()));
|
||||||
|
@ -733,7 +757,7 @@ impl EphemeralOffer {
|
||||||
alice_e0_keypair,
|
alice_e0_keypair,
|
||||||
alice_e1_keypair,
|
alice_e1_keypair,
|
||||||
},
|
},
|
||||||
packet_size + AES_GCM_TAG_SIZE + HMAC_SIZE,
|
packet_size,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -823,6 +847,16 @@ fn assemble_and_armor_DATA<const MAX_PACKET_SIZE: usize>(buffer: &mut [u8; MAX_P
|
||||||
Ok(tag_end)
|
Ok(tag_end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn append_random_padding(b: &mut [u8]) -> &mut [u8] {
|
||||||
|
if b.len() > AES_GCM_TAG_SIZE + HMAC_SIZE {
|
||||||
|
let random_padding_len = (random::next_u32_secure() as usize) % (b.len() - (AES_GCM_TAG_SIZE + HMAC_SIZE));
|
||||||
|
b[..random_padding_len].fill(0);
|
||||||
|
&mut b[random_padding_len..]
|
||||||
|
} else {
|
||||||
|
b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn assemble_KEY_OFFER<const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
|
fn assemble_KEY_OFFER<const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: usize>(
|
||||||
buffer: &mut [u8; MAX_PACKET_SIZE],
|
buffer: &mut [u8; MAX_PACKET_SIZE],
|
||||||
|
@ -860,11 +894,9 @@ fn assemble_KEY_OFFER<const MAX_PACKET_SIZE: usize, const STATIC_PUBLIC_SIZE: us
|
||||||
b[1] = 0; // reserved for future use
|
b[1] = 0; // reserved for future use
|
||||||
b = &mut b[2..];
|
b = &mut b[2..];
|
||||||
|
|
||||||
let random_padding_len = (random::next_u32_secure() as usize) % (MAX_PACKET_SIZE - (b.len() + AES_GCM_TAG_SIZE + HMAC_SIZE));
|
b = append_random_padding(b);
|
||||||
random::fill_bytes_secure(&mut b[..random_padding_len]);
|
|
||||||
b = &mut b[random_padding_len..];
|
|
||||||
|
|
||||||
b.len()
|
MAX_PACKET_SIZE - b.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
@ -931,11 +963,9 @@ fn assemble_KEY_COUNTER_OFFER<const MAX_PACKET_SIZE: usize>(
|
||||||
b[1] = 0; // reserved for future use
|
b[1] = 0; // reserved for future use
|
||||||
b = &mut b[2..];
|
b = &mut b[2..];
|
||||||
|
|
||||||
let random_padding_len = (random::next_u32_secure() as usize) % (MAX_PACKET_SIZE - (b.len() + AES_GCM_TAG_SIZE + HMAC_SIZE));
|
b = append_random_padding(b);
|
||||||
random::fill_bytes_secure(&mut b[..random_padding_len]);
|
|
||||||
b = &mut b[random_padding_len..];
|
|
||||||
|
|
||||||
b.len()
|
MAX_PACKET_SIZE - b.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
@ -972,10 +1002,161 @@ fn kbkdf512(key: &[u8], label: u8) -> Secret<64> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn get_aes_gcm_nonce(deobfuscated_packet: &[u8]) -> [u8; 16] {
|
fn get_aes_gcm_nonce(deobfuscated_packet: &[u8]) -> [u8; 16] {
|
||||||
let mut tmp = 0_u128.to_ne_bytes();
|
let mut tmp = 0_u128.to_ne_bytes();
|
||||||
tmp[..HEADER_SIZE].copy_from_slice(deobfuscated_packet);
|
tmp[..HEADER_SIZE].copy_from_slice(&deobfuscated_packet[..HEADER_SIZE]);
|
||||||
tmp
|
tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cold]
|
#[cold]
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
extern "C" fn unlikely_branch() {}
|
extern "C" fn unlikely_branch() {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alice_bob() {
|
||||||
|
let psk: Secret<64> = Secret::default();
|
||||||
|
let mut a_buffer = [0_u8; 1500];
|
||||||
|
let mut b_buffer = [0_u8; 1500];
|
||||||
|
let alice_static_keypair = P384KeyPair::generate();
|
||||||
|
let bob_static_keypair = P384KeyPair::generate();
|
||||||
|
let outgoing_obfuscator_to_alice = Obfuscator::new(alice_static_keypair.public_key_bytes());
|
||||||
|
let outgoing_obfuscator_to_bob = Obfuscator::new(bob_static_keypair.public_key_bytes());
|
||||||
|
|
||||||
|
let mut from_alice: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut from_bob: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
|
// Session TO Bob, on Alice's side.
|
||||||
|
let (alice, packet) = new_session(
|
||||||
|
&mut a_buffer,
|
||||||
|
SessionId::new_random(),
|
||||||
|
alice_static_keypair.public_key_bytes(),
|
||||||
|
&alice_static_keypair,
|
||||||
|
bob_static_keypair.public_key_bytes(),
|
||||||
|
bob_static_keypair.public_key(),
|
||||||
|
&psk,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let alice = Rc::new(alice);
|
||||||
|
from_alice.push(packet.to_vec());
|
||||||
|
|
||||||
|
// Session FROM Alice, on Bob's side.
|
||||||
|
let mut bob: Option<Rc<Session<u32>>> = None;
|
||||||
|
|
||||||
|
while !from_alice.is_empty() || !from_bob.is_empty() {
|
||||||
|
if let Some(packet) = from_alice.pop() {
|
||||||
|
let r = receive(
|
||||||
|
packet.as_slice(),
|
||||||
|
&mut b_buffer,
|
||||||
|
&bob_static_keypair,
|
||||||
|
&outgoing_obfuscator_to_bob,
|
||||||
|
|p: &[u8; P384_PUBLIC_KEY_SIZE]| P384PublicKey::from_bytes(p),
|
||||||
|
|sid| {
|
||||||
|
if let Some(bob) = bob.as_ref() {
|
||||||
|
if sid == bob.id {
|
||||||
|
Some(bob.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|_: &[u8; P384_PUBLIC_KEY_SIZE]| {
|
||||||
|
if bob.is_none() {
|
||||||
|
Some((SessionId::new_random(), psk.clone(), 0))
|
||||||
|
} else {
|
||||||
|
panic!("Bob received a second new session request from Alice");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if let Ok(r) = r {
|
||||||
|
match r {
|
||||||
|
ReceiveResult::OkData(_) => {
|
||||||
|
println!("alice->bob DATA");
|
||||||
|
}
|
||||||
|
ReceiveResult::OkSendReply(p) => {
|
||||||
|
from_bob.push(p.to_vec());
|
||||||
|
}
|
||||||
|
ReceiveResult::OkNewSession(ns, p) => {
|
||||||
|
if bob.is_some() {
|
||||||
|
panic!("attempt to create new session on Bob's side when he already has one");
|
||||||
|
}
|
||||||
|
let _ = bob.replace(Rc::new(ns));
|
||||||
|
from_bob.push(p.to_vec());
|
||||||
|
println!("alice->bob NEW SESSION established!");
|
||||||
|
}
|
||||||
|
ReceiveResult::Ok => {
|
||||||
|
println!("alice->bob OK");
|
||||||
|
}
|
||||||
|
ReceiveResult::Duplicate => {
|
||||||
|
println!("alice->bob duplicate packet");
|
||||||
|
}
|
||||||
|
ReceiveResult::Ignored => {
|
||||||
|
println!("alice->bob ignored packet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("ERROR (alice->bob): {}", r.err().unwrap().to_string());
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(packet) = from_bob.pop() {
|
||||||
|
let r = receive(
|
||||||
|
packet.as_slice(),
|
||||||
|
&mut b_buffer,
|
||||||
|
&alice_static_keypair,
|
||||||
|
&outgoing_obfuscator_to_alice,
|
||||||
|
|p: &[u8; P384_PUBLIC_KEY_SIZE]| P384PublicKey::from_bytes(p),
|
||||||
|
|sid| {
|
||||||
|
if sid == alice.id {
|
||||||
|
Some(alice.clone())
|
||||||
|
} else {
|
||||||
|
panic!("received from Bob addressed to unknown session ID, not Alice");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|_: &[u8; P384_PUBLIC_KEY_SIZE]| {
|
||||||
|
panic!("Alice received an unexpected new session request from Bob");
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if let Ok(r) = r {
|
||||||
|
match r {
|
||||||
|
ReceiveResult::OkData(_) => {
|
||||||
|
println!("bob->alice DATA");
|
||||||
|
}
|
||||||
|
ReceiveResult::OkSendReply(p) => {
|
||||||
|
from_alice.push(p.to_vec());
|
||||||
|
}
|
||||||
|
ReceiveResult::OkNewSession(_, _) => {
|
||||||
|
panic!("attempt to create new session on Alice's side; Bob should not initiate");
|
||||||
|
}
|
||||||
|
ReceiveResult::Ok => {
|
||||||
|
println!("bob->alice OK");
|
||||||
|
}
|
||||||
|
ReceiveResult::Duplicate => {
|
||||||
|
println!("bob->alice duplicate packet");
|
||||||
|
}
|
||||||
|
ReceiveResult::Ignored => {
|
||||||
|
println!("bob->alice ignored packet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("ERROR (bob->alice): {}", r.err().unwrap().to_string());
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,6 @@ debug_events = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zerotier-core-crypto = { path = "../zerotier-core-crypto" }
|
zerotier-core-crypto = { path = "../zerotier-core-crypto" }
|
||||||
pqc_kyber = { path = "../third_party/kyber", features = ["kyber512", "reference"], default-features = false }
|
|
||||||
async-trait = "^0"
|
async-trait = "^0"
|
||||||
base64 = "^0"
|
base64 = "^0"
|
||||||
lz4_flex = { version = "^0", features = ["safe-encode", "safe-decode", "checked-decode"] }
|
lz4_flex = { version = "^0", features = ["safe-encode", "safe-decode", "checked-decode"] }
|
||||||
|
|
|
@ -8,7 +8,6 @@ mod fragmentedpacket;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod inetaddress;
|
mod inetaddress;
|
||||||
mod mac;
|
mod mac;
|
||||||
mod noise;
|
|
||||||
mod path;
|
mod path;
|
||||||
mod peer;
|
mod peer;
|
||||||
mod rootset;
|
mod rootset;
|
||||||
|
|
Loading…
Add table
Reference in a new issue