diff --git a/server/src/lib.rs b/server/src/lib.rs index 9118de3..b0d1cf0 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -70,7 +70,7 @@ use tokio_util::sync::CancellationToken; use crate::logging::ACCESS_LOGGER; use crate::proxy_protocol::read_proxy_header; -use crate::tls::{make_config, subject_from_cert}; +use crate::tls::{find_matching_ca, issuer_from_cert, make_config, subject_from_cert}; pub enum RequestCategory { Enumerate(String), @@ -957,7 +957,8 @@ fn create_tls_server( let svc_monitoring_settings = monitoring_settings.clone(); let subscriptions = collector_subscriptions.clone(); let collector_heartbeat_tx = collector_heartbeat_tx.clone(); - let thumbprint = tls_config.thumbprint.clone(); + let ca_thumbprints = tls_config.thumbprints.clone(); + let tls_acceptor = tls_acceptor.clone(); // Create a "rx" channel end for the task @@ -998,21 +999,37 @@ fn create_tls_server( } }; - // get peer certificate - let cert = stream + // get peer certificates (full chain) + let certificate_chain = stream .get_ref() .1 .peer_certificates() - .expect("Peer certificate should exist") // client auth has to happen, so this should not fail + .expect("Peer certificate should exist"); // client auth has to happen, so this should not fail + let cert = certificate_chain .first() .expect("Peer certificate should not be empty") // client cert cannot be empty if authentication succeeded .clone(); let subject = subject_from_cert(cert.as_ref()).expect("Could not parse client certificate"); + debug!("Incoming TLS connection from subject '{}'", &subject); + + let issuer = issuer_from_cert(cert.as_ref()) + .expect("Could not parse issuer from client certificate"); + debug!("Client certificate issued by '{}'", &issuer); + debug!("Known CAs: {:?}", ca_thumbprints); + + // Try to find a trusted CA in the certificate chain + let matching_ca_entry = match find_matching_ca(certificate_chain, &ca_thumbprints) { + Ok(entry) => entry, + Err(e) => { + warn!( "No trusted CA found in certificate chain for connection from '{}' (subject: '{}'): {:?}", real_client_addr, subject, e); + return; + } + }; // Initialize Authentication context once for each TCP connection - let auth_ctx = AuthenticationContext::Tls(subject, thumbprint); + let auth_ctx = AuthenticationContext::Tls(subject.clone(), matching_ca_entry); // Hyper needs a wrapper for the stream let io = TokioIo::new(stream); diff --git a/server/src/tls.rs b/server/src/tls.rs index 390d7a5..9eec35c 100644 --- a/server/src/tls.rs +++ b/server/src/tls.rs @@ -1,8 +1,9 @@ use anyhow::{bail, Context, Result}; use common::encoding::encode_utf16le; use hex::ToHex; -use log::{debug, info}; +use log::{debug, info, warn}; use sha1::{Digest, Sha1}; +use std::collections::HashMap; use std::fs; use std::io::BufReader; use std::sync::Arc; @@ -66,7 +67,7 @@ pub fn compute_thumbprint(cert_content: &[u8]) -> String { pub struct TlsConfig { pub server: Arc, - pub thumbprint: String, + pub thumbprints: HashMap, } /// Create configuration for TLS connection @@ -77,18 +78,35 @@ pub fn make_config(args: &common::settings::Tls) -> Result { load_priv_key(args.server_private_key()).context("Could not load private key")?; let ca_certs = load_certs(args.ca_certificate()).context("Could not load CA certificate")?; - let ca_cert_content: &[u8] = ca_certs - .first() - .context("CA certificate should contain at least one certificate")? - .as_ref(); - let thumbprint = compute_thumbprint(ca_cert_content); - - debug!("CA Thumbprint from certificate : {}", thumbprint); + // Ensure at least one CA is present + if ca_certs.is_empty() { + bail!("CA certificate should contain at least one certificate"); + } let mut client_auth_roots = RootCertStore::empty(); + let mut ca_thumbprints = HashMap::new(); - // Put all certificates from given CA certificate file into certificate store + // Compute thumbprint for each CA and add to trust store for root in ca_certs { + let subject = subject_from_cert(root.as_ref())?; + let thumbprint = compute_thumbprint(root.as_ref()); + debug!("CA Thumbprint from certificate: {}", &thumbprint); + if let Some(existing) = ca_thumbprints.get(&subject) { + if existing != &thumbprint { + bail!( + "Duplicate CA subject with different thumbprints: {}", + &subject + ); + } + // already present, skip, but warn + warn!( + "Duplicate CA certificate found for subject: {}, thumbprint: {}", + &subject, &thumbprint + ); + continue; + } + ca_thumbprints.insert(subject, thumbprint); + client_auth_roots .add(root) .context("Could not add certificate to root of trust")?; @@ -107,20 +125,21 @@ pub fn make_config(args: &common::settings::Tls) -> Result { .with_protocol_versions(ALL_VERSIONS) .context("Could not build configuration defaults")? .with_client_cert_verifier(client_cert_verifier) // add verifier - .with_single_cert(cert, priv_key) // add server vertification + .with_single_cert(cert, priv_key) // add server verification .context("Bad configuration certificate or key")?; // any http version is ok config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()]; info!( - "Loaded TLS configuration with server certificate {}", - args.server_certificate() + "Loaded TLS configuration with server certificate {} and {} CA(s)", + args.server_certificate(), + ca_thumbprints.len() ); Ok(TlsConfig { server: Arc::new(config), - thumbprint, + thumbprints: ca_thumbprints, }) } @@ -158,6 +177,57 @@ pub fn subject_from_cert(cert: &[u8]) -> Result { bail!("CommonName not found") } +pub fn issuer_from_cert(cert: &[u8]) -> Result { + // load certificate to decompose its content + let cert = X509Certificate::from_der(cert)?.1; + + let oid_registry = OidRegistry::default().with_x509(); // registry of OIDs we will need + let rdn_iter = cert.issuer.iter_rdn(); // iterator on RDNs + + for subject_attribute in rdn_iter { + // Each entry contains a list of AttributeTypeAndValue objects, + // so we fetch their attribute type (= the OID representing each of them) + let sn = subject_attribute.iter(); + + for set in sn { + // OID of the sub-entry + let typ = set.attr_type(); + + // get the SN corresponding to the OID (None if it does not exist) + let oid_reg = oid_registry.get(typ).map(|oid| oid.sn()); + + // the value we are interested in is only contained where the commonName is + if oid_reg == Some("commonName") { + // get data as text => FQDN of the client + if let Ok(name) = set.as_str() { + return Ok(name.to_string()); + } else { + bail!("CommonName is empty") + } + } + } + } + bail!("CommonName not found") +} + +pub fn find_matching_ca( + peer_certs: &[CertificateDer], + ca_thumbprints: &HashMap, +) -> Result { + peer_certs + .iter() + .find_map(|cert| { + let issuer = issuer_from_cert(cert.as_ref()).ok()?; + debug!("Checking issuer '{}'", &issuer); + + ca_thumbprints.get(&issuer).map(|ca_entry| { + debug!("Found matching CA for issuer '{}'", &issuer); + ca_entry.clone() + }) + }) + .context("No trusted CA found in certificate chain") +} + /// Read and decode request payload pub async fn get_request_payload( parts: hyper::http::request::Parts,