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
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}