Skip to main content

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}