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}