aws_smithy_http_client/client/
tls.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5use crate::cfg::{cfg_rustls, cfg_s2n_tls};
6use crate::HttpClientError;
7
8/// Choice of underlying cryptography library
9#[derive(Debug, Eq, PartialEq, Clone)]
10#[non_exhaustive]
11pub enum Provider {
12    #[cfg(any(
13        feature = "rustls-aws-lc",
14        feature = "rustls-aws-lc-fips",
15        feature = "rustls-ring"
16    ))]
17    /// TLS provider based on [rustls](https://github.com/rustls/rustls)
18    Rustls(rustls_provider::CryptoMode),
19    /// TLS provider based on [s2n-tls](https://github.com/aws/s2n-tls)
20    #[cfg(feature = "s2n-tls")]
21    S2nTls,
22}
23
24/// TLS related configuration object
25#[derive(Debug, Clone)]
26pub struct TlsContext {
27    #[allow(unused)]
28    trust_store: TrustStore,
29}
30
31impl TlsContext {
32    /// Create a new [TlsContext] builder
33    pub fn builder() -> TlsContextBuilder {
34        TlsContextBuilder::new()
35    }
36}
37
38impl Default for TlsContext {
39    fn default() -> Self {
40        TlsContext::builder().build().expect("valid default config")
41    }
42}
43
44/// Builder for TLS related configuration
45#[derive(Debug)]
46pub struct TlsContextBuilder {
47    trust_store: TrustStore,
48}
49
50impl TlsContextBuilder {
51    fn new() -> Self {
52        TlsContextBuilder {
53            trust_store: TrustStore::default(),
54        }
55    }
56
57    /// Configure the trust store to use for the TLS context
58    pub fn with_trust_store(mut self, trust_store: TrustStore) -> Self {
59        self.trust_store = trust_store;
60        self
61    }
62
63    /// Build a new [TlsContext]
64    pub fn build(self) -> Result<TlsContext, HttpClientError> {
65        Ok(TlsContext {
66            trust_store: self.trust_store,
67        })
68    }
69}
70
71/// PEM encoded certificate
72#[allow(unused)]
73#[derive(Debug, Clone)]
74struct CertificatePEM(Vec<u8>);
75
76impl From<&[u8]> for CertificatePEM {
77    fn from(value: &[u8]) -> Self {
78        CertificatePEM(value.to_vec())
79    }
80}
81
82/// Container for root certificates able to provide a root-of-trust for connection authentication
83///
84/// Platform native root certificates are enabled by default. To start with a clean trust
85/// store use [TrustStore::empty]
86#[derive(Debug, Clone)]
87pub struct TrustStore {
88    enable_native_roots: bool,
89    custom_certs: Vec<CertificatePEM>,
90}
91
92impl TrustStore {
93    /// Create a new empty trust store
94    pub fn empty() -> Self {
95        Self {
96            enable_native_roots: false,
97            custom_certs: Vec::new(),
98        }
99    }
100
101    /// Enable or disable using the platform's native trusted root certificate store
102    ///
103    /// Default: true
104    pub fn with_native_roots(mut self, enable_native_roots: bool) -> Self {
105        self.enable_native_roots = enable_native_roots;
106        self
107    }
108
109    /// Add the PEM encoded certificate to the trust store
110    ///
111    /// This may be called more than once to add multiple certificates.
112    /// NOTE: PEM certificate contents are not validated until passed to the configured
113    /// TLS provider.
114    pub fn with_pem_certificate(mut self, pem_bytes: impl Into<Vec<u8>>) -> Self {
115        // ideally we'd validate here but rustls-pki-types converts to DER when loading and S2N
116        // still expects PEM encoding. Store the raw bytes and let the TLS implementation validate
117        self.custom_certs.push(CertificatePEM(pem_bytes.into()));
118        self
119    }
120
121    /// Add the PEM encoded certificate to the trust store
122    ///
123    /// This may be called more than once to add multiple certificates.
124    /// NOTE: PEM certificate contents are not validated until passed to the configured
125    /// TLS provider.
126    pub fn add_pem_certificate(&mut self, pem_bytes: impl Into<Vec<u8>>) -> &mut Self {
127        self.custom_certs.push(CertificatePEM(pem_bytes.into()));
128        self
129    }
130}
131
132impl Default for TrustStore {
133    fn default() -> Self {
134        Self {
135            enable_native_roots: true,
136            custom_certs: Vec::new(),
137        }
138    }
139}
140
141cfg_rustls! {
142    /// rustls based support and adapters
143    pub mod rustls_provider {
144        use crate::client::tls::Provider;
145        use rustls::crypto::CryptoProvider;
146
147        /// Choice of underlying cryptography library (this only applies to rustls)
148        #[derive(Debug, Eq, PartialEq, Clone)]
149        #[non_exhaustive]
150        pub enum CryptoMode {
151            /// Crypto based on [ring](https://github.com/briansmith/ring)
152            #[cfg(feature = "rustls-ring")]
153            Ring,
154            /// Crypto based on [aws-lc](https://github.com/aws/aws-lc-rs)
155            #[cfg(feature = "rustls-aws-lc")]
156            AwsLc,
157            /// FIPS compliant variant of [aws-lc](https://github.com/aws/aws-lc-rs)
158            #[cfg(feature = "rustls-aws-lc-fips")]
159            AwsLcFips,
160        }
161
162        impl CryptoMode {
163            fn provider(self) -> CryptoProvider {
164                match self {
165                    #[cfg(feature = "rustls-aws-lc")]
166                    CryptoMode::AwsLc => rustls::crypto::aws_lc_rs::default_provider(),
167
168                    #[cfg(feature = "rustls-ring")]
169                    CryptoMode::Ring => rustls::crypto::ring::default_provider(),
170
171                    #[cfg(feature = "rustls-aws-lc-fips")]
172                    CryptoMode::AwsLcFips => {
173                        let provider = rustls::crypto::default_fips_provider();
174                        assert!(
175                            provider.fips(),
176                            "FIPS was requested but the provider did not support FIPS"
177                        );
178                        provider
179                    }
180                }
181            }
182        }
183
184        impl Provider {
185            /// Create a TLS provider based on [rustls](https://github.com/rustls/rustls)
186            /// and the given [`CryptoMode`]
187            pub fn rustls(mode: CryptoMode) -> Provider {
188                Provider::Rustls(mode)
189            }
190        }
191
192        pub(crate) mod build_connector {
193            use crate::client::tls::rustls_provider::CryptoMode;
194            use crate::tls::TlsContext;
195            use hyper_util::client::legacy as client;
196            use client::connect::HttpConnector;
197            use rustls::crypto::CryptoProvider;
198            use std::sync::Arc;
199            use rustls_pki_types::CertificateDer;
200            use rustls_pki_types::pem::PemObject;
201            use rustls_native_certs::CertificateResult;
202            use std::sync::LazyLock;
203
204            /// Cached native certificates
205            ///
206            /// Creating a `with_native_roots()` hyper_rustls client re-loads system certs
207            /// each invocation (which can take 300ms on OSx). Cache the loaded certs
208            /// to avoid repeatedly incurring that cost.
209            pub(crate) static NATIVE_ROOTS: LazyLock<Vec<CertificateDer<'static>>> = LazyLock::new(|| {
210                let CertificateResult { certs, errors, .. } = rustls_native_certs::load_native_certs();
211                if !errors.is_empty() {
212                    tracing::warn!("native root CA certificate loading errors: {errors:?}")
213                }
214
215                if certs.is_empty() {
216                    tracing::warn!("no native root CA certificates found!");
217                }
218
219                // NOTE: unlike hyper-rustls::with_native_roots we don't validate here, we'll do that later
220                // for now we have a collection of certs that may or may not be valid.
221                certs
222            });
223
224            fn restrict_ciphers(base: CryptoProvider) -> CryptoProvider {
225                let suites = &[
226                    rustls::CipherSuite::TLS13_AES_256_GCM_SHA384,
227                    rustls::CipherSuite::TLS13_AES_128_GCM_SHA256,
228                    // TLS1.2 suites
229                    rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
230                    rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
231                    rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
232                    rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
233                    rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
234                ];
235                let supported_suites = suites
236                    .iter()
237                    .flat_map(|suite| {
238                        base.cipher_suites
239                            .iter()
240                            .find(|s| &s.suite() == suite)
241                            .cloned()
242                    })
243                    .collect::<Vec<_>>();
244                CryptoProvider {
245                    cipher_suites: supported_suites,
246                    ..base
247                }
248            }
249
250            impl TlsContext {
251                fn rustls_root_certs(&self) -> rustls::RootCertStore {
252                    let mut roots = rustls::RootCertStore::empty();
253                    if self.trust_store.enable_native_roots {
254                        let (valid, _invalid) = roots.add_parsable_certificates(
255                           NATIVE_ROOTS.clone()
256                        );
257                        debug_assert!(valid > 0, "TrustStore configured to enable native roots but no valid root certificates parsed!");
258                    }
259
260                    for pem_cert in &self.trust_store.custom_certs {
261                        let ders = CertificateDer::pem_slice_iter(&pem_cert.0).collect::<Result<Vec<_>, _> >().expect("valid PEM certificate");
262                        for cert in ders {
263                            roots.add(cert).expect("cert parsable")
264                        }
265                    }
266
267                    roots
268                }
269            }
270
271            pub(crate) fn wrap_connector<R>(
272                mut conn: HttpConnector<R>,
273                crypto_mode: CryptoMode,
274                tls_context: &TlsContext,
275            ) -> hyper_rustls::HttpsConnector<HttpConnector<R>> {
276                let root_certs = tls_context.rustls_root_certs();
277                conn.enforce_http(false);
278                hyper_rustls::HttpsConnectorBuilder::new()
279                    .with_tls_config(
280                        rustls::ClientConfig::builder_with_provider(Arc::new(restrict_ciphers(crypto_mode.provider())))
281                            .with_safe_default_protocol_versions()
282                            .expect("Error with the TLS configuration. Please file a bug report under https://github.com/smithy-lang/smithy-rs/issues.")
283                            .with_root_certificates(root_certs)
284                            .with_no_client_auth()
285                    )
286                    .https_or_http()
287                    .enable_http1()
288                    .enable_http2()
289                    .wrap_connector(conn)
290            }
291        }
292    }
293}
294
295cfg_s2n_tls! {
296    /// s2n-tls based support and adapters
297    pub(crate) mod s2n_tls_provider {
298        pub(crate) mod build_connector {
299            use hyper_util::client::legacy as client;
300            use client::connect::HttpConnector;
301            use s2n_tls::security::Policy;
302            use crate::tls::TlsContext;
303            use std::sync::LazyLock;
304
305            // Default S2N security policy which sets protocol versions and cipher suites
306            //  See https://aws.github.io/s2n-tls/usage-guide/ch06-security-policies.html
307            const S2N_POLICY_VERSION: &str = "20230317";
308
309            fn base_config() -> s2n_tls::config::Builder {
310                let mut builder = s2n_tls::config::Config::builder();
311                let policy = Policy::from_version(S2N_POLICY_VERSION).unwrap();
312                builder.set_security_policy(&policy).expect("valid s2n security policy");
313                // default is true
314                builder.with_system_certs(false).unwrap();
315                builder
316            }
317
318            static CACHED_CONFIG: LazyLock<s2n_tls::config::Config> = LazyLock::new(|| {
319                let mut config = base_config();
320                config.with_system_certs(true).unwrap();
321                // actually loads the system certs
322                config.build().expect("valid s2n config")
323            });
324
325            impl TlsContext {
326                fn s2n_config(&self) -> s2n_tls::config::Config {
327                    // TODO(s2n-tls): s2n does not support turning a config back into a builder or a way to load a trust store and re-use it
328                    // instead if we are only using the defaults then use a cached config, otherwise pay the cost to build a new one
329                    if self.trust_store.enable_native_roots && self.trust_store.custom_certs.is_empty() {
330                        CACHED_CONFIG.clone()
331                    } else {
332                        let mut config = base_config();
333                        config.with_system_certs(self.trust_store.enable_native_roots).unwrap();
334                        for pem_cert in &self.trust_store.custom_certs {
335                            config.trust_pem(pem_cert.0.as_slice()).expect("valid certificate");
336                        }
337                        config.build().expect("valid s2n config")
338                    }
339                }
340            }
341
342            pub(crate) fn wrap_connector<R>(
343                mut http_connector: HttpConnector<R>,
344                tls_context: &TlsContext,
345            ) -> s2n_tls_hyper::connector::HttpsConnector<HttpConnector<R>> {
346                let config = tls_context.s2n_config();
347                http_connector.enforce_http(false);
348                let mut builder = s2n_tls_hyper::connector::HttpsConnector::builder_with_http(http_connector, config);
349                builder.with_plaintext_http(true);
350                builder.build()
351            }
352        }
353    }
354}