From 6f6142982b0919fdaddfe3b9293f7ccae4ab3755 Mon Sep 17 00:00:00 2001 From: trivernis Date: Fri, 6 Nov 2020 14:54:58 +0100 Subject: [PATCH] Change auth to two-sided encrypted authentication Signed-off-by: trivernis --- Cargo.toml | 5 +- src/crypto/mod.rs | 11 ++- src/event/mod.rs | 11 ++- src/result.rs | 4 + src/server/mod.rs | 170 +++++++++++++++++++++--------------- src/server/server_events.rs | 15 ++-- tests/test_communication.rs | 5 ++ 7 files changed, 142 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c35149..ec58e7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,7 @@ sha2 = "0.9.2" generic-array = "0.14.4" typenum = "1.12.0" x25519-dalek = "1.1.0" -crossbeam-utils = "0.8.0" \ No newline at end of file +crossbeam-utils = "0.8.0" + +[dev-dependencies] +simple_logger = "1.11.0" \ No newline at end of file diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index e8e4fbc..f90f193 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -48,6 +48,7 @@ impl CryptoStream { pub fn send(&self, mut event: Event) -> VentedResult<()> { let number = self.sent_count.fetch_add(1, Ordering::SeqCst); let nonce = generate_nonce(number); + let ciphertext = self.secret_box.lock().encrypt( &nonce, Payload { @@ -59,10 +60,14 @@ impl CryptoStream { let mut length_raw = [0u8; 8]; BigEndian::write_u64(&mut length_raw, ciphertext.len() as u64); + log::trace!("Encoded event '{}' to raw message", event.name); + stream.write(&length_raw)?; stream.write(&ciphertext)?; stream.flush()?; + log::trace!("Event sent"); + Ok(()) } @@ -75,6 +80,7 @@ impl CryptoStream { let length = BigEndian::read_u64(&length_raw); let mut ciphertext = vec![0u8; length as usize]; stream.read(&mut ciphertext)?; + log::trace!("Received raw message"); let number = self.recv_count.fetch_add(1, Ordering::SeqCst); let nonce = generate_nonce(number); @@ -86,7 +92,10 @@ impl CryptoStream { }, )?; - Event::from_bytes(&mut &plaintext[..]) + let event = Event::from_bytes(&mut &plaintext[..])?; + log::trace!("Decoded message to event '{}'", event.name); + + Ok(event) } } diff --git a/src/event/mod.rs b/src/event/mod.rs index a38f76b..c58e2ca 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -24,9 +24,9 @@ pub struct Event { impl Event { /// Creates a new Event with an empty payload - pub fn new(name: String) -> Self { + pub fn new(name: S) -> Self { Self { - name, + name: name.to_string(), payload: Vec::with_capacity(0), } } @@ -34,9 +34,12 @@ impl Event { impl Event { /// Creates a new Event with a payload - pub fn with_payload(name: String, payload: &T) -> Self { + pub fn with_payload(name: S, payload: &T) -> Self { let payload = rmp_serde::encode::to_vec(payload).unwrap(); - Self { name, payload } + Self { + name: name.to_string(), + payload, + } } /// Returns the byte representation for the message diff --git a/src/result.rs b/src/result.rs index 326aac0..b3c940c 100644 --- a/src/result.rs +++ b/src/result.rs @@ -14,6 +14,8 @@ pub enum VentedError { CryptoError(crypto_box::aead::Error), UnexpectedEvent(String), UnknownNode(String), + Rejected, + AuthFailed, } impl fmt::Display for VentedError { @@ -28,6 +30,8 @@ impl fmt::Display for VentedError { Self::UnknownNode(n) => write!(f, "Received connection from unknown node: {}", n), Self::NotReady => write!(f, "The connection is still being established."), Self::NotAServer(n) => write!(f, "The given node {} is not a server", n), + Self::Rejected => write!(f, "The connection was rejected"), + Self::AuthFailed => write!(f, "Failed to authenticate the other party"), } } } diff --git a/src/server/mod.rs b/src/server/mod.rs index fbaa753..635cd48 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -11,8 +11,8 @@ use crate::result::VentedError::UnknownNode; use crate::result::{VentedError, VentedResult}; use crate::server::data::{Node, ServerConnectionContext}; use crate::server::server_events::{ - AuthPayload, NodeInformationPayload, AUTH_EVENT, CONNECT_EVENT, CONN_ACCEPT_EVENT, - CONN_CHALLENGE_EVENT, CONN_REJECT_EVENT, READY_EVENT, + AuthPayload, ChallengePayload, NodeInformationPayload, ACCEPT_EVENT, AUTH_EVENT, + CHALLENGE_EVENT, CONNECT_EVENT, READY_EVENT, REJECT_EVENT, }; use crossbeam_utils::sync::WaitGroup; use parking_lot::Mutex; @@ -188,7 +188,13 @@ impl VentedServer { let event_handler = Arc::clone(¶ms.event_handler); pool.lock().execute(move || { - let stream = VentedServer::get_crypto_stream(params, stream).expect("Listener failed"); + let stream = match VentedServer::get_crypto_stream(params, stream) { + Ok(stream) => stream, + Err(e) => { + log::error!("Failed to establish encrypted connection: {}", e); + return; + } + }; event_handler .lock() .handle_event(Event::new(READY_EVENT.to_string())); @@ -202,19 +208,19 @@ impl VentedServer { Ok(()) } + /// Establishes a crypto stream for the given stream fn get_crypto_stream( params: ServerConnectionContext, - mut stream: TcpStream, + stream: TcpStream, ) -> VentedResult { - let (node_id, secret_box) = VentedServer::perform_key_exchange( + let (node_id, stream) = VentedServer::perform_key_exchange( params.is_server, - &mut stream, + stream, params.node_id.clone(), params.global_secret, params.known_nodes, )?; - let stream = CryptoStream::new(stream, secret_box)?; params .connections .lock() @@ -251,11 +257,11 @@ impl VentedServer { /// Performs a key exchange fn perform_key_exchange( is_server: bool, - stream: &mut TcpStream, + stream: TcpStream, own_node_id: String, global_secret: SecretKey, known_nodes: Arc>>, - ) -> VentedResult<(String, ChaChaBox)> { + ) -> VentedResult<(String, CryptoStream)> { let secret_key = SecretKey::generate(&mut rand::thread_rng()); if is_server { Self::perform_server_key_exchange( @@ -278,15 +284,15 @@ impl VentedServer { /// Performs the client side DH key exchange fn perform_client_key_exchange( - mut stream: &mut TcpStream, + mut stream: TcpStream, secret_key: &SecretKey, own_node_id: String, global_secret: SecretKey, known_nodes: Arc>>, - ) -> VentedResult<(String, ChaChaBox)> { + ) -> VentedResult<(String, CryptoStream)> { stream.write( &Event::with_payload( - CONNECT_EVENT.to_string(), + CONNECT_EVENT, &NodeInformationPayload { public_key: secret_key.public_key().to_bytes(), node_id: own_node_id, @@ -296,59 +302,45 @@ impl VentedServer { )?; stream.flush()?; let event = Event::from_bytes(&mut stream)?; - if event.name != CONN_CHALLENGE_EVENT { - return Err(VentedError::UnknownNode(event.name)); + if event.name != CONNECT_EVENT { + return Err(VentedError::UnexpectedEvent(event.name)); } let NodeInformationPayload { public_key, node_id, } = event.get_payload::().unwrap(); - let public_key = PublicKey::from(public_key); - let shared_auth_secret = - StaticSecret::from(global_secret.to_bytes()).diffie_hellman(&public_key); - stream.write( - &Event::with_payload( - AUTH_EVENT.to_string(), - &AuthPayload { - calculated_secret: shared_auth_secret.to_bytes(), - }, - ) - .as_bytes(), - )?; + let public_key = PublicKey::from(public_key); + let secret_box = ChaChaBox::new(&public_key, &secret_key); - let event = Event::from_bytes(&mut stream)?; - if event.name != CONN_ACCEPT_EVENT { - return Err(VentedError::UnknownNode(event.name)); - } - let known_nodes = known_nodes.lock(); - let node_static_info = event.get_payload::()?; - let node_data = if let Some(data) = known_nodes - .iter() - .find(|n| n.id == node_static_info.node_id) - { + let node_data = if let Some(data) = known_nodes.lock().iter().find(|n| n.id == node_id) { data.clone() } else { + stream.write(&Event::new(REJECT_EVENT).as_bytes())?; + stream.flush()?; return Err(UnknownNode(node_id)); }; - if node_data.public_key.to_bytes() != node_static_info.public_key { - return Err(UnknownNode(node_id)); - } - let secret_box = ChaChaBox::new(&public_key, &secret_key); + let mut stream = CryptoStream::new(stream, secret_box)?; - Ok((node_id, secret_box)) + log::trace!("Authenticating recipient..."); + Self::authenticate_other(&mut stream, node_data.public_key)?; + log::trace!("Authenticating self..."); + Self::authenticate_self(&mut stream, StaticSecret::from(global_secret.to_bytes()))?; + log::trace!("Connection fully authenticated."); + + Ok((node_id, stream)) } /// Performs a DH key exchange by using the crypto_box module and events /// On success it returns a secret box with the established secret and the node id of the client fn perform_server_key_exchange( - mut stream: &mut TcpStream, + mut stream: TcpStream, secret_key: &SecretKey, own_node_id: String, global_secret: SecretKey, known_nodes: Arc>>, - ) -> VentedResult<(String, ChaChaBox)> { + ) -> VentedResult<(String, CryptoStream)> { let event = Event::from_bytes(&mut stream)?; if event.name != CONNECT_EVENT { return Err(VentedError::UnexpectedEvent(event.name)); @@ -359,54 +351,96 @@ impl VentedServer { } = event.get_payload::().unwrap(); let public_key = PublicKey::from(public_key); - let known_nodes = known_nodes.lock(); - let node_data = if let Some(data) = known_nodes.iter().find(|n| n.id == node_id) { + let node_data = if let Some(data) = known_nodes.lock().iter().find(|n| n.id == node_id) { data.clone() } else { - stream.write(&Event::new(CONN_REJECT_EVENT.to_string()).as_bytes())?; + stream.write(&Event::new(REJECT_EVENT).as_bytes())?; stream.flush()?; return Err(UnknownNode(node_id)); }; - let secret_box = ChaChaBox::new(&public_key, &secret_key); stream.write( &Event::with_payload( - CONN_CHALLENGE_EVENT.to_string(), + CONNECT_EVENT, &NodeInformationPayload { public_key: secret_key.public_key().to_bytes(), - node_id: own_node_id.clone(), + node_id: own_node_id, }, ) .as_bytes(), )?; stream.flush()?; - let auth_event = Event::from_bytes(&mut stream)?; + let secret_box = ChaChaBox::new(&public_key, &secret_key); + let mut stream = CryptoStream::new(stream, secret_box)?; + + log::trace!("Authenticating self..."); + Self::authenticate_self(&mut stream, StaticSecret::from(global_secret.to_bytes()))?; + log::trace!("Authenticating recipient..."); + Self::authenticate_other(&mut stream, node_data.public_key)?; + log::trace!("Connection fully authenticated."); + + Ok((node_id, stream)) + } + + /// Performs the challenged side of the authentication challenge + fn authenticate_self(stream: &CryptoStream, static_secret: StaticSecret) -> VentedResult<()> { + let challenge_event = stream.read()?; + + if challenge_event.name != CHALLENGE_EVENT { + stream.send(Event::new(REJECT_EVENT))?; + return Err(VentedError::UnexpectedEvent(challenge_event.name)); + } + let ChallengePayload { public_key } = challenge_event.get_payload()?; + let auth_key = static_secret.diffie_hellman(&PublicKey::from(public_key)); + + stream.send(Event::with_payload( + AUTH_EVENT, + &AuthPayload { + calculated_secret: auth_key.to_bytes(), + }, + ))?; + + let response = stream.read()?; + + match response.name.as_str() { + ACCEPT_EVENT => Ok(()), + REJECT_EVENT => Err(VentedError::Rejected), + _ => { + stream.send(Event::new(REJECT_EVENT))?; + Err(VentedError::UnexpectedEvent(response.name)) + } + } + } + + /// Authenticates the other party by using their stored public key and a generated secret + fn authenticate_other( + stream: &CryptoStream, + other_static_public: PublicKey, + ) -> VentedResult<()> { + let auth_secret = SecretKey::generate(&mut rand::thread_rng()); + stream.send(Event::with_payload( + CHALLENGE_EVENT, + &ChallengePayload { + public_key: auth_secret.public_key().to_bytes(), + }, + ))?; + + let auth_event = stream.read()?; if auth_event.name != AUTH_EVENT { + stream.send(Event::new(REJECT_EVENT))?; return Err(VentedError::UnexpectedEvent(auth_event.name)); } - let AuthPayload { calculated_secret } = auth_event.get_payload::()?; + let AuthPayload { calculated_secret } = auth_event.get_payload()?; let expected_secret = - StaticSecret::from(secret_key.to_bytes()).diffie_hellman(&node_data.public_key); + StaticSecret::from(auth_secret.to_bytes()).diffie_hellman(&other_static_public); if expected_secret.to_bytes() != calculated_secret { - stream.write(&Event::new(CONN_REJECT_EVENT.to_string()).as_bytes())?; - stream.flush()?; - return Err(UnknownNode(node_id)); + stream.send(Event::new(REJECT_EVENT))?; + Err(VentedError::AuthFailed) } else { - stream.write( - &Event::with_payload( - CONN_ACCEPT_EVENT.to_string(), - &NodeInformationPayload { - node_id: own_node_id, - public_key: global_secret.public_key().to_bytes(), - }, - ) - .as_bytes(), - )?; - stream.flush()?; + stream.send(Event::new(ACCEPT_EVENT))?; + Ok(()) } - - Ok((node_id, secret_box)) } } diff --git a/src/server/server_events.rs b/src/server/server_events.rs index f816c82..c65cb88 100644 --- a/src/server/server_events.rs +++ b/src/server/server_events.rs @@ -1,10 +1,10 @@ use serde::{Deserialize, Serialize}; -pub(crate) const CONNECT_EVENT: &str = "client:connect"; -pub(crate) const AUTH_EVENT: &str = "client:authenticate"; -pub(crate) const CONN_CHALLENGE_EVENT: &str = "server:conn_challenge"; -pub(crate) const CONN_ACCEPT_EVENT: &str = "server:conn_accept"; -pub(crate) const CONN_REJECT_EVENT: &str = "server:conn_reject"; +pub(crate) const CONNECT_EVENT: &str = "conn:connect"; +pub(crate) const AUTH_EVENT: &str = "conn:authenticate"; +pub(crate) const CHALLENGE_EVENT: &str = "conn:challenge"; +pub(crate) const ACCEPT_EVENT: &str = "conn:accept"; +pub(crate) const REJECT_EVENT: &str = "conn:reject"; pub const READY_EVENT: &str = "connection:ready"; @@ -14,6 +14,11 @@ pub(crate) struct NodeInformationPayload { pub public_key: [u8; 32], } +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct ChallengePayload { + pub public_key: [u8; 32], +} + #[derive(Serialize, Deserialize, Debug)] pub(crate) struct AuthPayload { pub calculated_secret: [u8; 32], diff --git a/tests/test_communication.rs b/tests/test_communication.rs index d438002..67d49aa 100644 --- a/tests/test_communication.rs +++ b/tests/test_communication.rs @@ -8,8 +8,13 @@ use vented::server::data::Node; use vented::server::server_events::READY_EVENT; use vented::server::VentedServer; +fn setup() { + simple_logger::SimpleLogger::new().init().unwrap(); +} + #[test] fn test_server_communication() { + setup(); let ping_count = Arc::new(AtomicUsize::new(0)); let pong_count = Arc::new(AtomicUsize::new(0)); let ready_count = Arc::new(AtomicUsize::new(0));