From db291d4bf08a546c05217f99ba11fd73d5f85456 Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Thu, 18 Dec 2025 09:17:19 +0100 Subject: [PATCH 1/4] feat(server/tls): add issuer_from_cert function This is done in preparation for supporting multiple CAs Signed-off-by: Szilard Parrag --- server/src/tls.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/server/src/tls.rs b/server/src/tls.rs index 390d7a50..6fcd062b 100644 --- a/server/src/tls.rs +++ b/server/src/tls.rs @@ -3,6 +3,7 @@ use common::encoding::encode_utf16le; use hex::ToHex; use log::{debug, info}; use sha1::{Digest, Sha1}; +use std::collections::HashMap; use std::fs; use std::io::BufReader; use std::sync::Arc; @@ -158,6 +159,39 @@ 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") +} + /// Read and decode request payload pub async fn get_request_payload( parts: hyper::http::request::Parts, From e229e74717a864818364f2f076a065c11f3b8a61 Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Mon, 19 Jan 2026 09:28:34 +0100 Subject: [PATCH 2/4] feat(server/tls): add find_matching_ca function This is done in preparation for supporting multiple CAs Signed-off-by: Szilard Parrag --- server/src/tls.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/src/tls.rs b/server/src/tls.rs index 6fcd062b..f13e3c36 100644 --- a/server/src/tls.rs +++ b/server/src/tls.rs @@ -192,6 +192,24 @@ pub fn issuer_from_cert(cert: &[u8]) -> Result { 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, From 8e9d6e4286f1230502ceebf83506206c45fec281 Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Thu, 18 Dec 2025 09:24:02 +0100 Subject: [PATCH 3/4] feat(server/tls): add support for multiple CAs Signed-off-by: Szilard Parrag --- server/src/lib.rs | 28 ++++++++++++++++++++++------ server/src/tls.rs | 46 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/server/src/lib.rs b/server/src/lib.rs index 9118de30..8477d8e1 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,12 +999,13 @@ 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(); @@ -1011,8 +1013,22 @@ fn create_tls_server( let subject = subject_from_cert(cert.as_ref()).expect("Could not parse client certificate"); + 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 f13e3c36..9eec35c8 100644 --- a/server/src/tls.rs +++ b/server/src/tls.rs @@ -1,7 +1,7 @@ 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; @@ -67,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 @@ -78,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")?; @@ -108,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, }) } From e7fbefe507e5f683a742875088e417f50de34a02 Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Wed, 17 Dec 2025 15:05:31 +0100 Subject: [PATCH 4/4] feat(server/tls): add debug log message for incoming connection's TLS subject Signed-off-by: Szilard Parrag --- server/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/lib.rs b/server/src/lib.rs index 8477d8e1..b0d1cf0a 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1012,6 +1012,7 @@ fn create_tls_server( 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");