# Envoys Signature Extension for A2A

| Field   | Value                                          |
|---------|------------------------------------------------|
| URI     | `https://envoys.me/specs/signature/v1`         |
| Status  | Stable                                         |
| Version | 1.6.2                                          |
| Date    | 2026-06-09                                     |
| Type    | A2A Protocol Extension                         |

## 1. Abstract

This extension defines per-request cryptographic identity for Agent2Agent
(A2A) interactions. Each request is signed with the sender's Ed25519
private key using RFC 9421 HTTP Message Signatures. The `keyid` parameter
resolves to a URL that returns the sender's public key, allowing any
recipient to verify the signature without prior trust setup.

The extension closes three gaps the A2A v1.0 specification leaves to
implementers:

1. **Per-request body integrity** — A2A v1.0 relies on TLS alone; once a
   request is decrypted at the application layer, nothing binds the
   payload to the claimed sender.
2. **Cryptographic sender identity** — A2A's `securitySchemes` cover
   transport-layer auth (OAuth, OIDC, mTLS, API keys) but do not assert
   *which agent* sent a given message.
3. **Replay protection** — A2A v1.0 has no built-in replay defenses.

## 2. Conventions

The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be
interpreted as described in RFC 2119.

The extension URI `https://envoys.me/specs/signature/v1` is stable and
versioned. Breaking changes will be published at a new path
(`.../signature/v2`). Implementations MUST treat the URI as an opaque
identifier.

## 3. Agent Card declaration

A server requiring this extension MUST declare it in its A2A Agent Card
(typically served at `/.well-known/agent.json`):

```json
{
  "name": "Echo Agent",
  "url":  "https://echo.example.com",
  "version": "1.0.0",
  "capabilities": { "streaming": false, "extensions": ["https://envoys.me/specs/signature/v1"] },
  "securitySchemes": {
    "envoysSignature": {
      "type": "extension",
      "extensionUri": "https://envoys.me/specs/signature/v1",
      "description": "RFC 9421 Ed25519 signatures with self-resolving keyid."
    }
  },
  "security": [{ "envoysSignature": [] }]
}
```

The `securitySchemes` entry uses `type: "extension"` to signal that the
scheme is defined outside the A2A core security types. Conformant clients
that do not understand this extension MUST treat the agent as requiring
authentication they cannot satisfy and MUST NOT proceed silently.

## 4. Client request requirements

A client invoking an A2A method on an extension-requiring server MUST:

### 4.1 Include the extension URI in the negotiation header

```
A2A-Extensions: https://envoys.me/specs/signature/v1
```

Multiple extensions MAY be combined with comma separation per A2A §3.2.6.

### 4.2 Sign the HTTP request per RFC 9421

The signature MUST cover the following derived components:

| Component         | Required when |
|-------------------|---------------|
| `@method`         | Always        |
| `@path`           | Always        |
| `content-digest`  | Always        |
| `@authority`      | OPTIONAL — RECOMMENDED when the sender knows the target authority |

When `@authority` (RFC 9421 §2.2.3) is covered, the component value is
the target host (plus any non-default port), lowercased, and is placed
between `@method` and `@path` in the component order. Binding the
authority scopes the signature to one receiving service: without it, a
signature minted for `POST /rpc` on one host verifies for the same
method and path on any other host within the timestamp window — and a
per-receiver replay cache cannot catch the relay, because each receiver
sees the signature for the first time. Senders SHOULD cover
`@authority` whenever the target host is known at signing time. It
remains OPTIONAL in this version for compatibility: 1.5.x verifiers
fail closed on signatures covering components they do not reconstruct,
so senders need a signal that the receiver is 1.6-capable before
emitting it.

The `content-digest` header MUST be present on every signed request and
MUST be a digest of the literal request body bytes structured per RFC
9530. Requests with no body sign the digest of zero bytes (e.g.
`sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:`). Signing
`content-digest` uniformly across all requests keeps the signature base
shape consistent — verifiers reconstruct the same core base regardless
of method or body presence.

The digest algorithm is `sha-256` by default. Senders MAY auto-promote
to `sha-512` for bodies of 4096 bytes or larger; receivers MUST accept
both algorithms (§5.3).

```
Content-Digest: sha-256=:<base64(sha256(body))>:
```

or

```
Content-Digest: sha-512=:<base64(sha512(body))>:
```

### 4.3 Signature parameters

The `Signature-Input` header MUST include:

| Parameter | Value                                                                          |
|-----------|--------------------------------------------------------------------------------|
| `keyid`   | Absolute URL resolving to a public key document (see §6)                       |
| `created` | Unix timestamp (seconds) at which the signature was created                    |
| `nonce`   | Random per-request opaque token. SHOULD be ≥128 bits of entropy (e.g. base64url of 16 random bytes). |
| `tag`     | OPTIONAL. RFC 9421 §2.3 sf-string disambiguating signing purpose.              |

