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}