Skip to main content

Router Attestation

This page defines the wire shape, endpoint, header carrier, and verification flow for TEE attestation of the TMP Router. It is the normative companion to the informative trust framing in Privacy Architecture and the router protocol surface in The TMP Router. For design rationale, alternatives considered, and security analysis see specs/tmp-router-attestation.md.
Experimental — declare trusted_match.router_attestation in experimental_features (separate from trusted_match.core and trusted_match.verified_identity, so a participant can implement core TMP without committing to attestation). Fields and behavior on this surface are not subject to deprecation cycles until 3.0.0 GA; the surface may change between 3.x releases with at least 6 weeks’ notice. See experimental status for the full contract.
The protocol’s privacy guarantee is structural — context and identity flow through code paths that share no state (Privacy Architecture). Without attestation, that guarantee depends on operational trust that the deployed binary matches the audited source. Attestation removes that trust requirement: the verifier reads an attestation document from the router, verifies it against the platform vendor’s root, and confirms that the binary running inside the enclave matches the published, audited source code, subject to the verifier’s local measurement allowlist. This page is how the verifier does that.

Endpoint

  • Path: /.well-known/tmp-router-attestation (per RFC 8615). Public; no authentication.
  • Query parameter: nonce — base64url-encoded (RFC 4648 §5, unpadded). The decoded value MUST be at least 16 and at most 32 raw bytes of cryptographically random data (22–43 characters in the encoded form). The router MUST reject requests where nonce is shorter than 16 raw bytes, longer than 32 raw bytes, or contains characters outside the base64url alphabet.
  • Method: GET. No request body.
  • Response: HTTP 200, Content-Type: application/json, body validating against /schemas/trusted-match/router-attestation.json.
  • Error responses: HTTP 400 for nonce-validation failures, 503 when the enclave is unable to produce an attestation document (e.g., during startup or vendor-side outage). The router MUST NOT return 200 with a partial or malformed envelope.
The endpoint is rate-limit-sensitive (envelope production involves vendor-side cryptographic operations); routers SHOULD rate-limit per source IP (recommended: 10 req/sec) and SHOULD cache nothing — every request produces a fresh envelope so the verifier-supplied nonce binds to this fetch.

Envelope

The response is a single object matching /schemas/trusted-match/router-attestation.json:

Field reference

The schema declares additionalProperties: false at the top level; the ext field is the only vendor-extension surface. Receivers MUST reject envelopes with unknown top-level fields.

Nonce requirements

A verifier MUST:
  1. Generate at least 16 bytes (and at most 32 bytes) of cryptographically random data per verification. The verifier MUST NOT reuse a nonce across verifications.
  2. Encode the bytes as base64url (RFC 4648 §5, unpadded; characters [A-Za-z0-9_-], no = padding).
  3. Place the encoded value in the nonce query parameter on the GET /.well-known/tmp-router-attestation request.
  4. After receiving the envelope, byte-compare envelope.nonce against the value the verifier sent. A mismatch is an immediate reject (Verification step 1).
  5. Confirm that the platform-specific user-data slot of attestation_document contains the same nonce, per the verifier kit’s slot-projection rule for attestation_format (Verification step 4).
The nonce is the verifier-controlled freshness signal. Without it, a captured envelope is replayable for the lifetime of expires_at against any other verifier. The slot-projection check in step 5 is what makes the nonce cryptographically — rather than merely textually — bound to the fetched envelope.

Binding rule

The JWK in signing_key MUST appear bound in the platform user-data slot of attestation_document alongside the nonce. Verifiers MUST reject when the bound public key does not byte-match the envelope’s signing_key after canonical JWK serialization (RFC 7638 JSON Web Key Thumbprint comparison is the acceptable comparison form).
This rule is the protocol-level invariant that anchors the existing per-provider X-AdCP-Signature/X-AdCP-Key-Id signature path to the attested binary. Without it, a router could attest a clean binary and then sign requests with a different (deployment-side) key — every per-provider signature would verify, but no protocol mechanism would detect the swap. With it, every per-provider signature inherits the attestation: the signing key is provably the key the attested enclave committed to. The bound key is the same key the provider already verifies request signatures against (Identity Match signed fields). The trust anchor in agent-signing-key.json is the discovery surface for the publishable key; the envelope carries the JWK directly (not the thumbprint) so the verifier can re-derive the canonical form and compare. The slot-projection convention (how the JWK and nonce are laid out in the platform user-data slot) is defined in each verifier kit, not in this schema, because the slot name and concatenation rules differ per platform (Nitro user_data, TDX/SEV-SNP REPORTDATA, GCP claim format).

