Skip to main content

lys_core/
error.rs

1//! [`TrustError`] and the crate-wide [`TrustResult`] alias.
2//!
3//! Every fallible public API on the trust primitives returns
4//! `TrustResult<T>`. Each variant names a distinct trust operation, and every
5//! `Display` string carries the operation name so callers can surface a
6//! precise diagnostic without parsing free-form text. Variants that carry a
7//! dynamic cause use a `reason: String` field; signature verification has a
8//! dedicated parameterless variant so callers can match it structurally
9//! rather than by string.
10
11/// Errors returned from the trust primitives.
12///
13/// Variants are grouped by operation: certificate lifecycle (generation,
14/// parsing, verification, revocation), the Merkle transparency log, sealed
15/// payload transport (seal/unseal), key management, signing, and the
16/// dedicated signature-verification failure.
17///
18/// # Why this is `#[non_exhaustive]`
19///
20/// Downstream `match`es must carry a wildcard arm, which makes **every future
21/// variant an additive change instead of a breaking one**. That matters more
22/// here than in a typical library: this crate gains a verification surface with
23/// each new artifact class, and each one wants its own non-oracle failure value
24/// (see [`Self::ReceiptVerification`] and [`Self::BundleVerification`]). Without
25/// this attribute, giving a new artifact class an honest error would mean a
26/// semver break, and the cheap way out — reusing another class's variant —
27/// makes errors mean less over time.
28///
29/// # Non-oracle variants carry no cause, deliberately
30///
31/// The verification failures are deliberately causeless. A caller learns *that*
32/// an artifact failed, never *which check* failed, because these types are
33/// reachable from network-exposed surfaces where a distinguishable error is a
34/// parsing oracle. Callers diagnosing their own artifacts verify the embedded
35/// pieces individually, where the errors are specific and actionable.
36#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum TrustError {
39    /// Generating a certificate failed.
40    #[error("certificate generation failed: {reason}")]
41    CertificateGeneration {
42        /// Human-readable cause of the generation failure.
43        reason: String,
44    },
45
46    /// Parsing a certificate or extracting one of its fields failed.
47    #[error("certificate parsing failed: {reason}")]
48    CertificateParsing {
49        /// Human-readable cause of the parsing failure.
50        reason: String,
51    },
52
53    /// Verifying a certificate chain failed.
54    #[error("certificate verification failed: {reason}")]
55    CertificateVerification {
56        /// Human-readable cause of the verification failure.
57        reason: String,
58    },
59
60    /// A certificate revocation operation failed.
61    ///
62    /// Reserved for consumer-side operations: revocation tracking is
63    /// explicitly a consumer concern (a design non-goal for this crate), so
64    /// this crate never constructs the variant itself. It exists so
65    /// consumers implementing revocation stores can surface failures through
66    /// the shared [`TrustError`] type.
67    #[error("certificate revocation failed: {reason}")]
68    CertificateRevocation {
69        /// Human-readable cause of the revocation failure.
70        reason: String,
71    },
72
73    /// A Merkle transparency-log operation failed.
74    #[error("merkle tree operation failed: {reason}")]
75    MerkleTree {
76        /// Human-readable cause of the Merkle operation failure.
77        reason: String,
78    },
79
80    /// Sealing a payload for a recipient failed.
81    #[error("seal failed: {reason}")]
82    Seal {
83        /// Human-readable cause of the seal failure.
84        reason: String,
85    },
86
87    /// Unsealing failed — deliberately omits the cause so callers cannot
88    /// distinguish wrong-key from tampered-ciphertext (non-oracle).
89    #[error("unseal failed")]
90    UnsealFailed,
91
92    /// Attestation verification failed — the sender's public key did not
93    /// match, or the signature over the sealed payload was invalid.
94    #[error("attestation verification failed")]
95    AttestationFailed,
96
97    /// A key-management operation failed: file I/O, environment loading,
98    /// base64 decoding, or key-material length validation.
99    #[error("key management failed: {reason}")]
100    KeyManagement {
101        /// Human-readable cause of the key-management failure.
102        reason: String,
103    },
104
105    /// Producing a signature failed.
106    ///
107    /// Reserved for consumer-side signing pipelines. This crate's own
108    /// Ed25519 signing is infallible (dalek 2 deterministic signing), so the
109    /// crate never constructs the variant itself; consumers whose signing
110    /// paths can fail (HSMs, remote signers, key lookups) surface those
111    /// failures through it.
112    #[error("signing failed: {reason}")]
113    Signing {
114        /// Human-readable cause of the signing failure.
115        reason: String,
116    },
117
118    /// A signature failed verification, or the supplied signature or public
119    /// key bytes were structurally invalid.
120    ///
121    /// This is also the attestation artifact class's single non-oracle
122    /// failure: a malformed or non-canonical COSE attestation artifact, a
123    /// payload-hash mismatch, and an invalid signature all collapse to this
124    /// one value, so callers cannot distinguish which check rejected the
125    /// artifact.
126    #[error("invalid signature")]
127    InvalidSignature,
128
129    /// Building or encoding a checkpoint or signed note failed (invalid
130    /// origin or key name, malformed body).
131    #[error("checkpoint encoding failed: {reason}")]
132    CheckpointEncoding {
133        /// Human-readable cause of the encoding failure.
134        reason: String,
135    },
136
137    /// Parsing a checkpoint body failed. Used on already-verified body text
138    /// and operator-supplied text; artifact verification collapses it
139    /// (non-oracle).
140    #[error("checkpoint parsing failed: {reason}")]
141    CheckpointParsing {
142        /// Human-readable cause of the parsing failure.
143        reason: String,
144    },
145
146    /// A note verifier key string was malformed or internally inconsistent.
147    /// Trusted operator input — carries an actionable reason.
148    #[error("invalid note verifier key: {reason}")]
149    VerifierKey {
150        /// Human-readable cause of the verifier-key failure.
151        reason: String,
152    },
153
154    /// A signed note failed verification — deliberately omits the cause so
155    /// callers cannot distinguish malformed envelope, unknown key, or bad
156    /// signature (non-oracle).
157    #[error("note verification failed")]
158    NoteVerification,
159
160    /// Building a log proof artifact failed (tree too large for JSON-safe
161    /// integers, size/index invariant violations at build time).
162    #[error("log artifact encoding failed: {reason}")]
163    LogArtifactEncoding {
164        /// Human-readable cause of the encoding failure.
165        reason: String,
166    },
167
168    /// A log proof artifact failed verification — deliberately omits the
169    /// cause (non-oracle): bad checkpoint signature, size mismatch, root
170    /// mismatch, malformed hashes, and kind confusion are indistinguishable.
171    #[error("log artifact verification failed")]
172    LogArtifactVerification,
173
174    /// An anchor receipt failed verification — deliberately omits the cause
175    /// (non-oracle): a bad signature, the wrong anchor key, a tampered
176    /// inclusion path, a malformed or non-canonical artifact, and a leaf the
177    /// receipt does not prove are all indistinguishable.
178    ///
179    /// Distinct from [`Self::InvalidSignature`], which it previously reused: a
180    /// receipt failure is not necessarily a signature failure, and saying so
181    /// misdescribed every other way one can fail. The *class* of artifact is
182    /// already known to the caller from the function they called, so naming it
183    /// leaks nothing a caller did not supply.
184    #[error("receipt verification failed")]
185    ReceiptVerification,
186
187    /// An anchor delegation failed verification — deliberately omits the cause
188    /// (non-oracle): a malformed or non-canonical artifact, the wrong content
189    /// type, a `kid` that is not the root key the caller named, an origin the
190    /// caller did not ask for, an unknown role, an unusable delegated key, an
191    /// empty origin, a non-empty unprotected header and a bad signature all
192    /// return this one value.
193    ///
194    /// The collapse matters more here than for most artifact classes. A
195    /// delegation is the statement *"this key speaks for that origin"*, so a
196    /// verifier that reported which check fired would let a prober separate
197    /// "your signature is wrong" from "you named the wrong root key" from "you
198    /// named the wrong origin" — that is, it would answer questions about the
199    /// verifier's own configuration to anyone who can hand it bytes.
200    ///
201    /// # A collapsed error type is not a non-oracle on its own
202    ///
203    /// This variant makes the failures indistinguishable **in the return
204    /// value**, which is one channel. It is not by itself a claim that they are
205    /// indistinguishable, and an earlier version of this doc said they were.
206    /// An adversarial review measured a **32.8×** timing separation between a
207    /// `kid`/origin mismatch and a bad signature, because the first two returned
208    /// before the Ed25519 verification ran and that verification dominates
209    /// everything preceding it. The error values were identical throughout.
210    ///
211    /// **The general rule, which this crate had stated only about error values:
212    /// a collapsed error type is not a non-oracle if the amount of work done
213    /// differs per cause.** An error enum is observable in one channel; a
214    /// function is observable in several — wall time, allocation, cache
215    /// behaviour. `delegation::verify_delegation` therefore runs its signature
216    /// verification unconditionally and combines all three results at the end.
217    /// Any future verifier reaching for this variant inherits that obligation
218    /// along with it. (Named without an intra-doc link: it lives behind
219    /// `unstable-anchor`, and a link from ungated docs to a gated item resolves
220    /// under `--all-features` and breaks in the default doc build.)
221    #[error("delegation verification failed")]
222    DelegationVerification,
223
224    /// Building a delegation artifact failed: the claim cannot be encoded into
225    /// a delegation this crate would itself accept.
226    ///
227    /// # ⚠️ Operator-facing only — never return this from a path a stranger can reach
228    ///
229    /// This variant **may carry a cause**, and that is exactly why its blast
230    /// radius has to be stated rather than left to judgement. It is for the
231    /// *issuing* side: the caller supplied every input, holds the signing key,
232    /// and already knows everything the reason could tell them, so a descriptive
233    /// message costs nothing and saves an operator staring at "delegation
234    /// verification failed" because their origin was 3885 bytes.
235    ///
236    /// **Reusing it anywhere reachable by an untrusted party — in
237    /// `delegation::verify_delegation`, or in any future
238    /// parse-then-act helper — reintroduces the parsing oracle that
239    /// [`Self::DelegationVerification`] exists to close.** The two variants are
240    /// not interchangeable and the compiler cannot tell them apart; only this
241    /// sentence can. It follows the same split the crate already makes for
242    /// [`Self::CheckpointEncoding`]/[`Self::NoteVerification`] and
243    /// [`Self::LogArtifactEncoding`]/[`Self::LogArtifactVerification`].
244    ///
245    /// It exists because encode and decode were allowed to disagree once. The
246    /// artifact size cap was enforced on the decode side only, so an origin of
247    /// 3884 bytes signed and verified while 3885 signed successfully and then
248    /// failed every verification afterwards — precisely the "fails at some
249    /// later, less debuggable moment" outcome that assembling-with-verification
250    /// exists to prevent. Every constraint the decoder enforces is now refused
251    /// at encode too, and this variant is how that refusal is reported.
252    #[error("delegation encoding failed: {reason}")]
253    DelegationEncoding {
254        /// Human-readable cause of the encoding failure.
255        reason: String,
256    },
257
258    /// A verification bundle failed verification — deliberately omits the
259    /// cause (non-oracle): a malformed container, a broken inclusion proof, a
260    /// receipt from the wrong anchor, and a chain whose links do not join are
261    /// all indistinguishable.
262    ///
263    /// Distinct from [`Self::LogArtifactVerification`], which it previously
264    /// reused, for the same reason: a bundle is a different artifact class with
265    /// a different set of ways to be wrong.
266    #[error("bundle verification failed")]
267    BundleVerification,
268}
269
270/// Convenience alias for `Result<T, TrustError>`.
271pub type TrustResult<T> = std::result::Result<T, TrustError>;
272
273#[cfg(test)]
274#[path = "error_tests.rs"]
275mod tests;