saluki_tls/lib.rs
1//! Transport Layer Security (TLS) configuration and helpers.
2
3use std::sync::{Arc, Mutex, OnceLock};
4
5use rustls::{client::Resumption, ClientConfig, RootCertStore};
6use saluki_error::{generic_error, GenericError};
7use tracing::debug;
8
9/// Tracks if the default cryptography provider for `rustls` has been set.
10static DEFAULT_CRYPTO_PROVIDER_SET: OnceLock<()> = OnceLock::new();
11
12/// Default root certificate store to use for TLS when one isn't explicitly provided.
13static DEFAULT_ROOT_CERT_STORE_MUTEX: Mutex<()> = Mutex::new(());
14static DEFAULT_ROOT_CERT_STORE: OnceLock<Arc<RootCertStore>> = OnceLock::new();
15
16// Various defaults for TLS configuration.
17const DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS: usize = 8;
18
19/// A TLS client configuration builder.
20///
21/// Exposes various options for configuring a client's TLS configuration that would otherwise be cumbersome to
22/// configure, and provides sane defaults for many common options.
23///
24/// # Missing
25///
26/// - ability to configure client authentication
27pub struct ClientTLSConfigBuilder {
28 max_tls12_resumption_sessions: Option<usize>,
29 root_cert_store: Option<RootCertStore>,
30}
31
32impl ClientTLSConfigBuilder {
33 pub fn new() -> Self {
34 Self {
35 max_tls12_resumption_sessions: None,
36 root_cert_store: None,
37 }
38 }
39
40 /// Sets the maximum number of TLS 1.2 sessions to cache.
41 ///
42 /// Defaults to 8.
43 pub fn with_max_tls12_resumption_sessions(mut self, max: usize) -> Self {
44 self.max_tls12_resumption_sessions = Some(max);
45 self
46 }
47
48 /// Sets the root certificate store to use for the client.
49 ///
50 /// Defaults to the "default" root certificate store initialized from the platform. (See [`load_platform_root_certificates`].)
51 pub fn with_root_cert_store(mut self, store: RootCertStore) -> Self {
52 self.root_cert_store = Some(store);
53 self
54 }
55
56 /// Builds the client TLS configuration.
57 ///
58 /// # Errors
59 ///
60 /// If the default root cert store (see [`load_platform_root_certificates`]) has not been initialized, and a root
61 /// cert store has not been provided, or if the resulting configuration is not FIPS compliant, an error will be
62 /// returned.
63 pub fn build(self) -> Result<ClientConfig, GenericError> {
64 let max_tls12_resumption_sessions = self
65 .max_tls12_resumption_sessions
66 .unwrap_or(DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS);
67
68 let root_cert_store = self.root_cert_store.map(Arc::new).map(Ok).unwrap_or_else(|| {
69 DEFAULT_ROOT_CERT_STORE
70 .get()
71 .map(Arc::clone)
72 .ok_or(generic_error!("Default TLS root certificate store not initialized."))
73 })?;
74
75 let mut config = ClientConfig::builder()
76 .with_root_certificates(root_cert_store)
77 .with_no_client_auth();
78
79 // One unfortunate thing is that by creating `config` above, it assigns the default value for `Resumption` before
80 // we reset it down here... which means the big, beefy default one gets allocated and then immediately thrown
81 // away.
82 config.resumption = Resumption::in_memory_sessions(max_tls12_resumption_sessions);
83
84 // Do our final check that this configuration is FIPS compliant.
85 #[cfg(feature = "fips")]
86 if !config.fips() {
87 return Err(generic_error!("Client TLS configuration is not FIPS compliant."));
88 }
89
90 Ok(config)
91 }
92}
93
94/// Initializes the default TLS cryptography provider used by `rustls`.
95///
96/// This explicitly sets the [AWS-LC][aws_lc] provider as the default provider for all future TLS configurations, which
97/// provides the ability to run in FIPS mode for FIPS-compliant builds.
98///
99/// This is the only supported cryptography provider in Saluki.
100///
101/// # Errors
102///
103/// If the default cryptography provider has already been set, an error will be returned.
104///
105/// [aws_lc]: https://github.com/aws/aws-lc-rs
106pub fn initialize_default_crypto_provider() -> Result<(), GenericError> {
107 if DEFAULT_CRYPTO_PROVIDER_SET.get().is_some() {
108 return Err(generic_error!("Default TLS cryptography provider already initialized."));
109 }
110
111 // Set the process-wide default `CryptoProvider` to AWS-LC.
112 //
113 // This locks in AWS-LC as the default provider for all future TLS configurations, regardless of whether they use
114 // the configuration builders here or not. (The main caveat is that it's only relevant if `rustls` is being used.)
115 rustls::crypto::aws_lc_rs::default_provider()
116 .install_default()
117 .map_err(|_| generic_error!("Failed to install AWS-LC as default cryptography provider. This is likely due to a conflicting provider already being installed."))?;
118
119 // With the process-wide default having been set, mark it as having been set.
120 DEFAULT_CRYPTO_PROVIDER_SET
121 .set(())
122 .expect("should be impossible for DEFAULT_CRYPTO_PROVIDER_SET to be initialized twice");
123
124 Ok(())
125}
126
127/// Initializes the default root certificate store from the platform's native certificate store.
128///
129/// ## Environment Variables
130///
131/// | Environment Variable | Description |
132/// |----------------------|---------------------------------------------------------------------------------------|
133/// | SSL_CERT_FILE | File containing an arbitrary number of certificates in PEM format. |
134/// | SSL_CERT_DIR | Directory utilizing the hierarchy and naming convention used by OpenSSL's [c_rehash]. |
135///
136/// If **either** (or **both**) are set, certificates are only loaded from the locations specified via environment
137/// variables and not the platform- native certificate store.
138///
139/// ## Certificate Validity
140///
141/// All certificates are expected to be in PEM format. A file may contain multiple certificates.
142///
143/// Example:
144///
145/// ```text
146/// -----BEGIN CERTIFICATE-----
147/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
148/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
149/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
150/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
151/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
152/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
153/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
154/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
155/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
156/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
157/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
158/// /q4AaOeMSQ+2b1tbFfLn
159/// -----END CERTIFICATE-----
160/// -----BEGIN CERTIFICATE-----
161/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
162/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
163/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
164/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
165/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
166/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
167/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
168/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
169/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
170/// YyRIHN8wfdVoOw==
171/// -----END CERTIFICATE-----
172///
173/// ```
174///
175/// For reasons of compatibility, an attempt is made to skip invalid sections of a certificate file but this means it's
176/// also possible for a malformed certificate to be skipped.
177///
178/// If a certificate isn't loaded, and no error is reported, check if:
179///
180/// 1. the certificate is in PEM format (see example above)
181/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
182/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
183/// 4. there is a line break after the certificate.
184///
185/// ## Errors
186///
187/// If any error occurs during the locating or loading the platform's native certificate store, an error will be returned.
188///
189/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
190pub fn load_platform_root_certificates() -> Result<(), GenericError> {
191 let _guard = DEFAULT_ROOT_CERT_STORE_MUTEX
192 .lock()
193 .map_err(|_| generic_error!("Default TLS root certificate store update lock poisoned."))?;
194 if DEFAULT_ROOT_CERT_STORE.get().is_some() {
195 return Err(generic_error!(
196 "Default TLS root certificate store already initialized."
197 ));
198 }
199
200 let mut root_cert_store = RootCertStore::empty();
201
202 let result = rustls_native_certs::load_native_certs();
203 if !result.errors.is_empty() {
204 let joined_errors = result
205 .errors
206 .iter()
207 .map(|e| e.to_string())
208 .collect::<Vec<_>>()
209 .join(", ");
210
211 return Err(generic_error!(
212 "Failed to load certificates from platform's native certificate store: {}",
213 joined_errors
214 ));
215 }
216
217 let (added, failed) = root_cert_store.add_parsable_certificates(result.certs);
218 if failed == 0 && added > 0 {
219 debug!(
220 "Added {} certificates from environment to the default root certificate store.",
221 added
222 );
223 } else if failed > 0 && added > 0 {
224 debug!("Added {} certificates from environment to the default root certificate store, but failed to add {} certificates.", added, failed);
225 } else {
226 return Err(generic_error!(
227 "Failed to add any certificates from environment to the default root certificate store."
228 ));
229 }
230
231 // The reason it should be impossible is that we intentionally only set it _here_, and we do so after acquiring the
232 // mutex, and only then do we make sure that it hasn't been set before proceeding to try to set it.
233 DEFAULT_ROOT_CERT_STORE
234 .set(Arc::new(root_cert_store))
235 .expect("should be impossible for DEFAULT_ROOT_CERT_STORE to be initialized twice");
236
237 Ok(())
238}