use std::path::{Path, PathBuf};
use std::sync::Arc;
use rustls::pki_types::CertificateDer;
use rustls::pki_types::pem::PemObject;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum JwksTrustError {
#[error("read {0}: {1}")]
Io(PathBuf, std::io::Error),
#[error("no certificates in {0}")]
Empty(PathBuf),
#[error("rustls add cert: {0}")]
Rustls(#[from] rustls::Error),
}
pub fn build_client_config_from_pem(
path: &Path,
) -> Result<Arc<rustls::ClientConfig>, JwksTrustError> {
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(path)
.map_err(|e| JwksTrustError::Io(path.into(), std::io::Error::other(e.to_string())))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| JwksTrustError::Io(path.into(), std::io::Error::other(e.to_string())))?;
if certs.is_empty() {
return Err(JwksTrustError::Empty(path.into()));
}
let mut roots = rustls::RootCertStore::empty();
for cert in certs {
roots.add(cert)?;
}
Ok(Arc::new(
rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use assert2::assert;
use std::fs::File;
use std::io::Write;
fn install_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
fn dev_cert_pem() -> &'static str {
include_str!("../tests/fixtures/dev_cert.pem")
}
#[test]
fn build_client_config_from_pem_loads_single_cert() {
install_provider();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ca.pem");
File::create(&path)
.unwrap()
.write_all(dev_cert_pem().as_bytes())
.unwrap();
let cfg = build_client_config_from_pem(&path).expect("single cert PEM should load");
let _: Arc<rustls::ClientConfig> = cfg;
}
#[test]
fn build_client_config_from_pem_loads_concatenated_chain() {
install_provider();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ca-chain.pem");
let mut f = File::create(&path).unwrap();
f.write_all(dev_cert_pem().as_bytes()).unwrap();
f.write_all(b"\n").unwrap();
f.write_all(dev_cert_pem().as_bytes()).unwrap();
let cfg = build_client_config_from_pem(&path);
assert!(cfg.is_ok(), "concatenated chain should load: {cfg:?}");
}
#[test]
fn build_client_config_from_pem_rejects_missing_file() {
install_provider();
let err = build_client_config_from_pem(Path::new("/nonexistent/path.pem")).unwrap_err();
assert!(
matches!(err, JwksTrustError::Io(_, _)),
"expected Io, got {err:?}",
);
}
#[test]
fn build_client_config_from_pem_rejects_empty_pem() {
install_provider();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.pem");
File::create(&path)
.unwrap()
.write_all(b"# no certs in this file\n")
.unwrap();
let err = build_client_config_from_pem(&path).unwrap_err();
assert!(
matches!(err, JwksTrustError::Empty(_) | JwksTrustError::Io(_, _)),
"expected Empty or Io, got {err:?}",
);
}
#[test]
fn build_client_config_from_pem_rejects_non_pem_garbage() {
install_provider();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("garbage.bin");
File::create(&path).unwrap().write_all(&[0u8; 64]).unwrap();
let err = build_client_config_from_pem(&path);
assert!(
err.is_err(),
"arbitrary bytes should not parse as a PEM bundle"
);
}
}