Tweak V1 identity PoW, rewrite root set to be cleaner, docs, warning removal, test fix in AES-GMAC-SIV.

This commit is contained in:
Adam Ierymenko 2021-11-05 15:30:54 -04:00
parent 986641221d
commit 39aeab6819
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
12 changed files with 392 additions and 355 deletions

View file

@ -2,6 +2,7 @@
name = "aes-gmac-siv"
version = "0.1.0"
edition = "2018"
license = "BSD"
[profile.release]
opt-level = 3

View file

@ -3,39 +3,37 @@ AES-GMAC-SIV
Rust implementation for ZeroTier 2.0.
*WARNING: while this construction has been subjected to peer review, this code has not (yet!). Use at your own risk.*
*WARNING: while the AES-GMAC-SIV construction has been subjected to peer review, this code has not (yet!). Use at your own risk.*
## Introduction
AES-GMAC-SIV is a "synthetic IV" (SIV) cipher construction implemented using only FIPS140 and NIST accepted cryptographic building blocks: AES-ECB (single block), AES-CTR, and GMAC (the MAC part of GCM, which can also be used separately).
AES-GMAC-SIV is a "synthetic IV" (SIV) cipher construction implemented using only FIPS140 and NIST accepted cryptographic building blocks: AES-ECB (used to mix input IV and GMAC result), AES-CTR, and GMAC (the MAC part of GCM, which can also be used separately).
AES-GMAC-SIV is almost identical to [AES-GCM-SIV](https://en.wikipedia.org/wiki/AES-GCM-SIV), but that mode uses a non-standard MAC called POLYVAL in place of GMAC. POLYVAL is basically little-endian GMAC but the fact that it is not standard GMAC means it's not found in most cryptographic libraries and is not approved by FIPS140 and many other sets of compliance guidelines.
This also contains a simple AES-CTR wrapper for convenience.
## Why SIV? Why not just GCM?
Stream ciphers like AES-CTR, ChaCha20, and others require a number called an initialization vector (IV) for each use. The IV is sometimes called a nonce, or *number used once*, because using the same value for different messages with the same key is a major no-no.
Repeating an IV/nonce with the same key allows both messages to be decrypted. This is because XOR, which is used to apply the stream cipher's pseudorandom bits as a one time pad, is commutative. Repeating a nonce can in some cases also allow an attacker to attack the MAC (e.g. GMAC or Poly1305) and forge messages that will appear valid. In many systems compromising the MAC is more serious than compromising encryption for a few messages.
Repeating an IV/nonce with the same key allows both messages to be decrypted. This is because XOR, which is used to apply the stream cipher's pseudorandom bits as a one time pad, is commutative. Repeating a nonce can in some cases also allow an attacker to attack the MAC (e.g. GMAC or Poly1305) and forge messages that will appear valid. Message forgery is often more serious than loss of plaintext secrecy for a few messages as it permits active attacks.
SIV modes provide strong protection against IV reuse by generating a *synthetic IV* from the plaintext. This means that two different plaintexts will almost certainly perform encryption using different IVs even if the input IV is duplicated.
SIV modes provide strong protection against IV reuse by generating a *synthetic IV* from the plaintext. This means that two different plaintexts will result in different IVs even if the input IV is duplicated.
With SIV a duplicate IV has no effect at all except in the case where the same IV is used to encrypt the same message twice. In this case the encrypted messages would also be identical, revealing that a duplicate was sent, but because both the IV and message are the same this would not compromise security like IV reuse does in standard modes.
SIV modes could be used with no external IV at all without compromising plaintext secrecy, but this is not recommended since it would leak message duplication. An external IV is supplied in this and other common SIV constructions to avoid this, since it causes duplicate messages to result in entirely different ciphertexts.
With SIV a duplicate IV has no effect at all except in the case where the same IV is used to encrypt the same message twice. In this case the encrypted messages would also be identical, revealing that a duplicate was sent, but because both the IV and message are the same this would not compromise security like IV reuse does in standard modes. It's technically possible to use SIV modes with no IV at all without compromising plaintext secrecy, but this isn't recommended as it would still always leak message duplication.
We recommend treating AES-GMAC-SIV (and other SIV modes) as if they were normal stream ciphers and endeavoring to make the IV unique as those would require.
SIV modes might seem like paranoia, but accidental IV reuse is easier than you might think. Here's a few scenarios where it might happen:
* Live cloning of virtual machines or application state, resulting in two clones with identical random number generator states.
* Embedded devices that initialize PRNGs from deterministic sources.
* Forgetting to use atomics or a mutex to synchronize an IV counter variable in multithreaded code.
* Edge case memory model differences between processors affecting synchronization of an IV counter variable.
* Concurrent use of a non-thread-safe random number generator.
* Multiple dependencies in a project initializing or using a random source from a common shared library that uses static state.
* Live cloning of virtual machines or application state, resulting in two clones with identical random number generator states.
* Memory errors or other bugs that break IV generation. If these can be triggered remotely, this could be an attack vector.
* Time changes if the clock is used as an input to generate the IV.
* Multiple dependencies in a project stomping on the same random number generator state.
* Memory errors that corrupt an IV counter variable, especially if they can be triggered remotely.
* "Rowhammer" and similar hardware attacks targeting the IV counter.
* Time changes or attacks against NTP if a clock is used as input in genreating an IV.
... and so on. "Sudden death" on IV re-use is a foot-gun that's worth removing.
@ -56,13 +54,13 @@ Per-message parameters:
Encryption steps:
1. Pad 64-bit IV to 96 bits and reset GMAC for next message.
1. Pad 64-bit IV to 96 bits and use it to initialize GMAC.
2. Feed AAD (if any) into GMAC.
3. Pad AAD length to a multiple of 16 by feeding zeroes into GMAC.
3. Pad AAD length to a multiple of 16 by feeding zeroes into GMAC to ensure unique encoding.
4. Feed plaintext into GMAC to compute final MAC.
5. XOR lower 64 bits and higher 64 bits of GMAC tag.
6. Concatenate IV and 64-bit shortened tag to form a 128-bit block.
7. AES-ECB encrypt this IV+tag to yield message tag and AES-CTR IV.
6. Concatenate original input IV and 64-bit shortened tag to form a 128-bit block.
7. AES-ECB encrypt this IV+tag, yielding an opaque 128-bit message tag and AES-CTR IV.
8. Clear bit 31 (from the right) in the tag and use this to initialize AES-CTR. Bit 31 is cleared so AES-CTR implementations that use a 32-bit counter will not overflow for messages less than 2^31 bytes in length.
9. Encrypt plaintext with AES-CTR.
@ -70,27 +68,30 @@ The message tag is the 128-bit encrypted block from step 7 before bit 31 is clea
Decryption steps:
1. Initialize AES-CTR with the tag (with bit 31 cleared as in step 8 above).
1. Initialize AES-CTR with the tag after clearning bit 31 as in step 8 above.
2. Decrypt ciphertext with AES-CTR.
3. AES-ECB *decrypt* 128-bit tag to yield original IV and 64-bit shortened GMAC tag.
4. Initialize GMAC as in encryption step 1.
4. Initialize GMAC as in encryption step 1 using first 64 bits of the decrypted message tag from step 3.
5. Feed AAD into GMAC (if any).
6. Zero-pad AAD to a multiple of 16 as in encryption.
7. Feed *decrypted plaintext* into GMAC.
8. Generate GMAC tag, XOR least and most significant 64 bits, and check MAC. Discard packet if these do not match.
Note that while MAC comparison is only 64 bits, this MAC is concealed within an encrypted block that mixes its bits with the IV. This helps prevent an attacker from directly attacking GMAC or attempting to exploit an oracle. An attacker could only detect a 64-bit MAC collision if the IV was also duplicated, which is another reason to supply unique IVs.
Notes:
* The extra step (7 in encrypt, 3 in decrypt) of AES-ECB encrypting (mixing) the bits of the input IV and GMAC tag is there because two identical plaintexts would yield identical GMAC tags. Without this single block encryption step this would leak plaintext duplication to anyone monitoring traffic. This step has no security impact on AES-CTR.
* Clearing of bit 31 in the AES-CTR IV as in step 8 of encryption is technically not required but is there for compatibility with some AES-CTR implementations. If message size exceeds (16 * 2^31) bytes this becomes moot, but that's huge for a single message. If an AES-CTR implementation supports 64-bit or 128-bit counter semantics then this construction can be used for messages of effectively unlimited size.
## Performance
Performance is very close to AES-GCM on a given platform. It's very slightly slower because encryption requires two passes, but for short messages the second pass will operate on data already in the CPU's L0 cache which minimizes the actual overhead.
You can run tests with `cargo test -- --nocapture` and see encrypt and decrypt performance. Here's some single core benchmarks:
You can run tests with `cargo test --release -- --nocapture` and see encrypt and decrypt performance. Here's some single core benchmarks:
* AMD Threadripper 2990WX: **2055.60 MiB/sec**, decrypt **2050.09 MiB/sec**.
* M1 Mac Mini (2021): encrypt **4690.89 MiB/sec**, decrypt **4977.39 MiB/sec**.
* AMD Threadripper 2990WX: **2055.60 MiB/sec**, decrypt **2050.09 MiB/sec** (per core).
* Apple M1 Mac Mini (2021): encrypt **4868.89 MiB/sec**, decrypt **5059.39 MiB/sec** (per performance core).
Since this uses AES it will be much slower on systems that don't have AES hardware extensions. On the flip side it's extremely fast on systems that do, and also more energy efficient than ARX-type ciphers that slam the ALU to achieve high performance.
In general this construction performs better than ChaChaPoly or other ARX ciphers on processors that have AES hardware acceleration and considerably worse on processors that lack it. Performance on systems without hardware acceleration is generally still good enough for most applications.
## Cryptanalysis
@ -104,7 +105,7 @@ AES-ECB, AES-CTR, and GMAC are all algorithms allowed by FIPS-140. For FIPS purp
## Dependencies
This is implemented using the [libgcrypt](https://github.com/gpg/libgcrypt) library (via Rust bindings) on Linux, BSD, and Windows, and built-in CommonCrypto libraries on MacOS and iOS. CommonCrypto was used on Mac because libgcrypt has issues on the ARM64 platform with Apple's clang.
This is implemented using the [libgcrypt](https://github.com/gpg/libgcrypt) library (via Rust bindings) on Linux, BSD, and Windows, and built-in CommonCrypto libraries on MacOS and iOS. CommonCrypto was used on Mac because libgcrypt has issues on the ARM64 platform with Apple's clang. An OpenSSL based implementation is also included but it is only used on a few platforms where libgcrypt does not yet support native hardware acceleration, such as IBM S390x.
## License

View file

@ -62,9 +62,7 @@ mod tests {
c.reset();
c.decrypt_init(&tag);
c.decrypt_in_place(&mut buf);
if !c.decrypt_finish() {
panic!("decrypt tag check failed!");
}
let _ = c.decrypt_finish().expect("decrypt_finish() failed!");
for i in 1..12345 {
if buf[i] != (i & 0xff) as u8 {
panic!("decrypt data check failed!");
@ -74,7 +72,7 @@ mod tests {
}
println!("Encrypt/decrypt test OK");
let benchmark_iterations: usize = 50000;
let benchmark_iterations: usize = 80000;
let start = SystemTime::now();
for _ in 0..benchmark_iterations {
c.reset();

View file

@ -5,8 +5,10 @@ edition = "2018"
license = "MPL-2.0"
authors = ["ZeroTier, Inc. <contact@zerotier.com>", "Adam Ierymenko <adam.ierymenko@zerotier.com>"]
[patch.crates-io]
[dependencies]
rand_core = "0.5"
rand_core = "0.5.0"
aes-gmac-siv = { path = "../aes-gmac-siv" }
gcrypt = "0.7.0"
x25519-dalek = { version = "1.2.0", features = ["u64_backend"] }

View file

@ -20,10 +20,16 @@ fn hash_int_le(sha: &mut SHA512, i: u64) {
}
}
/// Compute balloon memory-hard hash using SHA-512 and SHA-384 for the final.
/// Compute balloon (variant) memory-hard hash.
///
/// SPACE_COST must be a multiple of 64. This is checked with an assertion.
/// DELTA is usually 3.
pub fn hash<const SPACE_COST: usize, const TIME_COST: usize, const DELTA: usize>(password: &[u8], salt: &[u8]) -> [u8; crate::hash::SHA384_HASH_SIZE] {
///
/// This differs slightly from "standard" balloon hash in that AES (CBC) is
/// used for the expand step and the final hash hashes the entire buffer. It
/// also takes no salt since it's only used for one purpose here and that's
/// not password hashing.
pub fn hash<const SPACE_COST: usize, const TIME_COST: usize, const DELTA: usize>(password: &[u8]) -> [u8; crate::hash::SHA384_HASH_SIZE] {
debug_assert_ne!(SPACE_COST, 0);
debug_assert_ne!(TIME_COST, 0);
debug_assert_ne!(DELTA, 0);
@ -36,23 +42,21 @@ pub fn hash<const SPACE_COST: usize, const TIME_COST: usize, const DELTA: usize>
let mut sha = SHA512::new();
sha.update(&zero64); // 0 cnt
sha.update(password);
sha.update(salt);
buf[0..64].copy_from_slice(sha.finish_get_ref());
/* Expand */
let mut cnt = 1_u64;
/* Expand (use AES as PRNG in this version as it's much faster on most hardware) */
let mut expand_aes = gcrypt::cipher::Cipher::new(gcrypt::cipher::Algorithm::Aes, gcrypt::cipher::Mode::Ecb).unwrap();
expand_aes.set_key(&buf[0..32]);
let mut s: usize = 64;
while s < SPACE_COST {
sha.reset();
hash_int_le(&mut sha, cnt);
sha.update(&buf[(s - 64)..s]);
let ss = s + 64;
buf[s..ss].copy_from_slice(sha.finish_get_ref());
let ss = s + 16;
let _ = expand_aes.encrypt(unsafe { &*buf.as_ptr().add(s - 16).cast::<[u8; 16]>() }, &mut buf[s..ss]);
s = ss;
cnt += 1;
}
drop(expand_aes);
/* Mix */
let mut cnt = 1_u64 + ((SPACE_COST / 16) as u64);
for t in 0..TIME_COST {
sha.reset();
hash_int_le(&mut sha, cnt);
@ -64,7 +68,6 @@ pub fn hash<const SPACE_COST: usize, const TIME_COST: usize, const DELTA: usize>
for i in 0..DELTA {
sha.reset();
hash_int_le(&mut sha, cnt);
sha.update(salt);
hash_int_le(&mut sha, t as u64);
sha.update(&zero64); // s == 0
hash_int_le(&mut sha, i as u64);
@ -94,7 +97,6 @@ pub fn hash<const SPACE_COST: usize, const TIME_COST: usize, const DELTA: usize>
for i in 0..DELTA {
sha.reset();
hash_int_le(&mut sha, cnt);
sha.update(salt);
hash_int_le(&mut sha, t as u64);
hash_int_le(&mut sha, s as u64);
hash_int_le(&mut sha, i as u64);

View file

@ -18,7 +18,7 @@ urlencoding = "^2"
lz4_flex = { version = "^0", features = ["safe-encode", "safe-decode", "checked-decode"] }
dashmap = "^4"
parking_lot = "^0"
concat-arrays = "^0"
simple-error = "^0"
[target."cfg(not(windows))".dependencies]
libc = "^0"

View file

@ -26,6 +26,8 @@ impl Debug for InvalidFormatError {
impl Error for InvalidFormatError {}
/****/
pub struct InvalidParameterError(pub(crate) &'static str);
impl Display for InvalidParameterError {
@ -42,3 +44,22 @@ impl Debug for InvalidParameterError {
}
impl Error for InvalidParameterError {}
/****/
pub struct MalformedRecordError(pub(crate) &'static str);
impl Display for MalformedRecordError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "InvalidParameterError: {}", self.0)
}
}
impl Debug for MalformedRecordError {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}
impl Error for MalformedRecordError {}

View file

@ -14,8 +14,6 @@ use std::io::Write;
use std::ptr::{slice_from_raw_parts, slice_from_raw_parts_mut};
use std::str::FromStr;
use concat_arrays::concat_arrays;
use zerotier_core_crypto::balloon;
use zerotier_core_crypto::c25519::*;
use zerotier_core_crypto::hash::*;
@ -33,12 +31,56 @@ pub const IDENTITY_TYPE_1_SIGNATURE_SIZE: usize = P521_ECDSA_SIGNATURE_SIZE + ED
const V0_POW_MEMORY: usize = 2097152;
const V0_POW_THRESHOLD: u8 = 17;
const V1_POW_THRESHOLD: u8 = 5;
const V1_BALLOON_SPACE_COST: usize = 16384;
const V1_BALLOON_SPACE_COST: usize = 262144;
const V1_BALLOON_TIME_COST: usize = 3;
const V1_BALLOON_DELTA: usize = 3;
const V1_BALLOON_SALT: &'static [u8] = b"zt_id_v1";
const V1_PUBLIC_KEYS_SIGNATURE_AND_POW_SIZE: usize = C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_ECDSA_SIGNATURE_SIZE + SHA384_HASH_SIZE;
const V1_PUBLIC_KEYS_SIZE: usize = C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE;
const V1_PUBLIC_KEYS_SIGNATURE_AND_POW_SIZE: usize = V1_PUBLIC_KEYS_SIZE + P521_ECDSA_SIGNATURE_SIZE + SHA384_HASH_SIZE;
const V1_SECRET_KEYS_SIZE: usize = C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE;
fn concat_v1_public_keys(c25519: &[u8], ed25519: &[u8], p521_ecdh: &[u8], p521_ecdsa: &[u8]) -> [u8; V1_PUBLIC_KEYS_SIZE] {
let mut k = [0_u8; V1_PUBLIC_KEYS_SIZE];
debug_assert!(c25519.len() <= C25519_PUBLIC_KEY_SIZE);
debug_assert!(ed25519.len() <= ED25519_PUBLIC_KEY_SIZE);
debug_assert!(p521_ecdh.len() <= P521_PUBLIC_KEY_SIZE);
debug_assert!(p521_ecdsa.len() <= P521_PUBLIC_KEY_SIZE);
k[(C25519_PUBLIC_KEY_SIZE - c25519.len())..C25519_PUBLIC_KEY_SIZE].copy_from_slice(c25519);
k[(C25519_PUBLIC_KEY_SIZE + (ED25519_PUBLIC_KEY_SIZE - ed25519.len()))..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE)].copy_from_slice(ed25519);
k[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + (P521_PUBLIC_KEY_SIZE - p521_ecdh.len()))..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE)].copy_from_slice(p521_ecdh);
k[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + (P521_PUBLIC_KEY_SIZE - p521_ecdsa.len()))..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE)].copy_from_slice(p521_ecdsa);
k
}
fn concat_v1_public_all(c25519: &[u8], ed25519: &[u8], p521_ecdh: &[u8], p521_ecdsa: &[u8], signature: &[u8], balloon_hash_digest: &[u8]) -> [u8; V1_PUBLIC_KEYS_SIGNATURE_AND_POW_SIZE] {
let mut k = [0_u8; V1_PUBLIC_KEYS_SIGNATURE_AND_POW_SIZE];
debug_assert!(c25519.len() <= C25519_PUBLIC_KEY_SIZE);
debug_assert!(ed25519.len() <= ED25519_PUBLIC_KEY_SIZE);
debug_assert!(p521_ecdh.len() <= P521_PUBLIC_KEY_SIZE);
debug_assert!(p521_ecdsa.len() <= P521_PUBLIC_KEY_SIZE);
debug_assert!(signature.len() <= P521_ECDSA_SIGNATURE_SIZE);
debug_assert_eq!(balloon_hash_digest.len(), SHA384_HASH_SIZE);
k[(C25519_PUBLIC_KEY_SIZE - c25519.len())..C25519_PUBLIC_KEY_SIZE].copy_from_slice(c25519);
k[(C25519_PUBLIC_KEY_SIZE + (ED25519_PUBLIC_KEY_SIZE - ed25519.len()))..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE)].copy_from_slice(ed25519);
k[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + (P521_PUBLIC_KEY_SIZE - p521_ecdh.len()))..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE)].copy_from_slice(p521_ecdh);
k[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + (P521_PUBLIC_KEY_SIZE - p521_ecdsa.len()))..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE)].copy_from_slice(p521_ecdsa);
k[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + (P521_ECDSA_SIGNATURE_SIZE - signature.len()))..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_ECDSA_SIGNATURE_SIZE)].copy_from_slice(signature);
k[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_ECDSA_SIGNATURE_SIZE)..].copy_from_slice(balloon_hash_digest);
k
}
fn concat_v1_secret_keys(c25519: &[u8], ed25519: &[u8], p521_ecdh: &[u8], p521_ecdsa: &[u8]) -> [u8; V1_SECRET_KEYS_SIZE] {
let mut k = [0_u8; V1_SECRET_KEYS_SIZE];
debug_assert!(c25519.len() <= C25519_SECRET_KEY_SIZE);
debug_assert!(ed25519.len() <= ED25519_SECRET_KEY_SIZE);
debug_assert!(p521_ecdh.len() <= P521_SECRET_KEY_SIZE);
debug_assert!(p521_ecdsa.len() <= P521_SECRET_KEY_SIZE);
k[(C25519_SECRET_KEY_SIZE - c25519.len())..C25519_SECRET_KEY_SIZE].copy_from_slice(c25519);
k[(C25519_SECRET_KEY_SIZE + (ED25519_SECRET_KEY_SIZE - ed25519.len()))..(C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE)].copy_from_slice(ed25519);
k[(C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + (P521_SECRET_KEY_SIZE - p521_ecdh.len()))..(C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE)].copy_from_slice(p521_ecdh);
k[(C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE + (P521_SECRET_KEY_SIZE - p521_ecdsa.len()))..(C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE)].copy_from_slice(p521_ecdsa);
k
}
#[derive(Copy, Clone)]
#[repr(u8)]
@ -144,11 +186,11 @@ impl Identity {
let p521_ecdsa = P521KeyPair::generate(false).unwrap();
let c25519_pub_bytes = c25519.public_bytes();
let ed25519_pub_bytes = ed25519.public_bytes();
let sign_buf: [u8; C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE] = concat_arrays!(c25519_pub_bytes, ed25519_pub_bytes, *p521_ecdh.public_key_bytes(), *p521_ecdsa.public_key_bytes());
let signing_buf = concat_v1_public_keys(&c25519_pub_bytes, &ed25519_pub_bytes, p521_ecdh.public_key_bytes(), p521_ecdsa.public_key_bytes());
loop {
// ECDSA is a randomized signature algorithm, so each signature will be different.
let sig = p521_ecdsa.sign(&sign_buf).unwrap();
let bh = balloon::hash::<{ V1_BALLOON_SPACE_COST }, { V1_BALLOON_TIME_COST }, { V1_BALLOON_DELTA }>(&sig, V1_BALLOON_SALT);
let sig = p521_ecdsa.sign(&signing_buf).unwrap();
let bh = balloon::hash::<{ V1_BALLOON_SPACE_COST }, { V1_BALLOON_TIME_COST }, { V1_BALLOON_DELTA }>(&sig);
if bh[0] < V1_POW_THRESHOLD {
let addr = Address::from_bytes(&bh[43..48]);
if addr.is_some() {
@ -188,26 +230,6 @@ impl Identity {
#[inline(always)]
pub fn address(&self) -> Address { self.address }
/// Compute a SHA384 hash of this identity's keys, including private keys if present.
pub fn hash_all_keys(&self) -> [u8; 48] {
let mut sha = SHA384::new();
sha.update(&self.c25519);
sha.update(&self.ed25519);
self.v1.as_ref().map(|p521| {
sha.update((*p521).0.public_key_bytes());
sha.update((*p521).1.public_key_bytes());
});
self.secrets.as_ref().map(|secrets| {
sha.update(&secrets.c25519.secret_bytes().as_ref());
sha.update(&secrets.ed25519.secret_bytes().as_ref());
secrets.v1.as_ref().map(|p521_secrets| {
sha.update((*p521_secrets).0.secret_key_bytes().as_ref());
sha.update((*p521_secrets).1.secret_key_bytes().as_ref());
});
});
sha.finish()
}
/// Locally validate this identity.
///
/// This can take a few milliseconds, especially on slower systems. V0 identities are slower
@ -229,13 +251,9 @@ impl Identity {
}
} else {
let p521 = self.v1.as_ref().unwrap();
let mut signing_buf = [0_u8; C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE];
signing_buf[0..C25519_PUBLIC_KEY_SIZE].copy_from_slice(&self.c25519);
signing_buf[C25519_PUBLIC_KEY_SIZE..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE)].copy_from_slice(&self.ed25519);
signing_buf[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE)..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE)].copy_from_slice((*p521).0.public_key_bytes());
signing_buf[(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE)..(C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE)].copy_from_slice((*p521).1.public_key_bytes());
let signing_buf = concat_v1_public_keys(&self.c25519, &self.ed25519, (*p521).0.public_key_bytes(), (*p521).1.public_key_bytes());
if (*p521).1.verify(&signing_buf, &(*p521).2) {
let bh = balloon::hash::<{ V1_BALLOON_SPACE_COST }, { V1_BALLOON_TIME_COST }, { V1_BALLOON_DELTA }>(&(*p521).2, V1_BALLOON_SALT);
let bh = balloon::hash::<{ V1_BALLOON_SPACE_COST }, { V1_BALLOON_TIME_COST }, { V1_BALLOON_DELTA }>(&(*p521).2);
(bh[0] < V1_POW_THRESHOLD) && bh.eq(&(*p521).3) && Address::from_bytes(&bh[43..48]).unwrap().eq(&self.address)
} else {
false
@ -279,9 +297,6 @@ impl Identity {
}
/// Sign this message with this identity.
///
/// Signature is performed using ed25519 EDDSA or NIST P-521 ECDSA depending on the identity
/// type. None is returned if this identity lacks secret keys or another error occurs.
pub fn sign(&self, msg: &[u8]) -> Option<Vec<u8>> {
self.secrets.as_ref().map_or(None, |secrets| {
let c25519_sig = secrets.ed25519.sign_zt(msg);
@ -339,7 +354,7 @@ impl Identity {
let secrets = self.secrets.as_ref().unwrap();
if secrets.v1.is_some() {
let p521_secrets = secrets.v1.as_ref().unwrap();
buf.append_u8((C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE) as u8)?;
buf.append_u8(V1_SECRET_KEYS_SIZE as u8)?;
buf.append_bytes_fixed(&secrets.c25519.secret_bytes().as_ref())?;
buf.append_bytes_fixed(&secrets.ed25519.secret_bytes().as_ref())?;
buf.append_bytes_fixed((*p521_secrets).0.secret_key_bytes().as_ref())?;
@ -468,7 +483,7 @@ impl Identity {
secrets.v1.as_ref().map_or_else(|| {
format!("{}:{}{}", self.to_string(), crate::util::hex::to_string(secrets.c25519.public_bytes().as_ref()), crate::util::hex::to_string(secrets.ed25519.secret_bytes().as_ref()))
}, |p521_secret| {
let secrets_concat: [u8; C25519_SECRET_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE + P521_SECRET_KEY_SIZE] = concat_arrays!(secrets.c25519.secret_bytes().0, secrets.ed25519.secret_bytes().0, p521_secret.0.secret_key_bytes().0, p521_secret.1.secret_key_bytes().0);
let secrets_concat = concat_v1_secret_keys(&secrets.c25519.secret_bytes().0, &secrets.ed25519.secret_bytes().0, &p521_secret.0.secret_key_bytes().0, &p521_secret.1.secret_key_bytes().0);
format!("{}:{}", self.to_string(), base64::encode_config(secrets_concat, base64::URL_SAFE_NO_PAD))
})
})
@ -480,7 +495,7 @@ impl ToString for Identity {
self.v1.as_ref().map_or_else(|| {
format!("{:0>10x}:0:{}{}", self.address.to_u64(), crate::util::hex::to_string(&self.c25519), crate::util::hex::to_string(&self.ed25519))
}, |p521_public| {
let public_concat: [u8; C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_ECDSA_SIGNATURE_SIZE + SHA384_HASH_SIZE] = concat_arrays!(self.c25519, self.ed25519, *((*p521_public).0.public_key_bytes()), *((*p521_public).1.public_key_bytes()), (*p521_public).2, (*p521_public).3);
let public_concat = concat_v1_public_all(&self.c25519, &self.ed25519, (*p521_public).0.public_key_bytes(), (*p521_public).1.public_key_bytes(), &(*p521_public).2, &(*p521_public).3);
format!("{:0>10x}:1:{}", self.address.to_u64(), base64::encode_config(public_concat, base64::URL_SAFE_NO_PAD))
})
}
@ -579,6 +594,13 @@ impl FromStr for Identity {
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
let bytes = self.marshal_to_bytes(true);
Identity::unmarshal_from_bytes(bytes.as_slice()).unwrap().0
}
}
impl PartialEq for Identity {
fn eq(&self, other: &Self) -> bool {
self.address.eq(&other.address) && self.c25519.eq(&other.c25519) && self.ed25519.eq(&other.ed25519) && self.v1.as_ref().map_or_else(|| other.v1.is_none(), |v1| other.v1.as_ref().map_or(false, |other_v1| (*v1).0.eq(&(*other_v1).0) && (*v1).1.eq(&(*other_v1).1)))
@ -646,23 +668,14 @@ mod tests {
panic!("identity V0 marshal/unmarshal failed");
}
}
for bad_id in [
"7f3b8e50db:0:936b698c68f51508e9184f7510323a01da0e5778158244c83520614822e2352855ff4d82443823b866cdb553d02d8fa5da833fbee62472e666a60605b76194b9:0d46684e30d561c859bf7d530d2de0452605d8cf392db4beb2768ceda55e63673f11d84a9f31ce7504f0e3ce5dc9ab7ecf9662e555846d130422916482be5fbb",
"8bd225d6a9:0:98e7fc755ee0aa2e10bf37c0b8dd6f33b3164de04cf3f716584ee44df1fe9506ce1f3f2874c6d1450fc8fab339a95092ec7e628cddd26af93c4392e6564d9ee7:431bb44d22734d925538cbcdc7c2a80c0f71968041949f76ccb6f690f01b6cf45976071c86fcf2ddda2d463c8cfe6444b36c8ee0d057d665350acdcb86dff06f"
] {
let id = Identity::from_str(bad_id).unwrap();
if id.locally_validate() {
panic!("known-bad V0 identity validated");
}
}
}
#[test]
fn type1() {
//let start = std::time::SystemTime::now();
let start = std::time::SystemTime::now();
let id = Identity::generate(Type::P521);
//let end = std::time::SystemTime::now();
//println!("V1: {}ms {}", end.duration_since(start).unwrap().as_millis(), id.to_string());
let end = std::time::SystemTime::now();
println!("V1 generate: {}ms {}", end.duration_since(start).unwrap().as_millis(), id.to_string());
if !id.locally_validate() {
panic!("new V1 identity validation failed");
}
@ -675,8 +688,8 @@ mod tests {
panic!("invalid signature verification succeeded");
}
for good_id in [
"c24a2cd3a1:1:9X3OSJ3lnss_mc1q2RBW8fVI3OFHdYVVgoFVSkXep3XTiyiq-B41qgo7SqcdAvOh49QqqZV3CJIXlBUaJB5logGdo6_VyCqRkTMKpnn5JGpt8QAp71G22o1tinQ_TepeqWBShfW6ZB5GIM5iyQdf4IHqPKxLNKbxwulBektAe6iHhwBRK0iUAlEkB7H1gq1P4qTnWodc-KfaYPcz6BUJA9nW6NUF0WVRRfrPHqiWPPm3268bX_sKMq5ap5i5yQnvlkJmdAD1bh4SlF9kmgqVcixtffk4AKNYD3zcP0o8yQ8UUsQ036JbpS1huWXE8BnFLC93jUFwVbibki6okDAsPDB57gJmhwA72h9KMbPw2L4zoI53e0lrsWoNgGJGczTCTwS-Z3-Dd9yBwf-sSkEbMhgR_OtbRyrtiUvsmwKQUT-tEKFoF0NTRgAwsv6KQCv_LDm1XStRWpbRbQz-1Wof3LpKjTsKrNhWML50mOlgNV766bYbIUekcBS0OgzU14Pew7Fffap3qG7JngG7_CWcMrszpJwD3YtttgUVSBdzFiL1xKVm8dnO1XzZL1jXPnme-dMHX3wISPznKK-zgsqMkyPoaCovkoZ1KuzqFwILfIyXMweP70ZkGl7ep288-DjDsprHNfMr9lvMtRC-ZsuAwbwJrzW8-9TCSizToQ",
"4f0ab394d1:1:kT54oJ0eViPr73BB_VQWxOlqFkwCJgsPOeLrEcElNFSXVBPqAMMe6aWTdB_HvmFEfa_PExjXHdJJVlADyR3gTAE98hJ8sXOHoVLAoPK-3FE1P8BPAiX8aRWW_eTyDgWzJBOZclxM3gAlqK4e1ceRYdMvHL2NfUk1ro3sAyj0PRowWACgVRCIOeQwfVYrc_G4hods__qEsrnWXZYarGKlM8V4xkfErCUl1MhMHNSmRS7WvSBO71LHaUSr66EK2QQKEUEpwwEVF9YNonUPqzkQNntYSkOHBLaghZtEvvje31UbnqV0GmkMjv3pIf4YIsUNVKvezf95lF7QdLI8v1dWDefrJHkYlwCU92hjDUpiwiE7XxA4-MUnv6XeYseHRn5GQRS3cQCsMbd9XpToJXA08OCHkMRo1IR9UEDWUubw78SefRZPjxojPQFtpMuL7r8Alo8HK3VOrGw4Am2gvFic53TVWQNX0XfX5pixDTQaEkLQqieeWYCGSSXasLAggqecXMytcOdHa8TeqgBCHPGl-tXIYUotzMPQzEjRPRWeYe3_UQcrR75lvYlb43DQfHBLMrZ0gEJeJqc0u2rMS_gxS1ZJriBy9PDzXoF4HACnywkra4JkoP_CGpxAHVaCFuYDfNTTEr2LYCxkc8jUx36KD3sQaA8UQXxPCrOU0Q"
"39ac93e603:1:kbBpk8QuSmNuSJFMLLXLbXcQMLXTMQOfGB8phFansHwxFrXO0RLNJB4HX4bHwq1UScYv2gVlwqmluH1J9_zp2gDaXD1c4jS17Zfdwz672wvkEMIRLUMxxxS4Cc4G7Xfhwpeot2qsslOTVw6h0FTM5izdH6ddnMmJGEg-84a_eQwSQgCNAYvczolmQg9D_MRp6wPdJdBFsnUMmj4F73jro4EGSAhE39AE8hPSxnLNOdhyRHriV5kMkIMsukJrQ09ZxKTmrQCvPzrzUrugkSu6BAqrXSm-h8S9tJo1TVIG0cSbBOK1nJsAoYhOq-T-hBvyqvKsFi9Wkz5bnG6NiYN95Yd38d2VRQBm4Xx7MPQDH0Yk10A1hc_dgyHtFGKjyTrpkKIBHICx_ijg3iS8Uut0aZpIb6pnOs1mZyjSCDLkSKJoNkxE8btHOwHzDIY0utnmsh6sCZ8wWUzYyXKfA506o7z1x57U4qxSI8-_cqZg6DUEoLb0idYanObcN9YmXdUtacpRoLhJTMlowQHqJK3_i0xh5P4UoBHuK7xST1Yu411y9keyZAie771kJFttzR3AHP89IEn0rbfbUZmAL1icL2LxWIq6kIXF6vdfgAHngQ-C1kjC3pXoWyvglJ7B33AfejmktuytTdGxe0ve1MhsXk0J3WIgokQ5rJPmAw",
"953f45090f:1:Ghn8_lcEX4bovP90tjJQ4oTXoCf5muJYVaC599_5RW6BN7IgGCsxodiN9U1Pq1_IDYnuJ98KmlPXksE77kvGrQCWK1BnhUFVthtkk7CkEsLdtMu5NqNK_1SVuxnfVCNNELBBIZMIl22YVw8PLTwlPcWUn16m9sAD9jqptUrwAmGUcQANpUiED6k3heqszrR9IjGVVYAcIOOHYJrVz13dv0-Ty6iyoNYG12AuSPd-7suG1Ztgt2Zll8c80BGUAib0-dgJBwF8fV8Bn_Sji6kz7qyp8566U6NkbAA4znM5UvrUPIw6O5016RuO8YzDLE_LKI_erwB2CjZsC0TDOFEppkPLSxDBWgBe1PRwnKTUhP9Bq7vYVbpBs_mEd4b7a-ilce8dX-dxJJwIXIioCdL6fZzA5KqfjW1pG-M72ct6iLCmq1R4b26eiwDG9z-HwX-fRVEkzDeE1rQ2bAEnqTNtzMTItAdzF62LiPOZlZ8LQ-fNTbdThphx58uKczo8Dl44R89d1l81yTKFbgGrUU6lw5qByH6VuLuprOn96sQLv4w6M8DPVnS_5E5AOIoXJ7KmEltIUz_B45ZzgDgPrBjE3-E2ipSSxQe72ADAkQL-D2uCtZVa-UhRFbjUqzBMoTu2NqHYotUoxCsbTVI1d3duD5NefnkjjmqVP0UJDw"
] {
let id = Identity::from_str(good_id).unwrap();
if !id.locally_validate() {
@ -688,14 +701,5 @@ mod tests {
panic!("identity V1 marshal/unmarshal failed");
}
}
for bad_id in [
"65ecc4bb1d:1:EEefUe82UfSkeHGroZhgZKp_V3asFzcTct8faJOIiFk16MB6nXNEAk1xjbI9Otjtvudq49JOWR9IRSZuom0VugHW0TDg9z14_8F1L39M9y_6rxhO4oKdpcmN_0dUxOtL8t7dw4PfSS3sKh-rrwWut01hoewy6-J42nJ_hbe7q_nWFABt4BHfWp3qqwYvMosYLquwUD1BJRnF_rIOEX82YhN84eFnntQTqnMMS1M8ILxe5-A7naowp1IxsccD7WW1a3f_BQAmgZmRfWAJqaTERQ2qJtCR6cixGid4raka_YgySFcx6BDi43Okm3rwO3prLjbr_J4d97BXbINKOAEms6AAxO75pwABxMmJVO1VRnXP10Y22XWMWZiN39sDVGzXCD3uzGptr5B5dBJPTEwyK1abxbwiv30hZE9bzLNgsuX12KsHb-yvMQEYQ_NBwGgMtV0fWcc3vPadEqdO7PRofOiAft9CPTrtLsO9AI88PMNId72plYHzYkCvtnnttgHLNqJwOIoOxd0xxQHLz8BMfcTm7t9fPHl6zPOtmakAmHaSQdlMpTqrpR7NL0awixRBFauFkrpD7v0zWkjP5JpUUDK1smCxAxan7oTlkwQou_kY6Ac65-cROf24xyUit1k1IzS1OiwSmWuGplEJxUCGORBAytS9WXFV7MS7HQ",
"8b04dcccc5:1:G3esWdhJPDff4yq9N_oilC_Der7S_iz0ytCA1uvO1RGjp_EDnqHfTO48rYhR2jZ7x3ibNyv_ySHyXvyqqmBvtABS5KdLn-fBCY2YrhH4o-3sAWffqTTMHthlFC8iIwtIh3uWDSbPAZLxRnsKQQSx5ndid31MDIdCTo4hEa-bjtXodQCoMDqOhEQHVb-abI9ljT7rOs1aWyYHI7l6lrvuR9IEV7xt2S0Z1Kdky9jnJXjBDq8H8HipLyFPc_FsURMlT5l1YgDwAFmmEAE43teNv8jZBSBYlQ4fokG-2OLXBtuKQBZ6Sd8Els3YEgXhn2TJXQIK0lPH5lKnEjH5IaDJ8uAxvKrs4ABYmd4OYRCHohHDYOzlzoRFTT-57SsWSfVdtGioRFVwTcB8sAUIKumWCpVsD18zaFXDNwn4nfkvhvBoKlbCGYiERgErKF7_t0YF5nXy2V5LdPPLPVq1KsVR2kMmQyILxCl5PWyKv9dgdos_69MmTSuCA28CZ6lcJJ8ZmCC-v1lUZSqmrwHrYzf9BX4YBBrH2ZjtoYtHzgETagH-_7Tll04Ug9KFUlQgerDMWhhPiMsILg4JDpGM0XHvPMqL8TU4KIiNGet9dga3ONkbrZaUpn-dEJ_3yL1D6BDbgoLex8fW3ejm5SOkNhtqH0QPSFJDKyyLBNzMxQ",
] {
let id = Identity::from_str(bad_id).unwrap();
if id.locally_validate() {
panic!("known-bad V1 identity validated");
}
}
}
}

View file

@ -29,7 +29,6 @@ pub use identity::Identity;
pub use endpoint::Endpoint;
pub use dictionary::Dictionary;
pub use inetaddress::InetAddress;
pub use locator::Locator;
pub use peer::Peer;
pub use path::Path;
pub use node::{PacketBuffer, PacketBufferPool, PacketBufferFactory};

View file

@ -65,12 +65,6 @@ struct EphemeralKeyPair {
// SHA384(c25519 public | p521 public)
public_keys_hash: [u8; 48],
// Curve25519 ECDH key pair.
c25519: C25519KeyPair,
// NIST P-521 ECDH key pair.
p521: P521KeyPair,
}
/// A remote peer known to this node.
@ -463,10 +457,6 @@ impl Peer {
let mut dict = Dictionary::new();
dict.set_u64(HELLO_DICT_KEY_INSTANCE_ID, node.instance_id);
dict.set_u64(HELLO_DICT_KEY_CLOCK, ci.time_clock() as u64);
let _ = self.ephemeral_pair.lock().as_ref().map(|ephemeral_pair| {
dict.set_bytes(HELLO_DICT_KEY_EPHEMERAL_C25519, ephemeral_pair.c25519.public_bytes().to_vec());
dict.set_bytes(HELLO_DICT_KEY_EPHEMERAL_P521, ephemeral_pair.p521.public_key_bytes().to_vec());
});
if node.is_peer_root(self) {
// If the peer is a root we include some extra information for diagnostic and statistics
// purposes such as the CPU type, bits, and OS info. This is not sent to other peers.

View file

@ -6,217 +6,185 @@
* https://www.zerotier.com/
*/
use std::hash::{Hash, Hasher};
use std::io::Write;
use concat_arrays::concat_arrays;
use zerotier_core_crypto::c25519::*;
use zerotier_core_crypto::hash::SHA384;
use zerotier_core_crypto::p521::*;
use zerotier_core_crypto::secret::Secret;
use crate::vl1::{Endpoint, Identity};
use crate::vl1::{Identity, Endpoint, Dictionary};
use crate::vl1::buffer::Buffer;
use crate::vl1::protocol::PACKET_SIZE_MAX;
use crate::error::InvalidParameterError;
use zerotier_core_crypto::c25519::{ED25519_SECRET_KEY_SIZE, ED25519_PUBLIC_KEY_SIZE, ED25519_SIGNATURE_SIZE, Ed25519KeyPair, ed25519_verify};
use zerotier_core_crypto::p521::{P521_SECRET_KEY_SIZE, P521_PUBLIC_KEY_SIZE, P521_ECDSA_SIGNATURE_SIZE, P521KeyPair, P521PublicKey};
use zerotier_core_crypto::secret::Secret;
use zerotier_core_crypto::hash::SHA384;
use std::cmp::Ordering;
/// Old "planet" type with Ed25519 authenticated updates from ZeroTier v1.
const ROOT_SET_TYPE_LEGACY_PLANET: u8 = 1;
/// Old "moon" type, basically the same as "planet" as far as we are concerned.
const ROOT_SET_TYPE_LEGACY_MOON: u8 = 127;
const ROOT_SET_TYPE_ED25519_P521: u8 = 128;
/// Root set type.
///
/// Two of these are legacy from ZeroTier V1. The third is a root set signed by both
/// an Ed25519 key and a NIST P-521 key with these keys being bundled together.
#[derive(Clone, PartialEq, Eq)]
pub enum TypeAndID {
LegacyPlanet(u64),
LegacyMoon(u64),
Ed25519P521RootSet([u8; 48]),
}
/// New V2 type with FIPS-compliant extra signing key.
const ROOT_SET_TYPE_P521_ED25519: u8 = 128;
impl Hash for TypeAndID {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::LegacyPlanet(id) | Self::LegacyMoon(id) => state.write_u64(*id),
Self::Ed25519P521RootSet(id) => state.write(id),
}
}
}
/// Secret keys that can be used to update root sets after creation.
pub struct RootSetSecretKeys {
ed25519: Ed25519KeyPair,
p521: P521KeyPair,
}
impl RootSetSecretKeys {
const SECRET_BYTES_LEN: usize = 1 + ED25519_PUBLIC_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_PUBLIC_KEY_SIZE + P521_SECRET_KEY_SIZE;
const PUBLIC_BYTES_LEN: usize = 1 + ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE;
/// Generate a new set of root set secret keys.
pub fn generate() -> Self {
Self {
ed25519: Ed25519KeyPair::generate(false),
p521: P521KeyPair::generate(false).unwrap(),
}
}
/// Reconstruct from bytes as returned by to_secret_bytes() or return None if invalid.
pub fn from_bytes(b: &[u8]) -> Option<Self> {
if b.len() == Self::SECRET_BYTES_LEN && b[0] == ROOT_SET_TYPE_ED25519_P521 {
let ed25519 = Ed25519KeyPair::from_bytes(&b[1..ED25519_PUBLIC_KEY_SIZE + 1], &b[1 + ED25519_PUBLIC_KEY_SIZE..1 + ED25519_PUBLIC_KEY_SIZE + ED25519_SECRET_KEY_SIZE]);
let p521 = P521KeyPair::from_bytes(&b[1 + ED25519_PUBLIC_KEY_SIZE + ED25519_SECRET_KEY_SIZE..1 + ED25519_PUBLIC_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_PUBLIC_KEY_SIZE], &b[1 + ED25519_PUBLIC_KEY_SIZE + ED25519_SECRET_KEY_SIZE + P521_PUBLIC_KEY_SIZE..]);
if ed25519.is_none() || p521.is_none() {
None
} else {
Some(Self {
ed25519: ed25519.unwrap(),
p521: p521.unwrap(),
})
}
} else {
None
}
}
/// Get both public and secret keys in byte format.
pub fn to_secret_bytes(&self) -> Secret<{ Self::SECRET_BYTES_LEN }> {
Secret(concat_arrays!([ROOT_SET_TYPE_ED25519_P521], self.ed25519.public_bytes(), self.ed25519.secret_bytes().0, *self.p521.public_key_bytes(), self.p521.secret_key_bytes().0))
}
/// Get only public keys in byte format.
pub fn to_public_bytes(&self) -> [u8; ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE] {
concat_arrays!(self.ed25519.public_bytes(), *self.p521.public_key_bytes())
}
}
const ROOT_SET_META_DATA_OOB_UPDATE_URL: &'static str = "U";
const ROOT_SET_META_DATA_NAME: &'static str = "N";
const ROOT_SET_META_DATA_CONTACT: &'static str = "C";
/// A single root node with static endpoints where it can be reached.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[derive(Clone, PartialEq, Eq)]
pub struct Root {
/// Root node ZeroTier identity.
pub identity: Identity,
/// Static endpoints at which this root node may be reached.
pub endpoints: Vec<Endpoint>,
}
/// A signed bundle of root nodes.
///
/// In v1 this was the "World" object.
///
/// This is how roots are normally specified to nodes. The embedded signing key allows the
/// root set to be updated automatically. Updates can add, remove, or change the endpoints
/// of roots, allowing infrastructure updates with automatic client configuration as long
/// as at least one of the old roots is up to distribute the new ones.
#[derive(PartialEq, Eq)]
impl PartialOrd for Root {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Root {
fn cmp(&self, other: &Self) -> Ordering {
let idcmp = self.identity.cmp(&other.identity);
if idcmp.is_eq() {
self.endpoints.cmp(&other.endpoints)
} else {
idcmp
}
}
}
/// Secret key for creating root set updates.
pub struct RootSetSecretSigningKey {
ed25519_secret: Ed25519KeyPair,
p521_secret: P521KeyPair,
}
/// A bundle of roots that can be updated.
#[derive(Clone)]
pub struct RootSet {
pub id: TypeAndID,
pub timestamp: i64,
pub url: String,
type_: u8,
/// Unique ID of root set.
pub id: [u8; 48],
/// Roots and static endpoints.
pub roots: Vec<Root>,
pub signer: Vec<u8>,
pub signature: Vec<u8>,
/// Timestamp / revision number.
pub timestamp: i64,
/// Additional meta-data fields.
pub metadata: Dictionary,
ed25519_signing_key: [u8; 32 + ED25519_PUBLIC_KEY_SIZE], // LEGACY: first 32 bytes are ignored
ed25519_signature: [u8; 96], // LEGACY: old signatures have a hash appended
p521_signing_key: [u8; P521_PUBLIC_KEY_SIZE],
p521_signature: [u8; P521_ECDSA_SIGNATURE_SIZE],
}
impl RootSet {
pub const MAX_ROOTS: usize = u8::MAX as usize;
pub const MAX_ENDPOINTS_PER_ROOT: usize = u8::MAX as usize;
/// Shortcut to copy a byte array to a Buffer and unmarshal().
pub fn from_bytes(bytes: &[u8]) -> std::io::Result<RootSet> {
let mut tmp: Buffer<{ PACKET_SIZE_MAX }> = Buffer::new();
tmp.append_bytes(bytes)?;
let mut c: usize = 0;
RootSet::unmarshal(&tmp, &mut c)
}
/// Sign this root set and return true on success.
pub fn sign(&mut self, keys: &RootSetSecretKeys) -> bool {
self.signer = keys.to_public_bytes().to_vec();
self.id = TypeAndID::Ed25519P521RootSet(SHA384::hash(self.signer.as_slice()));
let mut buf: Buffer<{ PACKET_SIZE_MAX }> = Buffer::new();
if self.marshal_internal(&mut buf, true).is_err() {
return false;
/// Create and sign a new root set.
/// This cannot create legacy "planet" or "moon" type root sets. For those the old mkworld code must be used.
pub fn create(roots: &[Root], timestamp: i64, oob_update_url: Option<&str>, name: Option<&str>, contact: Option<&str>, signing_key: &RootSetSecretSigningKey) -> Result<RootSet, InvalidParameterError> {
let mut sorted_roots = roots.to_vec();
sorted_roots.sort();
sorted_roots.dedup();
if sorted_roots.is_empty() {
return Err(InvalidParameterError("empty root list"));
}
let ed25519 = keys.ed25519.sign(buf.as_bytes());
let p521 = keys.p521.sign(buf.as_bytes());
if p521.is_none() {
return false;
for r in sorted_roots.iter_mut() {
r.endpoints.sort();
r.endpoints.dedup();
if r.endpoints.is_empty() {
return Err(InvalidParameterError("at least one root has an empty endpoint list"));
}
}
let p521 = p521.unwrap();
self.signature.clear();
let _ = self.signature.write_all(&ed25519);
let _ = self.signature.write_all(&p521);
let mut md = Dictionary::new();
let _ = oob_update_url.map(|s| md.set_str(ROOT_SET_META_DATA_OOB_UPDATE_URL, s));
let _ = name.map(|s| md.set_str(ROOT_SET_META_DATA_NAME, s));
let _ = contact.map(|s| md.set_str(ROOT_SET_META_DATA_CONTACT, s));
true
let mut rs = RootSet {
type_: ROOT_SET_TYPE_P521_ED25519,
id: {
let mut h = SHA384::new();
h.update(&signing_key.ed25519_secret.public_bytes());
h.update(signing_key.p521_secret.public_key_bytes());
h.finish()
},
roots: sorted_roots,
timestamp,
metadata: md,
ed25519_signing_key: {
let mut tmp = [0_u8; 64];
tmp[32..64].copy_from_slice(&signing_key.ed25519_secret.public_bytes());
tmp
},
ed25519_signature: [0_u8; 96],
p521_signing_key: signing_key.p521_secret.public_key_bytes().clone(),
p521_signature: [0_u8; P521_ECDSA_SIGNATURE_SIZE],
};
let mut signing_buf: Buffer<{ PACKET_SIZE_MAX }> = Buffer::new();
let _ = rs.marshal_internal(&mut signing_buf, true).expect("internal error marshaling for signature");
rs.ed25519_signature = signing_key.ed25519_secret.sign_zt(signing_buf.as_bytes());
rs.p521_signature = signing_key.p521_secret.sign(signing_buf.as_bytes()).expect("error signing root set with ECDSA");
Ok(rs)
}
fn marshal_internal<const BL: usize>(&self, buf: &mut Buffer<BL>, for_signing: bool) -> std::io::Result<()> {
if self.roots.len() > u8::MAX as usize {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "maximum roots per root set: 255"));
}
if for_signing {
buf.append_u64(0x7f7f7f7f7f7f7f7f)?;
}
match &self.id {
TypeAndID::LegacyPlanet(id) | TypeAndID::LegacyMoon(id) => {
buf.append_u8(if matches!(self.id, TypeAndID::LegacyPlanet(_)) {
ROOT_SET_TYPE_LEGACY_PLANET
} else {
ROOT_SET_TYPE_LEGACY_MOON
})?;
buf.append_u64(*id)?;
buf.append_u64(self.timestamp as u64)?;
if self.signer.len() != 64 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "legacy signer can only be 64 bytes"));
}
buf.append_bytes(self.signer.as_slice())?;
if !for_signing {
if self.signature.len() != 96 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "legacy signature can only be 96 bytes"));
}
buf.append_bytes(self.signature.as_slice())?;
}
}
TypeAndID::Ed25519P521RootSet(_) => {
buf.append_u8(ROOT_SET_TYPE_ED25519_P521)?;
buf.append_varint(self.timestamp as u64)?;
let url = self.url.as_bytes();
buf.append_varint(url.len() as u64)?;
buf.append_bytes(url)?;
if self.signer.len() != (ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE) {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "signer can only be 164 bytes"));
}
buf.append_bytes(self.signer.as_slice())?;
if !for_signing {
if self.signature.len() != (ED25519_SIGNATURE_SIZE + P521_ECDSA_SIGNATURE_SIZE) {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "signature can only be 192 bytes"));
}
buf.append_bytes(self.signature.as_slice())?;
}
}
buf.append_u8(self.type_)?;
if self.type_ == ROOT_SET_TYPE_LEGACY_PLANET || self.type_ == ROOT_SET_TYPE_LEGACY_MOON {
buf.append_bytes(&self.id[0..8]);
} else {
buf.append_bytes_fixed(&self.id);
}
buf.append_u64(self.timestamp as u64)?;
buf.append_bytes_fixed(&self.ed25519_signing_key)?;
if !for_signing {
buf.append_bytes_fixed(&self.ed25519_signature)?;
}
buf.append_u8(self.roots.len() as u8)?;
if self.type_ == ROOT_SET_TYPE_LEGACY_PLANET || self.type_ == ROOT_SET_TYPE_LEGACY_MOON {
if self.roots.len() > 4 as usize || self.id.len() != 8 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid legacy type root set"));
}
} else if self.type_ == ROOT_SET_TYPE_P521_ED25519 {
buf.append_bytes_fixed(&self.p521_signing_key);
if !for_signing {
buf.append_bytes_fixed(&self.p521_signature);
}
} else {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid root set type"));
}
buf.append_varint(self.roots.len() as u64)?;
for root in self.roots.iter() {
root.identity.marshal(buf, false)?;
if root.endpoints.len() > u8::MAX as usize {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "maximum endpoints per root: 255"));
if (self.type_ == ROOT_SET_TYPE_LEGACY_PLANET || self.type_ == ROOT_SET_TYPE_LEGACY_MOON) && root.endpoints.len() > 127 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid legacy type root set"));
}
buf.append_u8(root.endpoints.len() as u8)?;
buf.append_varint(root.endpoints.len() as u64)?;
for ep in root.endpoints.iter() {
ep.marshal(buf)?;
}
}
if matches!(self.id, TypeAndID::LegacyMoon(_)) {
buf.append_u8(0)?;
if self.type_ == ROOT_SET_TYPE_LEGACY_MOON {
buf.append_u16(0)?;
} else if self.type_ == ROOT_SET_TYPE_P521_ED25519 {
let mdb = self.metadata.to_bytes();
buf.append_varint(mdb.len() as u64)?;
buf.append_bytes(mdb.as_slice())?;
}
if for_signing {
@ -226,74 +194,130 @@ impl RootSet {
Ok(())
}
/// Shortcut to copy a byte array to a Buffer and unmarshal().
pub fn from_bytes(bytes: &[u8]) -> std::io::Result<RootSet> {
let mut tmp: Buffer<{ PACKET_SIZE_MAX }> = Buffer::new();
tmp.append_bytes(bytes)?;
let mut c: usize = 0;
RootSet::unmarshal(&tmp, &mut c)
}
#[inline(always)]
pub fn marshal<const BL: usize>(&self, buf: &mut Buffer<BL>) -> std::io::Result<()> {
self.marshal_internal(buf, false)
}
pub fn unmarshal<const BL: usize>(buf: &Buffer<BL>, cursor: &mut usize) -> std::io::Result<Self> {
let read_roots = |buf: &Buffer<BL>, cursor: &mut usize| -> std::io::Result<Vec<Root>> {
let root_count = buf.read_u8(cursor)? as usize;
let mut roots = Vec::<Root>::with_capacity(root_count);
for _ in 0..root_count {
let identity = Identity::unmarshal(buf, cursor)?;
let endpoint_count = buf.read_u8(cursor)? as usize;
let mut endpoints = Vec::<Endpoint>::with_capacity(endpoint_count);
for _ in 0..endpoint_count {
endpoints.push(Endpoint::unmarshal(buf, cursor)?);
pub fn unmarshal<const BL: usize>(buf: &Buffer<BL>, cursor: &mut usize) -> std::io::Result<RootSet> {
let type_ = buf.read_u8(cursor)?;
if type_ != ROOT_SET_TYPE_LEGACY_PLANET && type_ != ROOT_SET_TYPE_LEGACY_MOON && type_ != ROOT_SET_TYPE_P521_ED25519 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "unrecognized root set type"));
}
let is_legacy = type_ == ROOT_SET_TYPE_LEGACY_PLANET || type_ == ROOT_SET_TYPE_LEGACY_MOON;
let mut id = [0_u8; 48];
if is_legacy {
id[0..8].copy_from_slice(buf.read_bytes_fixed::<8>(cursor)?);
} else {
id.copy_from_slice(buf.read_bytes_fixed::<48>(cursor)?);
}
let timestamp = buf.read_u64(cursor)? as i64;
let ed25519_signing_key = buf.read_bytes_fixed::<{ 32 + ED25519_PUBLIC_KEY_SIZE }>(cursor)?;
let ed25519_signature = buf.read_bytes_fixed::<96>(cursor)?;
let mut p521_signing_key = [0_u8; P521_PUBLIC_KEY_SIZE];
let mut p521_signature = [0_u8; P521_ECDSA_SIGNATURE_SIZE];
let mut root_count;
if type_ == ROOT_SET_TYPE_P521_ED25519 {
p521_signing_key.copy_from_slice(buf.read_bytes_fixed::<P521_PUBLIC_KEY_SIZE>(cursor)?);
p521_signature.copy_from_slice(buf.read_bytes_fixed::<P521_ECDSA_SIGNATURE_SIZE>(cursor)?);
root_count = buf.read_varint(cursor)? as usize;
} else {
root_count = buf.read_u8(cursor)? as usize;
}
if is_legacy && root_count > 4 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid legacy type root set (max roots: 4)"));
}
let mut roots: Vec<Root> = Vec::new();
for _ in 0..root_count {
let id = Identity::unmarshal(buf, cursor)?;
let endpoint_count = if is_legacy {
buf.read_u8(cursor)? as usize
} else {
buf.read_varint(cursor)? as usize
};
if is_legacy && endpoint_count > 127 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid legacy type root set (max endpoints per root: 127)"));
}
roots.push(Root {
identity: id,
endpoints: {
let mut ep: Vec<Endpoint> = Vec::new();
for _ in 0..endpoint_count {
ep.push(Endpoint::unmarshal(buf, cursor)?);
}
ep
}
roots.push(Root { identity, endpoints });
}
Ok(roots)
};
})
}
let type_id = buf.read_u8(cursor)?;
match type_id {
ROOT_SET_TYPE_LEGACY_PLANET | ROOT_SET_TYPE_LEGACY_MOON => {
let id = buf.read_u64(cursor)?;
let id = if type_id == ROOT_SET_TYPE_LEGACY_PLANET { TypeAndID::LegacyPlanet(id) } else { TypeAndID::LegacyMoon(id) };
let timestamp = buf.read_u64(cursor)?;
let signer = buf.read_bytes(64, cursor)?.to_vec();
let signature = buf.read_bytes(96, cursor)?.to_vec();
let roots = read_roots(buf, cursor)?;
if type_id == ROOT_SET_TYPE_LEGACY_MOON {
*cursor += buf.read_u8(cursor)? as usize;
let mut dict = None;
if type_ == ROOT_SET_TYPE_LEGACY_MOON {
*cursor += buf.read_u16(cursor)? as usize;
} else if type_ == ROOT_SET_TYPE_P521_ED25519 {
let dict_size = buf.read_varint(cursor)? as usize;
if dict_size > 0 {
dict = Dictionary::from_bytes(buf.read_bytes(dict_size, cursor)?);
if dict.is_none() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid meta-data dictionary"));
}
Ok(Self {
id,
timestamp: timestamp as i64,
url: String::new(),
roots,
signer,
signature,
})
}
}
ROOT_SET_TYPE_ED25519_P521 => {
let timestamp = buf.read_varint(cursor)? as i64;
let url = String::from_utf8_lossy(buf.read_bytes(buf.read_varint(cursor)? as usize, cursor)?).to_string();
let signer = buf.read_bytes(ED25519_PUBLIC_KEY_SIZE + P521_PUBLIC_KEY_SIZE, cursor)?.to_vec();
let signature = buf.read_bytes(ED25519_SIGNATURE_SIZE + P521_ECDSA_SIGNATURE_SIZE, cursor)?.to_vec();
let id = TypeAndID::Ed25519P521RootSet(SHA384::hash(signer.as_slice()));
Ok(Self {
id,
timestamp,
url,
roots: read_roots(buf, cursor)?,
signer,
signature,
})
Ok(RootSet {
type_,
id,
roots,
timestamp,
metadata: dict.unwrap_or_else(|| Dictionary::new()),
ed25519_signing_key: ed25519_signing_key.clone(),
ed25519_signature: ed25519_signature.clone(),
p521_signing_key,
p521_signature
})
}
/// Test whether this root set should replace another root set with the same unique ID.
pub fn should_replace(&self, other: &RootSet) -> bool {
if self.type_ == other.type_ && self.timestamp > other.timestamp && self.id.eq(&other.id) {
if self.type_ == ROOT_SET_TYPE_LEGACY_PLANET || self.type_ == ROOT_SET_TYPE_LEGACY_MOON {
self.ed25519_signing_key.eq(&other.ed25519_signing_key)
} else if self.type_ == ROOT_SET_TYPE_P521_ED25519 {
self.ed25519_signing_key.eq(&other.ed25519_signing_key) && self.p521_signing_key.eq(&other.p521_signing_key)
} else {
false
}
_ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "unrecognized type"))
} else {
false
}
}
}
impl Hash for RootSet {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(self.timestamp as u64);
self.id.hash(state);
/// Verify this root set's signatures.
pub fn verify_signatures(&self) -> bool {
let mut signing_buf: Buffer<{ PACKET_SIZE_MAX }> = Buffer::new();
if self.marshal_internal(&mut signing_buf, true).is_ok() {
if ed25519_verify(&self.ed25519_signing_key[32..64], &self.ed25519_signature[0..64], signing_buf.as_bytes()) {
if self.type_ != ROOT_SET_TYPE_P521_ED25519 {
P521PublicKey::from_bytes(&self.p521_signing_key).map_or(false, |p521| p521.verify(signing_buf.as_bytes(), &self.p521_signature))
} else {
true
}
} else {
false
}
} else {
false
}
}
}

View file

@ -127,17 +127,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "concat-arrays"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df715824eb382e34b7afb7463b0247bf41538aeba731fba05241ecdb5dc3747"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "console"
version = "0.15.0"
@ -967,6 +956,12 @@ version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335"
[[package]]
name = "simple-error"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc47a29ce97772ca5c927f75bac34866b16d64e07f330c3248e2d7226623901b"
[[package]]
name = "slab"
version = "0.4.5"
@ -1307,11 +1302,11 @@ name = "zerotier-network-hypervisor"
version = "2.0.0"
dependencies = [
"base64",
"concat-arrays",
"dashmap",
"libc",
"lz4_flex",
"parking_lot",
"simple-error",
"urlencoding",
"winapi",
"zerotier-core-crypto",