1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + | use crate::cfg::{cfg_rustls, cfg_s2n_tls};
|
6 + | use crate::HttpClientError;
|
7 + |
|
8 + | /// Choice of underlying cryptography library
|
9 + | #[derive(Debug, Eq, PartialEq, Clone)]
|
10 + | #[non_exhaustive]
|
11 + | pub 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)]
|
26 + | pub struct TlsContext {
|
27 + | #[allow(unused)]
|
28 + | trust_store: TrustStore,
|
29 + | }
|
30 + |
|
31 + | impl TlsContext {
|
32 + | /// Create a new [TlsContext] builder
|
33 + | pub fn builder() -> TlsContextBuilder {
|
34 + | TlsContextBuilder::new()
|
35 + | }
|
36 + | }
|
37 + |
|
38 + | impl 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)]
|
46 + | pub struct TlsContextBuilder {
|
47 + | trust_store: TrustStore,
|
48 + | }
|
49 + |
|
50 + | impl 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)]
|
74 + | struct CertificatePEM(Vec<u8>);
|
75 + |
|
76 + | impl 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)]
|
87 + | pub struct TrustStore {
|
88 + | enable_native_roots: bool,
|
89 + | custom_certs: Vec<CertificatePEM>,
|
90 + | }
|
91 + |
|
92 + | impl 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 + |
|
132 + | impl Default for TrustStore {
|
133 + | fn default() -> Self {
|
134 + | Self {
|
135 + | enable_native_roots: true,
|
136 + | custom_certs: Vec::new(),
|
137 + | }
|
138 + | }
|
139 + | }
|
140 + |
|
141 + | cfg_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 + |
|
295 + | cfg_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 + | }
|