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 seespecs/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.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 wherenonceis 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
400for nonce-validation failures,503when the enclave is unable to produce an attestation document (e.g., during startup or vendor-side outage). The router MUST NOT return200with a partial or malformed envelope.
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:- 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.
- Encode the bytes as base64url (RFC 4648 §5, unpadded; characters
[A-Za-z0-9_-], no=padding). - Place the encoded value in the
noncequery parameter on theGET /.well-known/tmp-router-attestationrequest. - After receiving the envelope, byte-compare
envelope.nonceagainst the value the verifier sent. A mismatch is an immediate reject (Verification step 1). - Confirm that the platform-specific user-data slot of
attestation_documentcontains the same nonce, per the verifier kit’s slot-projection rule forattestation_format(Verification step 4).
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 inThis rule is the protocol-level invariant that anchors the existing per-providersigning_keyMUST appear bound in the platform user-data slot ofattestation_documentalongside the nonce. Verifiers MUST reject when the bound public key does not byte-match the envelope’ssigning_keyafter canonical JWK serialization (RFC 7638 JSON Web Key Thumbprint comparison is the acceptable comparison form).
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
/.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: truefor the calling deployment — MUST attach on every request. - When the recipient provider’s registration carries
attestation_requirement.required: falseor omits the block — MUST NOT attach (the header adds up to ~10 KB per request depending onattestation_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:- Reads
X-TMP-Attestation, base64url-decodes, JSON-parses, schema-validates against/schemas/trusted-match/router-attestation.json. - 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’smin_freshness_sec— per-request nonce is impractical because the provider did not initiate the fetch. Per-request integrity is supplied by the per-providerX-AdCP-Signatureon the request payload (signed by the samesigning_keyfrom the envelope), and the binding rule ensures that signing key is the attested one. - Caches the verified envelope by
signing_keythumbprint for at mostmin_freshness_sec. Subsequent requests under the samekidreuse the cached verification; onkidchange or expiry, re-verify. - On any verification failure (failure modes), rejects the request with HTTP
403and anerrorbody whosecodereflects the failure class.
attestation_requirement: { required: false } or absent).
Verification flow
A verifier (a provider withattestation_requirement.required: true, or any actor independently auditing a router) MUST perform the following steps, in order, on each verification:
- Nonce echo (envelope-fetch path only). If the verifier obtained the envelope via
GET /.well-known/tmp-router-attestation, byte-compareenvelope.nonceagainst the value the verifier sent. Mismatch → reject (nonce_mismatch). On the per-requestX-TMP-Attestationpath this step is skipped; freshness is enforced by step 3. - Expiry. Compare current time to
envelope.expires_at. If current time is past, reject (envelope_expired). - Min-freshness (provider policy). If the verifier’s
attestation_requirement.min_freshness_secis set, compare to the envelope’s verifier-derived issuance age (the verifier kit projects anissued_atout of the platform document). If age exceeds the policy, reject (envelope_stale). - Format support. If
envelope.attestation_formatis not in the verifier’sattestation_requirement.acceptable_formats, reject (unsupported_format). - Platform document verification. Hand
attestation_documentto the verifier kit forattestation_format. The kit MUST: verify the document’s signature against the platform vendor’s root certificate authority; extract measurements; project the user-data slot. - Slot-bound nonce. Compare the nonce in the projected user-data slot to
envelope.nonce. Mismatch → reject (slot_nonce_mismatch). - Binding rule. Compare the JWK in the projected user-data slot to
envelope.signing_keyusing 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. - 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). - Cache. On success, cache the verified envelope keyed by
envelope.signing_key’s RFC 7638 thumbprint for at mostmin_freshness_sec.
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’sattestation_requirement.min_freshness_sec(default 300 seconds, same as the existing signing-key TTL). - On a
kidchange, invalidate the cached envelope and re-fetch eagerly — the same eager-re-fetch rule the spec already applies to signing keys atspecification.mdx#key-rotation(“When a signature fails verification, the router SHOULD re-fetch the key before rejecting — the agent may have rotated”). - Treat
revoked_aton the trust-anchor signing key as a revoked envelope: the envelope’ssigning_key.kidis the samekidthe verifier already looks up againstagent-signing-key.json, so the existing revocation propagation rules apply unchanged.
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.
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 claimingtrusted_match.router_attestation support MUST:
- Expose
GET /.well-known/tmp-router-attestationon every host that accepts router traffic. - Validate
noncelength and base64url alphabet on every request to that endpoint; reject malformed nonces with HTTP400. - Emit envelopes whose
signing_keyis bound inattestation_document’s user-data slot alongsidenonceper the format-specific verifier-kit convention. - Attach
X-TMP-Attestationto every outbound request to a provider whose registration carriesattestation_requirement.required: true. - Refresh the cached envelope on signing-key rotation or when
expires_atis past.
trusted_match.router_attestation support MUST:
- Declare
attestation_requirementon its provider registration when it requires attestation. - Run the Verification flow on the inbound
X-TMP-Attestationheader for every request whenrequired: true. - Reject with HTTP
403and a TMPerrorbody 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 bymin_freshness_sec.
Examples
The following examples use fictional entities from the AdCP character bible (Acme Outdoor, Pinnacle Agency, StreamHaus).Envelope (Nitro)
Provider registration declaring attestation required
X-TMP-Attestation to every POST /context and POST /identity request; Pinnacle Agency runs the verification flow and rejects on any failure.