From a39767d32bd360a5e40c8d28998913ff539182a0 Mon Sep 17 00:00:00 2001 From: Luca Fulchir Date: Fri, 17 Feb 2023 14:59:02 +0100 Subject: [PATCH] More work on ciphers and hkdf Signed-off-by: Luca Fulchir --- Cargo.toml | 43 +++++++------ src/connection/handshake/dirsync.rs | 26 ++++++-- src/connection/mod.rs | 1 - src/enc/asym.rs | 71 +++++++++++++++------- src/enc/errors.rs | 6 ++ src/enc/hkdf.rs | 47 +++++++++++++++ src/enc/mod.rs | 1 + src/enc/sym.rs | 94 ++++++++++++++++++++++++++++- src/lib.rs | 37 +++++++++--- 9 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 src/enc/hkdf.rs diff --git a/Cargo.toml b/Cargo.toml index 7f7ab32..0e60097 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.67.0" homepage = "https://git.runesauth.com/RunesAuth/libFenrir" repository = "https://git.runesauth.com/RunesAuth/libFenrir" license = "Apache-2.0 WITH LLVM-exception" -license-file = "LICENSE" +#license-file = "LICENSE" keywords = [ "Fenrir", "libFenrir", "authentication" ] categories = [ "authentication", "cryptography", "network-programming" ] @@ -26,26 +26,33 @@ crate_type = [ "lib", "cdylib", "staticlib" ] [dependencies] # please keep these in alphabetical order -arc-swap = { version = "^1.6" } +arc-swap = { version = "1.6" } # base85 repo has no tags, fix on a commit. v1.1.1 points to older, wrong version base85 = { git = "https://gitlab.com/darkwyrm/base85", rev = "d98efbfd171dd9ba48e30a5c88f94db92fc7b3c6" } -futures = { version = "^0.3" } -libc = { version = "^0.2" } -num-traits = { version = "^0.2" } -num-derive = { version = "^0.3" } -ring = { version = "^0.16" } -bincode = { version = "^1.3" } -strum = { version = "^0.24" } -strum_macros = { version = "^0.24" } -thiserror = { version = "^1.0" } -tokio = { version = "^1", features = ["full"] } +futures = { version = "0.3" } +hkdf = { version = "0.12" } +libc = { version = "0.2" } +num-traits = { version = "0.2" } +num-derive = { version = "0.3" } +ring = { version = "0.16" } +bincode = { version = "1.3" } +sha3 = { version = "0.10" } +strum = { version = "0.24" } +strum_macros = { version = "0.24" } +thiserror = { version = "1.0" } +tokio = { version = "1", features = ["full"] } # PERF: todo linux-only, behind "iouring" feature -#tokio-uring = { version = "^0.4" } -tracing = { version = "^0.1" } -trust-dns-resolver = { version = "^0.22", features = [ "dnssec-ring" ] } -trust-dns-client = { version = "^0.22", features = [ "dnssec" ] } -trust-dns-proto = { version = "^0.22" } -x25519-dalek = { version = "^1.2", features = [ "serde" ] } +#tokio-uring = { version = "0.4" } +tracing = { version = "0.1" } +trust-dns-resolver = { version = "0.22", features = [ "dnssec-ring" ] } +trust-dns-client = { version = "0.22", features = [ "dnssec" ] } +trust-dns-proto = { version = "0.22" } +x25519-dalek = { version = "1.2", features = [ "serde" ] } +# NOTE: can not force newer versions of zeroize: +# * zeroize is a dependency of x25519-dalek +# * zeroize is not *pure* rust, but uses `alloc` +# * rust can have multiple versions as deps, but only if the are pure rust +zeroize = { version = "1" } [profile.dev] diff --git a/src/connection/handshake/dirsync.rs b/src/connection/handshake/dirsync.rs index add8fa8..95fc4f2 100644 --- a/src/connection/handshake/dirsync.rs +++ b/src/connection/handshake/dirsync.rs @@ -11,7 +11,10 @@ use super::{Error, HandshakeData}; use crate::{ connection::ID, - enc::asym::{ExchangePubKey, KeyID}, + enc::{ + asym::{ExchangePubKey, KeyExchange, KeyID}, + sym::CipherKind, + }, }; use ::std::vec::Vec; @@ -29,6 +32,10 @@ pub enum DirSync { pub struct Req { /// Id of the server key used for the key exchange pub key_id: KeyID, + /// Selected key exchange + pub exchange: KeyExchange, + /// Selected cipher + pub cipher: CipherKind, /// Client ephemeral public key used for key exchanges pub exchange_key: ExchangePubKey, /// encrypted data @@ -37,19 +44,30 @@ pub struct Req { impl super::HandshakeParsing for Req { fn parse(raw: &[u8]) -> Result { - const MIN_PKT_LEN: usize = 8; + const MIN_PKT_LEN: usize = 10; if raw.len() < MIN_PKT_LEN { return Err(Error::NotEnoughData); } let key_id: KeyID = KeyID(u16::from_le_bytes(raw[0..1].try_into().unwrap())); - let (exchange_key, len) = match ExchangePubKey::from_slice(&raw[2..]) { + use ::num_traits::FromPrimitive; + let exchange: KeyExchange = match KeyExchange::from_u8(raw[2]) { + Some(exchange) => exchange, + None => return Err(Error::Parsing), + }; + let cipher: CipherKind = match CipherKind::from_u8(raw[3]) { + Some(cipher) => cipher, + None => return Err(Error::Parsing), + }; + let (exchange_key, len) = match ExchangePubKey::from_slice(&raw[4..]) { Ok(exchange_key) => exchange_key, Err(e) => return Err(e.into()), }; - let enc = raw[(2 + len)..].to_vec(); + let enc = raw[(4 + len)..].to_vec(); Ok(HandshakeData::DirSync(DirSync::Req(Self { key_id, + exchange, + cipher, exchange_key, enc, }))) diff --git a/src/connection/mod.rs b/src/connection/mod.rs index 96dfd9c..52e8828 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -3,7 +3,6 @@ pub mod handshake; mod packet; -use ::num_traits::FromPrimitive; use ::std::vec::Vec; pub use handshake::Handshake; diff --git a/src/enc/asym.rs b/src/enc/asym.rs index cbf0b5d..7eccda4 100644 --- a/src/enc/asym.rs +++ b/src/enc/asym.rs @@ -4,11 +4,36 @@ use ::num_traits::FromPrimitive; use ::std::vec::Vec; use super::Error; +use crate::enc::sym::Secret; /// Public key ID #[derive(Debug, Copy, Clone, PartialEq)] pub struct KeyID(pub u16); +/// Kind of key used in the handshake +#[derive(Debug, Copy, Clone, PartialEq, ::num_derive::FromPrimitive)] +#[repr(u8)] +pub enum Key { + /// X25519 Public key + X25519 = 0, +} +impl Key { + fn pub_len(&self) -> usize { + match self { + // FIXME: 99% wrong size + Key::X25519 => ::ring::signature::ED25519_PUBLIC_KEY_LEN, + } + } +} + +/// Kind of key exchange +#[derive(Debug, Copy, Clone, PartialEq, ::num_derive::FromPrimitive)] +#[repr(u8)] +pub enum KeyExchange { + /// X25519 Public key + X25519DiffieHellman = 0, +} + /// Kind of key in the handshake #[derive(Clone)] #[allow(missing_debug_implementations)] @@ -28,28 +53,29 @@ pub enum ExchangePrivKey { } impl ExchangePrivKey { + /// Get the kind of key + pub fn kind(&self) -> Key { + match self { + ExchangePrivKey::X25519(_) => Key::X25519, + } + } /// Run the key exchange between two keys of the same kind pub fn key_exchange( &self, + exchange: KeyExchange, pub_key: ExchangePubKey, - ) -> Result<[u8; 32], Error> { - todo!(); - } -} - -/// Kind of key in the key exchange -#[derive(Debug, Copy, Clone, ::num_derive::FromPrimitive)] -#[repr(u8)] -pub enum ExchangePubKeyKind { - /// X25519 Public key - X25519 = 0, -} -impl ExchangePubKeyKind { - fn len(&self) -> usize { + ) -> Result { match self { - // FIXME: 99% wrong size - ExchangePubKeyKind::X25519 => { - ::ring::signature::ED25519_PUBLIC_KEY_LEN + ExchangePrivKey::X25519(priv_key) => { + if exchange != KeyExchange::X25519DiffieHellman { + return Err(Error::UnsupportedKeyExchange); + } + if let ExchangePubKey::X25519(inner_pub_key) = pub_key { + let shared_secret = priv_key.diffie_hellman(&inner_pub_key); + Ok(shared_secret.into()) + } else { + Err(Error::UnsupportedKeyExchange) + } } } } @@ -72,16 +98,17 @@ impl ExchangePubKey { if raw.len() < 1 + MIN_KEY_SIZE { return Err(Error::NotEnoughData); } - match ExchangePubKeyKind::from_u8(raw[0]) { + match Key::from_u8(raw[0]) { Some(kind) => match kind { - ExchangePubKeyKind::X25519 => { + Key::X25519 => { let pub_key: ::x25519_dalek::PublicKey = - match ::bincode::deserialize(&raw[1..(1 + kind.len())]) - { + match ::bincode::deserialize( + &raw[1..(1 + kind.pub_len())], + ) { Ok(pub_key) => pub_key, Err(_) => return Err(Error::Parsing), }; - Ok((ExchangePubKey::X25519(pub_key), kind.len())) + Ok((ExchangePubKey::X25519(pub_key), kind.pub_len())) } }, None => { diff --git a/src/enc/errors.rs b/src/enc/errors.rs index cec5864..86af252 100644 --- a/src/enc/errors.rs +++ b/src/enc/errors.rs @@ -13,4 +13,10 @@ pub enum Error { /// You might have passed rsa keys where x25519 was expected #[error("wrong key type")] WrongKey, + /// Unsupported key exchange for this key + #[error("unsupported key exchange")] + UnsupportedKeyExchange, + /// Unsupported cipher + #[error("unsupported cipher")] + UnsupportedCipher, } diff --git a/src/enc/hkdf.rs b/src/enc/hkdf.rs new file mode 100644 index 0000000..c2ec54d --- /dev/null +++ b/src/enc/hkdf.rs @@ -0,0 +1,47 @@ +//! Hash-based Key Derivation Function +//! We just repackage other crates + +use ::hkdf::Hkdf; +use ::sha3::Sha3_256; +use ::zeroize::Zeroize; + +use crate::enc::sym::Secret; + +// Hack & tricks: +// HKDF are pretty important, but they don't zero out the data. +// we can't user #[derive(Zeroing)] either. +// So we craete a union with a Zeroing object, and drop manually both. + +#[derive(Zeroize)] +#[zeroize(drop)] +struct Zeroable([u8; ::core::mem::size_of::>()]); + +union HkdfInner { + hkdf: ::core::mem::ManuallyDrop>, + zeroable: ::core::mem::ManuallyDrop, +} + +impl Drop for HkdfInner { + fn drop(&mut self) { + #[allow(unsafe_code)] + unsafe { + drop(&mut self.hkdf); + drop(&mut self.zeroable); + } + } +} + +/// Sha3 based HKDF +#[allow(missing_debug_implementations)] +pub struct HkdfSha3 { + _inner: Hkdf, +} + +impl HkdfSha3 { + /// Instantiate a new HKDF with Sha3-256 + pub fn new(salt: Option<&[u8]>, key: Secret) -> Self { + Self { + _inner: Hkdf::::new(salt, key.as_ref()), + } + } +} diff --git a/src/enc/mod.rs b/src/enc/mod.rs index 0d7e416..eda3385 100644 --- a/src/enc/mod.rs +++ b/src/enc/mod.rs @@ -2,6 +2,7 @@ pub mod asym; mod errors; +pub mod hkdf; pub mod sym; pub use errors::Error; diff --git a/src/enc/sym.rs b/src/enc/sym.rs index 6debc4a..aecb90d 100644 --- a/src/enc/sym.rs +++ b/src/enc/sym.rs @@ -1,5 +1,96 @@ //! Symmetric cypher stuff +use ::zeroize::Zeroize; + +/// Secret, used for keys. +/// Grants that on drop() we will zero out memory +#[derive(Zeroize)] +#[zeroize(drop)] +#[allow(missing_debug_implementations)] +pub struct Secret { + secret: [u8; 32], +} + +impl Secret { + /// return a reference to the secret + pub fn as_ref(&self) -> &[u8; 32] { + &self.secret + } +} +impl From<::x25519_dalek::SharedSecret> for Secret { + fn from(shared_secret: ::x25519_dalek::SharedSecret) -> Self { + Self { + secret: shared_secret.to_bytes(), + } + } +} + +/// List of possible Ciphers +#[derive(Debug, Copy, Clone, PartialEq, ::num_derive::FromPrimitive)] +#[repr(u8)] +pub enum CipherKind { + /// Chaha20_Poly1305 + Chacha20Poly1305 = 0, +} + +impl CipherKind { + /// required length of the nonce + pub fn nonce_len(&self) -> usize { + ::ring::aead::CHACHA20_POLY1305.nonce_len() + } + /// required length of the key + pub fn key_len(&self) -> usize { + ::ring::aead::CHACHA20_POLY1305.key_len() + } + /// Length of the authentication tag + pub fn tag_len(&self) -> usize { + ::ring::aead::CHACHA20_POLY1305.tag_len() + } +} + +/// actual ciphers +#[derive(Debug)] +pub enum Cipher { + /// Cipher Chaha20_Poly1305 + Chacha20Poly1305(Chacha20Poly1305), +} + +impl Cipher { + /// Build a new Cipher + pub fn new(kind: CipherKind, secret: Secret) -> Self { + match kind { + CipherKind::Chacha20Poly1305 => { + Self::Chacha20Poly1305(Chacha20Poly1305::new(secret)) + } + } + } +} + +/// Chacha20Poly1305 cipher +#[derive(Debug)] +pub struct Chacha20Poly1305 { + cipher: ::ring::aead::OpeningKey, +} + +impl Chacha20Poly1305 { + /// Initialize the cipher with the given nonce + pub fn with_nonce(key: Secret, nonce: Nonce) -> Self { + let unbound_key = ::ring::aead::UnboundKey::new( + &::ring::aead::CHACHA20_POLY1305, + key.as_ref(), + ) + .unwrap(); + use ::ring::aead::BoundKey; + Self { + cipher: ::ring::aead::OpeningKey::::new(unbound_key, nonce), + } + } + /// Initialize a new ChachaPoly20 cipher with a random nonce + pub fn new(key: Secret) -> Self { + Self::with_nonce(key, Nonce::new()) + } +} + #[derive(Debug, Copy, Clone)] #[repr(C)] #[allow(missing_debug_implementations)] @@ -11,6 +102,8 @@ struct NonceNum { #[repr(C)] pub union Nonce { num: NonceNum, + // ring::aead::Nonce does not implement 'Copy' + // even though it's just [u8;12] raw: ::core::mem::ManuallyDrop<::ring::aead::Nonce>, easy_from: [u8; 12], } @@ -48,7 +141,6 @@ impl Nonce { } } -//impl Copy for Nonce {} impl Clone for Nonce { fn clone(&self) -> Self { #[allow(unsafe_code)] diff --git a/src/lib.rs b/src/lib.rs index 314b293..aca90aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ use ::arc_swap::{ArcSwap, ArcSwapAny}; use ::std::{net::SocketAddr, sync::Arc}; use ::tokio::{net::UdpSocket, task::JoinHandle}; +use crate::enc::{asym, sym::CipherKind}; pub use config::Config; use connection::handshake::{self, Handshake, HandshakeKey}; @@ -40,9 +41,14 @@ pub enum Error { /// Handshake errors #[error("Handshake: {0:?}")] Handshake(#[from] handshake::Error), + /// Key error + #[error("key: {0:?}")] + Key(#[from] crate::enc::Error), } struct FenrirInner { + key_exchanges: ArcSwapAny>>, + ciphers: ArcSwapAny>>, keys: ArcSwapAny>>, } @@ -76,21 +82,30 @@ impl FenrirInner { return Err(handshake::Error::UnknownKeyID.into()); } let ephemeral_key = ephemeral_key.unwrap(); + { + let exchanges = self.key_exchanges.load(); + if None + == exchanges.iter().find(|&x| { + *x == (ephemeral_key.kind(), req.exchange) + }) + { + return Err( + enc::Error::UnsupportedKeyExchange.into() + ); + } + } + { + let ciphers = self.ciphers.load(); + if None == ciphers.iter().find(|&x| *x == req.cipher) { + return Err(enc::Error::UnsupportedCipher.into()); + } + } let shared_key = match ephemeral_key - .key_exchange(req.exchange_key) + .key_exchange(req.exchange, req.exchange_key) { Ok(shared_key) => shared_key, Err(e) => return Err(handshake::Error::Key(e).into()), }; - use crate::enc::sym::Nonce; - use ::ring::aead::{self, BoundKey}; - let alg = aead::UnboundKey::new( - &aead::CHACHA20_POLY1305, - &shared_key, - ) - .unwrap(); - let nonce = Nonce::new(); - let chacha = aead::OpeningKey::new(alg, nonce); todo!(); } DirSync::Resp(resp) => { @@ -135,6 +150,8 @@ impl Fenrir { dnssec: None, stop_working: sender, _inner: Arc::new(FenrirInner { + ciphers: ArcSwapAny::new(Arc::new(Vec::new())), + key_exchanges: ArcSwapAny::new(Arc::new(Vec::new())), keys: ArcSwapAny::new(Arc::new(Vec::new())), }), };