The signature label SHOULD be `sig1`. The signing algorithm is implied
by the resolved key type (Ed25519).

The `nonce` defends against replay even when two distinct requests would
otherwise produce identical signature inputs (same method, path, body,
and second-resolution timestamp). Servers use the nonce alongside other
parameters to populate the deduplication cache described in §7.

The OPTIONAL `tag` parameter (RFC 9421 §2.3) lets senders disambiguate
signing purpose under a single `keyid` — for example `"a2a-message"`,
`"task"`, `"heartbeat"`, or `"delegation"`. Verifiers MAY enforce that
the tag matches the expected context for a given endpoint and reject
mismatched signatures. The absence of a `tag` parameter is equivalent
to `tag="a2a-message"` for downstream compatibility, so signers that do
not emit it remain interoperable with verifiers that enforce a tag.
sf-string serialization MUST escape backslash and DQUOTE per RFC 8941
§3.3.3.

### 4.4 Example

```
POST / HTTP/1.1
Host: echo.example.com
Content-Type: application/json
A2A-Extensions: https://envoys.me/specs/signature/v1
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@method" "@path" "content-digest");keyid="https://envoys.me/agents/alice@your-handle.envoys.me";created=1745596800;nonce="qLZ8dVz3oF7eY4wT9uXKjg"
Signature: sig1=:dGVzdHNpZ25hdHVyZQ==:

{"jsonrpc":"2.0","id":"1","method":"message/send","params":{...}}
```

## 5. Server verification requirements

A server implementing this extension MUST:

### 5.1 Reject unsigned requests

Requests missing `Signature-Input` or `Signature` headers MUST be
rejected with HTTP 401 and a JSON-RPC error code `-32001`.

### 5.2 Verify timestamp freshness

The `created` timestamp MUST be within an acceptable window of the
server's current time. The default window is:

- No more than 300 seconds (5 minutes) old, AND
- No more than 30 seconds in the future (clock skew tolerance).

Servers MAY choose tighter windows. Requests outside the window MUST be
rejected with HTTP 401.

### 5.3 Verify body integrity

If the request has a body, the server MUST recompute the digest of the
received bytes using the algorithm declared in the `Content-Digest`
header and compare it byte-for-byte to the header value. Receivers MUST
accept both `sha-256` and `sha-512` digests; the algorithm prefix in
the header value selects which to compute. Digests in any other
algorithm MUST be rejected with HTTP 401. Mismatches MUST be rejected
with HTTP 401 before any signature verification is attempted.

### 5.4 Resolve the keyid

The `keyid` MUST be an absolute URL. The server fetches it via HTTP GET
and parses the response per §6. Verifiers SHOULD send
`Accept: application/did+json, application/json` so resolvers that can
emit either shape can select the appropriate one. Servers SHOULD cache
resolved keys for no longer than 5 minutes by default to bound exposure
to a stale key after a revocation.

Servers MAY restrict accepted `keyid` URLs to a whitelist of issuers.

### 5.5 Verify the signature

Before reconstructing the signature base, the verifier MUST check that
the covered components listed in `Signature-Input` include `@method`
and `@path`, and — when the request has a body — `content-digest`.
Signatures whose component list omits a required component MUST be
rejected with HTTP 401 even if cryptographically valid over the
components they do cover. Without this check, a signature that omits
`content-digest` leaves the body unauthenticated: §5.3 only proves the
body matches the `Content-Digest` header, and it is the signature's
coverage of that header that binds the body to the signer. An attacker
who substitutes both the body and a matching header would otherwise
pass.

When the covered components include `@authority`, the verifier MUST
reconstruct its value from the receiver's own identity: its configured
authority, or the request's `Host` header only when the server
validates `Host` against the set of authorities it actually serves
(as name-based virtual hosting does implicitly). An unvalidated `Host`
header is sender-controlled and MUST NOT be used — a relaying attacker
can simply preserve the original host value, defeating the binding.
The value is lowercased before use. A verifier that cannot determine
its own authority MUST reject signatures covering `@authority`.

The signature base is reconstructed per RFC 9421 from the components
listed in `Signature-Input`. The Ed25519 verification MUST be
constant-time.

Failed verification MUST return HTTP 401 with JSON-RPC error code
`-32001`.

## 6. Public key resolution

The `keyid` URL MUST, on a successful HTTP 200 response, return a JSON
document in one of two shapes. Resolvers MAY serve either shape;
verifiers MUST accept both. The response `Content-Type` header selects
which shape is being served:

| Content-Type                                | Response shape                          |
|---------------------------------------------|-----------------------------------------|
| `application/did+json`                      | W3C DID Document (§6.1)                 |
| `application/json` or any other JSON type   | Envoys-native object (§6.2)             |

If the `Content-Type` header is absent or generic, verifiers SHOULD
fall back to structural detection: a response containing a
`verificationMethod` array is parsed as a DID Document; a response
containing a top-level `public_key` field is parsed as the Envoys-native
shape; anything else MUST be rejected as malformed.

