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
74 // ── Identity / Manifest ─────────────────────────────────────────────
75 /// Identity binding could not be verified.
76 IdentityFailed,
77 /// Manifest expires_at is in the past.
78 ManifestExpired,
79 /// Manifest signature did not verify.
80 ManifestSignatureInvalid,
81 /// Manifest proof-of-possession did not verify.
82 ManifestPopFailed,
83 /// Manifest version not supported by this implementation.
84 ManifestVersionUnknown,
85
86 // ── Trust ───────────────────────────────────────────────────────────
87 /// Trust evaluation failed for an unspecified policy reason.
88 TrustFailed,
89 /// Requested capability not granted.
90 PolicyViolation,
91 /// Issuer's keys could not be resolved.
92 KeyResolutionFailed,
93 /// Peer's identity issuer is not in this peer's trust anchors.
94 IncompatibleTrustAnchors,
95
96 // ── Mutual handshake ────────────────────────────────────────────────
97 /// PoP signature in MUTUAL_COMMIT/_ACK did not verify.
98 PopVerificationFailed,
99 /// pop_nonce_echo did not match the previously sent nonce.
100 NonceMismatch,
101 /// Peer-issued TCT audience did not equal own AID.
102 AudienceMismatch,
103 /// Peer-issued TCT grants exceed peer's offered_capabilities.
104 GrantOverflow,
105 /// Received TCT did not include required peer capabilities.
106 InsufficientGrants,
107 /// Proposed handshake_mode not supported.
108 HandshakeModeUnsupported,
109
110 // ── TCT-specific ────────────────────────────────────────────────────
111 /// TCT expires_at is in the past.
112 TctExpired,
113
114 // ── PoP ─────────────────────────────────────────────────────────────
115 /// Downstream PoP challenge was malformed or stale.
116 PopChallengeInvalid,
117 /// Downstream PoP response did not verify.
118 PopResponseInvalid,
119
120 // ── Delegation ──────────────────────────────────────────────────────
121 /// Delegation token: audience did not match self AID.
122 DelegationAudienceMismatch,
123 /// Delegation token: scope contained capabilities outside grant_proof.
124 DelegationScopeExceeded,
125 /// Delegation token: grant_proof signature or subject binding invalid.
126 DelegationInvalidGrantProof,
127 /// Delegation token: source TCT has been revoked.
128 DelegationSourceTctRevoked,
129 /// Delegation token: signature did not verify.
130 DelegationInvalidSignature,
131 /// Delegation token: token or grant proof has expired.
132 DelegationExpired,
133 /// Delegation token: PoP binding (cnf) verification failed.
134 DelegationPopFailed,
135 /// Delegation token: chain length exceeds v0.1 single-hop limit.
136 DelegationMultihopNotSupported,
137 /// Multi-hop delegation: chain length exceeds `max_delegation_hops`
138 /// (RFC-AITP-0011).
139 DelegationHopLimitExceeded,
140 /// Multi-hop delegation: `chain_hash` does not match the `chain`
141 /// array contents (truncation or tampering detected — RFC-AITP-0011).
142 DelegationChainHashMismatch,
143 /// Manifest service: no manifest for the requested AID.
144 ManifestNotFound,
145 /// TCT verification: signature did not validate under issuer's key.
146 TctSignatureInvalid,
147 /// TCT verification: jti is in issuer's deny list.
148 TctRevoked,
149 /// TCT verification: TCT `expires_at` exceeds the issuing peer's
150 /// Manifest `expires_at` (RFC-AITP-0004 §4.3).
151 TctExpiresAfterManifest,
152
153 // ── Session Bundle (RFC-AITP-0010, Draft) ───────────────────────────
154 /// Coordinator's outer bundle signature failed verification under
155 /// the coordinator's Manifest key.
156 BundleInvalidSignature,
157 /// `version` is not `"aitp/0.1"` (or a later supported version).
158 BundleVersionMismatch,
159 /// Bundle `expires_at` is in the past at verification time.
160 BundleExpired,
161 /// `expires_at` is greater than
162 /// `min(participants[*].tct.expires_at)` (RFC-AITP-0010 §6).
163 BundleExpiryWindowInvariant,
164 /// One or more `participants[*].tct.issuer` values do not equal
165 /// `coordinator`.
166 BundleCoordinatorIssuerMismatch,
167 /// A `participants[i].tct.audience` does not equal
168 /// `participants[i].aid`.
169 BundleAudienceMismatch,
170 /// `participants` array is empty.
171 BundleEmptyParticipants,
172 /// At least one embedded participant TCT failed standard TCT
173 /// verification.
174 BundleParticipantTctInvalid,
175 /// Receiver's AID is not in `participants[*].aid`.
176 BundleNotMember,
177 /// Aggregate fallback — implementations MAY return this when a
178 /// deployment policy requires a single-error surface for bundles,
179 /// in lieu of the specific BUNDLE_* codes above.
180 SessionBundleInvalid,
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 /// Pinned wire strings — the contract with the AITP error-code registry.
188 /// Drift here would silently break interop with other implementations.
189 #[test]
190 fn pinned_wire_strings() {
191 let cases: &[(ErrorCode, &str)] = &[
192 (ErrorCode::AudienceMismatch, "AUDIENCE_MISMATCH"),
193 (ErrorCode::TctExpired, "TCT_EXPIRED"),
194 (
195 ErrorCode::ManifestSignatureInvalid,
196 "MANIFEST_SIGNATURE_INVALID",
197 ),
198 (ErrorCode::ReplayDetected, "REPLAY_DETECTED"),
199 (
200 ErrorCode::DelegationSourceTctRevoked,
201 "DELEGATION_SOURCE_TCT_REVOKED",
202 ),
203 (ErrorCode::InvalidEnvelope, "INVALID_ENVELOPE"),
204 (ErrorCode::InvalidSignature, "INVALID_SIGNATURE"),
205 (ErrorCode::TimestampExpired, "TIMESTAMP_EXPIRED"),
206 (ErrorCode::UnknownVersion, "UNKNOWN_VERSION"),
207 (ErrorCode::IdentityFailed, "IDENTITY_FAILED"),
208 (ErrorCode::PolicyViolation, "POLICY_VIOLATION"),
209 (ErrorCode::GrantOverflow, "GRANT_OVERFLOW"),
210 (ErrorCode::InsufficientGrants, "INSUFFICIENT_GRANTS"),
211 (ErrorCode::KeyResolutionFailed, "KEY_RESOLUTION_FAILED"),
212 (ErrorCode::ManifestExpired, "MANIFEST_EXPIRED"),
213 (ErrorCode::ManifestPopFailed, "MANIFEST_POP_FAILED"),
214 (
215 ErrorCode::ManifestVersionUnknown,
216 "MANIFEST_VERSION_UNKNOWN",
217 ),
218 (
219 ErrorCode::IncompatibleTrustAnchors,
220 "INCOMPATIBLE_TRUST_ANCHORS",
221 ),
222 (ErrorCode::PopVerificationFailed, "POP_VERIFICATION_FAILED"),
223 (ErrorCode::NonceMismatch, "NONCE_MISMATCH"),
224 (ErrorCode::PopChallengeInvalid, "POP_CHALLENGE_INVALID"),
225 (ErrorCode::PopResponseInvalid, "POP_RESPONSE_INVALID"),
226 (
227 ErrorCode::DelegationAudienceMismatch,
228 "DELEGATION_AUDIENCE_MISMATCH",
229 ),
230 (
231 ErrorCode::DelegationScopeExceeded,
232 "DELEGATION_SCOPE_EXCEEDED",
233 ),
234 (
235 ErrorCode::DelegationInvalidGrantProof,
236 "DELEGATION_INVALID_GRANT_PROOF",
237 ),
238 (
239 ErrorCode::DelegationInvalidSignature,
240 "DELEGATION_INVALID_SIGNATURE",
241 ),
242 (ErrorCode::DelegationExpired, "DELEGATION_EXPIRED"),
243 (ErrorCode::DelegationPopFailed, "DELEGATION_POP_FAILED"),
244 (
245 ErrorCode::DelegationMultihopNotSupported,
246 "DELEGATION_MULTIHOP_NOT_SUPPORTED",
247 ),
248 (
249 ErrorCode::DelegationHopLimitExceeded,
250 "DELEGATION_HOP_LIMIT_EXCEEDED",
251 ),
252 (
253 ErrorCode::DelegationChainHashMismatch,
254 "DELEGATION_CHAIN_HASH_MISMATCH",
255 ),
256 (ErrorCode::ManifestNotFound, "MANIFEST_NOT_FOUND"),
257 (ErrorCode::TctSignatureInvalid, "TCT_SIGNATURE_INVALID"),
258 (ErrorCode::TctRevoked, "TCT_REVOKED"),
259 (
260 ErrorCode::TctExpiresAfterManifest,
261 "TCT_EXPIRES_AFTER_MANIFEST",
262 ),
263 // Session Bundle (RFC-AITP-0010, Draft)
264 (
265 ErrorCode::BundleInvalidSignature,
266 "BUNDLE_INVALID_SIGNATURE",
267 ),
268 (ErrorCode::BundleVersionMismatch, "BUNDLE_VERSION_MISMATCH"),
269 (ErrorCode::BundleExpired, "BUNDLE_EXPIRED"),
270 (
271 ErrorCode::BundleExpiryWindowInvariant,
272 "BUNDLE_EXPIRY_WINDOW_INVARIANT",
273 ),
274 (
275 ErrorCode::BundleCoordinatorIssuerMismatch,
276 "BUNDLE_COORDINATOR_ISSUER_MISMATCH",
277 ),
278 (
279 ErrorCode::BundleAudienceMismatch,
280 "BUNDLE_AUDIENCE_MISMATCH",
281 ),
282 (
283 ErrorCode::BundleEmptyParticipants,
284 "BUNDLE_EMPTY_PARTICIPANTS",
285 ),
286 (
287 ErrorCode::BundleParticipantTctInvalid,
288 "BUNDLE_PARTICIPANT_TCT_INVALID",
289 ),
290 (ErrorCode::BundleNotMember, "BUNDLE_NOT_MEMBER"),
291 (ErrorCode::SessionBundleInvalid, "SESSION_BUNDLE_INVALID"),
292 ];
293 for (code, wire) in cases {
294 let v = serde_json::to_value(code).unwrap();
295 assert_eq!(v.as_str().unwrap(), *wire, "encode {:?}", code);
296 let back: ErrorCode = serde_json::from_value(v).unwrap();
297 assert_eq!(back, *code, "decode {}", wire);
298 }
299 }
300
301 #[test]
302 fn round_trip_through_json_string() {
303 let s = serde_json::to_string(&ErrorCode::PopVerificationFailed).unwrap();
304 assert_eq!(s, "\"POP_VERIFICATION_FAILED\"");
305 let back: ErrorCode = serde_json::from_str(&s).unwrap();
306 assert_eq!(back, ErrorCode::PopVerificationFailed);
307 }
308
309 #[test]
310 fn rejects_unknown_wire_strings() {
311 assert!(serde_json::from_str::<ErrorCode>("\"NOT_A_REAL_CODE\"").is_err());
312 assert!(serde_json::from_str::<ErrorCode>("\"audience_mismatch\"").is_err());
313 }
314}