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}