Skip to main content

saluki_tls/
lib.rs

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