From a271ca6c8c0ecb4db1bc381df50922cff990934e Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 3 Apr 2023 22:22:48 +0300 Subject: [PATCH] Properly extract cert names --- Cargo.lock | 14 +++++++++ Cargo.toml | 1 + proxy/Cargo.toml | 1 + proxy/src/certs.rs | 75 ++++++++++++++++++++++++++++++++++++++++------ proxy/src/main.rs | 4 +-- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c026b60644..6d0cbb728c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1659,6 +1659,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "h2" version = "0.3.15" @@ -3129,6 +3142,7 @@ dependencies = [ "consumption_metrics", "futures", "git-version", + "globset", "hashbrown 0.13.2", "hashlink", "hex", diff --git a/Cargo.toml b/Cargo.toml index 74cbb7b5da..4ec0e61c9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ futures = "0.3" futures-core = "0.3" futures-util = "0.3" git-version = "0.3" +globset = "0.4.10" hashbrown = "0.13" hashlink = "0.8.1" hex = "0.4" diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml index 8b00538131..a27525c40b 100644 --- a/proxy/Cargo.toml +++ b/proxy/Cargo.toml @@ -16,6 +16,7 @@ clap.workspace = true consumption_metrics.workspace = true futures.workspace = true git-version.workspace = true +globset.workspace = true hashbrown.workspace = true hashlink.workspace = true hex.workspace = true diff --git a/proxy/src/certs.rs b/proxy/src/certs.rs index d7f96b87df..1db636ccf2 100644 --- a/proxy/src/certs.rs +++ b/proxy/src/certs.rs @@ -4,15 +4,16 @@ use rustls::{ }; use std::{io, sync::Arc}; +/// App-level configuration structs for TLS certificates. pub mod config { use super::*; - use serde::Deserialize; + use serde::{de, Deserialize}; use std::path::Path; /// Collection of TLS-related configurations of virtual proxy servers. #[derive(Debug, Default, Clone, Deserialize)] #[serde(transparent)] - pub struct TlsServers(Vec); + pub struct TlsServers(pub Vec); impl TlsServers { /// Load [`Self`] config from a file. @@ -29,6 +30,7 @@ pub mod config { } } + /// Helps deserialize certificate chain from a string. #[derive(Debug, Clone, Deserialize)] #[serde(transparent)] pub struct TlsCert( @@ -42,9 +44,10 @@ pub mod config { D: serde::Deserializer<'de>, { let text = String::deserialize(des)?; - parse_certs(&mut text.as_bytes()).map_err(serde::de::Error::custom) + parse_certs(&mut text.as_bytes()).map_err(de::Error::custom) } + /// Helps deserialize private key from a string. #[derive(Debug, Clone, Deserialize)] #[serde(transparent)] pub struct TlsKey( @@ -58,17 +61,20 @@ pub mod config { D: serde::Deserializer<'de>, { let text = String::deserialize(des)?; - parse_key(&mut text.as_bytes()).map_err(serde::de::Error::custom) + parse_key(&mut text.as_bytes()).map_err(de::Error::custom) } - /// TODO: explain. + /// Represents TLS config of a single virtual proxy server. #[derive(Debug, Clone, Deserialize)] pub struct TlsServer { + /// Proxy server's certificate chain. pub certificate: TlsCert, + /// Proxy server's private key. pub private_key: TlsKey, } } +/// Parse TLS certificate chain from a byte buffer. fn parse_certs(buf: &mut impl io::BufRead) -> io::Result> { let chain = rustls_pemfile::certs(buf)? .into_iter() @@ -78,6 +84,7 @@ fn parse_certs(buf: &mut impl io::BufRead) -> io::Result io::Result { let mut keys = rustls_pemfile::pkcs8_private_keys(buf)?; @@ -92,18 +99,68 @@ fn parse_key(buf: &mut impl io::BufRead) -> io::Result { Ok(rustls::PrivateKey(keys.pop().unwrap())) } +/// Extract domain names from a certificate: first CN, then SANs. +/// Further reading: https://www.rfc-editor.org/rfc/rfc4985 +fn certificate_names(cert: &rustls::Certificate) -> anyhow::Result> { + use x509_parser::{extensions::GeneralName, x509::AttributeTypeAndValue}; + + let get_dns_name = |gn: &GeneralName| match gn { + GeneralName::DNSName(name) => Some(name.to_string()), + _other => None, + }; + + let get_common_name = |attr: &AttributeTypeAndValue| { + // There really shouldn't be anything but string here. + attr.attr_value().as_string().expect("bad CN attribute") + }; + + let (rest, cert) = x509_parser::parse_x509_certificate(cert.0.as_ref())?; + anyhow::ensure!(rest.is_empty(), "excessive bytes in DER certificate"); + + // Extract CN, Common Name. + let mut names: Vec = cert + .subject() + .iter_common_name() + .map(get_common_name) + .collect(); + + // Now append SANs, Subject Alternative Names, if any. + if let Some(extension) = cert.subject_alternative_name()? { + let alt_names = &extension.value.general_names; + names.extend(alt_names.iter().filter_map(get_dns_name)); + } + + Ok(names) +} + +struct CertResolverEntry { + raw: Arc, + names: Vec, +} + pub struct CertResolver { - resolver: rustls::server::ResolvesServerCertUsingSni, + certs: Vec, } impl CertResolver { - pub fn new() -> Self { - todo!("CertResolver ctor") + pub fn new(config: config::TlsServers) -> anyhow::Result { + let mut builder = globset::GlobSetBuilder::new(); + for server in config.0 { + let Some(cert) = server.certificate.0.first() else { + tracing::warn!("found empty certificate, skipping"); + continue; + }; + + let names = certificate_names(cert)?; + dbg!(names); + } + + todo!() } } impl ResolvesServerCert for CertResolver { fn resolve(&self, message: ClientHello) -> Option> { - self.resolver.resolve(message) + todo!("implement") } } diff --git a/proxy/src/main.rs b/proxy/src/main.rs index 403526ded3..c072e25dd7 100644 --- a/proxy/src/main.rs +++ b/proxy/src/main.rs @@ -133,7 +133,7 @@ fn build_tls_config(args: &clap::ArgMatches) -> anyhow::Result let tls_config = args.get_one::("tls-config"); let main = tls_config.map(TlsServers::from_config_file).transpose()?; - tracing::info!(?main, "config"); + certs::CertResolver::new(main.unwrap()); let tls_cert = args.get_one::("tls-cert"); let tls_key = args.get_one::("tls-key"); @@ -144,7 +144,7 @@ fn build_tls_config(args: &clap::ArgMatches) -> anyhow::Result _ => bail!("either both or neither tls-key and tls-cert must be specified"), }; - todo!() + todo!("TlsConfig") } fn build_metrics_config(args: &clap::ArgMatches) -> anyhow::Result> {