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#[derive(Debug, thiserror::Error)]
18pub enum TrustError {
19    /// Generating a certificate failed.
20    #[error("certificate generation failed: {reason}")]
21    CertificateGeneration {
22        /// Human-readable cause of the generation failure.
23        reason: String,
24    },
25
26    /// Parsing a certificate or extracting one of its fields failed.
27    #[error("certificate parsing failed: {reason}")]
28    CertificateParsing {
29        /// Human-readable cause of the parsing failure.
30        reason: String,
31    },
32
33    /// Verifying a certificate chain failed.
34    #[error("certificate verification failed: {reason}")]
35    CertificateVerification {
36        /// Human-readable cause of the verification failure.
37        reason: String,
38    },
39
40    /// A certificate revocation operation failed.
41    ///
42    /// Reserved for consumer-side operations: revocation tracking is
43    /// explicitly a consumer concern (a design non-goal for this crate), so
44    /// this crate never constructs the variant itself. It exists so
45    /// consumers implementing revocation stores can surface failures through
46    /// the shared [`TrustError`] type.
47    #[error("certificate revocation failed: {reason}")]
48    CertificateRevocation {
49        /// Human-readable cause of the revocation failure.
50        reason: String,
51    },
52
53    /// A Merkle transparency-log operation failed.
54    #[error("merkle tree operation failed: {reason}")]
55    MerkleTree {
56        /// Human-readable cause of the Merkle operation failure.
57        reason: String,
58    },
59
60    /// Sealing a payload for a recipient failed.
61    #[error("seal failed: {reason}")]
62    Seal {
63        /// Human-readable cause of the seal failure.
64        reason: String,
65    },
66
67    /// Unsealing failed — deliberately omits the cause so callers cannot
68    /// distinguish wrong-key from tampered-ciphertext (non-oracle).
69    #[error("unseal failed")]
70    UnsealFailed,
71
72    /// Attestation verification failed — the sender's public key did not
73    /// match, or the signature over the sealed payload was invalid.
74    #[error("attestation verification failed")]
75    AttestationFailed,
76
77    /// A key-management operation failed: file I/O, environment loading,
78    /// base64 decoding, or key-material length validation.
79    #[error("key management failed: {reason}")]
80    KeyManagement {
81        /// Human-readable cause of the key-management failure.
82        reason: String,
83    },
84
85    /// Producing a signature failed.
86    ///
87    /// Reserved for consumer-side signing pipelines. This crate's own
88    /// Ed25519 signing is infallible (dalek 2 deterministic signing), so the
89    /// crate never constructs the variant itself; consumers whose signing
90    /// paths can fail (HSMs, remote signers, key lookups) surface those
91    /// failures through it.
92    #[error("signing failed: {reason}")]
93    Signing {
94        /// Human-readable cause of the signing failure.
95        reason: String,
96    },
97
98    /// A signature failed verification, or the supplied signature or public
99    /// key bytes were structurally invalid.
100    ///
101    /// This is also the attestation artifact class's single non-oracle
102    /// failure: a malformed or non-canonical COSE attestation artifact, a
103    /// payload-hash mismatch, and an invalid signature all collapse to this
104    /// one value, so callers cannot distinguish which check rejected the
105    /// artifact.
106    #[error("invalid signature")]
107    InvalidSignature,
108
109    /// Building or encoding a checkpoint or signed note failed (invalid
110    /// origin or key name, malformed body).
111    #[error("checkpoint encoding failed: {reason}")]
112    CheckpointEncoding {
113        /// Human-readable cause of the encoding failure.
114        reason: String,
115    },
116
117    /// Parsing a checkpoint body failed. Used on already-verified body text
118    /// and operator-supplied text; artifact verification collapses it
119    /// (non-oracle).
120    #[error("checkpoint parsing failed: {reason}")]
121    CheckpointParsing {
122        /// Human-readable cause of the parsing failure.
123        reason: String,
124    },
125
126    /// A note verifier key string was malformed or internally inconsistent.
127    /// Trusted operator input — carries an actionable reason.
128    #[error("invalid note verifier key: {reason}")]
129    VerifierKey {
130        /// Human-readable cause of the verifier-key failure.
131        reason: String,
132    },
133
134    /// A signed note failed verification — deliberately omits the cause so
135    /// callers cannot distinguish malformed envelope, unknown key, or bad
136    /// signature (non-oracle).
137    #[error("note verification failed")]
138    NoteVerification,
139
140    /// Building a log proof artifact failed (tree too large for JSON-safe
141    /// integers, size/index invariant violations at build time).
142    #[error("log artifact encoding failed: {reason}")]
143    LogArtifactEncoding {
144        /// Human-readable cause of the encoding failure.
145        reason: String,
146    },
147
148    /// A log proof artifact failed verification — deliberately omits the
149    /// cause (non-oracle): bad checkpoint signature, size mismatch, root
150    /// mismatch, malformed hashes, and kind confusion are indistinguishable.
151    #[error("log artifact verification failed")]
152    LogArtifactVerification,
153}
154
155/// Convenience alias for `Result<T, TrustError>`.
156pub type TrustResult<T> = std::result::Result<T, TrustError>;
157
158#[cfg(test)]
159#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn display_certificate_generation() {
165        let err = TrustError::CertificateGeneration {
166            reason: "rcgen rejected params".to_string(),
167        };
168        let display = err.to_string();
169        assert!(
170            display.contains("certificate generation failed"),
171            "got: {display}"
172        );
173        assert!(display.contains("rcgen rejected params"), "got: {display}");
174    }
175
176    #[test]
177    fn display_certificate_parsing() {
178        let err = TrustError::CertificateParsing {
179            reason: "malformed DER".to_string(),
180        };
181        let display = err.to_string();
182        assert!(
183            display.contains("certificate parsing failed"),
184            "got: {display}"
185        );
186        assert!(display.contains("malformed DER"), "got: {display}");
187    }
188
189    #[test]
190    fn display_certificate_verification() {
191        let err = TrustError::CertificateVerification {
192            reason: "issuer mismatch".to_string(),
193        };
194        let display = err.to_string();
195        assert!(
196            display.contains("certificate verification failed"),
197            "got: {display}"
198        );
199        assert!(display.contains("issuer mismatch"), "got: {display}");
200    }
201
202    #[test]
203    fn display_certificate_revocation() {
204        let err = TrustError::CertificateRevocation {
205            reason: "unknown fingerprint".to_string(),
206        };
207        let display = err.to_string();
208        assert!(
209            display.contains("certificate revocation failed"),
210            "got: {display}"
211        );
212        assert!(display.contains("unknown fingerprint"), "got: {display}");
213    }
214
215    #[test]
216    fn display_merkle_tree() {
217        let err = TrustError::MerkleTree {
218            reason: "index out of range".to_string(),
219        };
220        let display = err.to_string();
221        assert!(
222            display.contains("merkle tree operation failed"),
223            "got: {display}"
224        );
225        assert!(display.contains("index out of range"), "got: {display}");
226    }
227
228    #[test]
229    fn display_seal() {
230        let err = TrustError::Seal {
231            reason: "HKDF expand failed".to_string(),
232        };
233        let display = err.to_string();
234        assert!(display.contains("seal failed"), "got: {display}");
235        assert!(display.contains("HKDF expand failed"), "got: {display}");
236    }
237
238    #[test]
239    fn display_key_management() {
240        let err = TrustError::KeyManagement {
241            reason: "identity key file has invalid length".to_string(),
242        };
243        let display = err.to_string();
244        assert!(display.contains("key management failed"), "got: {display}");
245        assert!(
246            display.contains("identity key file has invalid length"),
247            "got: {display}"
248        );
249    }
250
251    #[test]
252    fn display_signing() {
253        let err = TrustError::Signing {
254            reason: "no signing key configured".to_string(),
255        };
256        let display = err.to_string();
257        assert!(display.contains("signing failed"), "got: {display}");
258        assert!(
259            display.contains("no signing key configured"),
260            "got: {display}"
261        );
262    }
263
264    #[test]
265    fn display_unseal_failed() {
266        let err = TrustError::UnsealFailed;
267        let display = err.to_string();
268        assert!(display.contains("unseal failed"), "got: {display}");
269    }
270
271    #[test]
272    fn display_attestation_failed() {
273        let err = TrustError::AttestationFailed;
274        let display = err.to_string();
275        assert!(
276            display.contains("attestation verification failed"),
277            "got: {display}"
278        );
279    }
280
281    #[test]
282    fn display_invalid_signature() {
283        let err = TrustError::InvalidSignature;
284        let display = err.to_string();
285        assert!(display.contains("invalid signature"), "got: {display}");
286    }
287
288    #[test]
289    fn display_checkpoint_encoding() {
290        let err = TrustError::CheckpointEncoding {
291            reason: "origin contains '+'".to_string(),
292        };
293        let display = err.to_string();
294        assert!(
295            display.contains("checkpoint encoding failed"),
296            "got: {display}"
297        );
298        assert!(display.contains("origin contains '+'"), "got: {display}");
299    }
300
301    #[test]
302    fn display_checkpoint_parsing() {
303        let err = TrustError::CheckpointParsing {
304            reason: "tree size has a leading zero".to_string(),
305        };
306        let display = err.to_string();
307        assert!(
308            display.contains("checkpoint parsing failed"),
309            "got: {display}"
310        );
311        assert!(
312            display.contains("tree size has a leading zero"),
313            "got: {display}"
314        );
315    }
316
317    #[test]
318    fn display_verifier_key() {
319        let err = TrustError::VerifierKey {
320            reason: "declared key ID does not match".to_string(),
321        };
322        let display = err.to_string();
323        assert!(
324            display.contains("invalid note verifier key"),
325            "got: {display}"
326        );
327        assert!(
328            display.contains("declared key ID does not match"),
329            "got: {display}"
330        );
331    }
332
333    /// Non-oracle: the note-verification failure string is a single generic
334    /// message that never distinguishes a malformed envelope from an unknown
335    /// key from a bad signature.
336    #[test]
337    fn note_verification_display_is_single_and_generic() {
338        let display = TrustError::NoteVerification.to_string();
339        assert_eq!(display, "note verification failed");
340        for oracle_word in ["signature", "key", "envelope", "structure", "base64"] {
341            assert!(
342                !display.contains(oracle_word),
343                "non-oracle message must not mention {oracle_word}: {display}"
344            );
345        }
346    }
347
348    #[test]
349    fn display_log_artifact_encoding() {
350        let err = TrustError::LogArtifactEncoding {
351            reason: "tree size exceeds the JSON-safe bound".to_string(),
352        };
353        let display = err.to_string();
354        assert!(
355            display.contains("log artifact encoding failed"),
356            "got: {display}"
357        );
358        assert!(
359            display.contains("tree size exceeds the JSON-safe bound"),
360            "got: {display}"
361        );
362    }
363
364    /// Non-oracle: the artifact-verification failure string is a single
365    /// generic message that never distinguishes checkpoint, size, root,
366    /// hash, or kind failures.
367    #[test]
368    fn log_artifact_verification_display_is_single_and_generic() {
369        let display = TrustError::LogArtifactVerification.to_string();
370        assert_eq!(display, "log artifact verification failed");
371        for oracle_word in ["signature", "checkpoint", "root", "size", "hash", "format"] {
372            assert!(
373                !display.contains(oracle_word),
374                "non-oracle message must not mention {oracle_word}: {display}"
375            );
376        }
377    }
378
379    #[test]
380    fn trust_result_alias_accepts_ok_and_err() {
381        let ok: TrustResult<u8> = Ok(7);
382        let err: TrustResult<u8> = Err(TrustError::InvalidSignature);
383        assert!(ok.is_ok());
384        assert!(err.is_err());
385    }
386}