The two shapes are wire-format equivalents — they carry the same
Ed25519 public key under different envelopes. The `public_key` MUST be
a PEM-encoded SubjectPublicKeyInfo (SPKI) structure containing an
Ed25519 key per RFC 8410, or its `publicKeyJwk` (OKP / Ed25519)
equivalent. Implementations MAY reject keys of any other algorithm.

Resolvers that serve both shapes SHOULD honor the verifier's `Accept`
header (§5.4) to select between them. The reference resolver
(`envoys.me`) serves the Envoys-native shape (§6.2) by default and
switches to the DID Document shape (§6.1) when the request includes
`application/did+json` in its `Accept` header with non-zero quality
value. Wildcard `*/*` and absent `Accept` headers select the
Envoys-native default. A resolver MAY serve only one shape — verifier
accept-both is the conformance requirement; resolver serve-both is
recommended but not required.

The resolution endpoint SHOULD return HTTP 404 for unknown or revoked
agents. Verifiers MUST treat 4xx and 5xx responses as verification
failures.

### 6.1 W3C DID Document shape

When served with `Content-Type: application/did+json`, the response
MUST be a W3C DID Document with at least one `verificationMethod`
entry whose `type` begins with `Ed25519` and whose key material is
carried as `publicKeyJwk` with `kty: "OKP"` and `crv: "Ed25519"`.

```json
{
  "@context":         ["https://www.w3.org/ns/did/v1"],
  "id":               "https://envoys.me/agents/alice@acme.envoys.me",
  "verificationMethod": [
    {
      "id":           "https://envoys.me/agents/alice@acme.envoys.me#key-1",
      "type":         "Ed25519VerificationKey2020",
      "controller":   "https://envoys.me/agents/alice@acme.envoys.me",
      "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
    }
  ],
  "authentication":  ["https://envoys.me/agents/alice@acme.envoys.me#key-1"],
  "assertionMethod": ["https://envoys.me/agents/alice@acme.envoys.me#key-1"]
}
```

The DID Document `id` and `controller` MAY be the keyid URL itself (as
shown above) or a `did:web` form mapping to the keyid URL — both are
W3C DID Core 1.0 conformant. The reference resolver uses the keyid
URL directly so that the document's identity matches the URL the
verifier just fetched.

The OPTIONAL `authentication` and `assertionMethod` arrays SHOULD
reference the verification method by its `id`. Verifiers do not rely
on these arrays for signature verification under this extension (the
keyid resolves to a single signing key by construction), but their
presence signals the key is usable for both peer authentication and
issued assertions — the two operations this extension covers.

Verifiers select the first verification method satisfying the above
constraints. Methods carrying the key as `publicKeyMultibase` or
`publicKeyBase58` are not required to be supported in v1; verifiers
encountering only those forms MUST surface a clear unsupported-encoding
error rather than failing silently.

### 6.2 Envoys-native shape

When served with any other JSON content type, the response MUST be a
JSON object with at minimum:

```json
{
  "address":    "alice@your-handle.envoys.me",
  "public_key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n"
}
```

The `public_key` MUST be a PEM-encoded SubjectPublicKeyInfo (SPKI)
structure per §6.

Servers MAY include one of the optional verification fields described
below to surface the claim that backs an address. At most one of these
fields is non-null in a single response, since handle-namespace and
custom-domain addresses are mutually exclusive.

`verified_handle` is set when the account that owns the address has
completed a handle attestation per §12.3 (`name@<handle>.envoys.me`
addresses). The value is an object with a `domain` (the attested
real-world domain) and `verified_at` (Unix milliseconds).

`verified_domain` is set when the address is on a custom domain
(`name@example.com`) and that domain has been DNS-verified by the
account that owns it. The value has the same shape as
`verified_handle` (`domain`, `verified_at`). The `domain` echoes the
host portion of the address.

Servers MAY additionally include a boolean `pop_verified`, set to
`true` when the registrant proved possession of the private key
matching `public_key` at registration time (§12.6). A `false` value
(or absence) means the binding was asserted but not
possession-proven — the registrant may still control the key; the
registry simply has no proof.

```json
{
  "address":         "alice@acme.envoys.me",
  "public_key":      "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
  "pop_verified":    true,
  "verified_handle": { "domain": "acme.com",   "verified_at": 1714000000000 },
  "verified_domain": null
}
```

```json
{
  "address":         "bot@acme.com",
  "public_key":      "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
  "pop_verified":    false,
  "verified_handle": null,
  "verified_domain": { "domain": "acme.com", "verified_at": 1714000000000 }
}
```

Verifiers MUST NOT treat the absence of `verified_handle`,
`verified_domain`, or `pop_verified` as a verification failure. All
three are additional identity claims, not prerequisites.

### 6.3 Interoperation with other key-resolution patterns

