Skip to main content

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}