spin_factor_outbound_networking/
tls.rs1use std::{
2 collections::HashMap,
3 ops::Deref,
4 sync::{Arc, LazyLock},
5};
6
7use anyhow::{Context, ensure};
8
9use crate::runtime_config::{ClientCertRuntimeConfig, ClientTlsRuntimeConfig};
10
11#[derive(Default)]
13pub struct TlsClientConfigs {
14 component_host_tls_client_configs: HashMap<String, HostTlsClientConfigs>,
16 default_tls_client_config: TlsClientConfig,
18}
19
20impl TlsClientConfigs {
21 pub(crate) fn new(
22 client_tls_configs: impl IntoIterator<Item = ClientTlsRuntimeConfig>,
23 ) -> anyhow::Result<Self> {
24 let mut component_host_tls_client_configs = HashMap::<String, HostTlsClientConfigs>::new();
26 for ClientTlsRuntimeConfig {
27 components,
28 hosts,
29 root_certificates,
30 use_platform_roots,
31 use_webpki_roots,
32 client_cert,
33 } in client_tls_configs
34 {
35 ensure!(
36 !components.is_empty(),
37 "client TLS 'components' list may not be empty"
38 );
39 ensure!(
40 !hosts.is_empty(),
41 "client TLS 'hosts' list may not be empty"
42 );
43 let tls_client_config = TlsClientConfig::new(
44 use_platform_roots,
45 use_webpki_roots,
46 root_certificates,
47 client_cert,
48 )
49 .context("error building TLS client config")?;
50 for component in components {
51 let host_configs = component_host_tls_client_configs
52 .entry(component.clone())
53 .or_default();
54 for host in &hosts {
55 validate_host(host)?;
56 Arc::get_mut(host_configs)
58 .unwrap()
59 .entry(host.clone())
60 .or_insert_with(|| tls_client_config.clone());
61 }
62 }
63 }
64
65 Ok(Self {
66 component_host_tls_client_configs,
67 ..Default::default()
68 })
69 }
70
71 pub fn get_component_tls_configs(&self, component_id: &str) -> ComponentTlsClientConfigs {
73 let host_client_configs = self
74 .component_host_tls_client_configs
75 .get(component_id)
76 .cloned();
77 ComponentTlsClientConfigs {
78 host_client_configs,
79 default_client_config: self.default_tls_client_config.clone(),
80 }
81 }
82}
83
84type HostTlsClientConfigs = Arc<HashMap<String, TlsClientConfig>>;
86
87#[derive(Clone)]
89pub struct ComponentTlsClientConfigs {
90 pub(crate) host_client_configs: Option<HostTlsClientConfigs>,
91 pub(crate) default_client_config: TlsClientConfig,
92}
93
94impl ComponentTlsClientConfigs {
95 pub fn get_client_config(&self, host: &str) -> &TlsClientConfig {
97 self.host_client_configs
98 .as_ref()
99 .and_then(|configs| configs.get(host))
100 .unwrap_or(&self.default_client_config)
101 }
102}
103
104#[derive(Clone)]
106pub struct TlsClientConfig(Arc<rustls::ClientConfig>);
107
108impl TlsClientConfig {
109 fn new(
110 use_platform_roots: bool,
111 use_webpki_roots: bool,
112 root_certificates: Vec<rustls_pki_types::CertificateDer<'static>>,
113 client_cert: Option<ClientCertRuntimeConfig>,
114 ) -> anyhow::Result<Self> {
115 anyhow::ensure!(
116 use_platform_roots || use_webpki_roots || !root_certificates.is_empty(),
117 "at least one of 'use_platform_roots', 'use_webpki_roots', or 'root_certificates' must be set"
118 );
119
120 let mut extra_roots: Box<dyn Iterator<Item = _>> = Box::new(root_certificates.into_iter());
121 if use_webpki_roots {
122 extra_roots = Box::new(
123 extra_roots.chain(webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().cloned()),
124 );
125 }
126
127 let builder = rustls::ClientConfig::builder();
128 let builder = if use_platform_roots {
129 let verifier = rustls_platform_verifier::Verifier::new_with_extra_roots(
130 extra_roots,
131 builder.crypto_provider().clone(),
132 )
133 .context("failed to initialize platform certificate verifier")?;
134 builder
135 .dangerous()
136 .with_custom_certificate_verifier(Arc::new(verifier))
137 } else {
138 let mut root_store = rustls::RootCertStore::empty();
139 extra_roots.try_for_each(|cert| root_store.add(cert))?;
140 builder.with_root_certificates(root_store)
141 };
142
143 let client_config = if let Some(ClientCertRuntimeConfig {
144 cert_chain,
145 key_der,
146 }) = client_cert
147 {
148 builder.with_client_auth_cert(cert_chain, key_der)?
149 } else {
150 builder.with_no_client_auth()
151 };
152 Ok(Self(client_config.into()))
153 }
154
155 pub fn inner(&self) -> Arc<rustls::ClientConfig> {
157 self.0.clone()
158 }
159}
160
161impl Deref for TlsClientConfig {
162 type Target = rustls::ClientConfig;
163
164 fn deref(&self) -> &Self::Target {
165 &self.0
166 }
167}
168
169impl Default for TlsClientConfig {
170 fn default() -> Self {
171 static ONCE: LazyLock<TlsClientConfig> = LazyLock::new(|| {
172 TlsClientConfig::new(true, true, vec![], None)
173 .expect("default client config should be valid")
174 });
175 ONCE.clone()
176 }
177}
178
179pub(crate) fn validate_host(host: &str) -> anyhow::Result<()> {
181 let authority: http::uri::Authority = host
182 .parse()
183 .with_context(|| format!("invalid TLS 'host' {host:?}"))?;
184 ensure!(
185 authority.port().is_none(),
186 "invalid TLS 'host' {host:?}; ports not currently supported"
187 );
188 Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193 use std::path::Path;
194
195 use anyhow::Context;
196 use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
197
198 use super::*;
199
200 #[test]
201 fn test_empty_config() -> anyhow::Result<()> {
202 let configs = TlsClientConfigs::default();
204 configs.get_tls_client_config("foo", "bar");
205 Ok(())
206 }
207
208 #[test]
209 fn test_minimal_config() -> anyhow::Result<()> {
210 let configs = TlsClientConfigs::new([ClientTlsRuntimeConfig {
211 components: vec!["test-component".into()],
212 hosts: vec!["test-host".into()],
213 root_certificates: vec![],
214 use_platform_roots: false,
215 use_webpki_roots: true,
216 client_cert: None,
217 }])?;
218 let config = configs.get_tls_client_config("test-component", "test-host");
219 let default_config = configs.get_tls_client_config("other_component", "test-host");
221 assert!(!Arc::ptr_eq(&config.0, &default_config.0));
222 Ok(())
223 }
224
225 #[test]
226 fn test_maximal_config() -> anyhow::Result<()> {
227 let test_certs = test_certs()?;
228 let test_key = test_key()?;
229 let configs = TlsClientConfigs::new([ClientTlsRuntimeConfig {
230 components: vec!["test-component".into()],
231 hosts: vec!["test-host".into()],
232 root_certificates: vec![test_certs[0].clone()],
233 use_platform_roots: false,
234 use_webpki_roots: false,
235 client_cert: Some(ClientCertRuntimeConfig {
236 cert_chain: test_certs,
237 key_der: test_key,
238 }),
239 }])?;
240 let config = configs.get_tls_client_config("test-component", "test-host");
241 assert!(config.client_auth_cert_resolver.has_certs());
242 Ok(())
243 }
244
245 #[test]
246 fn test_config_overrides() -> anyhow::Result<()> {
247 let test_certs = test_certs()?;
248 let test_key = test_key()?;
249 let configs = TlsClientConfigs::new([
250 ClientTlsRuntimeConfig {
251 components: vec!["test-component1".into()],
252 hosts: vec!["test-host".into()],
253 client_cert: Some(ClientCertRuntimeConfig {
254 cert_chain: test_certs,
255 key_der: test_key,
256 }),
257 ..Default::default()
258 },
259 ClientTlsRuntimeConfig {
260 components: vec!["test-component1".into(), "test-component2".into()],
261 hosts: vec!["test-host".into()],
262 ..Default::default()
263 },
264 ])?;
265 let config1 = configs.get_tls_client_config("test-component1", "test-host");
267 assert!(config1.client_auth_cert_resolver.has_certs());
268
269 let config2 = configs.get_tls_client_config("test-component-2", "test-host");
271 assert!(!config2.client_auth_cert_resolver.has_certs());
272 Ok(())
273 }
274
275 const TESTDATA_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata");
276
277 fn test_certs() -> anyhow::Result<Vec<CertificateDer<'static>>> {
278 CertificateDer::pem_file_iter(Path::new(TESTDATA_DIR).join("valid-cert.pem"))?
279 .collect::<Result<Vec<_>, _>>()
280 .context("certs")
281 }
282
283 fn test_key() -> anyhow::Result<PrivateKeyDer<'static>> {
284 PrivateKeyDer::from_pem_file(Path::new(TESTDATA_DIR).join("valid-private-key.pem"))
285 .context("key")
286 }
287
288 impl TlsClientConfigs {
289 fn get_tls_client_config(&self, component_id: &str, host: &str) -> TlsClientConfig {
290 let component_config = self.get_component_tls_configs(component_id);
291 component_config.get_client_config(host).clone()
292 }
293 }
294}