aboutsummaryrefslogtreecommitdiff
path: root/src/client.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client.rs')
-rw-r--r--src/client.rs369
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>,