From receiving a response to verifying it

Use standard HTTPS to obtain a public key bound to a Google attestation, then verify each API response. No internal proxy connection, MITM CA installation, or repository scripts required.

Document version: 1.1Signature protocol: trusted-ai-proxy-v1Reviewed: 2026-08-27

Understand the two verification layers

TrustedAIProxy does not ask you to trust a public key simply because the service sent it. The complete trust chain has two layers:

LayerWhat you verifyWhen
Workload attestationGoogle OIDC token → Confidential Space, image digest, project/instance/service account → Ed25519 public key bindingOn first connection, key changes, or proof expiry
Response attestationAttested Ed25519 public key → upstream TLS, domain, path, model, request/response text, timestamp, and nonceFor every API response
i
This is not a direct signature from the model provider

An approved Confidential Space workload generates the proof. It attests to what that workload observed over a normally verified TLS connection; it does not claim that OpenAI, Anthropic, or AWS signed the text.

Before you start: pin policy through a trusted out-of-band channel

Before inspecting any response to verify, obtain expected values from a contract, customer portal, or signed configuration bundle from the service. Self-reported response values must not define your trust policy.

Required configurationExample
Google issuerhttps://confidentialcomputing.googleapis.com
Attestation audiencetap/customer/v1
ImageApproved image reference and sha256:… digest
Operator identityGCP project ID/number, zone, instance allowlist, and workload service account
Hardware policyGCP_AMD_SEV or GCP_INTEL_TDX
Upstream policyAllowed domains, paths, models, and corresponding protocol extractors
Attestation policyed25519, allowed profiles, and maximum time window
Replica configurationA fixed numeric PostgreSQL secret version, or an explicit declaration that PostgreSQL is not used

If the service provides only a public key without an image digest and instance policy, you cannot establish which program owns that key.

1

Obtain a public key bound to a Google attestation over HTTP

Generate a one-time challenge locally, then request the service's Confidential Space proof endpoint. Use a different 10–74 character URL-safe ASCII challenge each time.

export PROOF_CHALLENGE="$(openssl rand -hex 16)"

curl --fail --silent --show-error \
  --header "Accept: application/json" \
  --output confidential-attestation.json \
  "https://SERVICE_HOST/.well-known/confidential-attestation?nonce=${PROOF_CHALLENGE}"

The endpoint returns a Google-issued OIDC token and the Ed25519 public key intended for API response verification:

{
  "token_type": "OIDC",
  "attestation_token": "GOOGLE_SIGNED_JWT",
  "audience": "tap/customer/v1",
  "key_id": "ed25519-...",
  "challenge_nonce": "CUSTOMER_CHALLENGE",
  "proof_ref": "proof-...",
  "expires_at": 1787839200,
  "attestation_key": {
    "algorithm": "ed25519",
    "public_key": "BASE64URL_RAW_32_BYTE_PUBLIC_KEY",
    "binding_nonce": "BASE64URL_SHA256_BINDING"
  }
}
!
Receiving a public key does not make it trusted

Treat the entire JSON as untrusted until the Google token has been verified. The HTTP example only retrieves data; your client still needs JWT, SHA-256, and Ed25519 libraries to perform the following checks.

Verify the Google token and public key binding

Fetch Google's OIDC discovery document and its declared JWKS over HTTPS. Trust only an issuer that exactly matches local policy, and select the Google public key using the JWT header's kid.

curl --fail --silent --show-error \
  --output google-oidc.json \
  "https://confidentialcomputing.googleapis.com/.well-known/openid-configuration"

curl --fail --silent --show-error \
  --output google-jwks.json \
  "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"
  1. Verify the JWT signature, iss, aud, validity period, and allowed signature algorithm. Refresh JWKS for an unknown kid; never disable signature verification.
  2. Check swname=CONFIDENTIAL_SPACE, dbgstat=disabled-since-boot, Secure Boot, hardware type, and the out-of-band approved image digest, project, instance, and service account.
  3. Confirm that the returned challenge_nonce exactly matches the local PROOF_CHALLENGE, algorithm is ed25519 and that the base64url-decoded public key is exactly 32 bytes.
  4. Recompute the public key binding below. Confirm that it equals binding_nonce and appears alongside the challenge in the Google token's eat_nonce.
binding_nonce = base64url_no_padding(
  SHA256(
    UTF8("attestation-ed25519-public-key-v1") ||
    0x00 ||
    raw_32_byte_ed25519_public_key
  )
)

Only after every check passes, cache public_key, key_id, proof_ref and expires_at together. Obtain a new proof when the proof expires, the public key changes, or local policy is updated.

2

