Skip to main content

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}