Per-request attestation: X-TMP-Attestation

When a provider’s registration carries attestation_requirement.required: true, the router MUST attach an X-TMP-Attestation header to every outbound POST /context and POST /identity request to that provider.

Header format

The header value is the base64url-encoded (RFC 4648 §5, unpadded) UTF-8 JSON serialization of the same envelope returned by /.well-known/tmp-router-attestation. Routers SHOULD fetch and cache a verified envelope keyed by signing-key kid, and MUST refresh on rotation or when expires_at is past.

When attached

  • When the recipient provider’s registration carries attestation_requirement.required: true for the calling deployment — MUST attach on every request.
  • When the recipient provider’s registration carries attestation_requirement.required: false or omits the block — MUST NOT attach (the header adds up to ~10 KB per request depending on attestation_format — Nitro is ~5 KB, TDX/SEV-SNP and GCP tokens are ~1-2 KB — and providers that haven’t opted in shouldn’t pay it).

Provider-side handling

A provider that requires attestation:
  1. Reads X-TMP-Attestation, base64url-decodes, JSON-parses, schema-validates against /schemas/trusted-match/router-attestation.json.
  2. Runs the Verification flow below, with the caveat that the per-request nonce check (step 1) is relaxed in favor of the envelope’s expires_at + the provider’s min_freshness_sec — per-request nonce is impractical because the provider did not initiate the fetch. Per-request integrity is supplied by the per-provider X-AdCP-Signature on the request payload (signed by the same signing_key from the envelope), and the binding rule ensures that signing key is the attested one.
  3. Caches the verified envelope by signing_key thumbprint for at most min_freshness_sec. Subsequent requests under the same kid reuse the cached verification; on kid change or expiry, re-verify.
  4. On any verification failure (failure modes), rejects the request with HTTP 403 and an error body whose code reflects the failure class.
A provider that does not require attestation ignores the header (attestation_requirement: { required: false } or absent).

Verification flow

A verifier (a provider with attestation_requirement.required: true, or any actor independently auditing a router) MUST perform the following steps, in order, on each verification:
  1. Nonce echo (envelope-fetch path only). If the verifier obtained the envelope via GET /.well-known/tmp-router-attestation, byte-compare envelope.nonce against the value the verifier sent. Mismatch → reject (nonce_mismatch). On the per-request X-TMP-Attestation path this step is skipped; freshness is enforced by step 3.
  2. Expiry. Compare current time to envelope.expires_at. If current time is past, reject (envelope_expired).
  3. Min-freshness (provider policy). If the verifier’s attestation_requirement.min_freshness_sec is set, compare to the envelope’s verifier-derived issuance age (the verifier kit projects an issued_at out of the platform document). If age exceeds the policy, reject (envelope_stale).
  4. Format support. If envelope.attestation_format is not in the verifier’s attestation_requirement.acceptable_formats, reject (unsupported_format).
  5. Platform document verification. Hand attestation_document to the verifier kit for attestation_format. The kit MUST: verify the document’s signature against the platform vendor’s root certificate authority; extract measurements; project the user-data slot.
  6. Slot-bound nonce. Compare the nonce in the projected user-data slot to envelope.nonce. Mismatch → reject (slot_nonce_mismatch).
  7. Binding rule. Compare the JWK in the projected user-data slot to envelope.signing_key using RFC 7638 thumbprint equality. Mismatch → reject (signing_key_not_bound). This is the load-bearing check; without it the envelope provides no protocol-level guarantee that signed requests originate from the attested binary.
  8. Measurement policy. Compare the extracted measurements to the verifier’s local allowlist. AdCP does not constrain this allowlist (the allowlist is a deploy-side decision per the rationale doc). On allowlist mismatch, reject (measurement_disallowed).
  9. Cache. On success, cache the verified envelope keyed by envelope.signing_key’s RFC 7638 thumbprint for at most min_freshness_sec.
