Skip to main content

toolkit/bootstrap/
crypto.rs

1use std::sync::OnceLock;
2
3/// Error returned when the crypto provider cannot be installed.
4// `Clone` required by `OnceLock<Result<_>>` cache in `init_crypto_provider` --
5// the cached result is cloned on every call.
6// `PartialEq`/`Eq` used by tests asserting the cached result is stable.
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8#[non_exhaustive]
9pub enum CryptoProviderError {
10    /// Another crypto provider was already installed (FIPS mode).
11    #[error("failed to install FIPS crypto provider - another provider is already installed")]
12    FipsProviderConflict,
13    /// Windows is not in FIPS mode — the system-wide Group Policy
14    /// `FipsAlgorithmPolicy` is not enabled, so CNG would not enforce the
15    /// FIPS-Approved algorithm subset at runtime. Fail-closed: refuse to
16    /// start rather than silently degrade the FIPS claim.
17    #[cfg(target_os = "windows")]
18    #[error(
19        "Windows is not in FIPS mode (HKLM\\System\\CurrentControlSet\\Control\\Lsa\\FipsAlgorithmPolicy != 1). \
20         Enable system-wide FIPS via Group Policy (Computer Configuration > Windows Settings > \
21         Security Settings > Local Policies > Security Options > 'System cryptography: Use FIPS \
22         compliant algorithms') and reboot before launching this service."
23    )]
24    SystemFipsModeNotEnabled,
25}
26
27static INIT_RESULT: OnceLock<Result<(), CryptoProviderError>> = OnceLock::new();
28
29/// Install the process-wide default rustls [`CryptoProvider`](rustls::crypto::CryptoProvider).
30///
31/// Dispatch:
32///
33/// - **`fips` feature + macOS**: installs the Apple corecrypto-backed provider
34///   from `rustls-corecrypto-provider`, **restricted to TLS 1.3 cipher
35///   suites** via `fips_provider()`. corecrypto is shipped inside macOS and
36///   validated by Apple under FIPS 140-3 per OS release; see
37///   <https://csrc.nist.gov/projects/cryptographic-module-validation-program>
38///   for the cert matching the running macOS version. TLS 1.2 cipher suites
39///   are excluded under `fips` because Apple does not expose a separately
40///   CAVS-validated TLS PRF primitive (unlike aws-lc-fips on Linux); see
41///   the `cf-gears-rustls-corecrypto-provider` README "FIPS claim boundaries"
42///   section. On macOS the `rustls/fips` feature is not activated (see
43///   `rustls-fips-shim`), so the AWS-LC FIPS dylib is not linked.
44/// - **`fips` feature + Windows**: installs the Windows CNG-backed provider
45///   from `rustls-cng-crypto`'s `fips_provider()`. CNG is shipped inside
46///   Windows and validated by Microsoft under FIPS 140-3 per OS release.
47///   Requires Windows to be in system-wide FIPS mode
48///   (`HKLM\System\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy = 1`,
49///   set via Group Policy); fails closed with
50///   [`CryptoProviderError::SystemFipsModeNotEnabled`] otherwise. As with
51///   macOS, `rustls/fips` is not activated on this target, so the AWS-LC
52///   FIPS dylib is not linked.
53/// - **`fips` feature + other** (Linux, etc.): installs the FIPS-validated
54///   AWS-LC provider (`aws-lc-fips-sys`, NIST Certificate #4816). The cert's
55///   OE covers Linux but not Darwin/Windows, which is why those targets use
56///   different providers.
57/// - **Standard mode** (no `fips` feature): installs the `aws-lc-rs` provider
58///   explicitly. This is required because both `ring` and `aws-lc-rs` are
59///   compiled into the binary (ring via `pingora-rustls`), and rustls
60///   0.23 panics when it cannot auto-detect a single provider. Conflicts here
61///   are non-fatal: if another provider was installed first, it stays active,
62///   the conflict is logged at `warn!`, and `Ok(())` is returned.
63///
64/// This **must** be called before any TLS configuration, HTTP client, database
65/// connection, or JWT operation is created.
66///
67/// Safe to call multiple times -- only the first invocation has an effect;
68/// subsequent calls return the cached first-call result.
69///
70/// # FIPS-claim caveats
71///
72/// On the resulting provider, `provider.fips() == true` is a **runtime
73/// witness** under the witness-pattern rework — it is `true` only when both
74/// (a) every primitive routes through a CMVP-validated module *and* (b)
75/// the runtime OE check agrees. It is no longer an unconditional design-
76/// intent claim.
77///
78/// **macOS**: the corecrypto crate runs an OE check at first provider
79/// construction (`cf_gears_rustls_corecrypto_provider::oe::fips_witness_ok`).
80/// On a macOS major outside the active corecrypto CMVP cert OE, **every**
81/// `fips()` impl in the provider returns `false` and a single
82/// `tracing::warn!` is emitted. The post-install witness `assert!` then
83/// **panics** — a misconfigured FIPS build must never silently proceed.
84/// Use `CF_GEARS_FIPS_OE_OVERRIDE=1` to force the witness to `true`
85/// for CI on pre-release macOS — never for production. See the
86/// `cf-gears-rustls-corecrypto-provider` README "Runtime FIPS witness" section
87/// and FIPS PRD §8.3.
88///
89/// Downstream TLS configuration code (`toolkit_http::tls::apply_fips_hardening`)
90/// performs its own `config.fips()` check and returns `Err` rather than
91/// panicking — `ClientConfig::fips()` depends on per-config settings
92/// (`require_ems`, protocol versions) beyond the provider itself, so the
93/// TLS-layer check is a defence-in-depth complement to the bootstrap
94/// assertion here.
95///
96/// **Linux / Windows**: runtime OE-validation is not yet implemented; OE
97/// coverage is verified via the release checklist (manual CMVP cert search,
98/// PRD §9.3). Tracked as a follow-up in PRD §10.
99///
100/// # Errors
101///
102/// - [`CryptoProviderError::FipsProviderConflict`] if the `fips` feature is
103///   enabled and another rustls provider was installed first.
104/// - [`CryptoProviderError::SystemFipsModeNotEnabled`] on Windows+`fips` when
105///   the OS is not in system-wide FIPS mode.
106///
107/// # Panics
108///
109/// Panics (via `assert!`) under `--features fips` if the installed crypto
110/// provider does not report `fips() == true`. This is an intentional
111/// fail-closed guard against misconfigured builds (wrong feature flags,
112/// wrong OS, or macOS OE mismatch without the override env-var).
113pub fn init_crypto_provider() -> Result<(), CryptoProviderError> {
114    INIT_RESULT
115        .get_or_init(|| {
116            #[cfg(all(feature = "fips", target_os = "macos"))]
117            {
118                // Under toolkit's `fips` feature the dependency tree
119                // activates `rustls-corecrypto-provider/fips`, which
120                // routes `default_provider()` to the TLS-1.3-only FIPS-
121                // claim variant — same pattern as `rustls-cng-crypto`'s
122                // feature flag. We therefore install the unified entry
123                // point (no need to remember which factory under which
124                // build profile).
125                if let Err(prev) = rustls_corecrypto_provider::default_provider().install_default()
126                {
127                    tracing::error!(
128                        previous_provider = ?prev,
129                        "FIPS crypto provider conflict: another rustls provider was already installed"
130                    );
131                    return Err(CryptoProviderError::FipsProviderConflict);
132                }
133                tracing::info!("FIPS-140-3 crypto provider installed (Apple corecrypto, macOS, TLS 1.3-only)");
134            }
135
136            #[cfg(all(feature = "fips", target_os = "windows"))]
137            {
138                // Fail-closed: when Windows is not in system-wide FIPS mode,
139                // `rustls_cng_crypto::fips_provider()` returns a CryptoProvider
140                // with empty `cipher_suites` / `kx_groups` (rustls's per-suite
141                // `.fips()` flag is the gate; in non-FIPS-mode Windows none
142                // qualify). The upstream crate's `fips::enabled()` helper is
143                // `pub(crate)`, so we detect the same condition via the
144                // documented empty-provider shape rather than poking
145                // `BCryptGetFipsAlgorithmMode` ourselves. Refuse to install
146                // rather than degrade the FIPS claim with a non-handshakeable
147                // provider.
148                let provider = rustls_cng_crypto::fips_provider();
149                if provider.cipher_suites.is_empty() {
150                    tracing::error!(
151                        "Windows FIPS mode not enabled (FipsAlgorithmPolicy != 1); \
152                         rustls-cng-crypto returned an empty FIPS provider"
153                    );
154                    return Err(CryptoProviderError::SystemFipsModeNotEnabled);
155                }
156                if let Err(prev) = provider.install_default() {
157                    tracing::error!(
158                        previous_provider = ?prev,
159                        "FIPS crypto provider conflict: another rustls provider was already installed"
160                    );
161                    return Err(CryptoProviderError::FipsProviderConflict);
162                }
163                tracing::info!("FIPS-140-3 crypto provider installed (Windows CNG)");
164            }
165
166            #[cfg(all(feature = "fips", not(any(target_os = "macos", target_os = "windows"))))]
167            {
168                if let Err(prev) = rustls::crypto::default_fips_provider().install_default() {
169                    tracing::error!(
170                        previous_provider = ?prev,
171                        "FIPS crypto provider conflict: another rustls provider was already installed"
172                    );
173                    return Err(CryptoProviderError::FipsProviderConflict);
174                }
175                tracing::info!("FIPS-140-3 crypto provider installed (AWS-LC FIPS module)");
176            }
177
178            #[cfg(feature = "fips")]
179            {
180                let Some(provider) = rustls::crypto::CryptoProvider::get_default() else {
181                    unreachable!("provider must be installed at this point");
182                };
183                assert!(
184                    provider.fips(),
185                    "FIPS post-install witness failed: installed provider does not report fips()==true"
186                );
187                tracing::info!("FIPS post-install witness: OK");
188            }
189
190            #[cfg(not(feature = "fips"))]
191            {
192                if let Err(prev) = rustls::crypto::aws_lc_rs::default_provider().install_default() {
193                    // Non-fatal: another provider is already active, TLS still works.
194                    tracing::warn!(
195                        previous_provider = ?prev,
196                        "aws-lc-rs crypto provider not installed: another default provider was already set"
197                    );
198                } else {
199                    tracing::info!("aws-lc-rs crypto provider installed");
200                }
201            }
202
203            Ok(())
204        })
205        .clone()
206}