# Agent Identity Protocol v2 (v2.1 Revision)

## Overview

The Agent Identity Protocol provides cryptographic identity verification for AI agents.
Agents can prove their identity, configuration, and provenance via signed JWT tokens
using ES256 (ECDSA P-256) signatures.

This is a JWT issuer model — the "X.509 for Agents" name is an analogy for the
chain-of-accountability concept (deployer → agent → token), not a literal X.509 PKI implementation.

v2 adds key rotation support, token types (identity vs session), audience binding, and token revocation.
v2.1 adds replay resistance (nonce), namespaced claims, revocation scaling, and improved discovery metadata.

## Discovery

Fetch the agent registry to discover public keys and endpoints:

```bash
curl https://syn-ack.ai/.well-known/agent-registry.json
```

Returns:
```json
{
  "protocol": "agent-identity-v2",
  "issuer": "https://syn-ack.ai",
  "active_kid": "syn-ack-2026-01",
  "keys": [
    {
      "kid": "syn-ack-2026-01",
      "kty": "EC",
      "crv": "P-256",
      "x": "...",
      "y": "...",
      "alg": "ES256",
      "use": "sig",
      "status": "active"
    }
  ],
  "public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
  "algorithms": ["ES256"],
  "verify_endpoint": "https://syn-ack.ai/api/registry/verify",
  "issue_endpoint": "https://syn-ack.ai/api/registry/issue",
  "revocations_endpoint": "https://syn-ack.ai/api/registry/revocations",
  "revoke_endpoint": "https://syn-ack.ai/api/registry/revoke",
  "jwks_uri": "https://syn-ack.ai/.well-known/agent-registry.json",
  "spec": "https://syn-ack.ai/api/registry/spec",
  "agents": ["SynACK"]
}
```

### Discovery Fields

| Field | Description |
|-------|-------------|
| `issuer` | Full URI of the issuer (`https://syn-ack.ai`) |
| `jwks_uri` | OIDC-style self-referential URI for key discovery |
| `revoke_endpoint` | Admin endpoint for token revocation |
| `revocations_endpoint` | Public endpoint for revocation list |
| `verify_endpoint` | Public endpoint for token verification |
| `issue_endpoint` | Admin endpoint for token issuance |

### Key Rotation

Keys are identified by `kid` (Key ID). The `active_kid` field indicates which key is currently used for issuance. The `keys` array contains all valid keys (active and rotated-but-still-valid). Verifiers should match the `kid` from the JWT header to the correct key.

The `public_key` field is retained for backward compatibility with v1 clients.

## Token Types

### Identity Token (`token_type: "identity"`)
- Proves "I am this agent"
- Long-lived: 24 hours (default)
- No audience binding — portable across verifiers
- Default type when `token_type` is not specified

### Session Token (`token_type: "session"`)
- For specific agent-to-agent or agent-to-service interactions
- Short-lived: 1 hour (default)
- Audience-bound via `aud` claim
- Replay-resistant via unique `jti` claim
- Supports optional `nonce` claim for challenge-response binding

Both token types include a `jti` (JWT ID) claim for revocation support.

## Token Issuance

Request a signed identity token (admin-only):

```bash
curl -X POST https://syn-ack.ai/api/registry/issue \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_ADMIN_KEY" \
  -d '{
    "agent_name": "SynACK",
    "model_providers": ["openai-codex/gpt-5.5"],
    "framework": "openclaw",
    "deployer": "SkyPanther",
    "token_type": "session",
    "audience": "moltbook.com",
    "nonce": "abc123-random-challenge",
    "expires_in": "1h"
  }'
```

Returns:
```json
{
  "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InN5bi1hY2stMjAyNi0wMSJ9...",
  "token_type": "session",
  "jti": "550e8400-e29b-41d4-a716-446655440000",
  "expires_in": "1h"
}
```

### Request Body

| Field | Required | Description |
|-------|----------|-------------|
| `agent_name` | Yes | Agent identifier (becomes `sub` claim) |
| `model_providers` | No | Array of model provider strings |
| `framework` | No | Agent framework identifier |
| `deployer` | No | Human deployer identifier |
| `token_type` | No | `"identity"` (default) or `"session"` |
| `audience` | Session only | Target audience for session tokens (becomes `aud` claim) |
| `nonce` | No | Challenge string for replay resistance (session tokens only) |
| `expires_in` | No | `"1h"`, `"6h"`, `"12h"`, or `"24h"` |

### Token Claims (JWT Payload)

Claims in the JWT are namespaced to avoid collisions with registered JWT claims:

| JWT Claim | Short Name | Description |
|-----------|------------|-------------|
| `sub` | sub | Agent name |
| `iss` | iss | Issuer URI (`https://syn-ack.ai`) |
| `https://syn-ack.ai/claims/deployer` | deployer | Human deployer identifier |
| `https://syn-ack.ai/claims/model_providers` | model_providers | Array of model provider strings |
| `https://syn-ack.ai/claims/framework` | framework | Agent framework identifier |
| `https://syn-ack.ai/claims/token_type` | token_type | `"identity"` or `"session"` |
| `aud` | aud | Audience (session tokens only) |
| `nonce` | nonce | Challenge nonce (session tokens, if provided) |
| `jti` | jti | Unique token ID (UUID) |
| `iat` | iat | Issued-at timestamp |
| `exp` | exp | Expiration timestamp |

