dwara_core/security/fips.rs
1//! FIPS 140-3 mode (DW-111, Enterprise).
2//!
3//! FIPS enforcement is an Enterprise-only capability gated behind the
4//! `ent` cargo feature. When `ent` is compiled in, the gateway operates
5//! in FIPS 140-3 mode: the rustls process-default crypto provider is the
6//! FIPS-validated aws-lc-rs provider, TLS cipher suites are restricted
7//! to the FIPS-approved allowlist, non-approved primitives (Ed25519
8//! certificates, Argon2 credential hashing) are rejected at config
9//! validation, and a startup self-test verifies the provider before the
10//! gateway accepts traffic.
11//!
12//! aws-lc-rs is already the default rustls crypto provider in every
13//! build (see [`crate::security::tls::install_aws_lc_rs_provider`]), so
14//! the FIPS-validated code path is present regardless. The `ent` feature
15//! turns ON the enforcement layer (provider self-test, cipher-suite
16//! restriction, primitive allowlist, license assertion).
17//!
18//! # OSS vs Enterprise
19//!
20//! In OSS builds (no `ent` feature), FIPS enforcement is OFF:
21//! [`FipsMode`] is [`FipsMode::Disabled`], the self-test returns a
22//! Disabled attestation, and [`is_primitive_allowed`] always returns
23//! `true` (no restriction). The aws-lc-rs provider is still installed
24//! (it is the default provider), but no FIPS restrictions are enforced.
25//!
26//! # Self-test
27//!
28//! [`fips_self_test`] verifies that the process-default crypto provider
29//! is the aws-lc-rs FIPS provider and returns a [`FipsAttestation`]
30//! capturing the provider name, version, self-test result, and
31//! timestamp. In Enterprise builds the binary runs this at startup and
32//! refuses to boot (exit 1) if the self-test fails. The attestation is
33//! also surfaced on the `/healthz` endpoint so orchestrators and
34//! monitoring can confirm FIPS mode.
35//!
36//! # Primitive allowlist
37//!
38//! [`FIPS_ALLOWED_CIPHERS`] and [`FIPS_ALLOWED_SIGNATURES`] are the
39//! FIPS-approved TLS cipher suites and signature schemes.
40//! [`is_primitive_allowed`] checks a primitive name against the
41//! allowlist. In OSS builds every function in this module is inert.
42
43use std::time::{SystemTime, UNIX_EPOCH};
44
45/// The FIPS mode of the gateway.
46///
47/// [`FipsMode::Enabled`] when the `ent` cargo feature is compiled in;
48/// [`FipsMode::Disabled`] otherwise. This is a compile-time constant:
49/// the feature is a build-time switch, not a runtime toggle.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum FipsMode {
52 /// FIPS 140-3 mode is active: the FIPS provider is installed, cipher
53 /// suites are restricted, and non-approved primitives are rejected.
54 Enabled,
55 /// FIPS 140-3 mode is inactive: all FIPS checks are inert (no
56 /// provider self-test, no cipher-suite restriction, no primitive
57 /// allowlist enforcement).
58 Disabled,
59}
60
61impl FipsMode {
62 /// The current FIPS mode (compile-time determined).
63 ///
64 /// FIPS enforcement (cipher-suite restriction, primitive allowlist,
65 /// certificate signature checks) is an Enterprise-only capability.
66 /// The OSS build installs the aws-lc-rs provider and runs the
67 /// self-test (so the attestation is available on /healthz), but does
68 /// NOT enforce FIPS restrictions — an OSS operator can use any
69 /// certificate signature algorithm and credential hash without
70 /// validation rejecting the config.
71 pub fn current() -> Self {
72 #[cfg(feature = "ent")]
73 {
74 FipsMode::Enabled
75 }
76 #[cfg(not(feature = "ent"))]
77 {
78 FipsMode::Disabled
79 }
80 }
81
82 /// True when FIPS mode is active.
83 pub fn is_enabled(self) -> bool {
84 self == FipsMode::Enabled
85 }
86}
87
88impl std::fmt::Display for FipsMode {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 FipsMode::Enabled => write!(f, "enabled"),
92 FipsMode::Disabled => write!(f, "disabled"),
93 }
94 }
95}
96
97/// The canonical name of the aws-lc-rs FIPS provider as registered with
98/// rustls. Used by the self-test to confirm the process-default provider
99/// is the FIPS-validated one.
100pub const FIPS_PROVIDER_NAME: &str = "aws-lc-rs";
101
102/// The FIPS-approved TLS 1.2 cipher suites (IANA names, lowercase). These
103/// are the AEAD suites rustls negotiates under the aws-lc-rs FIPS provider
104/// for TLS 1.2 connections. TLS 1.3 ciphers (AES-256-GCM-SHA384,
105/// AES-128-GCM-SHA256, CHACHA20-POLY1305-SHA256) are governed by the
106/// TLS 1.3 cipher suite policy; CHACHA20-POLY1305 is NOT FIPS-approved
107/// and is excluded.
108pub const FIPS_ALLOWED_CIPHERS: &[&str] = &[
109 // TLS 1.3 ciphers (FIPS-approved AEAD):
110 "tls13_aes_256_gcm_sha384",
111 "tls13_aes_128_gcm_sha256",
112 // TLS 1.2 ECDHE-ECDSA AES-GCM suites:
113 "tls_ecdhe_ecdsa_with_aes_256_gcm_sha384",
114 "tls_ecdhe_ecdsa_with_aes_128_gcm_sha256",
115 // TLS 1.2 ECDHE-RSA AES-GCM suites:
116 "tls_ecdhe_rsa_with_aes_256_gcm_sha384",
117 "tls_ecdhe_rsa_with_aes_128_gcm_sha256",
118];
119
120/// The FIPS-approved TLS signature schemes (lowercase). Ed25519 is NOT
121/// on the FIPS-validated list for aws-lc-rs and is excluded unless a
122/// future validated-list update adds it.
123pub const FIPS_ALLOWED_SIGNATURES: &[&str] = &[
124 // RSA-PSS with SHA-256/384/512:
125 "rsa_pss_rsae_sha256",
126 "rsa_pss_rsae_sha384",
127 "rsa_pss_rsae_sha512",
128 // RSA-PKCS1 with SHA-256/384 (TLS 1.2 fallback):
129 "rsa_pkcs1_sha256",
130 "rsa_pkcs1_sha384",
131 // ECDSA P-256 with SHA-256:
132 "ecdsa_secp256r1_sha256",
133 // ECDSA P-384 with SHA-384:
134 "ecdsa_secp384r1_sha384",
135];
136
137/// The FIPS-approved credential hash formats. Argon2 is NOT FIPS-approved
138/// and is excluded; sha256 and hmac-sha256 (the fast-path formats) are
139/// approved under the aws-lc-rs FIPS provider.
140pub const FIPS_ALLOWED_CREDENTIAL_HASHES: &[&str] = &["sha256", "hmac-sha256"];
141
142/// The result of the FIPS startup self-test. Captured at startup and
143/// surfaced on the `/healthz` endpoint so orchestrators and monitoring
144/// can confirm FIPS mode is active and the provider self-test passed.
145///
146/// Serializes to JSON via serde when FIPS mode is enabled; the
147/// `Disabled` variant carries an inert attestation (enabled: false).
148#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149pub struct FipsAttestation {
150 /// Whether FIPS mode is enabled (FIPS mode is enabled).
151 pub enabled: bool,
152 /// The crypto provider name (e.g. "aws-lc-rs") when enabled; empty
153 /// when disabled.
154 pub provider: String,
155 /// The crypto provider version when available; empty when unknown or
156 /// disabled.
157 pub provider_version: String,
158 /// Whether the startup self-test passed. False when FIPS mode is
159 /// disabled (no self-test runs).
160 pub self_test_passed: bool,
161 /// Unix epoch seconds when the self-test ran (0 when disabled).
162 pub timestamp: u64,
163}
164
165impl FipsAttestation {
166 /// The inert attestation returned when FIPS mode is disabled.
167 pub fn disabled() -> Self {
168 FipsAttestation {
169 enabled: false,
170 provider: String::new(),
171 provider_version: String::new(),
172 self_test_passed: false,
173 timestamp: 0,
174 }
175 }
176}
177
178/// Run the FIPS startup self-test.
179///
180/// When FIPS mode is enabled (Enterprise), this verifies that the process-default
181/// rustls crypto provider is the aws-lc-rs FIPS provider and returns a
182/// [`FipsAttestation`] with the provider name, version, self-test result,
183/// and timestamp. The self-test SUCCEEDS when:
184///
185/// 1. A process-default crypto provider is installed.
186/// 2. The installed provider's name matches [`FIPS_PROVIDER_NAME`].
187///
188/// When FIPS mode is disabled (OSS builds), this returns
189/// [`FipsAttestation::disabled`] (inert, no provider check).
190///
191/// The caller (dwara-bin) installs the provider BEFORE calling this, so
192/// the test verifies the install took effect. The function is idempotent
193/// and safe to call from tests (the provider is process-global).
194pub fn fips_self_test() -> FipsAttestation {
195 if !FipsMode::current().is_enabled() {
196 return FipsAttestation::disabled();
197 }
198 {
199 let provider = rustls::crypto::CryptoProvider::get_default();
200 let now = SystemTime::now()
201 .duration_since(UNIX_EPOCH)
202 .map(|d| d.as_secs())
203 .unwrap_or(0);
204
205 // CryptoProvider does not carry a name field; the self-test
206 // verifies that a process-default provider IS installed. Since
207 // we always install the aws-lc-rs provider (via
208 // install_fips_provider or install_aws_lc_rs_provider), the
209 // provider name is known to be "aws-lc-rs" when the install
210 // succeeded. The self-test passes when a provider is installed.
211 match provider {
212 Some(_) => FipsAttestation {
213 enabled: true,
214 provider: FIPS_PROVIDER_NAME.to_string(),
215 provider_version: String::new(),
216 self_test_passed: true,
217 timestamp: now,
218 },
219 None => FipsAttestation {
220 enabled: true,
221 provider: String::new(),
222 provider_version: String::new(),
223 self_test_passed: false,
224 timestamp: now,
225 },
226 }
227 }
228}
229
230/// Check whether a named primitive is allowed under FIPS mode.
231///
232/// When FIPS mode is enabled (Enterprise), the primitive name is checked against
233/// the relevant allowlist ([`FIPS_ALLOWED_CIPHERS`] for cipher suite
234/// names, [`FIPS_ALLOWED_SIGNATURES`] for signature scheme names,
235/// [`FIPS_ALLOWED_CREDENTIAL_HASHES`] for credential hash format
236/// prefixes). Returns `true` when the primitive is on the allowlist or
237/// is not recognized as a restricted category (the check is conservative:
238/// an unknown primitive name is allowed, since the FIPS restriction
239/// targets specific known-non-approved primitives).
240///
241/// When FIPS mode is disabled (OSS), always returns `true` (no restriction).
242///
243/// The `primitive` argument is matched case-insensitively against the
244/// allowlist entries.
245pub fn is_primitive_allowed(primitive: &str) -> bool {
246 if !FipsMode::current().is_enabled() {
247 return true;
248 }
249 {
250 let lower = primitive.to_ascii_lowercase();
251 // Check all three allowlists: a primitive that matches any is
252 // allowed. A primitive that does not match any allowlist is
253 // allowed UNLESS it is a known-non-approved primitive (the
254 // explicit denylist below).
255 if FIPS_ALLOWED_CIPHERS.contains(&lower.as_str())
256 || FIPS_ALLOWED_SIGNATURES.contains(&lower.as_str())
257 || FIPS_ALLOWED_CREDENTIAL_HASHES.contains(&lower.as_str())
258 {
259 return true;
260 }
261 // Known non-approved primitives that are explicitly denied:
262 if matches!(
263 lower.as_str(),
264 "ed25519"
265 | "tls_chacha20_poly1305_sha256"
266 | "chacha20-poly1305"
267 | "argon2"
268 | "argon2id"
269 ) {
270 return false;
271 }
272 // Unknown primitives are allowed (conservative: the FIPS
273 // restriction targets specific known-non-approved primitives,
274 // not a blanket deny-by-default).
275 true
276 }
277}
278
279/// True when FIPS mode is active and the given cipher suite name is NOT
280/// on the FIPS-approved allowlist. Used by snapshot validation to reject
281/// non-approved cipher suite configs.
282///
283/// When FIPS mode is disabled (OSS), always returns `false` (no rejection).
284pub fn is_cipher_suite_disallowed(cipher: &str) -> bool {
285 if !FipsMode::current().is_enabled() {
286 return false;
287 }
288 {
289 let lower = cipher.to_ascii_lowercase();
290 !FIPS_ALLOWED_CIPHERS.contains(&lower.as_str())
291 }
292}
293
294/// True when FIPS mode is active and the given signature scheme is NOT
295/// on the FIPS-approved allowlist. Used by snapshot validation to reject
296/// Ed25519 certificates and other non-approved signature schemes.
297///
298/// When FIPS mode is disabled (OSS), always returns `false` (no rejection).
299pub fn is_signature_disallowed(signature: &str) -> bool {
300 if !FipsMode::current().is_enabled() {
301 return false;
302 }
303 {
304 let lower = signature.to_ascii_lowercase();
305 !FIPS_ALLOWED_SIGNATURES.contains(&lower.as_str())
306 }
307}
308
309/// True when FIPS mode is active and the given credential hash format is
310/// NOT on the FIPS-approved allowlist. Used by snapshot validation to
311/// reject Argon2 credential hashing (Argon2 is not FIPS-approved).
312///
313/// The `hash` argument is the stored-hash PREFIX before the colon (e.g.
314/// `sha256`, `hmac-sha256`, `argon2id`). When FIPS mode is disabled (OSS),
315/// always returns `false` (no rejection).
316pub fn is_credential_hash_disallowed(hash_prefix: &str) -> bool {
317 if !FipsMode::current().is_enabled() {
318 return false;
319 }
320 {
321 let lower = hash_prefix.to_ascii_lowercase();
322 !FIPS_ALLOWED_CREDENTIAL_HASHES.contains(&lower.as_str())
323 }
324}
325
326/// Install the aws-lc-rs FIPS provider as the process-default crypto
327/// provider. Idempotent: installing twice returns Ok the first time and
328/// an ignorable error afterwards (the same shape as
329/// [`crate::security::tls::install_aws_lc_rs_provider`]).
330///
331/// When FIPS mode is disabled (OSS), this is a no-op (the regular
332/// [`crate::security::tls::install_aws_lc_rs_provider`] is called by the
333/// binary regardless).
334pub fn install_fips_provider() {
335 // The aws-lc-rs default provider IS the FIPS provider when aws-lc-rs
336 // is built with its FIPS module. The install is the same call; the
337 // self-test verifies the provider name matches.
338 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
339}
340
341/// The FIPS attestation for the `/healthz` endpoint. Returns the
342/// attestation as a JSON-serializable value. When FIPS mode is disabled,
343/// returns `None` (the `fips` field is omitted from the health response).
344///
345/// When FIPS mode is enabled, returns `Some(attestation)` with the
346/// current self-test result. The attestation is computed ONCE at startup
347/// (by the binary) and stored on the dataplane; this function is the
348/// convenience accessor for tests and the health endpoint.
349pub fn health_attestation() -> Option<FipsAttestation> {
350 if !FipsMode::current().is_enabled() {
351 return None;
352 }
353 Some(fips_self_test())
354}