osproxy_transport/tls.rs
1//! The TLS crypto seam.
2//!
3//! [`CryptoProvider`] is the abstraction the rest of the proxy programs against
4//! for TLS (`docs/02` §3, `docs/07`). The concrete backend is chosen **at build
5//! time** by a mutually-exclusive crate feature, so a FIPS artifact never links a
6//! non-validated crypto crate (and vice versa), this is a separate compiled
7//! binary, not a runtime switch (ADR-009, ADR-004):
8//!
9//! - **`non-fips`** (default): `RingProvider`, rustls's pure-Rust `ring`
10//! provider, no native toolchain, fast local/dev builds. `fips_mode()` is
11//! `false`; nothing may claim FIPS.
12//! - **`fips`**: `AwsLcFipsProvider`, the CMVP-validated aws-lc-rs module
13//! (builds AWS-LC-FIPS via cmake + C toolchain + Go). `fips_mode()` is `true`.
14//!
15//! Both implement the same trait and produce an interchangeable [`ServerConfig`],
16//! so request-path and server code never name a concrete provider, they use the
17//! [`DefaultCryptoProvider`](crate::DefaultCryptoProvider) alias the active
18//! feature resolves. No request-path code branches on FIPS.
19//!
20//! Independently of which module backs it, every provider pins the wire policy
21//! to the FIPS-approved set ([`FIPS_APPROVED_SUITES`], TLS 1.2/1.3, ADR-004
22//! caveat #3, NFR-S5): the module's validation is what differs between `ring` and
23//! aws-lc-rs FIPS, not the suites offered, so the suite/version restriction lives
24//! here in the config layer and is testable without the FIPS toolchain.
25
26use std::sync::Arc;
27
28use thiserror::Error;
29use tokio_rustls::rustls::crypto::CryptoProvider as RustlsCryptoProvider;
30use tokio_rustls::rustls::pki_types::pem::PemObject;
31use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
32use tokio_rustls::rustls::version::{TLS12, TLS13};
33use tokio_rustls::rustls::{CipherSuite, ServerConfig, SupportedProtocolVersion};
34
35/// The FIPS-approved TLS cipher suites the proxy offers (`docs/07` §2 caveat 3,
36/// NFR-S5):
37/// TLS 1.3 and TLS 1.2 AES-GCM only. CHACHA20-POLY1305 is deliberately excluded,
38/// it is not a FIPS-approved suite. This wire policy is applied to *every*
39/// provider, FIPS-validated or not, so the suites negotiated are identical
40/// regardless of the underlying module; the FIPS module changes validation, not
41/// the suites on the wire. The set is keyed on the provider-independent
42/// [`CipherSuite`] identifier so the aws-lc-rs provider pins the exact same list.
43pub const FIPS_APPROVED_SUITES: &[CipherSuite] = &[
44 CipherSuite::TLS13_AES_128_GCM_SHA256,
45 CipherSuite::TLS13_AES_256_GCM_SHA384,
46 CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
47 CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
48 CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
49 CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
50];
51
52/// The FIPS-approved TLS protocol versions: 1.2 and 1.3 only (NFR-S5). Older
53/// versions are refused at negotiation.
54const FIPS_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13, &TLS12];
55
56/// The pluggable TLS backend.
57///
58/// Hands out a ready [`ServerConfig`] for terminating downstream TLS and reports
59/// whether the backend is operating in a FIPS-validated mode (provider-dependent:
60/// `false` for the `ring` build, `true` for the aws-lc-rs FIPS build).
61pub trait CryptoProvider: Send + Sync + 'static {
62 /// The server-side TLS configuration for terminating client connections.
63 fn server_config(&self) -> Arc<ServerConfig>;
64
65 /// Whether the backend is a FIPS-validated module in FIPS mode.
66 fn fips_mode(&self) -> bool;
67}
68
69/// A failure building a [`RingProvider`] from PEM material.
70#[non_exhaustive]
71#[derive(Debug, Error)]
72pub enum TlsError {
73 /// The certificate or key PEM could not be parsed.
74 #[error("invalid PEM: {0}")]
75 Pem(&'static str),
76 /// No private key was found in the key PEM.
77 #[error("no private key found")]
78 NoKey,
79 /// rustls rejected the certificate/key configuration.
80 #[error("rustls configuration error: {0}")]
81 Config(String),
82}
83
84/// A [`CryptoProvider`] backed by rustls's pure-Rust `ring` module
85/// (`non-fips` feature). Not FIPS-validated, `fips_mode()` is always `false`.
86/// Server-auth and mutual-TLS, built from PEM.
87#[cfg(feature = "non-fips")]
88#[derive(Debug, Clone)]
89pub struct RingProvider {
90 server_config: Arc<ServerConfig>,
91}
92
93#[cfg(feature = "non-fips")]
94impl RingProvider {
95 /// Builds a server-authentication-only provider from a PEM certificate chain
96 /// and private key (no client-certificate verification).
97 ///
98 /// # Errors
99 ///
100 /// Returns [`TlsError`] if the PEM cannot be parsed or rustls rejects the
101 /// certificate/key pair.
102 pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> Result<Self, TlsError> {
103 let base = tokio_rustls::rustls::crypto::ring::default_provider();
104 Ok(Self {
105 server_config: build_server_config(base, cert_pem, key_pem, None)?,
106 })
107 }
108
109 /// Builds a mutual-TLS provider: clients must present a certificate that
110 /// chains to a root in `client_ca_pem`, and the verified identity is exposed
111 /// to the handler.
112 ///
113 /// # Errors
114 ///
115 /// Returns [`TlsError`] if any PEM cannot be parsed or rustls rejects the
116 /// verifier or certificate/key pair.
117 pub fn from_pem_mtls(
118 cert_pem: &[u8],
119 key_pem: &[u8],
120 client_ca_pem: &[u8],
121 ) -> Result<Self, TlsError> {
122 let base = tokio_rustls::rustls::crypto::ring::default_provider();
123 Ok(Self {
124 server_config: build_server_config(base, cert_pem, key_pem, Some(client_ca_pem))?,
125 })
126 }
127}
128
129#[cfg(feature = "non-fips")]
130impl CryptoProvider for RingProvider {
131 fn server_config(&self) -> Arc<ServerConfig> {
132 Arc::clone(&self.server_config)
133 }
134
135 fn fips_mode(&self) -> bool {
136 // ring is not a FIPS module; FIPS requires the `fips` build (aws-lc-rs).
137 false
138 }
139}
140
141/// A [`CryptoProvider`] backed by the CMVP-validated **aws-lc-rs** module in FIPS
142/// mode (`fips` feature). The wire policy and PEM handling are identical to
143/// [`RingProvider`], only the underlying validated module differs (ADR-004).
144#[cfg(feature = "fips")]
145#[derive(Debug, Clone)]
146pub struct AwsLcFipsProvider {
147 server_config: Arc<ServerConfig>,
148}
149
150#[cfg(feature = "fips")]
151impl AwsLcFipsProvider {
152 /// Builds a server-authentication-only FIPS provider from PEM.
153 ///
154 /// # Errors
155 ///
156 /// Returns [`TlsError`] if the PEM cannot be parsed or rustls rejects the
157 /// certificate/key pair.
158 pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> Result<Self, TlsError> {
159 Ok(Self {
160 server_config: build_server_config(fips_base()?, cert_pem, key_pem, None)?,
161 })
162 }
163
164 /// Builds a mutual-TLS FIPS provider; clients must present a certificate
165 /// chaining to a root in `client_ca_pem`.
166 ///
167 /// # Errors
168 ///
169 /// Returns [`TlsError`] if any PEM cannot be parsed or rustls rejects the
170 /// verifier or certificate/key pair.
171 pub fn from_pem_mtls(
172 cert_pem: &[u8],
173 key_pem: &[u8],
174 client_ca_pem: &[u8],
175 ) -> Result<Self, TlsError> {
176 Ok(Self {
177 server_config: build_server_config(
178 fips_base()?,
179 cert_pem,
180 key_pem,
181 Some(client_ca_pem),
182 )?,
183 })
184 }
185}
186
187/// The aws-lc-rs base provider, but only if the linked module is **actually** in
188/// FIPS mode. A FIPS artifact built without AWS-LC-FIPS truly engaging would
189/// otherwise link, report `fips_mode() == false`, and ship silently, so we fail
190/// loudly at construction instead (the provider's FIPS status is a build fact, so
191/// this fails fast on the very first TLS provider built).
192#[cfg(feature = "fips")]
193fn fips_base() -> Result<RustlsCryptoProvider, TlsError> {
194 let base = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider();
195 if base.fips() {
196 Ok(base)
197 } else {
198 Err(TlsError::Config(
199 "aws-lc-rs is not operating in FIPS mode: the `fips` build did not engage \
200 AWS-LC-FIPS (check the AWS-LC-FIPS toolchain and that the `fips` feature \
201 is active)"
202 .to_owned(),
203 ))
204 }
205}
206
207#[cfg(feature = "fips")]
208impl CryptoProvider for AwsLcFipsProvider {
209 fn server_config(&self) -> Arc<ServerConfig> {
210 Arc::clone(&self.server_config)
211 }
212
213 fn fips_mode(&self) -> bool {
214 // Report what the linked module actually is, not a build-time assumption:
215 // a true FIPS module answers `true` here.
216 self.server_config.crypto_provider().fips()
217 }
218}
219
220/// Builds a downstream [`ServerConfig`] from a base crypto provider plus PEM
221/// material, applying the shared policy every provider obeys: cipher suites
222/// pinned to [`FIPS_APPROVED_SUITES`], versions to [`FIPS_VERSIONS`], optional
223/// mutual-TLS client verification, and h2/http-1.1 ALPN. The only thing that
224/// varies between the `ring` and aws-lc-rs builds is the `base` passed in.
225fn build_server_config(
226 base: RustlsCryptoProvider,
227 cert_pem: &[u8],
228 key_pem: &[u8],
229 client_ca_pem: Option<&[u8]>,
230) -> Result<Arc<ServerConfig>, TlsError> {
231 let provider = fips_pinned_provider(base);
232 let versions = ServerConfig::builder_with_provider(provider.clone())
233 .with_protocol_versions(FIPS_VERSIONS)
234 .map_err(|e| TlsError::Config(e.to_string()))?;
235 let builder = match client_ca_pem {
236 None => versions.with_no_client_auth(),
237 Some(ca_pem) => {
238 let mut roots = tokio_rustls::rustls::RootCertStore::empty();
239 for ca in parse_certs(ca_pem)? {
240 roots.add(ca).map_err(|e| TlsError::Config(e.to_string()))?;
241 }
242 let verifier =
243 tokio_rustls::rustls::server::WebPkiClientVerifier::builder_with_provider(
244 Arc::new(roots),
245 provider,
246 )
247 .build()
248 .map_err(|e| TlsError::Config(e.to_string()))?;
249 versions.with_client_cert_verifier(verifier)
250 }
251 };
252 let certs = parse_certs(cert_pem)?;
253 let key = parse_key(key_pem)?;
254 let mut config = builder
255 .with_single_cert(certs, key)
256 .map_err(|e| TlsError::Config(e.to_string()))?;
257 // Advertise HTTP/2 (preferred) then HTTP/1.1 via ALPN so a TLS client can
258 // negotiate h2; the auto ingress builder serves whichever is selected.
259 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
260 Ok(Arc::new(config))
261}
262
263/// A crypto provider with its cipher-suite list filtered to the FIPS-approved set
264/// ([`FIPS_APPROVED_SUITES`]). Keyed on the provider-independent [`CipherSuite`]
265/// id, so both the `ring` and aws-lc-rs bases pin the identical list, only the
266/// validated module differs, not the wire policy.
267fn fips_pinned_provider(base: RustlsCryptoProvider) -> Arc<RustlsCryptoProvider> {
268 let cipher_suites = base
269 .cipher_suites
270 .iter()
271 .copied()
272 .filter(|cs| FIPS_APPROVED_SUITES.contains(&cs.suite()))
273 .collect();
274 Arc::new(RustlsCryptoProvider {
275 cipher_suites,
276 ..base
277 })
278}
279
280/// The verified mTLS client identity from a completed handshake, if the peer
281/// presented a certificate: a stable `cert:{fingerprint}` id, never the
282/// certificate material. Shared by the HTTP and gRPC ingress paths.
283#[must_use]
284pub(crate) fn client_subject_from_tls(
285 tls: &tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
286) -> Option<String> {
287 let (_, conn) = tls.get_ref();
288 conn.peer_certificates()
289 .and_then(<[_]>::first)
290 .map(|cert| format!("cert:{}", cert_fingerprint(cert.as_ref())))
291}
292
293/// A stable identity for a verified client certificate: the lowercase hex
294/// SHA-256 fingerprint of its DER. Not the certificate material, so it is safe
295/// to carry as an id and surface in telemetry.
296#[must_use]
297pub(crate) fn cert_fingerprint(der: &[u8]) -> String {
298 // Hash with whichever validated module the build linked, so even the cert
299 // fingerprint stays on the FIPS module in a FIPS build (ring and aws-lc-rs
300 // share this `digest` API).
301 #[cfg(feature = "non-fips")]
302 let digest = ring::digest::digest(&ring::digest::SHA256, der);
303 #[cfg(feature = "fips")]
304 let digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, der);
305 let mut out = String::with_capacity(digest.as_ref().len() * 2);
306 for byte in digest.as_ref() {
307 out.push(char::from_digit(u32::from(byte >> 4), 16).unwrap_or('0'));
308 out.push(char::from_digit(u32::from(byte & 0x0f), 16).unwrap_or('0'));
309 }
310 out
311}
312
313/// Parses a PEM certificate chain into DER certificates, via the
314/// `rustls-pki-types` `PemObject` API (the maintained successor to
315/// `rustls-pemfile`).
316fn parse_certs(pem: &[u8]) -> Result<Vec<CertificateDer<'static>>, TlsError> {
317 let certs: Result<Vec<_>, _> = CertificateDer::pem_slice_iter(pem).collect();
318 let certs = certs.map_err(|_| TlsError::Pem("certificate"))?;
319 if certs.is_empty() {
320 return Err(TlsError::Pem("certificate (none found)"));
321 }
322 Ok(certs)
323}
324
325/// Parses the first private key (PKCS#8, PKCS#1, or SEC1) from PEM.
326fn parse_key(pem: &[u8]) -> Result<PrivateKeyDer<'static>, TlsError> {
327 PrivateKeyDer::from_pem_slice(pem).map_err(|_| TlsError::NoKey)
328}