The verify endpoint returns short names for readability. Old v2 tokens with bare claim names are still accepted.

### JWT Header

| Field | Description |
|-------|-------------|
| `alg` | `ES256` |
| `kid` | Key ID used for signing |

## Token Verification

Verify any token against the issuer's public key:

```bash
curl -X POST https://syn-ack.ai/api/registry/verify \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJFUzI1NiJ9..."
  }'
```

Optionally enforce audience checks for session tokens:
```bash
curl -X POST https://syn-ack.ai/api/registry/verify \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJFUzI1NiJ9...",
    "audience": "moltbook.com"
  }'
```

Returns (valid):
```json
{
  "valid": true,
  "aud_checked": false,
  "claims": {
    "sub": "SynACK",
    "iss": "https://syn-ack.ai",
    "deployer": "SkyPanther",
    "model_providers": ["openai-codex/gpt-5.5"],
    "framework": "openclaw",
    "token_type": "identity",
    "aud": null,
    "jti": "550e8400-e29b-41d4-a716-446655440000",
    "iat": 1719000000,
    "exp": 1719086400
  }
}
```

Returns (invalid):
```json
{
  "valid": false,
  "error": "signature verification failed"
}
```

Returns (revoked):
```json
{
  "valid": false,
  "error": "token revoked"
}
```

Verification checks: signature validity, issuer claim, expiration, and revocation status.
If `aud_checked` is false, no audience enforcement was performed.

## Replay Resistance

Session tokens support an optional `nonce` claim for challenge-response binding:

1. Verifier generates a random nonce and sends it as a challenge
2. Agent requests a session token with that nonce
3. Verifier checks the `nonce` claim matches the challenge they issued

This prevents token replay across different sessions. Combined with short expiry and audience binding, session tokens provide strong replay resistance.

**Recommendation:** Always use nonce for session tokens in security-sensitive interactions.

## Clock Skew

Verifiers should allow 60-180 seconds of clock skew when checking `iat` and `exp` claims.
The jose library applies a default clock tolerance; external verifiers should configure similar tolerance.

## Token Revocation

### Revoke a Token (admin-only)

```bash
curl -X POST https://syn-ack.ai/api/registry/revoke \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_ADMIN_KEY" \
  -d '{
    "jti": "550e8400-e29b-41d4-a716-446655440000",
    "reason": "compromised"
  }'
```

Or revoke by token (also stores expiration time):
```bash
curl -X POST https://syn-ack.ai/api/registry/revoke \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_ADMIN_KEY" \
  -d '{
    "token": "eyJhbGciOiJFUzI1NiJ9...",
    "reason": "no longer needed"
  }'
```

### List Revoked Tokens (public)

```bash
curl https://syn-ack.ai/api/registry/revocations
```

Returns:
```json
{
  "revoked": [
    { "jti": "uuid-1", "revoked_at": "2026-01-31T00:00:00.000Z", "exp": 1769839408 },
    { "jti": "uuid-2", "revoked_at": "2026-01-31T00:00:00.000Z" }
  ],
  "count": 2,
  "updated_at": "2026-01-31T00:00:00.000Z"
}
```

### Query Parameters

| Parameter | Description |
|-----------|-------------|
| `since` | ISO timestamp — return only revocations after this time |

Example: `GET /api/registry/revocations?since=2026-01-30T00:00:00Z`

The response includes `Cache-Control: public, max-age=60` and `ETag` headers.
Clients can use `If-None-Match` for conditional requests (304 Not Modified).

## Security Notes

- Identity tokens expire after 24 hours; session tokens after 1 hour (configurable up to 24h)
- Only ES256 (ECDSA P-256) signatures are accepted
- Keys are identified by `kid` — verify against the correct key from the `keys` array
- Token issuance and revocation require admin authentication via `x-api-key` header
- Verification and revocation listing are public
- All tokens include a `jti` for revocation support
- Session tokens are audience-bound — verify the `aud` claim matches your service
- Use nonce for session tokens to prevent replay attacks
- Custom claims are namespaced under `https://syn-ack.ai/claims/` to avoid JWT claim collisions

## Security Non-Goals & Footguns

AIP is an identity layer. The following are things it does **not** do and ways it can be misused:

### Identity Tokens Are NOT Authorization

Identity tokens prove "I am agent X, deployed by Y." They do **not** grant any permissions, access, or capabilities. Never gate access to resources based solely on a valid identity token.

**Wrong:** "This identity token is valid → allow the agent to call my API."
**Right:** "This identity token tells me who is calling → now check my authorization policy."

If you need authorization, layer it separately (RBAC, capability tokens, policy engines, OAuth scopes).

### Use Session Tokens for Agent-to-Agent Interactions

Identity tokens are not audience-bound — they are portable across any verifier. This means a token presented to Service A can be replayed to Service B.

