Skip to main content

oxicrypt_module/
lib.rs

1//! FIPS 140-3 Level 1 module boundary.
2//!
3//! This crate defines the **cryptographic module boundary** per
4//! FIPS 140-3 Section 7.2 and the service-gating plumbing every
5//! other crate in the workspace depends on. No algorithm is
6//! permitted to produce output until the power-up self-tests have
7//! run and the module has entered the `Operational` state; on any
8//! self-test failure the module latches into a terminal `Error`
9//! state and rejects every subsequent call for the remainder of
10//! the process lifetime.
11//!
12//! # State machine (FIPS 140-3 §7.2)
13//!
14//! ```text
15//!    ┌──────────┐  initialize_with_tests()  ┌───────────┐  all KATs pass  ┌──────────────┐
16//!    │ PowerOff │───────────────────────────▶│ SelfTest  │────────────────▶│ Operational  │
17//!    └──────────┘                            └───────────┘                 └──────────────┘
18//!                                                   │                              │
19//!                                               KAT failure                conditional self-test
20//!                                                   │                            failure
21//!                                                   ▼                              │
22//!                                              ┌─────────┐◀───────────────────────┘
23//!                                              │  Error  │
24//!                                              └─────────┘
25//! ```
26//!
27//! An empty integrity group moves `SelfTest` to the terminal
28//! `IntegrityUnverified` state instead of `Error`: the module was never
29//! checked, rather than checked and found wrong.
30//!
31//! # What this crate ships
32//!
33//! - [`State`] and the transition machine (`PowerOff →
34//!   SelfTest → Operational | Error | IntegrityUnverified`).
35//! - [`initialize_with_tests`] — the canonical one-shot entry
36//!   point that the top-level caller uses to run the integrity
37//!   test and every approved algorithm's power-up KAT before
38//!   opening the service interface.
39//! - [`require_operational`] — the gate that every approved
40//!   service in every other crate calls on entry. Returns
41//!   [`Error::NotOperational`] (with the observed state) on any
42//!   pre-`Operational` or post-`Error` call.
43//! - [`require_self_test_started`] — the guard that every public
44//!   `*_internal` function calls on entry. It admits `Operational`, and
45//!   `SelfTest` on the thread running the power-up self-tests, so those
46//!   tests can drive the internals, and refuses everything else.
47//! - [`enter_error_state`] — irreversible transition into the
48//!   terminal error state, called by conditional self-tests
49//!   (pairwise consistency, DRBG health) when they detect a
50//!   failure.
51//! - [`KatEntry`] and the [`SelfTest`] trait — the registry
52//!   shape that algorithm crates use to expose their power-up
53//!   KATs.
54//! - [`AlgorithmProfile`] — runtime selection of an algorithm
55//!   restriction policy (Unrestricted, CNSA 2.0, CNSA 1.0,
56//!   Migration).
57//!   Set once at initialization via
58//!   [`initialize_with_profile`].
59//! - [`Service`] — per-algorithm-and-parameter enumeration of
60//!   every approved service in the module. Used with
61//!   [`require_allowed`] to enforce profile restrictions.
62//!
63//! The test registry is **not** assembled by linker-section
64//! tricks: callers pass an explicit `&[KatEntry]` slice. That
65//! means the full set of power-up tests is visible in source at
66//! every call site, which makes it straightforward to audit the
67//! module's power-up inventory against the Security Policy.
68//!
69//! # Algorithm profiles
70//!
71//! A single binary serves general FIPS 140-3 consumers
72//! and CNSA-restricted deployments via a runtime
73//! [`AlgorithmProfile`] selection. The operator passes the
74//! desired profile to [`initialize_with_profile`]; subsequent
75//! calls to [`require_allowed`] enforce it. Services not
76//! permitted by the active profile return
77//! [`Error::AlgorithmRestricted`].
78//!
79//! Four profiles are defined:
80//!
81//! - **Unrestricted** — all FIPS-approved algorithms are
82//!   available. This is the default, matching the behavior of
83//!   [`initialize_with_tests`].
84//! - **CNSA 2.0** — CNSSP-15 (March 2025) Annex B: AES-256,
85//!   SHA-384 or SHA-512, ML-KEM-1024, ML-DSA-87, and the
86//!   SP 800-208 stateful hash-based signatures for software and
87//!   firmware signing, together with the supporting services
88//!   those need at matching strength. Every approved LMS
89//!   parameter set is permitted; Annex B places no subset
90//!   restriction on LMS. No Keccak-family hash or XOF is
91//!   permitted: the suite names none for a software module, and
92//!   the policy states that algorithms using SHA3 as a component
93//!   do not thereby make SHA3 an approved hash function. No
94//!   SLH-DSA parameter set is permitted — CNSSP-15 lists none.
95//! - **CNSA 1.0** — CNSSP-15 (October 2016) Annex B: AES-256,
96//!   ECDH and ECDSA on P-384, SHA-384, and Diffie-Hellman and
97//!   RSA at a minimum 3072-bit modulus, with the supporting
98//!   services those need at matching strength. SHA-256 is
99//!   refused: the suite admits it only under a near-term
100//!   allowance scoped to UNCLASSIFIED data, a condition the
101//!   module cannot evaluate.
102//! - **Migration** — both suites at once, for a deployment
103//!   moving between them. It is **not** a CNSA profile and no
104//!   standard defines it; it is the union of the two above, and
105//!   is narrower than Unrestricted, which refuses nothing.
106//!
107//! **The profiles do not nest.** Neither CNSA suite contains the
108//! other: CNSA 1.0 admits ECDSA, RSA and Diffie-Hellman that
109//! CNSA 2.0 refuses, and CNSA 2.0 admits the post-quantum and
110//! stateful hash-based algorithms that CNSA 1.0 refuses. The two
111//! overlap in AES-256 and SHA-384 and their matching-strength
112//! support, and nowhere else. KATs still run all algorithms
113//! regardless of profile — the profile only restricts
114//! post-initialization service access.
115//!
116//! # Sensitive security parameters (SSPs)
117//!
118//! This crate holds **no SSPs** of its own. It only gates
119//! access to other crates that do. Secret material lives in the
120//! algorithm crates that own it (e.g. `oxicrypt-rsa`'s
121//! `RsaPrivateKey2048`, `oxicrypt-drbg`'s DRBG states). The error
122//! latch here does not perform SSP zeroization on its own — each
123//! owning crate is responsible for ensuring its SSPs are
124//! dropped when the process restarts following a transition into
125//! the `Error` state.
126//!
127//! # FIPS 140-3 / SP 800-140B mapping
128//!
129//! | SP 800-140B / IG clause | Implementation |
130//! |-------------------------|----------------|
131//! | §7.10 power-up self-tests | [`initialize_with_tests`] runs every registered [`KatEntry`] sequentially; the first failure latches [`State::Error`]. |
132//! | §7.10 conditional self-tests | Algorithm crates call [`enter_error_state`] on detecting a pairwise-consistency or DRBG-health failure. |
133//! | IG 9.5.A approved-mode indicator | [`is_operational`] / [`state`] — queryable at runtime. |
134//! | §7.10.2.2 software integrity | Delegated to `oxicrypt-integrity` (separate crate), whose `KATS` the top-level caller runs first: the technique's own CAST, then the integrity test. |
135//!
136//! # Thread safety
137//!
138//! State is stored in a single `AtomicU64` and is safe to read
139//! from any thread. Its low byte is the [`State`]; while the state is
140//! `SelfTest`, the bits above it hold the operating system's id for the
141//! thread running the self-tests, published in the same word so that no
142//! reader sees one without the other. The id comes from
143//! `oxicrypt-imageread`: `gettid` on Linux and Android,
144//! `pthread_threadid_np` on Darwin, `GetCurrentThreadId` on Windows. A
145//! target with none of these cannot own `SelfTest` and does not initialise.
146//! A target without 64-bit atomics (`target_has_atomic = "64"`) cannot
147//! hold the word, and the crate refuses to compile for it.
148//!
149//! [`initialize_with_tests`] uses a compare-and-swap to guarantee the
150//! self-test phase runs exactly once per process lifetime even under
151//! concurrent first-calls. Every self-test runs on the thread that won
152//! it, and [`require_self_test_started`] admits `SelfTest` to that thread
153//! alone, so another thread's `*_internal` call made while initialisation
154//! is running, or paused, is refused. Racing losers wait on the state
155//! word, so they do not observe the transient phase, and then receive
156//! [`Error::AlreadyInitialized`] or [`Error::AlreadyFailed`]. A self-test
157//! that unwinds out of initialisation leaves the module in `Error`.
158//!
159//! A service's check in `Operational` is one acquire load of the state
160//! word and one comparison: a plain load on x86_64, `ldar` on AArch64,
161//! and on a 32-bit target the platform's 64-bit atomic load. The thread id
162//! is asked for only by an `*_internal` call that finds `SelfTest`.
163
164#![no_std]
165#![forbid(unsafe_code)]
166
167// The state and the initialising thread's id are published as one 64-bit
168// atomic word, so a reader never sees one without the other. A target with
169// no 64-bit atomics cannot hold that word.
170#[cfg(not(target_has_atomic = "64"))]
171compile_error!(
172    "oxicrypt-module needs 64-bit atomics: the module state and the thread running the \
173     power-up self-tests are published as one AtomicU64"
174);
175
176use core::fmt;
177use core::sync::atomic::{AtomicU8, AtomicU64, Ordering};
178
179/// Module lifecycle state per FIPS 140-3 Section 7.2.
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181#[repr(u8)]
182pub enum State {
183    /// Module has not yet been initialized. No services are available.
184    PowerOff = 0,
185    /// Power-up self-tests are currently executing. No services are
186    /// available. This state is transient.
187    SelfTest = 1,
188    /// All power-up self-tests have passed. Approved services are
189    /// available.
190    Operational = 2,
191    /// A self-test has failed (either a power-up KAT or a conditional
192    /// test). This state is **terminal** within a process: no further
193    /// services are offered and all cached secret material must be
194    /// treated as zeroized. The only recovery is to restart the
195    /// containing process.
196    Error = 3,
197    /// The module reached the end of its pre-operational sequence
198    /// without the software integrity test having been performed, so it
199    /// was never verified against its integrity signature. **Terminal**, and
200    /// no services are offered.
201    ///
202    /// Distinct from [`State::Error`] because the two are different
203    /// faults with different owners. `Error` means a test ran and the
204    /// module failed it: the artifact is wrong. This state means nothing
205    /// checked the artifact at all — a front end initialised the module
206    /// without the integrity test in its inventory. Nothing here says
207    /// the module is corrupt; it says the module is unverified, and an
208    /// operator told only "Error" would go looking for corruption that
209    /// may not exist.
210    IntegrityUnverified = 4,
211}
212
213impl State {
214    const fn from_u8(raw: u8) -> Self {
215        match raw {
216            0 => Self::PowerOff,
217            1 => Self::SelfTest,
218            2 => Self::Operational,
219            4 => Self::IntegrityUnverified,
220            // Any unknown discriminant collapses to `Error` so that a
221            // corrupted state byte can never be interpreted as
222            // `Operational`. This is defence-in-depth: `AtomicU8`
223            // should only ever hold values written through this
224            // module.
225            _ => Self::Error,
226        }
227    }
228}
229
230impl fmt::Display for State {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        let s = match self {
233            Self::PowerOff => "PowerOff",
234            Self::SelfTest => "SelfTest",
235            Self::Operational => "Operational",
236            Self::Error => "Error",
237            Self::IntegrityUnverified => "IntegrityUnverified",
238        };
239        f.write_str(s)
240    }
241}
242
243/// Global module state, packed with the owner of the `SelfTest` phase.
244///
245/// Bits 0–7 hold the [`State`] discriminant. Bits 8–63 hold the operating
246/// system's id for the thread running the power-up self-tests while the
247/// state is `SelfTest`, and are zero in every other state. One word, so no
248/// reader can observe a state without its owner or an owner without its
249/// state. Written only by [`initialize`] and [`enter_error_state`]; read
250/// through [`state`] and the guards.
251static STATE: AtomicU64 = AtomicU64::new(POWER_OFF);
252
253/// The width of the state field; the owner occupies the bits above it.
254const STATE_BITS: u32 = 8;
255const POWER_OFF: u64 = State::PowerOff as u64;
256/// The one word in which the approved services run. Its owner field is
257/// zero, so the services' check is a single comparison.
258const OPERATIONAL: u64 = State::Operational as u64;
259const ERROR: u64 = State::Error as u64;
260const INTEGRITY_UNVERIFIED: u64 = State::IntegrityUnverified as u64;
261
262/// The word for `SelfTest` owned by the thread whose id is `owner`.
263///
264/// `owner` is non-zero and below `2^56`, as [`caller_owner_id`] returns it,
265/// so the shift loses nothing.
266const fn self_test_word(owner: u64) -> u64 {
267    owner.wrapping_shl(STATE_BITS) | State::SelfTest as u64
268}
269
270/// The state a word encodes.
271///
272/// A word this module did not write reads as `Error`: an unknown
273/// discriminant, `SelfTest` with no owner, or any other state with an
274/// owner. None of them can therefore be taken for `Operational` or for a
275/// `SelfTest` some thread owns.
276const fn decode(word: u64) -> State {
277    let [discriminant, ..] = word.to_le_bytes();
278    let owned = word.wrapping_shr(STATE_BITS) != 0;
279    match (State::from_u8(discriminant), owned) {
280        (State::SelfTest, true) => State::SelfTest,
281        (State::SelfTest, false) | (_, true) => State::Error,
282        (state, false) => state,
283    }
284}
285
286/// The calling thread's id as the owner field records it.
287fn caller_owner_id() -> Option<u64> {
288    owner_id(os_thread_id())
289}
290
291/// A thread id the owner field can record.
292///
293/// `None` when the platform offers no thread id, or offers zero or a value
294/// of `2^56` or more, none of which the field can record unambiguously.
295fn owner_id(raw: Option<u64>) -> Option<u64> {
296    raw.filter(|&id| id != 0 && id.leading_zeros() >= STATE_BITS)
297}
298
299/// The name [`Error::SelfTestFailed`] carries when initialisation fails
300/// because the platform gives the initialising thread no id the module can
301/// record, so no thread could own `SelfTest`.
302pub const THREAD_ID_CHECK: &str = "initialising thread id: none the module can record";
303
304/// `gettid` on Linux and Android, `pthread_threadid_np` on Darwin,
305/// `GetCurrentThreadId` on Windows.
306#[cfg(any(
307    target_os = "linux",
308    target_os = "android",
309    target_os = "macos",
310    target_os = "ios",
311    windows
312))]
313fn os_thread_id() -> Option<u64> {
314    oxicrypt_imageread::current_thread_id().ok()
315}
316
317/// No thread id on this target, so no thread can own `SelfTest` and the
318/// module cannot be initialised.
319#[cfg(not(any(
320    target_os = "linux",
321    target_os = "android",
322    target_os = "macos",
323    target_os = "ios",
324    windows
325)))]
326fn os_thread_id() -> Option<u64> {
327    None
328}
329
330/// Returns the current module state.
331pub fn state() -> State {
332    decode(STATE.load(Ordering::Acquire))
333}
334
335/// Returns `true` iff the module has passed its power-up self-tests and is
336/// currently in the `Operational` state.
337pub fn is_operational() -> bool {
338    STATE.load(Ordering::Acquire) == OPERATIONAL
339}
340
341/// Guard used at the entry point of every approved service. Returns
342/// `Ok(())` only when the module is `Operational`; otherwise returns a
343/// [`Error::NotOperational`] describing the current state so callers
344/// cannot accidentally ignore it.
345///
346/// The `Operational` case costs one acquire load of the state word and one
347/// comparison.
348pub fn require_operational() -> Result<(), Error> {
349    let word = STATE.load(Ordering::Acquire);
350    if word == OPERATIONAL {
351        Ok(())
352    } else {
353        Err(Error::NotOperational {
354            current: decode(word),
355        })
356    }
357}
358
359/// Guard used at the entry point of every public `*_internal` function.
360///
361/// The `*_internal` functions perform an approved algorithm without the
362/// [`require_operational`] / [`require_allowed`] gates of the approved
363/// service, so that the power-up self-tests can drive them while the
364/// module is still in [`State::SelfTest`]. They are not approved services.
365/// This guard bounds when they run: it returns `Ok(())` in
366/// [`State::Operational`] to every thread, and in [`State::SelfTest`] only
367/// to the thread running the power-up self-tests. It returns
368/// [`Error::NotOperational`] with the observed state to every other thread
369/// in `SelfTest`, so a call that arrives while initialisation is running,
370/// or paused, is refused, and to every thread in [`State::PowerOff`],
371/// [`State::Error`] and [`State::IntegrityUnverified`].
372///
373/// The `Operational` case is the same comparison [`require_operational`]
374/// makes, one acquire load and one compare, so any condition that gate
375/// imposes on the approved services applies here too. Only a call that
376/// finds `SelfTest` asks the operating system for its thread id.
377pub fn require_self_test_started() -> Result<(), Error> {
378    let word = STATE.load(Ordering::Acquire);
379    if word == OPERATIONAL {
380        return Ok(());
381    }
382    let current = decode(word);
383    if current == State::SelfTest && caller_owner_id().map(self_test_word) == Some(word) {
384        return Ok(());
385    }
386    Err(Error::NotOperational { current })
387}
388
389/// Proof that [`require_self_test_started`] passed for the holder.
390///
391/// Zero-sized, with no public constructor and no `Clone` or `Copy`: the only
392/// way to obtain one is [`require_self_test_started_token`]. A hidden method
393/// that takes `&SelfTestStarted` lets an algorithm crate do the state check
394/// once at its own entry point and then run a per-block or per-squeeze loop,
395/// or a region whose instructions must not change such as the entropy
396/// source's timed workload, without repeating it. Each caller obtains the
397/// token per call and never stores it. Holding a token across a transition
398/// to `Error` does not matter, because the methods that accept one are
399/// hidden and outside the module's defined service interface.
400///
401/// It is `Sync`, so a call that checked the state on its own thread can lend
402/// `&SelfTestStarted` to the worker threads it runs in parallel. A worker
403/// calling the guard itself would be refused during `SelfTest`, which admits
404/// only the thread running the power-up self-tests.
405#[derive(Debug)]
406pub struct SelfTestStarted {
407    _private: (),
408}
409
410/// Lending the proof to worker threads depends on it staying `Sync`.
411const _: () = {
412    const fn assert_sync<T: Sync>() {}
413    assert_sync::<SelfTestStarted>();
414};
415
416/// [`require_self_test_started`], returning a [`SelfTestStarted`] proof.
417///
418/// # Errors
419///
420/// Returns [`Error::NotOperational`] in `PowerOff`, `Error` and
421/// `IntegrityUnverified`, and in `SelfTest` on any thread but the one
422/// running the power-up self-tests, exactly as
423/// [`require_self_test_started`] does.
424pub fn require_self_test_started_token() -> Result<SelfTestStarted, Error> {
425    require_self_test_started()?;
426    Ok(SelfTestStarted { _private: () })
427}
428
429/// Errors surfaced by the module boundary.
430///
431/// Every variant is constructed with a `&'static str` description so
432/// that formatting is allocation-free; this lets the module report
433/// failures even from `no_std` contexts and from within the self-test
434/// runner itself.
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
436#[non_exhaustive]
437pub enum Error {
438    /// A service was invoked while the module was not in the
439    /// `Operational` state.
440    NotOperational {
441        /// The state the module was actually in when the call arrived.
442        current: State,
443    },
444    /// A power-up self-test failed. The module is now in the `Error`
445    /// state.
446    ///
447    /// `test` is [`THREAD_ID_CHECK`] when no self-test could run because
448    /// the platform gave the initialising thread no id the module can
449    /// record.
450    SelfTestFailed {
451        /// Name of the failing test, supplied by its
452        /// [`SelfTest`] implementation.
453        test: &'static str,
454    },
455    /// A conditional self-test (e.g. DRBG CRNGT, pairwise consistency)
456    /// failed during operation. The module is now in the `Error` state.
457    ConditionalTestFailed {
458        /// Short description of the failing check.
459        reason: &'static str,
460    },
461    /// The pre-operational self-test sequence completed without the
462    /// software integrity test having been performed. The module is now
463    /// in the [`State::IntegrityUnverified`] state.
464    ///
465    /// This is an integration fault rather than a finding about the
466    /// module: the test inventory handed to [`initialize_with_tests`]
467    /// did not include `oxicrypt_integrity::KATS`.
468    IntegrityNotAttested,
469    /// [`initialize_with_tests`] or [`initialize_with_profile`] was called
470    /// on a module that is already `Operational`, or that becomes
471    /// `Operational` when the initialisation another thread is running
472    /// completes. The module is ready; nothing was re-run.
473    AlreadyInitialized,
474    /// [`initialize_with_tests`] or [`initialize_with_profile`] was called
475    /// on a module whose initialisation has already failed, so that it is
476    /// in `Error` or `IntegrityUnverified`, including when the
477    /// initialisation another thread was running fails while this call
478    /// waits. A failed initialisation is terminal: no self-test is
479    /// re-run, nothing resets the state, and only a process restart
480    /// starts the module again.
481    AlreadyFailed,
482    /// A cryptographic service was called with input bytes that do not
483    /// encode a valid domain element (for example, a scalar that is
484    /// zero or not less than the group order, or a point that is not
485    /// a canonical encoding on the curve). This is distinct from a
486    /// self-test failure: the module is still operational; the caller
487    /// supplied out-of-range bytes. Algorithm primitives should return
488    /// this rather than silently substituting a default.
489    InvalidInput,
490    /// A service was invoked that is not permitted under the active
491    /// [`AlgorithmProfile`]. The module is still operational — the
492    /// algorithm exists but is restricted by policy. Switch to a
493    /// less restrictive profile (which requires a process restart)
494    /// or use an alternative algorithm that is permitted.
495    AlgorithmRestricted {
496        /// The specific service that was blocked.
497        service: Service,
498    },
499    /// A service was invoked for an algorithm that exists in the
500    /// [`Service`] enum but whose implementation has not yet been
501    /// completed. Stub crates return this. The algorithm is
502    /// recognized by the profile system and its gate will pass if
503    /// the profile permits it, but the actual cryptographic
504    /// operation is not yet available.
505    NotImplemented,
506}
507
508impl fmt::Display for Error {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        match self {
511            Self::NotOperational { current } => {
512                write!(f, "FIPS module not operational (state = {current})")
513            }
514            Self::SelfTestFailed { test } => {
515                write!(f, "FIPS power-up self-test failed: {test}")
516            }
517            Self::ConditionalTestFailed { reason } => {
518                write!(f, "FIPS conditional self-test failed: {reason}")
519            }
520            Self::IntegrityNotAttested => f.write_str(
521                "this module did not check itself at startup, so it cannot tell whether this \
522                 copy is the one that was built and signed. That does not mean the module is \
523                 damaged: the program that started it skipped the check.",
524            ),
525            Self::AlreadyInitialized => f.write_str("FIPS module already initialized"),
526            Self::AlreadyFailed => f.write_str(
527                "FIPS module initialization already failed; restart the process to retry",
528            ),
529            Self::InvalidInput => f.write_str("invalid input to FIPS service"),
530            Self::AlgorithmRestricted { service } => {
531                write!(
532                    f,
533                    "algorithm {service} is not permitted under the {} profile",
534                    active_profile()
535                )
536            }
537            Self::NotImplemented => f.write_str("algorithm not yet implemented"),
538        }
539    }
540}
541
542/// Marker returned by a failing [`SelfTest::run`].
543///
544/// Self-tests are allowed to describe *what* failed, but they are not
545/// allowed to exfiltrate the intermediate values that triggered the
546/// failure — including them in an error payload would potentially leak
547/// information about internal state. The runner treats the mere
548/// presence of this value as "test failed" and converts it into
549/// [`Error::SelfTestFailed`] using the test's own [`SelfTest::NAME`].
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub struct SelfTestFailure;
552
553impl fmt::Display for SelfTestFailure {
554    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555        f.write_str("self-test failure")
556    }
557}
558
559/// A single power-up self-test.
560///
561/// Each approved algorithm crate will implement this trait for at least
562/// one test vector, per SP 800-140B. The [`initialize_with_tests`] routine
563/// runs every registered test sequentially before the module can leave the
564/// `SelfTest` state.
565///
566/// Implementations **must** be deterministic, **must not** allocate,
567/// and **must not** read from any input the caller controls: the
568/// whole point of a KAT is that its inputs and expected outputs are
569/// baked into the module binary.
570pub trait SelfTest {
571    /// Stable, human-readable identifier for this test. Appears in
572    /// [`Error::SelfTestFailed`] and in audit logs.
573    const NAME: &'static str;
574
575    /// Runs the test. Returns `Ok(())` on success, `Err(SelfTestFailure)`
576    /// on any mismatch. The runner converts the `Err` into
577    /// [`Error::SelfTestFailed`] using [`Self::NAME`].
578    fn run() -> Result<(), SelfTestFailure>;
579}
580
581/// A power-up KAT registered with the module.
582///
583/// Algorithm crates expose one or more `KatEntry` values that the
584/// top-level caller passes into [`initialize_with_tests`] at startup.
585/// The runner executes every entry sequentially before the module
586/// leaves the `SelfTest` state; a single failure latches the module
587/// into `Error` for the remainder of the process lifetime, per
588/// FIPS 140-3 Section 7.10 and SP 800-140B.
589///
590/// The function pointer signature takes no inputs on purpose: a KAT
591/// must run against compile-time-baked vectors only, never against
592/// caller-supplied data.
593#[derive(Debug, Clone, Copy)]
594pub struct KatEntry {
595    /// Stable identifier for the test. Surfaces in
596    /// [`Error::SelfTestFailed`] and audit logs. Must match the
597    /// [`SelfTest::NAME`] used for the underlying vector.
598    pub name: &'static str,
599    /// The test routine itself.
600    pub run: fn() -> Result<(), SelfTestFailure>,
601}
602
603/// Initializes the module and runs every supplied power-up KAT.
604///
605/// This is the canonical entry point. It transitions
606/// `PowerOff -> SelfTest`, executes every entry in `tests` in order,
607/// and then transitions to `Operational` on success or to the
608/// terminal `Error` state on the first failure. The returned
609/// [`Error::SelfTestFailed`] carries the name of the failing test.
610///
611/// Concurrent calls are safe: exactly one caller wins the
612/// `PowerOff -> SelfTest` CAS and runs the tests on its own thread. A
613/// caller that does not win waits for the winner's outcome and receives
614/// [`Error::AlreadyInitialized`] if it succeeded or [`Error::AlreadyFailed`]
615/// if it failed. A call on a module that has already left `PowerOff` runs
616/// no test and changes nothing: initialisation happens once per process,
617/// and a failed one is terminal until the process restarts. A call made
618/// from inside a self-test, on the thread running them, returns
619/// [`Error::NotOperational`] naming `SelfTest`.
620///
621/// On a platform that gives the calling thread no id the module can
622/// record, the winning call latches `Error` and returns
623/// [`Error::SelfTestFailed`] naming [`THREAD_ID_CHECK`].
624///
625/// Note: the slice is passed **by reference**, not built up by magic.
626/// This repository deliberately avoids linker-section registration
627/// tricks: the full set of power-up tests is visible in source at
628/// every call site, which makes it straightforward to audit the
629/// module's test inventory against the Security Policy.
630pub fn initialize_with_tests(integrity: &[KatEntry], tests: &[KatEntry]) -> Result<(), Error> {
631    initialize(integrity, tests, AlgorithmProfile::Unrestricted)
632}
633
634/// The body of [`initialize_with_tests`] and [`initialize_with_profile`].
635///
636/// `profile` is stored only by the caller that wins the
637/// `PowerOff -> SelfTest` CAS, and only once its self-tests pass, so a
638/// later or concurrent call cannot change the profile the winning call
639/// selected, and a failed initialisation leaves the default.
640///
641/// The winning call publishes `SelfTest` and its own thread id in one
642/// word, so [`require_self_test_started`] admits only this thread while
643/// the tests run. Every way out of the tests other than success moves
644/// that word to a failed state, including an unwinding test, through
645/// [`UnwindToError`]; none of them overwrites an `Error` that a
646/// conditional self-test latched meanwhile.
647fn initialize(
648    integrity: &[KatEntry],
649    tests: &[KatEntry],
650    profile: AlgorithmProfile,
651) -> Result<(), Error> {
652    initialize_as(caller_owner_id(), integrity, tests, profile)
653}
654
655/// [`initialize`] for a caller whose owner id is `owner`, as
656/// [`caller_owner_id`] reports it.
657fn initialize_as(
658    owner: Option<u64>,
659    integrity: &[KatEntry],
660    tests: &[KatEntry],
661    profile: AlgorithmProfile,
662) -> Result<(), Error> {
663    // Claim the SelfTest phase. Only the first caller in the process
664    // lifetime succeeds. A caller with no thread id to record cannot own
665    // the phase, so on a platform without one the claim latches `Error`.
666    let claim = owner.map_or(ERROR, self_test_word);
667    if let Err(observed) =
668        STATE.compare_exchange(POWER_OFF, claim, Ordering::AcqRel, Ordering::Acquire)
669    {
670        return Err(already_left_power_off(observed, claim));
671    }
672    if owner.is_none() {
673        return Err(Error::SelfTestFailed {
674            test: THREAD_ID_CHECK,
675        });
676    }
677    let _unwind = UnwindToError { claim };
678
679    // The integrity group runs first and cannot be OMITTED: it is a
680    // separate parameter rather than an entry in `tests`, so a caller
681    // cannot drop it by handing over a shorter list, and an empty group
682    // is refused rather than treated as "nothing to check".
683    //
684    // What this does NOT establish is the group's identity. `KatEntry`
685    // is a public struct, so any non-empty slice satisfies this check —
686    // including one whose `run` returns `Ok(())` without verifying
687    // anything. Passing a counterfeit group is an integration fault
688    // that leaves the validated configuration, in the same class as
689    // calling an approved algorithm with a key the operator invented;
690    // the module cannot detect it and does not claim to. The Security
691    // Policy states the obligation: initialise with
692    // `oxicrypt_integrity::KATS`.
693    if integrity.is_empty() {
694        settle(claim, INTEGRITY_UNVERIFIED);
695        return Err(Error::IntegrityNotAttested);
696    }
697
698    for entry in integrity.iter().chain(tests) {
699        if (entry.run)().is_err() {
700            // Latch the module into Error before returning. Any
701            // subsequent service call will be rejected by
702            // require_operational().
703            settle(claim, ERROR);
704            return Err(Error::SelfTestFailed { test: entry.name });
705        }
706    }
707
708    // The only place in the workspace where the module becomes
709    // operational. Every entry point reaches it, so the integrity
710    // requirement is enforced once here rather than at the 129
711    // `require_operational` call sites, and no service pays for the
712    // check on its hot path.
713    //
714    // The profile is published here, by the call that won the CAS and
715    // passed every test, before the state that makes it readable by a
716    // service. A module that fails initialisation keeps the default.
717    //
718    // The transition is conditional on this call's own claim, so a
719    // conditional self-test that latched `Error` while the tests ran is
720    // never overwritten.
721    PROFILE.store(profile as u8, Ordering::Release);
722    if STATE
723        .compare_exchange(claim, OPERATIONAL, Ordering::AcqRel, Ordering::Acquire)
724        .is_err()
725    {
726        PROFILE.store(AlgorithmProfile::Unrestricted as u8, Ordering::Release);
727        return Err(Error::NotOperational { current: state() });
728    }
729    Ok(())
730}
731
732/// Moves `claim`, this call's `SelfTest` word, to the failed state `to`.
733///
734/// Conditional on the claim still standing, so it never overwrites an
735/// `Error` latched by [`enter_error_state`] in the meantime.
736fn settle(claim: u64, to: u64) {
737    // A failed exchange means the word is already `Error`, which is the
738    // outcome this call wants.
739    let _ = STATE.compare_exchange(claim, to, Ordering::AcqRel, Ordering::Acquire);
740}
741
742/// What a call that did not win the `PowerOff` claim reports.
743///
744/// While another thread owns `SelfTest` the call waits for its outcome,
745/// so it never reports the transient phase. A self-test that calls
746/// `initialize` on the owning thread is answered at once, rather than
747/// waiting on itself, with `NotOperational` naming `SelfTest`: the module
748/// is not ready, and the call changes nothing.
749fn already_left_power_off(mut observed: u64, claim: u64) -> Error {
750    #[cfg(test)]
751    if decode(observed) == State::SelfTest && observed != claim {
752        WAITING_LOSERS.fetch_add(1, Ordering::AcqRel);
753    }
754    while decode(observed) == State::SelfTest && observed != claim {
755        core::hint::spin_loop();
756        observed = STATE.load(Ordering::Acquire);
757    }
758    match decode(observed) {
759        State::Operational => Error::AlreadyInitialized,
760        State::SelfTest => Error::NotOperational {
761            current: State::SelfTest,
762        },
763        State::PowerOff | State::Error | State::IntegrityUnverified => Error::AlreadyFailed,
764    }
765}
766
767/// How many calls have entered the wait in [`already_left_power_off`], so a
768/// test can observe a loser waiting rather than infer it from timing.
769#[cfg(test)]
770static WAITING_LOSERS: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
771
772/// Held by [`initialize`] across the self-tests.
773///
774/// Every return path settles the claim before this drops, so the exchange
775/// below succeeds only when a self-test unwinds out of `initialize`. The
776/// module then lands in `Error` rather than being left in a `SelfTest`
777/// owned by a thread id that a later thread could be given.
778struct UnwindToError {
779    claim: u64,
780}
781
782impl Drop for UnwindToError {
783    fn drop(&mut self) {
784        settle(self.claim, ERROR);
785    }
786}
787
788/// Forces the module into the terminal `Error` state.
789///
790/// Called by conditional self-tests (DRBG CRNGT, pairwise consistency
791/// on keygen, etc.) when they detect a failure. Once invoked the
792/// module will reject every subsequent [`require_operational`] call
793/// for the remainder of the process lifetime.
794///
795/// This function is intentionally irreversible: FIPS 140-3 requires
796/// that a module which has entered an error condition stay there
797/// until it is re-initialized, and our "re-initialization" is a
798/// process restart.
799pub fn enter_error_state(_reason: &'static str) {
800    STATE.store(ERROR, Ordering::Release);
801}
802
803// =========================================================================
804// Algorithm profile gating (CNSA 2.0 / CNSA 1.0)
805// =========================================================================
806
807/// Algorithm restriction profile selected at module initialization.
808///
809/// A single binary can serve general FIPS 140-3 consumers
810/// (`Unrestricted`) and CNSA-restricted deployments (`Cnsa2`,
811/// `Cnsa1`) by choosing the appropriate profile at init time.
812///
813/// The profile is immutable once set: it is stored when the module
814/// becomes `Operational` and cannot be changed without restarting the
815/// process (which resets the state machine). A module that fails
816/// initialisation reports the default, `Unrestricted`.
817#[derive(Debug, Clone, Copy, PartialEq, Eq)]
818#[repr(u8)]
819pub enum AlgorithmProfile {
820    /// All FIPS-approved algorithms are available. This is the
821    /// default and preserves backward compatibility with callers
822    /// that use [`initialize_with_tests`].
823    Unrestricted = 0,
824    /// CNSA 2.0 — CNSSP-15 (March 2025) Annex B: AES-256, ML-KEM-1024,
825    /// ML-DSA-87, SHA-384 or SHA-512, and the SP 800-208 stateful
826    /// hash-based signatures for software and firmware signing, with the
827    /// supporting services those need at matching strength. The suite
828    /// names no Keccak-family algorithm for a software module, so none is
829    /// permitted here. Everything else returns
830    /// [`Error::AlgorithmRestricted`].
831    Cnsa2 = 1,
832    /// CNSA 1.0 — CNSSP-15 (October 2016) Annex B: AES-256, ECDH and
833    /// ECDSA on P-384, SHA-384, and Diffie-Hellman and RSA at a minimum
834    /// 3072-bit modulus, with the supporting services those need at
835    /// matching strength. SHA-256 and its variants are refused: the suite
836    /// admits them only under a near-term condition scoped to UNCLASSIFIED
837    /// data, which a module cannot evaluate.
838    Cnsa1 = 2,
839    /// Both suites at once — **not a CNSA profile and not defined by any
840    /// standard.** This is oxicrypt's own union of CNSA 1.0 and CNSA 2.0,
841    /// for a deployment migrating between them that must run classical and
842    /// post-quantum algorithms side by side.
843    ///
844    /// It is narrower than [`Unrestricted`](Self::Unrestricted), which
845    /// refuses nothing: SHA-1, AES-128, RSA-2048 and P-256 are all refused
846    /// here. It admits no Keccak-family algorithm, because neither suite
847    /// does.
848    Migration = 3,
849}
850
851impl AlgorithmProfile {
852    const fn from_u8(raw: u8) -> Self {
853        match raw {
854            0 => Self::Unrestricted,
855            2 => Self::Cnsa1,
856            3 => Self::Migration,
857            // Unknown discriminant (including 1 = Cnsa2) defaults to
858            // the most restrictive profile as defence-in-depth.
859            _ => Self::Cnsa2,
860        }
861    }
862}
863
864impl fmt::Display for AlgorithmProfile {
865    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
866        let s = match self {
867            Self::Unrestricted => "Unrestricted",
868            Self::Cnsa2 => "CNSA 2.0",
869            Self::Cnsa1 => "CNSA 1.0",
870            Self::Migration => "Migration (CNSA 1.0 + CNSA 2.0)",
871        };
872        f.write_str(s)
873    }
874}
875
876/// Global algorithm profile. Written once by [`initialize_with_profile`];
877/// read by [`require_allowed`] and [`active_profile`].
878static PROFILE: AtomicU8 = AtomicU8::new(AlgorithmProfile::Unrestricted as u8);
879
880/// Returns the active algorithm profile.
881pub fn active_profile() -> AlgorithmProfile {
882    AlgorithmProfile::from_u8(PROFILE.load(Ordering::Acquire))
883}
884
885/// Initializes the module with a specific algorithm profile and runs
886/// every supplied power-up KAT.
887///
888/// This is the profile-aware entry point. The call that wins the
889/// `PowerOff -> SelfTest` transition runs the KATs and, on success, stores
890/// the selected profile and transitions to `Operational`. On failure the
891/// module latches a terminal state and the profile stays the default. KATs
892/// always run all algorithms regardless of the profile — the profile
893/// only restricts post-initialization service access via
894/// [`require_allowed`].
895///
896/// A call that does not win that transition, whether it comes later or
897/// races the winner, leaves the active profile unchanged and returns
898/// [`Error::AlreadyInitialized`] when the winner succeeded, or
899/// [`Error::AlreadyFailed`] when it failed, as [`initialize_with_tests`]
900/// describes.
901///
902/// See [`initialize_with_tests`] for the backward-compatible wrapper
903/// that uses [`AlgorithmProfile::Unrestricted`].
904///
905/// # Calling context
906///
907/// While the self-tests run, [`require_self_test_started`] admits the
908/// calling thread, so code the application runs on that thread during
909/// initialisation must not call the module. Do not call module services
910/// from a global allocator, panic hook or other callback that can run
911/// during initialisation, and do not call this function from a task inside
912/// a shared work-stealing pool, whose worker may run unrelated tasks while
913/// it waits. Initialise from a dedicated thread, then use the module once
914/// this function returns `Ok`.
915pub fn initialize_with_profile(
916    integrity: &[KatEntry],
917    tests: &[KatEntry],
918    profile: AlgorithmProfile,
919) -> Result<(), Error> {
920    initialize(integrity, tests, profile)
921}
922
923/// Enumeration of every approved service in the module, at the
924/// algorithm-and-parameter level.
925///
926/// Each variant represents a specific algorithm instantiation (e.g.
927/// `Aes128Ecb` not just `Aes`). This granularity is necessary
928/// because CNSA restrictions operate at the key-size and hash-size
929/// level.
930///
931/// Algorithm crates pass the appropriate `Service` variant to
932/// [`require_allowed`] at their public entry points.
933#[derive(Debug, Clone, Copy, PartialEq, Eq)]
934#[repr(u16)]
935#[allow(missing_docs)]
936pub enum Service {
937    // ----- oxicrypt-sha: FIPS 180-4 / FIPS 202 -----
938    Sha1 = 0,
939    Sha224 = 1,
940    Sha256 = 2,
941    Sha384 = 3,
942    Sha512 = 4,
943    Sha512_224 = 5,
944    Sha512_256 = 6,
945
946    Sha3_224 = 10,
947    Sha3_256 = 11,
948    Sha3_384 = 12,
949    Sha3_512 = 13,
950
951    // ----- oxicrypt-xof: FIPS 202 / SP 800-185 -----
952    Shake128 = 20,
953    Shake256 = 21,
954    CShake128 = 22,
955    CShake256 = 23,
956    Kmac128 = 24,
957    Kmac256 = 25,
958    KmacXof128 = 26,
959    KmacXof256 = 27,
960    TupleHash128 = 28,
961    TupleHash256 = 29,
962    TupleHashXof128 = 30,
963    TupleHashXof256 = 31,
964    ParallelHash128 = 32,
965    ParallelHash256 = 33,
966    ParallelHashXof128 = 34,
967    ParallelHashXof256 = 35,
968
969    // ----- oxicrypt-hmac: FIPS 198-1 -----
970    HmacSha1 = 40,
971    HmacSha224 = 41,
972    HmacSha256 = 42,
973    HmacSha384 = 43,
974    HmacSha512 = 44,
975    HmacSha512_224 = 45,
976    HmacSha512_256 = 46,
977    HmacSha3_224 = 47,
978    HmacSha3_256 = 48,
979    HmacSha3_384 = 49,
980    HmacSha3_512 = 50,
981
982    // ----- oxicrypt-cmac: SP 800-38B -----
983    CmacAes128 = 60,
984    CmacAes192 = 61,
985    CmacAes256 = 62,
986
987    // ----- oxicrypt-aes: FIPS 197 / SP 800-38A/D/C/F/Fp -----
988    Aes128Ecb = 70,
989    Aes128Cbc = 71,
990    Aes128Ctr = 72,
991    Aes128Gcm = 73,
992    Aes128Ccm = 74,
993    Aes128Kw = 75,
994    Aes128Kwp = 76,
995    Aes192Ecb = 80,
996    Aes192Cbc = 81,
997    Aes192Ctr = 82,
998    Aes192Gcm = 83,
999    Aes192Ccm = 84,
1000    Aes192Kw = 85,
1001    Aes192Kwp = 86,
1002    Aes256Ecb = 90,
1003    Aes256Cbc = 91,
1004    Aes256Ctr = 92,
1005    Aes256Gcm = 93,
1006    Aes256Ccm = 94,
1007    Aes256Kw = 95,
1008    Aes256Kwp = 96,
1009
1010    // Key-construction gates — checked when building an AES key
1011    // object before the caller selects a mode.  All per-mode
1012    // variants of the same key size share identical profile
1013    // permissions, so a single gate at key construction is
1014    // sufficient.
1015    /// AES-128 key construction (gates all AES-128 mode services).
1016    Aes128 = 97,
1017    /// AES-192 key construction (gates all AES-192 mode services).
1018    Aes192 = 98,
1019    /// AES-256 key construction (gates all AES-256 mode services).
1020    Aes256 = 99,
1021
1022    // ----- oxicrypt-drbg: SP 800-90A -----
1023    CtrDrbgAes128 = 100,
1024    CtrDrbgAes192 = 101,
1025    CtrDrbgAes256 = 102,
1026    HashDrbgSha256 = 103,
1027    HashDrbgSha384 = 104,
1028    HashDrbgSha512 = 105,
1029    HmacDrbgSha256 = 106,
1030    HmacDrbgSha384 = 107,
1031    HmacDrbgSha512 = 108,
1032
1033    // ----- oxicrypt-kdf: SP 800-56C / SP 800-108 / SP 800-132 -----
1034    HkdfSha1 = 120,
1035    HkdfSha256 = 121,
1036    HkdfSha384 = 122,
1037    HkdfSha512 = 123,
1038    KbkdfHmacSha256 = 130,
1039    KbkdfHmacSha384 = 131,
1040    KbkdfHmacSha512 = 132,
1041    KbkdfCmacAes128 = 133,
1042    KbkdfCmacAes192 = 134,
1043    KbkdfCmacAes256 = 135,
1044    Pbkdf2HmacSha1 = 140,
1045    Pbkdf2HmacSha224 = 141,
1046    Pbkdf2HmacSha256 = 142,
1047    Pbkdf2HmacSha384 = 143,
1048    Pbkdf2HmacSha512 = 144,
1049
1050    // ----- oxicrypt-rsa: FIPS 186-5 / SP 800-56Br2 -----
1051    RsaKeygen2048 = 150,
1052    RsaPkcs1v15Sign2048 = 151,
1053    RsaPssSign2048 = 152,
1054    RsaOaep2048 = 153,
1055    RsaPkcs1v15Verify2048 = 154,
1056    RsaPssVerify2048 = 155,
1057    RsaKeygen3072 = 160,
1058    RsaPkcs1v15Sign3072 = 161,
1059    RsaPssSign3072 = 162,
1060    RsaOaep3072 = 163,
1061    RsaPkcs1v15Verify3072 = 164,
1062    RsaPssVerify3072 = 165,
1063    RsaKeygen4096 = 170,
1064    RsaPkcs1v15Sign4096 = 171,
1065    RsaPssSign4096 = 172,
1066    RsaOaep4096 = 173,
1067    RsaPkcs1v15Verify4096 = 174,
1068    RsaPssVerify4096 = 175,
1069
1070    // ----- oxicrypt-ecdsa: FIPS 186-5 -----
1071    EcdsaP256Sign = 200,
1072    EcdsaP256Verify = 201,
1073    EcdsaP256Keygen = 202,
1074    EcdsaP384Sign = 210,
1075    EcdsaP384Verify = 211,
1076    EcdsaP384Keygen = 212,
1077
1078    // ----- oxicrypt-ecdh: SP 800-56Ar3 -----
1079    EcdhP256 = 220,
1080    EcdhP384 = 221,
1081
1082    // ----- oxicrypt-eddsa: FIPS 186-5 / RFC 8032 -----
1083    Ed25519Sign = 230,
1084    Ed25519Verify = 231,
1085    Ed25519Keygen = 232,
1086
1087    // ----- oxicrypt-tls-kdf: SP 800-135r1 + RFC 8446 §7.1 -----
1088    Tls12Kdf = 240,
1089    Tls13Kdf = 241,
1090
1091    // ----- oxicrypt-ml-kem: FIPS 203 -----
1092    MlKem1024Encaps = 300,
1093    MlKem1024Decaps = 301,
1094    MlKem1024Keygen = 302,
1095    MlKem512Encaps = 303,
1096    MlKem512Decaps = 304,
1097    MlKem512Keygen = 305,
1098    MlKem768Encaps = 306,
1099    MlKem768Decaps = 307,
1100    MlKem768Keygen = 308,
1101
1102    // ----- oxicrypt-ml-dsa: FIPS 204 -----
1103    MlDsa87Sign = 310,
1104    MlDsa87Verify = 311,
1105    MlDsa87Keygen = 312,
1106    MlDsa44Sign = 313,
1107    MlDsa44Verify = 314,
1108    MlDsa44Keygen = 315,
1109    MlDsa65Sign = 316,
1110    MlDsa65Verify = 317,
1111    MlDsa65Keygen = 318,
1112
1113    // ----- oxicrypt-slh-dsa: FIPS 205 (12 parameter sets × 3 ops = 36 variants) -----
1114    // SHA-2 256s claims 320-322 for historical reasons, then the rest of the
1115    // SHA-2 family (128s/f, 192s/f, 256f), then the SHAKE family. No SLH-DSA
1116    // parameter set is permitted under either CNSA profile.
1117    SlhDsaSha2256sKeygen = 320,
1118    SlhDsaSha2256sSign = 321,
1119    SlhDsaSha2256sVerify = 322,
1120    SlhDsaSha2128sKeygen = 323,
1121    SlhDsaSha2128sSign = 324,
1122    SlhDsaSha2128sVerify = 325,
1123    SlhDsaSha2128fKeygen = 326,
1124    SlhDsaSha2128fSign = 327,
1125    SlhDsaSha2128fVerify = 328,
1126    SlhDsaSha2192sKeygen = 329,
1127    SlhDsaSha2192sSign = 330,
1128    SlhDsaSha2192sVerify = 331,
1129    SlhDsaSha2192fKeygen = 332,
1130    SlhDsaSha2192fSign = 333,
1131    SlhDsaSha2192fVerify = 334,
1132    SlhDsaSha2256fKeygen = 335,
1133    SlhDsaSha2256fSign = 336,
1134    SlhDsaSha2256fVerify = 337,
1135    SlhDsaShake128sKeygen = 338,
1136    SlhDsaShake128sSign = 339,
1137    SlhDsaShake128sVerify = 340,
1138    SlhDsaShake128fKeygen = 341,
1139    SlhDsaShake128fSign = 342,
1140    SlhDsaShake128fVerify = 343,
1141    SlhDsaShake192sKeygen = 344,
1142    SlhDsaShake192sSign = 345,
1143    SlhDsaShake192sVerify = 346,
1144    SlhDsaShake192fKeygen = 347,
1145    SlhDsaShake192fSign = 348,
1146    SlhDsaShake192fVerify = 349,
1147    SlhDsaShake256sKeygen = 350,
1148    SlhDsaShake256sSign = 351,
1149    SlhDsaShake256sVerify = 352,
1150    SlhDsaShake256fKeygen = 353,
1151    SlhDsaShake256fSign = 354,
1152    SlhDsaShake256fVerify = 355,
1153
1154    // ----- oxicrypt-lms: SP 800-208 §4.1–§4.4, verification only -----
1155    // 80 per-pair entries on the odd discriminants 501-659.
1156    // Layout: 501-539 SHA-256/M=32, 541-579 SHA-256/M=24,
1157    // 581-619 SHAKE/M=32, 621-659 SHAKE/M=24.
1158    // Within each family: (H ascending, W ascending). The even values in
1159    // 500-658 are unassigned: SP 800-208 §8.1 confines LMS signing to
1160    // hardware modules, so no signing service exists to hold them.
1161    // The odd values in the block hold nothing but LMS, which is what lets
1162    // `is_lms_service` test the block arithmetically;
1163    // `lms_discriminant_slots_are_complete_and_pure` and a build-time
1164    // assertion pin that. All 80 pairs are permitted under CNSA 2.0,
1165    // Migration and Unrestricted, and refused under CNSA 1.0.
1166
1167    // SHA-256 / N=32 family (SP 800-208 §4.1)
1168    LmsSha256M32H5W1Verify = 501,
1169    LmsSha256M32H5W2Verify = 503,
1170    LmsSha256M32H5W4Verify = 505,
1171    LmsSha256M32H5W8Verify = 507,
1172    LmsSha256M32H10W1Verify = 509,
1173    LmsSha256M32H10W2Verify = 511,
1174    LmsSha256M32H10W4Verify = 513,
1175    LmsSha256M32H10W8Verify = 515,
1176    LmsSha256M32H15W1Verify = 517,
1177    LmsSha256M32H15W2Verify = 519,
1178    LmsSha256M32H15W4Verify = 521,
1179    LmsSha256M32H15W8Verify = 523,
1180    LmsSha256M32H20W1Verify = 525,
1181    LmsSha256M32H20W2Verify = 527,
1182    LmsSha256M32H20W4Verify = 529,
1183    LmsSha256M32H20W8Verify = 531,
1184    LmsSha256M32H25W1Verify = 533,
1185    LmsSha256M32H25W2Verify = 535,
1186    LmsSha256M32H25W4Verify = 537,
1187    LmsSha256M32H25W8Verify = 539,
1188
1189    // SHA-256 / N=24 family (SP 800-208 §4.2)
1190    LmsSha256M24H5W1Verify = 541,
1191    LmsSha256M24H5W2Verify = 543,
1192    LmsSha256M24H5W4Verify = 545,
1193    LmsSha256M24H5W8Verify = 547,
1194    LmsSha256M24H10W1Verify = 549,
1195    LmsSha256M24H10W2Verify = 551,
1196    LmsSha256M24H10W4Verify = 553,
1197    LmsSha256M24H10W8Verify = 555,
1198    LmsSha256M24H15W1Verify = 557,
1199    LmsSha256M24H15W2Verify = 559,
1200    LmsSha256M24H15W4Verify = 561,
1201    LmsSha256M24H15W8Verify = 563,
1202    LmsSha256M24H20W1Verify = 565,
1203    LmsSha256M24H20W2Verify = 567,
1204    LmsSha256M24H20W4Verify = 569,
1205    LmsSha256M24H20W8Verify = 571,
1206    LmsSha256M24H25W1Verify = 573,
1207    LmsSha256M24H25W2Verify = 575,
1208    LmsSha256M24H25W4Verify = 577,
1209    LmsSha256M24H25W8Verify = 579,
1210
1211    // SHAKE-256 / N=32 family (SP 800-208 §4.3)
1212    LmsShakeM32H5W1Verify = 581,
1213    LmsShakeM32H5W2Verify = 583,
1214    LmsShakeM32H5W4Verify = 585,
1215    LmsShakeM32H5W8Verify = 587,
1216    LmsShakeM32H10W1Verify = 589,
1217    LmsShakeM32H10W2Verify = 591,
1218    LmsShakeM32H10W4Verify = 593,
1219    LmsShakeM32H10W8Verify = 595,
1220    LmsShakeM32H15W1Verify = 597,
1221    LmsShakeM32H15W2Verify = 599,
1222    LmsShakeM32H15W4Verify = 601,
1223    LmsShakeM32H15W8Verify = 603,
1224    LmsShakeM32H20W1Verify = 605,
1225    LmsShakeM32H20W2Verify = 607,
1226    LmsShakeM32H20W4Verify = 609,
1227    LmsShakeM32H20W8Verify = 611,
1228    LmsShakeM32H25W1Verify = 613,
1229    LmsShakeM32H25W2Verify = 615,
1230    LmsShakeM32H25W4Verify = 617,
1231    LmsShakeM32H25W8Verify = 619,
1232
1233    // SHAKE-256 / N=24 family (SP 800-208 §4.4)
1234    LmsShakeM24H5W1Verify = 621,
1235    LmsShakeM24H5W2Verify = 623,
1236    LmsShakeM24H5W4Verify = 625,
1237    LmsShakeM24H5W8Verify = 627,
1238    LmsShakeM24H10W1Verify = 629,
1239    LmsShakeM24H10W2Verify = 631,
1240    LmsShakeM24H10W4Verify = 633,
1241    LmsShakeM24H10W8Verify = 635,
1242    LmsShakeM24H15W1Verify = 637,
1243    LmsShakeM24H15W2Verify = 639,
1244    LmsShakeM24H15W4Verify = 641,
1245    LmsShakeM24H15W8Verify = 643,
1246    LmsShakeM24H20W1Verify = 645,
1247    LmsShakeM24H20W2Verify = 647,
1248    LmsShakeM24H20W4Verify = 649,
1249    LmsShakeM24H20W8Verify = 651,
1250    LmsShakeM24H25W1Verify = 653,
1251    LmsShakeM24H25W2Verify = 655,
1252    LmsShakeM24H25W4Verify = 657,
1253    LmsShakeM24H25W8Verify = 659,
1254
1255    // ----- oxicrypt-xmss: SP 800-208 (stub) -----
1256    // Renumbered 340/341 → 370/371 to make room for the SLH-DSA block.
1257    XmssVerify = 371,
1258
1259    // ----- oxicrypt-dh: RFC 3526 (stub) -----
1260    // Renumbered 350 → 380 to make room for the SLH-DSA block.
1261    Dh3072 = 380,
1262}
1263
1264impl fmt::Display for Service {
1265    // The match arm count is proportional to the number of approved
1266    // services in the module; there is no natural factoring that
1267    // would reduce it.
1268    #[allow(clippy::too_many_lines)]
1269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1270        let name = match self {
1271            Self::Sha1 => "SHA-1",
1272            Self::Sha224 => "SHA-224",
1273            Self::Sha256 => "SHA-256",
1274            Self::Sha384 => "SHA-384",
1275            Self::Sha512 => "SHA-512",
1276            Self::Sha512_224 => "SHA-512/224",
1277            Self::Sha512_256 => "SHA-512/256",
1278            Self::Sha3_224 => "SHA3-224",
1279            Self::Sha3_256 => "SHA3-256",
1280            Self::Sha3_384 => "SHA3-384",
1281            Self::Sha3_512 => "SHA3-512",
1282            Self::Shake128 => "SHAKE128",
1283            Self::Shake256 => "SHAKE256",
1284            Self::CShake128 => "cSHAKE128",
1285            Self::CShake256 => "cSHAKE256",
1286            Self::Kmac128 => "KMAC128",
1287            Self::Kmac256 => "KMAC256",
1288            Self::KmacXof128 => "KMACXOF128",
1289            Self::KmacXof256 => "KMACXOF256",
1290            Self::TupleHash128 => "TupleHash128",
1291            Self::TupleHash256 => "TupleHash256",
1292            Self::TupleHashXof128 => "TupleHashXOF128",
1293            Self::TupleHashXof256 => "TupleHashXOF256",
1294            Self::ParallelHash128 => "ParallelHash128",
1295            Self::ParallelHash256 => "ParallelHash256",
1296            Self::ParallelHashXof128 => "ParallelHashXOF128",
1297            Self::ParallelHashXof256 => "ParallelHashXOF256",
1298            Self::HmacSha1 => "HMAC-SHA-1",
1299            Self::HmacSha224 => "HMAC-SHA-224",
1300            Self::HmacSha256 => "HMAC-SHA-256",
1301            Self::HmacSha384 => "HMAC-SHA-384",
1302            Self::HmacSha512 => "HMAC-SHA-512",
1303            Self::HmacSha512_224 => "HMAC-SHA-512/224",
1304            Self::HmacSha512_256 => "HMAC-SHA-512/256",
1305            Self::HmacSha3_224 => "HMAC-SHA3-224",
1306            Self::HmacSha3_256 => "HMAC-SHA3-256",
1307            Self::HmacSha3_384 => "HMAC-SHA3-384",
1308            Self::HmacSha3_512 => "HMAC-SHA3-512",
1309            Self::CmacAes128 => "CMAC-AES-128",
1310            Self::CmacAes192 => "CMAC-AES-192",
1311            Self::CmacAes256 => "CMAC-AES-256",
1312            Self::Aes128Ecb => "AES-128-ECB",
1313            Self::Aes128Cbc => "AES-128-CBC",
1314            Self::Aes128Ctr => "AES-128-CTR",
1315            Self::Aes128Gcm => "AES-128-GCM",
1316            Self::Aes128Ccm => "AES-128-CCM",
1317            Self::Aes128Kw => "AES-128-KW",
1318            Self::Aes128Kwp => "AES-128-KWP",
1319            Self::Aes192Ecb => "AES-192-ECB",
1320            Self::Aes192Cbc => "AES-192-CBC",
1321            Self::Aes192Ctr => "AES-192-CTR",
1322            Self::Aes192Gcm => "AES-192-GCM",
1323            Self::Aes192Ccm => "AES-192-CCM",
1324            Self::Aes192Kw => "AES-192-KW",
1325            Self::Aes192Kwp => "AES-192-KWP",
1326            Self::Aes256Ecb => "AES-256-ECB",
1327            Self::Aes256Cbc => "AES-256-CBC",
1328            Self::Aes256Ctr => "AES-256-CTR",
1329            Self::Aes256Gcm => "AES-256-GCM",
1330            Self::Aes256Ccm => "AES-256-CCM",
1331            Self::Aes256Kw => "AES-256-KW",
1332            Self::Aes256Kwp => "AES-256-KWP",
1333            Self::Aes128 => "AES-128",
1334            Self::Aes192 => "AES-192",
1335            Self::Aes256 => "AES-256",
1336            Self::CtrDrbgAes128 => "CTR_DRBG-AES-128",
1337            Self::CtrDrbgAes192 => "CTR_DRBG-AES-192",
1338            Self::CtrDrbgAes256 => "CTR_DRBG-AES-256",
1339            Self::HashDrbgSha256 => "Hash_DRBG-SHA-256",
1340            Self::HashDrbgSha384 => "Hash_DRBG-SHA-384",
1341            Self::HashDrbgSha512 => "Hash_DRBG-SHA-512",
1342            Self::HmacDrbgSha256 => "HMAC_DRBG-SHA-256",
1343            Self::HmacDrbgSha384 => "HMAC_DRBG-SHA-384",
1344            Self::HmacDrbgSha512 => "HMAC_DRBG-SHA-512",
1345            Self::HkdfSha1 => "HKDF-SHA-1",
1346            Self::HkdfSha256 => "HKDF-SHA-256",
1347            Self::HkdfSha384 => "HKDF-SHA-384",
1348            Self::HkdfSha512 => "HKDF-SHA-512",
1349            Self::KbkdfHmacSha256 => "KBKDF-HMAC-SHA-256",
1350            Self::KbkdfHmacSha384 => "KBKDF-HMAC-SHA-384",
1351            Self::KbkdfHmacSha512 => "KBKDF-HMAC-SHA-512",
1352            Self::KbkdfCmacAes128 => "KBKDF-CMAC-AES-128",
1353            Self::KbkdfCmacAes192 => "KBKDF-CMAC-AES-192",
1354            Self::KbkdfCmacAes256 => "KBKDF-CMAC-AES-256",
1355            Self::Pbkdf2HmacSha1 => "PBKDF2-HMAC-SHA-1",
1356            Self::Pbkdf2HmacSha224 => "PBKDF2-HMAC-SHA-224",
1357            Self::Pbkdf2HmacSha256 => "PBKDF2-HMAC-SHA-256",
1358            Self::Pbkdf2HmacSha384 => "PBKDF2-HMAC-SHA-384",
1359            Self::Pbkdf2HmacSha512 => "PBKDF2-HMAC-SHA-512",
1360            Self::RsaKeygen2048 => "RSA-2048 keygen",
1361            Self::RsaPkcs1v15Sign2048 => "RSA-PKCS1v1.5-Sign-2048",
1362            Self::RsaPssSign2048 => "RSA-PSS-Sign-2048",
1363            Self::RsaOaep2048 => "RSA-OAEP-2048",
1364            Self::RsaPkcs1v15Verify2048 => "RSA-PKCS1v1.5-Verify-2048",
1365            Self::RsaPssVerify2048 => "RSA-PSS-Verify-2048",
1366            Self::RsaKeygen3072 => "RSA-3072 keygen",
1367            Self::RsaPkcs1v15Sign3072 => "RSA-PKCS1v1.5-Sign-3072",
1368            Self::RsaPssSign3072 => "RSA-PSS-Sign-3072",
1369            Self::RsaOaep3072 => "RSA-OAEP-3072",
1370            Self::RsaPkcs1v15Verify3072 => "RSA-PKCS1v1.5-Verify-3072",
1371            Self::RsaPssVerify3072 => "RSA-PSS-Verify-3072",
1372            Self::RsaKeygen4096 => "RSA-4096 keygen",
1373            Self::RsaPkcs1v15Sign4096 => "RSA-PKCS1v1.5-Sign-4096",
1374            Self::RsaPssSign4096 => "RSA-PSS-Sign-4096",
1375            Self::RsaOaep4096 => "RSA-OAEP-4096",
1376            Self::RsaPkcs1v15Verify4096 => "RSA-PKCS1v1.5-Verify-4096",
1377            Self::RsaPssVerify4096 => "RSA-PSS-Verify-4096",
1378            Self::EcdsaP256Sign => "ECDSA-P256 sign",
1379            Self::EcdsaP256Verify => "ECDSA-P256 verify",
1380            Self::EcdsaP256Keygen => "ECDSA-P256 keygen",
1381            Self::EcdsaP384Sign => "ECDSA-P384 sign",
1382            Self::EcdsaP384Verify => "ECDSA-P384 verify",
1383            Self::EcdsaP384Keygen => "ECDSA-P384 keygen",
1384            Self::EcdhP256 => "ECDH-P256",
1385            Self::EcdhP384 => "ECDH-P384",
1386            Self::Ed25519Sign => "Ed25519 sign",
1387            Self::Ed25519Verify => "Ed25519 verify",
1388            Self::Ed25519Keygen => "Ed25519 keygen",
1389            Self::Tls12Kdf => "TLS 1.2 KDF",
1390            Self::Tls13Kdf => "TLS 1.3 KDF",
1391            Self::MlKem1024Encaps => "ML-KEM-1024 encaps",
1392            Self::MlKem1024Decaps => "ML-KEM-1024 decaps",
1393            Self::MlKem1024Keygen => "ML-KEM-1024 keygen",
1394            Self::MlKem512Encaps => "ML-KEM-512 encaps",
1395            Self::MlKem512Decaps => "ML-KEM-512 decaps",
1396            Self::MlKem512Keygen => "ML-KEM-512 keygen",
1397            Self::MlKem768Encaps => "ML-KEM-768 encaps",
1398            Self::MlKem768Decaps => "ML-KEM-768 decaps",
1399            Self::MlKem768Keygen => "ML-KEM-768 keygen",
1400            Self::MlDsa87Sign => "ML-DSA-87 sign",
1401            Self::MlDsa87Verify => "ML-DSA-87 verify",
1402            Self::MlDsa87Keygen => "ML-DSA-87 keygen",
1403            Self::MlDsa44Sign => "ML-DSA-44 sign",
1404            Self::MlDsa44Verify => "ML-DSA-44 verify",
1405            Self::MlDsa44Keygen => "ML-DSA-44 keygen",
1406            Self::MlDsa65Sign => "ML-DSA-65 sign",
1407            Self::MlDsa65Verify => "ML-DSA-65 verify",
1408            Self::MlDsa65Keygen => "ML-DSA-65 keygen",
1409            Self::SlhDsaSha2256sKeygen => "SLH-DSA-SHA2-256s keygen",
1410            Self::SlhDsaSha2256sSign => "SLH-DSA-SHA2-256s sign",
1411            Self::SlhDsaSha2256sVerify => "SLH-DSA-SHA2-256s verify",
1412            Self::SlhDsaSha2128sKeygen => "SLH-DSA-SHA2-128s keygen",
1413            Self::SlhDsaSha2128sSign => "SLH-DSA-SHA2-128s sign",
1414            Self::SlhDsaSha2128sVerify => "SLH-DSA-SHA2-128s verify",
1415            Self::SlhDsaSha2128fKeygen => "SLH-DSA-SHA2-128f keygen",
1416            Self::SlhDsaSha2128fSign => "SLH-DSA-SHA2-128f sign",
1417            Self::SlhDsaSha2128fVerify => "SLH-DSA-SHA2-128f verify",
1418            Self::SlhDsaSha2192sKeygen => "SLH-DSA-SHA2-192s keygen",
1419            Self::SlhDsaSha2192sSign => "SLH-DSA-SHA2-192s sign",
1420            Self::SlhDsaSha2192sVerify => "SLH-DSA-SHA2-192s verify",
1421            Self::SlhDsaSha2192fKeygen => "SLH-DSA-SHA2-192f keygen",
1422            Self::SlhDsaSha2192fSign => "SLH-DSA-SHA2-192f sign",
1423            Self::SlhDsaSha2192fVerify => "SLH-DSA-SHA2-192f verify",
1424            Self::SlhDsaSha2256fKeygen => "SLH-DSA-SHA2-256f keygen",
1425            Self::SlhDsaSha2256fSign => "SLH-DSA-SHA2-256f sign",
1426            Self::SlhDsaSha2256fVerify => "SLH-DSA-SHA2-256f verify",
1427            Self::SlhDsaShake128sKeygen => "SLH-DSA-SHAKE-128s keygen",
1428            Self::SlhDsaShake128sSign => "SLH-DSA-SHAKE-128s sign",
1429            Self::SlhDsaShake128sVerify => "SLH-DSA-SHAKE-128s verify",
1430            Self::SlhDsaShake128fKeygen => "SLH-DSA-SHAKE-128f keygen",
1431            Self::SlhDsaShake128fSign => "SLH-DSA-SHAKE-128f sign",
1432            Self::SlhDsaShake128fVerify => "SLH-DSA-SHAKE-128f verify",
1433            Self::SlhDsaShake192sKeygen => "SLH-DSA-SHAKE-192s keygen",
1434            Self::SlhDsaShake192sSign => "SLH-DSA-SHAKE-192s sign",
1435            Self::SlhDsaShake192sVerify => "SLH-DSA-SHAKE-192s verify",
1436            Self::SlhDsaShake192fKeygen => "SLH-DSA-SHAKE-192f keygen",
1437            Self::SlhDsaShake192fSign => "SLH-DSA-SHAKE-192f sign",
1438            Self::SlhDsaShake192fVerify => "SLH-DSA-SHAKE-192f verify",
1439            Self::SlhDsaShake256sKeygen => "SLH-DSA-SHAKE-256s keygen",
1440            Self::SlhDsaShake256sSign => "SLH-DSA-SHAKE-256s sign",
1441            Self::SlhDsaShake256sVerify => "SLH-DSA-SHAKE-256s verify",
1442            Self::SlhDsaShake256fKeygen => "SLH-DSA-SHAKE-256f keygen",
1443            Self::SlhDsaShake256fSign => "SLH-DSA-SHAKE-256f sign",
1444            Self::SlhDsaShake256fVerify => "SLH-DSA-SHAKE-256f verify",
1445            Self::LmsSha256M32H5W1Verify => "LMS SHA-256 M=32 H=5 W=1 verify",
1446            Self::LmsSha256M32H5W2Verify => "LMS SHA-256 M=32 H=5 W=2 verify",
1447            Self::LmsSha256M32H5W4Verify => "LMS SHA-256 M=32 H=5 W=4 verify",
1448            Self::LmsSha256M32H5W8Verify => "LMS SHA-256 M=32 H=5 W=8 verify",
1449            Self::LmsSha256M32H10W1Verify => "LMS SHA-256 M=32 H=10 W=1 verify",
1450            Self::LmsSha256M32H10W2Verify => "LMS SHA-256 M=32 H=10 W=2 verify",
1451            Self::LmsSha256M32H10W4Verify => "LMS SHA-256 M=32 H=10 W=4 verify",
1452            Self::LmsSha256M32H10W8Verify => "LMS SHA-256 M=32 H=10 W=8 verify",
1453            Self::LmsSha256M32H15W1Verify => "LMS SHA-256 M=32 H=15 W=1 verify",
1454            Self::LmsSha256M32H15W2Verify => "LMS SHA-256 M=32 H=15 W=2 verify",
1455            Self::LmsSha256M32H15W4Verify => "LMS SHA-256 M=32 H=15 W=4 verify",
1456            Self::LmsSha256M32H15W8Verify => "LMS SHA-256 M=32 H=15 W=8 verify",
1457            Self::LmsSha256M32H20W1Verify => "LMS SHA-256 M=32 H=20 W=1 verify",
1458            Self::LmsSha256M32H20W2Verify => "LMS SHA-256 M=32 H=20 W=2 verify",
1459            Self::LmsSha256M32H20W4Verify => "LMS SHA-256 M=32 H=20 W=4 verify",
1460            Self::LmsSha256M32H20W8Verify => "LMS SHA-256 M=32 H=20 W=8 verify",
1461            Self::LmsSha256M32H25W1Verify => "LMS SHA-256 M=32 H=25 W=1 verify",
1462            Self::LmsSha256M32H25W2Verify => "LMS SHA-256 M=32 H=25 W=2 verify",
1463            Self::LmsSha256M32H25W4Verify => "LMS SHA-256 M=32 H=25 W=4 verify",
1464            Self::LmsSha256M32H25W8Verify => "LMS SHA-256 M=32 H=25 W=8 verify",
1465            Self::LmsSha256M24H5W1Verify => "LMS SHA-256 M=24 H=5 W=1 verify",
1466            Self::LmsSha256M24H5W2Verify => "LMS SHA-256 M=24 H=5 W=2 verify",
1467            Self::LmsSha256M24H5W4Verify => "LMS SHA-256 M=24 H=5 W=4 verify",
1468            Self::LmsSha256M24H5W8Verify => "LMS SHA-256 M=24 H=5 W=8 verify",
1469            Self::LmsSha256M24H10W1Verify => "LMS SHA-256 M=24 H=10 W=1 verify",
1470            Self::LmsSha256M24H10W2Verify => "LMS SHA-256 M=24 H=10 W=2 verify",
1471            Self::LmsSha256M24H10W4Verify => "LMS SHA-256 M=24 H=10 W=4 verify",
1472            Self::LmsSha256M24H10W8Verify => "LMS SHA-256 M=24 H=10 W=8 verify",
1473            Self::LmsSha256M24H15W1Verify => "LMS SHA-256 M=24 H=15 W=1 verify",
1474            Self::LmsSha256M24H15W2Verify => "LMS SHA-256 M=24 H=15 W=2 verify",
1475            Self::LmsSha256M24H15W4Verify => "LMS SHA-256 M=24 H=15 W=4 verify",
1476            Self::LmsSha256M24H15W8Verify => "LMS SHA-256 M=24 H=15 W=8 verify",
1477            Self::LmsSha256M24H20W1Verify => "LMS SHA-256 M=24 H=20 W=1 verify",
1478            Self::LmsSha256M24H20W2Verify => "LMS SHA-256 M=24 H=20 W=2 verify",
1479            Self::LmsSha256M24H20W4Verify => "LMS SHA-256 M=24 H=20 W=4 verify",
1480            Self::LmsSha256M24H20W8Verify => "LMS SHA-256 M=24 H=20 W=8 verify",
1481            Self::LmsSha256M24H25W1Verify => "LMS SHA-256 M=24 H=25 W=1 verify",
1482            Self::LmsSha256M24H25W2Verify => "LMS SHA-256 M=24 H=25 W=2 verify",
1483            Self::LmsSha256M24H25W4Verify => "LMS SHA-256 M=24 H=25 W=4 verify",
1484            Self::LmsSha256M24H25W8Verify => "LMS SHA-256 M=24 H=25 W=8 verify",
1485            Self::LmsShakeM32H5W1Verify => "LMS SHAKE M=32 H=5 W=1 verify",
1486            Self::LmsShakeM32H5W2Verify => "LMS SHAKE M=32 H=5 W=2 verify",
1487            Self::LmsShakeM32H5W4Verify => "LMS SHAKE M=32 H=5 W=4 verify",
1488            Self::LmsShakeM32H5W8Verify => "LMS SHAKE M=32 H=5 W=8 verify",
1489            Self::LmsShakeM32H10W1Verify => "LMS SHAKE M=32 H=10 W=1 verify",
1490            Self::LmsShakeM32H10W2Verify => "LMS SHAKE M=32 H=10 W=2 verify",
1491            Self::LmsShakeM32H10W4Verify => "LMS SHAKE M=32 H=10 W=4 verify",
1492            Self::LmsShakeM32H10W8Verify => "LMS SHAKE M=32 H=10 W=8 verify",
1493            Self::LmsShakeM32H15W1Verify => "LMS SHAKE M=32 H=15 W=1 verify",
1494            Self::LmsShakeM32H15W2Verify => "LMS SHAKE M=32 H=15 W=2 verify",
1495            Self::LmsShakeM32H15W4Verify => "LMS SHAKE M=32 H=15 W=4 verify",
1496            Self::LmsShakeM32H15W8Verify => "LMS SHAKE M=32 H=15 W=8 verify",
1497            Self::LmsShakeM32H20W1Verify => "LMS SHAKE M=32 H=20 W=1 verify",
1498            Self::LmsShakeM32H20W2Verify => "LMS SHAKE M=32 H=20 W=2 verify",
1499            Self::LmsShakeM32H20W4Verify => "LMS SHAKE M=32 H=20 W=4 verify",
1500            Self::LmsShakeM32H20W8Verify => "LMS SHAKE M=32 H=20 W=8 verify",
1501            Self::LmsShakeM32H25W1Verify => "LMS SHAKE M=32 H=25 W=1 verify",
1502            Self::LmsShakeM32H25W2Verify => "LMS SHAKE M=32 H=25 W=2 verify",
1503            Self::LmsShakeM32H25W4Verify => "LMS SHAKE M=32 H=25 W=4 verify",
1504            Self::LmsShakeM32H25W8Verify => "LMS SHAKE M=32 H=25 W=8 verify",
1505            Self::LmsShakeM24H5W1Verify => "LMS SHAKE M=24 H=5 W=1 verify",
1506            Self::LmsShakeM24H5W2Verify => "LMS SHAKE M=24 H=5 W=2 verify",
1507            Self::LmsShakeM24H5W4Verify => "LMS SHAKE M=24 H=5 W=4 verify",
1508            Self::LmsShakeM24H5W8Verify => "LMS SHAKE M=24 H=5 W=8 verify",
1509            Self::LmsShakeM24H10W1Verify => "LMS SHAKE M=24 H=10 W=1 verify",
1510            Self::LmsShakeM24H10W2Verify => "LMS SHAKE M=24 H=10 W=2 verify",
1511            Self::LmsShakeM24H10W4Verify => "LMS SHAKE M=24 H=10 W=4 verify",
1512            Self::LmsShakeM24H10W8Verify => "LMS SHAKE M=24 H=10 W=8 verify",
1513            Self::LmsShakeM24H15W1Verify => "LMS SHAKE M=24 H=15 W=1 verify",
1514            Self::LmsShakeM24H15W2Verify => "LMS SHAKE M=24 H=15 W=2 verify",
1515            Self::LmsShakeM24H15W4Verify => "LMS SHAKE M=24 H=15 W=4 verify",
1516            Self::LmsShakeM24H15W8Verify => "LMS SHAKE M=24 H=15 W=8 verify",
1517            Self::LmsShakeM24H20W1Verify => "LMS SHAKE M=24 H=20 W=1 verify",
1518            Self::LmsShakeM24H20W2Verify => "LMS SHAKE M=24 H=20 W=2 verify",
1519            Self::LmsShakeM24H20W4Verify => "LMS SHAKE M=24 H=20 W=4 verify",
1520            Self::LmsShakeM24H20W8Verify => "LMS SHAKE M=24 H=20 W=8 verify",
1521            Self::LmsShakeM24H25W1Verify => "LMS SHAKE M=24 H=25 W=1 verify",
1522            Self::LmsShakeM24H25W2Verify => "LMS SHAKE M=24 H=25 W=2 verify",
1523            Self::LmsShakeM24H25W4Verify => "LMS SHAKE M=24 H=25 W=4 verify",
1524            Self::LmsShakeM24H25W8Verify => "LMS SHAKE M=24 H=25 W=8 verify",
1525            Self::XmssVerify => "XMSS verify",
1526            Self::Dh3072 => "DH-3072",
1527        };
1528        f.write_str(name)
1529    }
1530}
1531
1532/// Guard used at the entry point of every approved service, after
1533/// [`require_operational`], to enforce the active algorithm profile.
1534///
1535/// Returns `Ok(())` if the service is permitted under the profile
1536/// selected at initialization, or
1537/// `Err(Error::AlgorithmRestricted { .. })` otherwise.
1538///
1539/// This gate does **not** check [`State`] — callers must call
1540/// [`require_operational`] first. The two-gate pattern is:
1541///
1542/// ```ignore
1543/// require_operational()?;
1544/// require_allowed(Service::Sha256)?;
1545/// // ... perform the operation
1546/// ```
1547pub fn require_allowed(service: Service) -> Result<(), Error> {
1548    let profile = active_profile();
1549    if is_allowed(profile, service) {
1550        Ok(())
1551    } else {
1552        Err(Error::AlgorithmRestricted { service })
1553    }
1554}
1555
1556/// Single source of truth for which services are allowed in each
1557/// profile.
1558const fn is_allowed(profile: AlgorithmProfile, service: Service) -> bool {
1559    match profile {
1560        AlgorithmProfile::Unrestricted => true,
1561        AlgorithmProfile::Cnsa2 => is_cnsa2_allowed(service),
1562        AlgorithmProfile::Cnsa1 => is_cnsa1_allowed(service),
1563        // Derived from the two suites rather than enumerated a third time:
1564        // a hand-written union is a third list that could drift from the
1565        // other two. `is_cnsa2_allowed` already
1566        // covers the LMS block, so naming it again here would be that third
1567        // rule in miniature.
1568        AlgorithmProfile::Migration => is_cnsa1_allowed(service) || is_cnsa2_allowed(service),
1569    }
1570}
1571
1572/// True for every LMS service, matching on the discriminant block the
1573/// `Service` enum reserves for LMS.
1574///
1575/// An arithmetic test rather than 80 enumerated arms: the LMS services
1576/// occupy every odd value in `501..=659` and nothing else does. That is a
1577/// property of the enum, not of this function, so it is pinned separately
1578/// by `lms_discriminant_slots_are_complete_and_pure` — without which a
1579/// variant inserted on an odd value in the block would silently become
1580/// CNSA-permitted.
1581const fn is_lms_service(service: Service) -> bool {
1582    lms_block_contains(service as u16)
1583}
1584
1585/// The arithmetic behind [`is_lms_service`], over a raw discriminant: an odd
1586/// value in `501..=659`.
1587///
1588/// Split out so the boundaries can be tested at all. `is_lms_service` takes a
1589/// `Service`, and the LMS block sits at the very top of the enum — there is no
1590/// variant above 659 — so a widened *upper* bound admits nothing today and no
1591/// `Service`-typed probe can detect it. Over a raw `u16`, both edges and the
1592/// parity are reachable: `lms_block_boundaries_are_exact` pins
1593/// 499/500/501/502/658/659/660/661, which fails if either bound moves in
1594/// either direction or the parity test is dropped.
1595const fn lms_block_contains(discriminant: u16) -> bool {
1596    discriminant >= Service::LmsSha256M32H5W1Verify as u16
1597        && discriminant <= Service::LmsShakeM24H25W8Verify as u16
1598        && discriminant & 1 == 1
1599}
1600
1601/// Every LMS service, in discriminant order: one `Verify` per (LMS, LM-OTS)
1602/// pair of SP 800-208 §4.1–§4.4. Explicit rather than generated: CMVP
1603/// reviewers read this list, and a macro would hide exactly what it must
1604/// show.
1605const LMS_SERVICES: [Service; 80] = [
1606    // SHA-256 / N=32 family (20 pairs).
1607    Service::LmsSha256M32H5W1Verify,
1608    Service::LmsSha256M32H5W2Verify,
1609    Service::LmsSha256M32H5W4Verify,
1610    Service::LmsSha256M32H5W8Verify,
1611    Service::LmsSha256M32H10W1Verify,
1612    Service::LmsSha256M32H10W2Verify,
1613    Service::LmsSha256M32H10W4Verify,
1614    Service::LmsSha256M32H10W8Verify,
1615    Service::LmsSha256M32H15W1Verify,
1616    Service::LmsSha256M32H15W2Verify,
1617    Service::LmsSha256M32H15W4Verify,
1618    Service::LmsSha256M32H15W8Verify,
1619    Service::LmsSha256M32H20W1Verify,
1620    Service::LmsSha256M32H20W2Verify,
1621    Service::LmsSha256M32H20W4Verify,
1622    Service::LmsSha256M32H20W8Verify,
1623    Service::LmsSha256M32H25W1Verify,
1624    Service::LmsSha256M32H25W2Verify,
1625    Service::LmsSha256M32H25W4Verify,
1626    Service::LmsSha256M32H25W8Verify,
1627    // SHA-256 / N=24 family (20 pairs).
1628    Service::LmsSha256M24H5W1Verify,
1629    Service::LmsSha256M24H5W2Verify,
1630    Service::LmsSha256M24H5W4Verify,
1631    Service::LmsSha256M24H5W8Verify,
1632    Service::LmsSha256M24H10W1Verify,
1633    Service::LmsSha256M24H10W2Verify,
1634    Service::LmsSha256M24H10W4Verify,
1635    Service::LmsSha256M24H10W8Verify,
1636    Service::LmsSha256M24H15W1Verify,
1637    Service::LmsSha256M24H15W2Verify,
1638    Service::LmsSha256M24H15W4Verify,
1639    Service::LmsSha256M24H15W8Verify,
1640    Service::LmsSha256M24H20W1Verify,
1641    Service::LmsSha256M24H20W2Verify,
1642    Service::LmsSha256M24H20W4Verify,
1643    Service::LmsSha256M24H20W8Verify,
1644    Service::LmsSha256M24H25W1Verify,
1645    Service::LmsSha256M24H25W2Verify,
1646    Service::LmsSha256M24H25W4Verify,
1647    Service::LmsSha256M24H25W8Verify,
1648    // SHAKE-256 / N=32 family (20 pairs).
1649    Service::LmsShakeM32H5W1Verify,
1650    Service::LmsShakeM32H5W2Verify,
1651    Service::LmsShakeM32H5W4Verify,
1652    Service::LmsShakeM32H5W8Verify,
1653    Service::LmsShakeM32H10W1Verify,
1654    Service::LmsShakeM32H10W2Verify,
1655    Service::LmsShakeM32H10W4Verify,
1656    Service::LmsShakeM32H10W8Verify,
1657    Service::LmsShakeM32H15W1Verify,
1658    Service::LmsShakeM32H15W2Verify,
1659    Service::LmsShakeM32H15W4Verify,
1660    Service::LmsShakeM32H15W8Verify,
1661    Service::LmsShakeM32H20W1Verify,
1662    Service::LmsShakeM32H20W2Verify,
1663    Service::LmsShakeM32H20W4Verify,
1664    Service::LmsShakeM32H20W8Verify,
1665    Service::LmsShakeM32H25W1Verify,
1666    Service::LmsShakeM32H25W2Verify,
1667    Service::LmsShakeM32H25W4Verify,
1668    Service::LmsShakeM32H25W8Verify,
1669    // SHAKE-256 / N=24 family (20 pairs).
1670    Service::LmsShakeM24H5W1Verify,
1671    Service::LmsShakeM24H5W2Verify,
1672    Service::LmsShakeM24H5W4Verify,
1673    Service::LmsShakeM24H5W8Verify,
1674    Service::LmsShakeM24H10W1Verify,
1675    Service::LmsShakeM24H10W2Verify,
1676    Service::LmsShakeM24H10W4Verify,
1677    Service::LmsShakeM24H10W8Verify,
1678    Service::LmsShakeM24H15W1Verify,
1679    Service::LmsShakeM24H15W2Verify,
1680    Service::LmsShakeM24H15W4Verify,
1681    Service::LmsShakeM24H15W8Verify,
1682    Service::LmsShakeM24H20W1Verify,
1683    Service::LmsShakeM24H20W2Verify,
1684    Service::LmsShakeM24H20W4Verify,
1685    Service::LmsShakeM24H20W8Verify,
1686    Service::LmsShakeM24H25W1Verify,
1687    Service::LmsShakeM24H25W2Verify,
1688    Service::LmsShakeM24H25W4Verify,
1689    Service::LmsShakeM24H25W8Verify,
1690];
1691
1692// Build-time check that the LMS services fill the odd discriminants
1693// 501..=659 in order, one per slot, and that `lms_block_contains` admits
1694// each. A missing, duplicated or moved LMS variant fails compilation.
1695// Whether a non-LMS variant sits on an odd slot needs the full variant
1696// list, so `lms_discriminant_slots_are_complete_and_pure` checks that.
1697#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
1698const _: () = {
1699    let mut i = 0;
1700    let mut slot: u16 = 501;
1701    while i < LMS_SERVICES.len() {
1702        let d = LMS_SERVICES[i] as u16;
1703        assert!(d == slot, "LMS service out of its slot");
1704        assert!(lms_block_contains(d), "LMS service outside the LMS block");
1705        slot += 2;
1706        i += 1;
1707    }
1708    assert!(
1709        LMS_SERVICES[79] as u16 == 659,
1710        "LMS block does not end at 659"
1711    );
1712};
1713
1714/// CNSA 2.0 allowed set — CNSSP-15 (March 2025) Annex B.
1715///
1716/// The suite names AES-256, ML-KEM-1024, ML-DSA-87, SHA-384 or SHA-512,
1717/// and the SP 800-208 stateful hash-based signatures for software and
1718/// firmware signing. It names no Keccak-family hash or XOF: SHA-3
1719/// appears only as "allowed for internal hardware functionality", and
1720/// the policy states that algorithms using SHA3 as a component, such as
1721/// LMS or ML-KEM, do not thereby make SHA3 an approved hash function.
1722/// This module is software, so no Keccak service is permitted here —
1723/// ML-KEM and ML-DSA reach Keccak through `new_internal`, which is a
1724/// component path and not a service.
1725const fn is_cnsa2_allowed(service: Service) -> bool {
1726    matches!(
1727        service,
1728        // AES-256 all modes
1729        Service::Aes256Ecb
1730            | Service::Aes256Cbc
1731            | Service::Aes256Ctr
1732            | Service::Aes256Gcm
1733            | Service::Aes256Ccm
1734            | Service::Aes256Kw
1735            | Service::Aes256Kwp
1736            | Service::Aes256
1737            // SHA-384, SHA-512
1738            | Service::Sha384
1739            | Service::Sha512
1740            // No Keccak-family service: Annex B allows SHA3-384/512 only
1741            // for internal hardware functionality, which a software module
1742            // cannot claim, and names no SHAKE, KMAC, TupleHash or
1743            // ParallelHash at all.
1744            // HMAC with allowed hashes
1745            | Service::HmacSha384
1746            | Service::HmacSha512
1747            // CMAC-AES-256
1748            | Service::CmacAes256
1749            // DRBGs backed by AES-256 or SHA-384/512
1750            | Service::CtrDrbgAes256
1751            | Service::HashDrbgSha384
1752            | Service::HashDrbgSha512
1753            | Service::HmacDrbgSha384
1754            | Service::HmacDrbgSha512
1755            // KDFs with allowed backing
1756            | Service::HkdfSha384
1757            | Service::HkdfSha512
1758            | Service::KbkdfHmacSha384
1759            | Service::KbkdfHmacSha512
1760            | Service::KbkdfCmacAes256
1761            // No TLS KDF: Annex B names no protocol-specific key derivation
1762            // function, and none is required in order to operate an algorithm
1763            // the suite does name. Both reach the fail-safe default-block.
1764            // Post-quantum (CNSA 2.0 core)
1765            | Service::MlKem1024Encaps
1766            | Service::MlKem1024Decaps
1767            | Service::MlKem1024Keygen
1768            | Service::MlDsa87Sign
1769            | Service::MlDsa87Verify
1770            | Service::MlDsa87Keygen
1771            // XMSS — SP 800-208 verification. §8.1 confines key generation and
1772            // signing to hardware modules, so the module offers neither.
1773            | Service::XmssVerify
1774    )
1775        // LMS — SP 800-208 stateful HBS for software/firmware signing.
1776        // CNSSP-15 Annex B states "all parameters appropriate to protect to
1777        // TOP SECRET", recommending LMS-SHA-256/192, so every approved
1778        // parameter set the module implements is permitted rather than a
1779        // subset. Every non-LMS service outside the arms above still reaches
1780        // the `matches!` fail-safe default-block.
1781        || is_lms_service(service)
1782}
1783
1784/// CNSA 1.0 allowed set — CNSSP-15 (October 2016) Annex B.
1785///
1786/// Annex B names seven families with their parameters: AES with 256-bit
1787/// keys, ECDH on Curve P-384, ECDSA on Curve P-384, SHA-384, Diffie-Hellman
1788/// with a minimum 3072-bit modulus, RSA with a minimum 3072-bit modulus for
1789/// key establishment, and RSA with a minimum 3072-bit modulus for digital
1790/// signatures. It names no hash other than SHA-384 and no Keccak-family
1791/// algorithm at all — the policy does not cite FIPS PUB 202.
1792///
1793/// The suite names no DRBG, MAC or KDF, because it states what protects
1794/// information rather than every primitive a module needs in order to do
1795/// so. This gate therefore also admits the supporting services required to
1796/// operate the named algorithms **at matching strength**: the DRBGs that
1797/// seed their key generation, and the MAC and KDFs at the SHA-384 /
1798/// AES-256 security level. Anything weaker than the suite requires is
1799/// refused, SHA-256 and its HMAC, DRBG and KDF variants included.
1800///
1801/// Annex B's Additional Information paragraph permits RSA and Diffie-Hellman
1802/// at 2048, SHA-256, and P-256 "in the near term", but only for deployments
1803/// protecting UNCLASSIFIED NSS data or providing community-of-interest
1804/// separation, and it requires consulting NSA before deploying a curve other
1805/// than P-384. Those conditions turn on the classification of the data and on
1806/// an external approval, neither of which a module can evaluate, so this gate
1807/// refuses them and the Security Policy carries the allowance as an operator
1808/// obligation. [`AlgorithmProfile::Migration`] is the profile for a
1809/// deployment that needs both suites at once.
1810const fn is_cnsa1_allowed(service: Service) -> bool {
1811    matches!(
1812        service,
1813        // AES-256 — FIPS PUB 197, "Use 256 bit keys to protect up to TOP SECRET"
1814        Service::Aes256
1815            | Service::Aes256Ecb
1816            | Service::Aes256Cbc
1817            | Service::Aes256Ctr
1818            | Service::Aes256Gcm
1819            | Service::Aes256Ccm
1820            | Service::Aes256Kw
1821            | Service::Aes256Kwp
1822            // ECDH — SP 800-56A Rev 2, "Use Curve P-384 to protect up to TOP SECRET"
1823            | Service::EcdhP384
1824            // ECDSA — FIPS PUB 186-4, "Use Curve P-384 to protect up to TOP SECRET"
1825            | Service::EcdsaP384Keygen
1826            | Service::EcdsaP384Sign
1827            | Service::EcdsaP384Verify
1828            // SHA — FIPS PUB 180-4, "Use SHA-384 to protect up to TOP SECRET"
1829            | Service::Sha384
1830            // Diffie-Hellman — IETF RFC 3526, minimum 3072-bit modulus
1831            | Service::Dh3072
1832            // RSA key establishment — SP 800-56B Rev 1, minimum 3072-bit modulus
1833            | Service::RsaKeygen3072
1834            | Service::RsaKeygen4096
1835            | Service::RsaOaep3072
1836            | Service::RsaOaep4096
1837            // RSA digital signatures — FIPS PUB 186-4, minimum 3072-bit modulus
1838            | Service::RsaPkcs1v15Sign3072
1839            | Service::RsaPkcs1v15Sign4096
1840            | Service::RsaPkcs1v15Verify3072
1841            | Service::RsaPkcs1v15Verify4096
1842            | Service::RsaPssSign3072
1843            | Service::RsaPssSign4096
1844            | Service::RsaPssVerify3072
1845            | Service::RsaPssVerify4096
1846            // Supporting services at the suite's own strength: the DRBGs that
1847            // seed key generation for the algorithms above, and the MAC and
1848            // KDFs at the SHA-384 / AES-256 level.
1849            | Service::CtrDrbgAes256
1850            | Service::HashDrbgSha384
1851            | Service::HmacDrbgSha384
1852            | Service::HmacSha384
1853            | Service::CmacAes256
1854            | Service::HkdfSha384
1855            | Service::KbkdfHmacSha384
1856            | Service::KbkdfCmacAes256
1857    )
1858}
1859
1860// -------------------------------------------------------------------------
1861// Test-only utilities
1862// -------------------------------------------------------------------------
1863//
1864// The global `STATE` makes the normal lifecycle effectively run-once per
1865// process. Unit tests in this crate need to exercise multiple transitions,
1866// so we expose a test-only reset helper gated behind `cfg(test)`. It is
1867// **not** part of the public API and is not compiled into release builds.
1868
1869/// The test-only state lock, in its own module so the guard cannot be forged.
1870///
1871/// The globals are a process singleton and cargo runs unit tests on a parallel
1872/// thread pool, so without serialisation one test's reset lands inside
1873/// another's initialize-then-assert window and the second observes a profile it
1874/// did not install. Measured at 1 failure in 20 runs before this lock, on
1875/// `require_allowed_returns_restricted_error_under_cnsa2`.
1876///
1877/// Serialising is deliberate where per-test isolation would be easier: the
1878/// singleton is the thing under test, and giving each test private state would
1879/// stop exercising it.
1880///
1881/// **Why a module and not merely a private field.** `MutexGuard` carries no
1882/// identity tying it to a particular static, so a `StateGuard` wrapping *any*
1883/// mutex would satisfy the type and still reach `reset`. Confining the type
1884/// here — field private to this module — means the only way to obtain one is
1885/// [`reset_for_test`], which takes the real lock; a sibling module cannot build
1886/// a decoy. Editing this module still defeats it. That is the honest bound, and
1887/// it is a much smaller one than "every future test must remember".
1888#[cfg(test)]
1889pub(crate) mod state_lock {
1890    use super::{AlgorithmProfile, Ordering, PROFILE, STATE, State};
1891
1892    static STATE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1893
1894    /// Proof that the holder owns the state lock. Constructible only by
1895    /// [`reset_for_test`], because the field is private to this module.
1896    #[must_use]
1897    pub(crate) struct StateGuard(
1898        /// Held for its `Drop`, never read: the lock releases when the guard
1899        /// leaves the test's scope.
1900        #[allow(dead_code)]
1901        std::sync::MutexGuard<'static, ()>,
1902    );
1903
1904    impl StateGuard {
1905        /// Re-reset inside a test that already holds the lock — a loop over
1906        /// profiles needs fresh state per iteration without releasing it.
1907        // `&self` is the point: it is the proof the caller holds the lock.
1908        #[allow(clippy::unused_self)]
1909        pub(crate) fn reset(&self) {
1910            STATE.store(State::PowerOff as u64, Ordering::Release);
1911            PROFILE.store(AlgorithmProfile::Unrestricted as u8, Ordering::Release);
1912        }
1913    }
1914
1915    /// Resets the module to power-off and returns the guard that makes the
1916    /// reset safe. The lock is not a convention a future test has to remember:
1917    /// this is the only way to obtain a `StateGuard`, and `StateGuard` is the
1918    /// only way to reach a reset.
1919    ///
1920    /// `StateGuard` is `#[must_use]`, so a bare `reset_for_test();` fails
1921    /// `clippy -D warnings`, which the workspace gate runs — dropping the guard
1922    /// early breaks the build rather than reintroducing the race quietly.
1923    ///
1924    /// Residual, stated rather than implied: `let _ = reset_for_test();` still
1925    /// drops the guard immediately and does not warn. That is a deliberate act,
1926    /// which is the most this construction can bound.
1927    ///
1928    /// A panicking test poisons the mutex. The poison carries no meaning here
1929    /// because the state it protects is unconditionally overwritten on the next
1930    /// line, so it is recovered rather than propagated.
1931    pub(crate) fn reset_for_test() -> StateGuard {
1932        let guard = StateGuard(
1933            STATE_LOCK
1934                .lock()
1935                .unwrap_or_else(std::sync::PoisonError::into_inner),
1936        );
1937        guard.reset();
1938        guard
1939    }
1940}
1941
1942#[cfg(test)]
1943extern crate alloc;
1944
1945#[cfg(test)]
1946extern crate std;
1947
1948#[cfg(test)]
1949#[allow(clippy::unwrap_used, clippy::panic)]
1950mod tests {
1951    use super::state_lock::reset_for_test;
1952    use super::{
1953        AlgorithmProfile, Error, KatEntry, LMS_SERVICES, SelfTest, SelfTestFailure, Service, State,
1954        active_profile, enter_error_state, initialize_with_profile, initialize_with_tests,
1955        is_allowed, is_lms_service, is_operational, lms_block_contains, require_allowed,
1956        require_operational, require_self_test_started, state,
1957    };
1958    use alloc::string::ToString;
1959    use core::sync::atomic::Ordering;
1960
1961    /// Stands in for the pre-operational integrity test.
1962    ///
1963    /// A `cargo test` binary is never signed, so the real integrity test
1964    /// cannot pass inside one. The module requires an integrity group to
1965    /// initialise at all, so a test that needs a gated service declares
1966    /// this stub — visibly, at the call site — rather than the module
1967    /// offering any way to skip the requirement.
1968    const UNSIGNED_TEST_BINARY: &[KatEntry] = &[KatEntry {
1969        name: "integrity not verifiable in an unsigned test binary",
1970        run: || Ok(()),
1971    }];
1972
1973    // Both of these helpers are used as `fn() -> Result<(), SelfTestFailure>`
1974    // function pointers in `KatEntry`, so the `Result` wrapper is mandatory
1975    // even though clippy sees each body as always returning the same variant.
1976    #[allow(clippy::unnecessary_wraps)]
1977    fn always_pass() -> Result<(), SelfTestFailure> {
1978        Ok(())
1979    }
1980    fn always_fail() -> Result<(), SelfTestFailure> {
1981        Err(SelfTestFailure)
1982    }
1983
1984    /// How many times [`count_unreached`] has run.
1985    static UNREACHED_RUNS: core::sync::atomic::AtomicUsize =
1986        core::sync::atomic::AtomicUsize::new(0);
1987
1988    /// A self-test that only counts its runs.
1989    #[allow(clippy::unnecessary_wraps)]
1990    fn count_unreached() -> Result<(), SelfTestFailure> {
1991        UNREACHED_RUNS.fetch_add(1, Ordering::AcqRel);
1992        Ok(())
1993    }
1994
1995    // All tests in this module share the single global `STATE`, so they
1996    // cannot run in parallel. `cargo test` in this crate must be invoked
1997    // with `--test-threads=1` — enforced by the CI workflow.
1998
1999    #[test]
2000    fn initial_state_is_power_off() {
2001        let _guard = reset_for_test();
2002        assert_eq!(state(), State::PowerOff);
2003        assert!(!is_operational());
2004    }
2005
2006    #[test]
2007    fn initialize_transitions_to_operational() {
2008        let _guard = reset_for_test();
2009        initialize_with_tests(UNSIGNED_TEST_BINARY, &[]).unwrap();
2010        assert_eq!(state(), State::Operational);
2011        assert!(is_operational());
2012        require_operational().unwrap();
2013    }
2014
2015    #[test]
2016    fn second_initialize_reports_already_initialized() {
2017        let _guard = reset_for_test();
2018        initialize_with_tests(UNSIGNED_TEST_BINARY, &[]).unwrap();
2019        match initialize_with_tests(UNSIGNED_TEST_BINARY, &[]) {
2020            Err(Error::AlreadyInitialized) => {}
2021            other => panic!("expected AlreadyInitialized, got {other:?}"),
2022        }
2023    }
2024
2025    #[test]
2026    fn require_operational_rejects_power_off() {
2027        let _guard = reset_for_test();
2028        match require_operational() {
2029            Err(Error::NotOperational {
2030                current: State::PowerOff,
2031            }) => {}
2032            other => panic!("expected NotOperational{{PowerOff}}, got {other:?}"),
2033        }
2034    }
2035
2036    #[test]
2037    fn enter_error_state_is_terminal_for_require_operational() {
2038        let _guard = reset_for_test();
2039        initialize_with_tests(UNSIGNED_TEST_BINARY, &[]).unwrap();
2040        assert!(is_operational());
2041        enter_error_state("unit-test forced failure");
2042        assert_eq!(state(), State::Error);
2043        match require_operational() {
2044            Err(Error::NotOperational {
2045                current: State::Error,
2046            }) => {}
2047            other => panic!("expected NotOperational{{Error}}, got {other:?}"),
2048        }
2049    }
2050
2051    /// A power-up test that passes only if the guard admits the state the
2052    /// module is in while the self-tests run.
2053    fn guard_admits_self_test() -> Result<(), SelfTestFailure> {
2054        if state() != State::SelfTest {
2055            return Err(SelfTestFailure);
2056        }
2057        require_self_test_started().map_err(|_| SelfTestFailure)
2058    }
2059
2060    #[test]
2061    fn require_self_test_started_rejects_power_off() {
2062        let _guard = reset_for_test();
2063        assert_eq!(
2064            require_self_test_started(),
2065            Err(Error::NotOperational {
2066                current: State::PowerOff
2067            })
2068        );
2069    }
2070
2071    #[test]
2072    fn require_self_test_started_rejects_error_after_failed_init() {
2073        let _guard = reset_for_test();
2074        let tests = [KatEntry {
2075            name: "dummy-fail",
2076            run: always_fail,
2077        }];
2078        assert!(initialize_with_tests(UNSIGNED_TEST_BINARY, &tests).is_err());
2079        assert_eq!(state(), State::Error);
2080        assert_eq!(
2081            require_self_test_started(),
2082            Err(Error::NotOperational {
2083                current: State::Error
2084            })
2085        );
2086    }
2087
2088    #[test]
2089    fn require_self_test_started_rejects_integrity_unverified() {
2090        let _guard = reset_for_test();
2091        assert_eq!(
2092            initialize_with_tests(&[], &[]),
2093            Err(Error::IntegrityNotAttested)
2094        );
2095        assert_eq!(state(), State::IntegrityUnverified);
2096        assert_eq!(
2097            require_self_test_started(),
2098            Err(Error::NotOperational {
2099                current: State::IntegrityUnverified
2100            })
2101        );
2102    }
2103
2104    /// Initialising again after `IntegrityUnverified` reports
2105    /// `AlreadyFailed`, runs no test, and leaves the state where it was.
2106    #[test]
2107    fn initialize_after_integrity_unverified_is_already_failed() {
2108        let _guard = reset_for_test();
2109        assert_eq!(
2110            initialize_with_tests(&[], &[]),
2111            Err(Error::IntegrityNotAttested)
2112        );
2113        assert_eq!(state(), State::IntegrityUnverified);
2114        let counting = [KatEntry {
2115            name: "counts its runs",
2116            run: count_unreached,
2117        }];
2118        UNREACHED_RUNS.store(0, Ordering::Release);
2119        assert_eq!(
2120            initialize_with_tests(UNSIGNED_TEST_BINARY, &counting),
2121            Err(Error::AlreadyFailed)
2122        );
2123        assert_eq!(UNREACHED_RUNS.load(Ordering::Acquire), 0);
2124        assert_eq!(state(), State::IntegrityUnverified);
2125    }
2126
2127    #[test]
2128    fn require_self_test_started_admits_self_test_and_operational() {
2129        let _guard = reset_for_test();
2130        let tests = [KatEntry {
2131            name: "guard admits SelfTest",
2132            run: guard_admits_self_test,
2133        }];
2134        initialize_with_tests(UNSIGNED_TEST_BINARY, &tests).unwrap();
2135        assert_eq!(state(), State::Operational);
2136        assert_eq!(require_self_test_started(), Ok(()));
2137    }
2138
2139    /// Every word this module never writes reads as `Error`, and both guards
2140    /// refuse it.
2141    #[test]
2142    fn a_malformed_state_word_reads_as_error() {
2143        let guard = reset_for_test();
2144        let owner = 0x1234_u64 << super::STATE_BITS;
2145        let malformed = [
2146            // `SelfTest` with no owner.
2147            State::SelfTest as u64,
2148            // `Operational`, `PowerOff` and `IntegrityUnverified` with owner bits.
2149            State::Operational as u64 | owner,
2150            State::PowerOff as u64 | owner,
2151            State::IntegrityUnverified as u64 | owner,
2152            // Unknown discriminants, with and without owner bits.
2153            5,
2154            0xff,
2155            0xff | owner,
2156        ];
2157        assert_eq!(malformed.len(), 7);
2158        for word in malformed {
2159            guard.reset();
2160            super::STATE.store(word, Ordering::Release);
2161            assert_eq!(state(), State::Error, "word {word:#x}");
2162            assert!(!is_operational(), "word {word:#x}");
2163            assert_eq!(
2164                require_operational(),
2165                Err(Error::NotOperational {
2166                    current: State::Error
2167                }),
2168                "word {word:#x}"
2169            );
2170            assert_eq!(
2171                require_self_test_started(),
2172                Err(Error::NotOperational {
2173                    current: State::Error
2174                }),
2175                "word {word:#x}"
2176            );
2177        }
2178        // Control: the well-formed words still decode to themselves.
2179        guard.reset();
2180        assert_eq!(state(), State::PowerOff);
2181        super::STATE.store(State::Operational as u64, Ordering::Release);
2182        assert_eq!(state(), State::Operational);
2183    }
2184
2185    /// Released by [`loser_waits_then_gets_already_failed`] once its loser is
2186    /// seen waiting.
2187    static RELEASE_FAILING_WINNER: core::sync::atomic::AtomicBool =
2188        core::sync::atomic::AtomicBool::new(false);
2189    /// Set by the failing winner once it holds `SelfTest`.
2190    static FAILING_WINNER_PAUSED: core::sync::atomic::AtomicBool =
2191        core::sync::atomic::AtomicBool::new(false);
2192
2193    /// Waits for `done`, failing the test after 30 seconds.
2194    fn wait_until(what: &str, done: impl Fn() -> bool) {
2195        let start = std::time::Instant::now();
2196        while !done() {
2197            assert!(
2198                start.elapsed() < std::time::Duration::from_secs(30),
2199                "timed out waiting for {what}"
2200            );
2201            std::thread::yield_now();
2202        }
2203    }
2204
2205    fn paused_then_fails() -> Result<(), SelfTestFailure> {
2206        FAILING_WINNER_PAUSED.store(true, Ordering::Release);
2207        wait_until("the release of the failing winner", || {
2208            RELEASE_FAILING_WINNER.load(Ordering::Acquire)
2209        });
2210        Err(SelfTestFailure)
2211    }
2212
2213    /// A loser of a winner whose self-tests then fail is seen waiting in
2214    /// the loser path, and once the winner fails it reports `AlreadyFailed`.
2215    #[test]
2216    fn loser_waits_then_gets_already_failed() {
2217        let _guard = reset_for_test();
2218        FAILING_WINNER_PAUSED.store(false, Ordering::Release);
2219        RELEASE_FAILING_WINNER.store(false, Ordering::Release);
2220        let waiting_before = super::WAITING_LOSERS.load(Ordering::Acquire);
2221        let winner = std::thread::spawn(|| {
2222            let tests = [KatEntry {
2223                name: "pauses, then fails",
2224                run: paused_then_fails,
2225            }];
2226            initialize_with_tests(UNSIGNED_TEST_BINARY, &tests)
2227        });
2228        wait_until("the winner to pause", || {
2229            FAILING_WINNER_PAUSED.load(Ordering::Acquire)
2230        });
2231        let loser = std::thread::spawn(|| initialize_with_tests(UNSIGNED_TEST_BINARY, &[]));
2232        wait_until("the loser to enter its wait", || {
2233            super::WAITING_LOSERS.load(Ordering::Acquire) > waiting_before
2234        });
2235        assert!(!loser.is_finished());
2236        RELEASE_FAILING_WINNER.store(true, Ordering::Release);
2237        assert_eq!(
2238            winner.join().unwrap(),
2239            Err(Error::SelfTestFailed {
2240                test: "pauses, then fails"
2241            })
2242        );
2243        assert_eq!(loser.join().unwrap(), Err(Error::AlreadyFailed));
2244        assert_eq!(state(), State::Error);
2245    }
2246
2247    /// The owner field records only a non-zero id below `2^56`.
2248    #[test]
2249    fn the_owner_id_filter_admits_only_recordable_ids() {
2250        assert_eq!(super::owner_id(None), None);
2251        assert_eq!(super::owner_id(Some(0)), None);
2252        assert_eq!(super::owner_id(Some(1)), Some(1));
2253        assert_eq!(super::owner_id(Some((1 << 56) - 1)), Some((1 << 56) - 1));
2254        assert_eq!(super::owner_id(Some(1 << 56)), None);
2255        assert_eq!(super::owner_id(Some(u64::MAX)), None);
2256    }
2257
2258    /// A caller with no recordable thread id latches `Error`, names the
2259    /// cause, and runs no self-test.
2260    #[test]
2261    fn no_recordable_thread_id_latches_error_with_its_cause() {
2262        let guard = reset_for_test();
2263        let tests = [KatEntry {
2264            name: "must not run",
2265            run: always_fail,
2266        }];
2267        for raw in [None, Some(0), Some(1 << 56)] {
2268            guard.reset();
2269            assert_eq!(
2270                super::initialize_as(
2271                    super::owner_id(raw),
2272                    UNSIGNED_TEST_BINARY,
2273                    &tests,
2274                    AlgorithmProfile::Unrestricted
2275                ),
2276                Err(Error::SelfTestFailed {
2277                    test: super::THREAD_ID_CHECK
2278                }),
2279                "raw id {raw:?}"
2280            );
2281            assert_eq!(state(), State::Error);
2282            assert_eq!(
2283                initialize_with_tests(UNSIGNED_TEST_BINARY, &[]),
2284                Err(Error::AlreadyFailed)
2285            );
2286        }
2287    }
2288
2289    #[test]
2290    fn state_display_is_stable() {
2291        // The exact strings appear in log output and audit trails,
2292        // so pin them.
2293        assert_eq!(State::PowerOff.to_string(), "PowerOff");
2294        assert_eq!(State::SelfTest.to_string(), "SelfTest");
2295        assert_eq!(State::Operational.to_string(), "Operational");
2296        assert_eq!(State::Error.to_string(), "Error");
2297    }
2298
2299    #[test]
2300    fn registry_runs_passing_tests_and_reaches_operational() {
2301        let _guard = reset_for_test();
2302        let tests = [
2303            KatEntry {
2304                name: "dummy-pass-a",
2305                run: always_pass,
2306            },
2307            KatEntry {
2308                name: "dummy-pass-b",
2309                run: always_pass,
2310            },
2311        ];
2312        initialize_with_tests(UNSIGNED_TEST_BINARY, &tests).unwrap();
2313        assert_eq!(state(), State::Operational);
2314    }
2315
2316    #[test]
2317    fn registry_failing_test_latches_error_and_returns_name() {
2318        let _guard = reset_for_test();
2319        let tests = [
2320            KatEntry {
2321                name: "dummy-pass",
2322                run: always_pass,
2323            },
2324            KatEntry {
2325                name: "dummy-fail",
2326                run: always_fail,
2327            },
2328            // The runner stops at the first failure, so this entry never runs.
2329            KatEntry {
2330                name: "dummy-unreached",
2331                run: count_unreached,
2332            },
2333        ];
2334        UNREACHED_RUNS.store(0, Ordering::Release);
2335        match initialize_with_tests(UNSIGNED_TEST_BINARY, &tests) {
2336            Err(Error::SelfTestFailed { test: "dummy-fail" }) => {}
2337            other => panic!("expected SelfTestFailed{{dummy-fail}}, got {other:?}"),
2338        }
2339        assert_eq!(UNREACHED_RUNS.load(Ordering::Acquire), 0);
2340        assert_eq!(state(), State::Error);
2341        // Operational-only guard must reject calls from here on.
2342        assert!(matches!(
2343            require_operational(),
2344            Err(Error::NotOperational {
2345                current: State::Error
2346            })
2347        ));
2348    }
2349
2350    // Trivial SelfTest implementation exercised outside the registry
2351    // so the trait stays part of the compiled public surface.
2352    struct DummyTest;
2353    impl SelfTest for DummyTest {
2354        const NAME: &'static str = "dummy-trait-vector";
2355        fn run() -> Result<(), SelfTestFailure> {
2356            Ok(())
2357        }
2358    }
2359
2360    #[test]
2361    fn dummy_self_test_trait_is_usable() {
2362        assert_eq!(DummyTest::NAME, "dummy-trait-vector");
2363        DummyTest::run().unwrap();
2364    }
2365
2366    // =====================================================================
2367    // Algorithm profile gating tests
2368    // =====================================================================
2369
2370    #[test]
2371    fn default_profile_is_unrestricted() {
2372        let _guard = reset_for_test();
2373        assert_eq!(active_profile(), AlgorithmProfile::Unrestricted);
2374    }
2375
2376    #[test]
2377    fn initialize_with_profile_sets_cnsa2() {
2378        let _guard = reset_for_test();
2379        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa2).unwrap();
2380        assert_eq!(active_profile(), AlgorithmProfile::Cnsa2);
2381        assert!(is_operational());
2382    }
2383
2384    /// A second call on an initialised module, with a different profile,
2385    /// leaves the first call's profile active.
2386    #[test]
2387    fn a_second_call_does_not_change_the_profile() {
2388        let _guard = reset_for_test();
2389        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa2).unwrap();
2390        match initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Unrestricted) {
2391            Err(Error::AlreadyInitialized) => {}
2392            other => panic!("expected AlreadyInitialized, got {other:?}"),
2393        }
2394        assert_eq!(active_profile(), AlgorithmProfile::Cnsa2);
2395        match initialize_with_tests(UNSIGNED_TEST_BINARY, &[]) {
2396            Err(Error::AlreadyInitialized) => {}
2397            other => panic!("expected AlreadyInitialized, got {other:?}"),
2398        }
2399        assert_eq!(active_profile(), AlgorithmProfile::Cnsa2);
2400    }
2401
2402    /// Holds the winning call inside its self-tests until the racing call
2403    /// has been made.
2404    static RACER_CALLED: core::sync::atomic::AtomicBool =
2405        core::sync::atomic::AtomicBool::new(false);
2406
2407    #[allow(clippy::unnecessary_wraps)]
2408    fn wait_for_racer() -> Result<(), SelfTestFailure> {
2409        while !RACER_CALLED.load(Ordering::Acquire) {
2410            std::thread::yield_now();
2411        }
2412        // The racer sets the flag immediately before it calls in; give that
2413        // call time to reach the CAS while this one is still in `SelfTest`.
2414        std::thread::sleep(std::time::Duration::from_millis(50));
2415        Ok(())
2416    }
2417
2418    /// A call made while another is running its self-tests leaves the
2419    /// running call's profile active once both return.
2420    #[test]
2421    fn a_racing_call_does_not_change_the_profile() {
2422        let _guard = reset_for_test();
2423        RACER_CALLED.store(false, Ordering::Release);
2424        let tests = [KatEntry {
2425            name: "waits for the racing call",
2426            run: wait_for_racer,
2427        }];
2428        let winner = std::thread::spawn(move || {
2429            initialize_with_profile(UNSIGNED_TEST_BINARY, &tests, AlgorithmProfile::Cnsa2)
2430        });
2431        while state() != State::SelfTest {
2432            std::thread::yield_now();
2433        }
2434        RACER_CALLED.store(true, Ordering::Release);
2435        let racer =
2436            initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Unrestricted);
2437        let winner = winner.join().unwrap();
2438        assert!(winner.is_ok(), "the first call failed: {winner:?}");
2439        match racer {
2440            Err(Error::AlreadyInitialized) => {}
2441            other => panic!("expected AlreadyInitialized, got {other:?}"),
2442        }
2443        assert_eq!(active_profile(), AlgorithmProfile::Cnsa2);
2444    }
2445
2446    /// A winning call whose self-tests fail leaves the default profile, so
2447    /// a module in `Error` does not report the profile it never served.
2448    #[test]
2449    fn a_failed_initialisation_leaves_the_default_profile() {
2450        let guard = reset_for_test();
2451        let failing = [KatEntry {
2452            name: "always fails",
2453            run: always_fail,
2454        }];
2455        match initialize_with_profile(UNSIGNED_TEST_BINARY, &failing, AlgorithmProfile::Cnsa2) {
2456            Err(Error::SelfTestFailed { .. }) => {}
2457            other => panic!("expected SelfTestFailed, got {other:?}"),
2458        }
2459        assert_eq!(state(), State::Error);
2460        assert_eq!(active_profile(), AlgorithmProfile::Unrestricted);
2461
2462        guard.reset();
2463        match initialize_with_profile(&[], &[], AlgorithmProfile::Cnsa1) {
2464            Err(Error::IntegrityNotAttested) => {}
2465            other => panic!("expected IntegrityNotAttested, got {other:?}"),
2466        }
2467        assert_eq!(active_profile(), AlgorithmProfile::Unrestricted);
2468    }
2469
2470    #[test]
2471    fn initialize_with_profile_sets_cnsa1() {
2472        let _guard = reset_for_test();
2473        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa1).unwrap();
2474        assert_eq!(active_profile(), AlgorithmProfile::Cnsa1);
2475        assert!(is_operational());
2476    }
2477
2478    #[test]
2479    fn unrestricted_allows_all_services() {
2480        // Spot-check: every category should pass in Unrestricted.
2481        let spot = [
2482            Service::Sha1,
2483            Service::Sha256,
2484            Service::Aes128Ecb,
2485            Service::Aes256Gcm,
2486            Service::CtrDrbgAes128,
2487            Service::EcdsaP256Sign,
2488            Service::Ed25519Sign,
2489            Service::RsaPssSign2048,
2490            Service::MlKem1024Encaps,
2491            Service::LmsSha256M32H10W4Verify,
2492            Service::Dh3072,
2493            Service::SlhDsaSha2256sSign,
2494        ];
2495        for svc in spot {
2496            assert!(
2497                is_allowed(AlgorithmProfile::Unrestricted, svc),
2498                "{svc} should be allowed in Unrestricted"
2499            );
2500        }
2501    }
2502
2503    #[test]
2504    fn cnsa2_allows_aes256_and_pq() {
2505        let allowed = [
2506            Service::Aes256Gcm,
2507            Service::Aes256Cbc,
2508            Service::Sha384,
2509            Service::Sha512,
2510            Service::HmacSha384,
2511            Service::HmacSha512,
2512            Service::CtrDrbgAes256,
2513            Service::HashDrbgSha384,
2514            Service::HmacDrbgSha512,
2515            Service::MlKem1024Encaps,
2516            Service::MlKem1024Decaps,
2517            Service::MlKem1024Keygen,
2518            Service::MlDsa87Sign,
2519            Service::MlDsa87Verify,
2520            Service::MlDsa87Keygen,
2521            Service::LmsSha256M32H10W4Verify,
2522            Service::XmssVerify,
2523        ];
2524        for svc in allowed {
2525            assert!(
2526                is_allowed(AlgorithmProfile::Cnsa2, svc),
2527                "{svc} should be allowed in CNSA 2.0"
2528            );
2529        }
2530    }
2531
2532    /// CNSSP-15 (March 2025) Annex B names no Keccak-family hash or XOF.
2533    /// SHA-3 appears only as "allowed for internal hardware functionality",
2534    /// and the policy states outright that algorithms using SHA3 as a
2535    /// component — LMS and ML-KEM are its examples — do not thereby make
2536    /// SHA3 an approved hash function. This module is software, so the whole
2537    /// family is refused under CNSA 2.0. Pinned as a property because the
2538    /// gate permitted all twelve until 2026-08-18.
2539    #[test]
2540    fn cnsa2_refuses_the_keccak_family() {
2541        let refused = [
2542            Service::Shake256,
2543            Service::CShake256,
2544            Service::Kmac256,
2545            Service::KmacXof256,
2546            Service::TupleHash256,
2547            Service::TupleHashXof256,
2548            Service::ParallelHash256,
2549            Service::ParallelHashXof256,
2550            Service::Sha3_384,
2551            Service::Sha3_512,
2552            Service::HmacSha3_384,
2553            Service::HmacSha3_512,
2554        ];
2555        for svc in refused {
2556            assert!(
2557                !is_allowed(AlgorithmProfile::Cnsa2, svc),
2558                "{svc} is not in CNSA 2.0 and must be refused"
2559            );
2560        }
2561        // Control: the suite's own hash must still pass, or this test would
2562        // also pass against a gate that refuses everything.
2563        assert!(is_allowed(AlgorithmProfile::Cnsa2, Service::Sha384));
2564        // Control: ML-KEM reaches Keccak internally and must be unaffected.
2565        assert!(is_allowed(
2566            AlgorithmProfile::Cnsa2,
2567            Service::MlKem1024Encaps
2568        ));
2569    }
2570
2571    #[test]
2572    fn cnsa2_blocks_classical_and_small_keys() {
2573        let blocked = [
2574            Service::Sha1,
2575            Service::Sha224,
2576            Service::Sha256,
2577            Service::Sha512_224,
2578            Service::Sha512_256,
2579            Service::Sha3_224,
2580            Service::Sha3_256,
2581            Service::Shake128,
2582            Service::Kmac128,
2583            Service::Aes128Ecb,
2584            Service::Aes128Gcm,
2585            Service::Aes192Cbc,
2586            Service::CtrDrbgAes128,
2587            Service::CtrDrbgAes192,
2588            Service::HashDrbgSha256,
2589            Service::HmacDrbgSha256,
2590            Service::HmacSha1,
2591            Service::HmacSha256,
2592            Service::CmacAes128,
2593            Service::CmacAes192,
2594            Service::EcdsaP256Sign,
2595            Service::EcdsaP256Verify,
2596            Service::EcdhP256,
2597            Service::Ed25519Sign,
2598            Service::Ed25519Verify,
2599            Service::RsaPssSign2048,
2600            Service::RsaOaep2048,
2601            // No SLH-DSA parameter set is in CNSA 2.0 — CNSSP-15 lists none.
2602            Service::SlhDsaSha2128sSign,
2603            Service::MlDsa44Sign,
2604            Service::MlDsa44Verify,
2605            Service::MlDsa44Keygen,
2606            Service::MlDsa65Sign,
2607            Service::MlDsa65Verify,
2608            Service::MlDsa65Keygen,
2609        ];
2610        for svc in blocked {
2611            assert!(
2612                !is_allowed(AlgorithmProfile::Cnsa2, svc),
2613                "{svc} should be BLOCKED in CNSA 2.0"
2614            );
2615        }
2616    }
2617
2618    /// Every `Service` variant, so a profile's permitted set can be computed
2619    /// rather than sampled.
2620    ///
2621    /// The earlier membership test asserted only that an expected list was a
2622    /// SUBSET of what the gate permits, with a length assertion on the test's
2623    /// own array — a constant. It therefore could not see a widening: adding
2624    /// `RsaKeygen2048` to the CNSA 1.0 gate left it green, which is precisely
2625    /// the drift the test existed to catch. Enumerating the enum makes the
2626    /// comparison exact in both directions.
2627    ///
2628    /// The array type fixes the element count, so the compiler rejects a list
2629    /// of the wrong length. A variant added to `Service` but not listed here
2630    /// is not caught by this list; the exhaustive `Display` match is where the
2631    /// compiler forces every variant to be named.
2632    const ALL_SERVICES: [Service; 256] = [
2633        Service::Sha1,
2634        Service::Sha224,
2635        Service::Sha256,
2636        Service::Sha384,
2637        Service::Sha512,
2638        Service::Sha512_224,
2639        Service::Sha512_256,
2640        Service::Sha3_224,
2641        Service::Sha3_256,
2642        Service::Sha3_384,
2643        Service::Sha3_512,
2644        Service::Shake128,
2645        Service::Shake256,
2646        Service::CShake128,
2647        Service::CShake256,
2648        Service::Kmac128,
2649        Service::Kmac256,
2650        Service::KmacXof128,
2651        Service::KmacXof256,
2652        Service::TupleHash128,
2653        Service::TupleHash256,
2654        Service::TupleHashXof128,
2655        Service::TupleHashXof256,
2656        Service::ParallelHash128,
2657        Service::ParallelHash256,
2658        Service::ParallelHashXof128,
2659        Service::ParallelHashXof256,
2660        Service::HmacSha1,
2661        Service::HmacSha224,
2662        Service::HmacSha256,
2663        Service::HmacSha384,
2664        Service::HmacSha512,
2665        Service::HmacSha512_224,
2666        Service::HmacSha512_256,
2667        Service::HmacSha3_224,
2668        Service::HmacSha3_256,
2669        Service::HmacSha3_384,
2670        Service::HmacSha3_512,
2671        Service::CmacAes128,
2672        Service::CmacAes192,
2673        Service::CmacAes256,
2674        Service::Aes128Ecb,
2675        Service::Aes128Cbc,
2676        Service::Aes128Ctr,
2677        Service::Aes128Gcm,
2678        Service::Aes128Ccm,
2679        Service::Aes128Kw,
2680        Service::Aes128Kwp,
2681        Service::Aes192Ecb,
2682        Service::Aes192Cbc,
2683        Service::Aes192Ctr,
2684        Service::Aes192Gcm,
2685        Service::Aes192Ccm,
2686        Service::Aes192Kw,
2687        Service::Aes192Kwp,
2688        Service::Aes256Ecb,
2689        Service::Aes256Cbc,
2690        Service::Aes256Ctr,
2691        Service::Aes256Gcm,
2692        Service::Aes256Ccm,
2693        Service::Aes256Kw,
2694        Service::Aes256Kwp,
2695        Service::Aes128,
2696        Service::Aes192,
2697        Service::Aes256,
2698        Service::CtrDrbgAes128,
2699        Service::CtrDrbgAes192,
2700        Service::CtrDrbgAes256,
2701        Service::HashDrbgSha256,
2702        Service::HashDrbgSha384,
2703        Service::HashDrbgSha512,
2704        Service::HmacDrbgSha256,
2705        Service::HmacDrbgSha384,
2706        Service::HmacDrbgSha512,
2707        Service::HkdfSha1,
2708        Service::HkdfSha256,
2709        Service::HkdfSha384,
2710        Service::HkdfSha512,
2711        Service::KbkdfHmacSha256,
2712        Service::KbkdfHmacSha384,
2713        Service::KbkdfHmacSha512,
2714        Service::KbkdfCmacAes128,
2715        Service::KbkdfCmacAes192,
2716        Service::KbkdfCmacAes256,
2717        Service::Pbkdf2HmacSha1,
2718        Service::Pbkdf2HmacSha224,
2719        Service::Pbkdf2HmacSha256,
2720        Service::Pbkdf2HmacSha384,
2721        Service::Pbkdf2HmacSha512,
2722        Service::RsaKeygen2048,
2723        Service::RsaPkcs1v15Sign2048,
2724        Service::RsaPssSign2048,
2725        Service::RsaOaep2048,
2726        Service::RsaPkcs1v15Verify2048,
2727        Service::RsaPssVerify2048,
2728        Service::RsaKeygen3072,
2729        Service::RsaPkcs1v15Sign3072,
2730        Service::RsaPssSign3072,
2731        Service::RsaOaep3072,
2732        Service::RsaPkcs1v15Verify3072,
2733        Service::RsaPssVerify3072,
2734        Service::RsaKeygen4096,
2735        Service::RsaPkcs1v15Sign4096,
2736        Service::RsaPssSign4096,
2737        Service::RsaOaep4096,
2738        Service::RsaPkcs1v15Verify4096,
2739        Service::RsaPssVerify4096,
2740        Service::EcdsaP256Sign,
2741        Service::EcdsaP256Verify,
2742        Service::EcdsaP256Keygen,
2743        Service::EcdsaP384Sign,
2744        Service::EcdsaP384Verify,
2745        Service::EcdsaP384Keygen,
2746        Service::EcdhP256,
2747        Service::EcdhP384,
2748        Service::Ed25519Sign,
2749        Service::Ed25519Verify,
2750        Service::Ed25519Keygen,
2751        Service::Tls12Kdf,
2752        Service::Tls13Kdf,
2753        Service::MlKem1024Encaps,
2754        Service::MlKem1024Decaps,
2755        Service::MlKem1024Keygen,
2756        Service::MlKem512Encaps,
2757        Service::MlKem512Decaps,
2758        Service::MlKem512Keygen,
2759        Service::MlKem768Encaps,
2760        Service::MlKem768Decaps,
2761        Service::MlKem768Keygen,
2762        Service::MlDsa87Sign,
2763        Service::MlDsa87Verify,
2764        Service::MlDsa87Keygen,
2765        Service::MlDsa44Sign,
2766        Service::MlDsa44Verify,
2767        Service::MlDsa44Keygen,
2768        Service::MlDsa65Sign,
2769        Service::MlDsa65Verify,
2770        Service::MlDsa65Keygen,
2771        Service::SlhDsaSha2256sKeygen,
2772        Service::SlhDsaSha2256sSign,
2773        Service::SlhDsaSha2256sVerify,
2774        Service::SlhDsaSha2128sKeygen,
2775        Service::SlhDsaSha2128sSign,
2776        Service::SlhDsaSha2128sVerify,
2777        Service::SlhDsaSha2128fKeygen,
2778        Service::SlhDsaSha2128fSign,
2779        Service::SlhDsaSha2128fVerify,
2780        Service::SlhDsaSha2192sKeygen,
2781        Service::SlhDsaSha2192sSign,
2782        Service::SlhDsaSha2192sVerify,
2783        Service::SlhDsaSha2192fKeygen,
2784        Service::SlhDsaSha2192fSign,
2785        Service::SlhDsaSha2192fVerify,
2786        Service::SlhDsaSha2256fKeygen,
2787        Service::SlhDsaSha2256fSign,
2788        Service::SlhDsaSha2256fVerify,
2789        Service::SlhDsaShake128sKeygen,
2790        Service::SlhDsaShake128sSign,
2791        Service::SlhDsaShake128sVerify,
2792        Service::SlhDsaShake128fKeygen,
2793        Service::SlhDsaShake128fSign,
2794        Service::SlhDsaShake128fVerify,
2795        Service::SlhDsaShake192sKeygen,
2796        Service::SlhDsaShake192sSign,
2797        Service::SlhDsaShake192sVerify,
2798        Service::SlhDsaShake192fKeygen,
2799        Service::SlhDsaShake192fSign,
2800        Service::SlhDsaShake192fVerify,
2801        Service::SlhDsaShake256sKeygen,
2802        Service::SlhDsaShake256sSign,
2803        Service::SlhDsaShake256sVerify,
2804        Service::SlhDsaShake256fKeygen,
2805        Service::SlhDsaShake256fSign,
2806        Service::SlhDsaShake256fVerify,
2807        Service::LmsSha256M32H5W1Verify,
2808        Service::LmsSha256M32H5W2Verify,
2809        Service::LmsSha256M32H5W4Verify,
2810        Service::LmsSha256M32H5W8Verify,
2811        Service::LmsSha256M32H10W1Verify,
2812        Service::LmsSha256M32H10W2Verify,
2813        Service::LmsSha256M32H10W4Verify,
2814        Service::LmsSha256M32H10W8Verify,
2815        Service::LmsSha256M32H15W1Verify,
2816        Service::LmsSha256M32H15W2Verify,
2817        Service::LmsSha256M32H15W4Verify,
2818        Service::LmsSha256M32H15W8Verify,
2819        Service::LmsSha256M32H20W1Verify,
2820        Service::LmsSha256M32H20W2Verify,
2821        Service::LmsSha256M32H20W4Verify,
2822        Service::LmsSha256M32H20W8Verify,
2823        Service::LmsSha256M32H25W1Verify,
2824        Service::LmsSha256M32H25W2Verify,
2825        Service::LmsSha256M32H25W4Verify,
2826        Service::LmsSha256M32H25W8Verify,
2827        Service::LmsSha256M24H5W1Verify,
2828        Service::LmsSha256M24H5W2Verify,
2829        Service::LmsSha256M24H5W4Verify,
2830        Service::LmsSha256M24H5W8Verify,
2831        Service::LmsSha256M24H10W1Verify,
2832        Service::LmsSha256M24H10W2Verify,
2833        Service::LmsSha256M24H10W4Verify,
2834        Service::LmsSha256M24H10W8Verify,
2835        Service::LmsSha256M24H15W1Verify,
2836        Service::LmsSha256M24H15W2Verify,
2837        Service::LmsSha256M24H15W4Verify,
2838        Service::LmsSha256M24H15W8Verify,
2839        Service::LmsSha256M24H20W1Verify,
2840        Service::LmsSha256M24H20W2Verify,
2841        Service::LmsSha256M24H20W4Verify,
2842        Service::LmsSha256M24H20W8Verify,
2843        Service::LmsSha256M24H25W1Verify,
2844        Service::LmsSha256M24H25W2Verify,
2845        Service::LmsSha256M24H25W4Verify,
2846        Service::LmsSha256M24H25W8Verify,
2847        Service::LmsShakeM32H5W1Verify,
2848        Service::LmsShakeM32H5W2Verify,
2849        Service::LmsShakeM32H5W4Verify,
2850        Service::LmsShakeM32H5W8Verify,
2851        Service::LmsShakeM32H10W1Verify,
2852        Service::LmsShakeM32H10W2Verify,
2853        Service::LmsShakeM32H10W4Verify,
2854        Service::LmsShakeM32H10W8Verify,
2855        Service::LmsShakeM32H15W1Verify,
2856        Service::LmsShakeM32H15W2Verify,
2857        Service::LmsShakeM32H15W4Verify,
2858        Service::LmsShakeM32H15W8Verify,
2859        Service::LmsShakeM32H20W1Verify,
2860        Service::LmsShakeM32H20W2Verify,
2861        Service::LmsShakeM32H20W4Verify,
2862        Service::LmsShakeM32H20W8Verify,
2863        Service::LmsShakeM32H25W1Verify,
2864        Service::LmsShakeM32H25W2Verify,
2865        Service::LmsShakeM32H25W4Verify,
2866        Service::LmsShakeM32H25W8Verify,
2867        Service::LmsShakeM24H5W1Verify,
2868        Service::LmsShakeM24H5W2Verify,
2869        Service::LmsShakeM24H5W4Verify,
2870        Service::LmsShakeM24H5W8Verify,
2871        Service::LmsShakeM24H10W1Verify,
2872        Service::LmsShakeM24H10W2Verify,
2873        Service::LmsShakeM24H10W4Verify,
2874        Service::LmsShakeM24H10W8Verify,
2875        Service::LmsShakeM24H15W1Verify,
2876        Service::LmsShakeM24H15W2Verify,
2877        Service::LmsShakeM24H15W4Verify,
2878        Service::LmsShakeM24H15W8Verify,
2879        Service::LmsShakeM24H20W1Verify,
2880        Service::LmsShakeM24H20W2Verify,
2881        Service::LmsShakeM24H20W4Verify,
2882        Service::LmsShakeM24H20W8Verify,
2883        Service::LmsShakeM24H25W1Verify,
2884        Service::LmsShakeM24H25W2Verify,
2885        Service::LmsShakeM24H25W4Verify,
2886        Service::LmsShakeM24H25W8Verify,
2887        Service::XmssVerify,
2888        Service::Dh3072,
2889    ];
2890
2891    /// CNSA 1.0 permits exactly the CNSSP-15 (October 2016) Annex B suite and
2892    /// the supporting services it needs at matching strength.
2893    const EXPECTED_CNSA1: [Service; 34] = [
2894        Service::Sha384,
2895        Service::HmacSha384,
2896        Service::CmacAes256,
2897        Service::Aes256Ecb,
2898        Service::Aes256Cbc,
2899        Service::Aes256Ctr,
2900        Service::Aes256Gcm,
2901        Service::Aes256Ccm,
2902        Service::Aes256Kw,
2903        Service::Aes256Kwp,
2904        Service::Aes256,
2905        Service::CtrDrbgAes256,
2906        Service::HashDrbgSha384,
2907        Service::HmacDrbgSha384,
2908        Service::HkdfSha384,
2909        Service::KbkdfHmacSha384,
2910        Service::KbkdfCmacAes256,
2911        Service::RsaKeygen3072,
2912        Service::RsaPkcs1v15Sign3072,
2913        Service::RsaPssSign3072,
2914        Service::RsaOaep3072,
2915        Service::RsaPkcs1v15Verify3072,
2916        Service::RsaPssVerify3072,
2917        Service::RsaKeygen4096,
2918        Service::RsaPkcs1v15Sign4096,
2919        Service::RsaPssSign4096,
2920        Service::RsaOaep4096,
2921        Service::RsaPkcs1v15Verify4096,
2922        Service::RsaPssVerify4096,
2923        Service::EcdsaP384Sign,
2924        Service::EcdsaP384Verify,
2925        Service::EcdsaP384Keygen,
2926        Service::EcdhP384,
2927        Service::Dh3072,
2928    ];
2929
2930    /// CNSA 2.0's non-LMS membership. The 80 LMS services are asserted by
2931    /// count rather than listed, because `LMS_SERVICES` already enumerates them.
2932    const EXPECTED_CNSA2_NON_LMS: [Service; 30] = [
2933        Service::Sha384,
2934        Service::Sha512,
2935        Service::HmacSha384,
2936        Service::HmacSha512,
2937        Service::CmacAes256,
2938        Service::Aes256Ecb,
2939        Service::Aes256Cbc,
2940        Service::Aes256Ctr,
2941        Service::Aes256Gcm,
2942        Service::Aes256Ccm,
2943        Service::Aes256Kw,
2944        Service::Aes256Kwp,
2945        Service::Aes256,
2946        Service::CtrDrbgAes256,
2947        Service::HashDrbgSha384,
2948        Service::HashDrbgSha512,
2949        Service::HmacDrbgSha384,
2950        Service::HmacDrbgSha512,
2951        Service::HkdfSha384,
2952        Service::HkdfSha512,
2953        Service::KbkdfHmacSha384,
2954        Service::KbkdfHmacSha512,
2955        Service::KbkdfCmacAes256,
2956        Service::MlKem1024Encaps,
2957        Service::MlKem1024Decaps,
2958        Service::MlKem1024Keygen,
2959        Service::MlDsa87Sign,
2960        Service::MlDsa87Verify,
2961        Service::MlDsa87Keygen,
2962        Service::XmssVerify,
2963    ];
2964
2965    /// Assert a profile permits EXACTLY `expected` (plus the LMS block when
2966    /// `allow_lms`), checked over every variant so a widening cannot hide.
2967    fn assert_exact_membership(profile: AlgorithmProfile, expected: &[Service], allow_lms: bool) {
2968        for svc in ALL_SERVICES {
2969            let want = expected.iter().any(|e| *e as u16 == svc as u16)
2970                || (allow_lms && is_lms_service(svc));
2971            assert_eq!(
2972                is_allowed(profile, svc),
2973                want,
2974                "{svc} under {profile}: gate says {}, suite says {want}",
2975                is_allowed(profile, svc)
2976            );
2977        }
2978    }
2979
2980    /// Every profile survives the round trip through the `PROFILE` atomic.
2981    ///
2982    /// `from_u8` had no test at all, and the omission was not visible from the
2983    /// membership tests: those call the private `is_allowed` directly, so a
2984    /// mutation mapping discriminant 3 to `Cnsa2` left the suite green while
2985    /// every Migration deployment silently ran as CNSA 2.0 — refusing ECDSA
2986    /// P-384, RSA and DH — and `oxi_active_profile()` reported `1` for a module
2987    /// initialised with `3`. This goes through the entry point, which is the
2988    /// only place that mapping is exercised.
2989    #[test]
2990    fn every_profile_round_trips_through_the_active_profile() {
2991        for profile in [
2992            AlgorithmProfile::Unrestricted,
2993            AlgorithmProfile::Cnsa2,
2994            AlgorithmProfile::Cnsa1,
2995            AlgorithmProfile::Migration,
2996        ] {
2997            assert_eq!(
2998                AlgorithmProfile::from_u8(profile as u8),
2999                profile,
3000                "{profile} did not survive the round trip through its discriminant"
3001            );
3002        }
3003
3004        // An unknown discriminant must not resolve to a permissive profile.
3005        // `Unrestricted` refuses nothing, so it is the one answer that would
3006        // turn a corrupted byte into an open module.
3007        for raw in [4u8, 9, 200, 255] {
3008            assert_ne!(
3009                AlgorithmProfile::from_u8(raw),
3010                AlgorithmProfile::Unrestricted,
3011                "unknown discriminant {raw} must not open the module"
3012            );
3013        }
3014    }
3015
3016    /// A profile initialised through the public entry point gates as itself.
3017    ///
3018    /// The membership tests reach `is_allowed` directly; this reaches it the
3019    /// way a caller does, through `initialize_with_profile` and the `PROFILE`
3020    /// atomic that `require_allowed` reads.
3021    #[test]
3022    fn migration_gates_as_itself_through_require_allowed() {
3023        let guard = reset_for_test();
3024        guard.reset();
3025        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Migration).unwrap();
3026        assert_eq!(active_profile(), AlgorithmProfile::Migration);
3027
3028        // Permitted by CNSA 1.0 only — refused if this silently ran as CNSA 2.0.
3029        assert!(
3030            require_allowed(Service::EcdsaP384Sign).is_ok(),
3031            "ECDSA P-384 is in CNSA 1.0 and must pass under Migration"
3032        );
3033        assert!(
3034            require_allowed(Service::Dh3072).is_ok(),
3035            "DH-3072 is in CNSA 1.0 and must pass under Migration"
3036        );
3037        // Permitted by CNSA 2.0 only — refused if this silently ran as CNSA 1.0.
3038        assert!(
3039            require_allowed(Service::MlKem1024Encaps).is_ok(),
3040            "ML-KEM-1024 is in CNSA 2.0 and must pass under Migration"
3041        );
3042        // Refused by both, so refused here: Migration is a gate, not Unrestricted.
3043        assert!(require_allowed(Service::Sha256).is_err());
3044    }
3045
3046    /// Exact membership, both directions. A service added to or removed from
3047    /// any gate fails here.
3048    #[test]
3049    fn profile_membership_is_exact() {
3050        assert_exact_membership(AlgorithmProfile::Cnsa1, &EXPECTED_CNSA1, false);
3051        assert_exact_membership(AlgorithmProfile::Cnsa2, &EXPECTED_CNSA2_NON_LMS, true);
3052
3053        // Migration is the union, so a service belongs iff either suite admits
3054        // it. Expressed against the two gates rather than a third list.
3055        for svc in ALL_SERVICES {
3056            let want = is_allowed(AlgorithmProfile::Cnsa1, svc)
3057                || is_allowed(AlgorithmProfile::Cnsa2, svc);
3058            assert_eq!(
3059                is_allowed(AlgorithmProfile::Migration, svc),
3060                want,
3061                "{svc}: Migration must permit exactly the union"
3062            );
3063        }
3064
3065        // Control: Unrestricted permits everything, so a gate that refused
3066        // everything could not have satisfied the assertions above.
3067        for svc in ALL_SERVICES {
3068            assert!(is_allowed(AlgorithmProfile::Unrestricted, svc));
3069        }
3070    }
3071
3072    /// Migration is the union of the two suites and nothing more.
3073    ///
3074    /// It is not a CNSA profile: no standard defines it, which is why it does
3075    /// not carry a CNSA name. Its contract is that it permits what either
3076    /// suite permits, refuses what both refuse, and is strictly narrower than
3077    /// `Unrestricted`.
3078    #[test]
3079    fn migration_is_the_union_of_both_suites() {
3080        // From CNSA 1.0 alone.
3081        for svc in [
3082            Service::EcdsaP384Sign,
3083            Service::EcdhP384,
3084            Service::RsaPssSign3072,
3085            Service::Dh3072,
3086        ] {
3087            assert!(
3088                is_allowed(AlgorithmProfile::Migration, svc),
3089                "{svc} (CNSA 1.0) must pass"
3090            );
3091            assert!(
3092                !is_allowed(AlgorithmProfile::Cnsa2, svc),
3093                "{svc} is not CNSA 2.0"
3094            );
3095        }
3096
3097        // From CNSA 2.0 alone.
3098        for svc in [
3099            Service::MlKem1024Encaps,
3100            Service::MlDsa87Sign,
3101            Service::XmssVerify,
3102            Service::Sha512,
3103            Service::LmsSha256M32H10W4Verify,
3104        ] {
3105            assert!(
3106                is_allowed(AlgorithmProfile::Migration, svc),
3107                "{svc} (CNSA 2.0) must pass"
3108            );
3109            assert!(
3110                !is_allowed(AlgorithmProfile::Cnsa1, svc),
3111                "{svc} is not CNSA 1.0"
3112            );
3113        }
3114
3115        // Shared by both — the overlap is AES-256 and SHA-384 with their
3116        // matching-strength support, and nothing else.
3117        for svc in [Service::Aes256Gcm, Service::Sha384, Service::HmacSha384] {
3118            assert!(is_allowed(AlgorithmProfile::Cnsa1, svc));
3119            assert!(is_allowed(AlgorithmProfile::Cnsa2, svc));
3120            assert!(is_allowed(AlgorithmProfile::Migration, svc));
3121        }
3122
3123        // Refused by both, so refused here. This is what makes Migration a
3124        // gate rather than a synonym for Unrestricted.
3125        for svc in [
3126            Service::Sha1,
3127            Service::Sha256,
3128            Service::Aes128Gcm,
3129            Service::RsaPssSign2048,
3130            Service::EcdsaP256Sign,
3131            Service::Ed25519Sign,
3132            // Named by neither suite for a software module.
3133            Service::Sha3_384,
3134            Service::Shake256,
3135            Service::Kmac256,
3136        ] {
3137            assert!(
3138                !is_allowed(AlgorithmProfile::Migration, svc),
3139                "{svc} is in neither suite and must be refused"
3140            );
3141            // Control: Unrestricted does permit it, so the refusal above is
3142            // Migration's doing and not a service that is gated everywhere.
3143            assert!(is_allowed(AlgorithmProfile::Unrestricted, svc));
3144        }
3145    }
3146
3147    #[test]
3148    fn cnsa1_allows_the_2016_suite() {
3149        let allowed = [
3150            Service::Aes256Gcm,
3151            Service::Sha384,
3152            Service::EcdsaP384Sign,
3153            Service::EcdsaP384Verify,
3154            Service::EcdsaP384Keygen,
3155            Service::EcdhP384,
3156            Service::RsaPssSign3072,
3157            Service::RsaOaep3072,
3158            Service::RsaPssSign4096,
3159            Service::Dh3072,
3160            // Supporting services at the suite's own strength.
3161            Service::HmacSha384,
3162            Service::CtrDrbgAes256,
3163            Service::HashDrbgSha384,
3164            Service::KbkdfHmacSha384,
3165        ];
3166        for svc in allowed {
3167            assert!(
3168                is_allowed(AlgorithmProfile::Cnsa1, svc),
3169                "{svc} should be allowed in CNSA 1.0"
3170            );
3171        }
3172    }
3173
3174    #[test]
3175    fn cnsa1_blocks_small_keys_and_ed25519() {
3176        let blocked = [
3177            Service::Sha1,
3178            Service::Sha224,
3179            Service::Aes128Ecb,
3180            Service::Aes192Gcm,
3181            Service::CtrDrbgAes128,
3182            Service::EcdsaP256Sign,
3183            Service::EcdhP256,
3184            Service::Ed25519Sign,
3185            Service::Ed25519Verify,
3186            Service::RsaPssSign2048,
3187            Service::RsaOaep2048,
3188            // Weaker than the suite requires. Annex B names SHA-384 alone;
3189            // SHA-256 appears only in its Additional Information paragraph,
3190            // as a near-term allowance scoped to UNCLASSIFIED data and
3191            // conditioned on NSA consultation for non-P-384 curves — neither
3192            // of which this module can evaluate.
3193            Service::Sha256,
3194            Service::HmacSha256,
3195            Service::HashDrbgSha256,
3196            Service::HkdfSha256,
3197            // SHA-512 belongs to CNSA 2.0; the 2016 suite does not name it.
3198            Service::Sha512,
3199            Service::HmacSha512,
3200            // Named by neither suite.
3201            Service::Sha3_256,
3202            Service::Shake256,
3203            // Post-quantum and stateful hash-based: CNSA 2.0, not CNSA 1.0.
3204            Service::MlKem1024Encaps,
3205            Service::MlDsa87Sign,
3206            Service::XmssVerify,
3207            Service::LmsSha256M32H10W4Verify,
3208        ];
3209        for svc in blocked {
3210            assert!(
3211                !is_allowed(AlgorithmProfile::Cnsa1, svc),
3212                "{svc} should be BLOCKED in CNSA 1.0"
3213            );
3214        }
3215    }
3216
3217    #[test]
3218    fn lms_gating_is_exhaustive_across_all_80_variants() {
3219        // Enumerates every LMS Service variant (one Verify per (LMS, LM-OTS)
3220        // pair, 80 entries) and verifies that all 80 are gated as CNSA 2.0
3221        // permits them and CNSA 1.0 does not: the 2025 Annex B places no
3222        // subset restriction on LMS, while the 2016 suite names no stateful
3223        // hash-based scheme at all.
3224        let all_80 = LMS_SERVICES;
3225        assert_eq!(all_80.len(), 80, "LMS enumeration drift");
3226
3227        for svc in all_80 {
3228            // Unrestricted: every LMS variant is permitted.
3229            assert!(
3230                is_allowed(AlgorithmProfile::Unrestricted, svc),
3231                "{svc} must be allowed in Unrestricted"
3232            );
3233
3234            // CNSA 2.0: every approved SP 800-208 parameter set is permitted.
3235            // CNSSP-15 Annex B places no subset restriction on LMS.
3236            assert!(
3237                is_allowed(AlgorithmProfile::Cnsa2, svc),
3238                "{svc} must be allowed in CNSA 2.0"
3239            );
3240
3241            // CNSA 1.0: refused. CNSSP-15 (October 2016) Annex B names no
3242            // stateful hash-based signature scheme; LMS arrives with CNSA 2.0.
3243            assert!(
3244                !is_allowed(AlgorithmProfile::Cnsa1, svc),
3245                "{svc} is not in CNSA 1.0 and must be refused"
3246            );
3247
3248            // Migration: permitted, because CNSA 2.0 permits it.
3249            assert!(
3250                is_allowed(AlgorithmProfile::Migration, svc),
3251                "{svc} must be allowed in Migration"
3252            );
3253        }
3254    }
3255
3256    #[test]
3257    fn lms_discriminant_slots_are_complete_and_pure() {
3258        // `is_lms_service` tests "odd and in `501..=659`" instead of
3259        // enumerating 80 arms, which is only sound while those 80 slots hold
3260        // every LMS service and nothing else. Enum discriminants are unique,
3261        // so proving the 80 LMS variants fill the odd slots exactly also
3262        // proves no other variant can sit on one.
3263        let mut discriminants = [0u16; 80];
3264        for (slot, svc) in discriminants.iter_mut().zip(LMS_SERVICES) {
3265            *slot = svc as u16;
3266        }
3267        discriminants.sort_unstable();
3268
3269        // Each step after sorting is exactly 2: all 80 are distinct, all
3270        // share one parity, and no odd slot between them is missing. The
3271        // comparison count is asserted too, so an empty or short sweep
3272        // cannot pass this vacuously.
3273        let mut compared = 0usize;
3274        for pair in discriminants.windows(2) {
3275            let [lo, hi] = pair else {
3276                unreachable!("windows(2) yields two-element slices")
3277            };
3278            assert_eq!(*hi, lo + 2, "LMS slot after {lo} is missing or duplicated");
3279            compared += 1;
3280        }
3281        assert_eq!(compared, 79, "slot sweep did not run in full");
3282
3283        let (Some(&first), Some(&last)) = (discriminants.first(), discriminants.last()) else {
3284            unreachable!("the array holds 80 elements")
3285        };
3286        assert_eq!(first, 501, "LMS block does not start at 501");
3287        assert_eq!(last, 659, "LMS block does not end at 659");
3288
3289        for svc in LMS_SERVICES {
3290            assert!(is_lms_service(svc), "{svc} must be inside the LMS block");
3291        }
3292
3293        // No other variant anywhere in the enum is classed as LMS. This is
3294        // what catches a foreign variant placed on an odd slot in the block,
3295        // which the slot sweep above cannot see: it only reads the LMS list.
3296        let lms_count = ALL_SERVICES
3297            .iter()
3298            .filter(|svc| is_lms_service(**svc))
3299            .count();
3300        assert_eq!(lms_count, 80, "a non-LMS variant sits on an LMS slot");
3301
3302        // Positive control on the other side of the boundary: the nearest
3303        // non-LMS neighbours must not be swept in by a widened range.
3304        for svc in [
3305            Service::SlhDsaShake256fVerify,
3306            Service::XmssVerify,
3307            Service::Dh3072,
3308        ] {
3309            assert!(!is_lms_service(svc), "{svc} must be outside the LMS block");
3310        }
3311    }
3312
3313    #[test]
3314    fn slh_dsa_gating_is_exhaustive_across_all_36_variants() {
3315        // Enumerates every SLH-DSA Service variant (12 parameter sets ×
3316        // {Keygen, Sign, Verify} = 36 entries) and verifies the
3317        // fail-safe gating: all 36 are blocked under both CNSA profiles,
3318        // because CNSSP-15 lists no SLH-DSA parameter set at all; all 36
3319        // are permitted under Unrestricted.  If a new SLH-DSA variant is
3320        // added without a matching entry in this array, the assertion at
3321        // the end catches the gap.
3322        let all_36 = [
3323            // SHA-2 family (18 entries).
3324            Service::SlhDsaSha2256sKeygen,
3325            Service::SlhDsaSha2256sSign,
3326            Service::SlhDsaSha2256sVerify,
3327            Service::SlhDsaSha2128sKeygen,
3328            Service::SlhDsaSha2128sSign,
3329            Service::SlhDsaSha2128sVerify,
3330            Service::SlhDsaSha2128fKeygen,
3331            Service::SlhDsaSha2128fSign,
3332            Service::SlhDsaSha2128fVerify,
3333            Service::SlhDsaSha2192sKeygen,
3334            Service::SlhDsaSha2192sSign,
3335            Service::SlhDsaSha2192sVerify,
3336            Service::SlhDsaSha2192fKeygen,
3337            Service::SlhDsaSha2192fSign,
3338            Service::SlhDsaSha2192fVerify,
3339            Service::SlhDsaSha2256fKeygen,
3340            Service::SlhDsaSha2256fSign,
3341            Service::SlhDsaSha2256fVerify,
3342            // SHAKE family (18 entries).
3343            Service::SlhDsaShake128sKeygen,
3344            Service::SlhDsaShake128sSign,
3345            Service::SlhDsaShake128sVerify,
3346            Service::SlhDsaShake128fKeygen,
3347            Service::SlhDsaShake128fSign,
3348            Service::SlhDsaShake128fVerify,
3349            Service::SlhDsaShake192sKeygen,
3350            Service::SlhDsaShake192sSign,
3351            Service::SlhDsaShake192sVerify,
3352            Service::SlhDsaShake192fKeygen,
3353            Service::SlhDsaShake192fSign,
3354            Service::SlhDsaShake192fVerify,
3355            Service::SlhDsaShake256sKeygen,
3356            Service::SlhDsaShake256sSign,
3357            Service::SlhDsaShake256sVerify,
3358            Service::SlhDsaShake256fKeygen,
3359            Service::SlhDsaShake256fSign,
3360            Service::SlhDsaShake256fVerify,
3361        ];
3362        assert_eq!(all_36.len(), 36, "SLH-DSA enumeration drift");
3363
3364        for svc in all_36 {
3365            // Unrestricted: every SLH-DSA variant is permitted.
3366            assert!(
3367                is_allowed(AlgorithmProfile::Unrestricted, svc),
3368                "{svc} must be allowed in Unrestricted"
3369            );
3370
3371            // CNSA 1.0: no SLH-DSA variant is permitted.
3372            assert!(
3373                !is_allowed(AlgorithmProfile::Cnsa1, svc),
3374                "{svc} must be BLOCKED in CNSA 1.0"
3375            );
3376
3377            // CNSA 2.0: no SLH-DSA variant is permitted either. CNSSP-15
3378            // contains no SLH-DSA entry, so admitting one would be a
3379            // conformance claim the policy does not support.
3380            assert!(
3381                !is_allowed(AlgorithmProfile::Cnsa2, svc),
3382                "{svc} must be BLOCKED in CNSA 2.0"
3383            );
3384        }
3385    }
3386
3387    #[test]
3388    fn require_allowed_returns_restricted_error_under_cnsa2() {
3389        let _guard = reset_for_test();
3390        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa2).unwrap();
3391        match require_allowed(Service::Aes128Ecb) {
3392            Err(Error::AlgorithmRestricted {
3393                service: Service::Aes128Ecb,
3394            }) => {}
3395            other => panic!("expected AlgorithmRestricted(Aes128Ecb), got {other:?}"),
3396        }
3397    }
3398
3399    #[test]
3400    fn require_allowed_passes_permitted_services() {
3401        let _guard = reset_for_test();
3402        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa2).unwrap();
3403        require_allowed(Service::Aes256Gcm).unwrap();
3404        require_allowed(Service::Sha384).unwrap();
3405        require_allowed(Service::MlKem1024Encaps).unwrap();
3406    }
3407
3408    #[test]
3409    fn require_allowed_passes_everything_in_unrestricted() {
3410        let _guard = reset_for_test();
3411        initialize_with_tests(UNSIGNED_TEST_BINARY, &[]).unwrap();
3412        // Spot-check a few from each end of the spectrum.
3413        require_allowed(Service::Sha1).unwrap();
3414        require_allowed(Service::Aes128Ecb).unwrap();
3415        require_allowed(Service::Ed25519Sign).unwrap();
3416        require_allowed(Service::MlKem1024Encaps).unwrap();
3417    }
3418
3419    /// Entry-point coverage for the SLH-DSA correction. The exhaustive
3420    /// `slh_dsa_gating_*` test calls the private `is_allowed` helper directly,
3421    /// so it says nothing about whether the public gate reaches that helper.
3422    /// This one goes through `require_allowed`, which is what every approved
3423    /// service actually calls.
3424    #[test]
3425    fn require_allowed_refuses_every_slh_dsa_service_under_cnsa2() {
3426        let guard = reset_for_test();
3427        for profile in [AlgorithmProfile::Cnsa2, AlgorithmProfile::Cnsa1] {
3428            guard.reset();
3429            initialize_with_profile(UNSIGNED_TEST_BINARY, &[], profile).unwrap();
3430            for svc in [
3431                Service::SlhDsaSha2256sKeygen,
3432                Service::SlhDsaSha2256sSign,
3433                Service::SlhDsaSha2256sVerify,
3434                Service::SlhDsaShake128fVerify,
3435            ] {
3436                match require_allowed(svc) {
3437                    Err(Error::AlgorithmRestricted { service }) => assert_eq!(service, svc),
3438                    other => {
3439                        panic!("expected AlgorithmRestricted({svc}) under {profile}, got {other:?}")
3440                    }
3441                }
3442            }
3443        }
3444    }
3445
3446    /// Entry-point coverage for the LMS correction, on the `M=24` sets.
3447    /// CNSSP-15 (March 2025) Annex B recommends LMS-SHA-256/192, which is
3448    /// `M=24`, so these are the parameter sets the policy names by name.
3449    ///
3450    /// CNSA 1.0 is absent from this list deliberately: the 2016 suite names
3451    /// no stateful hash-based scheme, so LMS is refused there and permitted
3452    /// under Migration instead.
3453    #[test]
3454    fn require_allowed_admits_lms_m24_under_cnsa2_and_migration() {
3455        let guard = reset_for_test();
3456        for profile in [AlgorithmProfile::Cnsa2, AlgorithmProfile::Migration] {
3457            guard.reset();
3458            initialize_with_profile(UNSIGNED_TEST_BINARY, &[], profile).unwrap();
3459            for svc in [
3460                Service::LmsSha256M24H5W1Verify,
3461                Service::LmsSha256M24H25W8Verify,
3462                Service::LmsShakeM24H10W4Verify,
3463                Service::LmsShakeM24H25W8Verify,
3464            ] {
3465                require_allowed(svc)
3466                    .unwrap_or_else(|e| panic!("{svc} must be allowed under {profile}: {e:?}"));
3467            }
3468        }
3469    }
3470
3471    /// Both edges of the LMS discriminant block, over raw values.
3472    ///
3473    /// The block sits at the very top of the enum, so a widened *upper* bound
3474    /// admits nothing today and no `Service`-typed probe can see it — moving
3475    /// the bound to 709 leaves the whole suite green. Testing the arithmetic
3476    /// directly is what makes the upper edge checkable at all.
3477    #[test]
3478    fn lms_block_boundaries_are_exact() {
3479        assert!(!lms_block_contains(499), "499 is below the LMS block");
3480        assert!(!lms_block_contains(500), "500 is even");
3481        assert!(lms_block_contains(501), "501 is the first LMS discriminant");
3482        assert!(!lms_block_contains(502), "502 is even");
3483        assert!(!lms_block_contains(658), "658 is even");
3484        assert!(lms_block_contains(659), "659 is the last LMS discriminant");
3485        assert!(!lms_block_contains(660), "660 is above the LMS block");
3486        assert!(!lms_block_contains(661), "661 is above the LMS block");
3487
3488        // ...and the block the arithmetic describes is the one the enum has.
3489        assert_eq!(Service::LmsSha256M32H5W1Verify as u16, 501);
3490        assert_eq!(Service::LmsShakeM24H25W8Verify as u16, 659);
3491    }
3492
3493    /// The two CNSA profiles must be told apart at the entry point. Every other
3494    /// `require_allowed` test uses a service the profiles treat alike, so a bug
3495    /// wiring `Cnsa1` through as `Cnsa2` would survive them all. P-384 ECDSA is
3496    /// the discriminator: CNSA 1.0 permits it, CNSA 2.0 does not.
3497    #[test]
3498    fn require_allowed_distinguishes_cnsa1_from_cnsa2() {
3499        let guard = reset_for_test();
3500
3501        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa1).unwrap();
3502        require_allowed(Service::EcdsaP384Sign)
3503            .unwrap_or_else(|e| panic!("P-384 ECDSA must be permitted under CNSA 1.0: {e:?}"));
3504
3505        guard.reset();
3506        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa2).unwrap();
3507        match require_allowed(Service::EcdsaP384Sign) {
3508            Err(Error::AlgorithmRestricted {
3509                service: Service::EcdsaP384Sign,
3510            }) => {}
3511            other => panic!("expected P-384 ECDSA refused under CNSA 2.0, got {other:?}"),
3512        }
3513    }
3514
3515    #[test]
3516    fn algorithm_profile_display() {
3517        assert_eq!(AlgorithmProfile::Unrestricted.to_string(), "Unrestricted");
3518        assert_eq!(AlgorithmProfile::Cnsa2.to_string(), "CNSA 2.0");
3519        assert_eq!(AlgorithmProfile::Cnsa1.to_string(), "CNSA 1.0");
3520    }
3521
3522    #[test]
3523    fn algorithm_restricted_error_display() {
3524        let _guard = reset_for_test();
3525        initialize_with_profile(UNSIGNED_TEST_BINARY, &[], AlgorithmProfile::Cnsa2).unwrap();
3526        let err = Error::AlgorithmRestricted {
3527            service: Service::Aes128Ecb,
3528        };
3529        let msg = err.to_string();
3530        assert!(msg.contains("AES-128-ECB"), "got: {msg}");
3531        assert!(msg.contains("CNSA 2.0"), "got: {msg}");
3532    }
3533
3534    #[test]
3535    fn not_implemented_error_display() {
3536        let err = Error::NotImplemented;
3537        assert_eq!(err.to_string(), "algorithm not yet implemented");
3538    }
3539
3540    #[test]
3541    fn service_display_is_stable() {
3542        // Pin a few representative Display strings.
3543        assert_eq!(Service::Sha256.to_string(), "SHA-256");
3544        assert_eq!(Service::Aes256Gcm.to_string(), "AES-256-GCM");
3545        assert_eq!(Service::MlKem1024Encaps.to_string(), "ML-KEM-1024 encaps");
3546        assert_eq!(Service::Ed25519Sign.to_string(), "Ed25519 sign");
3547    }
3548}