ic_fips/lib.rs
1//! # ic-fips — the FIPS 140-3 module boundary
2//!
3//! ## What this is, and what it is not
4//!
5//! This crate implements the *discipline* FIPS 140-3 asks for: a defined module
6//! boundary, pre-operational self-tests, per-algorithm known-answer tests
7//! (CASTs), a latching error state, an approved mode of operation that refuses
8//! unapproved algorithms, and a service indicator that reports whether each
9//! call was approved.
10//!
11//! **It is not a validated module.** IronCrypto holds no CMVP certificate,
12//! and running [`initialize`] does not create one. Validation is a laboratory
13//! process against a specific binary on specific platforms. What this crate
14//! gives you is a codebase that is *shaped* for that process, and a runtime
15//! that tells the truth about its own status — see
16//! [`ic_ontology::runtime::has`] with `"fips-validated"`, which returns `false`
17//! and will keep returning `false` until a certificate exists.
18//!
19//! Claiming otherwise to an auditor, a customer, or an agent would be a
20//! misrepresentation, so every surface here is written to make the distinction
21//! impossible to miss.
22//!
23//! ## Using it
24//!
25//! ```
26//! use ic_fips::{initialize, Mode, ServiceIndicator};
27//!
28//! // Runs every known-answer test. Refuses service if any of them fail.
29//! initialize()?;
30//! ic_fips::set_mode(Mode::Approved)?;
31//!
32//! // Approved: AES-256-GCM is an approved security function.
33//! assert_eq!(ic_fips::check("aes-256-gcm")?, ServiceIndicator::Approved);
34//!
35//! // Refused: ChaCha20-Poly1305 is not approved, so approved mode blocks it
36//! // rather than letting it through with a warning.
37//! assert!(ic_fips::check("chacha20-poly1305").is_err());
38//! # Ok::<(), ic_core::Error>(())
39//! ```
40#![cfg_attr(not(feature = "std"), no_std)]
41#![forbid(unsafe_code)]
42#![deny(missing_docs)]
43#![warn(clippy::all)]
44
45use core::sync::atomic::{AtomicU8, Ordering};
46
47use ic_core::{ensure, Error, ErrorKind, Result};
48use ic_ontology::{FipsStatus, ImplStatus};
49
50pub mod selftest;
51
52pub use selftest::{run_all_self_tests, SelfTestReport, TestOutcome};
53
54// ---------------------------------------------------------------------------
55// Module state machine
56// ---------------------------------------------------------------------------
57
58const STATE_UNINITIALIZED: u8 = 0;
59const STATE_TESTING: u8 = 1;
60const STATE_OPERATIONAL_UNRESTRICTED: u8 = 2;
61const STATE_OPERATIONAL_APPROVED: u8 = 3;
62const STATE_ERROR: u8 = 4;
63
64static STATE: AtomicU8 = AtomicU8::new(STATE_UNINITIALIZED);
65
66/// The module's operating mode.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Mode {
69 /// Only algorithms the ontology marks as permitted in approved mode may be
70 /// used. Everything else is refused.
71 Approved,
72 /// Every implemented algorithm is available.
73 Unrestricted,
74}
75
76impl Mode {
77 /// Stable identifier used in CLI and MCP output.
78 pub const fn id(self) -> &'static str {
79 match self {
80 Self::Approved => "approved",
81 Self::Unrestricted => "unrestricted",
82 }
83 }
84}
85
86/// The module's lifecycle state.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum State {
89 /// Self-tests have not run; no cryptographic service is available.
90 Uninitialized,
91 /// Self-tests are running.
92 SelfTestInProgress,
93 /// Operating normally in the given mode.
94 Operational(Mode),
95 /// A self-test failed. The module refuses all service until the process
96 /// restarts; FIPS 140-3 requires the error state to latch.
97 Error,
98}
99
100impl State {
101 /// Stable identifier used in CLI and MCP output.
102 pub const fn id(self) -> &'static str {
103 match self {
104 Self::Uninitialized => "uninitialized",
105 Self::SelfTestInProgress => "self-test-in-progress",
106 Self::Operational(Mode::Approved) => "operational-approved",
107 Self::Operational(Mode::Unrestricted) => "operational-unrestricted",
108 Self::Error => "error",
109 }
110 }
111}
112
113fn decode(raw: u8) -> State {
114 match raw {
115 STATE_TESTING => State::SelfTestInProgress,
116 STATE_OPERATIONAL_UNRESTRICTED => State::Operational(Mode::Unrestricted),
117 STATE_OPERATIONAL_APPROVED => State::Operational(Mode::Approved),
118 STATE_ERROR => State::Error,
119 _ => State::Uninitialized,
120 }
121}
122
123/// The module's current state.
124pub fn state() -> State {
125 decode(STATE.load(Ordering::SeqCst))
126}
127
128/// The current mode, or `None` when the module is not operational.
129pub fn mode() -> Option<Mode> {
130 match state() {
131 State::Operational(m) => Some(m),
132 _ => None,
133 }
134}
135
136/// Run the pre-operational self-tests and bring the module up.
137///
138/// Idempotent: calling it again once operational is a no-op that preserves the
139/// current mode. If any known-answer test fails, the module latches into
140/// [`State::Error`] and every subsequent call fails with
141/// [`ErrorKind::ModuleErrorState`].
142///
143/// The module comes up [`Mode::Unrestricted`]; call [`set_mode`] to enter
144/// approved mode. That ordering is deliberate — entering approved mode is a
145/// decision the operator makes explicitly, never a default the caller might not
146/// have noticed.
147pub fn initialize() -> Result<SelfTestReport> {
148 match state() {
149 State::Error => {
150 return Err(Error::new(
151 ErrorKind::ModuleErrorState,
152 "module in error state",
153 ))
154 }
155 State::Operational(_) => return Ok(run_all_self_tests()),
156 _ => {}
157 }
158
159 STATE.store(STATE_TESTING, Ordering::SeqCst);
160 let report = run_all_self_tests();
161
162 if report.failed > 0 {
163 STATE.store(STATE_ERROR, Ordering::SeqCst);
164 return Err(Error::new(
165 ErrorKind::SelfTestFailed,
166 "pre-operational self-test failed; module latched in error state",
167 ));
168 }
169
170 STATE.store(STATE_OPERATIONAL_UNRESTRICTED, Ordering::SeqCst);
171 Ok(report)
172}
173
174/// Switch the operating mode.
175///
176/// Requires the module to be operational; returns
177/// [`ErrorKind::ModuleErrorState`] otherwise.
178pub fn set_mode(new_mode: Mode) -> Result<()> {
179 match state() {
180 State::Operational(_) => {
181 STATE.store(
182 match new_mode {
183 Mode::Approved => STATE_OPERATIONAL_APPROVED,
184 Mode::Unrestricted => STATE_OPERATIONAL_UNRESTRICTED,
185 },
186 Ordering::SeqCst,
187 );
188 Ok(())
189 }
190 State::Error => Err(Error::new(
191 ErrorKind::ModuleErrorState,
192 "module in error state",
193 )),
194 _ => Err(Error::new(
195 ErrorKind::ModuleErrorState,
196 "call initialize() before selecting a mode",
197 )),
198 }
199}
200
201/// Force the module into its error state.
202///
203/// Exposed so an application that detects corruption elsewhere can bring the
204/// module down with it. There is no way back short of restarting the process,
205/// by design.
206pub fn enter_error_state() {
207 STATE.store(STATE_ERROR, Ordering::SeqCst);
208}
209
210// ---------------------------------------------------------------------------
211// Service indicator
212// ---------------------------------------------------------------------------
213
214/// FIPS 140-3 requires a module to tell the caller whether the service it just
215/// used was an approved one. This is that indicator.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum ServiceIndicator {
218 /// An approved security function, used in the approved mode.
219 Approved,
220 /// Permitted, but not itself an approved security function — a raw block
221 /// cipher used as a component, for example.
222 ApprovedAsComponent,
223 /// A non-approved algorithm, used outside approved mode.
224 NotApproved,
225}
226
227impl ServiceIndicator {
228 /// Stable identifier used in CLI and MCP output.
229 pub const fn id(self) -> &'static str {
230 match self {
231 Self::Approved => "approved",
232 Self::ApprovedAsComponent => "approved-as-component",
233 Self::NotApproved => "not-approved",
234 }
235 }
236}
237
238/// Check whether `algorithm_id` may be used right now, and report its status.
239///
240/// This is the function every guarded call routes through. It enforces, in
241/// order: the module is operational, the algorithm exists in the ontology, it
242/// is implemented in this build, and — in approved mode — that it is permitted.
243pub fn check(algorithm_id: &str) -> Result<ServiceIndicator> {
244 match state() {
245 State::Operational(_) => {}
246 State::Error => {
247 return Err(Error::new(
248 ErrorKind::ModuleErrorState,
249 "module in error state",
250 ))
251 }
252 _ => {
253 return Err(Error::new(
254 ErrorKind::ModuleErrorState,
255 "module not initialized; call ic_fips::initialize()",
256 ))
257 }
258 }
259
260 let entry = ic_ontology::get(algorithm_id)
261 .ok_or(Error::new(ErrorKind::Unsupported, "unknown algorithm"))?;
262
263 ensure!(
264 entry.status == ImplStatus::Available,
265 Unsupported,
266 "algorithm is described by the ontology but not implemented in this build"
267 );
268
269 let approved_mode = mode() == Some(Mode::Approved);
270 if approved_mode && !entry.fips.permitted_in_approved_mode() {
271 return Err(Error::new(
272 ErrorKind::NotApprovedInFipsMode,
273 "algorithm is not approved; select an approved alternative or leave approved mode",
274 ));
275 }
276
277 Ok(match entry.fips {
278 FipsStatus::Approved | FipsStatus::Deprecated => ServiceIndicator::Approved,
279 FipsStatus::AllowedAsComponent => ServiceIndicator::ApprovedAsComponent,
280 FipsStatus::NotApproved | FipsStatus::Disallowed => ServiceIndicator::NotApproved,
281 })
282}
283
284/// Run `op` only if `algorithm_id` is permitted, returning the result along
285/// with the service indicator.
286///
287/// ```
288/// use ic_fips::{guarded, initialize};
289/// use ic_core::traits::Digest;
290///
291/// initialize()?;
292/// let (digest, indicator) = guarded("sha2-256", || ic_hash::Sha256::digest(b"data"))?;
293/// assert_eq!(indicator, ic_fips::ServiceIndicator::Approved);
294/// # let _ = digest;
295/// # Ok::<(), ic_core::Error>(())
296/// ```
297pub fn guarded<T, F: FnOnce() -> T>(algorithm_id: &str, op: F) -> Result<(T, ServiceIndicator)> {
298 let indicator = check(algorithm_id)?;
299 Ok((op(), indicator))
300}
301
302/// A statement of this module's validation status, for display to humans and
303/// agents that ask.
304///
305/// Deliberately blunt: an agent reading capability flags should never be able
306/// to conclude that an uncertified module is certified.
307pub const VALIDATION_STATEMENT: &str = "\
308IronCrypto implements the FIPS 140-3 operational discipline (approved-mode \
309policy, pre-operational and conditional self-tests, a latching error state, and \
310service indicators). It has NOT been submitted to or validated by the CMVP, and \
311holds no certificate number. Do not represent it as FIPS validated.";
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 /// The module state is process-global, so the state-machine assertions run
318 /// as one test rather than racing each other across threads.
319 #[test]
320 fn module_lifecycle_and_policy() {
321 assert_eq!(state(), State::Uninitialized);
322
323 // Nothing is permitted before initialization.
324 assert_eq!(
325 check("sha2-256").unwrap_err().kind(),
326 ErrorKind::ModuleErrorState
327 );
328 assert_eq!(
329 set_mode(Mode::Approved).unwrap_err().kind(),
330 ErrorKind::ModuleErrorState
331 );
332
333 let report = initialize().unwrap();
334 assert!(report.passed > 0);
335 assert_eq!(report.failed, 0);
336 assert_eq!(state(), State::Operational(Mode::Unrestricted));
337
338 // Unrestricted mode permits everything implemented.
339 assert_eq!(check("sha2-256").unwrap(), ServiceIndicator::Approved);
340 assert_eq!(
341 check("chacha20-poly1305").unwrap(),
342 ServiceIndicator::NotApproved
343 );
344 assert_eq!(
345 check("aes-256").unwrap(),
346 ServiceIndicator::ApprovedAsComponent
347 );
348
349 // Unknown and unimplemented algorithms are distinguished from policy
350 // refusals.
351 assert_eq!(
352 check("nonsense").unwrap_err().kind(),
353 ErrorKind::Unsupported
354 );
355 // ML-KEM-768 and ML-DSA-65 used to be refused here. They were
356 // FIPS-approved algorithms with working code and the status gated them
357 // anyway, because approval is about the algorithm while availability is
358 // about whether this implementation has been shown to *be* that
359 // algorithm -- and only the second gates use. They are checked against
360 // ACVP vectors now, so the second condition holds and they are accepted.
361 assert!(check("ml-kem-768").is_ok());
362 assert!(check("ml-dsa-65").is_ok());
363
364 // AES-GCM-SIV was the last example of the availability gate: working
365 // code, no published vector, refused whatever its approval status said.
366 // RFC 8452's vectors are in, so it is accepted now -- and returns
367 // NotApproved rather than Approved, which is the other half of the
368 // point. RFC 8452 is an IETF document; approval is NIST's to give.
369 assert_eq!(
370 check("aes-256-gcm-siv").unwrap(),
371 ServiceIndicator::NotApproved
372 );
373
374 // Nothing in the registry is `experimental` any more, so the rule is
375 // shown with an `excluded` entry instead. MD5 is described so that a
376 // request for it gets a reasoned refusal rather than silence, and
377 // describing it must never make it callable.
378 assert_eq!(check("md5").unwrap_err().kind(), ErrorKind::Unsupported);
379 assert_eq!(check("sha-1").unwrap_err().kind(), ErrorKind::Unsupported);
380
381 // Approved mode refuses unapproved algorithms.
382 set_mode(Mode::Approved).unwrap();
383 assert_eq!(mode(), Some(Mode::Approved));
384 assert_eq!(check("aes-256-gcm").unwrap(), ServiceIndicator::Approved);
385 assert_eq!(
386 check("chacha20-poly1305").unwrap_err().kind(),
387 ErrorKind::NotApprovedInFipsMode
388 );
389 assert_eq!(
390 check("ed25519").unwrap_err().kind(),
391 ErrorKind::NotApprovedInFipsMode
392 );
393 assert_eq!(
394 check("x25519").unwrap_err().kind(),
395 ErrorKind::NotApprovedInFipsMode
396 );
397
398 // `guarded` gates the closure on the same policy.
399 let (n, ind) = guarded("sha2-256", || 42).unwrap();
400 assert_eq!(n, 42);
401 assert_eq!(ind, ServiceIndicator::Approved);
402 assert!(guarded("chacha20-poly1305", || 42).is_err());
403
404 // Re-initializing is a no-op that preserves the mode.
405 initialize().unwrap();
406 assert_eq!(mode(), Some(Mode::Approved));
407
408 set_mode(Mode::Unrestricted).unwrap();
409 assert!(check("chacha20-poly1305").is_ok());
410
411 // The error state latches: nothing works after it, including
412 // initialize() and set_mode().
413 enter_error_state();
414 assert_eq!(state(), State::Error);
415 assert_eq!(
416 check("sha2-256").unwrap_err().kind(),
417 ErrorKind::ModuleErrorState
418 );
419 assert_eq!(
420 initialize().unwrap_err().kind(),
421 ErrorKind::ModuleErrorState
422 );
423 assert_eq!(
424 set_mode(Mode::Unrestricted).unwrap_err().kind(),
425 ErrorKind::ModuleErrorState
426 );
427 assert_eq!(mode(), None);
428 }
429
430 #[test]
431 fn state_identifiers_are_distinct() {
432 let ids = [
433 State::Uninitialized.id(),
434 State::SelfTestInProgress.id(),
435 State::Operational(Mode::Approved).id(),
436 State::Operational(Mode::Unrestricted).id(),
437 State::Error.id(),
438 ];
439 for (i, a) in ids.iter().enumerate() {
440 for b in ids.iter().skip(i + 1) {
441 assert_ne!(a, b);
442 }
443 }
444 }
445
446 #[test]
447 fn validation_statement_denies_certification() {
448 assert!(VALIDATION_STATEMENT.contains("NOT been submitted"));
449 assert!(!ic_ontology::runtime::has("fips-validated"));
450 }
451}