Skip to main content

basil_cose/
error.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Closed, diagnostic error enums.
6//!
7//! Every enum here is closed (no `Unknown`/catch-all arms for wire values) and
8//! never echoes secret bytes: arms carry labels, lengths, and codepoints only.
9
10use alloc::string::String;
11use core::fmt;
12
13/// An identifier newtype was constructed from out-of-range input.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ProfileError {
16    /// A key id must be 1..=128 bytes.
17    KeyIdLength {
18        /// The length actually supplied.
19        actual: usize,
20    },
21    /// A message id (CWT `cti`) must be 1..=64 bytes.
22    MessageIdLength {
23        /// The length actually supplied.
24        actual: usize,
25    },
26    /// A subject / audience / response-subject string must be non-empty.
27    EmptySubject,
28    /// A content type must be of `type/subtype` form with no surrounding
29    /// whitespace (RFC 9052 tstr content type carries a media type).
30    ContentTypeForm,
31    /// A KDF party identity must be non-empty when present.
32    EmptyPartyIdentity,
33    /// A signature must be non-empty.
34    EmptySignature,
35}
36
37impl fmt::Display for ProfileError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::KeyIdLength { actual } => {
41                write!(f, "key id must be 1..=128 bytes, got {actual}")
42            }
43            Self::MessageIdLength { actual } => {
44                write!(f, "message id must be 1..=64 bytes, got {actual}")
45            }
46            Self::EmptySubject => write!(f, "subject string must be non-empty"),
47            Self::ContentTypeForm => write!(
48                f,
49                "content type must be of type/subtype form without surrounding whitespace"
50            ),
51            Self::EmptyPartyIdentity => write!(f, "party identity must be non-empty when present"),
52            Self::EmptySignature => write!(f, "signature must be non-empty"),
53        }
54    }
55}
56
57impl core::error::Error for ProfileError {}
58
59/// Strict-decode rejection reasons.
60///
61/// The strict decoder rejects anything outside the profile: wrong or missing
62/// tags, indefinite lengths, non-deterministic encodings (RFC 8949 §4.2),
63/// duplicate or unknown labels, claims in unprotected headers, unknown
64/// codepoints, and malformed structure shapes.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum DecodeError {
67    /// The input is not well-formed CBOR (truncated, trailing garbage, or a
68    /// structurally invalid item).
69    Malformed,
70    /// The top-level item is not tagged.
71    NotTagged,
72    /// The top-level tag is not the expected COSE tag.
73    WrongTag {
74        /// The tag the profile requires here (18 or 96).
75        expected: u64,
76        /// The tag actually present.
77        actual: u64,
78    },
79    /// An indefinite-length item was encountered (forbidden by RFC 8949 §4.2).
80    IndefiniteLength,
81    /// An integer/length header was not minimally encoded (RFC 8949 §4.2).
82    NonMinimalEncoding,
83    /// Re-encoding the decoded message did not reproduce the input bytes:
84    /// the encoding is not the profile's deterministic encoding.
85    NonDeterministicEncoding,
86    /// A map carried the same label twice.
87    DuplicateLabel,
88    /// A label appeared somewhere the profile does not allow it.
89    UnknownLabel {
90        /// The offending label.
91        label: i64,
92    },
93    /// A text label was used; the profile is integer-labelled throughout.
94    TextLabel,
95    /// A known label carried the wrong CBOR type.
96    WrongType {
97        /// The label whose value had the wrong type.
98        label: i64,
99    },
100    /// A required header parameter is absent.
101    MissingHeader {
102        /// The absent label.
103        label: i64,
104    },
105    /// An algorithm codepoint outside the profile allow-set.
106    UnknownAlgorithm {
107        /// The offending codepoint.
108        alg: i64,
109    },
110    /// The `crit` header is absent on a layer that requires it.
111    CritMissing,
112    /// A profile label is present but not listed in `crit`.
113    CritIncomplete {
114        /// The unlisted label.
115        label: i64,
116    },
117    /// `crit` lists a label the profile does not place on this layer.
118    CritUnexpected {
119        /// The offending label.
120        label: i64,
121    },
122    /// A claim was found in an unprotected header.
123    ClaimsInUnprotected,
124    /// An unknown key appeared inside the CWT claims map.
125    UnknownClaim {
126        /// The offending CWT claim key.
127        claim: i64,
128    },
129    /// A CWT timestamp was fractional; the profile requires whole seconds.
130    FractionalTime,
131    /// A required CWT claim is absent (`iat` = 6, `cti` = 7).
132    MissingClaim {
133        /// The absent CWT claim key.
134        claim: i64,
135    },
136    /// The recipients array length is not exactly one.
137    RecipientCount {
138        /// The number of recipients actually present.
139        count: usize,
140    },
141    /// The recipient structure carries nested recipients.
142    NestedRecipients,
143    /// The recipient ciphertext is not `nil`; ECDH-ES direct key agreement
144    /// carries no recipient ciphertext.
145    RecipientCiphertextPresent,
146    /// The signed payload is absent (`nil`); detached payloads are not part
147    /// of this profile.
148    MissingPayload,
149    /// A sealed message's payload is not a tagged `COSE_Encrypt`.
150    EmbeddedNotEncrypt,
151    /// The ephemeral key is not an OKP/X25519 public key of the exact
152    /// profile shape.
153    EphemeralKeyShape,
154    /// A fixed-length field (ephemeral key, nonce, request hash) had the
155    /// wrong length.
156    InvalidLength {
157        /// The label or field the length belongs to.
158        label: i64,
159        /// The required length in bytes.
160        expected: usize,
161        /// The length actually supplied.
162        actual: usize,
163    },
164    /// An identifier failed its newtype validation while decoding.
165    Identifier(ProfileError),
166}
167
168impl fmt::Display for DecodeError {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            Self::Malformed => write!(f, "malformed CBOR input"),
172            Self::NotTagged => write!(f, "top-level item is not tagged"),
173            Self::WrongTag { expected, actual } => {
174                write!(f, "wrong COSE tag: expected {expected}, got {actual}")
175            }
176            Self::IndefiniteLength => write!(f, "indefinite-length item"),
177            Self::NonMinimalEncoding => write!(f, "non-minimal integer encoding"),
178            Self::NonDeterministicEncoding => write!(f, "non-deterministic encoding"),
179            Self::DuplicateLabel => write!(f, "duplicate map label"),
180            Self::UnknownLabel { label } => write!(f, "unknown label {label}"),
181            Self::TextLabel => write!(f, "text label outside the integer-labelled profile"),
182            Self::WrongType { label } => write!(f, "wrong CBOR type for label {label}"),
183            Self::MissingHeader { label } => write!(f, "missing required header {label}"),
184            Self::UnknownAlgorithm { alg } => write!(f, "algorithm {alg} outside the profile"),
185            Self::CritMissing => write!(f, "missing crit header"),
186            Self::CritIncomplete { label } => write!(f, "label {label} not listed in crit"),
187            Self::CritUnexpected { label } => write!(f, "unexpected crit entry {label}"),
188            Self::ClaimsInUnprotected => write!(f, "claims in unprotected header"),
189            Self::UnknownClaim { claim } => write!(f, "unknown CWT claim {claim}"),
190            Self::FractionalTime => write!(f, "fractional CWT timestamp"),
191            Self::MissingClaim { claim } => write!(f, "missing required CWT claim {claim}"),
192            Self::RecipientCount { count } => {
193                write!(f, "expected exactly one recipient, got {count}")
194            }
195            Self::NestedRecipients => write!(f, "nested recipients are not in the profile"),
196            Self::RecipientCiphertextPresent => {
197                write!(
198                    f,
199                    "recipient ciphertext must be nil for direct key agreement"
200                )
201            }
202            Self::MissingPayload => write!(f, "payload is absent"),
203            Self::EmbeddedNotEncrypt => {
204                write!(f, "sealed payload is not a tagged COSE_Encrypt")
205            }
206            Self::EphemeralKeyShape => {
207                write!(f, "ephemeral key is not the profile OKP/X25519 shape")
208            }
209            Self::InvalidLength {
210                label,
211                expected,
212                actual,
213            } => write!(f, "field {label} must be {expected} bytes, got {actual}"),
214            Self::Identifier(e) => write!(f, "invalid identifier: {e}"),
215        }
216    }
217}
218
219impl core::error::Error for DecodeError {
220    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
221        match self {
222            Self::Identifier(e) => Some(e),
223            _ => None,
224        }
225    }
226}
227
228impl From<ProfileError> for DecodeError {
229    fn from(e: ProfileError) -> Self {
230        Self::Identifier(e)
231    }
232}
233
234/// Claim-set validation failures (temporal, audience, and role shape).
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum ClaimsError {
237    /// `iat` is further in the future than the allowed clock skew.
238    IssuedInFuture,
239    /// The message is past its effective expiry (with skew allowance).
240    Expired,
241    /// The explicit `exp` span exceeds the configured maximum TTL.
242    TtlTooLong {
243        /// The explicit span in seconds.
244        seconds: i64,
245    },
246    /// The explicit `exp` is not after `iat`.
247    NonPositiveTtl,
248    /// A present `aud` is not in the allowed audience set.
249    AudienceRejected,
250    /// A claim the role requires is absent (basil private label).
251    MissingClaim {
252        /// The absent label.
253        label: i64,
254    },
255    /// A claim the role forbids is present (basil private label).
256    ForbiddenClaim {
257        /// The offending label.
258        label: i64,
259    },
260}
261
262impl fmt::Display for ClaimsError {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        match self {
265            Self::IssuedInFuture => write!(f, "issued-at is in the future beyond allowed skew"),
266            Self::Expired => write!(f, "message expired"),
267            Self::TtlTooLong { seconds } => {
268                write!(f, "explicit ttl of {seconds}s exceeds the maximum")
269            }
270            Self::NonPositiveTtl => write!(f, "expiry is not after issued-at"),
271            Self::AudienceRejected => write!(f, "audience not allowed"),
272            Self::MissingClaim { label } => write!(f, "role requires claim {label}"),
273            Self::ForbiddenClaim { label } => write!(f, "role forbids claim {label}"),
274        }
275    }
276}
277
278impl core::error::Error for ClaimsError {}
279
280/// Why producing a signature failed.
281///
282/// The shipped local signer is infallible; this exists for remote
283/// (broker-backed) implementations whose signing operation can fail.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub enum SignError {
286    /// The signer does not support the requested algorithm.
287    AlgorithmUnsupported,
288    /// The backing provider (for example an RPC-backed signer) failed. The
289    /// message is diagnostic transport/provider detail, never key material.
290    Provider {
291        /// Human-readable provider failure detail.
292        message: String,
293    },
294}
295
296impl fmt::Display for SignError {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        match self {
299            Self::AlgorithmUnsupported => write!(f, "signer does not support the algorithm"),
300            Self::Provider { message } => write!(f, "signing provider failed: {message}"),
301        }
302    }
303}
304
305impl core::error::Error for SignError {}
306
307/// Why building a message failed.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum BuildError {
310    /// The claims' `sender_key_id` does not equal the signer's key id.
311    SenderKeyMismatch,
312    /// The claims do not satisfy the declared message role shape.
313    RoleShape(ClaimsError),
314    /// A `response_key_id` must be UTF-8: the `-70004` header is a tstr.
315    ResponseKeyNotText,
316    /// Randomness was unavailable (nonce / ephemeral generation).
317    Rng,
318    /// The AEAD seal operation failed (crypto-internal; should not occur for
319    /// in-range inputs) or the key agreement was non-contributory.
320    SealFailed,
321    /// The signer failed.
322    Sign(SignError),
323    /// Internal structure encoding failed (crypto-internal; should not occur
324    /// for profile-valid inputs).
325    Codec,
326}
327
328impl fmt::Display for BuildError {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        match self {
331            Self::SenderKeyMismatch => {
332                write!(f, "claims sender key id does not match the signer key id")
333            }
334            Self::RoleShape(e) => write!(f, "claims do not fit the message role: {e}"),
335            Self::ResponseKeyNotText => write!(f, "response key id must be UTF-8"),
336            Self::Rng => write!(f, "randomness unavailable"),
337            Self::SealFailed => write!(f, "seal failed"),
338            Self::Sign(e) => write!(f, "signing failed: {e}"),
339            Self::Codec => write!(f, "structure encoding failed"),
340        }
341    }
342}
343
344impl core::error::Error for BuildError {
345    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
346        match self {
347            Self::RoleShape(e) => Some(e),
348            Self::Sign(e) => Some(e),
349            _ => None,
350        }
351    }
352}
353
354impl From<SignError> for BuildError {
355    fn from(e: SignError) -> Self {
356        Self::Sign(e)
357    }
358}
359
360/// Why verification failed.
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub enum VerifyError {
363    /// Strict decode rejected the message.
364    Decode(DecodeError),
365    /// The signature did not verify.
366    SignatureInvalid,
367    /// The verifier does not know the signing key id.
368    UnknownKeyId,
369    /// The wire algorithm disagrees with the verifier's pinned expectation.
370    AlgorithmMismatch,
371    /// The `-70003` sender key id does not equal the outer `kid`.
372    SenderKeyMismatch,
373    /// Claim validation failed.
374    Claims(ClaimsError),
375    /// Claims were present but no validation parameters were supplied, or
376    /// validation parameters were supplied and no claims were present.
377    ClaimsPresenceMismatch,
378    /// The backing provider (for example an RPC-backed verifier) failed. The
379    /// message is diagnostic transport/provider detail, never key material.
380    Provider {
381        /// Human-readable provider failure detail.
382        message: String,
383    },
384}
385
386impl fmt::Display for VerifyError {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        match self {
389            Self::Decode(e) => write!(f, "strict decode failed: {e}"),
390            Self::SignatureInvalid => write!(f, "signature verification failed"),
391            Self::UnknownKeyId => write!(f, "unknown signing key id"),
392            Self::AlgorithmMismatch => write!(f, "algorithm does not match the pinned key"),
393            Self::SenderKeyMismatch => {
394                write!(f, "sender key id claim does not match the outer kid")
395            }
396            Self::Claims(e) => write!(f, "claim validation failed: {e}"),
397            Self::ClaimsPresenceMismatch => write!(
398                f,
399                "claims presence does not match the validation expectation"
400            ),
401            Self::Provider { message } => write!(f, "verification provider failed: {message}"),
402        }
403    }
404}
405
406impl core::error::Error for VerifyError {
407    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
408        match self {
409            Self::Decode(e) => Some(e),
410            Self::Claims(e) => Some(e),
411            _ => None,
412        }
413    }
414}
415
416impl From<DecodeError> for VerifyError {
417    fn from(e: DecodeError) -> Self {
418        Self::Decode(e)
419    }
420}
421
422impl From<ClaimsError> for VerifyError {
423    fn from(e: ClaimsError) -> Self {
424        Self::Claims(e)
425    }
426}
427
428/// Why opening (decrypting) a message failed.
429///
430/// Authentication failures are deliberately opaque: a wrong key, tampered
431/// ciphertext, mismatched external AAD, and a low-order ephemeral all surface
432/// as the single [`OpenError::OpenFailed`] arm (no oracle).
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub enum OpenError {
435    /// Strict decode rejected the message.
436    Decode(DecodeError),
437    /// The message is addressed to a different recipient key id.
438    RecipientKeyMismatch,
439    /// The KDF party identities on the wire do not match the pinned
440    /// expectation.
441    PartyMismatch,
442    /// AEAD authentication failed: wrong key, tampered ciphertext or headers,
443    /// mismatched external AAD, or a degenerate (low-order) ephemeral.
444    /// Opaque on purpose.
445    OpenFailed,
446    /// The backing provider (for example the broker unseal RPC) failed. The
447    /// message is diagnostic transport/provider detail, never key material.
448    Provider {
449        /// Human-readable provider failure detail.
450        message: String,
451    },
452}
453
454impl fmt::Display for OpenError {
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        match self {
457            Self::Decode(e) => write!(f, "strict decode failed: {e}"),
458            Self::RecipientKeyMismatch => write!(f, "message is for a different recipient key"),
459            Self::PartyMismatch => write!(f, "KDF party identities do not match expectation"),
460            Self::OpenFailed => write!(f, "open failed"),
461            Self::Provider { message } => write!(f, "open provider failed: {message}"),
462        }
463    }
464}
465
466impl core::error::Error for OpenError {
467    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
468        match self {
469            Self::Decode(e) => Some(e),
470            _ => None,
471        }
472    }
473}
474
475impl From<DecodeError> for OpenError {
476    fn from(e: DecodeError) -> Self {
477        Self::Decode(e)
478    }
479}