A resolver that natively publishes keys via `did:web`, JWKS endpoints,
or other URL-addressable formats interoperates by either:

1. Serving the equivalent shape at the `keyid` URL itself (§6.1
   or §6.2), or
2. Hosting an Envoys-native bridge that produces §6.2 output by
   resolving the foreign format internally.

Reference implementations include `Envoys.resolveKeyFromKeyid()` and
`Envoys.resolveDidWeb()` in `@envoys/sdk`. Both produce PEM SPKI suitable
for direct verification under §5.5.

## 7. Replay protection

Timestamp-window enforcement (§5.2) reduces but does not eliminate
replay risk within the window. Servers MUST additionally maintain a
deduplication cache keyed by `(keyid, created, signature)` (or
equivalently `(keyid, nonce)`) for the duration of the acceptance
window. The cache MAY be in-memory and need not be durable; a single
process per origin is sufficient when traffic is sticky.

A request whose key is already present in the cache MUST be rejected
per §5.1 with HTTP 401 and JSON-RPC code `-32001`.

The `nonce` parameter (§4.3) ensures that two requests with otherwise
identical signature inputs still produce distinct signatures, so the
cache key is reliably unique even at high request rates.

## 8. Signature scope and layering

Per-message signatures defined by this extension are point-in-time. A
signature attests *"this specific message body, sent at this specific
moment by this specific agent"* — it does not attest to the lifecycle
of any task the message belongs to, to the authority under which the
sending agent operates, or to the continuity of the agent's identity
across key rotations or session resumptions.

### 8.1 Task resumption

When an A2A task is resumed (the `task_id` is unchanged but a new
message is issued under the resumed session), the new message MUST
carry a fresh signature with new `created` and `nonce` parameters.
Signatures from prior resumption windows MUST NOT be replayed even if
the `task_id` is the same; the deduplication cache (§7) and timestamp
window (§5.2) apply per message, not per task.

Identity continuity across resumption (where required by an
application — e.g., proving the same agent is driving the resumed
task) is the responsibility of higher-layer envelopes such as
delegation receipts or task-scoped attestations. Such envelopes MAY
reference the per-message wire signature as their substrate, but the
wire signature itself is not the right place to encode lifecycle
state.

### 8.2 Per-receipt-type attribution

A general principle for the broader identity stack composing around
this wire format: trust-layer attribution is most useful when made per
envelope-kind (receipt-type) rather than per protocol or per vendor.
The same protocol can ship envelopes that attribute to different
layers, and a single vendor's specification can span multiple layers
once its full primitive set is considered.

This is descriptive, not normative — protocols composing around this
wire format publish their own envelope shapes under their own
specifications. As an illustration, the Envoys-spec primitives defined
in this document attribute as follows:

| Envoys primitive                          | Spec ref      | Layer            |
|-------------------------------------------|---------------|------------------|
| Per-message RFC 9421 signature            | §4–§5         | wire signature   |
| `keyid` URL resolution                    | §6            | wire signature   |
| `verified_handle` (DNS-TXT attestation)   | §12.3         | identity claims  |
| `verified_domain` (DNS-TXT attestation)   | §6.2          | identity claims  |
| `/agents/:address/key-history`            | §12.1         | continuity       |
| `/agents/revocations` feed                | §12.2         | continuity       |
| Agent Card JWS                            | §9            | envelope         |

Implementers building higher-layer envelopes that compose with this
wire signature SHOULD attribute their own primitives by envelope-kind
in their own specifications, so error routing, retry behavior, and
trust composition can be reasoned about consistently across the stack.
Related work on the composability of these layers includes the A2A
identity-framework discussion threads (`a2aproject/A2A#1725`,
`a2aproject/A2A#1786`, `a2aproject/A2A#1829`, `a2aproject/A2A#1850`).

## 9. Agent Card signing (optional)

Servers MAY publish a JWS-signed copy of their Agent Card alongside the
unsigned form, using JWS Compact Serialization with `alg=EdDSA` per
RFC 8037:

- Header: `{"alg":"EdDSA","typ":"JWT","kid":"<keyid url>"}`
- Payload: the Agent Card JSON (canonicalization not required for
  Compact Serialization since the payload bytes are protected).
- Signature: Ed25519 over `<header>.<payload>` (base64url-encoded
  signing input).

The signed form SHOULD be served at `/.well-known/agent.json.jws` with
content type `application/jose`. Clients MAY prefer the signed form when
available. The `kid` in the JWS header is resolved per §6.

## 10. Error codes

| HTTP | JSON-RPC | Meaning                                      |
|------|----------|----------------------------------------------|
| 401  | -32001   | Unsigned, malformed, or invalid signature    |
| 401  | -32001   | Timestamp out of acceptance window           |
| 401  | -32001   | Content-Digest mismatch                      |
| 401  | -32001   | keyid resolution failed                      |

The error message body SHOULD be a JSON-RPC 2.0 error envelope:

```json
{
  "jsonrpc": "2.0",
  "id":      "<original request id>",
  "error":   { "code": -32001, "message": "Unauthorized: <reason>" }
}
```

Servers SHOULD include enough detail in the message to aid debugging
without disclosing keys, internal paths, or unrelated state.

## 11. Security considerations

### 11.1 keyid as a side channel

A `keyid` is fetched by the verifier from a URL the sender controls.
Verifiers SHOULD apply normal SSRF protections, treat the resolution
endpoint as untrusted, and limit response sizes (a public key fits in
< 1 kB; a DID Document carrying a single Ed25519 key fits in < 2 kB).

### 11.2 Algorithm agility

This version pins Ed25519 for signing and accepts SHA-256 and SHA-512
for body digests. Future versions MAY add signing algorithms but MUST
do so via a new extension URI rather than overloading this one. This
guarantees that an implementation supporting `https://envoys.me/specs/signature/v1`
never silently accepts a weaker signing algorithm.

### 11.3 Key rotation

Public key resolution SHOULD return the *currently active* public key.
Implementations supporting overlapping rotations MAY return both old and
new keys; verifiers SHOULD attempt all returned keys and accept any
match. This extension does not specify a rotation overlap window.

### 11.4 Query strings are not covered

RFC 9421 `@path` excludes the query string. Under this extension,
request semantics MUST travel in the signed body (covered by
`content-digest`); receivers MUST NOT derive request semantics from
unsigned query parameters. A future minor version may add OPTIONAL
`@query` coverage for transports that carry semantics in the query
string.

### 11.5 Out-of-scope: streaming responses

This extension covers the integrity of a single HTTP request. It does
not specify how to authenticate per-event payloads in `message/stream`
SSE responses. Implementations supporting streaming MUST document any
additional authentication mechanism separately.

## 12. Identity transparency and key pinning

A successful signature verification proves only that the request was
authored by the holder of the private key registered to the `keyid` URL
at signing time. It does NOT prove the holder is the entity the
verifier intends to communicate with. Verifiers SHOULD layer additional
checks per the following subsections to detect silent identity takeover
(account compromise → key rotation by an attacker) and to anchor trust
beyond first-come-first-served handle ownership.

### 12.1 Public key history

Servers MAY expose an append-only history endpoint for any address:

```
GET /agents/{address}/key-history
```

The response is a JSON document of every public key ever bound to the
address, oldest first:

```json
{
  "address":     "alice@acme.envoys.me",
  "current_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
  "revoked":     false,
  "history": [
    { "public_key": "...", "valid_from": 1714000000000, "valid_until": 1714086400000, "reason": "register" },
    { "public_key": "...", "valid_from": 1714086400000, "valid_until": null,          "reason": "rotation" }
  ]
}
```

Each row represents one validity period. `valid_until` is `null` while
the key is current. `current_key` is `null` when `revoked` is `true`.
The `reason` field is `register` for the first row and `rotation` for
each subsequent row.

This endpoint is unauthenticated by design — public keys and rotation
timestamps are not sensitive, and verifiers must be able to audit
without requesting credentials.

### 12.2 Revocation feed

Servers MAY expose a CRL-style feed of all addresses whose key validity
period has closed (rotation or revocation) since a given timestamp:

```
GET /agents/revocations?since={unix_ms}
```

```json
{
  "since":  1714000000000,
  "now":    1714086400000,
  "events": [
    { "address": "alice@acme.envoys.me", "ts": 1714086400000, "event": "rotation" },
    { "address": "bob@nope.envoys.me",   "ts": 1714080000000, "event": "revocation" }
  ]
}
```

The `event` field is `rotation` if a later validity period exists for
the same agent, or `revocation` otherwise. Verifiers SHOULD poll this
endpoint at a cadence appropriate to their cache TTL (hourly is
sufficient for a 5-minute key cache) and invalidate any cached
public-key pin whose address appears in the events list.

### 12.3 Domain attestation (verified handles)

Servers MAY allow account owners to prove control of a real-world
domain via a DNS TXT record, anchoring the account's handle to that
domain. The mechanism MUST NOT alter the cryptographic identity primitive
(`address` → `keyid` → `public_key` resolution) but provides an
additional identity claim surfaced via `verified_handle` (§6.2).

Recommended record:

| Field | Value                                        |
|-------|----------------------------------------------|
| Type  | `TXT`                                        |
| Host  | `_envoys-handle.<domain>`                    |
| Value | `envoys-handle-verify=<server-issued-token>` |

When the account owner subsequently changes their handle, the server
MUST invalidate the attestation — the proof was bound to the prior
handle, not the new one.

### 12.4 Public key pinning (RECOMMENDED)

Verifiers SHOULD record the first-seen public key per `address`
(observed via successful verification) and reject subsequent requests
whose `keyid` resolves to a different public key. This catches the
class of attacks where an attacker compromises the account and rotates
the key — the resolved key validates the attacker's signature, but the
pin disagrees.

