Skip to main content

ic_ontology/
lib.rs

1//! # ic-ontology — the machine-readable cryptographic ontology
2//!
3//! This crate is what makes IronCrypto *agentic-first*. A conventional
4//! crypto library documents itself for humans: prose, examples, a changelog.
5//! An agent has to guess which primitive fits its task, guess whether its
6//! deployment allows it, and guess what will go wrong. Each guess is a chance
7//! to ship something broken.
8//!
9//! The ontology replaces those guesses with data:
10//!
11//! * **[`registry`]** — every algorithm, described in closed vocabulary terms:
12//!   class, purpose, strength against classical *and* quantum adversaries,
13//!   FIPS standing, standards, parameter bounds, usage constraints with
14//!   severities, relations to other algorithms, and the exact Rust path to
15//!   call.
16//! * **[`Query`]** — filter by what the task needs, not by name.
17//! * **[`select`]** — go straight from an [`Intent`] and a [`Policy`] to a
18//!   [`Recommendation`] carrying the choice, the reasoning, the rejected
19//!   alternatives, and the rules the caller must honour.
20//! * **[`export`]** — emit the whole thing as JSON, JSON-LD, Turtle, or JSON
21//!   Schema for tools that do not speak Rust.
22//!
23//! ## The property that matters most
24//!
25//! The registry describes algorithms this library does **not** implement, and
26//! declines rather than substituting something weaker. Asked for a policy
27//! nothing here satisfies, an agent gets a refusal instead of the nearest
28//! plausible answer:
29//!
30//! ```
31//! use ic_ontology::select::{recommend, Intent, NoRecommendation, Policy};
32//!
33//! // No hash offers this much quantum resistance, so the answer is "nothing",
34//! // not "the strongest one available".
35//! let outcome = recommend(
36//!     Intent::HashData,
37//!     Policy { require_fips: false, min_classical_bits: 0,
38//!              min_quantum_bits: 512, aes_hardware: false },
39//! );
40//! assert_eq!(outcome.unwrap_err(), NoRecommendation::NothingSatisfiesPolicy);
41//! ```
42//!
43//! An honest "no" is worth more to an autonomous caller than a plausible
44//! "yes".
45//!
46//! This example used to be post-quantum key agreement, which returned
47//! [`select::NoRecommendation::KnownButUnavailable`] because ML-KEM-768 was
48//! implemented but not checked against anyone else's values. It is checked
49//! against NIST's ACVP vectors now, so that call succeeds and the example had
50//! to move to a policy that still has no answer.
51#![cfg_attr(not(feature = "std"), no_std)]
52#![forbid(unsafe_code)]
53#![deny(missing_docs)]
54#![warn(clippy::all)]
55
56#[cfg(feature = "std")]
57extern crate std;
58
59pub mod profile;
60pub mod query;
61pub mod registry;
62pub mod rules;
63pub mod select;
64pub mod standards;
65pub mod types;
66
67#[cfg(feature = "std")]
68pub mod export;
69pub mod frameworks;
70
71pub mod runtime;
72
73pub use query::{all, get, related, Query};
74pub use registry::REGISTRY;
75pub use rules::{Rule, RULES};
76pub use select::{recommend, Intent, NoRecommendation, Policy, Recommendation};
77pub use types::{
78    Class, Constraint, Edge, Entry, FipsStatus, ImplStatus, Param, Performance, Purpose, Relation,
79    Severity, Strength, Unit,
80};
81
82/// The ontology's schema version.
83///
84/// Bumped when the vocabulary changes shape, independently of the library
85/// version, so a consumer can tell whether its parser still applies.
86pub const ONTOLOGY_VERSION: &str = "1.0";
87
88/// The error catalog, mirroring [`ic_core::ErrorKind`].
89///
90/// Exposed here so an agent can learn the full failure vocabulary — and each
91/// kind's recovery semantics — without triggering the errors first.
92pub mod errors {
93    use ic_core::ErrorKind;
94
95    /// A described error kind.
96    #[derive(Debug, Clone, Copy)]
97    pub struct ErrorDoc {
98        /// Stable identifier, matching [`ic_core::ErrorKind::id`].
99        pub id: &'static str,
100        /// What the failure means.
101        pub meaning: &'static str,
102        /// What the caller should do about it.
103        pub recovery: &'static str,
104        /// Whether retrying the identical call could succeed.
105        pub retryable: bool,
106        /// Whether changing the inputs could succeed.
107        pub caller_correctable: bool,
108    }
109
110    const KINDS: &[(ErrorKind, &str, &str)] = &[
111        (
112            ErrorKind::InvalidLength,
113            "A buffer was the wrong size for the algorithm's contract.",
114            "Read the parameter bounds from the ontology entry and resize the buffer.",
115        ),
116        (
117            ErrorKind::InvalidParameter,
118            "A key, nonce, or parameter was structurally unacceptable.",
119            "Check the entry's parameters and constraints; do not retry unchanged.",
120        ),
121        (
122            ErrorKind::AuthenticationFailed,
123            "A MAC, AEAD tag, or signature did not verify.",
124            "Treat the data as hostile. Do not use any plaintext produced alongside it, and do not \
125             report which byte differed.",
126        ),
127        (
128            ErrorKind::Unsupported,
129            "The algorithm is described by the ontology but not implemented in this build.",
130            "Query the ontology for an available alternative, or obtain the algorithm elsewhere.",
131        ),
132        (
133            ErrorKind::NotApprovedInFipsMode,
134            "The operation is not permitted while the module is in approved mode.",
135            "Select an approved algorithm, or leave approved mode deliberately and record that \
136             decision.",
137        ),
138        (
139            ErrorKind::SelfTestFailed,
140            "A known-answer test failed; the module has entered its error state.",
141            "Do not retry. The binary is corrupt or the build is broken; stop and investigate.",
142        ),
143        (
144            ErrorKind::ModuleErrorState,
145            "The module is latched in its error state and refuses all service.",
146            "Restart the process. A latched error state is not clearable at runtime by design.",
147        ),
148        (
149            ErrorKind::EntropyFailure,
150            "The entropy source failed or was unavailable.",
151            "Retry; if it persists, seed the DRBG from a source you supply.",
152        ),
153        (
154            ErrorKind::CounterExhausted,
155            "A DRBG reseed interval, GCM invocation limit, or sequence number ran out.",
156            "Reseed or rekey. Continuing past the limit voids the security claim.",
157        ),
158        (
159            ErrorKind::MalformedEncoding,
160            "Input could not be decoded, or was encoded non-canonically.",
161            "Fix the encoding. A non-canonical encoding is often an attack, not a bug.",
162        ),
163        (
164            ErrorKind::ProviderUnavailable,
165            "Whatever holds the key -- a token, a TPM, a key service -- could not be reached or \
166             could not answer. The inputs were not at fault.",
167            "Retry, with a delay and a limit. If it persists, the device or the service needs \
168             attention; changing the inputs will not help.",
169        ),
170        (
171            ErrorKind::ProviderRefused,
172            "Whatever holds the key answered, and refused: a PIN expired or locked, a policy, a \
173             missing permission. The inputs were not at fault.",
174            "Do not retry: a repeated attempt can lock a token. Report it to whoever administers \
175             the key.",
176        ),
177        (
178            ErrorKind::Internal,
179            "A library invariant was violated.",
180            "This is a bug in IronCrypto. Report it with the context string.",
181        ),
182    ];
183
184    /// Every error kind, documented.
185    pub fn catalog() -> impl Iterator<Item = ErrorDoc> {
186        KINDS.iter().map(|(kind, meaning, recovery)| ErrorDoc {
187            id: kind.id(),
188            meaning,
189            recovery,
190            retryable: kind.retryable(),
191            caller_correctable: kind.caller_correctable(),
192        })
193    }
194
195    /// Look up one error kind by identifier.
196    pub fn get(id: &str) -> Option<ErrorDoc> {
197        catalog().find(|d| d.id == id)
198    }
199
200    #[cfg(test)]
201    mod tests {
202        use super::*;
203
204        /// Every `ErrorKind` must be in the catalog.
205        ///
206        /// The test below checks the entries are unique and say something.
207        /// Neither property notices an entry that is *absent*, and an
208        /// undocumented error is the one case the catalog exists for: an agent
209        /// that hits it gets a kind identifier and no guidance.
210        ///
211        /// Completeness is delegated to `ErrorKind::ALL`, because this type is
212        /// `#[non_exhaustive]` and no crate but `ic-core` can match on it
213        /// exhaustively. The compiler enforces the list there; this asserts the
214        /// catalog covers it.
215        #[test]
216        fn every_error_kind_is_documented() {
217            for kind in ErrorKind::ALL {
218                assert!(
219                    get(kind.id()).is_some(),
220                    "{} has no catalog entry, so an agent that hits it gets an identifier and no guidance",
221                    kind.id()
222                );
223            }
224            assert_eq!(
225                catalog().count(),
226                ErrorKind::ALL.len(),
227                "the catalog and the variant list disagree on how many kinds there are"
228            );
229        }
230
231        #[test]
232        fn catalog_is_complete_and_unique() {
233            let docs: std::vec::Vec<_> = catalog().collect();
234            assert_eq!(docs.len(), KINDS.len());
235            for (i, a) in docs.iter().enumerate() {
236                for b in docs.iter().skip(i + 1) {
237                    assert_ne!(a.id, b.id);
238                }
239                assert!(!a.meaning.is_empty());
240                assert!(!a.recovery.is_empty());
241            }
242        }
243
244        #[test]
245        fn authentication_failure_is_not_retryable_or_correctable() {
246            let d = get("authentication-failed").unwrap();
247            assert!(!d.retryable);
248            assert!(!d.caller_correctable);
249        }
250
251        #[test]
252        fn what_is_retryable_is_what_may_pass_next_time() {
253            assert!(get("entropy-failure").unwrap().retryable);
254            assert!(get("provider-unavailable").unwrap().retryable);
255            // A refusal retried is how a token gets locked.
256            assert!(!get("provider-refused").unwrap().retryable);
257            assert!(!get("internal").unwrap().retryable);
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn every_available_entry_names_a_real_module_path() {
268        // The path must at least point into one of the workspace crates.
269        let prefixes = [
270            "ic_hash::",
271            "ic_mac::",
272            "ic_cipher::",
273            "ic_kdf::",
274            "ic_drbg::",
275            "ic_ec::",
276            "ic_rsa::",
277            "ic_mlkem::",
278            "ic_mldsa::",
279            "ic_hpke::",
280            "ic_lms::",
281            "ic_slhdsa::",
282        ];
283        for e in REGISTRY
284            .iter()
285            .filter(|e| matches!(e.status, ImplStatus::Available | ImplStatus::Experimental))
286        {
287            assert!(
288                prefixes.iter().any(|p| e.rust_path.starts_with(p)),
289                "{} points at {}, which is not a workspace crate",
290                e.id,
291                e.rust_path
292            );
293        }
294    }
295
296    #[test]
297    fn ontology_version_is_set() {
298        assert!(!ONTOLOGY_VERSION.is_empty());
299    }
300}