krabka_security/
jwks_trust.rs1use std::{
10 io,
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14
15use rustls::pki_types::{CertificateDer, pem::PemObject};
16use thiserror::Error;
17
18#[derive(Debug, Error)]
19pub enum JwksTrustError {
20 #[error("read {0}: {1}")]
21 Io(PathBuf, io::Error),
22 #[error("no certificates in {0}")]
23 Empty(PathBuf),
24 #[error("rustls add cert: {0}")]
25 Rustls(#[from] rustls::Error),
26}
27
28#[tracing::instrument(level = "info", skip_all, err)]
37pub fn build_client_config_from_pem(
40 path: &Path,
41) -> Result<Arc<rustls::ClientConfig>, JwksTrustError> {
42 let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(path)
43 .map_err(|e| JwksTrustError::Io(path.into(), io::Error::other(e.to_string())))?
44 .collect::<Result<Vec<_>, _>>()
45 .map_err(|e| JwksTrustError::Io(path.into(), io::Error::other(e.to_string())))?;
46 if certs.is_empty() {
47 return Err(JwksTrustError::Empty(path.into()));
48 }
49 let mut roots = rustls::RootCertStore::empty();
50 for cert in certs {
51 roots.add(cert)?;
52 }
53 Ok(Arc::new(
54 rustls::ClientConfig::builder()
55 .with_root_certificates(roots)
56 .with_no_client_auth(),
57 ))
58}
59
60#[cfg(test)]
61mod tests {
62 use std::{fs::File, io::Write};
63
64 use rustls::crypto::ring::default_provider;
65
66 use super::*;
67
68 fn install_provider() {
69 let _ = default_provider().install_default();
72 }
73
74 fn dev_cert_pem() -> &'static str {
75 include_str!("../tests/fixtures/dev_cert.pem")
76 }
77
78 #[test]
79 fn build_client_config_from_pem_loads_single_cert() {
80 install_provider();
81 let dir = tempfile::tempdir().unwrap();
82 let path = dir.path().join("ca.pem");
83 File::create(&path)
84 .unwrap()
85 .write_all(dev_cert_pem().as_bytes())
86 .unwrap();
87 let cfg = build_client_config_from_pem(&path).expect("single cert PEM should load");
88 let _: Arc<rustls::ClientConfig> = cfg;
92 }
93
94 #[test]
95 fn build_client_config_from_pem_loads_concatenated_chain() {
96 install_provider();
97 let dir = tempfile::tempdir().unwrap();
98 let path = dir.path().join("ca-chain.pem");
99 let mut f = File::create(&path).unwrap();
100 f.write_all(dev_cert_pem().as_bytes()).unwrap();
101 f.write_all(b"\n").unwrap();
102 f.write_all(dev_cert_pem().as_bytes()).unwrap();
103 let cfg = build_client_config_from_pem(&path);
104 assert2::assert!(cfg.is_ok());
105 }
106
107 #[test]
108 fn build_client_config_from_pem_rejects_missing_file() {
109 install_provider();
110 let err = build_client_config_from_pem(Path::new("/nonexistent/path.pem")).unwrap_err();
111 assert2::assert!(matches!(err, JwksTrustError::Io(_, _)));
112 }
113
114 #[test]
115 fn build_client_config_from_pem_rejects_empty_pem() {
116 install_provider();
117 let dir = tempfile::tempdir().unwrap();
118 let path = dir.path().join("empty.pem");
119 File::create(&path)
120 .unwrap()
121 .write_all(b"# no certs in this file\n")
122 .unwrap();
123 let err = build_client_config_from_pem(&path).unwrap_err();
124 assert2::assert!(matches!(
129 err,
130 JwksTrustError::Empty(_) | JwksTrustError::Io(_, _)
131 ));
132 }
133
134 #[test]
135 fn build_client_config_from_pem_rejects_non_pem_garbage() {
136 install_provider();
137 let dir = tempfile::tempdir().unwrap();
138 let path = dir.path().join("garbage.bin");
139 File::create(&path).unwrap().write_all(&[0u8; 64]).unwrap();
140 let err = build_client_config_from_pem(&path);
141 assert2::assert!(err.is_err());
142 }
143}