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}