diff options
Diffstat (limited to 'src/client.rs')
-rw-r--r-- | src/client.rs | 369 |
1 files changed, 10 insertions, 359 deletions
diff --git a/src/client.rs b/src/client.rs index b2da0c3..a7a12f2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,20 +1,15 @@ use std::{ - collections::HashMap, error::Error, - fmt, - fs::File, - io::{BufRead, BufReader, Write}, net::SocketAddr, - path::Path, sync::{Arc, Mutex}, }; use bevy::prelude::*; use bytes::Bytes; -use futures::{executor::block_on, sink::SinkExt}; +use futures::sink::SinkExt; use futures_util::StreamExt; use quinn::{ClientConfig, Endpoint}; -use rustls::{Certificate, ServerName}; +use rustls::Certificate; use serde::Deserialize; use tokio::{ runtime::Runtime, @@ -32,38 +27,22 @@ use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; use crate::{QuinnetError, DEFAULT_KILL_MESSAGE_QUEUE_SIZE, DEFAULT_MESSAGE_QUEUE_SIZE}; +use self::certificate::{ + load_known_hosts_store_from_config, CertVerificationStatus, CertVerifierAction, + CertificateFingerprint, CertificateInteractionEvent, CertificateUpdateEvent, + CertificateVerificationMode, ServName, SkipServerVerification, TofuServerVerification, +}; + +pub mod certificate; + pub const DEFAULT_INTERNAL_MESSAGE_CHANNEL_SIZE: usize = 100; pub const DEFAULT_KNOWN_HOSTS_FILE: &str = "quinnet/known_hosts"; -pub const DEFAULT_CERT_VERIFIER_BEHAVIOUR: CertVerifierBehaviour = - CertVerifierBehaviour::ImmediateAction(CertVerifierAction::AbortConnection); /// Connection event raised when the client just connected to the server. Raised in the CoreStage::PreUpdate stage. pub struct ConnectionEvent; /// ConnectionLost event raised when the client is considered disconnected from the server. Raised in the CoreStage::PreUpdate stage. pub struct ConnectionLostEvent; -/// Event raised when a user/app interaction is needed for the server's certificate validation -pub struct CertificateInteractionEvent { - pub status: CertVerificationStatus, - /// Mutex for interior mutability - action_sender: Mutex<Option<oneshot::Sender<CertVerifierAction>>>, -} - -impl CertificateInteractionEvent { - pub fn apply_cert_verifier_action(&self, action: CertVerifierAction) { - let mut sender = self.action_sender.lock().unwrap(); - if let Some(sender) = sender.take() { - sender.send(action).unwrap() - } - } -} - -/// Event raised when a new certificate is trusted -pub struct CertificateUpdateEvent { - pub server_name: ServName, - pub fingerprint: CertificateFingerprint, -} - /// Configuration of the client, used when connecting to a server #[derive(Debug, Deserialize, Clone)] pub struct ClientConfigurationData { @@ -108,68 +87,6 @@ impl ClientConfigurationData { } } -/// How the client should handle the server certificate. -#[derive(Debug, Clone)] -pub enum CertificateVerificationMode { - /// No verification will be done on the server certificate - SkipVerification, - /// Client will only trust a server certificate signed by a conventional certificate authority - SignedByCertificateAuthority, - /// The client will look up the server identifier in [`KnownHosts`]. - /// TODO Revamp doc - /// - If no identifier exists yet for this server, the client will accept the given server's certificate and return it. - /// - If some certificate already existed for this server, and the received one is different, an error will be raised. - TrustOnFirstUse(TrustOnFirstUseConfig), -} - -#[derive(Debug, Clone)] -pub struct TrustOnFirstUseConfig { - known_hosts: KnownHosts, - verifier_behaviour: HashMap<CertVerificationStatus, CertVerifierBehaviour>, -} - -impl Default for TrustOnFirstUseConfig { - fn default() -> Self { - TrustOnFirstUseConfig { - known_hosts: KnownHosts::HostsFile(DEFAULT_KNOWN_HOSTS_FILE.to_string()), - verifier_behaviour: HashMap::from([ - ( - CertVerificationStatus::UnknownCertificate, - CertVerifierBehaviour::ImmediateAction(CertVerifierAction::TrustAndStore), - ), - ( - CertVerificationStatus::UntrustedCertificate, - CertVerifierBehaviour::RequestClientAction, - ), - ( - CertVerificationStatus::TrustedCertificate, - CertVerifierBehaviour::ImmediateAction(CertVerifierAction::TrustOnce), - ), - ]), - } - } -} - -// pub enum CertVerificationStatus {} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum CertVerifierBehaviour { - /// Raises an event to the client app (containing the cert info) and waits for an API call - RequestClientAction, - /// Take action immediately, see [`CertVerifierAction`]. - ImmediateAction(CertVerifierAction), -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum CertVerifierAction { - /// Abort the connection and raise an error (containing the cert info) - AbortConnection, - /// Continue the connection discarding the cert info - TrustOnce, - /// Continue the connection and add the cert info to the store - TrustAndStore, -} - /// Current state of the client driver #[derive(Debug, PartialEq, Eq)] enum ClientState { @@ -281,272 +198,6 @@ impl Client { } } -/// Implementation of `ServerCertVerifier` that verifies everything as trustworthy. -struct SkipServerVerification; - -impl SkipServerVerification { - fn new() -> Arc<Self> { - Arc::new(Self) - } -} - -impl rustls::client::ServerCertVerifier for SkipServerVerification { - fn verify_server_cert( - &self, - _end_entity: &rustls::Certificate, - _intermediates: &[rustls::Certificate], - _server_name: &rustls::ServerName, - _scts: &mut dyn Iterator<Item = &[u8]>, - _ocsp_response: &[u8], - _now: std::time::SystemTime, - ) -> Result<rustls::client::ServerCertVerified, rustls::Error> { - Ok(rustls::client::ServerCertVerified::assertion()) - } -} - -/// SHA-256 hash of the certificate data in DER form -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CertificateFingerprint([u8; 32]); - -impl From<&rustls::Certificate> for CertificateFingerprint { - fn from(cert: &rustls::Certificate) -> CertificateFingerprint { - let hash = ring::digest::digest(&ring::digest::SHA256, &cert.0); - let fingerprint_bytes = hash.as_ref().try_into().unwrap(); - CertificateFingerprint(fingerprint_bytes) - } -} - -impl fmt::Display for CertificateFingerprint { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&base64::encode(&self.0), f) - } -} - -pub type CertStore = HashMap<ServName, CertificateFingerprint>; - -#[derive(Debug, Clone)] -pub enum KnownHosts { - Store(CertStore), - HostsFile(String), -} - -/// Implementation of `ServerCertVerifier` that follows the Trust on first use authentication scheme. -struct TofuServerVerification { - store: CertStore, - verifier_behaviour: HashMap<CertVerificationStatus, CertVerifierBehaviour>, - to_sync_client: mpsc::Sender<InternalAsyncMessage>, - - /// If present, the file where new fingerprints should be stored - hosts_file: Option<String>, -} - -impl TofuServerVerification { - fn new( - store: CertStore, - verifier_behaviour: HashMap<CertVerificationStatus, CertVerifierBehaviour>, - to_sync_client: mpsc::Sender<InternalAsyncMessage>, - hosts_file: Option<String>, - ) -> Arc<Self> { - Arc::new(Self { - store, - verifier_behaviour, - to_sync_client, - hosts_file, - }) - } - - fn apply_verifier_behaviour_for_status( - &self, - status: CertVerificationStatus, - server_name: &ServName, - fingerprint: CertificateFingerprint, - ) -> Result<rustls::client::ServerCertVerified, rustls::Error> { - let behaviour = self - .verifier_behaviour - .get(&status) - .unwrap_or(&DEFAULT_CERT_VERIFIER_BEHAVIOUR); - match behaviour { - CertVerifierBehaviour::ImmediateAction(action) => { - self.apply_verifier_immediate_action(action, server_name, fingerprint) - } - CertVerifierBehaviour::RequestClientAction => { - let (action_sender, cert_action_recv) = oneshot::channel::<CertVerifierAction>(); - self.to_sync_client - .try_send(InternalAsyncMessage::CertificateActionRequest { - status, - action_sender, - }) - .unwrap(); - match block_on(cert_action_recv) { - Ok(action) => { - self.apply_verifier_immediate_action(&action, server_name, fingerprint) - } - Err(err) => Err(rustls::Error::InvalidCertificateData(format!( - "Failed to receive CertVerifierAction: {}", - err - ))), - } - } - } - } - - fn apply_verifier_immediate_action( - &self, - action: &CertVerifierAction, - server_name: &ServName, - fingerprint: CertificateFingerprint, - ) -> Result<rustls::client::ServerCertVerified, rustls::Error> { - match action { - CertVerifierAction::AbortConnection => Err(rustls::Error::InvalidCertificateData( - format!("CertVerifierAction requested to abort the connection"), - )), - CertVerifierAction::TrustOnce => Ok(rustls::client::ServerCertVerified::assertion()), - CertVerifierAction::TrustAndStore => { - // If we need to store them to a file - if let Some(file) = &self.hosts_file { - let mut store_clone = self.store.clone(); - store_clone.insert(server_name.clone(), fingerprint.clone()); - if let Err(store_error) = store_known_hosts_to_file(&file, &store_clone) { - return Err(rustls::Error::General(format!( - "Failed to store new certificate entry: {}", - store_error - ))); - } - } - // In all cases raise an event containing the new certificate entry - match self - .to_sync_client - .try_send(InternalAsyncMessage::TrustedCertificateUpdate { - server_name: server_name.clone(), - fingerprint, - }) { - Ok(_) => Ok(rustls::client::ServerCertVerified::assertion()), - Err(_) => Err(rustls::Error::General(format!( - "Failed to signal new trusted certificate entry" - ))), - } - } - } - } -} - -impl rustls::client::ServerCertVerifier for TofuServerVerification { - fn verify_server_cert( - &self, - _end_entity: &rustls::Certificate, - _intermediates: &[rustls::Certificate], - _server_name: &rustls::ServerName, - _scts: &mut dyn Iterator<Item = &[u8]>, - _ocsp_response: &[u8], - _now: std::time::SystemTime, - ) -> Result<rustls::client::ServerCertVerified, rustls::Error> { - // TODO Could add some optional validity checks on the cert content. - let status; - let fingerprint = CertificateFingerprint::from(_end_entity); - let server_name = ServName(_server_name.clone()); - if let Some(known_fingerprint) = self.store.get(&server_name) { - if *known_fingerprint == fingerprint { - status = Some(CertVerificationStatus::TrustedCertificate); - } else { - status = Some(CertVerificationStatus::UntrustedCertificate); - } - } else { - status = Some(CertVerificationStatus::UnknownCertificate); - } - match status { - Some(status) => { - self.apply_verifier_behaviour_for_status(status, &server_name, fingerprint) - } - None => Err(rustls::Error::InvalidCertificateData(format!( - "Internal error, no CertVerificationStatus" - ))), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum CertVerificationStatus { - /// First time connecting to this host. - UnknownCertificate, - /// The certificate fingerprint does not match the one in the known hosts fingerprints store. - UntrustedCertificate, - /// Known host and certificate matching the one in the known hosts fingerprints store. - TrustedCertificate, -} - -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub struct ServName(ServerName); - -impl fmt::Display for ServName { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.0 { - ServerName::DnsName(dns) => fmt::Display::fmt(dns.as_ref(), f), - ServerName::IpAddress(ip) => fmt::Display::fmt(&ip, f), - _ => todo!(), - } - } -} - -fn store_known_hosts_to_file(file: &String, store: &CertStore) -> Result<(), Box<dyn Error>> { - let path = std::path::Path::new(file); - let prefix = path.parent().unwrap(); - std::fs::create_dir_all(prefix)?; - let mut store_file = File::create(path)?; - for entry in store { - writeln!(store_file, "{} {}", entry.0, entry.1)?; - } - Ok(()) -} - -fn parse_known_host_line( - line: String, -) -> Result<(ServName, CertificateFingerprint), Box<dyn Error>> { - let mut parts = line.split_whitespace(); - - let adr_str = parts.next().ok_or(QuinnetError::InvalidHostFile)?; - let serv_name = ServName(ServerName::try_from(adr_str)?); - - let fingerprint_b64 = parts.next().ok_or(QuinnetError::InvalidHostFile)?; - let fingerprint_bytes = base64::decode(&fingerprint_b64)?; - - match fingerprint_bytes.try_into() { - Ok(buf) => Ok((serv_name, CertificateFingerprint(buf))), - Err(_) => Err(Box::new(QuinnetError::InvalidHostFile)), - } -} - -fn load_known_hosts_from_file( - file_path: String, -) -> Result<(CertStore, Option<String>), Box<dyn Error>> { - let mut store = HashMap::new(); - for line in BufReader::new(File::open(&file_path)?).lines() { - let entry = parse_known_host_line(line?)?; - store.insert(entry.0, entry.1); - } - Ok((store, Some(file_path))) -} - -fn load_known_hosts_store_from_config( - known_host_config: KnownHosts, -) -> Result<(CertStore, Option<String>), Box<dyn Error>> { - match known_host_config { - KnownHosts::Store(store) => Ok((store, None)), - KnownHosts::HostsFile(file) => { - if !Path::new(&file).exists() { - warn!( - "Known hosts file `{}` not found, no known hosts loaded", - file - ); - Ok((HashMap::new(), Some(file))) - } else { - load_known_hosts_from_file(file) - } - } - } -} - fn configure_client( cert_mode: CertificateVerificationMode, to_sync_client: mpsc::Sender<InternalAsyncMessage>, |