atd_runtime/ucan/error.rs
1//! UCAN-lite parse / verify errors.
2//!
3//! Parse-stage errors (Phase B.1) all map to the wire-level
4//! `ERR_UCAN_INVALID = 1010` constant in `atd-protocol`. Verify-stage
5//! errors (Phase B.2) split across `ERR_UCAN_INVALID` (structural /
6//! signature), `ERR_UCAN_EXPIRED` (1011), `ERR_DELEGATION_TOO_DEEP`
7//! (1012), `ERR_AUDIENCE_MISMATCH` (1013).
8//!
9//! Spec: `docs/archive/superpowers/specs/2026-05-11-sp-capability-v2-design.md` §4.1 + §5.4
10
11use thiserror::Error;
12
13/// Errors returned by [`crate::ucan::parse_jwt`].
14///
15/// All variants map to wire code `ERR_UCAN_INVALID = 1010` —
16/// `retryable: false` (deterministic; same token → same failure).
17#[derive(Debug, Clone, PartialEq, Eq, Error)]
18pub enum UcanParseError {
19 /// JWT compact form requires exactly 3 `.`-separated segments.
20 #[error("malformed JWT: expected 3 segments, got {0}")]
21 MalformedJwt(usize),
22
23 /// Header or payload base64url-decode failed.
24 #[error("base64url decode failed in {segment}: {reason}")]
25 Base64Decode {
26 segment: &'static str,
27 reason: String,
28 },
29
30 /// Header or payload JSON deserialize failed.
31 #[error("JSON parse failed in {segment}: {reason}")]
32 JsonParse {
33 segment: &'static str,
34 reason: String,
35 },
36
37 /// `header.alg != "EdDSA"`. Spec §4.3.
38 #[error("unsupported alg: expected EdDSA, got {0:?}")]
39 UnsupportedAlg(String),
40
41 /// `header.typ != "ucan/1.0+jwt"`. Spec §4.1.
42 #[error("unsupported typ: expected ucan/1.0+jwt, got {0:?}")]
43 UnsupportedTyp(String),
44
45 /// `header.ucv != "1.0"`. Spec §4.1.
46 #[error("unsupported ucv: expected 1.0, got {0:?}")]
47 UnsupportedUcv(String),
48
49 /// `payload.cmd != "atd-cap"`. Cross-system replay prevention,
50 /// spec §4.5.
51 #[error("non-atd-cap UCAN: expected cmd=\"atd-cap\", got {0:?}")]
52 NonAtdCap(String),
53
54 /// `iss` or `aud` doesn't start with `did:key:z`. Spec §4.4 —
55 /// `did:web` / `did:agent` are deferred to follow-up SPs.
56 #[error("unsupported DID method in {field}: {did:?} (only did:key:z... accepted)")]
57 UnsupportedDidMethod { field: &'static str, did: String },
58}
59
60// Convenience aliases for the parser to use `.map_err(|e| ...)`.
61impl UcanParseError {
62 pub(crate) fn base64(segment: &'static str, e: impl std::fmt::Display) -> Self {
63 Self::Base64Decode {
64 segment,
65 reason: e.to_string(),
66 }
67 }
68
69 pub(crate) fn json(segment: &'static str, e: impl std::fmt::Display) -> Self {
70 Self::JsonParse {
71 segment,
72 reason: e.to_string(),
73 }
74 }
75}
76
77/// Errors returned by [`crate::ucan::verify_jwt`] / `verify_tokens`.
78///
79/// Parse-stage failures bubble up as `Parse(_)` → wire code
80/// `ERR_UCAN_INVALID = 1010`. Other verify-stage failures map to:
81///
82/// | Variant | Wire code |
83/// |---|---|
84/// | `Parse`, `BadSignature`, `WideningAttenuation`, `MultiParentNotSupported`, `MalformedDidKey`, `MalformedSignature`, `ChainBroken` | 1010 ERR_UCAN_INVALID |
85/// | `Expired` | 1011 ERR_UCAN_EXPIRED |
86/// | `ChainTooDeep` | 1012 ERR_DELEGATION_TOO_DEEP |
87/// | `AudienceMismatch` | 1013 ERR_AUDIENCE_MISMATCH |
88/// | `Revoked` | 1010 ERR_UCAN_INVALID (with revoked-cid hint) |
89///
90/// All `retryable: false` (deterministic).
91#[derive(Debug, Error)]
92pub enum UcanVerifyError {
93 /// Underlying parse failure (any [`UcanParseError`] variant).
94 #[error("UCAN parse error: {0}")]
95 Parse(#[from] UcanParseError),
96
97 /// Ed25519 signature verification failed for the named token CID.
98 #[error("Ed25519 signature verification failed for token {cid}")]
99 BadSignature { cid: String },
100
101 /// Signature segment failed to base64url-decode or had wrong length.
102 #[error("malformed signature on token {cid}: {reason}")]
103 MalformedSignature { cid: String, reason: String },
104
105 /// `did:key:z...` failed to decode (bad multibase, wrong multicodec
106 /// prefix, or wrong key length).
107 #[error("malformed did:key in {field}: {reason}")]
108 MalformedDidKey { field: &'static str, reason: String },
109
110 /// A link's `exp <= now()`. Distinct from `Parse` because the token
111 /// was well-formed — it just lapsed.
112 #[error("UCAN expired at link {cid}: exp={exp}, now={now}")]
113 Expired { cid: String, exp: i64, now: i64 },
114
115 /// Child claims caps the parent did not grant.
116 #[error("attenuation widening at link {cid}: parent={parent:?}, child={child:?}")]
117 WideningAttenuation {
118 cid: String,
119 parent: Vec<String>,
120 child: Vec<String>,
121 },
122
123 /// Link N's `iss` doesn't match link N-1's `aud`. The chain is broken.
124 #[error(
125 "chain broken between {parent_cid} (aud={parent_aud}) and {child_cid} (iss={child_iss})"
126 )]
127 ChainBroken {
128 parent_cid: String,
129 parent_aud: String,
130 child_cid: String,
131 child_iss: String,
132 },
133
134 /// The leaf's `aud` doesn't match the configured expected audience.
135 #[error("audience mismatch: leaf.aud={leaf_aud}, expected={expected}")]
136 AudienceMismatch { leaf_aud: String, expected: String },
137
138 /// The chain exceeds the configured `max_chain_depth`.
139 #[error("chain too deep: depth={depth}, max={max}")]
140 ChainTooDeep { depth: u8, max: u8 },
141
142 /// A link's CID was in the revocation store.
143 #[error("UCAN revoked: cid={cid}")]
144 Revoked { cid: String },
145
146 /// `prf` field has more than one parent. UCAN v1.0 spec allows
147 /// multi-parent; SP-capability-v2 v1 supports single-chain only.
148 #[error("multi-parent UCAN not supported in v1 (link {cid} has {n_parents} parents)")]
149 MultiParentNotSupported { cid: String, n_parents: usize },
150}
151
152/// Wire-code mapping per [`UcanVerifyError`] variant.
153///
154/// Returns `(code, retryable)` for use by dispatch when converting a
155/// verify error into a `Response::Error`. All verify errors are
156/// non-retryable (deterministic on the same input).
157pub fn wire_code(err: &UcanVerifyError) -> u16 {
158 use atd_protocol::{
159 ERR_AUDIENCE_MISMATCH, ERR_DELEGATION_TOO_DEEP, ERR_UCAN_EXPIRED, ERR_UCAN_INVALID,
160 };
161 match err {
162 UcanVerifyError::Expired { .. } => ERR_UCAN_EXPIRED,
163 UcanVerifyError::ChainTooDeep { .. } => ERR_DELEGATION_TOO_DEEP,
164 UcanVerifyError::AudienceMismatch { .. } => ERR_AUDIENCE_MISMATCH,
165 _ => ERR_UCAN_INVALID,
166 }
167}