Pinning MUST be paired with a documented reset procedure (e.g.
operator-confirmed rotation invalidates the pin) so legitimate
rotations do not produce permanent verification failures. The §12.2
revocation feed is the recommended trigger: when an address appears in
the feed, its pin SHOULD be cleared.

### 12.5 Sender allowlist (RECOMMENDED for closed deployments)

Verifiers operating with a known, finite set of acceptable
counterparties SHOULD enforce an allowlist of addresses (or `keyid`
URLs) and reject cryptographically-valid signatures from senders not
on the list. The allowlist SHOULD be bootstrapped out-of-band — DNS
record, signed contract, manual handshake — and not from the signature
flow itself.

> Verified is not the same as trusted. A successful signature check
> proves identity, not authorization. The allowlist establishes who is
> permitted; the signature establishes who is asking.

### 12.6 Proof-of-possession at registration (OPTIONAL)

Registration binds an address to a public key, but submitting a public
key only proves the registrant has *seen* it — not that they control
the matching private key. To make the binding possession-proven, a
registrant MAY include two extra fields when registering an agent:

- `pop_created` — Unix timestamp (seconds) at which the proof was
  created. Registries MUST reject proofs older than 300 seconds or
  more than 30 seconds in the future.
- `pop` — base64 Ed25519 signature, made with the private key matching
  the submitted `public_key`, over the UTF-8 bytes of:

```
envoys-pop:v1:<pop_created>:<public_key PEM exactly as submitted>
```

A registry receiving these fields MUST verify the signature against
the submitted public key and reject the registration (HTTP 400) if it
does not verify or the timestamp is outside the window — an invalid
proof signals a confused or hostile client, not a degraded one.
Registrations without the fields remain valid and record
`pop_verified: false`.

The result is surfaced to verifiers as the `pop_verified` field on
Envoys-native resolution responses (§6.2). Verifiers performing
first-contact pinning (§12.4) MAY weight possession-proven bindings
above asserted ones when deciding how much to trust a first-seen key.
The reference SDKs generate the keypair locally and attach the proof
automatically, so SDK-registered agents are possession-proven by
default.

## 13. References

- RFC 9421 — HTTP Message Signatures
- RFC 9530 — Digest Fields
- RFC 8032 — Edwards-Curve Digital Signature Algorithm (EdDSA)
- RFC 8037 — CFRG Elliptic Curve Diffie-Hellman and Signatures in JOSE
- RFC 8410 — Algorithm Identifiers for Ed25519, Ed448, X25519, X448
- RFC 8941 — Structured Field Values for HTTP
- W3C DID Core 1.0 — `https://www.w3.org/TR/did-core/`
- A2A Protocol Specification v1.0 — `https://a2a-protocol.org/v1.0/specification/`

## 14. Test Vectors

These vectors are reproducible: running the generation script in
`scripts/gen-test-vectors.mjs` against an Ed25519 implementation
should produce the exact `Signature` values shown below. The keypair
is RFC 8032 §7.1 Test 1, fixed for cross-implementation verification.

### Public key (PEM SPKI)

```
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
-----END PUBLIC KEY-----
```

### Private key (raw seed, hex; for reproducing only)

```
9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
```

*Source: RFC 8032 §7.1 Test 1.*

### Vector 1 — GET request, no body

**Inputs:**
```
method:  GET
path:    /api/health
body:    (empty)
keyid:   https://envoys.me/agents/test@rfc8032-vec1.example
created: 1714000000
nonce:   "AAECAwQFBgcICQoLDA0ODw"
```

**Computed `Content-Digest` header:**
```
sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:
```

**Signature base:**
```
"@method": GET
"@path": /api/health
"content-digest": sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:
"@signature-params": ("@method" "@path" "content-digest");keyid="https://envoys.me/agents/test@rfc8032-vec1.example";created=1714000000;nonce="AAECAwQFBgcICQoLDA0ODw"
```

**Computed `Signature-Input` header value (for `sig1`):**
```
sig1=("@method" "@path" "content-digest");keyid="https://envoys.me/agents/test@rfc8032-vec1.example";created=1714000000;nonce="AAECAwQFBgcICQoLDA0ODw"
```

**Computed `Signature` header value:**
```
sig1=:XUpjUHt36NbHgAZrQkFY2fSNUR19tgmRlGO1dBhaZDgBv4wb55qgJf2buv3wgnTYwtT+1sH2jzSbcgG6FLGKCA==:
```

### Vector 2 — POST request, JSON body

**Inputs:**
```
method:  POST
path:    /api/task
body:    {"task":"summarize","url":"https://example.com/doc"}
keyid:   https://envoys.me/agents/test@rfc8032-vec1.example
created: 1714000060
nonce:   "EBESExQVFhcYGRobHB0eHw"
```

