1use std::collections::HashMap;
17use std::hash::{Hash, Hasher};
18use std::sync::{Arc, OnceLock, RwLock};
19
20use rustls::pki_types::pem::PemObject;
21use rustls::pki_types::{CertificateDer, PrivateKeyDer};
22
23#[derive(Debug)]
24pub struct UpstreamTls {
25 client: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
26 ca: Option<Vec<CertificateDer<'static>>>,
27 pub verify: bool,
30 key: u64,
31}
32
33fn registry() -> &'static RwLock<HashMap<u64, Arc<UpstreamTls>>> {
36 static REGISTRY: OnceLock<RwLock<HashMap<u64, Arc<UpstreamTls>>>> = OnceLock::new();
37 REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
38}
39
40impl UpstreamTls {
41 pub fn load(
47 client_paths: Option<(&str, &str)>,
48 ca_path: Option<&str>,
49 verify: bool,
50 ) -> Result<Arc<Self>, String> {
51 let mut hasher = std::collections::hash_map::DefaultHasher::new();
52 verify.hash(&mut hasher);
53
54 let client = match client_paths {
55 Some((cert_path, key_path)) => {
56 let cert_pem = std::fs::read(cert_path)
57 .map_err(|e| format!("client_cert_path '{}': {}", cert_path, e))?;
58 let key_pem = std::fs::read(key_path)
59 .map_err(|e| format!("client_key_path '{}': {}", key_path, e))?;
60 cert_pem.hash(&mut hasher);
61 key_pem.hash(&mut hasher);
62 let certs = CertificateDer::pem_slice_iter(&cert_pem)
63 .collect::<Result<Vec<_>, _>>()
64 .map_err(|e| format!("client_cert_path '{}': {}", cert_path, e))?;
65 if certs.is_empty() {
66 return Err(format!(
67 "client_cert_path '{}': no certificates found",
68 cert_path
69 ));
70 }
71 let key = match PrivateKeyDer::pem_slice_iter(&key_pem).next() {
72 Some(Ok(key)) => key,
73 Some(Err(e)) => return Err(format!("client_key_path '{}': {}", key_path, e)),
74 None => {
75 return Err(format!(
76 "client_key_path '{}': no private key found",
77 key_path
78 ))
79 }
80 };
81 Some((certs, key))
82 }
83 None => None,
84 };
85
86 let ca = match ca_path {
87 Some(path) => {
88 let pem =
89 std::fs::read(path).map_err(|e| format!("ca_cert_path '{}': {}", path, e))?;
90 pem.hash(&mut hasher);
91 let certs = CertificateDer::pem_slice_iter(&pem)
92 .collect::<Result<Vec<_>, _>>()
93 .map_err(|e| format!("ca_cert_path '{}': {}", path, e))?;
94 if certs.is_empty() {
95 return Err(format!("ca_cert_path '{}': no certificates found", path));
96 }
97 Some(certs)
98 }
99 None => None,
100 };
101
102 let identity = Arc::new(Self {
103 client,
104 ca,
105 verify,
106 key: hasher.finish(),
107 });
108 identity.client_config()?; Ok(identity)
110 }
111
112 pub fn cache_key(&self) -> u64 {
115 self.key
116 }
117
118 pub fn client_config(&self) -> Result<rustls::ClientConfig, String> {
121 crate::server::tls::install_crypto_provider();
122
123 let builder = if self.verify {
124 let mut roots = rustls::RootCertStore::empty();
125 match &self.ca {
126 Some(certs) => {
128 let (added, _ignored) = roots.add_parsable_certificates(certs.iter().cloned());
129 if added == 0 {
130 return Err("ca_cert_path: no usable certificates".to_string());
131 }
132 }
133 None => {
134 let loaded = rustls_native_certs::load_native_certs();
135 let (added, _ignored) = roots.add_parsable_certificates(loaded.certs);
136 if added == 0 {
137 return Err(format!(
138 "no usable native root certificates ({} load error(s))",
139 loaded.errors.len()
140 ));
141 }
142 }
143 }
144 rustls::ClientConfig::builder().with_root_certificates(roots)
145 } else {
146 rustls::ClientConfig::builder()
147 .dangerous()
148 .with_custom_certificate_verifier(Arc::new(super::NoVerification(
149 rustls::crypto::ring::default_provider(),
150 )))
151 };
152
153 match &self.client {
154 Some((certs, key)) => builder
155 .with_client_auth_cert(certs.clone(), key.clone_key())
156 .map_err(|e| format!("client cert/key rejected: {}", e)),
157 None => Ok(builder.with_no_client_auth()),
158 }
159 }
160
161 pub fn register(this: &Arc<Self>) {
163 registry()
164 .write()
165 .unwrap()
166 .entry(this.key)
167 .or_insert_with(|| this.clone());
168 }
169
170 pub fn lookup(key: u64) -> Option<Arc<UpstreamTls>> {
172 registry().read().unwrap().get(&key).cloned()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn write_identity(tag: &str) -> (String, String, String) {
183 let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
184 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
185 let ca_key = rcgen::KeyPair::generate().unwrap();
186 let ca_cert = ca_params.self_signed(&ca_key).unwrap();
187 let ca_issuer = rcgen::Issuer::from_ca_cert_der(ca_cert.der(), &ca_key).unwrap();
188
189 let leaf_params = rcgen::CertificateParams::new(vec!["client".to_string()]).unwrap();
190 let leaf_key = rcgen::KeyPair::generate().unwrap();
191 let leaf_cert = leaf_params.signed_by(&leaf_key, &ca_issuer).unwrap();
192
193 let dir = std::env::temp_dir();
194 let pid = std::process::id();
195 let cert = dir.join(format!("featherbit_utls_{}_{}.crt", tag, pid));
196 let key = dir.join(format!("featherbit_utls_{}_{}.key", tag, pid));
197 let ca = dir.join(format!("featherbit_utls_{}_{}.ca.crt", tag, pid));
198 std::fs::write(&cert, leaf_cert.pem()).unwrap();
199 std::fs::write(&key, leaf_key.serialize_pem()).unwrap();
200 std::fs::write(&ca, ca_cert.pem()).unwrap();
201 (
202 cert.to_str().unwrap().to_string(),
203 key.to_str().unwrap().to_string(),
204 ca.to_str().unwrap().to_string(),
205 )
206 }
207
208 #[test]
209 fn test_upstream_tls_load_client_and_ca() {
210 let (cert, key, ca) = write_identity("happy");
211 let id = UpstreamTls::load(Some((&cert, &key)), Some(&ca), true).unwrap();
212 assert!(id.verify);
213 assert!(id.client_config().is_ok());
215 }
216
217 #[test]
218 fn test_upstream_tls_load_ca_only() {
219 let (_, _, ca) = write_identity("caonly");
220 assert!(UpstreamTls::load(None, Some(&ca), true).is_ok());
221 }
222
223 #[test]
224 fn test_upstream_tls_load_missing_file_errors() {
225 let err = UpstreamTls::load(Some(("/nonexistent.crt", "/nonexistent.key")), None, true)
226 .unwrap_err();
227 assert!(err.contains("/nonexistent.crt"), "err was: {}", err);
228 }
229
230 #[test]
231 fn test_upstream_tls_load_garbage_key_errors() {
232 let (cert, key, _) = write_identity("garbage");
233 std::fs::write(&key, "not a pem").unwrap();
234 assert!(UpstreamTls::load(Some((&cert, &key)), None, true).is_err());
235 }
236
237 #[test]
238 fn test_upstream_tls_load_mismatched_key_errors() {
239 let (cert, _, _) = write_identity("mismatch_a");
242 let (_, other_key, _) = write_identity("mismatch_b");
243 assert!(UpstreamTls::load(Some((&cert, &other_key)), None, true).is_err());
244 }
245
246 #[test]
247 fn test_upstream_tls_cache_key_content_hash() {
248 let (cert, key, ca) = write_identity("hash");
249 let a = UpstreamTls::load(Some((&cert, &key)), Some(&ca), true).unwrap();
250 let b = UpstreamTls::load(Some((&cert, &key)), Some(&ca), true).unwrap();
251 assert_eq!(a.cache_key(), b.cache_key());
254 let c = UpstreamTls::load(Some((&cert, &key)), Some(&ca), false).unwrap();
255 assert_ne!(a.cache_key(), c.cache_key());
256 let (cert2, key2, _) = write_identity("hash2");
257 let d = UpstreamTls::load(Some((&cert2, &key2)), None, true).unwrap();
258 assert_ne!(a.cache_key(), d.cache_key());
259 }
260
261 #[test]
262 fn test_upstream_tls_registry_roundtrip() {
263 let (cert, key, _) = write_identity("registry");
264 let id = UpstreamTls::load(Some((&cert, &key)), None, true).unwrap();
265 UpstreamTls::register(&id);
266 let found = UpstreamTls::lookup(id.cache_key()).expect("registered identity");
267 assert_eq!(found.cache_key(), id.cache_key());
268 assert!(UpstreamTls::lookup(id.cache_key().wrapping_add(1)).is_none());
269 }
270}