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