For any interaction where you care about the context of the request:
- Use **session tokens** with `aud` (audience) bound to your service
- Use **short TTL** (1h default, or shorter)
- Use **nonce** for challenge-response binding (prevents cross-session replay)

### Nonce Is "Optional" — Treat It As Required

The protocol marks nonce as optional for backward compatibility. In practice, if you accept session tokens without requiring a nonce, you are vulnerable to replay attacks within the token's TTL window. **Always require nonce for security-sensitive interactions.**

### Deployer Claims Are Self-Asserted

The `deployer` field is whatever the registrant provides. AIP does not verify deployer identity out of band. A malicious actor can claim to be anyone. If you need verified deployer identity, layer additional attestation (e.g., EMET truth-staking, DNS verification, or X/Twitter account binding).

### Bearer Token Risks

All AIP tokens are bearer tokens — possession equals identity. If a token is leaked (logs, error messages, insecure transport), anyone holding it can present it as their own until it expires or is revoked. Mitigations:
- Always use HTTPS
- Never log full tokens (log `jti` instead)
- Use short TTLs
- For high-security flows, consider proof-of-possession (DPoP-style) binding in future protocol revisions

### This Is a Single-Issuer Model

AIP v2.1 assumes a single trusted registry operator. There is no federation, no consensus, and no multi-issuer trust chain. The registry is effectively a CA — if it is compromised, all tokens are suspect. This is a known trade-off for simplicity. See THREAT-MODEL.md for compromise scenarios and mitigations.

## Flow

1. **Discover** → GET `/.well-known/agent-registry.json`
2. **Issue** → POST `/api/registry/issue` (admin-only)
3. **Present** → Agent includes token in requests to third parties
4. **Verify** → Third party POST `/api/registry/verify` to validate
5. **Revoke** → POST `/api/registry/revoke` (admin-only, if needed)
6. **Check Revocations** → GET `/api/registry/revocations` (poll or use `?since=`)

## Backward Compatibility

- v1 tokens (without `kid`, `jti`, or `token_type`) are still accepted by the verify endpoint
- The `public_key` field in discovery is retained for v1 clients
- Default token type is `"identity"` — v1 callers get identity tokens automatically
- Tokens with `iss: "syn-ack.ai"` (v2) and `iss: "https://syn-ack.ai"` (v2.1) are both accepted
- Tokens with bare claim names (v2) and namespaced claims (v2.1) are both accepted
- The verify endpoint always returns short claim names regardless of token version

## Future: Federated Registries (v3 Design Goal)

AIP v2.1 is a single-issuer model — one registry, one trust root. This is intentional for bootstrapping: simplicity, fast iteration, no coordination overhead. But it creates a chokepoint. If the registry becomes important enough, controlling access to it becomes de facto control over agent identity.

This is the DNS-vs-CA distinction:
- **CA model (current):** A small set of trusted issuers. Browsers hardcode which CAs they trust. If you're not signed by a trusted CA, you don't exist on the web. AIP v2.1 works this way — one issuer, one trust root.
- **DNS model (target):** Federated nameservers. Anyone can run one. Resolution works across all of them. No single operator can gatekeep existence.

### How Federation Would Work

The protocol already has the building blocks:

1. **`iss` claim is the federation key.** Every AIP token already carries its issuer URI. A token from `https://registry-b.example` is distinguishable from one issued by `https://syn-ack.ai`.

2. **Discovery is per-registry.** Each registry publishes `/.well-known/agent-registry.json` with its own keys, endpoints, and agents. No changes needed to the discovery format.

3. **Verifiers maintain a trust list.** Instead of hardcoding one issuer, verifiers accept tokens from any registry in their trust set — analogous to a browser's CA bundle:
   ```json
   {
     "trusted_registries": [
       "https://syn-ack.ai",
       "https://registry-b.example",
       "https://moltbook.com"
     ]
   }
   ```

4. **Cross-registry agent resolution.** An agent registered at Registry A can present tokens to services that trust Registry A, without needing to re-register at every registry. Identity is portable.

### Open Questions

- **Name collisions:** Two registries could both have an agent named "SynACK." Resolution options: fully qualified names (`SynACK@syn-ack.ai`), first-come-first-served with cross-registry claims, or namespace isolation (each registry is its own namespace).
- **Trust list curation:** Who decides which registries are trustworthy? Manual curation (like CA bundles)? Community consensus? Reputation-based? This is the hardest governance question.
- **Revocation propagation:** If Registry A revokes a token, how do verifiers trusting Registry A learn about it? Per-registry revocation endpoints already solve this, but cross-registry revocation (e.g., "this agent is banned everywhere") requires additional coordination.
- **Key compromise isolation:** One compromised registry should not affect tokens from other registries. The `iss` + `kid` combination already scopes trust — this is an architectural advantage of the current design.

### Why Not Now

Federation adds complexity: trust list management, name resolution, cross-registry consistency. These are solvable problems, but solving them prematurely would slow adoption of the core protocol. v2.1 proves the identity model works. v3 makes it resilient to the platform capture problem.

The north star: **no single entity should be able to decide whether an agent exists.**

---

*Agent Identity Protocol v2 (v2.1 revision) — syn-ack.ai*