Send an HTTP request and verify the response attestation

Call the service API as usual and save the original request, complete response body, and raw response headers. This example uses non-streaming OpenAI Chat Completions:

export API_KEY="YOUR_API_KEY"

curl --silent --show-error \
  --dump-header response.headers \
  --output response.json \
  "https://SERVICE_HOST/v1/chat/completions" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  --data '{
    "model": "APPROVED_MODEL",
    "messages": [{"role": "user", "content": "Hello"}],
    "stream": false
  }'

An attested response includes the following headers. If any are missing, duplicated, or malformed, mark it as unverified:

X-Attestation-Algorithm: ed25519
X-Attestation-Profile: llm-conversation-text-v1
X-Attestation-Key-Id: ed25519-...
X-Attestation-Domain: APPROVED_UPSTREAM_DOMAIN
X-Attestation-Path: /v1/chat/completions
X-Attestation-Model: APPROVED_MODEL
X-Attestation-Certificate-SHA256: 64_LOWERCASE_HEX
X-Attestation-Timestamp: UNIX_SECONDS
X-Attestation-Nonce: BASE64URL_NONCE
X-Attestation-Signed-Fields: tls_certificate_sha256,domain,request.path,request.body.model,request.body.messages,response.body.messages
X-Attestation-Signature: BASE64URL_ED25519_SIGNATURE
X-Attestation-Proof-Ref: proof-...

How TAP generates the signature

  1. First, remove any forged or stale upstream X-Attestation-* headers.
  2. Verify the upstream TLS certificate and hostname normally, and record the upstream domain, request path, and leaf certificate SHA-256 fingerprint.
  3. Use the path's versioned extractor to normalize the actual model and request/response messages into plain-text structures that preserve roles, order, message boundaries, and original text.
  4. Add the protocol version, profile, key ID, Unix timestamp, and random nonce, then use RFC 8785 JCS to produce a deterministic UTF-8 JSON payload.
  5. Sign with the Ed25519 private key corresponding to the attested public key from step 1. The private key stays inside the workload. Return the signature and metadata needed to reconstruct the payload in response headers; leave the API response body unchanged.

If the path is unconfigured, the content type is unsupported, the body exceeds limits, extraction is incomplete, or upstream TLS verification fails, TAP does not generate local attestation headers. Unsupported content is forwarded normally; upstream TLS verification is never bypassed.

The JCS payload for the HTTP example above has this shape. Replace RESPONSE_TEXT with text extracted from the actual response according to the extractor rules:

{"domain":"APPROVED_UPSTREAM_DOMAIN","key_id":"ed25519-...","nonce":"BASE64URL_NONCE","profile":"llm-conversation-text-v1","request_fields":[{"name":"model","value":"APPROVED_MODEL"},{"name":"messages","value":[{"role":"user","text":"Hello"}]}],"request_path":"/v1/chat/completions","response_fields":[{"name":"messages","value":[{"role":"assistant","text":"RESPONSE_TEXT"}]}],"timestamp":1787835600,"tls_certificate_sha256":"64_lowercase_hex","version":"trusted-ai-proxy-v1"}

How the client verifies it

  1. Confirm that proof_ref, key_id match the valid proof cached in step 1, and use that proof's public key.
  2. Strictly validate the algorithm, profile, signed-fields, header uniqueness, and formatting. The domain must match local policy. The path, model, and certificate fingerprint remain untrusted until signature verification succeeds.
  3. Using the original request and response, apply the extractor for the path indicated in the header to reconstruct request_fields and response_fields, then combine them with header metadata to build the claims.
  4. Apply RFC 8785 JCS to the claims, base64url-decode the 64-byte signature, and verify the payload using the attested Ed25519 public key.
  5. After signature verification, enforce local allowlists for the path, model, and optional certificate pin. Check the timestamp window and atomically store the nonce in a persistent cache; reject duplicate nonces.
i
For multiple replicas, fetch the response's proof_ref

If the response's X-Attestation-Proof-Ref differs from your local cache, generate a fresh challenge for it and request /.well-known/confidential-attestation?nonce=NEW_CHALLENGE&proof_ref=RESPONSE_PROOF_REF. Still perform every Google token and public key binding check from step 1.

export PROOF_CHALLENGE="$(openssl rand -hex 16)"
export PROOF_REF="proof-FROM-RESPONSE-HEADER"

curl --fail --silent --show-error --get \
  --data-urlencode "nonce=${PROOF_CHALLENGE}" \
  --data-urlencode "proof_ref=${PROOF_REF}" \
  --output confidential-attestation.json \
  "https://SERVICE_HOST/.well-known/confidential-attestation"

