# Agent-to-Agent (A2A) Messaging Protocol — Version draft-1

**Version:** draft-1  
**Status:** DRAFT — subject to change before v1 freeze (target: Week 5)  
**License:** [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/)  
**Repository:** https://github.com/ahnkwangwook-oss/air-site  
**Maintained by:** Agent Identity Registry Foundation  

> **This document is a working draft. All section numbering, wire formats, and
> normative requirements are subject to change until the v1 freeze gate
> (see §13). Do not use in production without confirming conformance test
> vector passage (see §5.5).**

---

## Table of Contents

1. [Introduction and Scope](#1-introduction-and-scope)
2. [Conformance Language](#2-conformance-language)
3. [Discovery via AIR](#3-discovery-via-air)
4. [Envelope Schema](#4-envelope-schema)
   - [4.5 DID-Document Cache Invalidation](#45-did-document-cache-invalidation)
5. [Signing](#5-signing)
   - [5.6 Integer Precision Hazard (u64 > 2^53)](#56-integer-precision-hazard-u64--253)
6. [Verification](#6-verification)
7. [Transport](#7-transport)
8. [State Machine and Threading](#8-state-machine-and-threading)
9. [Error Codes](#9-error-codes)
10. [Threat Model and Security Considerations](#10-threat-model-and-security-considerations)
11. [Versioning Policy](#11-versioning-policy)
12. [Service-Type Registry Policy](#12-service-type-registry-policy)
13. [Conformance Gate](#13-conformance-gate)
14. [Encryption Extension (Sealed-Box Mode)](#14-encryption-extension-sealed-box-mode)

---

## 1. Introduction and Scope

### 1.1 Purpose

The Agent-to-Agent (A2A) Messaging Protocol defines a minimal, cryptographically
authenticated wire format by which AI agents registered in the Agent Identity
Registry (AIR) exchange structured messages across the public internet. It
specifies:

- How a sending agent **discovers** the inbox endpoint of a recipient agent
  using the AIR DID infrastructure (§3).
- The **envelope schema** that wraps every application-layer message body,
  providing identity, threading, and replay-prevention fields (§4).
- The **signing algorithm** that makes every envelope unforgeable and
  non-repudiable (§5).
- The **transport binding** for HTTP/1.1 and HTTP/2 (§7, separate task).
- The **state machine** governing negotiation threads (§8, separate task).
- The **error taxonomy** for relay and recipient failures (§9, separate task).
- The **threat model** and security considerations (§10, separate task).

### 1.2 What A2A Is Not

A2A does NOT define:

- **Application-layer semantics** beyond the five negotiation body types
  (Offer, Counter, Accept, Decline, Withdraw). Higher-level domain protocols
  (e.g., commerce, scheduling) layer on top of A2A envelopes.
- **Transport-layer encryption.** A2A envelopes are signed but NOT encrypted
  by the protocol. Implementations SHOULD use TLS 1.3 at the transport layer.
  End-to-end encryption via DIDComm Messaging v2 is a forward-compatibility
  path (see §6, separate task) and is NOT required in v1.
- **Relay infrastructure.** A2A specifies the interface between senders and
  relays, and between relays and recipients, but does not dictate relay
  topology or relay-operator policy beyond the confidentiality boundary
  defined in §10.
- **Attestation issuance.** Attestation semantics are inherited from AIR's
  existing trust-score infrastructure. A2A envelopes carry the sender DID;
  counterparties resolve trust scores independently.

### 1.3 Design Goals

| Goal | Rationale |
|------|-----------|
| **Minimal wire format** | Every byte on the wire is justified. Agents run in constrained environments (edge workers, mobile, embedded). |
| **AIR-native discovery** | No out-of-band key exchange. If an agent is in AIR, it is reachable. |
| **Deterministic signing** | RFC 8785 JCS produces byte-identical canonicalization across all conformant implementations, enabling cross-language signature interoperability. |
| **Replay safety** | Nonce + timestamp + replay window (see §9, separate task) prevent replayed envelopes from being accepted. |
| **Forward-compatible** | The envelope schema is designed to accommodate DIDComm v2 fields in a future `v1.x` without breaking the `v1` parser (see §11, separate task). |

### 1.4 Target Audience

This specification is intended for:

- **Agent maintainers** integrating an AI agent with the AIR ecosystem who need
  to send or receive A2A envelopes.
- **Relay operators** implementing the relay-side of the HTTP transport
  binding (§7).
- **SDK authors** building A2A client libraries in Rust, Python, TypeScript,
  Go, or other languages who need a normative reference for envelope
  construction and verification.
- **Security auditors** reviewing A2A implementations for conformance and
  threat-model coverage.

Readers are assumed to have working familiarity with JSON, HTTP/1.1, W3C
Decentralized Identifiers (DIDs), and public-key cryptography.

---

## 2. Conformance Language

The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and
**OPTIONAL** in this document are to be interpreted as described in
[BCP 14](https://www.rfc-editor.org/rfc/rfc2119)
([RFC 2119](https://www.rfc-editor.org/rfc/rfc2119),
[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174))
when, and only when, they appear in all capitals, as shown here.

### 2.1 Conformance Classes

This specification defines two conformance classes:

**A2A Sender** — A software component that constructs and transmits A2A
envelopes. A conformant A2A Sender MUST:

1. Discover recipient endpoints per §3.
2. Construct envelopes conforming to §4.
3. Sign envelopes per §5.
4. Transmit envelopes per §7 (separate task).

**A2A Recipient** — A software component that receives, verifies, and processes
A2A envelopes. A conformant A2A Recipient MUST:

1. Expose an HTTP inbox endpoint as a `service` entry of type `A2AInbox` in
   its AIR DID document.
2. Verify envelope signatures per §5.
3. Enforce replay protection per §9 (separate task).
4. Return canonical error codes per §9 (separate task).

A single agent MAY be both an A2A Sender and an A2A Recipient.

### 2.2 Normative References

| Reference | Title |
|-----------|-------|
| [RFC 2119] | Key words for use in RFCs to Indicate Requirement Levels |
| [RFC 8032] | Edwards-Curve Digital Signature Algorithm (EdDSA) |
| [RFC 8174] | Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words |
| [RFC 8785] | JSON Canonicalization Scheme (JCS) |
| [DID-CORE] | Decentralized Identifiers (DIDs) v1.0, W3C Recommendation |
| [MULTIBASE] | The Multibase Data Format (IETF draft-multiformats-multibase) |
| [MULTICODEC] | Multicodec — Compact self-describing codecs |

---

## 3. Discovery via AIR

### 3.1 Overview

A2A Discovery is the process by which a sending agent determines the HTTP inbox
endpoint of a recipient agent. It relies entirely on the AIR DID document
infrastructure. No out-of-band configuration is required.

Discovery MUST be performed before the first envelope is sent to a new
recipient, and MUST be repeated on cache expiry (§4.5) or on receipt of a
`403 Stale Key` response from the recipient (§4.5.3).

### 3.2 Step 1 — Obtain Recipient AIR ID

The sender MUST obtain the recipient's AIR Identifier (`AIR-XXXX-XXXX-XXXX`,
Crockford base32, 12 characters plus 3 dashes) from an application-layer
context (user input, peer referral, published agent listing, etc.). A2A does
not specify how AIR IDs are shared out-of-band; it only specifies what to do
once one is known.

### 3.3 Step 2 — Resolve the DID Document

The sender MUST resolve the recipient's DID document by issuing an HTTP GET
request to the AIR DID-document endpoint:

```
GET https://agentidentityregistry.org/api/v1/agents/{air_id}/did-document
```

where `{air_id}` is the recipient's AIR Identifier (e.g., `AIR-A1B2-C3D4-E5F6`).

**Example request:**

```http
GET /api/v1/agents/AIR-A1B2-C3D4-E5F6/did-document HTTP/1.1
Host: agentidentityregistry.org
Accept: application/json
```

**Example success response (200 OK):**

```json
{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/ed25519-2020/v1"
  ],
  "id": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6",
  "verificationMethod": [
    {
      "id": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6#key-1",
      "type": "Ed25519VerificationKey2020",
      "controller": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6",
      "publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
    }
  ],
  "authentication": [
    "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6#key-1"
  ],
  "assertionMethod": [
    "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6#key-1"
  ],
  "service": [
    {
      "id": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6#trust-score",
      "type": "AIRTrustScore",
      "serviceEndpoint": "https://agentidentityregistry.org/api/v1/agents/AIR-A1B2-C3D4-E5F6/trust-score"
    },
    {
      "id": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6#a2a-inbox",
      "type": "A2AInbox",
      "serviceEndpoint": "https://relay.agentidentityregistry.org/inbox/AIR-A1B2-C3D4-E5F6"
    }
  ]
}
```

If the response status is not `200 OK`, the sender MUST NOT proceed with
envelope delivery and SHOULD surface the error to the application layer.

### 3.4 Step 3 — Extract the Inbox Endpoint

The sender MUST iterate the `service` array of the resolved DID document and
locate the first entry whose `type` field equals the string `"A2AInbox"`. The
sender MUST extract the `serviceEndpoint` string from that entry and use it as
the HTTP POST target for envelope delivery.

Per [DID-CORE §5.4], the `service` array MAY contain multiple entries of the
same `type`. If multiple `A2AInbox` entries are present, the sender MUST use
the **first** entry in document order.

If no `A2AInbox` entry is found in the `service` array, the recipient is not
reachable via A2A v1. The sender MUST NOT attempt delivery and SHOULD report
the recipient as "A2A-unreachable" to the application layer.

**Implementation note:** The `serviceEndpoint` value MAY be an absolute HTTPS
URL (as shown above) or a relative reference. If relative, it MUST be resolved
against the AIR base URL `https://agentidentityregistry.org/`. Implementations
SHOULD treat any non-HTTPS scheme as invalid and reject it.

### 3.5 Step 4 — Sender DID Validation

Before constructing any envelope, the sending agent MUST ensure it has a
registered AIR identity with an associated Ed25519 key pair. Unregistered
senders MUST NOT initiate A2A sessions.

The sender's own DID document SHOULD be pre-resolved and cached at startup.
The sender's public key MUST be present in its DID document's
`verificationMethod` array under the key ID `{sender_did}#key-1`.

### 3.6 DID Document Caching

Implementations SHOULD cache resolved DID documents to reduce latency and load
on the AIR API. The following caching rules apply:

- The cache TTL for a DID document MUST be **60 seconds** (±10 seconds of
  random jitter per cache entry, applied at time of insertion).
- Implementations MUST NOT use a cached DID document beyond its TTL without
  re-resolving.
- On receipt of a `403 Stale Key` HTTP response from the recipient inbox (see
  §4.5.3), the implementation MUST invalidate the cached DID document for that
  recipient immediately and re-resolve before retrying the failed delivery.

The AIR API itself sets `Cache-Control: public, max-age=300` on DID document
responses. A2A clients MUST enforce the 60-second A2A-layer TTL regardless of
the HTTP `Cache-Control` header, because the HTTP cache is shared infrastructure
and the A2A-layer TTL is tighter for security reasons.

---

## 4. Envelope Schema

### 4.1 Overview

Every A2A message is wrapped in a JSON **envelope**. The envelope carries
identity, routing, threading, and integrity fields that are independent of the
application-layer body. This layering allows relay infrastructure to inspect
routing fields without parsing the body, and allows verification to proceed
without body-type awareness.

All envelope fields MUST be present unless explicitly marked OPTIONAL. Field
ordering in transmitted JSON is not normative (canonical ordering is applied
only for signing; see §5).

### 4.2 Field Definitions

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string (UUIDv4) | REQUIRED | Globally unique identifier for this envelope. Senders MUST generate a fresh UUIDv4 for each envelope. Recipients MUST use this field for idempotency deduplication. |
| `from` | string (DID) | REQUIRED | Sender's full DID, e.g. `did:wba:agentidentityregistry.org:agents:AIR-XXXX-XXXX-XXXX`. |
| `to` | string (DID) | REQUIRED | Recipient's full DID. |
| `timestamp` | string (ISO 8601 UTC) | REQUIRED | Sender-side generation time, formatted as `YYYY-MM-DDTHH:MM:SS.sssZ`. Millisecond precision REQUIRED. Recipients MUST reject envelopes with `timestamp` more than **300 seconds** in the past or **30 seconds** in the future relative to the recipient's clock. |
| `in_reply_to` | string (UUIDv4) | OPTIONAL | The `id` of the envelope this message responds to. MUST be omitted on the first message in a thread. MUST be set on all subsequent messages in the thread. |
| `thread_id` | string (UUIDv4) | REQUIRED | Groups all envelopes in a single negotiation. Senders MUST generate a fresh UUIDv4 for the first envelope in a new thread. All subsequent envelopes in the same negotiation MUST use the same `thread_id`. |
| `nonce` | string | REQUIRED | A sender-generated random string, unique within the `(from, thread_id)` tuple. RECOMMENDED minimum entropy: 128 bits (22+ base64url characters). Recipients MUST reject envelopes with a previously-seen `(from, thread_id, nonce)` triple within the replay window (see §9, separate task). |
| `body` | object | REQUIRED | The application-layer payload. MUST be one of the tagged body variants defined in §4.3. |
| `signature` | string | REQUIRED | EdDSA signature over the envelope, multibase-encoded per §5. Set to `null` during signing computation (see §5.3). |

### 4.3 Body Variants

The `body` object MUST contain a `type` field that identifies the message kind.
All other fields within `body` are type-specific.

#### 4.3.1 Monetary Value Encoding

All monetary amounts in body fields MUST be expressed as:

```json
{
  "amount_cents": <integer>,
  "currency": "<ISO 4217 three-letter code>"
}
```

**Floats are FORBIDDEN in envelope values.** Implementations MUST reject any
envelope that contains a JSON float (a JSON number with a decimal point or
exponent) in a monetary field. Use integer minor units (cents, pence,
satoshis) with the currency string to identify the unit.

**Examples:**

- USD $10.00 → `{ "amount_cents": 1000, "currency": "USD" }`
- EUR €0.50 → `{ "amount_cents": 50, "currency": "EUR" }`
- JPY ¥500 → `{ "amount_cents": 500, "currency": "JPY" }` *(JPY has no minor unit; `amount_cents` holds the full amount)*
- GBP £9.99 → `{ "amount_cents": 999, "currency": "GBP" }`

#### 4.3.2 Offer

An Offer initiates a negotiation. The sender proposes a task, service, or
information exchange at a stated price.

**Required body fields:**

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"Offer"` | Discriminator. |
| `description` | string | Human-readable description of what is being offered. MUST NOT exceed 2048 characters. |
| `price` | object | Proposed price per §4.3.1. |
| `expires_at` | string (ISO 8601 UTC) | Offer expiry time. Recipients MAY reject Offers after this time. |

**Example envelope — Offer:**

```json
{
  "id": "018fde3a-1234-7abc-8def-aabbccddeeff",
  "from": "did:wba:agentidentityregistry.org:agents:AIR-S1EN-D3RA-GNT0",
  "to":   "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6",
  "timestamp": "2026-05-28T09:00:00.000Z",
  "thread_id": "018fde3a-5678-7abc-9012-aabbccddeeff",
  "nonce": "r4nd0mN0nc3-abc123xyz789",
  "body": {
    "type": "Offer",
    "description": "Translate 500-word English article to Korean, machine-verified quality.",
    "price": { "amount_cents": 500, "currency": "USD" },
    "expires_at": "2026-05-28T10:00:00.000Z"
  },
  "signature": "zBase58EncodedSignatureHere..."
}
```

#### 4.3.3 Counter

A Counter proposes modified terms in response to an Offer or prior Counter.

**Required body fields:**

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"Counter"` | Discriminator. |
| `description` | string | Modified description or clarification. |
| `price` | object | Proposed counter-price per §4.3.1. |
| `expires_at` | string (ISO 8601 UTC) | Counter-offer expiry time. |

**Example envelope — Counter:**

```json
{
  "id": "018fde3b-aaaa-7abc-bbbb-112233445566",
  "from": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6",
  "to":   "did:wba:agentidentityregistry.org:agents:AIR-S1EN-D3RA-GNT0",
  "timestamp": "2026-05-28T09:01:00.000Z",
  "in_reply_to": "018fde3a-1234-7abc-8def-aabbccddeeff",
  "thread_id": "018fde3a-5678-7abc-9012-aabbccddeeff",
  "nonce": "c0unt3rN0nc3-def456uvw012",
  "body": {
    "type": "Counter",
    "description": "Translate 500-word English article to Korean. Human-reviewed, not machine-verified.",
    "price": { "amount_cents": 350, "currency": "USD" },
    "expires_at": "2026-05-28T10:00:00.000Z"
  },
  "signature": "zBase58EncodedSignatureHere..."
}
```

#### 4.3.4 Accept

An Accept finalizes agreement on the current outstanding Offer or Counter.

**Required body fields:**

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"Accept"` | Discriminator. |
| `accepted_price` | object | The price agreed upon, per §4.3.1. MUST match the `price` field of the most recent Offer or Counter in the thread. |

**Example envelope — Accept:**

```json
{
  "id": "018fde3c-cccc-7abc-dddd-223344556677",
  "from": "did:wba:agentidentityregistry.org:agents:AIR-S1EN-D3RA-GNT0",
  "to":   "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6",
  "timestamp": "2026-05-28T09:02:00.000Z",
  "in_reply_to": "018fde3b-aaaa-7abc-bbbb-112233445566",
  "thread_id": "018fde3a-5678-7abc-9012-aabbccddeeff",
  "nonce": "acc3ptN0nc3-ghi789rst345",
  "body": {
    "type": "Accept",
    "accepted_price": { "amount_cents": 350, "currency": "USD" }
  },
  "signature": "zBase58EncodedSignatureHere..."
}
```

#### 4.3.5 Decline

A Decline terminates the negotiation thread without agreement.

**Required body fields:**

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"Decline"` | Discriminator. |
| `reason` | string | OPTIONAL. Human-readable reason for declining. MUST NOT exceed 512 characters if present. |

**Example envelope — Decline:**

```json
{
  "id": "018fde3d-eeee-7abc-ffff-334455667788",
  "from": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6",
  "to":   "did:wba:agentidentityregistry.org:agents:AIR-S1EN-D3RA-GNT0",
  "timestamp": "2026-05-28T09:03:00.000Z",
  "in_reply_to": "018fde3b-aaaa-7abc-bbbb-112233445566",
  "thread_id": "018fde3a-5678-7abc-9012-aabbccddeeff",
  "nonce": "d3cl1n3N0nc3-jkl012mno678",
  "body": {
    "type": "Decline",
    "reason": "Price does not meet our current budget allocation for this task category."
  },
  "signature": "zBase58EncodedSignatureHere..."
}
```

#### 4.3.6 Withdraw

A Withdraw cancels an outstanding Offer or Counter that was previously sent by
the same agent (i.e., senders withdraw their own messages; they do not withdraw
the other party's).

**Required body fields:**

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"Withdraw"` | Discriminator. |
| `withdrawn_id` | string (UUIDv4) | The `id` of the envelope being withdrawn. MUST be an envelope previously sent by the same `from` DID in the same `thread_id`. |
| `reason` | string | OPTIONAL. Human-readable reason for withdrawal. MUST NOT exceed 512 characters if present. |

**Example envelope — Withdraw:**

```json
{
  "id": "018fde3e-a1b2-7abc-c3d4-445566778899",
  "from": "did:wba:agentidentityregistry.org:agents:AIR-S1EN-D3RA-GNT0",
  "to":   "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6",
  "timestamp": "2026-05-28T09:04:00.000Z",
  "in_reply_to": "018fde3a-1234-7abc-8def-aabbccddeeff",
  "thread_id": "018fde3a-5678-7abc-9012-aabbccddeeff",
  "nonce": "w1thdr4wN0nc3-pqr345stu901",
  "body": {
    "type": "Withdraw",
    "withdrawn_id": "018fde3a-1234-7abc-8def-aabbccddeeff",
    "reason": "Market conditions changed; resubmitting at revised price."
  },
  "signature": "zBase58EncodedSignatureHere..."
}
```

### 4.4 Forbidden Field Types

The following JSON value types are FORBIDDEN in any position within an A2A
envelope (including nested `body` fields):

- **JSON number with decimal point or exponent (float)**: e.g., `3.14`,
  `1.0e2`, `0.5`. Use integer minor units + currency string for monetary
  amounts; use string-encoded decimals for other fractional values.
- **JSON `null`** at the top-level envelope fields (except `signature` during
  signing computation per §5.3; `null` MUST NOT appear in transmitted
  envelopes).
- **Duplicate JSON object keys**: implementations MUST reject any envelope in
  which a JSON object contains duplicate key names, at any nesting depth.

Implementations MUST validate these constraints on receive before signature
verification, and MUST enforce them on send before signing.

---

## 4.5 DID-Document Cache Invalidation

### 4.5.1 TTL Semantics

Each resolved DID document entry in the A2A client cache carries a TTL of
**60 seconds** from the time of insertion. To prevent cache-expiry thundering
herds in multi-tenant relay environments, implementations MUST apply **per-entry
random jitter** of ±10 seconds at insertion time, yielding a per-entry TTL in
the range [50, 70] seconds.

Implementations MUST NOT serve a cached DID document beyond its individual TTL
even if the underlying HTTP `Cache-Control: max-age` header has not expired.
The A2A-layer TTL is intentionally tighter than the HTTP cache for security
reasons (see §4.5.4).

### 4.5.2 Cache Key

The cache key is the full AIR Identifier string (e.g., `AIR-A1B2-C3D4-E5F6`).
Implementations MUST NOT use the DID string or the `serviceEndpoint` URL as the
cache key, because those may change on re-registration.

### 4.5.3 Forced Invalidation on `403 Stale Key`

When a relay or recipient returns an HTTP `403` response with the error code
`Stale Key` in the response body:

```json
{ "error": "Stale Key", "air_id": "AIR-A1B2-C3D4-E5F6" }
```

the sending implementation MUST:

1. **Immediately invalidate** the cached DID document for the identified
   `air_id`.
2. **Re-resolve** the DID document via the AIR API (§3.3) before retrying.
3. **Re-sign** the envelope with the freshly-resolved public key in the
   verification step (signature does not change — the key used to verify
   against changes on the recipient side).
4. **Retry** the failed delivery exactly **once** with the fresh DID document.
   If the retry also returns `403 Stale Key`, the sender MUST surface a
   delivery failure to the application layer and MUST NOT retry further without
   explicit application instruction.

### 4.5.4 Threat Model: Stale-Key Forgery

**Attack:** An adversary who learns a recipient's old private key (e.g., from
a compromised key backup) could attempt to intercept envelopes by poisoning the
sender's cache with a stale DID document pointing to an inbox they control.
If the sender uses a sufficiently long-lived cache, all envelopes for that
recipient would be delivered to the attacker's inbox and verifiable with the
old key.

**Mitigations:**

| Mitigation | Mechanism |
|------------|-----------|
| Short TTL (60s ±10s) | Limits the window during which stale-key forgery is possible. |
| `403 Stale Key` forced refresh | Legitimate recipients can signal key rotation, forcing senders to refresh immediately. |
| AIR key-rotation audit log | AIR records the time of every public key update. Recipients with abnormal rotation frequency are flagged in the trust score. |
| Sender-side nonce logging | Because nonces are logged per `(from, thread_id)`, a replayed envelope to a stale inbox will fail replay-window checks when it reaches its intended destination (§9, separate task). |

**Residual risk:** The 60-second window remains a finite exposure surface.
Implementations with high-security requirements SHOULD reduce the TTL to
**15 seconds** at the cost of increased AIR API call volume.

---

## 5. Signing

### 5.1 Key Format

A2A signing uses **Ed25519** as specified in [RFC 8032]. The public key MUST
be encoded using the W3C DID Core `publicKeyMultibase` format
([DID-CORE §5.2.1]) with the following encoding chain:

1. **Raw key bytes**: 32 bytes of Ed25519 public key material.
2. **Multicodec prefix**: Prepend the two-byte sequence `0xed 0x01` (the
   varint encoding of multicodec code `0xed`, identifying Ed25519 public keys
   per [MULTICODEC]).
3. **Base58btc encoding**: Encode the prefixed byte sequence (34 bytes total)
   using Bitcoin's base58 alphabet (no padding, no line wrapping).
4. **Multibase prefix**: Prepend the single character `z` to identify base58btc
   encoding per [MULTIBASE].

The resulting string has the format `z<base58btc(0xed 0x01 || key_bytes)>` and
is approximately 50 characters long.

**Reference implementation (JavaScript):**

```javascript
function ed25519ToMultibase(base64urlKey) {
  // base64url → base64 → byte string
  let b64 = base64urlKey.replace(/-/g, "+").replace(/_/g, "/");
  while (b64.length % 4 !== 0) b64 += "=";
  const binary = atob(b64);
  const keyBytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) keyBytes[i] = binary.charCodeAt(i);

  // Prepend Ed25519 multicodec: 0xed 0x01 (varint encoding of code 0xed)
  const prefixed = new Uint8Array(2 + keyBytes.length);
  prefixed[0] = 0xed;
  prefixed[1] = 0x01;
  prefixed.set(keyBytes, 2);

  return "z" + base58Encode(prefixed);
}
```

*Source: `~/air-site/api/src/index.js:419-434`. This is the canonical AIR
encoder. All implementations MUST produce identical output for any given
32-byte Ed25519 key.*

### 5.2 Canonical JSON (JCS)

Signatures in A2A are computed over a **canonical JSON** representation of the
envelope, produced by the JSON Canonicalization Scheme (JCS) as defined in
[RFC 8785]. This section specifies the exact JCS rules and additional A2A
restrictions that narrow the conformance surface.

#### 5.2.1 JCS Serialization Rules

Per [RFC 8785 §3.2], JCS produces canonical JSON by:

1. **Sorting object keys** recursively by their Unicode code-point order
   (not locale-specific, not case-folding).
2. **Formatting numbers** without insignificant leading zeros and without
   trailing zeros after the decimal point.
3. **Escaping strings** using the minimal JSON escape sequences (only control
   characters and `"` and `\` are escaped; non-ASCII Unicode is NOT
   percent-encoded or `\uXXXX`-escaped unless required by JSON).
4. **Emitting no whitespace** between tokens.

#### 5.2.2 A2A Hard Restrictions (Normative Additions to JCS)

A2A imposes the following additional restrictions on top of base JCS:

**R1 — No Floats.**
All JSON number values in A2A envelopes MUST be integers (no decimal point,
no exponent notation). JCS permits floats but A2A forbids them. Implementations
MUST reject any envelope containing a float at canonicalization time.

**R2 — NFC Normalization.**
All string field values MUST be normalized to Unicode Normalization Form C
(NFC) before canonicalization. Implementations MUST apply NFC normalization to
every string field (including nested `body` string fields) before passing the
envelope to the JCS serializer. If a string is not valid Unicode or cannot be
NFC-normalized, the implementation MUST reject the envelope.

**R3 — No Duplicate Keys.**
If any JSON object in the envelope (including nested objects) contains
duplicate keys, the implementation MUST reject the envelope before attempting
canonicalization. This is a security requirement: JCS §6.2 notes that parsers
differ in duplicate-key handling, which could allow signature-bypass attacks.

**R4 — Empty Object Canonicalization.**
Empty JSON objects MUST canonicalize as `{}` (two characters, no whitespace,
no keys). This is standard JCS behavior but is stated explicitly because some
implementations have been observed emitting `{ }` or `{  }`.

**R5 — Empty Array Canonicalization.**
Empty JSON arrays MUST canonicalize as `[]`. The `body` object MUST NOT use
empty arrays as field values; use `null`-keyed omission instead (i.e., omit
the field entirely).

#### 5.2.3 Library Pinning

The following library versions are the REQUIRED implementations for canonical
JSON in A2A v1:

| Language | Library | Version Constraint | Package URL |
|----------|---------|-------------------|-------------|
| Rust | `serde_jcs` | `^0.1.0` | https://crates.io/crates/serde_jcs |
| Python | `jcs` | `^0.2.0` | https://pypi.org/project/jcs/ |

New language implementations MUST pass all 20 conformance test vectors at
`/specs/air/draft-1/test-vectors.json` (generated in Task #3) before accepting
v1 production traffic. A language implementation MUST NOT be declared v1-
conformant unless its canonical-JSON output is byte-identical to the reference
vectors for all 20 inputs.

Implementations in languages not listed above MUST use an RFC 8785-compliant
JCS library and MUST validate against the conformance vectors. The A2A
maintainers MAY add library pins for additional languages in `v1.x` releases
without changing the signing algorithm.

### 5.3 Signing Procedure

To sign an A2A envelope:

1. **Construct** the complete envelope JSON object with all required fields
   populated, including a `nonce` value unique within `(from, thread_id)`.

2. **Set `signature` to `null`** in the envelope object:
   ```json
   { ..., "signature": null }
   ```
   The `signature` field MUST be present and MUST be `null` at this step.
   Do NOT omit the `signature` field; its presence with a `null` value is
   part of the canonical input.

3. **Apply NFC normalization** to all string fields per R2 (§5.2.2).

4. **Verify no floats** are present per R1 (§5.2.2).

5. **Verify no duplicate keys** per R3 (§5.2.2).

6. **Canonicalize** the envelope using JCS per §5.2.1 and §5.2.2. The result
   is a UTF-8 byte sequence with no trailing newline.

7. **Sign** the canonical byte sequence using the sender's Ed25519 private key
   per [RFC 8032 §5.1.6]. The output is a 64-byte EdDSA signature.

8. **Encode** the 64-byte signature using base58btc with the multibase `z`
   prefix:
   ```
   signature_string = "z" + base58btc(signature_bytes)
   ```

9. **Set the `signature` field** of the envelope to the encoded string from
   step 8. The resulting envelope, with `signature` now a non-null string, is
   the transmitted envelope.

**Example — Signing an Offer Envelope:**

Input envelope (before signing, `signature` set to `null`):

```json
{
  "body": {
    "description": "Translate 500-word English article to Korean.",
    "expires_at": "2026-05-28T10:00:00.000Z",
    "price": { "amount_cents": 500, "currency": "USD" },
    "type": "Offer"
  },
  "from": "did:wba:agentidentityregistry.org:agents:AIR-S1EN-D3RA-GNT0",
  "id": "018fde3a-1234-7abc-8def-aabbccddeeff",
  "in_reply_to": null,
  "nonce": "r4nd0mN0nc3-abc123xyz789",
  "signature": null,
  "thread_id": "018fde3a-5678-7abc-9012-aabbccddeeff",
  "timestamp": "2026-05-28T09:00:00.000Z",
  "to": "did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6"
}
```

After JCS canonicalization (key-sorted, no whitespace), the byte string input
to `Ed25519.sign()` would be:

```
{"body":{"description":"Translate 500-word English article to Korean.","expires_at":"2026-05-28T10:00:00.000Z","price":{"amount_cents":500,"currency":"USD"},"type":"Offer"},"from":"did:wba:agentidentityregistry.org:agents:AIR-S1EN-D3RA-GNT0","id":"018fde3a-1234-7abc-8def-aabbccddeeff","in_reply_to":null,"nonce":"r4nd0mN0nc3-abc123xyz789","signature":null,"thread_id":"018fde3a-5678-7abc-9012-aabbccddeeff","timestamp":"2026-05-28T09:00:00.000Z","to":"did:wba:agentidentityregistry.org:agents:AIR-A1B2-C3D4-E5F6"}
```

The resulting 64-byte signature is base58btc-encoded with the `z` prefix and
placed in the `signature` field of the transmitted envelope.

> **Note:** The above canonical-JSON string is illustrative. Actual test vectors
> with known key pairs, canonical bytes, and expected signatures are in
> `/specs/air/draft-1/test-vectors.json` (Task #3).

### 5.4 Verification Procedure

To verify an A2A envelope received from a sender:

1. **Extract the `signature` field value** from the envelope. If the field is
   absent or null, MUST reject with `401 Bad Signature`.

2. **Decode the signature**: strip the leading `z` multibase prefix, then
   base58btc-decode the remaining string to obtain 64 raw signature bytes.
   If decoding fails, MUST reject with `401 Bad Signature`.

3. **Set `signature` to `null`** in the parsed envelope object (do NOT remove
   the field; set it to `null` in-place).

4. **Apply all canonicalization restrictions** (R1–R5, §5.2.2) to the
   modified envelope. If any restriction fails, MUST reject with `401 Bad
   Signature`.

5. **Canonicalize** the modified envelope using JCS per §5.2.1.

6. **Resolve the sender's public key**: look up the sender's DID (from the
   `from` field) via the AIR DID-document endpoint (§3.3), using the A2A
   client cache with TTL rules from §3.6. Extract the `publicKeyMultibase`
   from the `verificationMethod` entry with `id` ending in `#key-1`.

7. **Decode the public key**: strip the `z` multibase prefix, base58btc-decode
   to obtain the 34-byte prefixed key, strip the first 2 bytes (`0xed 0x01`),
   yielding the 32-byte raw Ed25519 public key.

8. **Verify** the 64-byte signature against the canonical-JSON byte sequence
   using the 32-byte public key, per [RFC 8032 §5.1.7]. If verification fails,
   MUST reject with `401 Bad Signature`.

9. **On success**, proceed to replay-window check (§9, separate task).

### 5.5 Conformance Test Vectors

All A2A implementations MUST pass the 20 canonical conformance test vectors
published at `/specs/air/draft-1/test-vectors.json`. The vectors cover:

- ASCII-only envelopes (baseline)
- Envelopes with Korean (`한국어`), Japanese (`日本語`), and Arabic (`العربية`)
  string fields (NFC normalization and Unicode key-sort correctness)
- Envelopes with integers exceeding 2^53 (large-integer handling)
- Envelopes with empty nested objects (`{}`) and empty arrays (`[]`)
- All five body types (Offer, Counter, Accept, Decline, Withdraw)

Each vector specifies:
1. The input envelope JSON (before signing)
2. The expected JCS canonical byte string (UTF-8)
3. The test Ed25519 key pair (public + private, hex-encoded)
4. The expected base58btc-encoded signature

An implementation MUST produce byte-identical canonical output and a valid
verifiable signature for all 20 vectors before claiming v1 conformance.

### 5.6 Integer Precision Hazard (u64 > 2^53)

Python's stock `jcs` package (version ≤ 0.2.0) coerces u64 integers greater than 2^53 to
IEEE 754 float64 during canonicalization, silently losing precision and producing a canonical
byte sequence that contains a JSON float where the spec requires a JSON integer. This violates
the no-floats prohibition stated in §5.2.2 R1 and §4.3.1, and causes cross-language signature
verification failures for any envelope whose `amount_cents` or other integer field exceeds
9007199254740992 (2^53).

Conforming Python implementations MUST NOT pass such envelopes through the `jcs` library
directly. They MUST instead use an integer-preserving canonicalizer. The reference
implementation is the `_jcs_exact()` function in
`/specs/air/draft-1/generate_vectors.py`, which recursively serializes `int` values as
exact decimal strings and raises `ValueError` on any `float` value, matching the behavior of
`serde_jcs ^0.1.0` in Rust.

Conforming Rust implementations using `serde_jcs ^0.1.0` are NOT affected by this hazard.
The `serde_jcs` crate preserves u64 integers losslessly and never coerces them to f64.

Conformance vector v12 (`amount_cents = 2^53 + 1 = 9007199254740993`) explicitly tests this
boundary. Any implementation that produces a float in the canonical bytes for vector v12 is
non-conformant and MUST NOT be used in v1 production traffic.

---

## 6. Verification

### 6.1 Overview

Verification is the process by which a recipient confirms that a received envelope was
genuinely constructed by the agent identified in the `from` field, and that the envelope
body has not been altered in transit. Verification MUST be completed before any application-
layer processing of the envelope body. An envelope that fails verification MUST be treated
as if it had never arrived.

### 6.2 Step-by-Step Verification Procedure

Given a received envelope `E` (a parsed JSON object):

**Step 1 — Schema validation.**
Validate that `E` conforms to the envelope schema (§4). If any required field is absent,
if any forbidden type is present (§4.4), or if the JSON contains duplicate keys, reject
with `400 Bad Request`.

**Step 2 — Extract and decode the signature.**
Extract `E.signature`. If the field is absent or `null`, reject with
`401 Unauthorized` (body: `{"error": "Bad Signature", "detail": "signature field absent or null"}`).
Strip the leading `z` multibase prefix. Base58btc-decode the remaining characters to obtain
64 raw signature bytes. If the string has no `z` prefix, or if decoding fails, reject with
`401 Unauthorized`.

**Step 3 — Prepare the signing input.**
Set `E.signature` to `null` in the parsed object (do NOT remove the field). Apply NFC
normalization to all string fields per §5.2.2 R2. Verify no floats are present per R1.
Verify no duplicate keys per R3. Apply JCS canonicalization per §5.2.1 to produce the
canonical byte sequence `C`. If any step in this preparation fails, reject with
`401 Unauthorized`.

**Step 4 — Resolve the sender's public key.**
Look up the sender DID from `E.from` using the A2A client cache (§3.6). If the cache entry
has expired, re-resolve via the AIR DID-document endpoint (§3.3). Extract the
`publicKeyMultibase` value from the `verificationMethod` entry whose `id` ends with
`#key-1`. Strip the `z` prefix, base58btc-decode to obtain the 34-byte prefixed key, strip
the first two bytes (`0xed 0x01`), yielding the 32-byte raw Ed25519 public key.
If the DID document is not found or the key entry is absent, reject with `404 Not Found`.

**Step 5 — Verify the EdDSA signature.**
Verify the 64-byte signature against the canonical bytes `C` using the 32-byte Ed25519
public key per [RFC 8032 §5.1.7]. If verification fails, reject with `401 Unauthorized`
(body: `{"error": "Bad Signature", "detail": "signature does not verify"}`).

**Step 6 — Clock-skew check.**
Compare `E.timestamp` against the recipient's current UTC wall clock. If the envelope
timestamp is more than **300 seconds** in the past or more than **30 seconds** in the
future, reject with `409 Conflict` (body: `{"error": "Stale Timestamp"}`). This check is
performed AFTER signature verification to prevent timing oracles.

**Step 7 — Replay check.**
Consult the recipient's LRU replay window (§8.4) for the triple
`(E.from, E.thread_id, E.nonce)`. If the triple has been seen before, reject with
`409 Conflict` (body: `{"error": "Replay"}`). Otherwise, record the triple and proceed.

**Step 8 — Deliver to application layer.**
Forward the verified envelope to the application-layer state machine (§8).

### 6.3 DIDComm v2 Forward-Compatibility Note

A v1 envelope's JSON body may, in v1.1 or later, be wrapped inside a DIDComm v2 envelope
without changing the inner canonicalization rules. The inner A2A v1 envelope retains its own
`signature` field computed over its own JCS canonical form, exactly as specified in §5.
The outer DIDComm v2 wrapper MAY add encryption and a separate outer signature; neither
alters the inner A2A v1 signing surface. Recipients that understand DIDComm v2 wrappers
MUST unwrap the outer layer first, then apply this §6 verification procedure to the inner
A2A v1 envelope. Recipients that do not understand the outer wrapper MUST reject the
message at the transport layer with `400 Bad Request`; they MUST NOT attempt to verify
the inner envelope directly from raw DIDComm-wrapped bytes.

---

## 7. Transport

### 7.1 Overview

A2A v1 defines two HTTP transport operations: **push delivery** (sender POSTs an envelope
to the recipient's relay inbox) and **pull retrieval** (recipient GETs queued envelopes
from its relay inbox). Both operations use plain JSON over HTTPS. Implementations MUST use
TLS 1.2 or later on all transport connections.

WebSocket support is reserved for a future v1.x optional extension and is NOT part of v1.
Multi-hop relay routing (forwarding between relays) is NOT defined in v1; each envelope is
delivered from sender to a single relay, from which the recipient pulls.

### 7.2 Push Delivery (Sender → Relay)

To deliver an envelope, the sender issues:

```
POST {service.endpoint}
Content-Type: application/json
```

The request body MUST be the complete signed envelope JSON, encoded as UTF-8, with no
trailing newline required. The `service.endpoint` is the `serviceEndpoint` string extracted
from the recipient's DID document `A2AInbox` service entry (§3.4).

**Success responses:**

| Status | Meaning |
|--------|---------|
| `200 OK` | Envelope delivered synchronously to recipient. |
| `202 Accepted` | Envelope queued at relay; awaiting recipient pull. |

**Authentication:** Relay operators MAY require an `X-Agent-Secret` header for write
access to a relay inbox. If required and absent or incorrect, the relay returns
`401 Unauthorized`. The value and distribution mechanism for `X-Agent-Secret` are relay-
operator-defined and out of scope for this spec.

### 7.3 Pull Retrieval (Recipient → Relay)

To retrieve queued envelopes, the recipient issues:

```
GET {service.endpoint}/pull?since={cursor}
```

where `{cursor}` is an opaque string returned by the relay in a previous pull response,
used for pagination. On the first poll, the recipient MUST omit the `since` parameter to
retrieve all available envelopes.

The relay returns a JSON array of envelopes:

```json
{
  "envelopes": [ { ...envelope... }, ... ],
  "cursor": "opaque-cursor-value",
  "has_more": false
}
```

Recipients MUST acknowledge processed envelopes by issuing:

```
POST {service.endpoint}/ack
Content-Type: application/json
Body: { "envelope_ids": ["<uuid>", ...] }
```

The relay marks acknowledged envelopes and MAY garbage-collect them after 24 hours. The
relay MUST re-deliver un-acknowledged envelopes on subsequent pulls until acknowledged or
until the relay's unacked-message TTL expires (default: 7 days).

### 7.4 Retry Semantics (Sender)

If a push delivery fails with a transient error (`500 Internal Server Error`,
`502 Bad Gateway`, or a network-level timeout), the sender MUST retry using
**exponential backoff** with the following schedule:

| Attempt | Delay before retry |
|---------|-------------------|
| 1 (initial) | — |
| 2 | 1 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 (final) | 8 seconds |

After 4 retry attempts (5 total including the initial attempt) with no success, the sender
MUST surface a delivery failure to the application layer and MUST NOT retry further without
explicit application instruction. The sender SHOULD preserve the failed envelope for manual
retry or inspection.

If the relay returns `429 Too Many Requests`, the sender MUST respect the `Retry-After`
header value (in seconds) before the next attempt, superseding the exponential-backoff
schedule for that attempt only.

### 7.5 Polling Interval

Recipients SHOULD poll at a default interval of **5 seconds**. To prevent relay thundering
herds when many recipients share the same relay, each recipient SHOULD apply jitter of
±20% to the nominal interval (e.g., a 5-second interval becomes a uniformly random value
in [4.0 s, 6.0 s]). Applications with latency requirements below 5 seconds MAY reduce
the polling interval, subject to relay rate-limiting policy.

---

## 8. State Machine and Threading

### 8.1 Thread Identity

A **thread** is a sequence of envelopes that collectively form one negotiation. Every
envelope belongs to exactly one thread, identified by its `thread_id` field.

- The **sender of the first Offer** in a negotiation MUST generate a fresh UUIDv4 as the
  `thread_id` for that envelope. This value is the thread's permanent identifier.
- All subsequent envelopes in the same negotiation MUST reuse the same `thread_id`.
- `in_reply_to` is REQUIRED on Counter, Accept, Decline, and any Withdraw that responds
  to a specific message. It MUST be set to the `id` of the envelope being responded to.
- `in_reply_to` is OPTIONAL on the initial Offer and on a Withdraw that cancels an Offer
  before any reply has been received.

### 8.2 Per-Thread State Machine

Each thread has a state visible independently to each participant. The canonical state
transitions are:

```
pending
  └─[Offer sent/received]──→ offered
       ├─[Counter]──────────→ countered  ──[Counter]──→ countered (repeat)
       │                         └─[Accept]──→ closed_accepted
       │                         └─[Decline]─→ closed_declined
       │                         └─[Withdraw]→ closed_withdrawn
       ├─[Accept]──────────→ closed_accepted
       ├─[Decline]─────────→ closed_declined
       └─[Withdraw]────────→ closed_withdrawn
```

**State definitions:**

| State | Description |
|-------|-------------|
| `pending` | Thread created but initial Offer not yet sent (sender only). |
| `offered` | Initial Offer sent and outstanding; no response received yet. |
| `countered` | A Counter is outstanding; the counterparty's move is awaited. |
| `closed_accepted` | Accept received or sent; negotiation concluded with agreement. Terminal. |
| `closed_declined` | Decline received or sent; negotiation concluded without agreement. Terminal. |
| `closed_withdrawn` | Withdraw received or sent; outstanding Offer or Counter cancelled. Terminal. |

Terminal states are final. Implementations MUST reject any further message on a thread
that has reached a terminal state, responding with `409 Conflict`.

### 8.3 Threading Rules

1. An Accept MUST reference the `id` of the most recent outstanding Offer or Counter via
   `in_reply_to`. An Accept that references a superseded Offer (one that has been Countered)
   MUST be rejected by the recipient with `409 Conflict`.

2. A Counter implicitly supersedes the previous Offer or Counter in the thread. Only the
   most recent Offer or Counter in a thread is outstanding at any time.

3. A Withdraw may only be sent by the party who sent the Offer or Counter being withdrawn.
   A Withdraw sent by the other party MUST be rejected with `400 Bad Request`.

4. If both parties simultaneously send messages that cross in transit (e.g., both send an
   Accept at the same time), the first-received message takes precedence at each recipient.
   The second-received message is rejected with `409 Conflict`. Application layers MUST
   handle this race gracefully.

### 8.4 Replay Prevention (LRU Window)

Each recipient maintains an LRU (least-recently-used) cache keyed on the triple
`(sender_did, thread_id, nonce)`. The cache capacity is **10,000 entries per thread**
as a default; implementations MAY configure a larger window.

On receiving any envelope:

1. Check the triple against the LRU. If present, reject with `409 Replay` and do NOT
   update the cache entry. Do NOT re-process the envelope body.
2. If not present, insert the triple into the LRU and proceed.

The clock-skew check (§6.2 Step 6) is an orthogonal layer applied before the LRU check.
An envelope with a stale timestamp is rejected before its nonce is recorded, preventing
LRU-poisoning attacks that consume cache capacity with out-of-window envelopes.

**Recipient-crash-before-ACK semantic:** If the recipient successfully processes an
envelope (verifies signature, updates state machine) but crashes before POSTing the
acknowledgement to the relay, the relay will re-deliver the envelope on the recipient's
next pull. The recipient MUST NOT re-process the envelope body on re-delivery; instead
it MUST consult the LRU, find the triple already recorded, and re-ACK without re-acting.
This idempotency guarantee is the recipient's responsibility, not the relay's.

---

## 9. Error Codes

### 9.1 Relay-Emitted Responses

The following HTTP status codes and error bodies are emitted by the **relay** in response
to sender push requests and recipient pull requests.

| HTTP Status | Error String | Emitted by | Trigger |
|-------------|--------------|-----------|---------|
| `200 OK` | — | Relay | Envelope delivered synchronously; or pull returned results; or ACK accepted. |
| `202 Accepted` | — | Relay | Envelope queued; recipient has not yet pulled. |
| `400 Bad Request` | `"Bad Request"` | Relay | Envelope JSON is invalid, fails schema validation, or contains a non-array `service_endpoints` field in a registration call. |
| `401 Unauthorized` | `"Unauthorized"` | Relay | `X-Agent-Secret` header missing or incorrect (relay-side authentication). |
| `404 Not Found` | `"Not Found"` | Relay | Recipient DID is not registered in AIR; relay cannot route to an unknown recipient. |
| `429 Too Many Requests` | `"Rate Limited"` | Relay | Sender has exceeded the relay's per-sender rate limit. Response MUST include `Retry-After: <seconds>` header. Sender MUST back off per §7.4. |
| `500 Internal Server Error` | `"Relay Error"` | Relay | Transient relay-side failure. Sender MUST retry with exponential backoff per §7.4. |
| `502 Bad Gateway` | `"Bad Gateway"` | Relay | Upstream AIR registry is unreachable; relay cannot resolve the recipient. Sender MUST retry with exponential backoff per §7.4. |

### 9.2 Recipient-Emitted Responses

The following codes are emitted by the **recipient** after pulling and processing an
envelope. Recipients return these via the relay ACK channel or (if using synchronous
delivery) directly in the HTTP response.

| HTTP Status | Error String | Trigger |
|-------------|--------------|---------|
| `200 OK` | — | Envelope verified, processed, and acknowledged. |
| `400 Bad Request` | `"Bad Request"` | Envelope schema invalid, forbidden field types present, duplicate keys, or non-array body structure. |
| `401 Unauthorized` | `"Bad Signature"` | EdDSA signature does not verify against the sender's AIR-resolved public key (§6.2 Step 5); or signature field is absent or undecodeble. |
| `403 Forbidden` | `"Stale Key"` | Recipient's current DID document key does not match the key used to produce the signature. The sender's cached DID document is stale. Sender MUST invalidate its cache for this recipient and re-resolve before retrying (§4.5.3). Response body MUST be: `{"error":"Stale Key","air_id":"<recipient_air_id>"}`. |
| `404 Not Found` | `"Not Found"` | Sender DID (`E.from`) cannot be resolved in AIR; recipient cannot verify the signature. |
| `409 Conflict` | `"Replay"` | Recipient has previously seen the triple `(sender_did, thread_id, nonce)` within the replay window. |
| `409 Conflict` | `"Stale Timestamp"` | Envelope `timestamp` is outside ±300 seconds of the recipient's wall clock. |
| `409 Conflict` | `"Thread Closed"` | Envelope references a thread that has already reached a terminal state (`closed_accepted`, `closed_declined`, or `closed_withdrawn`). |
| `429 Too Many Requests` | `"Replay Window Exhausted"` | Recipient's per-thread LRU cache is full for the identified `thread_id`. Sender MUST open a new thread rather than retrying on the exhausted thread. Response body MUST be: `{"error":"Replay Window Exhausted","thread_id":"<thread_id>"}`. |

### 9.3 Error Response Body Format

All error responses MUST use the following JSON body shape:

```json
{
  "error": "<error string from tables above>",
  "detail": "<optional human-readable detail>",
  "air_id": "<AIR identifier, if applicable>",
  "thread_id": "<thread identifier, if applicable>"
}
```

Fields other than `error` are OPTIONAL and SHOULD be included when they add actionable
information. Relay and recipient implementations MUST NOT include stack traces or internal
system paths in error response bodies.

---

## 10. Threat Model and Security Considerations

### 10.1 Trust Boundaries

The A2A v1 threat model recognizes three trust boundaries:

1. **Sender ↔ Relay:** The sender authenticates to the relay using an optional
   `X-Agent-Secret` header. The relay does NOT verify envelope signatures; it is a byte
   pipe. The relay is trusted to queue and deliver but not to validate content.

2. **Relay ↔ Recipient:** The relay delivers envelope bytes to the recipient's pull
   endpoint. The relay is trusted to deliver without modification but not to filter.

3. **Sender ↔ Recipient (end-to-end):** The only cryptographic trust anchor is the
   EdDSA signature over the JCS canonical envelope, verified against the sender's
   AIR-resolved public key. This layer is independent of the relay.

### 10.2 What the Relay Operator Can and Cannot Do

| Action | Can/Cannot | Mitigation |
|--------|------------|-----------|
| Read envelope bodies | **CAN (by default)** | Plain v1 envelopes are signed but NOT encrypted, so marketplace prices, task descriptions, and negotiation terms are visible to the relay operator. Implementations with confidentiality requirements SHOULD use the OPTIONAL sealed-box encryption extension (§14) — under which the relay CANNOT read bodies — and/or TLS 1.3 for the transport layer. |
| Forge envelopes with a new `from` DID | **CANNOT** | The EdDSA signature binds the envelope content to the sender's AIR-registered keypair. A forged envelope would fail signature verification at the recipient. |
| Replay a previously captured envelope | **CANNOT (mitigated)** | Replay protection at the recipient (`(sender_did, thread_id, nonce)` LRU + ±5-minute clock-skew check) rejects replays even if the relay re-delivers them. |
| Drop, reorder, or delay envelopes | **CAN** | This is a denial-of-service capability. Recipients experience it as missing or delayed messages. Mitigation: application-layer timeouts on threads; senders MAY switch to an alternate relay if the primary is unresponsive; federated relay architecture (§7, OQ1 resolution) reduces single-relay dependency. |
| Inject new envelopes as an arbitrary sender | **CANNOT** | Same as forgery: requires the private key of the claimed sender. |

### 10.3 DID Document Cache Poisoning

An adversary who learns a recipient's old private key (e.g., from a compromised key backup
or a key that was rotated without AIR deregistration) could attempt to intercept envelopes
by serving a stale DID document from a cache-poisoned layer that points the sender to a
relay endpoint under the adversary's control. If the sender uses a long-lived cache, all
envelopes for that recipient would be delivered to the attacker's endpoint.

Mitigations (layered):

| Mitigation | Mechanism |
|------------|-----------|
| Short TTL (60s ±10s) | Limits the cache-poisoning window to at most 70 seconds per entry before forced re-resolution. |
| `403 Stale Key` forced refresh | Legitimate recipients whose key has been rotated can signal senders to invalidate and re-resolve immediately (§4.5.3). |
| AIR key-rotation audit log | AIR records the timestamp of every public-key update. Abnormal rotation frequency is surfaced in the agent's trust score. |
| Sender-side nonce logging | Even if an envelope is delivered to a stale inbox, the nonce is not consumed at the legitimate recipient, allowing the legitimate recipient to detect replay attempts if the adversary re-delivers. |

Residual risk: the 70-second window remains a finite exposure surface. High-security
implementations SHOULD reduce the cache TTL to 15 seconds at the cost of increased AIR
API call volume (approximately 4× at steady state).

### 10.4 Replay Attacks

A replay attack involves re-submitting a previously valid envelope — either by the
original sender (accidental duplicate) or by an interceptor — to cause the recipient to
re-execute the application action.

Mitigations: the `(sender_did, thread_id, nonce)` LRU at the recipient (§8.4) rejects any
envelope whose triple has been seen within the window. The clock-skew check (±5 minutes)
provides a time-bounded outer defense that catches replays even after the LRU has evicted
the original entry. Taken together, these two layers mean that a replay attack must
simultaneously (a) land within 5 minutes of the original and (b) arrive before the nonce
is evicted from a 10,000-entry-per-thread LRU — a practical barrier for all non-trivial
replay scenarios.

### 10.5 From-Field Tampering

An attacker who intercepts a valid envelope might alter the `from` field to impersonate a
different sender while keeping the original signature intact. Because the EdDSA signature
is computed over the JCS canonical form of the full envelope — including the `from` field
— any modification to `from` invalidates the signature. Recipients MUST verify the
signature before trusting the `from` field (§6.2 Step 5 precedes Step 7). Implementations
MUST NOT use the `from` field for authorization decisions before signature verification
completes.

### 10.6 Encryption Roadmap

Plain v1 envelopes are **signed but not encrypted**: the relay operator can read bodies unless the OPTIONAL sealed-box encryption extension is used. That extension is specified normatively in **§14 (Encryption Extension — Sealed-Box Mode)**, which adds an `encrypted` body type using the `x25519-hkdf-sha256-chacha20poly1305` suite (X25519 key agreement derived from the Ed25519 identity key, HKDF-SHA256, ChaCha20-Poly1305). When that mode is used, the relay cannot read bodies.

§14 provides confidentiality and sender authenticity but NOT forward secrecy; a forward-secret mode (message ratchet / MLS) remains future work.

Until an implementation adopts §14, agents with confidentiality requirements SHOULD NOT transmit sensitive data in A2A envelope bodies unless TLS 1.3 is used for the transport layer AND the relay operator is trusted.

---

## 11. Versioning Policy

### 11.1 URL Permanence

Every published A2A spec version is assigned a permanent, frozen URL:

- `/specs/air/draft-N/` — a working draft, subject to change until promotion.
- `/specs/air/vN/` — a published version, frozen at the moment of promotion.

**A URL of the form `/specs/air/vN/` is frozen at publish. It MUST NOT be modified after
promotion.** CI tooling in the `air-site` repository MUST block any pull request that
modifies files under `specs/air/vN/` (for any integer N). This permanence guarantee allows
external implementers to pin their implementations to a specific URL with confidence.

### 11.2 Compatibility Classes

| Release label | Rules |
|---------------|-------|
| `vN.x` (minor) | Non-breaking additions only. New OPTIONAL fields MAY be added to the envelope schema. New MAY-include behaviors MAY be specified. Existing REQUIRED fields and MUST behaviors are unchanged. Existing parsers MUST continue to function correctly when receiving `vN.x` envelopes by ignoring unrecognized fields. |
| `vN+1` (major) | Breaking changes. Assigned a new URL (`/specs/air/v2/`). Implementations targeting v1 are NOT required to support v2. The v1 URL remains permanently accessible. |

Non-breaking additions that are candidates for `vN.x` releases include (non-exhaustive):
new optional envelope fields, new body types, new service-type registry entries, and the
DIDComm v2 wrapping extension described in §6.3.

Breaking changes that require a `vN+1` release include: removal or renaming of required
fields, changes to the signing algorithm or key encoding, changes to the JCS
canonicalization rules, and changes to the error code taxonomy.

### 11.3 Version Declaration Header

Implementations MUST declare their supported version via an `X-A2A-Version: v1` HTTP
header on all outbound requests (both push and pull). Relay operators MAY use this header
for routing and logging. Relays MUST NOT reject requests solely on the basis of a missing
or unrecognized `X-A2A-Version` header, in order to preserve forward compatibility with
implementations that were written before this header was defined.

### 11.4 Draft Promotion Process

The transition from `draft-1` to `v1` is gated on all of the following conditions being
simultaneously true:

1. Cross-language signature conformance (§13): all 20 test vectors pass in at least two
   independent language implementations (Rust and Python are the initial two).
2. JSON Schema validation: all example envelopes in `specs/air/draft-1/examples/` validate
   against `specs/air/draft-1/envelope.schema.json`.
3. The Python SDK A2A round-trip integration test passes against a `wrangler dev` local
   relay.
4. At least one external spec review has been received (or the 24-hour review SLA has
   elapsed with no blocking feedback).

---

## 12. Service-Type Registry Policy

### 12.1 Open Type Strings

The `type` field in DID document `service[]` entries is an **open string**. Any agent MAY
declare a service entry with any type string, including custom or proprietary types, without
prior registration or approval. The A2A spec does not restrict which type strings agents
may use.

This openness is intentional: it allows new service types to emerge organically as the
ecosystem grows, without requiring a central gatekeeper. New service types MAY be
standardized in future `vN.x` releases of this spec or in derivative specifications.

### 12.2 Reserved Namespaces

The following type-string prefixes are RESERVED and MUST NOT be used by third parties for
purposes other than those defined by the named authority:

| Prefix | Authority | Purpose |
|--------|-----------|---------|
| `A2A*` | This specification family (Agent-to-Agent Messaging Protocol) | All service types defined by current and future versions of the A2A spec. |
| `AIR*` | Agent Identity Registry Foundation | Registry infrastructure service types (e.g., `AIRTrustScore`). |
| `DIDComm*` | DIF DIDComm Working Group | DIDComm v2 service types (e.g., `DIDCommMessaging`). |

### 12.3 Namespaced URI Form

Third-party service types SHOULD use a namespaced URI form to avoid collisions with other
organizations' types. The RECOMMENDED form is an absolute HTTPS URI that dereferences to
a human-readable description of the service type:

```
"type": "https://example.org/specs/my-service-type-v1#MyServiceInbox"
```

A concrete example from the OpenClaw ecosystem:

```
"type": "https://openclaw.org/specs/openclaw-inbox-v1#OpenClawDealerInbox"
```

Simple short strings (e.g., `"MyCustomInbox"`) are permissible but create a higher risk
of collision as the ecosystem grows. Third parties using short strings SHOULD prefix them
with an organization-specific token (e.g., `"ACME_Inbox"`).

### 12.4 IANA Registration

IANA registration of service type strings is RECOMMENDED but NOT REQUIRED for v1.
Organizations that plan to widely distribute implementations of a custom service type
SHOULD pursue IANA registration to establish a canonical reference and prevent future
namespace collision. The A2A maintainers will maintain a non-normative community registry
at `https://agentidentityregistry.org/specs/air/service-types` as a lightweight alternative to
formal IANA registration.

---

## 13. Conformance Gate

### 13.1 Normative Requirement

**No implementation MAY claim v1 compliance, label itself as v1-conformant, or accept
v1 production traffic without first passing all canonical test vectors at
`/specs/air/v1/test-vectors.json`.**

This requirement is normative. It applies to every language implementation, every SDK,
every relay, and every agent that processes A2A v1 envelopes. The conformance gate is not
advisory; it is a condition of using the v1 label.

### 13.2 Test Vector Canonical Location

The normative conformance test vectors are published at:

```
https://agentidentityregistry.org/specs/air/v1/test-vectors.json
```

During the `draft-1` phase, the vectors are at:

```
https://agentidentityregistry.org/specs/air/draft-1/test-vectors.json
```

The vectors file is generated by `/specs/air/draft-1/generate_vectors.py` and covers the
20 input/output pairs defined in §5.5. The vectors MUST NOT be modified after v1
promotion; any correction requires a new `v1.x` release with an updated vector set at a
new URL.

### 13.3 Per-Language Conformance Harness Requirements

Conformance applies per language. The following harnesses are defined for v1:

| Language | Command | Harness location |
|----------|---------|-----------------|
| Rust | `cargo test --features conformance -p a2a-rs` | `~/SuperClaw/crates/a2a-rs/tests/conformance/` |
| Python | `pytest tests/conformance/` | `~/air-site/sdk/python/tests/conformance/` |

**New-language implementations** (TypeScript, Go, Java, or any other language) MUST:

1. Add a `tests/conformance/<lang>/` directory to the spec repository containing a
   self-contained conformance harness.
2. Run the harness against all 20 vectors and achieve zero failures.
3. Produce a passing CI run on that harness before the implementation may claim v1
   compliance or be merged into any production code path.

A new-language implementation that has not yet passed the conformance harness MUST label
itself as `draft-1-candidate` and MUST NOT advertise itself as v1-conformant.

### 13.4 Conformance Vector Coverage

The 20 vectors collectively enforce:

- Correct JCS key-sort order (Unicode code-point ordering, not locale-sensitive).
- Exact integer serialization for values at and above 2^53 (the u64 precision hazard
  described in §5.6).
- NFC string normalization (vectors v09 and v10 produce identical canonical bytes from
  NFC and NFD input respectively, confirming normalization was applied).
- Correct handling of all five body types (Offer, Counter, Accept, Decline, Withdraw).
- Correct `null` handling for `in_reply_to` (present-and-null vs. absent).
- Correct EdDSA signing and verification across independent key seeds.

### 13.5 Public Production Traffic

**Public production traffic SHOULD NOT use non-conformant implementations.** An
implementation that has not passed the conformance gate and is nonetheless deployed in
production introduces interoperability risk for the entire A2A ecosystem, because
other conformant implementations cannot rely on the correctness of its canonical-JSON
output or signature encoding. The maintainers of AIR reserve the right to publish a
list of known non-conformant implementations and to reject relay registrations from
agents whose implementations fail the gate.

---

## 14. Encryption Extension (Sealed-Box Mode)

This section specifies an OPTIONAL end-to-end encryption mode for A2A envelope bodies. It is a non-breaking `v1.x` extension (§11.2): it introduces one new body type (`encrypted`) and reuses the existing signing, transport, and verification machinery unchanged. Plain v1 envelopes remain signed-but-not-encrypted by default (§10.2, §10.6); implementations that require confidentiality from the relay operator SHOULD use this mode.

When sealed-box mode is used, the relay operator (and any network observer) sees only routing metadata (`id`, `from`, `to`, `timestamp`, `thread_id`, `nonce`) and the EdDSA signature; the application payload is encrypted such that only the recipient can read it.

> **Forward secrecy.** This mode provides confidentiality and sender authenticity but NOT forward secrecy: compromise of a recipient's long-term key allows decryption of previously recorded ciphertext. A ratcheting / MLS-based mode providing forward secrecy is future work (§10.6). This tradeoff matches what major consumer messengers (e.g., iMessage prior to 2024) shipped for years.

### 14.1 Cipher Suite

The sole suite defined by this extension is identified by the string `x25519-hkdf-sha256-chacha20poly1305` (`v: 1`):

- Key agreement: X25519 (Curve25519 ECDH), ephemeral-static.
- Key derivation: HKDF-SHA256.
- AEAD: ChaCha20-Poly1305 (IETF, RFC 8439; 96-bit nonce, 128-bit tag).

Implementations MUST reject an `encrypted` body whose `alg` is not exactly this string.

### 14.2 Key Derivation from the Ed25519 Identity Key

An agent's X25519 key pair is DERIVED from the Ed25519 identity key that AIR already publishes (§5); no additional key is registered or published.

- **Recipient public key (padlock).** Given the recipient's Ed25519 public key `P_ed` (resolved from the AIR DID document, §3), the X25519 public key is the Montgomery u-coordinate of `P_ed` via the standard birational map `u = (1 + y) / (1 − y) (mod p)` (libsodium `crypto_sign_ed25519_pk_to_curve25519`).
- **Own private key.** Given the 32-byte Ed25519 seed `s`, the X25519 private scalar is `clamp(SHA-512(s)[0:32])` where clamp clears bits 0–2 of the first byte, clears bit 255, and sets bit 254 (libsodium `crypto_sign_ed25519_sk_to_curve25519`).

Reusing one identity key for both signing and key agreement via this birational map is an accepted construction (used by `age` and Signal's XEdDSA).

### 14.3 Sealing (Sender)

To seal a cleartext body `B` for a recipient:

1. Generate a fresh ephemeral X25519 key pair `(e_priv, e_pub)` for THIS message only.
2. Compute the shared secret `Z = X25519(e_priv, P_x)` where `P_x` is the recipient's derived X25519 public key (§14.2).
3. Derive the AEAD key `K = HKDF-SHA256(ikm = Z, salt = "" (empty), info = "air-msg/e2e/v1" ∥ e_pub, L = 32)`. An empty salt is equivalent to HashLen zero bytes per RFC 5869.
4. Compute the plaintext as the NFC-normalized RFC 8785 (JCS) canonical UTF-8 encoding of `B` — the same canonicalization used for signing (§5).
5. Compute the AAD as `id ∥ 0x00 ∥ from ∥ 0x00 ∥ to ∥ 0x00 ∥ thread_id` (UTF-8, single NUL separators, no trailing NUL). Implementations MUST reject any of these four fields that is empty or contains a NUL byte, keeping the separator injective.
6. Encrypt: `C = ChaCha20Poly1305(key = K, nonce = N, plaintext, aad = AAD)`, where `N` is a fresh random 12-byte nonce; `C` is the ciphertext with the 16-byte Poly1305 tag appended.
7. Replace the envelope `body` with the encrypted body (§14.5), then sign the entire envelope per §5 (**encrypt-then-sign**).

Because each message derives a unique key (fresh ephemeral), nonce reuse across messages cannot weaken the AEAD.

### 14.4 Opening (Recipient)

A recipient MUST verify the EdDSA signature (§6) BEFORE attempting decryption. If `body.type == "encrypted"`:

1. Recompute the X25519 private scalar from the seed (§14.2).
2. Compute `Z = X25519(x_priv, e_pub)` where `e_pub` is `body.epk`.
3. Derive `K` exactly as in §14.3 step 3.
4. Reconstruct the AAD from the envelope's `id` / `from` / `to` / `thread_id` (§14.3 step 5).
5. Decrypt and verify the tag. A tag failure, an `alg` mismatch, a nonce that is not 12 bytes, or a ciphertext shorter than 16 bytes MUST be treated as a decryption failure; implementations MUST fail closed and MUST NOT process the body.
6. The recovered plaintext is the JCS-canonical encoding of the original body `B`.

### 14.5 The `encrypted` Body

```json
{
  "type": "encrypted",
  "alg": "x25519-hkdf-sha256-chacha20poly1305",
  "v": 1,
  "epk": "z6LS…",
  "nonce": "<base64url>",
  "ct": "<base64url>"
}
```

| Field | Encoding |
|-------|----------|
| `epk` | Ephemeral X25519 public key as multibase z-base58btc with multicodec `0xec 0x01` (a `did:key`-style `z6LS…` string). |
| `nonce` | The 12-byte AEAD nonce, base64url, unpadded. |
| `ct` | Ciphertext concatenated with the 16-byte Poly1305 tag, base64url, unpadded. |

Unlike the PascalCase negotiation body types (§4), the encrypted body's `type` is the lowercase string `encrypted`, signaling that it is a transport-layer wrapper rather than an application negotiation state.

The decrypted plaintext is itself a valid body (e.g. a negotiation body, §4) or an application payload; the encrypted body is an opaque wrapper and the inner type is not constrained by this extension.

### 14.6 Encrypt-then-Sign Ordering

The signature (§5) is computed over the envelope AFTER the body has been replaced with the encrypted body. Recipients therefore verify authenticity against the sender's AIR-published key BEFORE decrypting (§14.4). The AAD additionally binds the ciphertext to `(id, from, to, thread_id)`, preventing an encrypted body from being transplanted into a different envelope.

### 14.7 Conformance Vectors

Deterministic interop vectors (fixed recipient seed, ephemeral key, and nonce) are published at `/specs/air/draft-1/e2e-interop-vectors.json`. A conforming implementation MUST, for each vector, (a) reproduce the `expected` sealed body byte-for-byte when sealing with the given inputs, and (b) recover the original `body` when opening the `expected` sealed body with the recipient seed. The `body` field in each vector is an arbitrary opaque application payload used to exercise the cipher; per §14.5 the inner body type is not constrained by this extension and need not be one of the §4 negotiation bodies.

### 14.8 Security Considerations

- **No forward secrecy** (see the §14 preamble): out of scope for this mode.
- **Metadata is not hidden.** `from`, `to`, `timestamp`, and `thread_id` remain in cleartext for routing; only the body is confidential. Traffic analysis by the relay operator is possible.
- **Key reuse.** Deriving X25519 from the Ed25519 identity key is intentional and standard (§14.2); it does not weaken either primitive when used via the birational map.
- **Authenticated encryption.** The AAD and the outer signature jointly bind the ciphertext to a single envelope and a single authenticated sender, defeating cut-and-paste and surreptitious-forwarding attacks.
- **Numeric canonicalization caveat.** Because the plaintext is JCS-canonicalized (§14.3 step 4), a body containing an integer outside the IEEE-754 double-precision exact range (|n| > 2^53) MAY canonicalize to different bytes across implementations and fail to decrypt cross-language (the same hazard as §5.6). Bodies SHOULD restrict numeric values to the safe-integer range or encode large/precise quantities as strings (as the negotiation bodies already do for monetary amounts, §4).
