aitp_core/error.rs
1//! AITP error codes from the registry.
2//!
3//! See `agentidentitytrustprotocol/registries/error-codes.md` for the
4//! authoritative list.
5
6use serde::{Deserialize, Serialize};
7
8/// Top-level error type for AITP operations.
9///
10/// Each variant maps to one or more wire-level [`ErrorCode`] values.
11/// Protocol-specific crates have their own narrower error types
12/// (e.g. `TctError`, `ManifestError`) that flatten into this with
13/// `From` impls.
14///
15/// Marked `#[non_exhaustive]` so future error categories can be added
16/// without a semver-major bump. Downstream matches must include a
17/// fall-through arm.
18#[derive(Debug, thiserror::Error)]
19#[non_exhaustive]
20pub enum AitpError {
21 /// Replay-protection or envelope-level rejection.
22 #[error("envelope rejected: {0}")]
23 Envelope(String),
24
25 /// Identity proof was invalid (OIDC, pinned key, etc.).
26 #[error("identity verification failed: {0}")]
27 Identity(String),
28
29 /// Manifest-level error (signature, PoP, expiry).
30 #[error("manifest error: {0}")]
31 Manifest(String),
32
33 /// TCT-level error (signature, audience, expiry, grants).
34 #[error("TCT error: {0}")]
35 Tct(String),
36
37 /// Delegation-token error.
38 #[error("delegation error: {0}")]
39 Delegation(String),
40
41 /// Cryptographic failure (signature, key parsing).
42 #[error("crypto error: {0}")]
43 Crypto(String),
44
45 /// Other / catch-all.
46 #[error("AITP error: {0}")]
47 Other(String),
48}
49
50/// Wire-level error code as it appears on the protocol.
51///
52/// Serialized as `SCREAMING_SNAKE_CASE` strings matching the registry.
53///
54/// Marked `#[non_exhaustive]` so new codes added to the spec's error
55/// registry can ship in a future minor version without breaking
56/// downstream `match` statements. Downstream matches must include a
57/// fall-through arm.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
60#[non_exhaustive]
61pub enum ErrorCode {
62 // ── Envelope-level ──────────────────────────────────────────────────
63 /// Envelope JSON failed schema validation.
64 InvalidEnvelope,
65 /// Envelope signature did not verify.
66 InvalidSignature,
67 /// Duplicate message_id seen.
68 ReplayDetected,
69 /// Timestamp outside ±300s tolerance.
70 TimestampExpired,
71 /// Protocol version not supported.
72 UnknownVersion,
73 /// Compact-JWS header `alg` is not the sole value derived from the
74 /// signer's AID — including `none` in any capitalization and unknown
75 /// algorithms (RFC-AITP-0001 §5.4.5).
76 TokenAlgMismatch,
77 /// Compact-JWS header `typ` does not exactly match the value expected
78 /// for the verification context (`aitp-tct+jwt`, `aitp-grant+jwt`, or
79 /// `aitp-delegation+jwt`) (RFC-AITP-0001 §5.4.5).
80 TokenTypMismatch,
81
82 // ── Identity / Manifest ─────────────────────────────────────────────
83 /// Identity binding could not be verified.
84 IdentityFailed,
85 /// Manifest expires_at is in the past.
86 ManifestExpired,
87 /// Manifest signature did not verify.
88 ManifestSignatureInvalid,
89 /// Manifest proof-of-possession did not verify.
90 ManifestPopFailed,
91 /// Manifest version not supported by this implementation.
92 ManifestVersionUnknown,
93
94 // ── Trust ───────────────────────────────────────────────────────────
95 /// Trust evaluation failed for an unspecified policy reason.
96 TrustFailed,
97 /// Requested capability not granted.
98 PolicyViolation,
99 /// Issuer's keys could not be resolved.
100 KeyResolutionFailed,
101 /// Peer's identity issuer is not in this peer's trust anchors.
102 IncompatibleTrustAnchors,
103
104 // ── Mutual handshake ────────────────────────────────────────────────
105 /// PoP signature in MUTUAL_COMMIT/_ACK did not verify.
106 PopVerificationFailed,
107 /// pop_nonce_echo did not match the previously sent nonce.
108 NonceMismatch,
109 /// Peer-issued TCT audience did not equal own AID.
110 AudienceMismatch,
111 /// Peer-issued TCT grants exceed peer's offered_capabilities.
112 GrantOverflow,
113 /// Received TCT did not include required peer capabilities.
114 InsufficientGrants,
115 /// Proposed handshake_mode not supported.
116 HandshakeModeUnsupported,
117
118 // ── TCT-specific ────────────────────────────────────────────────────
119 /// TCT expires_at is in the past.
120 TctExpired,
121
122 // ── PoP ─────────────────────────────────────────────────────────────
123 /// Downstream PoP challenge was malformed or stale.
124 PopChallengeInvalid,
125 /// Downstream PoP response did not verify.
126 PopResponseInvalid,
127
128 // ── Delegation ──────────────────────────────────────────────────────
129 /// Delegation token: audience did not match self AID.
130 DelegationAudienceMismatch,
131 /// Delegation token: scope contained capabilities outside grant_proof.
132 DelegationScopeExceeded,
133 /// Delegation token: embedded voucher JWS signature invalid,
134 /// `voucher.iss` ≠ verifier's AID, or `voucher.sub` ≠ outer `iss`.
135 /// Renamed in v0.2 from `DELEGATION_INVALID_GRANT_PROOF` (the
136 /// `grant_proof` reconstruction mechanism was removed by the JWS
137 /// migration).
138 DelegationInvalidVoucher,
139 /// Delegation token: source TCT has been revoked.
140 DelegationSourceTctRevoked,
141 /// Delegation token: signature did not verify.
142 DelegationInvalidSignature,
143 /// Delegation token: token or grant proof has expired.
144 DelegationExpired,
145 /// Delegation token: PoP binding (cnf) verification failed.
146 DelegationPopFailed,
147 /// Delegation token: chain length exceeds v0.1 single-hop limit.
148 DelegationMultihopNotSupported,
149 /// Multi-hop delegation: chain length exceeds `max_delegation_hops`
150 /// (RFC-AITP-0011).
151 DelegationHopLimitExceeded,
152 /// Multi-hop delegation: `chain_hash` does not match the `chain`
153 /// array contents (truncation or tampering detected — RFC-AITP-0011).
154 DelegationChainHashMismatch,
155 /// Manifest service: no manifest for the requested AID.
156 ManifestNotFound,
157 /// TCT verification: signature did not validate under issuer's key.
158 TctSignatureInvalid,
159 /// TCT verification: jti is in issuer's deny list.
160 TctRevoked,
161 /// TCT verification: TCT `expires_at` exceeds the issuing peer's
162 /// Manifest `expires_at` (RFC-AITP-0004 §4.3).
163 TctExpiresAfterManifest,
164
165 // ── Session Bundle (RFC-AITP-0010, Draft) ───────────────────────────
166 /// Coordinator's outer bundle signature failed verification under
167 /// the coordinator's Manifest key.
168 BundleInvalidSignature,
169 /// `version` is not `"aitp/0.2"` (or a later supported version).
170 BundleVersionMismatch,
171 /// Bundle `expires_at` is in the past at verification time.
172 BundleExpired,
173 /// `expires_at` is greater than
174 /// `min(participants[*].tct.expires_at)` (RFC-AITP-0010 §6).
175 BundleExpiryWindowInvariant,
176 /// One or more `participants[*].tct.issuer` values do not equal
177 /// `coordinator`.
178 BundleCoordinatorIssuerMismatch,
179 /// A `participants[i].tct.audience` does not equal
180 /// `participants[i].aid`.
181 BundleAudienceMismatch,
182 /// `participants` array is empty.
183 BundleEmptyParticipants,
184 /// At least one embedded participant TCT failed standard TCT
185 /// verification.
186 BundleParticipantTctInvalid,
187 /// Receiver's AID is not in `participants[*].aid`.
188 BundleNotMember,
189 /// Aggregate fallback — implementations MAY return this when a
190 /// deployment policy requires a single-error surface for bundles,
191 /// in lieu of the specific BUNDLE_* codes above.
192 SessionBundleInvalid,
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 /// Pinned wire strings — the contract with the AITP error-code registry.
200 /// Drift here would silently break interop with other implementations.
201 #[test]
202 fn pinned_wire_strings() {
203 let cases: &[(ErrorCode, &str)] = &[
204 (ErrorCode::AudienceMismatch, "AUDIENCE_MISMATCH"),
205 (ErrorCode::TctExpired, "TCT_EXPIRED"),
206 (
207 ErrorCode::ManifestSignatureInvalid,
208 "MANIFEST_SIGNATURE_INVALID",
209 ),
210 (ErrorCode::ReplayDetected, "REPLAY_DETECTED"),
211 (
212 ErrorCode::DelegationSourceTctRevoked,
213 "DELEGATION_SOURCE_TCT_REVOKED",
214 ),
215 (ErrorCode::InvalidEnvelope, "INVALID_ENVELOPE"),
216 (ErrorCode::InvalidSignature, "INVALID_SIGNATURE"),
217 (ErrorCode::TimestampExpired, "TIMESTAMP_EXPIRED"),
218 (ErrorCode::UnknownVersion, "UNKNOWN_VERSION"),
219 (ErrorCode::TokenAlgMismatch, "TOKEN_ALG_MISMATCH"),
220 (ErrorCode::TokenTypMismatch, "TOKEN_TYP_MISMATCH"),
221 (ErrorCode::IdentityFailed, "IDENTITY_FAILED"),
222 (ErrorCode::PolicyViolation, "POLICY_VIOLATION"),
223 (ErrorCode::GrantOverflow, "GRANT_OVERFLOW"),
224 (ErrorCode::InsufficientGrants, "INSUFFICIENT_GRANTS"),
225 (ErrorCode::KeyResolutionFailed, "KEY_RESOLUTION_FAILED"),
226 (ErrorCode::ManifestExpired, "MANIFEST_EXPIRED"),
227 (ErrorCode::ManifestPopFailed, "MANIFEST_POP_FAILED"),
228 (
229 ErrorCode::ManifestVersionUnknown,
230 "MANIFEST_VERSION_UNKNOWN",
231 ),
232 (
233 ErrorCode::IncompatibleTrustAnchors,
234 "INCOMPATIBLE_TRUST_ANCHORS",
235 ),
236 (ErrorCode::PopVerificationFailed, "POP_VERIFICATION_FAILED"),
237 (ErrorCode::NonceMismatch, "NONCE_MISMATCH"),
238 (ErrorCode::PopChallengeInvalid, "POP_CHALLENGE_INVALID"),
239 (ErrorCode::PopResponseInvalid, "POP_RESPONSE_INVALID"),
240 (
241 ErrorCode::DelegationAudienceMismatch,
242 "DELEGATION_AUDIENCE_MISMATCH",
243 ),
244 (
245 ErrorCode::DelegationScopeExceeded,
246 "DELEGATION_SCOPE_EXCEEDED",
247 ),
248 (
249 ErrorCode::DelegationInvalidVoucher,
250 "DELEGATION_INVALID_VOUCHER",
251 ),
252 (
253 ErrorCode::DelegationInvalidSignature,
254 "DELEGATION_INVALID_SIGNATURE",
255 ),
256 (ErrorCode::DelegationExpired, "DELEGATION_EXPIRED"),
257 (ErrorCode::DelegationPopFailed, "DELEGATION_POP_FAILED"),
258 (
259 ErrorCode::DelegationMultihopNotSupported,
260 "DELEGATION_MULTIHOP_NOT_SUPPORTED",
261 ),
262 (
263 ErrorCode::DelegationHopLimitExceeded,
264 "DELEGATION_HOP_LIMIT_EXCEEDED",
265 ),
266 (
267 ErrorCode::DelegationChainHashMismatch,
268 "DELEGATION_CHAIN_HASH_MISMATCH",
269 ),
270 (ErrorCode::ManifestNotFound, "MANIFEST_NOT_FOUND"),
271 (ErrorCode::TrustFailed, "TRUST_FAILED"),
272 (
273 ErrorCode::HandshakeModeUnsupported,
274 "HANDSHAKE_MODE_UNSUPPORTED",
275 ),
276 (ErrorCode::TctSignatureInvalid, "TCT_SIGNATURE_INVALID"),
277 (ErrorCode::TctRevoked, "TCT_REVOKED"),
278 (
279 ErrorCode::TctExpiresAfterManifest,
280 "TCT_EXPIRES_AFTER_MANIFEST",
281 ),
282 // Session Bundle (RFC-AITP-0010, Draft)
283 (
284 ErrorCode::BundleInvalidSignature,
285 "BUNDLE_INVALID_SIGNATURE",
286 ),
287 (ErrorCode::BundleVersionMismatch, "BUNDLE_VERSION_MISMATCH"),
288 (ErrorCode::BundleExpired, "BUNDLE_EXPIRED"),
289 (
290 ErrorCode::BundleExpiryWindowInvariant,
291 "BUNDLE_EXPIRY_WINDOW_INVARIANT",
292 ),
293 (
294 ErrorCode::BundleCoordinatorIssuerMismatch,
295 "BUNDLE_COORDINATOR_ISSUER_MISMATCH",
296 ),
297 (
298 ErrorCode::BundleAudienceMismatch,
299 "BUNDLE_AUDIENCE_MISMATCH",
300 ),
301 (
302 ErrorCode::BundleEmptyParticipants,
303 "BUNDLE_EMPTY_PARTICIPANTS",
304 ),
305 (
306 ErrorCode::BundleParticipantTctInvalid,
307 "BUNDLE_PARTICIPANT_TCT_INVALID",
308 ),
309 (ErrorCode::BundleNotMember, "BUNDLE_NOT_MEMBER"),
310 (ErrorCode::SessionBundleInvalid, "SESSION_BUNDLE_INVALID"),
311 ];
312 for (code, wire) in cases {
313 let v = serde_json::to_value(code).unwrap();
314 assert_eq!(v.as_str().unwrap(), *wire, "encode {:?}", code);
315 let back: ErrorCode = serde_json::from_value(v).unwrap();
316 assert_eq!(back, *code, "decode {}", wire);
317 }
318 }
319
320 #[test]
321 fn round_trip_through_json_string() {
322 let s = serde_json::to_string(&ErrorCode::PopVerificationFailed).unwrap();
323 assert_eq!(s, "\"POP_VERIFICATION_FAILED\"");
324 let back: ErrorCode = serde_json::from_str(&s).unwrap();
325 assert_eq!(back, ErrorCode::PopVerificationFailed);
326 }
327
328 #[test]
329 fn rejects_unknown_wire_strings() {
330 assert!(serde_json::from_str::<ErrorCode>("\"NOT_A_REAL_CODE\"").is_err());
331 assert!(serde_json::from_str::<ErrorCode>("\"audience_mismatch\"").is_err());
332 }
333}