Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
98 changes: 84 additions & 14 deletions server/src/tls.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -66,7 +67,7 @@ pub fn compute_thumbprint(cert_content: &[u8]) -> String {

pub struct TlsConfig {
pub server: Arc<ServerConfig>,
pub thumbprint: String,
pub thumbprints: HashMap<String, String>,
}

/// Create configuration for TLS connection
Expand All @@ -77,18 +78,35 @@ pub fn make_config(args: &common::settings::Tls) -> Result<TlsConfig> {
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")?;
Expand All @@ -107,20 +125,21 @@ pub fn make_config(args: &common::settings::Tls) -> Result<TlsConfig> {
.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,
})
}

Expand Down Expand Up @@ -158,6 +177,57 @@ pub fn subject_from_cert(cert: &[u8]) -> Result<String> {
bail!("CommonName not found")
}

pub fn issuer_from_cert(cert: &[u8]) -> Result<String> {
// 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<String, String>,
) -> Result<String> {
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,
Expand Down