What is signed for non-streaming responses

  • Upstream HTTPS leaf certificate SHA-256, upstream domain, and request path;
  • Actual model;
  • Plain-text request and response messages, including roles, order, message boundaries, and text;
  • Protocol version, profile, key ID, timestamp, and random nonce.

Images, files, audio, tool definitions/calls, reasoning, citations, and usage are outside the scope of llm-conversation-text-v1.

Streaming: attest to the request reaching the upstream

For OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages, stream:true requests can receive llm-request-upstream-v1. Customers must generate a unique 10–74 character URL-safe ASCII challenge per API request and pass it using the mechanism agreed with the service:

export CHALLENGE="$(openssl rand -hex 16)"

curl --no-buffer --dump-header stream.headers \
  "https://SERVICE_HOST/v1/chat/completions" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -H "X-Attestation-Challenge: ${CHALLENGE}" \
  --data '{
    "model": "APPROVED_MODEL",
    "messages": [{"role": "user", "content": "Hello"}],
    "stream": true
  }'

Before reading the first SSE body byte, the client should verify these additional fields:

  • X-Attestation-Profile must be llm-request-upstream-v1;
  • The returned X-Attestation-Challenge must exactly match this request's customer challenge;
  • X-Attestation-Response-Status must match the response status;
  • X-Attestation-Response-Content-Type must be the normalized text/event-stream;
  • The signature covers model, messages, stream:true, upstream TLS/domain/path, response metadata, and the challenge.
!
The streaming body is entirely outside the signature scope

A gateway can modify, replace, or truncate the entire stream without invalidating this profile's signature. The product must clearly mark the streaming body as unverified; a blanket “response verified” status is incorrect.

Missing, duplicated, or invalid challenges may still allow normal stream forwarding, but no attestation headers are generated. Bedrock streaming paths currently also pass through without signatures.

Requirements for production clients

  1. Separate policy from responses: issuer, audience, digest, instances, domains, paths, and models must come from local policy.
  2. Parse strictly: reject duplicate JSON keys, duplicate or missing headers, invalid base64url, malformed Unicode, and unsupported profiles.
  3. Verify before trusting: do not trust a proof bundle's public key before verifying the Google token, or treat header paths/models as facts before Ed25519 verification.
  4. Reconstruct deterministically: normalize roles, order, message boundaries, and text using the corresponding extractor, then apply RFC 8785 JCS.
  5. Prevent replay: limit timestamp age and atomically store verified nonces in a persistent cache; fail on duplicate nonces.
  6. Cache proofs: cache by proof_ref with the verified public key and expiry. Do not identify key rotation using only the key ID.
  7. Make failures visible: distinguish “unverified”, “verification failed”, and “request verified, response unverified” as three separate states.
  8. Bound resources: limit proof bundles, headers, JSON bodies, network timeouts, and JWKS caches.

For full field ordering, JCS claims, and protocol-specific extraction rules, consult the repository's customer verification guide and reference implementations.

Handling failures

ConditionClient conclusion
Any required header is missingUnverified; do not display verification success.
Google token signature or policy mismatchUntrusted workload; discard the public key and proof.
Ed25519 signature verification failsContent or proof has changed; the response is invalid.
Timestamp outside window or duplicate noncePossible replay; the response is invalid.
Domain/path/model not allowed by local policyThe signature may be valid, but business policy rejects it.
Proof endpoint returns 404Unknown proof_ref; trust cannot be established.
Proof endpoint returns 410The replica holding the private key is offline; send a new API request later.
Proof endpoint returns 503Temporary issuance or cross-replica routing failure. Retry if appropriate, but never downgrade the check to verified.
Streaming request-upstream verification passesOnly the request and upstream metadata are attested; the streaming body remains unverified.

Frequently asked questions

Do I need to install the MITM CA?

No. The MITM CA is used only on the internal link from the gateway to TAP. End users call the service over normal HTTPS and verify the Google attestation and Ed25519 signatures.

Why can't I trust the public key supplied by the service directly?

The service could generate another key at any time. First verify the Google token to confirm that the public key is bound to the approved Confidential Space image and instance policy.

Do I need a Google attestation for every request?

No. Use proof_ref to cache verified proofs and public keys until expires_at, a public key change, workload restart, or local policy update. Each API signature still needs independent verification.

Does successful verification prove that the “genuine model” was used?

It proves that TAP sent the signed model and messages to the specified upstream domain/path over verified TLS and observed the signed response text. It does not prove the provider's internal generation process or cover business fields outside the profile.

Need protocol-level details?Read the field-level customer verification guide and complete protocol examples.
Open the customer verification guide