Skip to main content

metamorphic_log/
error.rs

1//! Error types for the transparency-log engine.
2//!
3//! This module defines the crate-wide [`Error`](enum@Error) enum and [`Result`]
4//! alias. Variants are added slice-by-slice as the verification core, tile
5//! substrate, checkpoint signing, and CONIKS layers land. Slice 1 (#327)
6//! introduces the canonical-leaf and RFC 6962 / RFC 9162 proof-verification
7//! variants.
8
9use thiserror::Error;
10
11/// Convenience alias for results returned by this crate.
12pub type Result<T> = core::result::Result<T, Error>;
13
14/// All possible errors from transparency-log operations.
15///
16/// The enum is `#[non_exhaustive]`: downstream code must include a wildcard arm
17/// so new variants in later slices are not a breaking change.
18#[derive(Debug, Error, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum Error {
21    /// A generic verification failure with a human-readable explanation.
22    ///
23    /// Prefer one of the specific variants below where it applies; this exists
24    /// for cases that do not warrant a dedicated variant.
25    #[error("verification failed: {0}")]
26    Verification(String),
27
28    /// A leaf index was greater than or equal to the tree size.
29    ///
30    /// Inclusion proofs require `0 <= index < size`.
31    #[error("leaf index {index} is beyond tree size {size}")]
32    IndexBeyondSize {
33        /// The requested leaf index.
34        index: u64,
35        /// The tree size the proof was verified against.
36        size: u64,
37    },
38
39    /// The supplied proof had the wrong number of hashes for the given
40    /// `(index, size)` (inclusion) or `(size1, size2)` (consistency).
41    #[error("wrong proof size: got {got}, want {want}")]
42    WrongProofSize {
43        /// The number of hashes actually supplied.
44        got: usize,
45        /// The number of hashes the proof shape requires.
46        want: usize,
47    },
48
49    /// A hash in a proof, leaf, or root did not have the expected byte length
50    /// (RFC 6962 SHA-256 nodes are always 32 bytes).
51    #[error("invalid hash length: got {got} bytes, want {want}")]
52    InvalidHashLength {
53        /// The actual byte length.
54        got: usize,
55        /// The expected byte length.
56        want: usize,
57    },
58
59    /// A recomputed root did not match the supplied/expected root.
60    ///
61    /// This is the headline negative outcome of inclusion and consistency
62    /// verification: the proof did not bind the claimed leaf/old-tree to the
63    /// supplied root.
64    #[error("root mismatch: recomputed root does not match the expected root")]
65    RootMismatch,
66
67    /// A consistency proof was requested from an empty (size-0) tree, which is
68    /// not meaningful: there is no earlier root to be consistent with.
69    #[error("consistency proof from an empty tree is meaningless")]
70    EmptyTreeConsistency,
71
72    /// `size2 < size1` for a consistency proof. Consistency is only defined
73    /// when the second (newer) tree is at least as large as the first.
74    #[error("tree size regression: size2 ({size2}) < size1 ({size1})")]
75    SizeRegression {
76        /// The earlier (smaller) tree size.
77        size1: u64,
78        /// The later (claimed larger) tree size.
79        size2: u64,
80    },
81
82    /// A consistency proof between two equal tree sizes carried a non-empty
83    /// proof. When `size1 == size2` the proof MUST be empty and the two roots
84    /// MUST be equal.
85    #[error("consistency proof between equal sizes must be empty")]
86    NonEmptyEqualSizeProof,
87
88    /// The canonical leaf encoding was malformed (e.g. a length-prefixed field
89    /// would overrun the available bytes, or a context label is invalid).
90    #[error("malformed canonical leaf: {0}")]
91    MalformedLeaf(String),
92
93    /// A C2SP `tlog-tiles` tile coordinate or tile-path component was invalid
94    /// (e.g. level out of range, partial-tile width out of `1..=255`, or a path
95    /// that does not match `tile/<L>/<N>[.p/<W>]`).
96    #[error("malformed tile: {0}")]
97    MalformedTile(String),
98
99    /// A C2SP `checkpoint` note body was malformed (missing origin/size/root
100    /// lines, a non-decimal or leading-zero size, an empty extension line, or a
101    /// root hash that is not exactly 32 bytes once base64-decoded).
102    #[error("malformed checkpoint: {0}")]
103    MalformedCheckpoint(String),
104
105    /// A C2SP `signed-note` could not be parsed (not valid UTF-8, a forbidden
106    /// ASCII control character, no blank-line/signature separator, or a
107    /// malformed signature line / verifier key).
108    #[error("malformed signed note: {0}")]
109    MalformedNote(String),
110
111    /// A signature line referenced a known key (matching name **and** key id)
112    /// but the signature failed to verify. Per the C2SP `signed-note` spec the
113    /// whole note is rejected in this case.
114    #[error("invalid signature for known key {name:?} (key id {key_id:08x})")]
115    InvalidSignature {
116        /// The key name from the verifier / signature line.
117        name: String,
118        /// The 4-byte key id, as a big-endian `u32`.
119        key_id: u32,
120    },
121
122    /// The note parsed correctly but no signature from any supplied trusted key
123    /// verified, so the note text MUST NOT be trusted.
124    #[error("note has no verifiable signature from a trusted key")]
125    NoTrustedSignature,
126
127    /// An additive hybrid post-quantum composite signature could not be produced
128    /// or its key material could not be decoded/derived (via the
129    /// metamorphic-crypto composite primitive). A *verification* failure of an
130    /// otherwise well-formed line is reported as [`Error::InvalidSignature`]
131    /// instead, matching the classical path and the C2SP `signed-note` rule.
132    #[error("hybrid composite signature error: {0}")]
133    HybridSignature(String),
134
135    /// A CONIKS namespace label was malformed (empty, or containing a byte
136    /// outside the printable-ASCII-excluding-`/` set). The namespace is the
137    /// per-tenant domain separator threaded through every VRF, commitment, and
138    /// prefix-tree hash, so it must be unambiguous.
139    #[error("malformed namespace: {0}")]
140    MalformedNamespace(String),
141
142    /// A VRF operation failed structurally (e.g. a key/proof of the wrong byte
143    /// length, or a proof component that is not a valid curve point). A VRF
144    /// proof that is well-formed but does not verify against `(public_key,
145    /// alpha)` is reported as [`Error::VrfProofInvalid`], not this variant.
146    #[error("vrf error: {0}")]
147    Vrf(String),
148
149    /// A VRF proof was well-formed but did not verify: the claimed
150    /// identity→index binding is not authentic under the namespace's VRF public
151    /// key. CONIKS lookup/absence proofs are rejected in this case, because the
152    /// private index they rely on is unproven.
153    #[error("vrf proof did not verify against the namespace public key")]
154    VrfProofInvalid,
155
156    /// A commitment failed to open: the supplied `(value, opening)` does not
157    /// reproduce the committed digest. The commitment binds an index to a value
158    /// (SHA3-512, post-quantum), so a mismatch means the proof does not bind the
159    /// claimed value.
160    #[error("commitment did not open to the claimed value")]
161    CommitmentMismatch,
162
163    /// A CONIKS lookup or absence proof was structurally malformed (e.g. an
164    /// authentication-path component of the wrong length, or a sibling bitmap
165    /// inconsistent with the supplied sibling hashes).
166    #[error("malformed coniks proof: {0}")]
167    MalformedConiksProof(String),
168
169    /// A CONIKS lookup or absence proof was well-formed but did not verify: the
170    /// authentication path did not recompute the expected directory root. This
171    /// is the headline negative outcome of CONIKS proof verification.
172    #[error(
173        "coniks proof root mismatch: recomputed directory root does not match the expected root"
174    )]
175    ConiksRootMismatch,
176
177    /// A [`NamespacePolicy`](crate::policy::NamespacePolicy) record was
178    /// structurally malformed: an unknown enum tag, a length-prefixed field that
179    /// overruns the buffer, an invalid namespace, a `prev_policy_hash` that is
180    /// present but not exactly 64 bytes, or a field combination that is illegal
181    /// in this format version (e.g. a `commitment_hash` that does not match the
182    /// one derived from `security_level`, a `vrf_mode` other than `Classical`,
183    /// or `PureCnsa2` at a level below Cat-5).
184    #[error("malformed namespace policy: {0}")]
185    MalformedPolicy(String),
186
187    /// A proposed policy migration was rejected: the new version does not chain
188    /// to the prior one (`prev_policy_hash` / `policy_schema_version` /
189    /// `effective_from` discontinuity), or it would **weaken** the namespace's
190    /// declared posture (e.g. Cat-5 → Cat-3, a commitment-hash downgrade, or a
191    /// VRF-mode downgrade). Migrations are append-only and may only strengthen;
192    /// a weakening is surfaced here rather than silently applied.
193    #[error("policy migration rejected: {0}")]
194    PolicyMigrationRejected(String),
195
196    /// The **declared == observed** check failed: an artifact's *observed* crypto
197    /// posture does not match the *declared* [`NamespacePolicy`] posture. This is
198    /// the headline negative outcome of policy enforcement — a checkpoint
199    /// signature, CONIKS VRF suite, or commitment-hash parameter that disagrees
200    /// with what the active policy version requires is a hard rejection (no
201    /// silent downgrade).
202    ///
203    /// [`NamespacePolicy`]: crate::policy::NamespacePolicy
204    #[error("posture mismatch: declared {declared}, observed {observed}")]
205    PostureMismatch {
206        /// The posture the active policy version declares.
207        declared: String,
208        /// The posture actually observed on the artifact.
209        observed: String,
210    },
211
212    /// No [`NamespacePolicy`](crate::policy::NamespacePolicy) version is in force
213    /// for the requested tree position (or the policy chain is empty), so a
214    /// verifier cannot resolve which posture an entry at that position was
215    /// required to use. An entry can only be enforced against a policy whose
216    /// half-open validity range `[effective_from_n, effective_from_{n+1})`
217    /// contains its position.
218    #[error("no namespace policy in force: {0}")]
219    UnknownNamespacePolicy(String),
220}