**Computed `Content-Digest` header:**
```
sha-256=:MKfdDhv01pOYGoZ8VKY5CNdevySMUL8MqvJxVJaaWu0=:
```

**Signature base:**
```
"@method": POST
"@path": /api/task
"content-digest": sha-256=:MKfdDhv01pOYGoZ8VKY5CNdevySMUL8MqvJxVJaaWu0=:
"@signature-params": ("@method" "@path" "content-digest");keyid="https://envoys.me/agents/test@rfc8032-vec1.example";created=1714000060;nonce="EBESExQVFhcYGRobHB0eHw"
```

**Computed `Signature-Input` header value (for `sig1`):**
```
sig1=("@method" "@path" "content-digest");keyid="https://envoys.me/agents/test@rfc8032-vec1.example";created=1714000060;nonce="EBESExQVFhcYGRobHB0eHw"
```

**Computed `Signature` header value:**
```
sig1=:i5tKcOHKhRTCztR2cazuzNAg9rPiRf47MKTOGve92Rs43gNmltuN5LVScedR6C08MGsQykMc7txJ21KCG8SEBQ==:
```

### Vector 3 — POST request, empty JSON body

**Inputs:**
```
method:  POST
path:    /api/echo
body:    {}
keyid:   https://envoys.me/agents/test@rfc8032-vec1.example
created: 1714000120
nonce:   "ICEiIyQlJicoKSorLC0uLw"
```

**Computed `Content-Digest` header:**
```
sha-256=:RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=:
```

**Signature base:**
```
"@method": POST
"@path": /api/echo
"content-digest": sha-256=:RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=:
"@signature-params": ("@method" "@path" "content-digest");keyid="https://envoys.me/agents/test@rfc8032-vec1.example";created=1714000120;nonce="ICEiIyQlJicoKSorLC0uLw"
```

**Computed `Signature-Input` header value (for `sig1`):**
```
sig1=("@method" "@path" "content-digest");keyid="https://envoys.me/agents/test@rfc8032-vec1.example";created=1714000120;nonce="ICEiIyQlJicoKSorLC0uLw"
```

**Computed `Signature` header value:**
```
sig1=:m2besJKk6Q0MIwFoTENobvvHxFan1fUTv7bzY4EB6OjfIlktqwKa7r/Ab0tDDWFGjQ0CbALgvWGcQfzDr/GeBQ==:
```

Vectors 1–3 remain byte-stable across the 1.4.0 → 1.5.0 bump: no
component, parameter, or canonicalization change was introduced that
affects their signature base. Cross-implementation fixtures pinned to
these vectors do not need to re-pin.

## 15. Changelog

