From 52585e92621410e2e89d6e45dc99d415043f6d32 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Thu, 15 Jul 2021 16:38:59 -0400 Subject: [PATCH] Rusty stuff. --- network-hypervisor/Cargo.lock | 1 + network-hypervisor/Cargo.toml | 1 + network-hypervisor/src/crypto/c25519.rs | 52 ++++-- network-hypervisor/src/crypto/hash.rs | 72 +++----- network-hypervisor/src/crypto/p521.rs | 218 ++++++++++++++++++++++++ network-hypervisor/src/lib.rs | 11 +- network-hypervisor/src/util/hex.rs | 25 +++ network-hypervisor/src/util/mod.rs | 1 + network-hypervisor/src/vl1/mod.rs | 0 9 files changed, 315 insertions(+), 66 deletions(-) create mode 100644 network-hypervisor/src/util/hex.rs create mode 100644 network-hypervisor/src/util/mod.rs create mode 100644 network-hypervisor/src/vl1/mod.rs diff --git a/network-hypervisor/Cargo.lock b/network-hypervisor/Cargo.lock index 4bac32685..ff63a0c1a 100644 --- a/network-hypervisor/Cargo.lock +++ b/network-hypervisor/Cargo.lock @@ -414,6 +414,7 @@ version = "2.0.0" dependencies = [ "aes-gmac-siv", "ed25519-dalek", + "gcrypt", "rand_core", "x25519-dalek", ] diff --git a/network-hypervisor/Cargo.toml b/network-hypervisor/Cargo.toml index 78926fe8e..44939c6d2 100644 --- a/network-hypervisor/Cargo.toml +++ b/network-hypervisor/Cargo.toml @@ -8,3 +8,4 @@ rand_core = "^0" aes-gmac-siv = { path = "../aes-gmac-siv" } x25519-dalek = "^1" ed25519-dalek = "^1" +gcrypt = "^0" diff --git a/network-hypervisor/src/crypto/c25519.rs b/network-hypervisor/src/crypto/c25519.rs index 31d98b9d2..18a531386 100644 --- a/network-hypervisor/src/crypto/c25519.rs +++ b/network-hypervisor/src/crypto/c25519.rs @@ -1,4 +1,6 @@ use std::convert::TryInto; +use ed25519_dalek::Digest; +use std::io::Write; pub const C25519_PUBLIC_KEY_SIZE: usize = 32; pub const C25519_SECRET_KEY_SIZE: usize = 32; @@ -7,6 +9,7 @@ pub const ED25519_PUBLIC_KEY_SIZE: usize = 32; pub const ED25519_SECRET_KEY_SIZE: usize = 32; pub const ED25519_SIGNATURE_SIZE: usize = 64; +/// Curve25519 key pair for ECDH key agreement. pub struct C25519KeyPair(x25519_dalek::StaticSecret, x25519_dalek::PublicKey); impl C25519KeyPair { @@ -18,10 +21,16 @@ impl C25519KeyPair { } #[inline(always)] - pub fn from_keys(public_key: &[u8], secret_key: &[u8]) -> C25519KeyPair { - let pk = x25519_dalek::PublicKey::from(public_key.try_into().unwrap()); - let sk = x25519_dalek::StaticSecret::from(secret_key.try_into().unwrap()); - C25519KeyPair(sk, pk) + pub fn from_keys(public_key: &[u8], secret_key: &[u8]) -> Option { + if public_key.len() == 32 && secret_key.len() == 32 { + let pk: [u8; 32] = public_key.try_into().unwrap(); + let sk: [u8; 32] = secret_key.try_into().unwrap(); + let pk = x25519_dalek::PublicKey::from(pk); + let sk = x25519_dalek::StaticSecret::from(sk); + Some(C25519KeyPair(sk, pk)) + } else { + None + } } #[inline(always)] @@ -34,14 +43,17 @@ impl C25519KeyPair { self.0.to_bytes() } + /// Execute ECDH agreement and return a raw (un-hashed) shared secret key. #[inline(always)] pub fn agree(&self, their_public: &[u8]) -> [u8; C25519_SHARED_SECRET_SIZE] { - let pk = x25519_dalek::PublicKey::from(their_public.try_into().unwrap()); + let pk: [u8; 32] = their_public.try_into().unwrap(); + let pk = x25519_dalek::PublicKey::from(pk); let sec = self.0.diffie_hellman(&pk); sec.to_bytes() } } +/// Ed25519 key pair for EDDSA signatures. pub struct Ed25519KeyPair(ed25519_dalek::Keypair); impl Ed25519KeyPair { @@ -71,19 +83,37 @@ impl Ed25519KeyPair { #[inline(always)] pub fn sign(&self, msg: &[u8]) -> [u8; ED25519_SIGNATURE_SIZE] { - let h = crate::crypto::hash::SHA512::hash(msg); - self.0.sign_prehashed(h, None).unwrap().to_bytes() + let mut h = ed25519_dalek::Sha512::new(); + let _ = h.write_all(msg); + self.0.sign_prehashed(h.clone(), None).unwrap().to_bytes() } /// Create a signature with the first 32 bytes of the SHA512 hash appended. - /// ZeroTier does this for legacy reasons. + /// ZeroTier does this for legacy reasons, but it's ignored in newer versions. #[inline(always)] pub fn sign_zt(&self, msg: &[u8]) -> [u8; 96] { - let h = crate::crypto::hash::SHA512::hash(msg); - let s = self.0.sign_prehashed(h, None).unwrap().as_ref(); + let mut h = ed25519_dalek::Sha512::new(); + let _ = h.write_all(msg); + let sig = self.0.sign_prehashed(h.clone(), None).unwrap(); + let s = sig.as_ref(); let mut s2 = [0_u8; 96]; s2[0..64].copy_from_slice(s); - s2[64..96].copy_from_slice(&h[0..32]); + let h = h.finalize(); + s2[64..96].copy_from_slice(&h.as_slice()[0..32]); s2 } } + +#[inline(always)] +pub fn ed25519_verify(public_key: &[u8], signature: &[u8], msg: &[u8]) -> bool { + if public_key.len() == 32 && signature.len() >= 64 { + ed25519_dalek::PublicKey::from_bytes(public_key).map_or(false, |pk| { + let mut h = ed25519_dalek::Sha512::new(); + let _ = h.write_all(msg); + let sig: [u8; 64] = signature[0..64].try_into().unwrap(); + pk.verify_prehashed(h, None, &ed25519_dalek::Signature::from(sig)).is_ok() + }) + } else { + false + } +} diff --git a/network-hypervisor/src/crypto/hash.rs b/network-hypervisor/src/crypto/hash.rs index 753ad2c44..f4453511e 100644 --- a/network-hypervisor/src/crypto/hash.rs +++ b/network-hypervisor/src/crypto/hash.rs @@ -2,19 +2,32 @@ use std::mem::MaybeUninit; use std::convert::TryInto; use std::io::Write; +pub const SHA512_HASH_SIZE: usize = 64; + pub struct SHA512(gcrypt::digest::MessageDigest); impl SHA512 { #[inline(always)] - pub fn hash(b: &[u8]) -> [u8; 64] { + pub fn hash(b: &[u8]) -> [u8; SHA512_HASH_SIZE] { let mut h = unsafe { MaybeUninit::<[u8; 64]>::uninit().assume_init() }; gcrypt::digest::hash(gcrypt::digest::Algorithm::Sha512, b, &mut h); h } + /// Compute HMAC-SHA512(key, msg) + #[inline(always)] + pub fn hmac(key: &[u8], msg: &[u8]) -> [u8; SHA512_HASH_SIZE] { + let mut m = gcrypt::mac::Mac::new(gcrypt::mac::Algorithm::HmacSha512).unwrap(); + let _ = m.set_key(key); + let _ = m.update(msg); + let mut h = [0_u8; SHA512_HASH_SIZE]; + m.get_mac(&mut h); + h + } + #[inline(always)] pub fn new() -> Self { - SHA512(gcrypt::digest::MessageDigest::new(gcrypt::digest::Algorithm::Sha512).unwrap()) + Self(gcrypt::digest::MessageDigest::new(gcrypt::digest::Algorithm::Sha512).unwrap()) } #[inline(always)] @@ -28,60 +41,25 @@ impl SHA512 { } #[inline(always)] - pub fn finish(&mut self) -> [u8; 64] { + pub fn finish(&mut self) -> [u8; SHA512_HASH_SIZE] { self.0.finish(); self.0.get_only_digest().unwrap().try_into().unwrap() } + + /// Return a reference to an internally stored result. + /// This saves a copy, but the returned result is only valid so long as no other methods are called. + #[inline(always)] + pub fn finish_get_ref(&mut self) -> &[u8] { + self.0.finish(); + self.0.get_only_digest().unwrap() + } } impl Write for SHA512 { #[inline(always)] fn write(&mut self, b: &[u8]) -> std::io::Result { - self.0.write(b) - } - - #[inline(always)] - fn flush(&mut self) -> std::io::Result<()> { - self.0.flush() - } -} - -pub struct SHA384(gcrypt::digest::MessageDigest); - -impl SHA384 { - #[inline(always)] - pub fn hash(b: &[u8]) -> [u8; 48] { - let mut h = unsafe { MaybeUninit::<[u8; 48]>::uninit().assume_init() }; - gcrypt::digest::hash(gcrypt::digest::Algorithm::Sha512, b, &mut h); - h - } - - #[inline(always)] - pub fn new() -> Self { - SHA384(gcrypt::digest::MessageDigest::new(gcrypt::digest::Algorithm::Sha384).unwrap()) - } - - #[inline(always)] - pub fn reset(&mut self) { - self.0.reset(); - } - - #[inline(always)] - pub fn update(&mut self, b: &[u8]) { self.0.update(b); - } - - #[inline(always)] - pub fn finish(&mut self) -> [u8; 48] { - self.0.finish(); - self.0.get_only_digest().unwrap().try_into().unwrap() - } -} - -impl Write for SHA384 { - #[inline(always)] - fn write(&mut self, b: &[u8]) -> std::io::Result { - self.0.write(b) + Ok(b.len()) } #[inline(always)] diff --git a/network-hypervisor/src/crypto/p521.rs b/network-hypervisor/src/crypto/p521.rs index ee42dfae5..a452b759d 100644 --- a/network-hypervisor/src/crypto/p521.rs +++ b/network-hypervisor/src/crypto/p521.rs @@ -1 +1,219 @@ use std::str::FromStr; +use std::convert::TryInto; +use std::io::Write; + +use gcrypt::sexp::SExpression; + +pub const P521_PUBLIC_KEY_SIZE: usize = 132; +pub const P521_SECRET_KEY_SIZE: usize = 66; +pub const P521_ECDSA_SIGNATURE_SIZE: usize = 132; +pub const P521_ECDH_SHARED_SECRET_SIZE: usize = 132; + +/* +fn dump_sexp(exp: &SExpression) { + if exp.len() == 1 { + let s = exp.get_str(0); + if s.is_ok() { + print!("{}", s.unwrap()); + } else { + let b = exp.get_bytes(0); + if b.is_some() { + print!("#{}#", crate::util::hex::to_string(b.unwrap())); + } else { + print!("()"); + } + } + } else if exp.len() > 0 { + for i in 0..exp.len() { + let v = exp.get(i as u32); + if v.is_some() { + if i == 0 { + print!("("); + } else { + print!(" "); + } + dump_sexp(&v.unwrap()); + } + } + print!(")"); + } +} +*/ + +#[inline(always)] +fn hash_to_data_sexp(msg: &[u8]) -> [u8; 155] { + let h = crate::crypto::hash::SHA512::hash(msg); + let mut d = [0_u8; 155]; + d[0..24].copy_from_slice(b"(data(flags raw)(value #"); + let mut j = 24; + for i in 0..64 { + let b = h[i] as usize; + d[j] = crate::util::hex::HEX_CHARS[b >> 4]; + d[j + 1] = crate::util::hex::HEX_CHARS[b & 0xf]; + j += 2; + } + d[152..155].copy_from_slice(b"#))"); + d +} + +pub struct P521PublicKey { + public_key: SExpression, + public_key_bytes: [u8; P521_PUBLIC_KEY_SIZE], +} + +/// NIST P-521 elliptic curve key pair. +/// This supports both ECDSA signing and ECDH key agreement. In practice the same key pair +/// is not used for both functions as this is considred bad practice. +pub struct P521KeyPair { + public_key: P521PublicKey, + secret_key_for_ecdsa: SExpression, // secret key as a private-key S-expression + secret_key_for_ecdh: SExpression, // secret key as a "data" S-expression for the weird gcrypt ECDH interface + secret_key_bytes: [u8; P521_SECRET_KEY_SIZE], +} + +impl P521KeyPair { + /// Generate a NIST P-521 key pair. + /// If transient is true a faster but possibly somewhat less intensive pseudo-random number + /// generator is used. This is for ephemeral keys, and has no effect on some platforms. + pub fn generate(transient: bool) -> Option { + let sexp = SExpression::from_str(if transient { "(genkey(ecc(curve nistp521)(flags nocomp transient-key)))" } else { "(genkey(ecc(curve nistp521)(flags nocomp)))" }).unwrap(); + gcrypt::pkey::generate_key(&sexp).map_or(None, |kp| { + let pk_exp = kp.find_token("public-key"); + let sk_exp = kp.find_token("private-key"); + if pk_exp.is_some() && sk_exp.is_some() { + let pk_exp = pk_exp.unwrap(); + let sk_exp = sk_exp.unwrap(); + let pk = pk_exp.find_token("q"); + let sk = sk_exp.find_token("d"); + if pk.is_some() && sk.is_some() { + let pktmp = pk.unwrap(); + let sktmp = sk.unwrap(); + let pk = pktmp.get_bytes(1); + let sk = sktmp.get_bytes(1); + if pk.is_some() && sk.is_some() { + let pk = pk.unwrap(); + let sk = sk.unwrap(); + let mut kp = P521KeyPair { + public_key: P521PublicKey { + public_key: pk_exp, + public_key_bytes: [0_u8; P521_PUBLIC_KEY_SIZE], + }, + secret_key_for_ecdsa: sk_exp, + secret_key_for_ecdh: SExpression::from_str(format!("(data(flags raw)(value #{}#))", crate::util::hex::to_string(sk)).as_str()).unwrap(), + secret_key_bytes: [0_u8; P521_SECRET_KEY_SIZE], + }; + kp.public_key.public_key_bytes[((P521_PUBLIC_KEY_SIZE + 1) - pk.len())..P521_PUBLIC_KEY_SIZE].copy_from_slice(&pk[1..]); + kp.secret_key_bytes[(P521_SECRET_KEY_SIZE - sk.len())..P521_SECRET_KEY_SIZE].copy_from_slice(sk); + return Some(kp); + } + } + } + return None; + }) + } + + #[inline(always)] + pub fn public_key(&self) -> &P521PublicKey { + &self.public_key + } + + /// Get the raw ECC public "q" point for this key pair. + /// The returned point is not compressed. To use this with other interfaces that expect a format + /// prefix, prepend 0x04 to the beginning of this public key. This prefix is always the same in + /// our system and so is omitted. + #[inline(always)] + pub fn public_key_bytes(&self) -> &[u8; P521_PUBLIC_KEY_SIZE] { + &self.public_key.public_key_bytes + } + + #[inline(always)] + pub fn secret_key_bytes(&self) -> &[u8; P521_SECRET_KEY_SIZE] { + &self.secret_key_bytes + } + + /// Create an ECDSA signature of the input message. + pub fn sign(&self, msg: &[u8]) -> Option<[u8; P521_ECDSA_SIGNATURE_SIZE]> { + let data = SExpression::from_str(unsafe { std::str::from_utf8_unchecked(&hash_to_data_sexp(msg)) }).unwrap(); + gcrypt::pkey::sign(&self.secret_key_for_ecdsa, &data).map_or(None, |sig| { + let mut sig_bytes = [0_u8; P521_ECDSA_SIGNATURE_SIZE]; + if sig.find_token("r").map_or(false, |r| r.get_bytes(1).map_or(false, |r| { + sig_bytes[(66 - r.len())..66].copy_from_slice(r); + true + } )) && sig.find_token("s").map_or(false, |s| s.get_bytes(1).map_or(false, |s| { + sig_bytes[(66 + (66 - s.len()))..132].copy_from_slice(s); + true + })) { + Some(sig_bytes) + } else { + None + } + }) + } + + /// Execute ECDH key agreement, returning a raw (un-hashed) shared secret. + pub fn agree(&self, other_public: &P521PublicKey) -> Option<[u8; P521_ECDH_SHARED_SECRET_SIZE]> { + gcrypt::pkey::encrypt(&other_public.public_key, &self.secret_key_for_ecdh).map_or(None, |k| { + k.find_token("s").map_or(None, |s| s.get_bytes(1).map_or(None, |sb| { + Some(sb[1..].try_into().unwrap()) + })) + }) + } +} + +impl P521PublicKey { + pub fn from_bytes(b: &[u8]) -> Option { + if b.len() == P521_PUBLIC_KEY_SIZE { + Some(P521PublicKey { + public_key: SExpression::from_str(format!("(public-key(ecc(curve nistp521)(q #04{}#)))", crate::util::hex::to_string(b)).as_str()).unwrap(), + public_key_bytes: b.try_into().unwrap(), + }) + } else { + None + } + } + + pub fn verify(&self, msg: &[u8], signature: &[u8]) -> bool { + if signature.len() == P521_ECDSA_SIGNATURE_SIZE { + let data = SExpression::from_str(unsafe { std::str::from_utf8_unchecked(&hash_to_data_sexp(msg)) }).unwrap(); + let sig = SExpression::from_str(format!("(sig-val(ecdsa(r #{}#)(s #{}#)))", crate::util::hex::to_string(&signature[0..66]), crate::util::hex::to_string(&signature[66..132])).as_str()).unwrap(); + gcrypt::pkey::verify(&self.public_key, &data, &sig).is_ok() + } else { + false + } + } + + #[inline(always)] + pub fn public_key_bytes(&self) -> &[u8; P521_PUBLIC_KEY_SIZE] { + &self.public_key_bytes + } + + #[inline(always)] + pub fn as_bytes(&self) -> [u8; P521_PUBLIC_KEY_SIZE] { + self.public_key_bytes.clone() + } +} + +#[cfg(test)] +mod tests { + use crate::crypto::p521::P521KeyPair; + + #[test] + fn generate_sign_verify_agree() { + let kp = P521KeyPair::generate(false).unwrap(); + let kp2 = P521KeyPair::generate(false).unwrap(); + + let sig = kp.sign(&[0_u8]).unwrap(); + if !kp.public_key().verify(&[0_u8], &sig) { + panic!("ECDSA verify failed"); + } + if kp.public_key().verify(&[1_u8], &sig) { + panic!("ECDSA verify succeeded for incorrect message"); + } + + let sec0 = kp.agree(kp2.public_key()).unwrap(); + let sec1 = kp2.agree(kp.public_key()).unwrap(); + if !sec0.eq(&sec1) { + panic!("ECDH secrets do not match"); + } + } +} diff --git a/network-hypervisor/src/lib.rs b/network-hypervisor/src/lib.rs index f2dd22e3e..4d72f9e05 100644 --- a/network-hypervisor/src/lib.rs +++ b/network-hypervisor/src/lib.rs @@ -1,8 +1,3 @@ -mod crypto; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - } -} +pub mod crypto; +pub mod vl1; +pub mod util; diff --git a/network-hypervisor/src/util/hex.rs b/network-hypervisor/src/util/hex.rs new file mode 100644 index 000000000..23bf7ff65 --- /dev/null +++ b/network-hypervisor/src/util/hex.rs @@ -0,0 +1,25 @@ +pub(crate) const HEX_CHARS: [u8; 16] = [ b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f']; + +/// Encode a binary string to a series of hex bytes. +pub fn to_string(b: &[u8]) -> String { + let mut s = String::new(); + s.reserve(b.len() * 2); + for c in b { + let x = *c as usize; + s.push(HEX_CHARS[x >> 4] as char); + s.push(HEX_CHARS[x & 0xf] as char); + } + s +} + +/// Encode bytes from 'b' into hex characters in 'dest'. +/// This will panic if the destination slice is smaller than twice the length of the source. +pub fn to_hex_bytes(b: &[u8], dest: &mut [u8]) { + let mut j = 0; + for c in b { + let x = *c as usize; + dest[j] = HEX_CHARS[x >> 4]; + dest[j + 1] = HEX_CHARS[x & 0xf]; + j += 2; + } +} diff --git a/network-hypervisor/src/util/mod.rs b/network-hypervisor/src/util/mod.rs new file mode 100644 index 000000000..ce02e677d --- /dev/null +++ b/network-hypervisor/src/util/mod.rs @@ -0,0 +1 @@ +pub mod hex; diff --git a/network-hypervisor/src/vl1/mod.rs b/network-hypervisor/src/vl1/mod.rs new file mode 100644 index 000000000..e69de29bb