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    version::{TLS12, TLS13},
13    ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme, SupportedProtocolVersion,
14};
15use saluki_error::{generic_error, GenericError};
16use tracing::debug;
17
18/// Tracks if the default cryptography provider for `rustls` has been set.
19static DEFAULT_CRYPTO_PROVIDER_SET: OnceLock<()> = OnceLock::new();
20
21/// Default root certificate store to use for TLS when one isn't explicitly provided.
22static DEFAULT_ROOT_CERT_STORE_MUTEX: Mutex<()> = Mutex::new(());
23static DEFAULT_ROOT_CERT_STORE: OnceLock<Arc<RootCertStore>> = OnceLock::new();
24
25// Various defaults for TLS configuration.
26const DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS: usize = 8;
27const TLS12_PLUS_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13, &TLS12];
28const TLS13_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13];
29
30/// Minimum TLS protocol version to use for client connections.
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32pub enum TlsMinimumVersion {
33    /// TLS 1.2 or newer.
34    #[default]
35    Tls12,
36
37    /// TLS 1.3 or newer.
38    Tls13,
39}
40
41impl TlsMinimumVersion {
42    const fn protocol_versions(self) -> &'static [&'static SupportedProtocolVersion] {
43        match self {
44            Self::Tls12 => TLS12_PLUS_PROTOCOL_VERSIONS,
45            Self::Tls13 => TLS13_PROTOCOL_VERSIONS,
46        }
47    }
48}
49
50/// A certificate verifier that accepts all server certificates without validation.
51///
52/// This is inherently insecure and should only be used for local/development connections where the
53/// server's identity is already established through other means (for example, connecting via Unix domain socket
54/// to a local process).
55#[derive(Debug)]
56struct AcceptAllServerCertVerifier {
57    provider: Arc<CryptoProvider>,
58}
59
60impl ServerCertVerifier for AcceptAllServerCertVerifier {
61    fn verify_server_cert(
62        &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>,
63        _ocsp_response: &[u8], _now: UnixTime,
64    ) -> Result<ServerCertVerified, rustls::Error> {
65        Ok(ServerCertVerified::assertion())
66    }
67
68    fn verify_tls12_signature(
69        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
70    ) -> Result<HandshakeSignatureValid, rustls::Error> {
71        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
72    }
73
74    fn verify_tls13_signature(
75        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
76    ) -> Result<HandshakeSignatureValid, rustls::Error> {
77        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
78    }
79
80    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
81        self.provider.signature_verification_algorithms.supported_schemes()
82    }
83}
84
85/// A TLS client configuration builder.
86///
87/// Exposes various options for configuring a client's TLS configuration that would otherwise be cumbersome to
88/// configure, and provides sane defaults for many common options.
89///
90/// # Missing
91///
92/// - ability to configure client authentication
93pub struct ClientTLSConfigBuilder {
94    max_tls12_resumption_sessions: Option<usize>,
95    min_tls_version: TlsMinimumVersion,
96    root_cert_store: Option<RootCertStore>,
97    danger_accept_invalid_certs: bool,
98}
99
100impl ClientTLSConfigBuilder {
101    pub fn new() -> Self {
102        Self {
103            max_tls12_resumption_sessions: None,
104            min_tls_version: TlsMinimumVersion::default(),
105            root_cert_store: None,
106            danger_accept_invalid_certs: false,
107        }
108    }
109
110    /// Sets the maximum number of TLS 1.2 sessions to cache.
111    ///
112    /// Defaults to 8.
113    pub fn with_max_tls12_resumption_sessions(mut self, max: usize) -> Self {
114        self.max_tls12_resumption_sessions = Some(max);
115        self
116    }
117
118    /// Sets the root certificate store to use for the client.
119    ///
120    /// Defaults to the "default" root certificate store initialized from the platform. (See [`load_platform_root_certificates`].)
121    pub fn with_root_cert_store(mut self, store: RootCertStore) -> Self {
122        self.root_cert_store = Some(store);
123        self
124    }
125
126    /// Sets the minimum TLS protocol version to allow for client connections.
127    ///
128    /// Defaults to TLS 1.2.
129    pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
130        self.min_tls_version = version;
131        self
132    }
133
134    /// Disables server certificate verification entirely.
135    ///
136    /// This is inherently insecure and should only be used for local/development connections where
137    /// the server's identity is already established through other means (for example, connecting via Unix
138    /// domain socket to a local process).
139    pub fn danger_accept_invalid_certs(mut self) -> Self {
140        self.danger_accept_invalid_certs = true;
141        self
142    }
143
144    /// Builds the client TLS configuration.
145    ///
146    /// # Errors
147    ///
148    /// If the default root cert store (see [`load_platform_root_certificates`]) hasn't been initialized, and a root
149    /// cert store hasn't been provided, or if the resulting configuration isn't FIPS compliant, an error will be
150    /// returned.
151    pub fn build(self) -> Result<ClientConfig, GenericError> {
152        let max_tls12_resumption_sessions = self
153            .max_tls12_resumption_sessions
154            .unwrap_or(DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS);
155        let protocol_versions = self.min_tls_version.protocol_versions();
156
157        let mut config = if self.danger_accept_invalid_certs {
158            let crypto_provider = CryptoProvider::get_default()
159                .map(Arc::clone)
160                .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
161            let verifier = Arc::new(AcceptAllServerCertVerifier {
162                provider: crypto_provider,
163            });
164
165            ClientConfig::builder_with_protocol_versions(protocol_versions)
166                .dangerous()
167                .with_custom_certificate_verifier(verifier)
168                .with_no_client_auth()
169        } else {
170            let root_cert_store = self.root_cert_store.map(Arc::new).map(Ok).unwrap_or_else(|| {
171                DEFAULT_ROOT_CERT_STORE
172                    .get()
173                    .map(Arc::clone)
174                    .ok_or(generic_error!("Default TLS root certificate store not initialized."))
175            })?;
176
177            ClientConfig::builder_with_protocol_versions(protocol_versions)
178                .with_root_certificates(root_cert_store)
179                .with_no_client_auth()
180        };
181
182        // One unfortunate thing is that by creating `config` above, it assigns the default value for `Resumption` before
183        // we reset it down here... which means the big, beefy default one gets allocated and then immediately thrown
184        // away.
185        config.resumption = Resumption::in_memory_sessions(max_tls12_resumption_sessions);
186
187        // Do our final check that this configuration is FIPS compliant.
188        #[cfg(feature = "fips")]
189        if !config.fips() {
190            return Err(generic_error!("Client TLS configuration is not FIPS compliant."));
191        }
192
193        Ok(config)
194    }
195}
196
197/// Initializes the default TLS cryptography provider used by `rustls`.
198///
199/// This explicitly sets the [AWS-LC][aws_lc] provider as the default provider for all future TLS configurations, which
200/// provides the ability to run in FIPS mode for FIPS-compliant builds.
201///
202/// This is the only supported cryptography provider in Saluki.
203///
204/// # Errors
205///
206/// If the default cryptography provider has already been set, an error will be returned.
207///
208/// [aws_lc]: https://github.com/aws/aws-lc-rs
209pub fn initialize_default_crypto_provider() -> Result<(), GenericError> {
210    if DEFAULT_CRYPTO_PROVIDER_SET.get().is_some() {
211        return Err(generic_error!("Default TLS cryptography provider already initialized."));
212    }
213
214    // Set the process-wide default `CryptoProvider` to AWS-LC.
215    //
216    // This locks in AWS-LC as the default provider for all future TLS configurations, regardless of whether they use
217    // the configuration builders here or not. (The main caveat is that it's only relevant if `rustls` is being used.)
218    rustls::crypto::aws_lc_rs::default_provider()
219        .install_default()
220        .map_err(|_| generic_error!("Failed to install AWS-LC as default cryptography provider. This is likely due to a conflicting provider already being installed."))?;
221
222    // With the process-wide default having been set, mark it as having been set.
223    DEFAULT_CRYPTO_PROVIDER_SET
224        .set(())
225        .expect("should be impossible for DEFAULT_CRYPTO_PROVIDER_SET to be initialized twice");
226
227    Ok(())
228}
229
230/// Initializes the default root certificate store from the platform's native certificate store.
231///
232/// ## Environment Variables
233///
234/// | Environment Variable | Description                                                                           |
235/// |----------------------|---------------------------------------------------------------------------------------|
236/// | SSL_CERT_FILE        | File containing an arbitrary number of certificates in PEM format.                    |
237/// | SSL_CERT_DIR         | Directory utilizing the hierarchy and naming convention used by OpenSSL's `c_rehash`. |
238///
239/// If **either** (or **both**) are set, certificates are only loaded from the locations specified via environment
240/// variables and not the platform- native certificate store.
241///
242/// ## Certificate Validity
243///
244/// All certificates are expected to be in PEM format. A file may contain multiple certificates.
245///
246/// Example:
247///
248/// ```text
249/// -----BEGIN CERTIFICATE-----
250/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
251/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
252/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
253/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
254/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
255/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
256/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
257/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
258/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
259/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
260/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
261/// /q4AaOeMSQ+2b1tbFfLn
262/// -----END CERTIFICATE-----
263/// -----BEGIN CERTIFICATE-----
264/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
265/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
266/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
267/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
268/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
269/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
270/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
271/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
272/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
273/// YyRIHN8wfdVoOw==
274/// -----END CERTIFICATE-----
275///
276/// ```
277///
278/// For reasons of compatibility, an attempt is made to skip invalid sections of a certificate file but this means it's
279/// also possible for a malformed certificate to be skipped.
280///
281/// If a certificate isn't loaded, and no error is reported, check if:
282///
283/// 1. the certificate is in PEM format (see example above)
284/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
285/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
286/// 4. there is a line break after the certificate.
287///
288/// ## Errors
289///
290/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
291/// returned. Missing files or directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and treated as
292/// "no certificates available" rather than as a load failure.
293///
294/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
295pub fn load_platform_root_certificates() -> Result<(), GenericError> {
296    let _guard = DEFAULT_ROOT_CERT_STORE_MUTEX
297        .lock()
298        .map_err(|_| generic_error!("Default TLS root certificate store update lock poisoned."))?;
299    if DEFAULT_ROOT_CERT_STORE.get().is_some() {
300        return Err(generic_error!(
301            "Default TLS root certificate store already initialized."
302        ));
303    }
304
305    let root_cert_store = load_platform_root_certificates_inner()?;
306
307    // The reason it should be impossible is that we intentionally only set it _here_, and we do so after acquiring the
308    // mutex, and only then do we make sure that it hasn't been set before proceeding to try to set it.
309    DEFAULT_ROOT_CERT_STORE
310        .set(Arc::new(root_cert_store))
311        .expect("should be impossible for DEFAULT_ROOT_CERT_STORE to be initialized twice");
312
313    Ok(())
314}
315
316/// Builds a `RootCertStore` from the platform's native certificate store.
317///
318/// Behaves identically to [`load_platform_root_certificates`] with respect to which certificates are loaded, but
319/// returns the constructed store instead of writing it into the process-wide default.
320///
321/// # Errors
322///
323/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
324/// returned. Otherwise, even if some certificates failed to parse, the store is returned with whatever certificates were
325/// successfully added. Missing files or directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and do
326/// not produce an error.
327pub fn load_platform_root_certificates_inner() -> Result<RootCertStore, GenericError> {
328    let mut root_cert_store = RootCertStore::empty();
329
330    let mut result = rustls_native_certs::load_native_certs();
331
332    // Drop "not found" IO errors before evaluating success or failure: a missing `SSL_CERT_FILE` or `SSL_CERT_DIR`
333    // should look like "no certificates available" rather than a load failure, since callers may simply not have set
334    // those env vars on this host.
335    result.errors.retain(|err| {
336        !matches!(
337            &err.kind,
338            rustls_native_certs::ErrorKind::Io { inner, .. } if inner.kind() == std::io::ErrorKind::NotFound,
339        )
340    });
341
342    // For whatever certificates we _did_ get back, try and add them to the root certificate store.
343    let (added, failed) = root_cert_store.add_parsable_certificates(result.certs);
344    if failed == 0 && added > 0 {
345        debug!(
346            "Added {} certificates from environment to the default root certificate store.",
347            added
348        );
349    } else if failed > 0 && added > 0 {
350        debug!("Added {} certificates from environment to the default root certificate store, but failed to add {} certificates.", added, failed);
351    } else {
352        // When we don't manage to add any certificates, it either means that:
353        // - we found no certificates to add
354        // - we hit an error when loading the certificates
355        // - we hit an error when trying to add the certificates to our root certificate store
356        //
357        // We only consider this operation to have truly failed if there were errors during the initial loading of the
358        // certificates.
359        if !result.errors.is_empty() {
360            let joined_errors = result
361                .errors
362                .iter()
363                .map(|e| e.to_string())
364                .collect::<Vec<_>>()
365                .join(", ");
366
367            return Err(generic_error!(
368                "Failed to load certificates from platform's native certificate store: {}",
369                joined_errors
370            ));
371        }
372    }
373
374    Ok(root_cert_store)
375}
376
377#[cfg(test)]
378mod tests {
379    use rustls::ProtocolVersion;
380
381    use super::TlsMinimumVersion;
382
383    #[test]
384    fn tls12_minimum_enables_tls12_and_tls13() {
385        let versions = TlsMinimumVersion::Tls12.protocol_versions();
386
387        assert_eq!(versions.len(), 2);
388        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
389        assert_eq!(versions[1].version, ProtocolVersion::TLSv1_2);
390    }
391
392    #[test]
393    fn tls13_minimum_enables_tls13_only() {
394        let versions = TlsMinimumVersion::Tls13.protocol_versions();
395
396        assert_eq!(versions.len(), 1);
397        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
398    }
399}