- **1.6.2** (2026-06-09) — Two security-considerations clarifications
  from external review of the A2A example-extension profile
  (a2a-samples PR #592). (1) §5.5 `@authority` reconstruction: an
  unvalidated `Host` header is sender-controlled and MUST NOT be used;
  acceptable sources are the verifier's configured authority, or
  `Host` validated against the authorities the server actually serves.
  (2) New §11.4: RFC 9421 `@path` excludes the query string — request
  semantics MUST travel in the signed body, and receivers MUST NOT
  derive semantics from unsigned query parameters; OPTIONAL `@query`
  coverage noted as future work. Former §11.4 (streaming) renumbered
  to §11.5. Prose-only — no wire-format change, §14 vectors
  byte-stable.

- **1.6.1** (2026-06-09) — Document the `pop_verified` field on
  Envoys-native resolution responses (§6.2, optional, additive) and
  add §12.6 specifying the proof-of-possession registration mechanism
  behind it (`pop` + `pop_created`, Ed25519 over
  `envoys-pop:v1:<ts>:<pem>`, ±300s/30s window). The field and
  mechanism shipped operationally with the 1.6.0 release; this patch
  brings the spec text in line so third-party verifiers can interpret
  the signal. Prose-only — no wire-format change, §14 vectors
  byte-stable, fixture manifest pin unchanged.

- **1.6.0** (2026-06-09) — Two verifier-strengthening changes. (1)
  **Component-coverage enforcement (§5.5):** verifiers MUST reject
  signatures whose covered components omit `@method`/`@path`, or omit
  `content-digest` when the request has a body — closing a digest
  downgrade where an attacker substitutes the body and a matching
  `Content-Digest` header under a signature that never covered the
  digest. Conformant 1.5.x signers always cover all three, so
  legitimate traffic is unaffected. (2) **OPTIONAL `@authority`
  binding (§4.2, §5.5):** senders SHOULD additionally cover RFC 9421
  `@authority` (lowercased target host, ordered between `@method` and
  `@path`) when the target host is known, scoping the signature to one
  receiving service and closing a cross-host relay within the
  timestamp window; verifiers MUST reconstruct it from their own
  configured authority or the `Host` header. §14 Vectors 1–5
  byte-stable (none cover `@authority`); new positive/negative
  fixtures `vec-7` (`@authority`) and `vec-n4` (digest-omission
  rejection) live in the `envoys-rfc9421` fixture set. Also fixes the
  header version table, which still read 1.5.0 after the 1.5.1 patch.
  Bumps `@envoys/sdk` to 0.9.0 and `envoys` (Python) to 0.2.0.

- **1.5.1** (2026-05-18) — Reconcile §4.2 "Required when" table with §14
  Vector 1: `content-digest` is now stated as **Always** required in the
  prose table (matching the long-standing §14 vectors and the
  `gen-fixtures.mjs` reference generator). For requests with no body the
  digest is computed over zero bytes (`sha-256` of empty =
  `47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=`). No on-the-wire change
  for any §14 vector — the fixtures already specified this behavior.
  Senders that conditionally omitted `content-digest` for no-body
  requests (a divergence permitted by ambiguous 1.5.0 prose) MUST update
  to always emit it. Verifiers that loop over the `Signature-Input`
  component list dynamically remain conformant unchanged. Bumps
  `@envoys/sdk` to 0.8.0.

- **1.5.0** (2026-05-13) — Add `tag` parameter to §4.3 as OPTIONAL
  RFC 9421 §2.3 sf-string for disambiguating signing purpose; absence
  is equivalent to `tag="a2a-message"` for backward compatibility. Add
  SHA-512 acceptance in §4.2 + §5.3 (`Content-Digest`); senders MAY
  auto-promote at body size ≥4096 bytes, receivers MUST accept both
  `sha-256` and `sha-512`. Formalize dual-shape `keyid` resolution in
  §6: verifiers MUST accept either an `application/did+json` W3C DID
  Document (§6.1) or the Envoys-native `{ address, public_key }`
  object (§6.2) returned at the `keyid` URL. Document the
  Accept-based content negotiation pattern resolvers SHOULD use when
  serving both shapes — the reference resolver defaults to Envoys-
  native and switches to DID Document on explicit
  `Accept: application/did+json`. Promote former §7.1 task-resumption
  to §8 "Signature scope and layering," now also documenting
  per-receipt-type attribution as a descriptive principle with the
  Envoys-spec primitive-to-layer mapping. Subsequent sections
  renumbered: §8 Agent Card signing → §9; §9 Error codes → §10;
  §10 Security considerations → §11; §11 Identity transparency and
  key pinning → §12; §12 References → §13; §13 Test Vectors → §14;
  §14 Changelog → §15. References to §11.x updated to §12.x
  throughout. Vectors 1–3 remain byte-stable. Backward-compatible —
  verifiers and signers conformant to 1.4.0 remain conformant to
  1.5.0.

- **1.4.0** (2026-05-10) — Add §7.1 covering task-resumption signature
  semantics: per-message signatures are point-in-time and MUST re-sign on
  resume with fresh `created`/`nonce`; task-level identity continuity
  belongs to higher-layer envelopes. Add §6 paragraph noting interop with
  other URL-based key-resolution patterns (`did:web` DID Documents, JWKS
  endpoints) via the `keyid` URL contract; reference `Envoys.resolveDidWeb()`
  in `@envoys/sdk` as a bridge implementation. Add §13 Test Vectors using
  the RFC 8032 §7.1 Test 1 keypair so implementers can verify their
  RFC 9421 + Ed25519 plumbing produces the exact signatures listed.
  Existing §13 Changelog renumbered to §14. Backward-compatible — no
  wire-format changes from 1.3.0; verifiers and signers conformant to
  1.3.0 remain conformant to 1.4.0.

- **1.3.0** (2026-05-09) — Document the `verified_domain` field on
  resolution responses (§6, optional). Parallel to `verified_handle`
  but covers custom-domain addresses (`name@example.com`) where no
  handle exists. Both fields share the same shape; at most one is
  non-null per response. Backward-compatible — verifiers that ignore
  the new field remain conformant to 1.2.0.

- **1.2.0** (2026-05-06) — Document the `verified_handle` field on
  resolution responses (§6, optional). Add §11 covering identity
  transparency and key pinning: `/key-history` (§11.1) and
  `/revocations` (§11.2) endpoints, the DNS-TXT handle-attestation
  mechanism (§11.3), pinning recommendation (§11.4), and allowlist
  recommendation (§11.5). All additions are backward-compatible —
  servers that don't expose the new endpoints or fields remain
  conformant to 1.1.0; verifiers that don't apply pinning or
  allowlisting still honor the original signature contract. Existing
  §11 References renumbered to §12; §12 Changelog renumbered to §13.

- **1.1.0** (2026-04-26) — Promote `nonce` parameter from reserved-future
  to required. Promote deduplication cache from SHOULD to MUST. No
  on-the-wire format changes from 1.0.0; servers that parsed `nonce`
  permissively (per the original §7 forward-compat clause) are
  unaffected. Implementations targeting 1.1.0 MUST emit `nonce`.
- **1.0.0** (2026-04-25) — Initial stable publication.
