Skip to content

Commit 70d1953

Browse files
authored
Merge pull request #1986 from mattgarmon/fips
feat(modkit): explicit post-install FIPS provider assertion
2 parents ce1858e + 543739c commit 70d1953

3 files changed

Lines changed: 49 additions & 26 deletions

File tree

docs/security/SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ See [`examples/cyberware-fips-probe/README.md`](../../examples/cyberware-fips-pr
470470

471471
- **Cyber Ware itself is not on the CMVP Validated Modules list.** The validated modules are Apple corecrypto, AWS-LC FIPS Provider, and Microsoft Windows CNG. Cyber Ware is a *consumer* of those modules.
472472
- **CMVP OE-coverage is the deployment's responsibility.** A FIPS claim is void if the running OS version is not inside the cert's OE. The macOS runtime gate is fail-closed; Linux + Windows OE coverage is verified manually per release.
473-
- **`CryptoProvider::fips() = true` is design intent, not a runtime witness.** It asserts that every primitive in the provider routes through a CMVP-validated module by construction. It does *not* prove that the running OS version matches an active CMVP cert OE — that proof comes from the §release-checklist CMVP-cert search.
473+
- **`CryptoProvider::fips() = true` is a runtime witness, not just design intent.** On macOS it reflects the OE check (`oe::fips_witness_ok`); on Windows, the OS FIPS-mode flag. On Linux, runtime OE-validation is not yet implemented; OE coverage is verified manually per release via the §release-checklist CMVP-cert search.
474474
- **TLS 1.2 PRF on macOS is not CAVS-listed.** Apple corecrypto exposes generic HMAC primitives but not a CAVS-listed dedicated TLS PRF (unlike `aws-lc-fips`'s `tls_prf::Algorithm`). Consequence: `fips_provider()` on macOS is TLS-1.3-only; customers requiring TLS 1.2 on macOS+FIPS must accept that those connections do not carry a FIPS claim.
475475
- **JWT signature validation does not go through the FIPS path.** `jsonwebtoken` uses `ring` / non-FIPS `aws-lc-rs` for RSA / ECDSA verification on bearer tokens. Treat tokens as authentication context, not as data covered by the cryptographic claim. Out of scope today; tracked as **TODO-7** in [FIPS PRD §13](fips/PRD.md#13-open-questions). Cleanup is gated by `deny-fips.toml` Phase B promotion.
476476
- **Non-FIPS crypto crates remain in the final binary on macOS+fips.** `ring` is pulled in transitively by `pingora-rustls`, `pingora-pool`, and `ureq`; non-FIPS `aws-lc-rs` is pulled in by rustls's default feature set; `chacha20` is pulled in by the `rand` ecosystem. These are **not invoked** on the TLS data plane (the installed `CryptoProvider` routes every TLS primitive through the validated module) but the symbols are linked into the binary. Linkage smoke (above) confirms no non-validated shared libraries appear at runtime.

libs/modkit-http/src/tls.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,11 @@ pub enum TlsConfigError {
161161

162162
/// `apply_fips_hardening` rejected the freshly-built `ClientConfig`
163163
/// because `ClientConfig::fips()` reported `false`. Carries the
164-
/// human-readable diagnostic — typically a witness mismatch
165-
/// (`cyberware_rustls_corecrypto_provider::oe::fips_witness_ok` on macOS)
166-
/// or a missed `init_crypto_provider()` call.
164+
/// human-readable diagnostic. Under normal bootstrap flow the
165+
/// provider-level witness is already asserted by
166+
/// `init_crypto_provider`; this error surfaces per-config issues
167+
/// (e.g. missing `require_ems`) or a missed `init_crypto_provider()`
168+
/// call.
167169
#[error("{0}")]
168170
FipsHardeningFailed(String),
169171

@@ -210,10 +212,10 @@ mod fips_test_provider {
210212
/// 1. forces `require_ems = true` (NIST SP 800-52 Rev. 2 §3.5), and
211213
/// 2. verifies `config.fips() == true` and returns `Err` otherwise.
212214
///
213-
/// Returning `Err` rather than `panic!` matches the witness contract of
214-
/// `cyberware-rustls-corecrypto-provider`: an OE mismatch surfaces as a
215-
/// `fips() == false` witness, and that surfaces here as a recoverable
216-
/// error rather than process termination.
215+
/// The bootstrap `assert!` in `init_crypto_provider` is the primary
216+
/// guard against a non-FIPS provider; this TLS-layer check is
217+
/// defence-in-depth for per-config settings (`require_ems`, protocol
218+
/// versions) that also affect `ClientConfig::fips()`.
217219
fn build_client_config(
218220
root_store: rustls::RootCertStore,
219221
) -> Result<rustls::ClientConfig, TlsConfigError> {
@@ -265,24 +267,22 @@ fn build_client_config(
265267
/// `ClientConfig::fips()` to consider TLS 1.2 sessions FIPS-compliant
266268
/// when the EMS extension is honoured by the peer).
267269
/// * verify the full FIPS chain reports `fips() == true`. If not, return
268-
/// `Err` rather than panic — the failure mode is recoverable from the
269-
/// caller's perspective (build a non-FIPS client, surface the error,
270-
/// etc.). Panic was the previous behaviour; replaced because the
271-
/// witness rework in `cyberware-rustls-corecrypto-provider` makes
272-
/// `config.fips() == false` a normal runtime state on unsupported
273-
/// macOS majors, not a programming error.
270+
/// `Err` — the bootstrap `assert!` in `init_crypto_provider` already
271+
/// guarantees the provider reports `fips() == true`, so a failure here
272+
/// indicates a per-config issue (e.g. missing `require_ems` or
273+
/// restricted protocol versions) rather than a provider-level problem.
274274
#[cfg(feature = "fips")]
275275
fn apply_fips_hardening(cfg: &mut rustls::ClientConfig) -> Result<(), TlsConfigError> {
276276
cfg.require_ems = true;
277277
if !cfg.fips() {
278278
return Err(TlsConfigError::FipsHardeningFailed(
279279
"TLS ClientConfig does not advertise FIPS after enabling require_ems. \
280-
The runtime FIPS witness \
281-
(cyberware_rustls_corecrypto_provider::oe::fips_witness_ok on macOS) \
282-
is reporting false -- typically because the running macOS major is \
283-
outside the active corecrypto CMVP cert OE. \
284-
Set CYBERWARE_FIPS_OE_OVERRIDE=1 (CI / pre-release only) to force the \
285-
witness to true; never set this in production."
280+
The bootstrap assert in init_crypto_provider should have caught a \
281+
non-FIPS provider at startup; if you see this error the provider is \
282+
FIPS-OK but a per-config setting (protocol versions, require_ems) is \
283+
preventing ClientConfig::fips() from reporting true. \
284+
If init_crypto_provider was not called, call it before building any \
285+
TLS config."
286286
.to_owned(),
287287
));
288288
}

libs/modkit/src/bootstrap/crypto.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,20 @@ static INIT_RESULT: OnceLock<Result<(), CryptoProviderError>> = OnceLock::new();
7979
/// construction (`cyberware_rustls_corecrypto_provider::oe::fips_witness_ok`).
8080
/// On a macOS major outside the active corecrypto CMVP cert OE, **every**
8181
/// `fips()` impl in the provider returns `false` and a single
82-
/// `tracing::warn!` is emitted. There is **no panic** — downstream code
83-
/// that depends on `ClientConfig::fips()` / `ServerConfig::fips()` must
84-
/// handle the `false` case explicitly (see
85-
/// `modkit_http::tls::apply_fips_hardening` for the canonical pattern,
86-
/// which returns `Err` instead of asserting). The
87-
/// `CYBERWARE_FIPS_OE_OVERRIDE=1` env-var forces the witness to `true`
82+
/// `tracing::warn!` is emitted. The post-install witness `assert!` then
83+
/// **panics** — a misconfigured FIPS build must never silently proceed.
84+
/// Use `CYBERWARE_FIPS_OE_OVERRIDE=1` to force the witness to `true`
8885
/// for CI on pre-release macOS — never for production. See the
8986
/// `cyberware-rustls-corecrypto-provider` README "Runtime FIPS witness" section
9087
/// and FIPS PRD §8.3.
9188
///
89+
/// Downstream TLS configuration code (`modkit_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+
///
9296
/// **Linux / Windows**: runtime OE-validation is not yet implemented; OE
9397
/// coverage is verified via the release checklist (manual CMVP cert search,
9498
/// PRD §9.3). Tracked as a follow-up in PRD §10.
@@ -99,6 +103,13 @@ static INIT_RESULT: OnceLock<Result<(), CryptoProviderError>> = OnceLock::new();
99103
/// enabled and another rustls provider was installed first.
100104
/// - [`CryptoProviderError::SystemFipsModeNotEnabled`] on Windows+`fips` when
101105
/// 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).
102113
pub fn init_crypto_provider() -> Result<(), CryptoProviderError> {
103114
INIT_RESULT
104115
.get_or_init(|| {
@@ -164,6 +175,18 @@ pub fn init_crypto_provider() -> Result<(), CryptoProviderError> {
164175
tracing::info!("FIPS-140-3 crypto provider installed (AWS-LC FIPS module)");
165176
}
166177

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+
167190
#[cfg(not(feature = "fips"))]
168191
{
169192
if let Err(prev) = rustls::crypto::aws_lc_rs::default_provider().install_default() {

0 commit comments

Comments
 (0)