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;