Skip to content

Commit 09eaaeb

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

File tree

20 files changed

+1872
-285
lines changed

20 files changed

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

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod config;
2+
pub use config::_config::TlsClientPolicyConfig;
3+
4+
#[cfg(feature = "rustls_0_23")]
5+
mod rustls_0_23;

0 commit comments

Comments
 (0)