use std::time::Duration;
use dynamic_config::Error;
use dynamic_config_store_core::tls::TlsConfig;
use ureq::tls::{Certificate, ClientCert, PemItem, PrivateKey, RootCerts};
pub(crate) fn agent(
tls: &TlsConfig,
timeout: Duration,
described: &str,
) -> Result<ureq::Agent, Error> {
let mut config = ureq::tls::TlsConfig::builder();
if let Some(pem) = tls.ca_certificate_pem(described)? {
let roots = certificates(&pem, described, "the CA certificate")?;
config = config.root_certs(RootCerts::new_with_certs(&roots));
}
if let Some((certificate, key)) = tls.client_certificate_pem(described)? {
let chain = certificates(&certificate, described, "the client certificate")?;
let key = PrivateKey::from_pem(&key)
.map_err(|_| malformed(described, "the client private key"))?;
config = config.client_cert(Some(ClientCert::new_with_certs(&chain, key)));
}
Ok(ureq::Agent::config_builder()
.timeout_global(Some(timeout))
.tls_config(config.build())
.build()
.new_agent())
}
fn certificates(
pem: &[u8],
described: &str,
what: &str,
) -> Result<Vec<Certificate<'static>>, Error> {
let mut certificates = Vec::new();
for item in ureq::tls::parse_pem(pem) {
match item.map_err(|_| malformed(described, what))? {
PemItem::Certificate(certificate) => certificates.push(certificate),
_ => continue,
}
}
if certificates.is_empty() {
return Err(malformed(described, what));
}
Ok(certificates)
}
fn malformed(described: &str, what: &str) -> Error {
Error::remote(format!(
"{described}: {what} is not PEM-encoded material of the kind expected"
))
}
#[cfg(test)]
mod tests {
use super::*;
fn material() -> (String, String, String) {
use rcgen::{BasicConstraints, CertificateParams, IsCa, KeyPair};
let ca_key = KeyPair::generate().unwrap();
let mut ca_params = CertificateParams::new(Vec::new()).unwrap();
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
let ca = ca_params.self_signed(&ca_key).unwrap();
let issuer = rcgen::Issuer::from_params(&ca_params, &ca_key);
let client_key = KeyPair::generate().unwrap();
let client = CertificateParams::new(vec!["myapp".to_owned()])
.unwrap()
.signed_by(&client_key, &issuer)
.unwrap();
(ca.pem(), client.pem(), client_key.serialize_pem())
}
#[test]
fn the_shared_vocabulary_reaches_ureqs_own_configuration() {
let (ca, certificate, key) = material();
let agent = agent(
&TlsConfig::new()
.with_ca_certificate_pem(ca)
.with_client_certificate_pem(certificate, key),
Duration::from_secs(3),
"store the-key",
)
.expect("the generated material is valid PEM");
let tls = agent.config().tls_config();
match tls.root_certs() {
RootCerts::Specific(certificates) => assert_eq!(
certificates.len(),
1,
"the named authority replaces the trust store rather than \
joining it"
),
other => panic!("the CA did not reach the agent: {other:?}"),
}
assert_eq!(
tls.client_cert()
.expect("the client certificate reached the agent")
.certs()
.len(),
1
);
assert!(
!tls.disable_verification(),
"there is no spelling for this, and nothing may turn it on by \
accident"
);
}
#[test]
fn every_certificate_in_a_bundle_is_trusted_rather_than_only_the_first() {
let (ca, certificate, _) = material();
let agent = agent(
&TlsConfig::new().with_ca_certificate_pem(format!("{ca}{certificate}")),
Duration::from_secs(3),
"store the-key",
)
.expect("a bundle is valid PEM");
match agent.config().tls_config().root_certs() {
RootCerts::Specific(certificates) => assert_eq!(certificates.len(), 2),
other => panic!("the bundle did not reach the agent: {other:?}"),
}
}
#[test]
fn a_malformed_private_key_never_quotes_itself_into_the_error() {
const PLANTED: &str = "PLANTED-PRIVATE-KEY-MATERIAL";
let (ca, certificate, _) = material();
let error = agent(
&TlsConfig::new()
.with_ca_certificate_pem(ca)
.with_client_certificate_pem(
certificate,
format!("-----BEGIN PRIVATE KEY-----\n{PLANTED}\n-----END PRIVATE KEY-----\n"),
),
Duration::from_secs(3),
"store the-key",
)
.expect_err("the key is not a key");
let printed = error.to_string();
assert!(!printed.contains(PLANTED), "{printed}");
assert!(printed.contains("the client private key"), "{printed}");
}
#[test]
fn material_that_holds_no_certificate_is_refused_rather_than_ignored() {
let error = agent(
&TlsConfig::new().with_ca_certificate_pem("not a certificate at all"),
Duration::from_secs(3),
"store the-key",
)
.expect_err("there is no certificate in there");
assert!(error.to_string().contains("the CA certificate"), "{error}");
}
#[test]
fn a_missing_file_names_the_path_and_the_material() {
let error = agent(
&TlsConfig::new().with_ca_certificate_file("/nonexistent/private-ca.pem"),
Duration::from_secs(3),
"store the-key",
)
.expect_err("the file is not there");
assert!(
error.to_string().contains("/nonexistent/private-ca.pem"),
"{error}"
);
}
}