Skip to main content

ic_ontology/
types.rs

1//! The ontology vocabulary: the types every registry entry is built from.
2//!
3//! These enums are the *terms* of the ontology. They are deliberately closed
4//! (not free text) so that an agent can reason over them — filter, compare,
5//! and rank — without natural-language understanding. Every variant carries a
6//! stable kebab-case [`id`](Class::id) used in JSON, JSON-LD, and CLI output.
7
8/// What kind of cryptographic object an algorithm is.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum Class {
11    /// A fixed-output hash function.
12    Hash,
13    /// An extendable-output function.
14    Xof,
15    /// A keyed message authentication code.
16    Mac,
17    /// A raw block cipher (a primitive, not a usable encryption scheme).
18    BlockCipher,
19    /// An unauthenticated confidentiality mode.
20    CipherMode,
21    /// An authenticated cipher with associated data.
22    Aead,
23    /// A key derivation function.
24    Kdf,
25    /// A password-based key derivation function.
26    PasswordKdf,
27    /// A deterministic random bit generator.
28    Drbg,
29    /// A key agreement (Diffie-Hellman style) scheme.
30    KeyAgreement,
31    /// A key encapsulation mechanism.
32    Kem,
33    /// A digital signature scheme.
34    Signature,
35    /// A threshold secret-sharing scheme.
36    SecretSharing,
37}
38
39impl Class {
40    /// Stable identifier used in every serialized form.
41    pub const fn id(self) -> &'static str {
42        match self {
43            Self::Hash => "hash",
44            Self::Xof => "xof",
45            Self::Mac => "mac",
46            Self::BlockCipher => "block-cipher",
47            Self::CipherMode => "cipher-mode",
48            Self::Aead => "aead",
49            Self::Kdf => "kdf",
50            Self::PasswordKdf => "password-kdf",
51            Self::Drbg => "drbg",
52            Self::KeyAgreement => "key-agreement",
53            Self::Kem => "kem",
54            Self::Signature => "signature",
55            Self::SecretSharing => "secret-sharing",
56        }
57    }
58
59    /// Every class term, for enumeration and schema generation.
60    pub const ALL: &'static [Class] = &[
61        Class::Hash,
62        Class::Xof,
63        Class::Mac,
64        Class::BlockCipher,
65        Class::CipherMode,
66        Class::Aead,
67        Class::Kdf,
68        Class::PasswordKdf,
69        Class::Drbg,
70        Class::KeyAgreement,
71        Class::Kem,
72        Class::Signature,
73        Class::SecretSharing,
74    ];
75
76    /// Parse a class from its identifier.
77    pub fn from_id(id: &str) -> Option<Class> {
78        Self::ALL.iter().copied().find(|c| c.id() == id)
79    }
80}
81
82/// The security goal an algorithm serves.
83///
84/// This is the axis an agent searches on: a task says "I need integrity", not
85/// "I need SHA-384".
86#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
87pub enum Purpose {
88    /// Detect accidental or malicious modification, without a key.
89    Integrity,
90    /// Hide the content of a message.
91    Confidentiality,
92    /// Prove a message came from a holder of the key.
93    Authentication,
94    /// Produce keys from other keying material.
95    KeyDerivation,
96    /// Produce keys from a low-entropy password.
97    PasswordHashing,
98    /// Establish a shared secret with a peer.
99    KeyEstablishment,
100    /// Generate unpredictable bits.
101    RandomGeneration,
102    /// Prove authorship in a way a third party can check.
103    NonRepudiation,
104    /// Bind a value to a commitment or identifier.
105    Commitment,
106}
107
108impl Purpose {
109    /// Stable identifier used in every serialized form.
110    pub const fn id(self) -> &'static str {
111        match self {
112            Self::Integrity => "integrity",
113            Self::Confidentiality => "confidentiality",
114            Self::Authentication => "authentication",
115            Self::KeyDerivation => "key-derivation",
116            Self::PasswordHashing => "password-hashing",
117            Self::KeyEstablishment => "key-establishment",
118            Self::RandomGeneration => "random-generation",
119            Self::NonRepudiation => "non-repudiation",
120            Self::Commitment => "commitment",
121        }
122    }
123
124    /// Every purpose term.
125    pub const ALL: &'static [Purpose] = &[
126        Purpose::Integrity,
127        Purpose::Confidentiality,
128        Purpose::Authentication,
129        Purpose::KeyDerivation,
130        Purpose::PasswordHashing,
131        Purpose::KeyEstablishment,
132        Purpose::RandomGeneration,
133        Purpose::NonRepudiation,
134        Purpose::Commitment,
135    ];
136
137    /// Parse a purpose from its identifier.
138    pub fn from_id(id: &str) -> Option<Purpose> {
139        Self::ALL.iter().copied().find(|p| p.id() == id)
140    }
141}
142
143/// An algorithm's standing under FIPS 140-3 and the SP 800-131A transition.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
145pub enum FipsStatus {
146    /// Approved for use in the FIPS-approved mode of operation.
147    Approved,
148    /// Allowed in approved mode as a component, but not itself an approved
149    /// security function (for example a raw block cipher inside a mode).
150    AllowedAsComponent,
151    /// Not approved; usable only outside approved mode.
152    NotApproved,
153    /// Approved today, scheduled for withdrawal.
154    Deprecated,
155    /// Withdrawn; must not be used for protection.
156    Disallowed,
157}
158
159impl FipsStatus {
160    /// Every variant, for callers that enumerate them.
161    ///
162    /// The published JSON Schema builds its `enum` from this. It used to
163    /// carry the strings inline, which is how `implementationStatus` came
164    /// to omit `experimental` after that status was added -- the schema
165    /// then rejected the very entries it most needed to describe.
166    pub const ALL: &'static [FipsStatus] = &[
167        FipsStatus::Approved,
168        FipsStatus::AllowedAsComponent,
169        FipsStatus::NotApproved,
170        FipsStatus::Deprecated,
171        FipsStatus::Disallowed,
172    ];
173
174    /// Stable identifier used in every serialized form.
175    pub const fn id(self) -> &'static str {
176        match self {
177            Self::Approved => "approved",
178            Self::AllowedAsComponent => "allowed-as-component",
179            Self::NotApproved => "not-approved",
180            Self::Deprecated => "deprecated",
181            Self::Disallowed => "disallowed",
182        }
183    }
184
185    /// Whether the FIPS policy engine permits this in approved mode.
186    pub const fn permitted_in_approved_mode(self) -> bool {
187        matches!(
188            self,
189            Self::Approved | Self::AllowedAsComponent | Self::Deprecated
190        )
191    }
192}
193
194/// Whether the algorithm actually exists in this build.
195///
196/// A registry that only listed what is implemented would let an agent conclude
197/// "IronCrypto has no approved signature scheme, so I'll use Ed25519" —
198/// exactly the wrong inference. Listing planned algorithms with an honest
199/// status lets the agent conclude "not available here; use another module".
200#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
201pub enum ImplStatus {
202    /// Implemented, vector-tested, and callable today.
203    ///
204    /// "Vector-tested" is the load-bearing word: the implementation has been
205    /// checked against values produced by something other than itself, so it
206    /// interoperates. See [`ImplStatus::Experimental`] for the case where that
207    /// evidence does not exist.
208    Available,
209    /// Implemented and property-tested, but never checked against a published
210    /// vector or a second implementation.
211    ///
212    /// The distinction matters more than it sounds. A cryptographic primitive
213    /// can be internally consistent — encrypt and decrypt round-trip, every
214    /// component behaves — and still compute something no other implementation
215    /// agrees with, because one constant or one byte order is wrong. Such a
216    /// build passes every test you can write against it alone.
217    ///
218    /// Anything marked this way is safe to experiment with and unsafe to
219    /// interoperate with. It is excluded from the FIPS approved mode and from
220    /// [`crate::select::recommend`] regardless of what its `fips` field says.
221    Experimental,
222    /// Specified in the ontology, not yet implemented.
223    Planned,
224    /// Deliberately excluded; see the entry's `notes`.
225    Excluded,
226}
227
228impl ImplStatus {
229    /// Every variant, for callers that enumerate them.
230    ///
231    /// The published JSON Schema builds its `enum` from this. It used to
232    /// carry the strings inline, which is how `implementationStatus` came
233    /// to omit `experimental` after that status was added -- the schema
234    /// then rejected the very entries it most needed to describe.
235    pub const ALL: &'static [ImplStatus] = &[
236        ImplStatus::Available,
237        ImplStatus::Experimental,
238        ImplStatus::Planned,
239        ImplStatus::Excluded,
240    ];
241
242    /// Stable identifier used in every serialized form.
243    pub const fn id(self) -> &'static str {
244        match self {
245            Self::Available => "available",
246            Self::Experimental => "experimental",
247            Self::Planned => "planned",
248            Self::Excluded => "excluded",
249        }
250    }
251}
252
253/// Rough throughput expectation on the **portable** backend.
254///
255/// This is deliberately a static, backend-independent property: the same
256/// algorithm is `Slow` on a microcontroller and fast on a server with AES-NI,
257/// and an entry cannot be both. Consult
258/// [`runtime::backend()`][crate::runtime::backend] for what the machine in
259/// front of you will actually do — a `Slow` AES entry on a
260/// `HardwareAccelerated` backend runs at GB/s, not MB/s.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
262pub enum Performance {
263    /// Fast enough for bulk data on any target.
264    Fast,
265    /// Usable for bulk data, noticeably slower than a hardware backend.
266    Moderate,
267    /// Acceptable for keys and small messages; avoid for bulk data.
268    Slow,
269    /// Intentionally expensive (password hashing).
270    DeliberatelySlow,
271}
272
273impl Performance {
274    /// Every variant, for callers that enumerate them.
275    ///
276    /// The published JSON Schema builds its `enum` from this. It used to
277    /// carry the strings inline, which is how `implementationStatus` came
278    /// to omit `experimental` after that status was added -- the schema
279    /// then rejected the very entries it most needed to describe.
280    pub const ALL: &'static [Performance] = &[
281        Performance::Fast,
282        Performance::Moderate,
283        Performance::Slow,
284        Performance::DeliberatelySlow,
285    ];
286
287    /// Stable identifier used in every serialized form.
288    pub const fn id(self) -> &'static str {
289        match self {
290            Self::Fast => "fast",
291            Self::Moderate => "moderate",
292            Self::Slow => "slow",
293            Self::DeliberatelySlow => "deliberately-slow",
294        }
295    }
296}
297
298/// A named size or count constraint on an algorithm's inputs.
299#[derive(Debug, Clone, Copy)]
300pub struct Param {
301    /// Parameter name, e.g. `"key"`, `"nonce"`, `"tag"`, `"iterations"`.
302    pub name: &'static str,
303    /// Unit the bounds are expressed in.
304    pub unit: Unit,
305    /// Smallest acceptable value, inclusive.
306    pub min: u64,
307    /// Largest acceptable value, inclusive.
308    pub max: u64,
309    /// The value to use absent a reason to differ.
310    pub recommended: u64,
311    /// What the parameter is for.
312    pub note: &'static str,
313}
314
315/// The unit a [`Param`] bound is measured in.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum Unit {
318    /// A length in bytes.
319    Bytes,
320    /// A repetition count.
321    Count,
322}
323
324impl Unit {
325    /// Every variant, for callers that enumerate them.
326    ///
327    /// The published JSON Schema builds its `enum` from this. It used to
328    /// carry the strings inline, which is how `implementationStatus` came
329    /// to omit `experimental` after that status was added -- the schema
330    /// then rejected the very entries it most needed to describe.
331    pub const ALL: &'static [Unit] = &[Unit::Bytes, Unit::Count];
332
333    /// Stable identifier used in every serialized form.
334    pub const fn id(self) -> &'static str {
335        match self {
336            Self::Bytes => "bytes",
337            Self::Count => "count",
338        }
339    }
340}
341
342/// A machine-checkable usage rule.
343///
344/// The `severity` is what makes this actionable: an agent can refuse to
345/// generate code that violates a `Critical` constraint, and merely warn on an
346/// `Advisory` one.
347#[derive(Debug, Clone, Copy)]
348pub struct Constraint {
349    /// Stable identifier, e.g. `"unique-nonce-per-key"`.
350    pub id: &'static str,
351    /// What the caller must do.
352    pub requirement: &'static str,
353    /// What goes wrong if they do not.
354    pub consequence: &'static str,
355    /// How bad the violation is.
356    pub severity: Severity,
357}
358
359/// How serious a [`Constraint`] violation is.
360#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
361pub enum Severity {
362    /// Violation breaks the security property outright.
363    Critical,
364    /// Violation weakens the security property materially.
365    Serious,
366    /// Violation is a best-practice issue.
367    Advisory,
368}
369
370impl Severity {
371    /// Every variant, for callers that enumerate them.
372    ///
373    /// The published JSON Schema builds its `enum` from this. It used to
374    /// carry the strings inline, which is how `implementationStatus` came
375    /// to omit `experimental` after that status was added -- the schema
376    /// then rejected the very entries it most needed to describe.
377    pub const ALL: &'static [Severity] =
378        &[Severity::Critical, Severity::Serious, Severity::Advisory];
379
380    /// Stable identifier used in every serialized form.
381    pub const fn id(self) -> &'static str {
382        match self {
383            Self::Critical => "critical",
384            Self::Serious => "serious",
385            Self::Advisory => "advisory",
386        }
387    }
388}
389
390/// Security strength in bits, against classical and quantum adversaries.
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub struct Strength {
393    /// Bits of security against a classical adversary.
394    pub classical: u16,
395    /// Bits of security against a quantum adversary, accounting for Grover
396    /// (symmetric, halved) and Shor (classical asymmetric, broken to 0).
397    pub quantum: u16,
398}
399
400impl Strength {
401    /// A symmetric primitive: Grover halves the effective strength.
402    pub const fn symmetric(bits: u16) -> Strength {
403        Strength {
404            classical: bits,
405            quantum: bits / 2,
406        }
407    }
408
409    /// A discrete-log or factoring primitive: Shor reduces it to nothing.
410    pub const fn classical_only(bits: u16) -> Strength {
411        Strength {
412            classical: bits,
413            quantum: 0,
414        }
415    }
416}
417
418/// A typed relation between two registry entries.
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum Relation {
421    /// This algorithm is constructed from the target.
422    BuiltOn,
423    /// This algorithm should be used instead of the target.
424    Supersedes,
425    /// The target should be used instead of this algorithm.
426    SupersededBy,
427    /// This algorithm is commonly paired with the target.
428    PairsWith,
429    /// This algorithm is a parameterization of the target family.
430    Specializes,
431}
432
433impl Relation {
434    /// Every variant, for callers that enumerate them.
435    ///
436    /// The published JSON Schema builds its `enum` from this. It used to
437    /// carry the strings inline, which is how `implementationStatus` came
438    /// to omit `experimental` after that status was added -- the schema
439    /// then rejected the very entries it most needed to describe.
440    pub const ALL: &'static [Relation] = &[
441        Relation::BuiltOn,
442        Relation::Supersedes,
443        Relation::SupersededBy,
444        Relation::PairsWith,
445        Relation::Specializes,
446    ];
447
448    /// Stable identifier used in every serialized form.
449    pub const fn id(self) -> &'static str {
450        match self {
451            Self::BuiltOn => "built-on",
452            Self::Supersedes => "supersedes",
453            Self::SupersededBy => "superseded-by",
454            Self::PairsWith => "pairs-with",
455            Self::Specializes => "specializes",
456        }
457    }
458}
459
460/// An outgoing edge in the ontology graph.
461#[derive(Debug, Clone, Copy)]
462pub struct Edge {
463    /// The kind of relation.
464    pub relation: Relation,
465    /// The `id` of the target entry.
466    pub target: &'static str,
467}
468
469/// A complete ontology entry for one algorithm.
470///
471/// Every field is machine-readable. `rust_path` and `example` close the loop:
472/// after an agent selects an algorithm, it knows exactly what to call and what
473/// the call looks like, without searching the source tree.
474#[derive(Debug, Clone, Copy)]
475pub struct Entry {
476    /// Stable, unique, kebab-case identifier.
477    pub id: &'static str,
478    /// Display name.
479    pub name: &'static str,
480    /// Other names this algorithm is known by, for lookup.
481    pub aliases: &'static [&'static str],
482    /// One-sentence description of what it is and when to use it.
483    pub summary: &'static str,
484    /// The kind of object.
485    pub class: Class,
486    /// Family grouping, e.g. `"SHA-2"`.
487    pub family: &'static str,
488    /// The security goals it serves.
489    pub purposes: &'static [Purpose],
490    /// Security strength.
491    pub strength: Strength,
492    /// FIPS standing.
493    pub fips: FipsStatus,
494    /// Whether it exists in this build.
495    pub status: ImplStatus,
496    /// Defining standards documents.
497    pub standards: &'static [&'static str],
498    /// Input size and count constraints.
499    pub params: &'static [Param],
500    /// Usage rules.
501    pub constraints: &'static [Constraint],
502    /// Relations to other entries.
503    pub edges: &'static [Edge],
504    /// Throughput expectation.
505    pub performance: Performance,
506    /// Fully-qualified Rust path to the implementation, or `""` when planned.
507    pub rust_path: &'static str,
508    /// A minimal, copy-pasteable usage snippet, or `""` when planned.
509    pub example: &'static str,
510    /// Anything an implementer needs to know that the fields above do not say.
511    pub notes: &'static str,
512}
513
514impl Entry {
515    /// Whether this entry serves the given purpose.
516    pub fn serves(&self, purpose: Purpose) -> bool {
517        self.purposes.contains(&purpose)
518    }
519
520    /// Whether `needle` matches this entry's id or any alias, case-insensitively.
521    pub fn matches_name(&self, needle: &str) -> bool {
522        if eq_ignore_case(self.id, needle) || eq_ignore_case(self.name, needle) {
523            return true;
524        }
525        self.aliases.iter().any(|a| eq_ignore_case(a, needle))
526    }
527
528    /// Whether this entry can be used while the module is in approved mode.
529    pub fn approved_mode_ok(&self) -> bool {
530        self.fips.permitted_in_approved_mode() && self.status == ImplStatus::Available
531    }
532
533    /// The most severe constraint attached to this entry, if any.
534    ///
535    /// This is `min_by_key`, not `max_by_key`, because [`Severity`] is declared
536    /// worst-first so that its derived `Ord` sorts the way a priority list
537    /// reads. That is a real dependency on the order of an enum's variants, so
538    /// the tests pin it: reordering `Severity` must fail loudly rather than
539    /// quietly turn this into `mildest_constraint`.
540    pub fn worst_constraint(&self) -> Option<&'static Constraint> {
541        self.constraints.iter().min_by_key(|c| c.severity)
542    }
543}
544
545fn eq_ignore_case(a: &str, b: &str) -> bool {
546    a.eq_ignore_ascii_case(b)
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    /// Every `ALL` list must really be all of them.
554    ///
555    /// The matches are exhaustive and this is the crate that defines the types,
556    /// so adding a variant stops this compiling until it is handled. Without
557    /// that, a list like this is a second place to remember, and the schema's
558    /// stale `implementationStatus` is what forgetting looks like.
559    #[test]
560    fn the_variant_lists_are_complete() {
561        #[allow(clippy::needless_match)]
562        fn identify_status(v: ImplStatus) -> ImplStatus {
563            match v {
564                ImplStatus::Available => ImplStatus::Available,
565                ImplStatus::Experimental => ImplStatus::Experimental,
566                ImplStatus::Planned => ImplStatus::Planned,
567                ImplStatus::Excluded => ImplStatus::Excluded,
568            }
569        }
570        #[allow(clippy::needless_match)]
571        fn identify_fips(v: FipsStatus) -> FipsStatus {
572            match v {
573                FipsStatus::Approved => FipsStatus::Approved,
574                FipsStatus::AllowedAsComponent => FipsStatus::AllowedAsComponent,
575                FipsStatus::NotApproved => FipsStatus::NotApproved,
576                FipsStatus::Deprecated => FipsStatus::Deprecated,
577                FipsStatus::Disallowed => FipsStatus::Disallowed,
578            }
579        }
580        #[allow(clippy::needless_match)]
581        fn identify_perf(v: Performance) -> Performance {
582            match v {
583                Performance::Fast => Performance::Fast,
584                Performance::Moderate => Performance::Moderate,
585                Performance::Slow => Performance::Slow,
586                Performance::DeliberatelySlow => Performance::DeliberatelySlow,
587            }
588        }
589        #[allow(clippy::needless_match)]
590        fn identify_severity(v: Severity) -> Severity {
591            match v {
592                Severity::Critical => Severity::Critical,
593                Severity::Serious => Severity::Serious,
594                Severity::Advisory => Severity::Advisory,
595            }
596        }
597        #[allow(clippy::needless_match)]
598        fn identify_unit(v: Unit) -> Unit {
599            match v {
600                Unit::Bytes => Unit::Bytes,
601                Unit::Count => Unit::Count,
602            }
603        }
604        #[allow(clippy::needless_match)]
605        fn identify_relation(v: Relation) -> Relation {
606            match v {
607                Relation::BuiltOn => Relation::BuiltOn,
608                Relation::Supersedes => Relation::Supersedes,
609                Relation::SupersededBy => Relation::SupersededBy,
610                Relation::PairsWith => Relation::PairsWith,
611                Relation::Specializes => Relation::Specializes,
612            }
613        }
614
615        // The exhaustive matches above catch a variant being *added* -- they
616        // stop compiling until it is handled, and handling it means looking at
617        // the list right there. They do not catch one being *removed* from a
618        // list, since a shorter list still maps each of its members to itself.
619        // These counts are the other half: they sit beside the matches, and the
620        // only way to satisfy both is to edit them together.
621        assert_eq!(ImplStatus::ALL.len(), 4);
622        assert_eq!(FipsStatus::ALL.len(), 5);
623        assert_eq!(Performance::ALL.len(), 4);
624        assert_eq!(Severity::ALL.len(), 3);
625        assert_eq!(Unit::ALL.len(), 2);
626        assert_eq!(Relation::ALL.len(), 5);
627
628        for v in ImplStatus::ALL {
629            assert_eq!(identify_status(*v), *v);
630        }
631        for v in FipsStatus::ALL {
632            assert_eq!(identify_fips(*v), *v);
633        }
634        for v in Performance::ALL {
635            assert_eq!(identify_perf(*v), *v);
636        }
637        for v in Severity::ALL {
638            assert_eq!(identify_severity(*v), *v);
639        }
640        for v in Unit::ALL {
641            assert_eq!(identify_unit(*v), *v);
642        }
643        for v in Relation::ALL {
644            assert_eq!(identify_relation(*v), *v);
645        }
646
647        // Identifiers are the join key every serialized form uses.
648        fn distinct(ids: &[&str]) -> bool {
649            ids.iter()
650                .enumerate()
651                .all(|(i, a)| !ids[i + 1..].contains(a))
652        }
653        assert!(distinct(
654            &ImplStatus::ALL.iter().map(|v| v.id()).collect::<Vec<_>>()
655        ));
656        assert!(distinct(
657            &FipsStatus::ALL.iter().map(|v| v.id()).collect::<Vec<_>>()
658        ));
659        assert!(distinct(
660            &Relation::ALL.iter().map(|v| v.id()).collect::<Vec<_>>()
661        ));
662    }
663
664    /// `Severity` is ordered worst-first, and something depends on it.
665    ///
666    /// [`Entry::worst_constraint`] selects with `min_by_key`. If these variants
667    /// were ever reordered — alphabetized, or a new one inserted at the top —
668    /// that selection would silently invert and every caller asking "what is
669    /// the worst thing about this algorithm" would be told the mildest.
670    #[test]
671    fn severity_is_ordered_worst_first() {
672        assert!(Severity::Critical < Severity::Serious);
673        assert!(Severity::Serious < Severity::Advisory);
674        assert_eq!(
675            [Severity::Advisory, Severity::Critical, Severity::Serious]
676                .iter()
677                .min(),
678            Some(&Severity::Critical),
679            "min() must mean worst, which is what worst_constraint relies on"
680        );
681    }
682
683    #[test]
684    fn class_ids_roundtrip() {
685        for c in Class::ALL {
686            assert_eq!(Class::from_id(c.id()), Some(*c));
687        }
688        assert_eq!(Class::from_id("nonsense"), None);
689    }
690
691    #[test]
692    fn purpose_ids_roundtrip() {
693        for p in Purpose::ALL {
694            assert_eq!(Purpose::from_id(p.id()), Some(*p));
695        }
696    }
697
698    #[test]
699    fn approved_mode_policy_is_explicit() {
700        assert!(FipsStatus::Approved.permitted_in_approved_mode());
701        assert!(FipsStatus::AllowedAsComponent.permitted_in_approved_mode());
702        assert!(FipsStatus::Deprecated.permitted_in_approved_mode());
703        assert!(!FipsStatus::NotApproved.permitted_in_approved_mode());
704        assert!(!FipsStatus::Disallowed.permitted_in_approved_mode());
705    }
706
707    #[test]
708    fn quantum_strength_models_grover_and_shor() {
709        assert_eq!(Strength::symmetric(256).quantum, 128);
710        assert_eq!(Strength::classical_only(128).quantum, 0);
711    }
712
713    #[test]
714    fn severity_orders_critical_first() {
715        assert!(Severity::Critical < Severity::Serious);
716        assert!(Severity::Serious < Severity::Advisory);
717    }
718
719    #[test]
720    fn case_insensitive_name_matching() {
721        assert!(eq_ignore_case("SHA-256", "sha-256"));
722        assert!(!eq_ignore_case("SHA-256", "sha-384"));
723        assert!(!eq_ignore_case("SHA-256", "sha-2560"));
724    }
725}