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}