Subsequent per-provider request signatures from the bound signing_key are then verified per the existing flow in Request Authentication. The binding rule is what makes those signatures inherit the attestation.

Failure modes

On any rejection, the provider returns HTTP 403 to the calling router with a TMP error body using the failure name as code (or internal_error if the failure is not in the table above), and SHOULD log the failure for operator review.

Caching

Verifier cache discipline for the envelope mirrors the existing TMP signing-key cache discipline at Key rotation — the convention TMP signing keys and TMPX HPKE keys already share. Verifiers SHOULD:
  • Cache the verified envelope keyed by the RFC 7638 thumbprint of signing_key, for at most the provider’s attestation_requirement.min_freshness_sec (default 300 seconds, same as the existing signing-key TTL).
  • On a kid change, invalidate the cached envelope and re-fetch eagerly — the same eager-re-fetch rule the spec already applies to signing keys at specification.mdx#key-rotation (“When a signature fails verification, the router SHOULD re-fetch the key before rejecting — the agent may have rotated”).
  • Treat revoked_at on the trust-anchor signing key as a revoked envelope: the envelope’s signing_key.kid is the same kid the verifier already looks up against agent-signing-key.json, so the existing revocation propagation rules apply unchanged.
The 300-second default is operationally aligned with the rest of the protocol; providers with a genuine reason to cache longer (e.g., very-high-throughput deployments) MAY raise it to up to 86400 seconds via min_freshness_sec on their registration. Lower values tighten the post-revocation window at the cost of more frequent envelope production.

Interaction with the per-provider signature flow

The router already signs per-provider requests over a canonical preimage (Identity Match signed fields). The attestation envelope does not replace that signature path; it anchors it.
  • The provider’s per-request verification path (verify Ed25519 signature against the publisher’s trust anchor in agent-signing-key.json) is unchanged.
  • The router’s per-provider re-signing on the Identity Match fan-out is unchanged.
  • The binding rule (Verification step 7) is added at envelope-verification time: the public key already in the trust anchor MUST byte-equal the public key bound in the platform user-data slot. Once the binding is verified, every subsequent per-request signature from that key inherits the attestation, because the signing key is provably held by the attested binary.
A provider that supports attestation but the binding check fails MUST reject every subsequent request signed by that kid until a fresh, successfully-verified envelope rotates the binding. A captured signing key held outside the enclave cannot satisfy the binding because the enclave that produces envelopes is bound to a different key.

Conformance

A TMP Router claiming trusted_match.router_attestation support MUST:
  • Expose GET /.well-known/tmp-router-attestation on every host that accepts router traffic.
  • Validate nonce length and base64url alphabet on every request to that endpoint; reject malformed nonces with HTTP 400.
  • Emit envelopes whose signing_key is bound in attestation_document’s user-data slot alongside nonce per the format-specific verifier-kit convention.
  • Attach X-TMP-Attestation to every outbound request to a provider whose registration carries attestation_requirement.required: true.
  • Refresh the cached envelope on signing-key rotation or when expires_at is past.
A provider claiming trusted_match.router_attestation support MUST:
  • Declare attestation_requirement on its provider registration when it requires attestation.
  • Run the Verification flow on the inbound X-TMP-Attestation header for every request when required: true.
  • Reject with HTTP 403 and a TMP error body on any verification failure, using the failure-mode names from the table above.
  • Cache verified envelopes by RFC 7638 thumbprint of the bound signing_key, bounded by min_freshness_sec.
A verifier (whether a provider or an independent auditor) claiming support MUST run the verification flow in order — the binding rule is not skippable.

Examples

The following examples use fictional entities from the AdCP character bible (Acme Outdoor, Pinnacle Agency, StreamHaus).

Envelope (Nitro)

Provider registration declaring attestation required

A router serving requests on behalf of StreamHaus calling Pinnacle Agency MUST attach X-TMP-Attestation to every POST /context and POST /identity request; Pinnacle Agency runs the verification flow and rejects on any failure.

Provider registration that does not require attestation (default)