diff options
-rw-r--r-- | src/client.rs | 369 | ||||
-rw-r--r-- | src/client/certificate.rs | 369 |
2 files changed, 379 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>, diff --git a/src/client/certificate.rs b/src/client/certificate.rs new file mode 100644 index 0000000..a359b57 --- /dev/null +++ b/src/client/certificate.rs @@ -0,0 +1,369 @@ +use std::{ + collections::HashMap, + error::Error, + fmt, + fs::File, + io::{BufRead, BufReader, Write}, + path::Path, + sync::{Arc, Mutex}, +}; + +use bevy::prelude::warn; +use futures::executor::block_on; +use rustls::ServerName; +use tokio::sync::{mpsc, oneshot}; + +use crate::QuinnetError; + +use super::{InternalAsyncMessage, DEFAULT_KNOWN_HOSTS_FILE}; + +pub const DEFAULT_CERT_VERIFIER_BEHAVIOUR: CertVerifierBehaviour = + CertVerifierBehaviour::ImmediateAction(CertVerifierAction::AbortConnection); + +/// 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 + pub(crate) 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, +} + +/// 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, 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!(), + } + } +} + +#[derive(Debug, Clone)] +pub struct TrustOnFirstUseConfig { + pub(crate) known_hosts: KnownHosts, + pub(crate) 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), + ), + ]), + } + } +} + +#[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, +} + +/// 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 verifies everything as trustworthy. +pub(crate) struct SkipServerVerification; + +impl SkipServerVerification { + pub(crate) 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()) + } +} + +/// Implementation of `ServerCertVerifier` that follows the Trust on first use authentication scheme. +pub(crate) 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 { + pub(crate) 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" + ))), + } + } +} + +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))) +} + +pub(crate) 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) + } + } + } +} |