use std::path::Path;
use std::sync::Arc;
use arc_swap::ArcSwap;
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
use tokio_rustls::rustls::client::danger::{
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
};
use tokio_rustls::rustls::client::{ResolvesClientCert, WebPkiServerVerifier};
use tokio_rustls::rustls::sign::CertifiedKey;
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme};
use zeroize::Zeroizing;
use crate::config::ClientIdentityConfig;
use crate::error::{Error, Result};
const HTTP_ALPN: [&[u8]; 2] = [b"h2", b"http/1.1"];
#[cfg(feature = "grpc")]
const GRPC_ALPN: [&[u8]; 1] = [b"h2"];
const PEER_CA_ROLE: &str = "peer CA";
pub(crate) struct IdentityMaterial {
cert_pem: Vec<u8>,
key_pem: Zeroizing<Vec<u8>>,
chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
}
impl std::fmt::Debug for IdentityMaterial {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IdentityMaterial")
.field("chain_len", &self.chain.len())
.field("cert_pem_len", &self.cert_pem.len())
.field("key_pem_len", &self.key_pem.len())
.finish_non_exhaustive()
}
}
#[must_use]
pub fn concat_identity_pem(cert_pem: &[u8], key_pem: &[u8]) -> Zeroizing<Vec<u8>> {
let needs_separator = !cert_pem.ends_with(b"\n");
let mut joined = Zeroizing::new(Vec::with_capacity(
cert_pem.len() + usize::from(needs_separator) + key_pem.len(),
));
joined.extend_from_slice(cert_pem);
if needs_separator {
joined.push(b'\n');
}
joined.extend_from_slice(key_pem);
joined
}
pub(crate) fn load_identity_material(config: &ClientIdentityConfig) -> Result<IdentityMaterial> {
use rustls_pki_types::pem::PemObject;
let cert_pem = std::fs::read(&config.cert_path).map_err(|e| {
Error::Internal(format!(
"Failed to open client identity cert file '{}': {}",
config.cert_path.display(),
e
))
})?;
let key_pem = Zeroizing::new(std::fs::read(&config.key_path).map_err(|e| {
Error::Internal(format!(
"Failed to open client identity key file '{}': {}",
config.key_path.display(),
e
))
})?);
let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_pem)
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| {
Error::Internal(format!(
"Failed to parse client identity certificates from '{}': {}",
config.cert_path.display(),
e
))
})?;
if chain.is_empty() {
return Err(Error::Internal(format!(
"Client identity cert file '{}' contains no certificates",
config.cert_path.display()
)));
}
let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(|e| {
Error::Internal(format!(
"Failed to parse client identity private key from '{}': {}",
config.key_path.display(),
e
))
})?;
let material = IdentityMaterial {
cert_pem,
key_pem,
chain,
key,
};
certified_key_from_material(&material, config)?;
Ok(material)
}
fn certified_key_from_material(
material: &IdentityMaterial,
config: &ClientIdentityConfig,
) -> Result<CertifiedKey> {
use tokio_rustls::rustls::crypto::CryptoProvider;
crate::crypto::ensure_default_crypto_provider();
let provider = CryptoProvider::get_default().ok_or_else(|| {
Error::Internal(
"No rustls crypto provider is installed; enable exactly one of the \
`crypto-aws-lc-rs` or `crypto-ring` features"
.to_string(),
)
})?;
let signing_key = provider
.key_provider
.load_private_key(material.key.clone_key())
.map_err(|e| {
Error::Internal(format!(
"Client identity private key from '{}' is not usable by the \
configured crypto provider: {}",
config.key_path.display(),
e
))
})?;
let certified = CertifiedKey::new(material.chain.clone(), signing_key);
certified.keys_match().map_err(|e| {
Error::Internal(format!(
"Client identity private key '{}' does not match the certificate \
in '{}': {}",
config.key_path.display(),
config.cert_path.display(),
e
))
})?;
Ok(certified)
}
fn build_root_store(config: &ClientIdentityConfig) -> Result<RootCertStore> {
let mut roots = RootCertStore::empty();
let Some(ref path) = config.root_ca_path else {
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
return Ok(roots);
};
let peer_roots = crate::tls::load_root_store(path, PEER_CA_ROLE)?;
if !config.exclusive_roots {
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
roots.roots.extend(peer_roots.roots);
Ok(roots)
}
fn read_validated_ca_bundle(path: &Path) -> Result<Vec<u8>> {
crate::tls::load_root_store(path, PEER_CA_ROLE)?;
std::fs::read(path).map_err(|e| {
Error::Internal(format!(
"Failed to read {} file '{}': {}",
PEER_CA_ROLE,
path.display(),
e
))
})
}
#[derive(Debug)]
struct RotatingClientCertResolver {
current: ArcSwap<CertifiedKey>,
}
impl RotatingClientCertResolver {
fn new(initial: CertifiedKey) -> Self {
Self {
current: ArcSwap::new(Arc::new(initial)),
}
}
fn store(&self, key: CertifiedKey) {
self.current.store(Arc::new(key));
}
#[cfg(test)]
fn current(&self) -> Arc<CertifiedKey> {
self.current.load_full()
}
}
impl ResolvesClientCert for RotatingClientCertResolver {
fn resolve(
&self,
_root_hint_subjects: &[&[u8]],
_sigschemes: &[SignatureScheme],
) -> Option<Arc<CertifiedKey>> {
Some(self.current.load_full())
}
fn has_certs(&self) -> bool {
true
}
}
#[derive(Debug)]
struct RotatingWebPkiVerifier {
current: ArcSwap<WebPkiServerVerifier>,
}
impl RotatingWebPkiVerifier {
fn new(initial: Arc<WebPkiServerVerifier>) -> Self {
Self {
current: ArcSwap::new(initial),
}
}
fn store(&self, verifier: Arc<WebPkiServerVerifier>) {
self.current.store(verifier);
}
}
impl ServerCertVerifier for RotatingWebPkiVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
ocsp_response: &[u8],
now: UnixTime,
) -> std::result::Result<ServerCertVerified, tokio_rustls::rustls::Error> {
self.current.load().verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
)
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
self.current
.load()
.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
self.current
.load()
.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.current.load().supported_verify_schemes()
}
}
fn build_peer_verifier(config: &ClientIdentityConfig) -> Result<Arc<WebPkiServerVerifier>> {
crate::crypto::ensure_default_crypto_provider();
let roots = build_root_store(config)?;
WebPkiServerVerifier::builder(Arc::new(roots))
.build()
.map_err(|e| {
Error::Internal(format!(
"Failed to build a peer certificate verifier from the configured \
{} roots: {}",
PEER_CA_ROLE, e
))
})
}
pub fn load_rustls_client_config(config: &ClientIdentityConfig) -> Result<Arc<ClientConfig>> {
let material = load_identity_material(config)?;
let roots = build_root_store(config)?;
crate::crypto::ensure_default_crypto_provider();
let client_config = ClientConfig::builder()
.with_root_certificates(roots)
.with_client_auth_cert(material.chain.clone(), material.key.clone_key())
.map_err(|e| Error::Internal(format!("Failed to build rustls client config: {}", e)))?;
Ok(Arc::new(client_config))
}
pub fn load_reqwest_identity(config: &ClientIdentityConfig) -> Result<reqwest::Identity> {
let material = load_identity_material(config)?;
let joined = concat_identity_pem(&material.cert_pem, &material.key_pem);
reqwest::Identity::from_pem(&joined).map_err(|e| {
Error::Internal(format!(
"Failed to build a reqwest identity from '{}' and '{}': {}",
config.cert_path.display(),
config.key_path.display(),
e
))
})
}
pub fn reqwest_client_builder(config: &ClientIdentityConfig) -> Result<reqwest::ClientBuilder> {
crate::crypto::ensure_default_crypto_provider();
let identity = load_reqwest_identity(config)?;
let mut builder = reqwest::Client::builder().identity(identity);
if let Some(ref path) = config.root_ca_path {
let pem = read_validated_ca_bundle(path)?;
let certs = reqwest::Certificate::from_pem_bundle(&pem).map_err(|e| {
Error::Internal(format!(
"Failed to build reqwest certificates from {} file '{}': {}",
PEER_CA_ROLE,
path.display(),
e
))
})?;
for cert in certs {
builder = builder.add_root_certificate(cert);
}
if config.exclusive_roots {
builder = builder.tls_built_in_root_certs(false);
}
}
Ok(builder)
}
#[cfg(feature = "grpc")]
pub fn tonic_client_tls_config(
config: &ClientIdentityConfig,
) -> Result<tonic::transport::ClientTlsConfig> {
crate::crypto::ensure_default_crypto_provider();
let material = load_identity_material(config)?;
let identity = tonic::transport::Identity::from_pem(&material.cert_pem, &material.key_pem);
let mut tls = tonic::transport::ClientTlsConfig::new().identity(identity);
let Some(ref path) = config.root_ca_path else {
return Ok(tls.trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()));
};
let pem = read_validated_ca_bundle(path)?;
tls = tls.ca_certificate(tonic::transport::Certificate::from_pem(pem));
if !config.exclusive_roots {
tls = tls.trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
Ok(tls)
}
#[derive(Clone)]
pub struct ClientIdentitySource {
inner: Arc<ClientIdentitySourceInner>,
}
struct ClientIdentitySourceInner {
client: Arc<reqwest::Client>,
resolver: Arc<RotatingClientCertResolver>,
verifier: Arc<RotatingWebPkiVerifier>,
tls: ClientConfig,
origin: ClientIdentityConfig,
}
impl ClientIdentitySource {
pub fn from_config(config: &ClientIdentityConfig) -> Result<Self> {
crate::crypto::ensure_default_crypto_provider();
let material = load_identity_material(config)?;
let resolver = Arc::new(RotatingClientCertResolver::new(
certified_key_from_material(&material, config)?,
));
let verifier = Arc::new(RotatingWebPkiVerifier::new(build_peer_verifier(config)?));
let tls = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::clone(&verifier) as Arc<dyn ServerCertVerifier>)
.with_client_cert_resolver(Arc::clone(&resolver) as Arc<dyn ResolvesClientCert>);
let client = build_client(&tls, config)?;
Ok(Self {
inner: Arc::new(ClientIdentitySourceInner {
client: Arc::new(client),
resolver,
verifier,
tls,
origin: config.clone(),
}),
})
}
#[must_use]
pub fn client(&self) -> Arc<reqwest::Client> {
Arc::clone(&self.inner.client)
}
#[must_use]
pub fn origin(&self) -> &ClientIdentityConfig {
&self.inner.origin
}
pub fn reload(&self) -> Result<()> {
let origin = &self.inner.origin;
match self.prepare_reload() {
Ok((key, verifier)) => {
self.inner.resolver.store(key);
self.inner.verifier.store(verifier);
tracing::info!(
cert_path = %origin.cert_path.display(),
key_path = %origin.key_path.display(),
"client identity reloaded in place; new handshakes use the new \
credentials and existing pooled connections are untouched"
);
Ok(())
}
Err(e) => {
tracing::error!(
cert_path = %origin.cert_path.display(),
key_path = %origin.key_path.display(),
error = %e,
"client identity reload failed; continuing to use the previous \
certificate and trust anchors. New credentials will not take \
effect until a reload succeeds."
);
Err(e)
}
}
}
fn prepare_reload(&self) -> Result<(CertifiedKey, Arc<WebPkiServerVerifier>)> {
let origin = &self.inner.origin;
let material = load_identity_material(origin)?;
let key = certified_key_from_material(&material, origin)?;
let verifier = build_peer_verifier(origin)?;
Ok((key, verifier))
}
fn tls_config_with_alpn(&self, alpn: &[&[u8]]) -> ClientConfig {
let mut tls = self.inner.tls.clone();
tls.alpn_protocols = alpn.iter().map(|p| p.to_vec()).collect();
tls
}
#[cfg(feature = "grpc")]
pub fn grpc_channel(
&self,
endpoint: tonic::transport::Endpoint,
) -> Result<tonic::transport::Channel> {
let scheme = endpoint.uri().scheme_str();
if scheme != Some("https") {
return Err(Error::Internal(format!(
"gRPC endpoint '{}' must use the https scheme to present a client \
identity, but it uses '{}'",
endpoint.uri(),
scheme.unwrap_or("(none)")
)));
}
let connector = RotatingTlsConnector {
tls: Arc::new(self.tls_config_with_alpn(&GRPC_ALPN)),
};
Ok(endpoint.connect_with_connector_lazy(connector))
}
#[cfg(feature = "grpc")]
pub fn tonic_client_tls_config_snapshot(&self) -> Result<tonic::transport::ClientTlsConfig> {
tonic_client_tls_config(&self.inner.origin)
}
}
impl std::fmt::Debug for ClientIdentitySource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientIdentitySource")
.field("origin", &self.inner.origin)
.finish_non_exhaustive()
}
}
#[cfg(feature = "grpc")]
#[derive(Clone)]
struct RotatingTlsConnector {
tls: Arc<ClientConfig>,
}
#[cfg(feature = "grpc")]
impl std::fmt::Debug for RotatingTlsConnector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RotatingTlsConnector")
.field("alpn_protocols", &self.tls.alpn_protocols)
.finish_non_exhaustive()
}
}
#[cfg(feature = "grpc")]
impl tower::Service<http::Uri> for RotatingTlsConnector {
type Response = hyper_util::rt::TokioIo<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
type Error = Error;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Self::Response>> + Send + 'static>,
>;
fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<()>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, uri: http::Uri) -> Self::Future {
let tls = Arc::clone(&self.tls);
Box::pin(async move { connect_tls(tls, uri).await })
}
}
#[cfg(feature = "grpc")]
fn resolve_target(host: &str) -> Result<(&str, ServerName<'static>)> {
let connect_host = host
.strip_prefix('[')
.and_then(|rest| rest.strip_suffix(']'))
.unwrap_or(host);
let server_name = ServerName::try_from(connect_host.to_string()).map_err(|e| {
Error::Internal(format!(
"gRPC endpoint host '{}' is not a valid TLS server name: {}",
connect_host, e
))
})?;
Ok((connect_host, server_name))
}
#[cfg(feature = "grpc")]
async fn connect_tls(
tls: Arc<ClientConfig>,
uri: http::Uri,
) -> Result<hyper_util::rt::TokioIo<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>> {
let host = uri.host().ok_or_else(|| {
Error::Internal(format!("gRPC endpoint '{}' has no host to connect to", uri))
})?;
let port = uri.port_u16().unwrap_or(443);
let (connect_host, server_name) = resolve_target(host)?;
let tcp = tokio::net::TcpStream::connect((connect_host, port))
.await
.map_err(|e| {
Error::Internal(format!(
"Failed to connect to gRPC endpoint '{}': {}",
uri, e
))
})?;
tcp.set_nodelay(true).map_err(|e| {
Error::Internal(format!(
"Failed to disable Nagle's algorithm on the connection to '{}': {}",
uri, e
))
})?;
let stream = tokio_rustls::TlsConnector::from(tls)
.connect(server_name, tcp)
.await
.map_err(|e| {
Error::Internal(format!(
"TLS handshake with gRPC endpoint '{}' failed: {}",
uri, e
))
})?;
Ok(hyper_util::rt::TokioIo::new(stream))
}
fn build_client(tls: &ClientConfig, config: &ClientIdentityConfig) -> Result<reqwest::Client> {
let mut tls = tls.clone();
tls.alpn_protocols = HTTP_ALPN.iter().map(|p| p.to_vec()).collect();
reqwest::Client::builder()
.use_preconfigured_tls(tls)
.build()
.map_err(|e| {
Error::Internal(format!(
"Failed to build a reqwest client for the identity in '{}': {}",
config.cert_path.display(),
e
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::PathBuf;
struct TestCert {
cert_pem: String,
key_pem: String,
}
fn generate_cert(name: &str) -> TestCert {
let certified = rcgen::generate_simple_self_signed(vec![name.to_string()])
.expect("self-signed cert generation");
TestCert {
cert_pem: certified.cert.pem(),
key_pem: certified.signing_key.serialize_pem(),
}
}
fn write_temp(contents: &str) -> tempfile::NamedTempFile {
let mut file = tempfile::NamedTempFile::new().expect("temp file");
file.write_all(contents.as_bytes()).expect("write temp");
file.flush().expect("flush temp");
file
}
fn write_identity(dir: &Path, cert: &TestCert) -> ClientIdentityConfig {
let cert_path = dir.join("client.pem");
let key_path = dir.join("client.key");
std::fs::write(&cert_path, &cert.cert_pem).expect("write cert");
std::fs::write(&key_path, &cert.key_pem).expect("write key");
config_for(cert_path, key_path)
}
fn config_for(cert_path: PathBuf, key_path: PathBuf) -> ClientIdentityConfig {
ClientIdentityConfig {
enabled: true,
cert_path,
key_path,
root_ca_path: None,
exclusive_roots: false,
}
}
fn pem_body(pem: &str) -> String {
pem.lines()
.filter(|line| !line.starts_with("-----"))
.collect::<Vec<_>>()
.join("")
}
#[test]
fn concat_identity_pem_inserts_a_separator_when_the_cert_lacks_one() {
let joined = concat_identity_pem(b"CERT", b"KEY");
assert_eq!(
joined.as_slice(),
b"CERT\nKEY",
"a cert without a trailing newline must not be spliced onto the key"
);
}
#[test]
fn concat_identity_pem_does_not_double_an_existing_separator() {
let joined = concat_identity_pem(b"CERT\n", b"KEY");
assert_eq!(
joined.as_slice(),
b"CERT\nKEY",
"a cert already ending in a newline must not gain a blank line"
);
}
#[test]
fn concat_identity_pem_preserves_both_documents_in_order() {
let cert = generate_cert("client");
let joined = concat_identity_pem(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes());
let text = String::from_utf8(joined.to_vec()).expect("pem is utf-8");
let cert_at = text.find("BEGIN CERTIFICATE").expect("cert present");
let key_at = text.find("PRIVATE KEY").expect("key present");
assert!(
cert_at < key_at,
"the certificate must precede the key in the joined buffer"
);
}
#[test]
fn load_identity_material_reads_a_matching_pair() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
let material = load_identity_material(&config).expect("a matching pair must load");
assert_eq!(
material.chain.len(),
1,
"a single self-signed cert must yield a one-element chain"
);
assert!(!material.key_pem.is_empty(), "the key bytes must be read");
}
#[test]
fn load_identity_material_rejects_a_missing_cert_file() {
let cert = generate_cert("client");
let key_file = write_temp(&cert.key_pem);
let config = config_for(
PathBuf::from("/nonexistent/client.pem"),
key_file.path().to_path_buf(),
);
let err = load_identity_material(&config)
.expect_err("a missing certificate must fail the load, not yield an empty chain");
assert!(
err.to_string()
.contains("Failed to open client identity cert file"),
"error must name the failure to open the cert: {err}"
);
}
#[test]
fn load_identity_material_rejects_a_missing_key_file() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let config = config_for(
cert_file.path().to_path_buf(),
PathBuf::from("/nonexistent/client.key"),
);
let err = load_identity_material(&config).expect_err("a missing key must fail the load");
assert!(
err.to_string()
.contains("Failed to open client identity key file"),
"error must name the failure to open the key: {err}"
);
}
#[test]
fn load_identity_material_rejects_a_cert_file_without_certificates() {
let cert = generate_cert("client");
let cert_file = write_temp("# no certificates here\n");
let key_file = write_temp(&cert.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
let err = load_identity_material(&config)
.expect_err("a cert file with no certificates must fail the load");
assert!(
err.to_string().contains("contains no certificates"),
"error must explain that the chain is empty: {err}"
);
}
#[test]
fn load_identity_material_rejects_an_unparseable_key() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp("-----BEGIN PRIVATE KEY-----\ntruncated");
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
let err = load_identity_material(&config).expect_err("a truncated key must fail the load");
assert!(
err.to_string()
.contains("Failed to parse client identity private key"),
"error must name the key parse failure: {err}"
);
}
#[test]
fn load_identity_material_rejects_a_mismatched_cert_and_key() {
let cert = generate_cert("client");
let other = generate_cert("someone-else");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&other.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
let err = load_identity_material(&config)
.expect_err("a key from a different key pair must fail the load");
assert!(
err.to_string().contains("does not match the certificate"),
"error must say the pair does not match: {err}"
);
}
#[test]
fn load_rustls_client_config_accepts_a_matching_pair() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
let tls = load_rustls_client_config(&config).expect("a matching pair must build");
assert!(
tls.alpn_protocols.is_empty(),
"the documented contract is that no ALPN protocols are set"
);
}
#[test]
fn load_rustls_client_config_rejects_a_mismatched_cert_and_key() {
let cert = generate_cert("client");
let other = generate_cert("someone-else");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&other.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
let err = load_rustls_client_config(&config)
.expect_err("the pair must be validated, not merely parsed");
assert!(
err.to_string().contains("does not match the certificate"),
"error must identify the mismatch rather than a generic build failure: {err}"
);
}
#[test]
fn load_rustls_client_config_rejects_an_unreadable_peer_ca() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let mut config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
config.root_ca_path = Some(PathBuf::from("/nonexistent/peer-ca.pem"));
let err = load_rustls_client_config(&config)
.expect_err("an unreadable peer CA must fail the whole build");
assert!(
err.to_string().contains("Failed to open peer CA file"),
"error must name the peer CA, not the server's client CA: {err}"
);
}
#[test]
fn additive_roots_keep_the_built_in_web_pki_anchors() {
let cert = generate_cert("client");
let ca = generate_cert("peer-ca");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let ca_file = write_temp(&ca.cert_pem);
let mut config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
config.root_ca_path = Some(ca_file.path().to_path_buf());
let roots = build_root_store(&config).expect("roots must build");
assert_eq!(
roots.len(),
webpki_roots::TLS_SERVER_ROOTS.len() + 1,
"the default is additive: the private CA joins the public roots"
);
}
#[test]
fn exclusive_roots_replace_the_built_in_web_pki_anchors() {
let cert = generate_cert("client");
let ca = generate_cert("peer-ca");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let ca_file = write_temp(&ca.cert_pem);
let mut config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
config.root_ca_path = Some(ca_file.path().to_path_buf());
config.exclusive_roots = true;
let roots = build_root_store(&config).expect("roots must build");
assert_eq!(
roots.len(),
1,
"exclusive roots must pin trust to the configured CA alone"
);
}
#[test]
fn no_peer_ca_falls_back_to_the_built_in_web_pki_anchors() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let mut config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
config.exclusive_roots = true;
let roots = build_root_store(&config).expect("roots must build");
assert_eq!(
roots.len(),
webpki_roots::TLS_SERVER_ROOTS.len(),
"exclusive_roots must be ignored without a bundle to replace them with"
);
}
#[test]
fn load_reqwest_identity_accepts_a_matching_pair() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
load_reqwest_identity(&config).expect("a matching pair must yield a reqwest identity");
}
#[test]
fn reqwest_client_builder_builds_a_usable_client() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
reqwest_client_builder(&config)
.expect("builder must be produced")
.build()
.expect("the builder must produce a client");
}
#[test]
fn reqwest_client_builder_rejects_a_peer_ca_without_certificates() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let ca_file = write_temp("# no certificates here\n");
let mut config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
config.root_ca_path = Some(ca_file.path().to_path_buf());
let err = reqwest_client_builder(&config)
.expect_err("an empty peer CA bundle must fail rather than trust nothing extra");
assert!(
err.to_string().contains("contains no certificates"),
"error must explain that the bundle is empty: {err}"
);
}
#[test]
fn from_config_rejects_credentials_that_do_not_load() {
let cert = generate_cert("client");
let key_file = write_temp(&cert.key_pem);
let config = config_for(
PathBuf::from("/nonexistent/client.pem"),
key_file.path().to_path_buf(),
);
ClientIdentitySource::from_config(&config)
.expect_err("no source must exist when the identity cannot be loaded");
}
#[test]
fn origin_reports_the_files_the_source_reloads_from() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
assert_eq!(source.origin().cert_path, config.cert_path);
assert_eq!(source.origin().key_path, config.key_path);
}
fn presented_leaf(source: &ClientIdentitySource) -> Vec<u8> {
source
.inner
.resolver
.current()
.end_entity_cert()
.expect("a resolver always holds a chain with a leaf")
.to_vec()
}
#[test]
fn a_successful_reload_swaps_the_key_the_resolver_presents() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let before = presented_leaf(&source);
let second = generate_cert("client");
std::fs::write(&config.cert_path, &second.cert_pem).expect("rewrite cert");
std::fs::write(&config.key_path, &second.key_pem).expect("rewrite key");
source
.reload()
.expect("rereading valid credentials must succeed");
assert_ne!(
presented_leaf(&source),
before,
"a successful reload must install the new certificate in the resolver, \
because that is what every subsequent handshake reads"
);
}
#[test]
fn a_cached_client_handle_observes_a_reload() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let cached = source.client();
let before = presented_leaf(&source);
let second = generate_cert("client");
std::fs::write(&config.cert_path, &second.cert_pem).expect("rewrite cert");
std::fs::write(&config.key_path, &second.key_pem).expect("rewrite key");
source.reload().expect("reload");
assert!(
Arc::ptr_eq(&cached, &source.client()),
"the handle must be the same client after a reload: rebuilding it would \
discard the connection pool and reintroduce the do-not-cache rule"
);
assert_ne!(
presented_leaf(&source),
before,
"the cached handle must nevertheless present the rotated certificate"
);
}
#[test]
fn failed_reload_preserves_the_last_good_identity() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let last_good = presented_leaf(&source);
let client = source.client();
std::fs::write(&config.cert_path, "-----BEGIN CERTIFICATE-----\ntruncated")
.expect("corrupt cert");
let err = source
.reload()
.expect_err("an unparseable certificate must fail the reload");
assert_eq!(
presented_leaf(&source),
last_good,
"a failed reload must keep the last-good identity, not drop it"
);
assert!(
Arc::ptr_eq(&source.client(), &client),
"a failed reload must not disturb the client either"
);
assert!(
!err.to_string().is_empty(),
"the failure must be reported to the caller: {err}"
);
let replacement = generate_cert("client");
std::fs::write(&config.cert_path, &replacement.cert_pem).expect("rewrite cert");
std::fs::write(&config.key_path, &replacement.key_pem).expect("rewrite key");
source.reload().expect("a later valid reload must succeed");
assert_ne!(
presented_leaf(&source),
last_good,
"the recovered reload must install the new identity"
);
}
#[test]
fn a_corrupt_ca_bundle_blocks_an_otherwise_valid_identity_rotation() {
let dir = tempfile::tempdir().expect("temp dir");
let mut config = write_identity(dir.path(), &generate_cert("client"));
let ca_path = dir.path().join("peer-ca.pem");
std::fs::write(&ca_path, generate_cert("peer-ca").cert_pem).expect("write ca");
config.root_ca_path = Some(ca_path.clone());
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let last_good = presented_leaf(&source);
let second = generate_cert("client");
std::fs::write(&config.cert_path, &second.cert_pem).expect("rewrite cert");
std::fs::write(&config.key_path, &second.key_pem).expect("rewrite key");
std::fs::write(&ca_path, "-----BEGIN CERTIFICATE-----\ntruncated").expect("corrupt ca");
source
.reload()
.expect_err("a corrupt CA bundle must fail the whole reload");
assert_eq!(
presented_leaf(&source),
last_good,
"the valid new certificate must NOT have been installed: the reload is \
all-or-nothing, and its trust anchors could not be loaded"
);
std::fs::write(&ca_path, generate_cert("peer-ca-2").cert_pem).expect("rewrite ca");
source.reload().expect("a fully valid reload must succeed");
assert_ne!(
presented_leaf(&source),
last_good,
"once every artifact loads, the rotation must apply in full"
);
}
#[test]
fn clones_of_a_source_observe_the_same_reload() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let caller_side = source.clone();
let before = presented_leaf(&caller_side);
let second = generate_cert("client");
std::fs::write(&config.cert_path, &second.cert_pem).expect("rewrite cert");
std::fs::write(&config.key_path, &second.key_pem).expect("rewrite key");
source.reload().expect("reload");
assert!(
Arc::ptr_eq(&caller_side.client(), &source.client()),
"every clone must hand out the same client"
);
assert_ne!(
presented_leaf(&caller_side),
before,
"the clone held by the caller must see the rotation"
);
}
#[test]
fn the_http_client_negotiates_http2_and_http11() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let tls = source.tls_config_with_alpn(&HTTP_ALPN);
assert_eq!(
tls.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()],
"the HTTP path must offer h2 first, then http/1.1, as reqwest does natively"
);
}
#[test]
fn the_resolver_always_reports_that_it_has_certificates() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
assert!(
source.inner.resolver.has_certs(),
"a source cannot exist without a valid identity, so the resolver must \
never tell rustls to skip client authentication"
);
assert!(
source.inner.resolver.resolve(&[], &[]).is_some(),
"resolve must return the identity even when the server sends no hints"
);
}
#[test]
fn rotating_type_debug_output_does_not_expose_key_material() {
let dir = tempfile::tempdir().expect("temp dir");
let cert = generate_cert("client");
let config = write_identity(dir.path(), &cert);
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let key_body = pem_body(&cert.key_pem);
assert!(
!key_body.is_empty(),
"the test needs a non-empty key body to search for"
);
for rendered in [
format!("{:?}", source.inner.resolver),
format!("{:?}", source.inner.verifier),
] {
assert!(
!rendered.contains(&key_body),
"Debug must never render private key material: {rendered}"
);
}
}
#[test]
fn debug_does_not_expose_key_material() {
let dir = tempfile::tempdir().expect("temp dir");
let cert = generate_cert("client");
let config = write_identity(dir.path(), &cert);
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let rendered = format!("{source:?}");
let key_body = pem_body(&cert.key_pem);
assert!(
!key_body.is_empty(),
"the test needs a non-empty key body to search for"
);
assert!(
!rendered.contains(&key_body),
"Debug must never render private key material"
);
assert!(
rendered.contains("ClientIdentitySource"),
"Debug must still identify the type: {rendered}"
);
assert!(
rendered.contains("cert_path"),
"Debug must report the origin so a misconfiguration is diagnosable: {rendered}"
);
}
#[cfg(feature = "grpc")]
#[test]
fn tonic_client_tls_config_accepts_a_matching_pair() {
let cert = generate_cert("client");
let cert_file = write_temp(&cert.cert_pem);
let key_file = write_temp(&cert.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
tonic_client_tls_config(&config).expect("a matching pair must yield a tonic TLS config");
}
#[cfg(feature = "grpc")]
#[test]
fn tonic_client_tls_config_rejects_unparseable_pem_at_config_time() {
let cert = generate_cert("client");
let cert_file = write_temp("-----BEGIN CERTIFICATE-----\ntruncated");
let key_file = write_temp(&cert.key_pem);
let config = config_for(
cert_file.path().to_path_buf(),
key_file.path().to_path_buf(),
);
tonic_client_tls_config(&config)
.expect_err("unparseable PEM must fail here rather than on the first gRPC call");
}
#[cfg(feature = "grpc")]
#[test]
fn tonic_snapshot_reflects_the_files_not_the_installed_client() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
source
.tonic_client_tls_config_snapshot()
.expect("a snapshot must build from valid files");
std::fs::write(&config.cert_path, "-----BEGIN CERTIFICATE-----\ntruncated")
.expect("corrupt cert");
source
.tonic_client_tls_config_snapshot()
.expect_err("a snapshot reads the files, so it must surface a corrupt rotation");
source
.client()
.get("https://example.invalid/")
.build()
.expect("the installed client must be unaffected by the corrupt files");
}
#[cfg(feature = "grpc")]
#[test]
fn grpc_channel_rejects_a_plaintext_endpoint() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let endpoint = tonic::transport::Endpoint::from_static("http://peer.internal:8443");
let err = source
.grpc_channel(endpoint)
.expect_err("a plaintext endpoint must not silently drop the client identity");
assert!(
err.to_string().contains("https"),
"the error must say the scheme is the problem: {err}"
);
}
#[cfg(feature = "grpc")]
#[tokio::test]
async fn grpc_channel_builds_lazily_from_an_https_endpoint() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let endpoint = tonic::transport::Endpoint::from_static("https://peer.invalid:8443");
source
.grpc_channel(endpoint)
.expect("a lazy channel must build without contacting the peer");
}
#[cfg(feature = "grpc")]
#[test]
fn the_grpc_path_offers_only_http2() {
let dir = tempfile::tempdir().expect("temp dir");
let config = write_identity(dir.path(), &generate_cert("client"));
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let connector = RotatingTlsConnector {
tls: Arc::new(source.tls_config_with_alpn(&GRPC_ALPN)),
};
assert_eq!(
connector.tls.alpn_protocols,
vec![b"h2".to_vec()],
"gRPC is defined over HTTP/2 only; offering http/1.1 would let a peer \
negotiate a protocol on which every RPC then fails"
);
assert!(
!format!("{connector:?}").contains("PRIVATE KEY"),
"the connector's Debug must not reach into key material"
);
}
#[cfg(feature = "grpc")]
#[test]
fn resolve_target_passes_a_plain_hostname_through_unchanged() {
let (host, name) = resolve_target("peer.internal").expect("a DNS name must resolve");
assert_eq!(host, "peer.internal", "the dial address must be unchanged");
assert_eq!(
name,
ServerName::try_from("peer.internal").expect("valid"),
"a hostname must be verified as a DNS name"
);
}
#[cfg(feature = "grpc")]
#[test]
fn resolve_target_accepts_an_ipv4_literal() {
let (host, name) = resolve_target("127.0.0.1").expect("an IPv4 literal must resolve");
assert_eq!(host, "127.0.0.1");
assert!(
matches!(name, ServerName::IpAddress(_)),
"an IP literal must be verified as an IP address, not a DNS name: {name:?}"
);
}
#[cfg(feature = "grpc")]
#[test]
fn resolve_target_strips_the_brackets_from_an_ipv6_literal() {
let (host, name) = resolve_target("[::1]").expect("a bracketed IPv6 literal must resolve");
assert_eq!(
host, "::1",
"the brackets are URI punctuation and must not reach the resolver"
);
assert!(
matches!(name, ServerName::IpAddress(_)),
"an IPv6 literal must be verified as an IP address: {name:?}"
);
}
#[cfg(feature = "grpc")]
#[test]
fn resolve_target_rejects_a_host_that_is_not_a_valid_server_name() {
let err = resolve_target("not a hostname")
.expect_err("an unparseable host must fail rather than reach the network");
assert!(
err.to_string().contains("is not a valid TLS server name"),
"the error must name the problem: {err}"
);
}
#[derive(Debug)]
struct CapturingClientVerifier {
seen: Arc<std::sync::Mutex<Vec<Vec<u8>>>>,
inner: Arc<dyn tokio_rustls::rustls::server::danger::ClientCertVerifier>,
}
impl tokio_rustls::rustls::server::danger::ClientCertVerifier for CapturingClientVerifier {
fn root_hint_subjects(&self) -> &[tokio_rustls::rustls::DistinguishedName] {
&[]
}
fn verify_client_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_now: UnixTime,
) -> std::result::Result<
tokio_rustls::rustls::server::danger::ClientCertVerified,
tokio_rustls::rustls::Error,
> {
self.seen
.lock()
.expect("the capture list is only locked to push")
.push(end_entity.to_vec());
Ok(tokio_rustls::rustls::server::danger::ClientCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
self.inner.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
self.inner.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.inner.supported_verify_schemes()
}
}
struct TestChain {
ca_pem: String,
leaf_pem: String,
leaf_key_pem: String,
}
fn generate_server_chain() -> TestChain {
use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, Issuer, KeyPair,
KeyUsagePurpose,
};
let ca_key = KeyPair::generate().expect("ca key");
let mut ca_params = CertificateParams::new(Vec::new()).expect("ca params");
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let mut ca_dn = DistinguishedName::new();
ca_dn.push(DnType::CommonName, "acton-service test CA");
ca_params.distinguished_name = ca_dn;
let ca_cert = ca_params.self_signed(&ca_key).expect("self-signed ca");
let leaf_key = KeyPair::generate().expect("leaf key");
let mut leaf_params =
CertificateParams::new(vec!["127.0.0.1".to_string()]).expect("leaf params");
let mut leaf_dn = DistinguishedName::new();
leaf_dn.push(DnType::CommonName, "acton-service test server");
leaf_params.distinguished_name = leaf_dn;
let issuer = Issuer::new(ca_params, ca_key);
let leaf_cert = leaf_params
.signed_by(&leaf_key, &issuer)
.expect("leaf signed by ca");
TestChain {
ca_pem: ca_cert.pem(),
leaf_pem: leaf_cert.pem(),
leaf_key_pem: leaf_key.serialize_pem(),
}
}
#[tokio::test]
async fn a_live_handshake_presents_the_rotated_certificate_on_the_same_client() {
use rustls_pki_types::pem::PemObject;
crate::crypto::ensure_default_crypto_provider();
let chain = generate_server_chain();
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let mut inner_roots = RootCertStore::empty();
for cert in CertificateDer::pem_slice_iter(chain.ca_pem.as_bytes()) {
inner_roots
.add(cert.expect("ca parses"))
.expect("ca is a usable anchor");
}
let inner_verifier =
tokio_rustls::rustls::server::WebPkiClientVerifier::builder(Arc::new(inner_roots))
.build()
.expect("inner verifier builds");
let server_certs: Vec<CertificateDer<'static>> =
CertificateDer::pem_slice_iter(chain.leaf_pem.as_bytes())
.collect::<std::result::Result<Vec<_>, _>>()
.expect("server chain parses");
let server_key = PrivateKeyDer::from_pem_slice(chain.leaf_key_pem.as_bytes())
.expect("server key parses");
let mut server_config = tokio_rustls::rustls::ServerConfig::builder()
.with_client_cert_verifier(Arc::new(CapturingClientVerifier {
seen: Arc::clone(&seen),
inner: inner_verifier,
}))
.with_single_cert(server_certs, server_key)
.expect("server config");
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port");
let port = listener.local_addr().expect("local addr").port();
let server = tokio::spawn(async move {
loop {
let Ok((tcp, _)) = listener.accept().await else {
return;
};
let acceptor = acceptor.clone();
tokio::spawn(async move {
let _ = acceptor.accept(tcp).await;
});
}
});
let dir = tempfile::tempdir().expect("temp dir");
let first = generate_cert("client-one");
let mut config = write_identity(dir.path(), &first);
let ca_path = dir.path().join("peer-ca.pem");
std::fs::write(&ca_path, &chain.ca_pem).expect("write ca");
config.root_ca_path = Some(ca_path);
config.exclusive_roots = true;
let source = ClientIdentitySource::from_config(&config).expect("initial load");
let client = source.client();
let handshake = |tls: ClientConfig| async move {
let tcp = tokio::net::TcpStream::connect(("127.0.0.1", port))
.await
.expect("connect to the test server");
tokio_rustls::TlsConnector::from(Arc::new(tls))
.connect(
ServerName::try_from("127.0.0.1").expect("valid server name"),
tcp,
)
.await
.expect("the handshake must succeed against the test CA")
};
handshake(source.tls_config_with_alpn(&HTTP_ALPN)).await;
let second = generate_cert("client-two");
std::fs::write(&config.cert_path, &second.cert_pem).expect("rewrite cert");
std::fs::write(&config.key_path, &second.key_pem).expect("rewrite key");
source.reload().expect("a valid rotation must reload");
handshake(source.tls_config_with_alpn(&HTTP_ALPN)).await;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while seen.lock().expect("capture list").len() < 2 {
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for two handshakes to be recorded"
);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
server.abort();
let presented = seen.lock().expect("capture list").clone();
let expected_first = CertificateDer::from_pem_slice(first.cert_pem.as_bytes())
.expect("first cert parses")
.to_vec();
let expected_second = CertificateDer::from_pem_slice(second.cert_pem.as_bytes())
.expect("second cert parses")
.to_vec();
assert_eq!(
presented[0], expected_first,
"the first handshake must present the originally configured certificate"
);
assert_eq!(
presented[1], expected_second,
"the second handshake must present the ROTATED certificate: rustls asked \
the resolver again, and the resolver had been swapped in place"
);
assert!(
Arc::ptr_eq(&client, &source.client()),
"and none of this rebuilt the HTTP client that shares the very same \
rotating configuration"
);
}
}