diff options
author | Henauxg <19689618+Henauxg@users.noreply.github.com> | 2022-11-09 20:03:56 +0100 |
---|---|---|
committer | Henauxg <19689618+Henauxg@users.noreply.github.com> | 2022-11-09 20:03:56 +0100 |
commit | ea058936fcc0a1d328cf8735682d719905237723 (patch) | |
tree | 09c4d54bd4c3ca291d114605e173b344a1fb4dcc | |
parent | a45884e2933a81832dd8c6dc5efc7e529e01184c (diff) |
[client] Continue TOFU implementation : known_hosts file
-rw-r--r-- | src/client.rs | 165 |
1 files changed, 131 insertions, 34 deletions
diff --git a/src/client.rs b/src/client.rs index 611a1c3..37d252a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,11 @@ use std::{ collections::HashMap, error::Error, + fmt, fs::File, - io::{BufRead, BufReader}, + io::{BufRead, BufReader, Write}, net::SocketAddr, + path::Path, sync::Arc, }; @@ -12,7 +14,7 @@ use bytes::Bytes; use futures::{channel::oneshot, executor::block_on, sink::SinkExt}; use futures_util::StreamExt; use quinn::{ClientConfig, Endpoint}; -use rustls::Certificate; +use rustls::{Certificate, ServerName}; use serde::Deserialize; use tokio::{ runtime::Runtime, @@ -166,6 +168,10 @@ pub(crate) enum InternalAsyncMessage { status: CertVerificationStatus, action_sender: oneshot::Sender<CertVerifierAction>, }, + TrustedCertificateUpdate { + server_name: ServName, + fingerprint: CertificateFingerprint, + }, } #[derive(Debug, Clone)] @@ -293,35 +299,50 @@ impl From<&rustls::Certificate> for CertificateFingerprint { } } +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(HashMap<rustls::ServerName, CertificateFingerprint>), + Store(CertStore), HostsFile(String), } /// Implementation of `ServerCertVerifier` that follows the Trust on first use authentication scheme. struct TofuServerVerification { - store: HashMap<rustls::ServerName, CertificateFingerprint>, + 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: HashMap<rustls::ServerName, CertificateFingerprint>, + 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 @@ -330,7 +351,7 @@ impl TofuServerVerification { .unwrap_or(&DEFAULT_CERT_VERIFIER_BEHAVIOUR); match behaviour { CertVerifierBehaviour::ImmediateAction(action) => { - TofuServerVerification::apply_verifier_immediate_action(action, fingerprint) + self.apply_verifier_immediate_action(action, server_name, fingerprint) } CertVerifierBehaviour::RequestClientAction => { let (action_sender, cert_action_recv) = oneshot::channel::<CertVerifierAction>(); @@ -341,12 +362,12 @@ impl TofuServerVerification { }) .unwrap(); match block_on(cert_action_recv) { - Ok(action) => TofuServerVerification::apply_verifier_immediate_action( - &action, - fingerprint, - ), - Err(_) => Err(rustls::Error::InvalidCertificateData(format!( - "Failed to receive CertVerifierAction" + Ok(action) => { + self.apply_verifier_immediate_action(&action, server_name, fingerprint) + } + Err(err) => Err(rustls::Error::InvalidCertificateData(format!( + "Failed to receive CertVerifierAction: {}", + err ))), } } @@ -354,21 +375,38 @@ impl TofuServerVerification { } fn apply_verifier_immediate_action( + &self, action: &CertVerifierAction, + server_name: &ServName, fingerprint: CertificateFingerprint, ) -> Result<rustls::client::ServerCertVerified, rustls::Error> { match action { - CertVerifierAction::AbortConnection => { - // TODO Error handling: details - Err(rustls::Error::InvalidCertificateData(format!( - "CertVerifierAction requested to abort the connection" - ))) - } + CertVerifierAction::AbortConnection => Err(rustls::Error::InvalidCertificateData( + format!("CertVerifierAction requested to abort the connection"), + )), CertVerifierAction::TrustOnce => Ok(rustls::client::ServerCertVerified::assertion()), CertVerifierAction::TrustAndStore => { - // TODO Fix: Store the fingerprint - todo!(); - Ok(rustls::client::ServerCertVerified::assertion()) + 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 + ))); + } + } + 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" + ))), + } } } } @@ -387,7 +425,8 @@ impl rustls::client::ServerCertVerifier for TofuServerVerification { // TODO Could add some optional validity checks on the cert content. let status; let fingerprint = CertificateFingerprint::from(_end_entity); - if let Some(known_fingerprint) = self.store.get(_server_name) { + 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 { @@ -397,7 +436,9 @@ impl rustls::client::ServerCertVerifier for TofuServerVerification { status = Some(CertVerificationStatus::UnknownCertificate); } match status { - Some(status) => self.apply_verifier_behaviour_for_status(status, fingerprint), + Some(status) => { + self.apply_verifier_behaviour_for_status(status, &server_name, fingerprint) + } None => Err(rustls::Error::InvalidCertificateData(format!( "Internal error, no CertVerificationStatus" ))), @@ -415,30 +456,75 @@ pub enum CertVerificationStatus { 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<(rustls::ServerName, CertificateFingerprint), Box<dyn Error>> { - todo!() +) -> 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<HashMap<rustls::ServerName, CertificateFingerprint>, Box<dyn Error>> { + 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() { + 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) + Ok((store, Some(file_path))) } fn load_known_hosts_store_from_config( known_host_config: KnownHosts, -) -> Result<HashMap<rustls::ServerName, CertificateFingerprint>, Box<dyn Error>> { +) -> Result<(CertStore, Option<String>), Box<dyn Error>> { match known_host_config { - KnownHosts::Store(store) => Ok(store), - // TODO Fix: Create a file if none exists + raise a warning - KnownHosts::HostsFile(file) => load_known_hosts_from_file(&file), + 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) + } + } } } @@ -459,13 +545,14 @@ fn configure_client( Ok(ClientConfig::with_native_roots()) } CertificateVerificationMode::TrustOnFirstUse(config) => { - let store = load_known_hosts_store_from_config(config.known_hosts)?; + let (store, store_file) = load_known_hosts_store_from_config(config.known_hosts)?; let crypto = rustls::ClientConfig::builder() .with_safe_defaults() .with_custom_certificate_verifier(TofuServerVerification::new( store, config.verifier_behaviour, to_sync_client, + store_file, )) .with_no_client_auth(); Ok(ClientConfig::new(Arc::new(crypto))) @@ -651,6 +738,16 @@ fn update_sync_client( action_sender, }); } + InternalAsyncMessage::TrustedCertificateUpdate { + server_name, + fingerprint, + } => { + warn!( + "Todo, TrustedCertificateUpdate {:?} {}", + server_name, fingerprint + ) + todo!(); + } } } } |