Skip to content

Commit 49adb83

Browse files
feat: Add client TLS configuration, with built-in support for rustls
1 parent fa3caf9 commit 49adb83

File tree

20 files changed

+1807
-285
lines changed

20 files changed

+1807
-285
lines changed

libs/Cargo.lock

Lines changed: 693 additions & 281 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ reqwest = { version = "0.12", default-features = false, features = [
9696
reqwest-middleware = "0.4"
9797
reqwest-retry = "0.7.0"
9898
reqwest-tracing = "0.5.8"
99+
rustls = "0.23"
100+
rustls-platform-verifier = "0.6.1"
99101
ring = "0.17.14"
100102
rlimit = "0.10.2"
101103
ron = "0.10"
@@ -111,6 +113,7 @@ serde_html_form = "0.2"
111113
serde_json = "1.0.142"
112114
serde_path_to_error = "0.1"
113115
serde_stacker = "0.1"
116+
serde_yaml = "0.9.33"
114117
sha2 = "0.10.9"
115118
similar = "2.7.0"
116119
smallvec = "1"

libs/pavex/Cargo.toml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ repository.workspace = true
1313
license.workspace = true
1414
readme = "README.md"
1515

16-
[lints.rust]
17-
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(pavex_ide_hint)'] }
18-
1916
[features]
20-
default = ["server", "server_request_id", "time", "cookie", "config"]
17+
default = ["server", "server_request_id", "time", "cookie", "config", "rustls_0_23"]
2118

2219
server = ["dep:hyper-util", "dep:socket2", "tokio/net"]
2320
config = ["dep:figment"]
2421
cookie = ["dep:biscotti", "time"]
2522
server_request_id = ["dep:uuid"]
2623
time = ["dep:jiff"]
24+
rustls_0_23 = ["dep:rustls", "dep:rustls-platform-verifier"]
25+
fips = ["rustls?/fips"]
26+
tls_crypto_provider_ring = ["rustls?/aws_lc_rs"]
27+
tls_crypto_provider_aws_lc_rs = ["rustls?/ring"]
2728

2829
[dependencies]
2930
bytes = { workspace = true }
@@ -46,6 +47,7 @@ persist_if_changed = { path = "../persist_if_changed", version = "0.2.7" }
4647

4748
# Configuration
4849
figment = { workspace = true, features = ["env", "yaml"], optional = true }
50+
serde_yaml = { workspace = true }
4951

5052
# Route parameters
5153
matchit = { workspace = true }
@@ -74,6 +76,10 @@ type-safe-id = { workspace = true }
7476
# Time facilities
7577
jiff = { workspace = true, features = ["serde"], optional = true }
7678

79+
# TLS
80+
rustls = { workspace = true, optional = true }
81+
rustls-platform-verifier = { workspace = true, optional = true }
82+
7783
tokio = { workspace = true, features = ["sync", "rt", "time"] }
7884
hyper = { workspace = true, features = ["full"] }
7985
hyper-util = { workspace = true, features = [
@@ -93,7 +99,9 @@ tracing = { workspace = true }
9399
reqwest = { workspace = true }
94100
itertools = { workspace = true }
95101
secrecy = { workspace = true, features = ["serde"] }
102+
serde_yaml = { workspace = true }
96103
pavex_tracing = { path = "../pavex_tracing" }
104+
uuid = { workspace = true, features = ["v7", "v4"] }
97105

98106
pavex_macros = { path = "../pavex_macros", features = [
99107
"allow_unreachable_pub",

libs/pavex/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub mod time {
3838
//! It's a re-export of the [`[email protected]`](https://docs.rs/jiff/0.2) crate.
3939
pub use jiff::*;
4040
}
41+
pub mod tls;
4142

4243
/// Define a [prebuilt type](https://pavex.dev/docs/guide/dependency_injection/prebuilt_types/).
4344
///
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
//! Configure the TLS policy for a client.
2+
//!
3+
//! Check out the documentation for [`TlsClientPolicyConfig`](super::TlsClientPolicyConfig) for
4+
//! a detailed explanation of the available configuration options.
5+
use serde::{Deserialize, Serialize};
6+
use std::path::PathBuf;
7+
8+
// Wrapped into a sub-module to avoid exposing `TlsClientPolicyConfig` in two places:
9+
// inside `pavex::tls::config` and `pavex::tls::client`.
10+
// We only want users to see `pavex::tls::client::TlsClientPolicyConfig`.
11+
pub(crate) mod _config {
12+
use super::*;
13+
14+
/// Configure the TLS policy for a client.
15+
///
16+
/// It covers:
17+
/// - The [cryptographic stack](`Self::crypto_provider`) used to secure the connection.
18+
/// - Which [TLS versions](`Self::allowed_versions`) are allowed.
19+
/// - The [certificate verification](`Self::certificate_verification`) mechanism used to verify server certificates.
20+
///
21+
/// For testing/development purposes only, it exposes a few [insecure](`Self::insecure`) configuration options
22+
/// that lower the security posture of your client.
23+
///
24+
/// # Defaults
25+
///
26+
/// The default configuration should be suitable for most production environments:
27+
///
28+
/// ```yaml
29+
#[doc = include_str!("../../../tests/fixtures/tls_config/default.yaml")]
30+
/// ```
31+
///
32+
/// # Overriding the default configuration
33+
///
34+
/// If you want to deviate from the default configuration, it's enough to specify the fields you
35+
/// want to override.
36+
///
37+
/// ## Example: Disable TLS 1.2
38+
///
39+
/// ```yaml
40+
#[doc = include_str!("../../../tests/fixtures/tls_config/disable_tls_1_2.yaml")]
41+
/// ```
42+
///
43+
/// ## Example: Trust additional root certificates
44+
///
45+
/// ```yaml
46+
#[doc = include_str!("../../../tests/fixtures/tls_config/additional_roots.yaml")]
47+
/// ```
48+
///
49+
/// ## Example: Disable certificate verification
50+
///
51+
/// ```yaml
52+
#[doc = include_str!("../../../tests/fixtures/tls_config/skip_verification.yaml")]
53+
/// ```
54+
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
55+
#[non_exhaustive]
56+
pub struct TlsClientPolicyConfig {
57+
/// The cryptographic stack used to secure the connection.
58+
///
59+
/// Refer to the documentation for [`CryptoProviderConfig`](CryptoProviderConfig)
60+
/// for more details.
61+
#[serde(default)]
62+
#[serde(with = "serde_yaml::with::singleton_map_recursive")]
63+
pub crypto_provider: CryptoProviderConfig,
64+
/// Which TLS versions are allowed.
65+
///
66+
/// Refer to the documentation for [`AllowedTlsVersionsConfig`](AllowedTlsVersionsConfig)
67+
/// for more details.
68+
#[serde(default)]
69+
pub allowed_versions: AllowedTlsVersionsConfig,
70+
/// The mechanism used to verify server certificates.
71+
///
72+
/// Refer to the documentation for [`CertificateVerificationConfig`](CertificateVerificationConfig)
73+
/// for more details.
74+
#[serde(default)]
75+
pub certificate_verification: CertificateVerificationConfig,
76+
/// Dangerous configuration options that lower the security
77+
/// posture of your client.
78+
///
79+
/// These options should never be used in production scenarios.
80+
/// They are available for testing/development purposes only.
81+
#[serde(default)]
82+
pub insecure: InsecureTlsClientConfig,
83+
}
84+
}
85+
86+
/// Which TLS versions are allowed.
87+
///
88+
/// By default, TLS 1.2 and TLS 1.3 are enabled.
89+
///
90+
/// # Security
91+
///
92+
/// The lack of support for TLS 1.0 and TLS 1.1 is intentional.
93+
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
94+
#[non_exhaustive]
95+
pub struct AllowedTlsVersionsConfig {
96+
/// Enables TLS 1.2 if `true`.
97+
///
98+
/// It requires the server to support TLS 1.2.
99+
#[serde(default = "default_v1_2")]
100+
pub v1_2: bool,
101+
/// Enables TLS 1.3 if `true`.
102+
///
103+
/// It requires the server to support TLS 1.3.
104+
#[serde(default = "default_v1_3")]
105+
pub v1_3: bool,
106+
}
107+
108+
fn default_v1_2() -> bool {
109+
true
110+
}
111+
112+
fn default_v1_3() -> bool {
113+
true
114+
}
115+
116+
impl Default for AllowedTlsVersionsConfig {
117+
fn default() -> Self {
118+
Self {
119+
v1_2: default_v1_2(),
120+
v1_3: default_v1_3(),
121+
}
122+
}
123+
}
124+
125+
/// Configure how server certificates are verified.
126+
///
127+
/// # Default
128+
///
129+
/// By default, we rely on verification machinery of the underlying operating system.
130+
/// Refer to the documentation for [`rustls-platform-verifier`](https://docs.rs/rustls-platform-verifier/latest/rustls_platform_verifier/)
131+
/// for more information on how each platform handles certificate verification.
132+
///
133+
/// # Customization
134+
///
135+
/// Set [`additional_roots`][`CertificateVerificationConfig::additional_roots`] to trust
136+
/// additional root certificates in addition to the ones already trusted
137+
/// by the operating system.
138+
///
139+
/// # Skipping Verification
140+
///
141+
/// If you want to skip certificate verification altogether, check out the [`insecure`][`super::TlsClientPolicyConfig::insecure`]
142+
/// options in [`TlsClientPolicyConfig`][`super::TlsClientPolicyConfig`].
143+
///
144+
/// ## Incorrect Usage
145+
///
146+
/// Setting [`use_os_verifier`][`CertificateVerificationConfig::use_os_verifier`] to `false`, with
147+
/// no [`additional_roots`][`CertificateVerificationConfig::additional_roots`] specified, does **not**
148+
/// disable certificate verification. It does instead cause all certificate verification attempts to fail.
149+
///
150+
/// We treat this scenario as a misconfiguration and return an error at runtime, when
151+
/// trying to initialize the client.
152+
#[derive(Debug, Clone, Deserialize, Serialize)]
153+
#[non_exhaustive]
154+
pub struct CertificateVerificationConfig {
155+
/// Whether to use the certificate verification machinery provided by
156+
/// the underlying operating system.
157+
///
158+
/// Defaults to `true`.
159+
#[serde(default = "default_use_os_verifier")]
160+
pub use_os_verifier: bool,
161+
/// Trust one or more additional root certificates.
162+
///
163+
/// If [`use_os_verifier`][`CertificateVerificationConfig::use_os_verifier`] is `false`,
164+
/// these will be the only trusted root certificates.
165+
/// If [`use_os_verifier`][`CertificateVerificationConfig::use_os_verifier`] is `true`, these will be
166+
/// trusted **in addition** to the ones already trusted by the underlying operating system.
167+
///
168+
/// They can either be loaded from files or inlined in configuration.
169+
#[serde(default)]
170+
#[serde(with = "serde_yaml::with::singleton_map_recursive")]
171+
pub additional_roots: Vec<RootCertificate>,
172+
}
173+
174+
fn default_use_os_verifier() -> bool {
175+
true
176+
}
177+
178+
impl Default for CertificateVerificationConfig {
179+
fn default() -> Self {
180+
CertificateVerificationConfig {
181+
use_os_verifier: default_use_os_verifier(),
182+
additional_roots: Default::default(),
183+
}
184+
}
185+
}
186+
187+
/// [Additional root certificates](`CertificateVerificationConfig::additional_roots`) to be trusted.
188+
#[derive(Debug, Clone, Deserialize, Serialize)]
189+
#[serde(rename_all = "snake_case")]
190+
#[non_exhaustive]
191+
pub enum RootCertificate {
192+
/// Retrieve the root certificate from a file.
193+
File {
194+
/// How to decode the root certificate inside the file.
195+
encoding: RootCertificateFileEncoding,
196+
/// The path to the root certificate file.
197+
path: PathBuf,
198+
},
199+
/// A root certificate inlined inside the provided configuration.
200+
Inline {
201+
/// A PEM-encoded X.509 certificate; as specified in [RFC 7468](https://datatracker.ietf.org/doc/html/rfc7468#section-5).
202+
data: String,
203+
},
204+
}
205+
206+
/// Supported encodings for the root certificate in [`RootCertificate::File`].
207+
#[derive(Debug, Clone, Deserialize, Serialize)]
208+
#[serde(rename_all = "snake_case")]
209+
#[non_exhaustive]
210+
pub enum RootCertificateFileEncoding {
211+
/// A DER-encoded X.509 certificate; as specified in [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1).
212+
Der,
213+
/// A PEM-encoded X.509 certificate; as specified in [RFC 7468](https://datatracker.ietf.org/doc/html/rfc7468#section-5).
214+
Pem,
215+
}
216+
217+
/// Dangerous configuration options to lower the security posture of a TLS client.
218+
#[derive(Debug, Clone, Deserialize, Serialize)]
219+
#[non_exhaustive]
220+
pub struct InsecureTlsClientConfig {
221+
/// Don't verify server certificates.
222+
///
223+
/// Extremely dangerous option, limit its usage to local development environments.
224+
#[serde(default = "default_skip_verification")]
225+
pub skip_verification: bool,
226+
}
227+
228+
impl Default for InsecureTlsClientConfig {
229+
fn default() -> Self {
230+
InsecureTlsClientConfig {
231+
skip_verification: default_skip_verification(),
232+
}
233+
}
234+
}
235+
236+
fn default_skip_verification() -> bool {
237+
false
238+
}
239+
240+
/// Which implementation to use for TLS cryptographic operations.
241+
#[derive(Debug, Default, Copy, Clone, Deserialize, Serialize)]
242+
#[serde(rename_all = "snake_case")]
243+
#[non_exhaustive]
244+
pub enum CryptoProviderConfig {
245+
/// Use [`aws-lc-rs`](https://docs.rs/aws-lc-rs/) for cryptographic operations.
246+
///
247+
/// If you require FIPS compliance, use the [`AwsLcRsFips`][`CryptoProviderConfig::AwsLcRsFips`]
248+
/// instead.
249+
#[default]
250+
AwsLcRs,
251+
/// Use [`aws-lc-rs`](https://docs.rs/aws-lc-rs/) for cryptographic operations,
252+
/// in FIPS-compliant mode.
253+
///
254+
/// # Additional constraints
255+
///
256+
/// FIPS mode is not supported on all platforms.
257+
/// Furthermore, `aws-lc-rs` requires additional system dependencies at build time.
258+
/// Check out their [documentation](https://docs.rs/aws-lc-rs/latest/aws_lc_rs/#fips) for
259+
/// more information.
260+
AwsLcRsFips,
261+
/// Use [`ring`](https://docs.rs/ring/) for cryptographic operations.
262+
Ring,
263+
}

libs/pavex/src/tls/client/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! Secure your client connections with TLS (Transport Layer Security).
2+
//!
3+
//! Check out the documentation for [`TlsClientPolicyConfig`] for
4+
//! a detailed explanation of the available configuration options.
5+
pub mod config;
6+
pub use config::_config::TlsClientPolicyConfig;
7+
8+
#[cfg(feature = "rustls_0_23")]
9+
mod rustls_0_23;

0 commit comments

Comments
 (0)