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}