From 9e1312b149a19f32ebdc5341a22b5ce09fec0240 Mon Sep 17 00:00:00 2001 From: Luca Fulchir Date: Wed, 22 Feb 2023 12:30:00 +0100 Subject: [PATCH] More work on authentication Still lots of unfinished stuff Signed-off-by: Luca Fulchir --- src/auth/mod.rs | 54 +++++++++++++- src/connection/handshake/dirsync.rs | 112 ++++++++++++++++++++++------ src/connection/handshake/mod.rs | 8 +- src/connection/packet.rs | 16 +++- src/enc/sym.rs | 23 +++--- src/lib.rs | 47 +++++++++++- 6 files changed, 215 insertions(+), 45 deletions(-) diff --git a/src/auth/mod.rs b/src/auth/mod.rs index f7fd3ec..17f43b9 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,5 +1,8 @@ //! Authentication reslated struct definitions +use ::ring::rand::SecureRandom; +use ::zeroize::Zeroize; + /// User identifier. 16 bytes for easy uuid conversion #[derive(Debug, Copy, Clone)] pub struct UserID([u8; 16]); @@ -11,13 +14,20 @@ impl From<[u8; 16]> for UserID { } impl UserID { + /// New random user id + pub fn new(rand: &::ring::rand::SystemRandom) -> Self { + let mut ret = Self([0; 16]); + rand.fill(&mut ret.0); + ret + } /// length of the User ID in bytes pub const fn len() -> usize { 16 } } /// Authentication Token, basically just 32 random bytes -#[derive(Copy, Clone)] +#[derive(Clone, Zeroize)] +#[zeroize(drop)] pub struct Token([u8; 32]); impl Token { @@ -42,3 +52,45 @@ impl ::core::fmt::Debug for Token { ::core::fmt::Debug::fmt("[hidden token]", f) } } + +/// domain representation +/// Security notice: internal representation is utf8, but we will +/// further limit to a "safe" subset of utf8 +// SECURITY: TODO: limit to a subset of utf8 +#[derive(Debug, Clone, PartialEq)] +pub struct Domain(String); + +impl TryFrom<&[u8]> for Domain { + type Error = (); + fn try_from(raw: &[u8]) -> Result { + let domain_string = match String::from_utf8(raw.into()) { + Ok(domain_string) => domain_string, + Err(_) => return Err(()), + }; + Ok(Domain(domain_string)) + } +} + +impl Domain { + /// length of the User ID in bytes + pub fn len(&self) -> usize { + self.0.len() + } +} + +/// The Service ID is a UUID associated with the service. +#[derive(Debug, Copy, Clone)] +pub struct ServiceID([u8; 16]); + +impl From<[u8; 16]> for ServiceID { + fn from(raw: [u8; 16]) -> Self { + ServiceID(raw) + } +} + +impl ServiceID { + /// length of the User ID in bytes + pub const fn len() -> usize { + 16 + } +} diff --git a/src/connection/handshake/dirsync.rs b/src/connection/handshake/dirsync.rs index 788a231..564a810 100644 --- a/src/connection/handshake/dirsync.rs +++ b/src/connection/handshake/dirsync.rs @@ -53,7 +53,7 @@ impl Req { } impl super::HandshakeParsing for Req { - fn parse(raw: &[u8]) -> Result { + fn deserialize(raw: &[u8]) -> Result { const MIN_PKT_LEN: usize = 10; if raw.len() < MIN_PKT_LEN { return Err(Error::NotEnoughData); @@ -119,53 +119,117 @@ impl ReqInner { } } +/// Informations needed for authentication +#[derive(Debug, Clone)] +pub struct AuthInfo { + /// User of the domain + pub user: auth::UserID, + /// Authentication token + pub token: auth::Token, + /// service ID that we want to use on the domain + pub service_id: auth::ServiceID, + /// subdomain on which we authenticate + // SECURITY: TODO: this should be padded to multiples of 32 bytes or so + // reason: packet length can let you infer the authentication domain + pub domain: auth::Domain, +} + +impl AuthInfo { + /// Minimum length of the authentication info + pub const MIN_PKT_LEN: usize = + auth::UserID::len() + auth::Token::len() + auth::ServiceID::len() + 1; + /// Actual length of the authentication info + pub fn len(&self) -> usize { + Self::MIN_PKT_LEN + self.domain.len() + } + /// deserialize from raw bytes + pub fn deserialize(raw: &[u8]) -> Result { + if raw.len() < Self::MIN_PKT_LEN { + return Err(Error::NotEnoughData); + } + let raw_user_id: [u8; auth::UserID::len()]; + let raw_token: [u8; auth::Token::len()]; + let raw_service_id: [u8; auth::ServiceID::len()]; + let mut start = 0; + let mut end = auth::UserID::len(); + raw_user_id = raw[start..end].try_into().unwrap(); + let user: auth::UserID = raw_user_id.into(); + start = end; + end = end + auth::Token::len(); + raw_token = raw[start..end].try_into().unwrap(); + let token: auth::Token = raw_token.into(); + start = end; + end = end + auth::ServiceID::len(); + raw_service_id = raw[start..end].try_into().unwrap(); + let service_id: auth::ServiceID = raw_service_id.into(); + start = end; + let domain_len = raw[start]; + start = start + 1; + end = start + domain_len as usize; + if raw.len() < end { + return Err(Error::NotEnoughData); + } + let domain: auth::Domain = match raw[start..end].try_into() { + Ok(domain) => domain, + Err(_) => return Err(Error::Parsing), + }; + Ok(Self { + user, + token, + service_id, + domain, + }) + } +} + /// Decrypted request data -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct ReqData { /// Random nonce, the client can use this to track multiple key exchanges pub nonce: Nonce, /// Client key id so the client can use and rotate keys pub client_key_id: KeyID, - /// User of the domain - pub user: auth::UserID, - /// Authentication token - pub token: auth::Token, /// Receiving connection id for the client pub id: ID, - // TODO: service info + /// Authentication data + pub auth: AuthInfo, } impl ReqData { + /// Minimum byte length of the request data + pub const MIN_PKT_LEN: usize = + 16 + KeyID::len() + ID::len() + AuthInfo::MIN_PKT_LEN; /// Parse the cleartext raw data - pub fn parse(raw: &ReqInner) -> Result { - const MIN_PKT_LEN: usize = 16 - + KeyID::len() - + auth::UserID::len() - + auth::Token::len() - + ID::len(); + pub fn deserialize(raw: &ReqInner) -> Result { let raw = match raw { + // raw is VecDeque, assume everything is on the first slice ReqInner::Cleartext(raw) => raw.as_slices().0, _ => return Err(Error::Parsing), }; - if raw.len() < MIN_PKT_LEN { + if raw.len() < Self::MIN_PKT_LEN { return Err(Error::NotEnoughData); } - let nonce: Nonce = raw.try_into().unwrap(); + let mut start = 0; + let mut end = 16; + let nonce: Nonce = raw[start..end].try_into().unwrap(); + start = end; + end = end + KeyID::len(); let client_key_id = - KeyID(u16::from_le_bytes(raw[16..17].try_into().unwrap())); - let raw_user: [u8; 16] = raw[18..34].try_into().unwrap(); - let user: auth::UserID = raw_user.into(); - let raw_token: [u8; 32] = raw[34..66].try_into().unwrap(); - let token: auth::Token = raw_token.into(); - let id: ID = u64::from_le_bytes(raw[66..74].try_into().unwrap()).into(); + KeyID(u16::from_le_bytes(raw[start..end].try_into().unwrap())); + start = end; + end = end + ID::len(); + let id: ID = + u64::from_le_bytes(raw[start..end].try_into().unwrap()).into(); if id.is_handshake() { return Err(Error::Parsing); } + start = end; + //end = raw.len(); + let auth = AuthInfo::deserialize(&raw[start..])?; Ok(Self { nonce, client_key_id, - user, - token, id, + auth, }) } } @@ -180,7 +244,7 @@ pub struct Resp { } impl super::HandshakeParsing for Resp { - fn parse(raw: &[u8]) -> Result { + fn deserialize(raw: &[u8]) -> Result { todo!() } } diff --git a/src/connection/handshake/mod.rs b/src/connection/handshake/mod.rs index f1149bd..ae8bf85 100644 --- a/src/connection/handshake/mod.rs +++ b/src/connection/handshake/mod.rs @@ -68,7 +68,7 @@ pub struct Handshake { impl Handshake { const MIN_PKT_LEN: usize = 8; /// Parse the packet and return the parsed handshake - pub fn parse(raw: &[u8]) -> Result { + pub fn deserialize(raw: &[u8]) -> Result { if raw.len() < Self::MIN_PKT_LEN { return Err(Error::NotEnoughData); } @@ -81,8 +81,8 @@ impl Handshake { None => return Err(Error::Parsing), }; let data = match handshake_kind { - Kind::DirSyncReq => dirsync::Req::parse(&raw[2..])?, - Kind::DirSyncResp => dirsync::Resp::parse(&raw[2..])?, + Kind::DirSyncReq => dirsync::Req::deserialize(&raw[2..])?, + Kind::DirSyncResp => dirsync::Resp::deserialize(&raw[2..])?, }; Ok(Self { fenrir_version, @@ -95,5 +95,5 @@ impl Handshake { } trait HandshakeParsing { - fn parse(raw: &[u8]) -> Result; + fn deserialize(raw: &[u8]) -> Result; } diff --git a/src/connection/packet.rs b/src/connection/packet.rs index db97c8b..7fef6b2 100644 --- a/src/connection/packet.rs +++ b/src/connection/packet.rs @@ -13,6 +13,20 @@ pub enum ConnectionID { } impl ConnectionID { + /// New random service ID + pub fn new_rand(rand: &::ring::rand::SystemRandom) -> Self { + use ::ring::rand::SecureRandom; + let mut raw = [0; 8]; + let mut num = 0; + while num == 0 { + rand.fill(&mut raw); + num = u64::from_le_bytes(raw); + } + #[allow(unsafe_code)] + unsafe { + ConnectionID::ID(::core::num::NonZeroU64::new_unchecked(num)) + } + } /// Quick check to know if this is an handshake pub fn is_handshake(&self) -> bool { *self == ConnectionID::Handshake @@ -23,7 +37,7 @@ impl ConnectionID { } /// write the ID to a buffer pub fn serialize(&self, out: &mut [u8]) { - assert!(out.len() == 8, "Insufficient buffer"); + assert!(out.len() == 8, "out buffer must be 8 bytes"); match self { ConnectionID::Handshake => out[..].copy_from_slice(&[0; 8]), ConnectionID::ID(id) => { diff --git a/src/enc/sym.rs b/src/enc/sym.rs index a7e1c80..ecf5c9b 100644 --- a/src/enc/sym.rs +++ b/src/enc/sym.rs @@ -9,30 +9,31 @@ use ::zeroize::Zeroize; #[derive(Zeroize)] #[zeroize(drop)] #[allow(missing_debug_implementations)] -pub struct Secret { - secret: [u8; 32], -} +pub struct Secret([u8; 32]); impl Secret { + /// New randomly generated secret + pub fn new_rand(rand: &::ring::rand::SystemRandom) -> Self { + use ::ring::rand::SecureRandom; + let mut ret = Self([0; 32]); + rand.fill(&mut ret.0); + ret + } /// return a reference to the secret pub fn as_ref(&self) -> &[u8; 32] { - &self.secret + &self.0 } } impl From<[u8; 32]> for Secret { fn from(shared_secret: [u8; 32]) -> Self { - Self { - secret: shared_secret, - } + Self(shared_secret) } } impl From<::x25519_dalek::SharedSecret> for Secret { fn from(shared_secret: ::x25519_dalek::SharedSecret) -> Self { - Self { - secret: shared_secret.to_bytes(), - } + Self(shared_secret.to_bytes()) } } @@ -234,7 +235,7 @@ impl ::core::fmt::Debug for Nonce { ) -> Result<(), ::std::fmt::Error> { #[allow(unsafe_code)] unsafe { - core::fmt::Debug::fmt(&self.num, f) + ::core::fmt::Debug::fmt(&self.num, f) } } } diff --git a/src/lib.rs b/src/lib.rs index fa19e94..621f4e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,8 @@ pub enum Error { // No async here struct FenrirInner { + // PERF: rand uses syscalls. should we do that async? + rand: ::ring::rand::SystemRandom, key_exchanges: ArcSwapAny>>, ciphers: ArcSwapAny>>, keys: ArcSwapAny>>, @@ -140,7 +142,7 @@ impl FenrirInner { return Err(handshake::Error::Key(e).into()); } } - req.set_data(dirsync::ReqData::parse(&req.data)?); + req.set_data(dirsync::ReqData::deserialize(&req.data)?); return Ok(HandshakeAction::AuthNeeded(handshake)); } @@ -156,6 +158,8 @@ type TokenChecker = fn( user: auth::UserID, token: auth::Token, + service_id: auth::ServiceID, + domain: auth::Domain, ) -> ::futures::future::BoxFuture<'static, Result>; /// Instance of a fenrir endpoint @@ -194,6 +198,7 @@ impl Fenrir { dnssec: None, stop_working: sender, _inner: Arc::new(FenrirInner { + rand: ::ring::rand::SystemRandom::new(), ciphers: ArcSwapAny::new(Arc::new(Vec::new())), key_exchanges: ArcSwapAny::new(Arc::new(Vec::new())), keys: ArcSwapAny::new(Arc::new(Vec::new())), @@ -376,7 +381,7 @@ impl Fenrir { let raw_id: [u8; 8] = buffer.try_into().expect("unreachable"); if ID::from(raw_id).is_handshake() { use connection::handshake::Handshake; - let handshake = match Handshake::parse(&buffer[8..]) { + let handshake = match Handshake::deserialize(&buffer[8..]) { Ok(handshake) => handshake, Err(e) => { ::tracing::warn!("Handshake parsing: {}", e); @@ -393,7 +398,7 @@ impl Fenrir { match action { HandshakeAction::AuthNeeded(hshake) => { let tk_check = match token_check.load_full() { - Some(tokenchecker) => tokenchecker, + Some(tk_check) => tk_check, None => { ::tracing::error!( "Handshake received, but no tocken_checker" @@ -418,7 +423,41 @@ impl Fenrir { return; } }; - tk_check(req_data.user, req_data.token).await; + let is_authenticated = match tk_check( + req_data.auth.user, + req_data.auth.token, + req_data.auth.service_id, + req_data.auth.domain, + ) + .await + { + Ok(is_authenticated) => is_authenticated, + Err(_) => { + ::tracing::error!( + "error in token auth" + ); + // TODO: retry? + return; + } + }; + if !is_authenticated { + ::tracing::warn!( + "Wrong authentication for user {:?}", + req_data.auth.user + ); + // TODO: error response + return; + } + // Client has correctly authenticated + // TODO: contact the service, get the key and + // connection ID + let srv_conn_id = + connection::ID::new_rand(&fenrir.rand); + let auth_conn_id = + connection::ID::new_rand(&fenrir.rand); + let srv_secret = + enc::sym::Secret::new_rand(&fenrir.rand); + todo!() } _ => {