#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use rustls::crypto::CryptoProvider;
use rustls::server::{ClientHello, ResolvesServerCert};
use rustls::sign::CertifiedKey;
use rustls_pki_types::pem::PemObject as _;
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
pub const DEFAULT_RELOAD_INTERVAL_SECS: u64 = 60;
#[derive(Debug, thiserror::Error)]
pub enum TlsError {
#[error("failed to read TLS certificate file `{path}`: {source}")]
ReadCert {
path: PathBuf,
source: std::io::Error,
},
#[error("failed to read TLS private key file `{path}`: {source}")]
ReadKey {
path: PathBuf,
source: std::io::Error,
},
#[error("failed to parse a PEM certificate in `{path}`: {source}")]
ParseCert {
path: PathBuf,
source: rustls_pki_types::pem::Error,
},
#[error("no certificates found in `{path}` (expected at least one PEM CERTIFICATE block)")]
NoCertificates {
path: PathBuf,
},
#[error("failed to parse a PEM private key in `{path}`: {source}")]
ParseKey {
path: PathBuf,
source: rustls_pki_types::pem::Error,
},
#[error(
"the TLS private key `{key}` is invalid or does not match the leaf certificate `{cert}`: \
{source}"
)]
InvalidKeyPair {
cert: PathBuf,
key: PathBuf,
source: Box<rustls::Error>,
},
#[error("failed to parse the leaf certificate in `{path}` for expiry inspection: {detail}")]
ParseLeaf {
path: PathBuf,
detail: String,
},
#[error("certificate #{position} in the chain in `{path}` is malformed: {detail}")]
ParseChainCert {
path: PathBuf,
position: usize,
detail: String,
},
#[error("the leaf certificate in `{path}` expired at {not_after} (UNIX {not_after_unix})")]
Expired {
path: PathBuf,
not_after: String,
not_after_unix: i64,
},
#[error(
"the leaf certificate in `{path}` is not valid until {not_before} (UNIX {not_before_unix})"
)]
NotYetValid {
path: PathBuf,
not_before: String,
not_before_unix: i64,
},
#[error(
"certificate #{position} in the chain in `{path}` expired at {not_after} \
(UNIX {not_after_unix})"
)]
ExpiredChainCert {
path: PathBuf,
position: usize,
not_after: String,
not_after_unix: i64,
},
#[error(
"certificate #{position} in the chain in `{path}` is not yet valid until {not_before} \
(UNIX {not_before_unix})"
)]
NotYetValidChainCert {
path: PathBuf,
position: usize,
not_before: String,
not_before_unix: i64,
},
#[error("failed to build the rustls server configuration: {source}")]
BuildConfig {
source: rustls::Error,
},
#[error("failed to read the mTLS client CA bundle `{path}`: {source}")]
ReadClientCa {
path: PathBuf,
source: std::io::Error,
},
#[error("failed to parse a PEM certificate in the mTLS client CA bundle `{path}`: {source}")]
ParseClientCa {
path: PathBuf,
source: rustls_pki_types::pem::Error,
},
#[error(
"no CAs found in the mTLS client CA bundle `{path}` (expected at least one PEM \
CERTIFICATE block)"
)]
NoClientCas {
path: PathBuf,
},
#[error(
"CA #{position} in the mTLS client CA bundle `{path}` is not a valid trust anchor: {source}"
)]
InvalidClientCa {
path: PathBuf,
position: usize,
source: Box<rustls::Error>,
},
#[error("failed to read the mTLS revocation list `{path}`: {source}")]
ReadCrl {
path: PathBuf,
source: std::io::Error,
},
#[error("failed to parse a PEM block in the mTLS revocation list `{path}`: {source}")]
ParseCrl {
path: PathBuf,
source: rustls_pki_types::pem::Error,
},
#[error("no revocation list found in `{path}` (expected at least one PEM X509 CRL block)")]
NoCrls {
path: PathBuf,
},
#[error("CRL #{position} in `{path}` is malformed: {detail}")]
ParseCrlDer {
path: PathBuf,
position: usize,
detail: String,
},
#[error(
"the mTLS revocation list `{crl_path}` has no CRL issued by {uncovered:?} \
from the client CA bundle `{ca_bundle_path}`; once any CRL is configured, \
rustls refuses handshakes whose revocation status is unknown, so these \
CAs' clients would be rejected — publish a CRL for each CA in the \
bundle or remove the uncovered CA"
)]
CrlCoverageGap {
ca_bundle_path: PathBuf,
crl_path: PathBuf,
uncovered: Vec<String>,
},
#[error("failed to build the mTLS client certificate verifier: {source}")]
BuildClientVerifier {
source: rustls::server::VerifierBuilderError,
},
#[error("failed to parse the verified client certificate: {detail}")]
ParsePeerCert {
detail: String,
},
}
pub mod client_auth;
#[must_use]
pub fn crypto_provider() -> Arc<CryptoProvider> {
Arc::new(rustls::crypto::ring::default_provider())
}
fn read_cert_chain(cert_path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsError> {
let pem = std::fs::read(cert_path).map_err(|source| TlsError::ReadCert {
path: cert_path.to_path_buf(),
source,
})?;
let mut chain = Vec::new();
for cert in CertificateDer::pem_slice_iter(&pem) {
let cert = cert.map_err(|source| TlsError::ParseCert {
path: cert_path.to_path_buf(),
source,
})?;
chain.push(cert);
}
if chain.is_empty() {
return Err(TlsError::NoCertificates {
path: cert_path.to_path_buf(),
});
}
Ok(chain)
}
fn read_private_key(key_path: &Path) -> Result<PrivateKeyDer<'static>, TlsError> {
let pem = std::fs::read(key_path).map_err(|source| TlsError::ReadKey {
path: key_path.to_path_buf(),
source,
})?;
PrivateKeyDer::from_pem_slice(&pem).map_err(|source| TlsError::ParseKey {
path: key_path.to_path_buf(),
source,
})
}
fn leaf_validity_unix(cert_path: &Path, leaf: &CertificateDer<'_>) -> Result<(i64, i64), TlsError> {
use x509_parser::prelude::FromDer as _;
let (_, parsed) =
x509_parser::certificate::X509Certificate::from_der(leaf.as_ref()).map_err(|e| {
TlsError::ParseLeaf {
path: cert_path.to_path_buf(),
detail: e.to_string(),
}
})?;
let validity = parsed.validity();
Ok((
validity.not_before.timestamp(),
validity.not_after.timestamp(),
))
}
fn validate_chain_certs(
cert_path: &Path,
chain: &[CertificateDer<'_>],
now_unix: i64,
) -> Result<(), TlsError> {
use x509_parser::prelude::FromDer as _;
for (idx, cert) in chain.iter().enumerate().skip(1) {
let position = idx + 1;
let (_, parsed) = x509_parser::certificate::X509Certificate::from_der(cert.as_ref())
.map_err(|e| TlsError::ParseChainCert {
path: cert_path.to_path_buf(),
position,
detail: e.to_string(),
})?;
let validity = parsed.validity();
let not_before = validity.not_before.timestamp();
let not_after = validity.not_after.timestamp();
if now_unix < not_before {
return Err(TlsError::NotYetValidChainCert {
path: cert_path.to_path_buf(),
position,
not_before: render_unix(not_before),
not_before_unix: not_before,
});
}
if not_after < now_unix {
return Err(TlsError::ExpiredChainCert {
path: cert_path.to_path_buf(),
position,
not_after: render_unix(not_after),
not_after_unix: not_after,
});
}
}
Ok(())
}
pub fn load_certified_key(
cert_path: &Path,
key_path: &Path,
provider: &CryptoProvider,
now_unix: i64,
) -> Result<Arc<CertifiedKey>, TlsError> {
let chain = read_cert_chain(cert_path)?;
let key = read_private_key(key_path)?;
let (not_before, not_after) = leaf_validity_unix(cert_path, &chain[0])?;
if now_unix < not_before {
return Err(TlsError::NotYetValid {
path: cert_path.to_path_buf(),
not_before: render_unix(not_before),
not_before_unix: not_before,
});
}
if not_after < now_unix {
return Err(TlsError::Expired {
path: cert_path.to_path_buf(),
not_after: render_unix(not_after),
not_after_unix: not_after,
});
}
validate_chain_certs(cert_path, &chain, now_unix)?;
let certified = CertifiedKey::from_der(chain, key, provider).map_err(|source| {
TlsError::InvalidKeyPair {
cert: cert_path.to_path_buf(),
key: key_path.to_path_buf(),
source: Box::new(source),
}
})?;
Ok(Arc::new(certified))
}
fn render_unix(secs: i64) -> String {
x509_parser::time::ASN1Time::from_timestamp(secs)
.ok()
.map_or_else(|| format!("UNIX {secs}"), |t| t.to_string())
}
#[derive(Debug)]
pub struct ReloadableCertResolver {
current: RwLock<Arc<CertifiedKey>>,
}
impl ReloadableCertResolver {
#[must_use]
pub const fn new(initial: Arc<CertifiedKey>) -> Self {
Self {
current: RwLock::new(initial),
}
}
pub fn store(&self, next: Arc<CertifiedKey>) {
let mut guard = self
.current
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = next;
}
#[must_use]
pub fn current(&self) -> Arc<CertifiedKey> {
let guard = self
.current
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Arc::clone(&guard)
}
}
impl ResolvesServerCert for ReloadableCertResolver {
fn resolve(&self, _client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
Some(self.current())
}
}
pub fn build_server_config(
provider: Arc<CryptoProvider>,
resolver: Arc<ReloadableCertResolver>,
) -> Result<Arc<rustls::ServerConfig>, TlsError> {
build_server_config_with_resolver(provider, resolver)
}
pub fn build_server_config_with_resolver(
provider: Arc<CryptoProvider>,
resolver: Arc<dyn ResolvesServerCert>,
) -> Result<Arc<rustls::ServerConfig>, TlsError> {
build_server_config_with_client_auth(provider, resolver, None)
}
pub fn build_server_config_with_client_auth(
provider: Arc<CryptoProvider>,
resolver: Arc<dyn ResolvesServerCert>,
client_verifier: Option<Arc<dyn rustls::server::danger::ClientCertVerifier>>,
) -> Result<Arc<rustls::ServerConfig>, TlsError> {
let builder = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|source| TlsError::BuildConfig { source })?;
let mut config = match client_verifier {
Some(verifier) => {
let mut config = builder
.with_client_cert_verifier(verifier)
.with_cert_resolver(resolver);
config.session_storage = Arc::new(rustls::server::NoServerSessionStorage {});
config.send_tls13_tickets = 0;
config
}
None => builder.with_no_client_auth().with_cert_resolver(resolver),
};
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Ok(Arc::new(config))
}
#[derive(Clone, Debug)]
pub struct TlsConnectInfo {
pub peer: std::net::SocketAddr,
pub client: Option<Arc<client_auth::ClientIdentity>>,
}
impl axum::extract::connect_info::Connected<axum::serve::IncomingStream<'_, TlsListener>>
for TlsConnectInfo
{
fn connect_info(stream: axum::serve::IncomingStream<'_, TlsListener>) -> Self {
Self::from_stream(stream.io(), *stream.remote_addr())
}
}
impl
axum::extract::connect_info::Connected<
axum::serve::IncomingStream<'_, crate::accept_drain::StopAcceptingOnShutdown<TlsListener>>,
> for TlsConnectInfo
{
fn connect_info(
stream: axum::serve::IncomingStream<
'_,
crate::accept_drain::StopAcceptingOnShutdown<TlsListener>,
>,
) -> Self {
Self::from_stream(stream.io(), *stream.remote_addr())
}
}
impl TlsConnectInfo {
fn from_stream(
io: &tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
peer: std::net::SocketAddr,
) -> Self {
let client = io
.get_ref()
.1
.peer_certificates()
.and_then(<[rustls_pki_types::CertificateDer<'_>]>::first)
.and_then(|leaf| match client_auth::ClientIdentity::from_der(leaf) {
Ok(identity) => Some(Arc::new(identity)),
Err(e) => {
tracing::warn!(peer = %peer, error = %e, "could not parse the verified client certificate");
None
}
});
Self { peer, client }
}
}
const MAX_CONCURRENT_HANDSHAKES: usize = 256;
const READY_CONN_CHANNEL_CAPACITY: usize = 1024;
pub struct TlsListener {
rx: tokio::sync::mpsc::Receiver<(
tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
std::net::SocketAddr,
)>,
local_addr: std::net::SocketAddr,
shutdown: tokio_util::sync::CancellationToken,
}
impl TlsListener {
#[must_use]
pub fn new(
tcp: tokio::net::TcpListener,
config: Arc<rustls::ServerConfig>,
handshake_timeout: std::time::Duration,
shutdown: tokio_util::sync::CancellationToken,
) -> Self {
let local_addr = tcp
.local_addr()
.expect("bound TLS listener must have a local address");
let acceptor = tokio_rustls::TlsAcceptor::from(config);
let (tx, rx) = tokio::sync::mpsc::channel(READY_CONN_CHANNEL_CAPACITY);
let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_HANDSHAKES));
let acceptor_shutdown = shutdown.clone();
tokio::spawn(async move {
run_acceptor(
tcp,
acceptor,
handshake_timeout,
semaphore,
tx,
acceptor_shutdown,
)
.await;
});
Self {
rx,
local_addr,
shutdown,
}
}
}
async fn run_acceptor(
tcp: tokio::net::TcpListener,
acceptor: tokio_rustls::TlsAcceptor,
handshake_timeout: std::time::Duration,
semaphore: Arc<tokio::sync::Semaphore>,
tx: tokio::sync::mpsc::Sender<(
tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
std::net::SocketAddr,
)>,
shutdown: tokio_util::sync::CancellationToken,
) {
loop {
let (stream, peer) = tokio::select! {
() = shutdown.cancelled() => break,
result = tcp.accept() => match result {
Ok(pair) => pair,
Err(e) => {
if is_transient_connection_error(&e) {
continue;
}
tracing::error!(error = %e, "TLS listener accept error");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
},
};
let Ok(permit) = Arc::clone(&semaphore).acquire_owned().await else {
break;
};
let acceptor = acceptor.clone();
let tx = tx.clone();
tokio::spawn(async move {
let _permit = permit;
match tokio::time::timeout(handshake_timeout, acceptor.accept(stream)).await {
Ok(Ok(tls)) => {
let _ = tx.send((tls, peer)).await;
}
Ok(Err(e)) => {
if !client_auth::record_handshake_rejection(&e, peer) {
tracing::debug!(
peer = %peer,
error = %e,
"TLS handshake failed; dropping connection"
);
}
}
Err(_elapsed) => {
tracing::debug!(peer = %peer, "TLS handshake timed out");
}
}
});
}
}
fn is_transient_connection_error(e: &std::io::Error) -> bool {
matches!(
e.kind(),
std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionReset
)
}
impl axum::serve::Listener for TlsListener {
type Io = tokio_rustls::server::TlsStream<tokio::net::TcpStream>;
type Addr = std::net::SocketAddr;
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
if let Some(conn) = self.rx.recv().await {
return conn;
}
self.shutdown.cancelled().await;
std::future::pending().await
}
fn local_addr(&self) -> std::io::Result<Self::Addr> {
Ok(self.local_addr)
}
}
#[derive(Debug, Clone, Copy)]
pub struct LeafInspection {
pub not_before_unix: i64,
pub not_after_unix: i64,
}
impl LeafInspection {
#[must_use]
pub const fn is_not_yet_valid(&self, now_unix: i64) -> bool {
now_unix < self.not_before_unix
}
#[must_use]
pub const fn days_until_expiry(&self, now_unix: i64) -> i64 {
(self.not_after_unix - now_unix) / 86_400
}
#[must_use]
pub const fn is_expired(&self, now_unix: i64) -> bool {
self.not_after_unix <= now_unix
}
}
pub fn inspect_leaf(
cert_path: &Path,
key_path: &Path,
now_unix: i64,
) -> Result<LeafInspection, TlsError> {
let chain = read_cert_chain(cert_path)?;
let key = read_private_key(key_path)?;
let (not_before, not_after) = leaf_validity_unix(cert_path, &chain[0])?;
validate_chain_certs(cert_path, &chain, now_unix)?;
let provider = crypto_provider();
CertifiedKey::from_der(chain, key, &provider).map_err(|source| TlsError::InvalidKeyPair {
cert: cert_path.to_path_buf(),
key: key_path.to_path_buf(),
source: Box::new(source),
})?;
Ok(LeafInspection {
not_before_unix: not_before,
not_after_unix: not_after,
})
}
pub fn certified_key_from_pem(
chain_pem: &[u8],
key_pem: &[u8],
provider: &CryptoProvider,
) -> Result<Arc<CertifiedKey>, String> {
let mut chain = Vec::new();
for cert in CertificateDer::pem_slice_iter(chain_pem) {
chain.push(cert.map_err(|e| format!("failed to parse ACME certificate PEM: {e}"))?);
}
if chain.is_empty() {
return Err("ACME certificate chain contained no PEM CERTIFICATE blocks".to_owned());
}
let key = PrivateKeyDer::from_pem_slice(key_pem)
.map_err(|e| format!("failed to parse ACME private key PEM: {e}"))?;
let certified = CertifiedKey::from_der(chain, key, provider)
.map_err(|e| format!("ACME private key does not match the issued certificate: {e}"))?;
Ok(Arc::new(certified))
}
pub fn leaf_not_after_from_pem(chain_pem: &[u8]) -> Result<i64, String> {
let leaf = CertificateDer::pem_slice_iter(chain_pem)
.next()
.ok_or_else(|| "certificate chain contained no PEM CERTIFICATE blocks".to_owned())?
.map_err(|e| format!("failed to parse certificate PEM: {e}"))?;
leaf_validity_unix(Path::new("<memory>"), &leaf)
.map(|(_, not_after)| not_after)
.map_err(|e| e.to_string())
}
#[allow(
clippy::disallowed_methods,
reason = "TLS certificate validity is judged against real wall time by \
design — see this function's doc comment. Injecting a virtual \
clock here would let a simulation misjudge a real certificate."
)]
pub(crate) fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
}
fn file_mtimes(
cert: &Path,
key: &Path,
) -> (Option<std::time::SystemTime>, Option<std::time::SystemTime>) {
let mtime = |p: &Path| std::fs::metadata(p).and_then(|m| m.modified()).ok();
(mtime(cert), mtime(key))
}
pub struct CertReloader {
resolver: Arc<ReloadableCertResolver>,
provider: Arc<CryptoProvider>,
cert_path: PathBuf,
key_path: PathBuf,
interval: std::time::Duration,
baseline: (Option<std::time::SystemTime>, Option<std::time::SystemTime>),
}
impl CertReloader {
pub fn load(
cert_path: PathBuf,
key_path: PathBuf,
provider: Arc<CryptoProvider>,
now_unix_secs: i64,
interval: std::time::Duration,
) -> Result<(Arc<ReloadableCertResolver>, Self), TlsError> {
let baseline = file_mtimes(&cert_path, &key_path);
let certified = load_certified_key(&cert_path, &key_path, &provider, now_unix_secs)?;
let resolver = Arc::new(ReloadableCertResolver::new(certified));
let reloader = Self {
resolver: Arc::clone(&resolver),
provider,
cert_path,
key_path,
interval,
baseline,
};
Ok((resolver, reloader))
}
pub async fn run(self, shutdown: tokio_util::sync::CancellationToken) {
let interval = if self.interval.is_zero() {
std::time::Duration::from_secs(DEFAULT_RELOAD_INTERVAL_SECS)
} else {
self.interval
};
let stat_mtimes = |cert: PathBuf, key: PathBuf| {
crate::time::spawn_blocking(move || file_mtimes(&cert, &key))
};
let mut last = self.baseline;
loop {
tokio::select! {
() = tokio::time::sleep(interval) => {}
() = shutdown.cancelled() => break,
}
let current = match stat_mtimes(self.cert_path.clone(), self.key_path.clone()).await {
Ok(mtimes) => mtimes,
Err(e) => {
tracing::warn!(error = %e, "TLS reload: mtime read task failed; skipping tick");
continue;
}
};
if current == last {
continue;
}
let cert_path = self.cert_path.clone();
let key_path = self.key_path.clone();
let provider = Arc::clone(&self.provider);
let loaded = crate::time::spawn_blocking(move || {
load_certified_key(&cert_path, &key_path, &provider, now_unix())
})
.await;
let loaded = match loaded {
Ok(result) => result,
Err(e) => {
tracing::warn!(error = %e, "TLS reload: load task failed; skipping tick");
continue;
}
};
match loaded {
Ok(next) => {
self.resolver.store(next);
last = current;
tracing::info!(
cert = %self.cert_path.display(),
"Reloaded TLS certificate after detecting a change on disk"
);
}
Err(e) => {
tracing::error!(
error = %e,
cert = %self.cert_path.display(),
"TLS certificate reload failed; keeping the previously loaded certificate"
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
const CERT_PEM: &str = include_str!("../tests/fixtures/tls/localhost.cert.pem");
const KEY_PEM: &str = include_str!("../tests/fixtures/tls/localhost.key.pem");
const MISMATCHED_KEY_PEM: &str = include_str!("../tests/fixtures/tls/other.key.pem");
const EXPIRED_CERT_PEM: &str = include_str!("../tests/fixtures/tls/expired.cert.pem");
const EXPIRED_KEY_PEM: &str = include_str!("../tests/fixtures/tls/expired.key.pem");
const MALFORMED_CERT_PEM: &str = "\
-----BEGIN CERTIFICATE-----
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
";
fn write_temp(dir: &Path, name: &str, contents: &str) -> PathBuf {
let path = dir.join(name);
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(contents.as_bytes()).unwrap();
path
}
fn now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.try_into()
.unwrap()
}
#[test]
fn load_valid_pair_succeeds() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let ck = load_certified_key(&cert, &key, &provider, now()).expect("valid pair loads");
assert!(!ck.cert.is_empty());
}
#[test]
fn malformed_intermediate_in_chain_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let chain_pem = format!("{CERT_PEM}\n{MALFORMED_CERT_PEM}");
let cert = write_temp(dir.path(), "chain.pem", &chain_pem);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
assert!(
matches!(err, TlsError::ParseChainCert { position: 2, .. }),
"a malformed intermediate must fail fast naming its chain position, got {err:?}"
);
assert!(
err.to_string().contains("#2"),
"error must name the offending position, got: {err}"
);
}
#[test]
fn valid_leaf_and_intermediate_chain_loads() {
let dir = tempfile::tempdir().unwrap();
let chain_pem = format!("{CERT_PEM}\n{CERT_PEM}");
let cert = write_temp(dir.path(), "chain.pem", &chain_pem);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let ck = load_certified_key(&cert, &key, &provider, now())
.expect("a valid leaf + well-formed intermediate must load");
assert_eq!(ck.cert.len(), 2, "both chain certificates are retained");
}
#[test]
fn expired_intermediate_in_chain_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let chain_pem = format!("{CERT_PEM}\n{EXPIRED_CERT_PEM}");
let path = write_temp(dir.path(), "chain.pem", &chain_pem);
let chain = read_cert_chain(&path).unwrap();
let err = validate_chain_certs(&path, &chain, now()).unwrap_err();
assert!(
matches!(err, TlsError::ExpiredChainCert { position: 2, .. }),
"an expired intermediate must fail fast at position 2, got {err:?}"
);
assert!(
err.to_string().contains("#2"),
"error must name the offending position, got: {err}"
);
assert!(
err.to_string().contains("expired"),
"error must report expiry, got: {err}"
);
}
#[test]
fn not_yet_valid_intermediate_in_chain_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let leaf = write_temp(dir.path(), "leaf.pem", CERT_PEM);
let leaf_key = write_temp(dir.path(), "leaf.key.pem", KEY_PEM);
let not_before = inspect_leaf(&leaf, &leaf_key, now())
.unwrap()
.not_before_unix;
let chain_pem = format!("{CERT_PEM}\n{CERT_PEM}");
let path = write_temp(dir.path(), "chain.pem", &chain_pem);
let chain = read_cert_chain(&path).unwrap();
let err = validate_chain_certs(&path, &chain, not_before - 3600).unwrap_err();
assert!(
matches!(err, TlsError::NotYetValidChainCert { position: 2, .. }),
"a not-yet-valid intermediate must fail fast at position 2, got {err:?}"
);
assert!(
err.to_string().contains("#2"),
"error must name the offending position, got: {err}"
);
assert!(
err.to_string().contains("not yet valid"),
"error must report the not-yet-valid window, got: {err}"
);
}
#[test]
fn valid_intermediate_in_chain_is_accepted() {
let dir = tempfile::tempdir().unwrap();
let chain_pem = format!("{CERT_PEM}\n{CERT_PEM}");
let path = write_temp(dir.path(), "chain.pem", &chain_pem);
let chain = read_cert_chain(&path).unwrap();
validate_chain_certs(&path, &chain, now())
.expect("a valid, in-window intermediate must be accepted");
}
#[test]
fn missing_cert_file_names_the_path() {
let dir = tempfile::tempdir().unwrap();
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let missing = dir.path().join("does-not-exist.pem");
let provider = crypto_provider();
let err = load_certified_key(&missing, &key, &provider, now()).unwrap_err();
assert!(matches!(err, TlsError::ReadCert { .. }));
assert!(err.to_string().contains("does-not-exist.pem"));
}
#[test]
fn unparseable_cert_pem_is_reported() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", "not a pem file");
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
assert!(matches!(
err,
TlsError::NoCertificates { .. } | TlsError::ParseCert { .. }
));
}
#[test]
fn mismatched_key_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", MISMATCHED_KEY_PEM);
let provider = crypto_provider();
let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
assert!(
matches!(err, TlsError::InvalidKeyPair { .. }),
"got {err:?}"
);
}
#[test]
fn expired_leaf_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", EXPIRED_CERT_PEM);
let key = write_temp(dir.path(), "k.pem", EXPIRED_KEY_PEM);
let provider = crypto_provider();
let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
assert!(matches!(err, TlsError::Expired { .. }), "got {err:?}");
}
#[test]
fn not_yet_valid_leaf_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let not_before = inspect_leaf(&cert, &key, now()).unwrap().not_before_unix;
let err = load_certified_key(&cert, &key, &provider, not_before - 3600).unwrap_err();
assert!(matches!(err, TlsError::NotYetValid { .. }), "got {err:?}");
load_certified_key(&cert, &key, &provider, not_before + 1)
.expect("cert loads once notBefore has passed");
}
#[test]
fn inspect_reports_not_yet_valid_window() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let inspection = inspect_leaf(&cert, &key, now()).expect("pair inspects");
assert!(inspection.is_not_yet_valid(inspection.not_before_unix - 1));
assert!(!inspection.is_not_yet_valid(inspection.not_before_unix));
assert!(!inspection.is_not_yet_valid(inspection.not_after_unix));
}
#[test]
fn inspect_reports_future_expiry() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let inspection = inspect_leaf(&cert, &key, now()).expect("valid pair inspects");
assert!(
inspection.days_until_expiry(now()) > 30,
"fixture should be valid far into the future"
);
}
#[test]
fn inspect_still_reports_expiry_for_expired_cert() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", EXPIRED_CERT_PEM);
let key = write_temp(dir.path(), "k.pem", EXPIRED_KEY_PEM);
let inspection = inspect_leaf(&cert, &key, now()).expect("expired pair still inspects");
assert!(inspection.days_until_expiry(now()) < 0);
}
#[test]
fn reloadable_resolver_swaps_certificate() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let first = load_certified_key(&cert, &key, &provider, now()).unwrap();
let resolver = ReloadableCertResolver::new(Arc::clone(&first));
assert!(Arc::ptr_eq(&resolver.current(), &first));
let second = load_certified_key(&cert, &key, &provider, now()).unwrap();
resolver.store(Arc::clone(&second));
assert!(Arc::ptr_eq(&resolver.current(), &second));
}
#[test]
fn server_config_advertises_h2_then_http11_alpn() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let certified = load_certified_key(&cert, &key, &provider, now()).unwrap();
let config =
build_server_config(provider, Arc::new(ReloadableCertResolver::new(certified)))
.expect("server config builds");
assert_eq!(
config.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()],
"the static [server.tls] path must advertise h2 first, then http/1.1"
);
}
#[test]
fn server_config_with_resolver_and_client_auth_advertise_the_same_alpn() {
let dir = tempfile::tempdir().unwrap();
let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
let key = write_temp(dir.path(), "k.pem", KEY_PEM);
let provider = crypto_provider();
let certified = load_certified_key(&cert, &key, &provider, now()).unwrap();
let expected = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let via_resolver = build_server_config_with_resolver(
Arc::clone(&provider),
Arc::new(ReloadableCertResolver::new(Arc::clone(&certified)))
as Arc<dyn ResolvesServerCert>,
)
.expect("resolver config builds");
assert_eq!(
via_resolver.alpn_protocols, expected,
"the resolver (ACME) entry point must advertise the same ALPN"
);
let ca = write_temp(
dir.path(),
"ca.pem",
include_str!("../tests/fixtures/tls/client/ca.cert.pem"),
);
let verifier = client_auth::build_client_verifier(
client_auth::load_client_roots(&ca).expect("client roots load"),
vec![],
crate::config::ClientAuthMode::Optional,
Arc::clone(&provider),
)
.expect("client verifier builds");
let via_client_auth = build_server_config_with_client_auth(
provider,
Arc::new(ReloadableCertResolver::new(certified)) as Arc<dyn ResolvesServerCert>,
Some(verifier),
)
.expect("client-auth config builds");
assert_eq!(
via_client_auth.alpn_protocols, expected,
"the client-auth arm must advertise the same ALPN"
);
}
#[test]
fn serve_stack_speaks_http2() {
let mut builder =
hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new());
let _ = builder.http2();
}
}