use std::sync::Arc;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls_pki_types::{CertificateDer, ServerName, UnixTime};
use rustls_platform_verifier::BuilderVerifierExt;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_rustls::TlsConnector;
use crate::core::error::{OlError, ERR_PROXY_CONFIG_INVALID};
use super::super::config::EgressConfig;
#[derive(Clone)]
pub struct TlsSetup {
proxy: Arc<ClientConfig>,
target: Arc<ClientConfig>,
target_http1: Arc<ClientConfig>,
}
impl TlsSetup {
pub fn new(cfg: &EgressConfig) -> Result<Self, OlError> {
super::super::init_crypto();
let extra = match cfg.ca_bundle.as_ref() {
Some(path) => read_roots(path)?,
None => Vec::new(),
};
let base = || -> Result<ClientConfig, OlError> {
if extra.is_empty() {
return Ok(ClientConfig::builder()
.with_platform_verifier()
.map_err(|e| tls_config_error(&format!("platform verifier: {e}")))?
.with_no_client_auth());
}
let builder = ClientConfig::builder();
let provider = builder.crypto_provider().clone();
let platform = rustls_platform_verifier::Verifier::new(provider.clone())
.map_err(|e| tls_config_error(&format!("platform verifier: {e}")))?;
let merged = MergedVerifier::new(Arc::new(platform), provider, extra.clone())?;
Ok(builder
.dangerous()
.with_custom_certificate_verifier(Arc::new(merged))
.with_no_client_auth())
};
let proxy = base()?;
let mut target = base()?;
target.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let mut target_http1 = base()?;
target_http1.alpn_protocols = vec![b"http/1.1".to_vec()];
Ok(Self {
proxy: Arc::new(proxy),
target: Arc::new(target),
target_http1: Arc::new(target_http1),
})
}
pub async fn connect_proxy<S>(
&self,
host: &str,
stream: S,
) -> std::io::Result<tokio_rustls::client::TlsStream<S>>
where
S: AsyncRead + AsyncWrite + Unpin,
{
TlsConnector::from(self.proxy.clone())
.connect(server_name(host)?, stream)
.await
}
pub async fn connect_target<S>(
&self,
host: &str,
stream: S,
http1_only: bool,
) -> std::io::Result<tokio_rustls::client::TlsStream<S>>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let config = if http1_only {
self.target_http1.clone()
} else {
self.target.clone()
};
TlsConnector::from(config)
.connect(server_name(host)?, stream)
.await
}
}
fn server_name(host: &str) -> std::io::Result<ServerName<'static>> {
ServerName::try_from(host.to_string()).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("\"{host}\" is not a valid TLS server name: {e}"),
)
})
}
fn tls_config_error(detail: &str) -> OlError {
OlError::new(
ERR_PROXY_CONFIG_INVALID,
format!("could not build the Negotiate transport's TLS configuration: {detail}"),
)
.with_suggestion("Check [proxy] ca_bundle points at a readable PEM bundle.")
}
fn read_roots(path: &std::path::Path) -> Result<Vec<CertificateDer<'static>>, OlError> {
let pem = std::fs::read(path).map_err(|e| {
OlError::new(
ERR_PROXY_CONFIG_INVALID,
format!("[proxy] ca_bundle {} cannot be read: {e}", path.display()),
)
.with_suggestion("Point ca_bundle at a readable PEM file, or remove the key.")
})?;
let mut reader = std::io::BufReader::new(std::io::Cursor::new(pem));
let certs: Vec<_> = rustls_pemfile_certs(&mut reader);
if certs.is_empty() {
return Err(OlError::new(
ERR_PROXY_CONFIG_INVALID,
format!(
"[proxy] ca_bundle {} contains no certificates",
path.display()
),
)
.with_suggestion("Check the file is the CA bundle you meant to point at."));
}
Ok(certs)
}
fn rustls_pemfile_certs<R: std::io::BufRead>(reader: &mut R) -> Vec<CertificateDer<'static>> {
const BEGIN: &str = "-----BEGIN CERTIFICATE-----";
const END: &str = "-----END CERTIFICATE-----";
let mut out = Vec::new();
let mut body = String::new();
let mut inside = false;
let mut line = String::new();
while reader.read_line(&mut line).unwrap_or(0) > 0 {
let trimmed = line.trim();
if trimmed == BEGIN {
inside = true;
body.clear();
} else if trimmed == END {
if inside {
if let Some(der) = super::b64_decode(&body) {
out.push(CertificateDer::from(der));
}
}
inside = false;
} else if inside {
body.push_str(trimmed);
}
line.clear();
}
out
}
#[derive(Debug)]
struct MergedVerifier {
platform: Arc<dyn ServerCertVerifier>,
extra: Arc<dyn ServerCertVerifier>,
}
impl MergedVerifier {
fn new(
platform: Arc<dyn ServerCertVerifier>,
provider: Arc<rustls::crypto::CryptoProvider>,
extra: Vec<CertificateDer<'static>>,
) -> Result<Self, OlError> {
let mut roots = rustls::RootCertStore::empty();
for cert in extra {
roots.add(cert).map_err(|e| {
tls_config_error(&format!("a ca_bundle certificate is not usable: {e}"))
})?;
}
let extra =
rustls::client::WebPkiServerVerifier::builder_with_provider(Arc::new(roots), provider)
.build()
.map_err(|e| tls_config_error(&format!("ca_bundle roots: {e}")))?;
Ok(Self { platform, extra })
}
}
impl ServerCertVerifier for MergedVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
match self.platform.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
) {
Ok(verified) => Ok(verified),
Err(os_error) => self
.extra
.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
.map_err(|_| os_error),
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
self.platform.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
self.platform.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.platform.supported_verify_schemes()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_configuration_without_a_bundle_builds() {
let setup = TlsSetup::new(&EgressConfig::direct()).expect("build");
assert!(
setup.proxy.alpn_protocols.is_empty(),
"the proxy hop must advertise no ALPN"
);
assert_eq!(
setup.target.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()],
"ALPN must be restored on the target hop"
);
assert_eq!(
setup.target_http1.alpn_protocols,
vec![b"http/1.1".to_vec()]
);
}
#[test]
fn a_real_pem_bundle_merges() {
let issued = rcgen::generate_simple_self_signed(vec!["ca.test".to_string()]).expect("cert");
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("ca.pem");
std::fs::write(&path, issued.cert.pem()).expect("write");
let mut cfg = EgressConfig::direct();
cfg.ca_bundle = Some(path);
TlsSetup::new(&cfg).expect("a bundle on top of the OS roots must build");
}
#[test]
fn an_empty_bundle_is_rejected_rather_than_silently_adding_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("empty.pem");
std::fs::write(&path, b"").expect("write");
let mut cfg = EgressConfig::direct();
cfg.ca_bundle = Some(path);
let err = match TlsSetup::new(&cfg) {
Err(e) => e,
Ok(_) => panic!("an empty bundle must be rejected"),
};
assert_eq!(err.code, ERR_PROXY_CONFIG_INVALID);
}
#[test]
fn the_pem_reader_finds_every_certificate_in_a_bundle() {
let one = rcgen::generate_simple_self_signed(vec!["a.test".into()]).expect("cert");
let two = rcgen::generate_simple_self_signed(vec!["b.test".into()]).expect("cert");
let bundle = format!("{}\n{}", one.cert.pem(), two.cert.pem());
let mut reader = std::io::BufReader::new(std::io::Cursor::new(bundle.into_bytes()));
let certs = rustls_pemfile_certs(&mut reader);
assert_eq!(certs.len(), 2);
assert_eq!(certs[0].as_ref(), one.cert.der().as_ref());
assert_eq!(certs[1].as_ref(), two.cert.der().as_ref());
}
#[test]
fn the_pem_reader_ignores_anything_outside_a_certificate_block() {
let issued = rcgen::generate_simple_self_signed(vec!["a.test".into()]).expect("cert");
let bundle = format!(
"# a comment\nsubject=CN=a.test\n{}\ntrailing junk\n",
issued.cert.pem()
);
let mut reader = std::io::BufReader::new(std::io::Cursor::new(bundle.into_bytes()));
assert_eq!(rustls_pemfile_certs(&mut reader).len(), 1);
}
}