Skip to main content

ic_core/
error.rs

1//! The single error domain for the whole library.
2
3use core::fmt;
4
5/// Result alias used by every fallible operation in IronCrypto.
6pub type Result<T> = core::result::Result<T, Error>;
7
8/// Machine-actionable classification of a failure.
9///
10/// The discriminants are stable and are mirrored verbatim into the ontology
11/// (`ic-ontology::error_catalog`) so an agent can reason about recovery
12/// strategy without parsing English prose.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[non_exhaustive]
15pub enum ErrorKind {
16    /// A buffer was too short or too long for the algorithm's contract.
17    InvalidLength,
18    /// A key, nonce, or parameter was structurally unacceptable.
19    InvalidParameter,
20    /// Authentication (MAC / AEAD tag / signature) failed to verify.
21    AuthenticationFailed,
22    /// The requested algorithm exists but is not implemented in this build.
23    Unsupported,
24    /// The operation is not permitted while the module is in FIPS approved mode.
25    NotApprovedInFipsMode,
26    /// A FIPS 140-3 self-test failed; the module has entered the error state.
27    SelfTestFailed,
28    /// The module is in a hard error state and refuses all cryptographic service.
29    ModuleErrorState,
30    /// The entropy source failed or did not pass its health tests.
31    EntropyFailure,
32    /// A counter (DRBG reseed, GCM invocation, sequence number) was exhausted.
33    CounterExhausted,
34    /// Input could not be decoded (hex, base64, DER, point encoding).
35    MalformedEncoding,
36    /// Whatever holds the key -- a token, a TPM, a key service -- could not
37    /// be reached or could not answer, and the inputs were not at fault: a
38    /// device error, a lost session, a network failure. The same call may
39    /// succeed later.
40    ///
41    /// Nothing in this library returns it. It is for implementations of
42    /// [`Signer`](crate::sig::Signer) and the like over keys held elsewhere,
43    /// which otherwise have no honest kind to report.
44    ProviderUnavailable,
45    /// Whatever holds the key answered, and the answer was no: a PIN expired
46    /// or locked out, a policy that forbids the operation, a credential
47    /// without the permission. The inputs were not at fault and the same
48    /// call will be refused again until someone changes that.
49    ///
50    /// A provider that cannot tell this from
51    /// [`ProviderUnavailable`](Self::ProviderUnavailable) reports this one,
52    /// so that nothing retries into a lockout.
53    ProviderRefused,
54    /// An internal invariant was violated — always a library bug.
55    Internal,
56}
57
58impl ErrorKind {
59    /// Every kind, for callers that enumerate them.
60    ///
61    /// This type is `#[non_exhaustive]`, so no other crate can match on it
62    /// exhaustively and none can tell whether it has seen them all. That is
63    /// deliberate — it lets a variant be added without breaking callers — but
64    /// it also means a list like the ontology's error catalog cannot check its
65    /// own completeness. This crate can, so the list is published from here and
66    /// a test keeps it honest.
67    pub const ALL: &'static [ErrorKind] = &[
68        ErrorKind::InvalidLength,
69        ErrorKind::InvalidParameter,
70        ErrorKind::AuthenticationFailed,
71        ErrorKind::MalformedEncoding,
72        ErrorKind::Unsupported,
73        ErrorKind::NotApprovedInFipsMode,
74        ErrorKind::SelfTestFailed,
75        ErrorKind::ModuleErrorState,
76        ErrorKind::EntropyFailure,
77        ErrorKind::CounterExhausted,
78        ErrorKind::ProviderUnavailable,
79        ErrorKind::ProviderRefused,
80        ErrorKind::Internal,
81    ];
82
83    /// Stable kebab-case identifier used in ontology exports and CLI/MCP output.
84    pub const fn id(self) -> &'static str {
85        match self {
86            Self::InvalidLength => "invalid-length",
87            Self::InvalidParameter => "invalid-parameter",
88            Self::AuthenticationFailed => "authentication-failed",
89            Self::Unsupported => "unsupported",
90            Self::NotApprovedInFipsMode => "not-approved-in-fips-mode",
91            Self::SelfTestFailed => "self-test-failed",
92            Self::ModuleErrorState => "module-error-state",
93            Self::EntropyFailure => "entropy-failure",
94            Self::CounterExhausted => "counter-exhausted",
95            Self::MalformedEncoding => "malformed-encoding",
96            Self::ProviderUnavailable => "provider-unavailable",
97            Self::ProviderRefused => "provider-refused",
98            Self::Internal => "internal",
99        }
100    }
101
102    /// Whether retrying the identical call could plausibly succeed.
103    ///
104    /// Agents use this to decide between *retry*, *re-parameterize*, and *abort*.
105    #[must_use]
106    pub const fn retryable(self) -> bool {
107        matches!(self, Self::EntropyFailure | Self::ProviderUnavailable)
108    }
109
110    /// Whether the caller should change inputs and try again.
111    #[must_use]
112    pub const fn caller_correctable(self) -> bool {
113        matches!(
114            self,
115            Self::InvalidLength
116                | Self::InvalidParameter
117                | Self::MalformedEncoding
118                | Self::Unsupported
119                | Self::NotApprovedInFipsMode
120                | Self::CounterExhausted
121        )
122    }
123}
124
125/// A failure from any IronCrypto operation.
126///
127/// Deliberately opaque about *why* an authentication check failed — the
128/// [`ErrorKind`] is the only distinguishing datum for verification failures, so
129/// error handling cannot become a decryption oracle.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct Error {
132    kind: ErrorKind,
133    context: &'static str,
134}
135
136impl Error {
137    /// Construct an error with a static context string (an algorithm or field name).
138    pub const fn new(kind: ErrorKind, context: &'static str) -> Self {
139        Self { kind, context }
140    }
141
142    /// The machine-actionable classification.
143    pub const fn kind(&self) -> ErrorKind {
144        self.kind
145    }
146
147    /// A short static hint naming the failing parameter or component.
148    pub const fn context(&self) -> &'static str {
149        self.context
150    }
151}
152
153impl fmt::Display for Error {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "{}: {}", self.kind.id(), self.context)
156    }
157}
158
159#[cfg(feature = "std")]
160impl std::error::Error for Error {}
161
162/// Shorthand for building an [`Error`].
163#[macro_export]
164macro_rules! err {
165    ($kind:ident, $ctx:literal) => {
166        $crate::Error::new($crate::ErrorKind::$kind, $ctx)
167    };
168}
169
170/// Shorthand for `return Err(err!(..))` guarded by a condition.
171#[macro_export]
172macro_rules! ensure {
173    ($cond:expr, $kind:ident, $ctx:literal) => {
174        if !($cond) {
175            return Err($crate::Error::new($crate::ErrorKind::$kind, $ctx));
176        }
177    };
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    /// `ALL` must really be all of them.
185    ///
186    /// The match below is exhaustive, and this is the crate that defines the
187    /// type, so `#[non_exhaustive]` does not apply here and adding a variant
188    /// stops this compiling until it is handled. Requiring `ALL` to contain
189    /// each one is what turns "the compiler noticed" into "the list was
190    /// updated".
191    #[test]
192    fn the_variant_list_is_complete() {
193        // The match is a no-op by construction, and that is the point: it
194        // exists so the compiler refuses this file when a variant is added,
195        // not to compute anything. Clippy is right that it does nothing and
196        // wrong that it is therefore unnecessary.
197        #[allow(clippy::needless_match)]
198        fn identify(kind: ErrorKind) -> ErrorKind {
199            match kind {
200                ErrorKind::InvalidLength => ErrorKind::InvalidLength,
201                ErrorKind::InvalidParameter => ErrorKind::InvalidParameter,
202                ErrorKind::AuthenticationFailed => ErrorKind::AuthenticationFailed,
203                ErrorKind::MalformedEncoding => ErrorKind::MalformedEncoding,
204                ErrorKind::Unsupported => ErrorKind::Unsupported,
205                ErrorKind::NotApprovedInFipsMode => ErrorKind::NotApprovedInFipsMode,
206                ErrorKind::SelfTestFailed => ErrorKind::SelfTestFailed,
207                ErrorKind::ModuleErrorState => ErrorKind::ModuleErrorState,
208                ErrorKind::EntropyFailure => ErrorKind::EntropyFailure,
209                ErrorKind::CounterExhausted => ErrorKind::CounterExhausted,
210                ErrorKind::ProviderUnavailable => ErrorKind::ProviderUnavailable,
211                ErrorKind::ProviderRefused => ErrorKind::ProviderRefused,
212                ErrorKind::Internal => ErrorKind::Internal,
213            }
214        }
215
216        // The match catches a variant being added: it stops compiling until
217        // the new one is handled, and handling it means editing the list right
218        // there. It does not catch one being dropped from `ALL`, because a
219        // shorter list still maps each of its members to itself. This count
220        // sits beside the match so the two are edited together.
221        assert_eq!(ErrorKind::ALL.len(), 13);
222
223        for kind in ErrorKind::ALL {
224            assert_eq!(identify(*kind), *kind);
225        }
226
227        // Identifiers are the join key the ontology matches on, so a duplicate
228        // would make two kinds indistinguishable to every consumer. Compared
229        // pairwise rather than collected, since this crate has no allocator.
230        for (i, a) in ErrorKind::ALL.iter().enumerate() {
231            for b in ErrorKind::ALL.iter().skip(i + 1) {
232                assert_ne!(a.id(), b.id(), "two kinds share an identifier");
233            }
234            assert!(!a.id().is_empty());
235        }
236    }
237
238    /// What a caller is told to do when a key's holder fails.
239    ///
240    /// The two kinds differ in exactly this, and a retry loop reads it: one
241    /// that retried a refusal would walk a token into its PIN lockout.
242    /// Neither is the caller's to correct by changing what it passed.
243    #[test]
244    fn only_an_unavailable_provider_is_worth_retrying() {
245        assert!(ErrorKind::ProviderUnavailable.retryable());
246        assert!(!ErrorKind::ProviderRefused.retryable());
247        assert!(!ErrorKind::ProviderUnavailable.caller_correctable());
248        assert!(!ErrorKind::ProviderRefused.caller_correctable());
249        // And the rest are as they were: only these two and a failed entropy
250        // source say a retry may help.
251        for kind in ErrorKind::ALL {
252            let expected = matches!(
253                kind,
254                ErrorKind::EntropyFailure | ErrorKind::ProviderUnavailable
255            );
256            assert_eq!(kind.retryable(), expected, "{}", kind.id());
257        }
258    }
259}