Skip to content

Commit 161ebc0

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

File tree

20 files changed

+1910
-285
lines changed

20 files changed

+1910
-285
lines changed

libs/Cargo.lock

Lines changed: 695 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ vergen-gitcl = { version = "1.0.8", features = ["build"] }
1919
ahash = "0.8"
2020
anstyle = "1.0.11"
2121
anyhow = "1.0.98"
22+
base64 = "0.22.1"
2223
better-panic = "0.3.0"
2324
bimap = "0.6.3"
2425
bincode = "2"
@@ -96,6 +97,9 @@ reqwest = { version = "0.12", default-features = false, features = [
9697
reqwest-middleware = "0.4"
9798
reqwest-retry = "0.7.0"
9899
reqwest-tracing = "0.5.8"
100+
rustls = "0.23"
101+
rustls-pki-types = "1.12.0"
102+
rustls-platform-verifier = "0.6.1"
99103
ring = "0.17.14"
100104
rlimit = "0.10.2"
101105
ron = "0.10"
@@ -111,6 +115,7 @@ serde_html_form = "0.2"
111115
serde_json = "1.0.142"
112116
serde_path_to_error = "0.1"
113117
serde_stacker = "0.1"
118+
serde_yaml = "0.9.33"
114119
sha2 = "0.10.9"
115120
similar = "2.7.0"
116121
smallvec = "1"

libs/pavex/Cargo.toml

Lines changed: 14 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-pki-types", "dep:rustls-platform-verifier", "dep:base64"]
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,12 @@ 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-pki-types = { workspace = true, optional = true }
82+
rustls-platform-verifier = { workspace = true, optional = true }
83+
base64 = { workspace = true, optional = true }
84+
7785
tokio = { workspace = true, features = ["sync", "rt", "time"] }
7886
hyper = { workspace = true, features = ["full"] }
7987
hyper-util = { workspace = true, features = [
@@ -93,7 +101,9 @@ tracing = { workspace = true }
93101
reqwest = { workspace = true }
94102
itertools = { workspace = true }
95103
secrecy = { workspace = true, features = ["serde"] }
104+
serde_yaml = { workspace = true }
96105
pavex_tracing = { path = "../pavex_tracing" }
106+
uuid = { workspace = true, features = ["v7", "v4"] }
97107

98108
pavex_macros = { path = "../pavex_macros", features = [
99109
"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: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
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+
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
15+
/// Configure the TLS policy for a client.
16+
///
17+
/// It covers:
18+
/// - The [cryptographic stack](`Self::crypto_provider`) used to secure the connection.
19+
/// - Which [TLS versions](`Self::allowed_versions`) are allowed.
20+
/// - The [certificate verification](`Self::certificate_verification`) mechanism used to verify server certificates.
21+
///
22+
/// For testing/development purposes only, it exposes a few [insecure](`Self::insecure`) configuration options
23+
/// that lower the security posture of your client.
24+
///
25+
/// # Defaults
26+
///
27+
/// The default configuration should be suitable for most production environments:
28+
///
29+
/// ```yaml
30+
#[doc = include_str!("../../../tests/fixtures/tls_config/default.yaml")]
31+
/// ```
32+
///
33+
/// # Overriding the default configuration
34+
///
35+
/// If you want to deviate from the default configuration, it's enough to specify the fields you
36+
/// want to override.
37+
///
38+
/// ## Example: Disable TLS 1.2
39+
///
40+
/// ```yaml
41+
#[doc = include_str!("../../../tests/fixtures/tls_config/disable_tls_1_2.yaml")]
42+
/// ```
43+
///
44+
/// ## Example: Trust additional root certificates
45+
///
46+
/// ```yaml
47+
#[doc = include_str!("../../../tests/fixtures/tls_config/additional_roots.yaml")]
48+
/// ```
49+
///
50+
/// ## Example: Disable certificate verification
51+
///
52+
/// ```yaml
53+
#[doc = include_str!("../../../tests/fixtures/tls_config/skip_verification.yaml")]
54+
/// ```
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+
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
87+
#[non_exhaustive]
88+
/// Which TLS versions are allowed.
89+
///
90+
/// By default, TLS 1.2 and TLS 1.3 are enabled.
91+
///
92+
/// # Security
93+
///
94+
/// The lack of support for TLS 1.0 and TLS 1.1 is intentional.
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+
#[derive(Debug, Clone, Deserialize, Serialize)]
126+
#[non_exhaustive]
127+
/// Configure how server certificates are verified.
128+
///
129+
/// # Default
130+
///
131+
/// By default, we rely on verification machinery of the underlying operating system.
132+
/// Refer to the documentation for [`rustls-platform-verifier`](https://docs.rs/rustls-platform-verifier/latest/rustls_platform_verifier/)
133+
/// for more information on how each platform handles certificate verification.
134+
///
135+
/// # Customization
136+
///
137+
/// Set [`additional_roots`][`CertificateVerificationConfig::additional_roots`] to trust
138+
/// additional root certificates in addition to the ones already trusted
139+
/// by the operating system.
140+
///
141+
/// # Skipping Verification
142+
///
143+
/// If you want to skip certificate verification altogether, check out the [`insecure`][`super::TlsClientPolicyConfig::insecure`]
144+
/// options in [`TlsClientPolicyConfig`][`super::TlsClientPolicyConfig`].
145+
///
146+
/// ## Incorrect Usage
147+
///
148+
/// Setting [`use_os_verifier`][`CertificateVerificationConfig::use_os_verifier`] to `false`, with
149+
/// no [`additional_roots`][`CertificateVerificationConfig::additional_roots`] specified, does **not**
150+
/// disable certificate verification. It does instead cause all certificate verification attempts to fail.
151+
///
152+
/// We treat this scenario as a misconfiguration and return an error at runtime, when
153+
/// trying to initialize the client.
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+
#[derive(Debug, Clone, Deserialize, Serialize)]
188+
#[serde(rename_all = "snake_case")]
189+
#[non_exhaustive]
190+
/// [Additional root certificates](`CertificateVerificationConfig::additional_roots`) to be trusted.
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 that's inlined inside the provided configuration.
200+
Inline {
201+
/// How to decode the root certificate.
202+
encoding: RootCertificateInlineEncoding,
203+
/// The root certificate data.
204+
data: String,
205+
},
206+
}
207+
208+
#[derive(Debug, Clone, Deserialize, Serialize)]
209+
#[serde(rename_all = "snake_case")]
210+
#[non_exhaustive]
211+
/// Supported encodings for the root certificate in [`RootCertificate::File`].
212+
pub enum RootCertificateFileEncoding {
213+
/// A DER-encoded X.509 certificate; as specified in [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1).
214+
Der,
215+
/// A PEM-encoded X.509 certificate; as specified in [RFC 7468](https://datatracker.ietf.org/doc/html/rfc7468#section-5).
216+
Pem,
217+
}
218+
219+
#[derive(Debug, Clone, Deserialize, Serialize)]
220+
#[serde(rename_all = "snake_case")]
221+
#[non_exhaustive]
222+
/// Supported encodings for the root certificate in [`RootCertificate::Inline`].
223+
pub enum RootCertificateInlineEncoding {
224+
/// A DER-encoded X.509 certificate; as specified in [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1).
225+
///
226+
/// Since DER is a binary format, we expect the data to be [base64-encoded](https://datatracker.ietf.org/doc/html/rfc4648#section-4).
227+
Base64Der,
228+
/// A PEM-encoded X.509 certificate; as specified in [RFC 7468](https://datatracker.ietf.org/doc/html/rfc7468#section-5).
229+
///
230+
/// Since PEM is a text format, we don't expect the data to be base64-encoded.
231+
Pem,
232+
}
233+
234+
#[derive(Debug, Clone, Deserialize, Serialize)]
235+
#[non_exhaustive]
236+
/// Dangerous configuration options to lower the security posture of a TLS client.
237+
pub struct InsecureTlsClientConfig {
238+
/// Don't verify server certificates.
239+
///
240+
/// Extremely dangerous option, limit its usage to local development environments.
241+
#[serde(default = "default_skip_verification")]
242+
pub skip_verification: bool,
243+
}
244+
245+
impl Default for InsecureTlsClientConfig {
246+
fn default() -> Self {
247+
InsecureTlsClientConfig {
248+
skip_verification: default_skip_verification(),
249+
}
250+
}
251+
}
252+
253+
fn default_skip_verification() -> bool {
254+
false
255+
}
256+
257+
#[derive(Debug, Clone, Deserialize, Serialize)]
258+
#[serde(rename_all = "kebab-case", tag = "name")]
259+
#[non_exhaustive]
260+
/// Which implementation to use for TLS cryptographic operations.
261+
pub enum CryptoProviderConfig {
262+
/// Use [`aws-lc-rs`](https://docs.rs/aws-lc-rs/) for cryptographic operations.
263+
AwsLcRs {
264+
#[serde(default)]
265+
/// Whether to require FIPS compliance.
266+
///
267+
/// # Additional constraints
268+
///
269+
/// FIPS mode is not supported on all platforms.
270+
/// Furthermore, `aws-lc-rs` requires additional system dependencies at build time.
271+
/// Check out their [documentation](https://docs.rs/aws-lc-rs/latest/aws_lc_rs/#fips) for
272+
/// more information.
273+
require_fips: bool,
274+
},
275+
/// Use [`ring`](https://docs.rs/ring/) for cryptographic operations.
276+
Ring,
277+
}
278+
279+
impl Default for CryptoProviderConfig {
280+
fn default() -> Self {
281+
CryptoProviderConfig::AwsLcRs {
282+
require_fips: false,
283+
}
284+
}
285+
}

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)