先理解两层验证
TrustedAIProxy 不要求你“相信服务方发来的公钥”。完整信任链分成两层:
| 层级 | 验证对象 | 频率 |
|---|---|---|
| 环境证明 | Google OIDC token → Confidential Space、镜像 digest、项目/实例/服务账号 → Ed25519 公钥绑定 | 首次连接、公钥变化或 proof 过期时 |
| 响应证明 | 经过证明的 Ed25519 公钥 → 上游 TLS、域名、path、model、请求/响应文本、时间戳与 nonce | 每一条业务响应 |
证明由你批准的 Confidential Space workload 生成。它证明该 workload 通过正常 TLS 连接观察到哪些内容,而不是宣称 OpenAI、Anthropic 或 AWS 对文本签过名。
开始前:从可信带外渠道固定验证策略
在查看任何待验证响应之前,先从合同、客户门户或由服务方签名的配置包取得期望值。响应中的自报值不能反过来定义你的信任策略。
| 必须预配置 | 示例 |
|---|---|
| Google issuer | https://confidentialcomputing.googleapis.com |
| Attestation audience | tap/customer/v1 |
| 镜像 | 批准的 image reference 与 sha256:… digest |
| 运营身份 | GCP project ID/number、zone、实例允许列表、workload 服务账号 |
| 硬件策略 | GCP_AMD_SEV 或 GCP_INTEL_TDX |
| 上游策略 | 允许的域名、path、model 与对应协议提取器 |
| 证明策略 | ed25519、允许的 profile、最大时间窗口 |
| 多副本设置 | 固定编号的 PostgreSQL Secret version,或明确声明不使用 PostgreSQL |
服务方若只给你一个公钥,却不提供镜像 digest 与实例策略,你无法确认这把密钥来自哪个程序。
通过 HTTP 获取经过 Google 证明绑定的公钥
先在本地生成一次性 challenge,再请求服务的 Confidential Space 证明端点。challenge 必须是每次不同的 10–74 位 URL-safe ASCII 字符。
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}"端点返回 Google 签发的 OIDC token,以及准备给业务响应验签使用的 Ed25519 公钥:
{
"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"
}
}在 Google token 验证完成前,整个 JSON 都是未受信输入。HTTP 示例负责取回数据;客户端仍需使用自己平台的 JWT、SHA-256 和 Ed25519 库执行下面的检查。
验证 Google token 与公钥绑定
通过 HTTPS 获取 Google OIDC discovery 和其中声明的 JWKS;只信任 discovery 中与本地策略完全一致的 issuer,并根据 JWT header 的 kid 选择 Google 公钥。
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"- 验证 JWT 签名、
iss、aud、有效时间和允许的签名算法;未知kid时刷新 JWKS,不能关闭验签。 - 核对
swname=CONFIDENTIAL_SPACE、dbgstat=disabled-since-boot、Secure Boot、硬件类型,以及带外批准的镜像 digest、项目、实例和服务账号。 - 确认返回的
challenge_nonce精确等于本地PROOF_CHALLENGE,algorithm为ed25519,base64url 解码后的公钥恰好为 32 字节。 - 按下式重新计算公钥 binding,并确认它同时等于
binding_nonce、且与 challenge 一起出现在 Google token 的eat_nonce中。
binding_nonce = base64url_no_padding(
SHA256(
UTF8("attestation-ed25519-public-key-v1") ||
0x00 ||
raw_32_byte_ed25519_public_key
)
)全部通过后,才把 public_key、key_id、proof_ref 与 expires_at 作为一组缓存。proof 过期、公钥变化或本地策略更新时重新取证。
发送 HTTP 请求并验证响应证明
像平常一样调用服务 API,同时保存原始请求、完整响应 body 和原始 response headers。下面是一个非流式 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": "你好"}],
"stream": false
}'可证明的响应会带有以下 headers;缺少、重复或格式非法时必须标记为未证明:
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-...TAP 如何生成这条签名
- 先清除上游可能伪造或残留的
X-Attestation-*headers。 - 正常验证上游 TLS 证书和 hostname;同时记录上游域名、request path 与叶子证书 SHA-256。
- 按路径对应的版本化提取器,将实际 model、请求消息和响应消息归一成保留角色、顺序、消息边界与原文的纯文本结构。
- 加入协议版本、profile、key ID、Unix 时间戳和随机 nonce,使用 RFC 8785 JCS 生成唯一的 UTF-8 JSON payload。
- 使用与第 1 步证明公钥对应、且不离开 workload 的 Ed25519 私钥签名,并将签名及重建 payload 所需的 metadata 放进 response headers;业务 response body 保持不变。
如果 path 未配置、content type 不支持、body 超限、语义提取不完整或上游 TLS 验证失败,TAP 会继续转发业务流量,但不会生成本地证明 headers。
以上 HTTP 示例对应的 JCS payload 形状如下。RESPONSE_TEXT 应替换为从实际响应按提取器规则得到的文本:
{"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":"你好"}]}],"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"}客户端如何验证
- 确认
proof_ref、key_id与第 1 步缓存的有效 proof 一致,并使用该 proof 中的公钥。 - 严格检查 algorithm、profile、signed-fields、header 唯一性与格式;domain 必须匹配本地策略。path、model 与证书指纹在验签前仍是未受信输入。
- 使用原始请求和响应,按 header 所示 path 对应的提取器重建
request_fields与response_fields,再用 headers 中的 metadata 构造 claims。 - 对 claims 执行 RFC 8785 JCS;base64url 解码 64 字节签名,并用经过证明的 Ed25519 公钥验证 payload。
- 验签通过后再对 path、model 和可选证书 pin 执行本地准入策略;检查 timestamp 未超窗,并把 nonce 原子写入持久化缓存,重复 nonce 直接拒绝。
如果业务响应的 X-Attestation-Proof-Ref 与本地缓存不同,为它生成新的 challenge,并请求 /.well-known/confidential-attestation?nonce=NEW_CHALLENGE&proof_ref=RESPONSE_PROOF_REF。仍须完整执行第 1 步的 Google token 和公钥绑定验证。
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"实际被签名的非流式内容
- 上游 HTTPS 叶子证书 SHA-256、上游域名与 request path;
- 实际 model;
- 请求与响应中的纯文本消息,包含角色、顺序、消息边界与文本;
- 协议版本、profile、key ID、时间戳和随机 nonce。
图片、文件、音频、工具定义/调用、推理过程、引用和 usage 不在 llm-conversation-text-v1 的证明范围内。
流式请求:只证明“请求到达上游”
OpenAI Chat Completions、OpenAI Responses 与 Anthropic Messages 的 stream:true 可以获得 llm-request-upstream-v1。客户必须为每次业务请求生成唯一的 10–74 位 URL-safe ASCII challenge,并通过业务服务约定的方式传递:
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": "你好"}],
"stream": true
}'在读取第一个 SSE body 字节前,客户端应验证这些额外字段:
X-Attestation-Profile必须为llm-request-upstream-v1;- 返回的
X-Attestation-Challenge精确等于本次客户 challenge; X-Attestation-Response-Status与响应状态一致;X-Attestation-Response-Content-Type为规范化后的text/event-stream;- 签名覆盖 model、messages、
stream:true、上游 TLS/域名/path、响应 metadata 与 challenge。
中转层可以修改、替换或截断整个流而不导致该 profile 验签失败。产品必须把流式正文明确显示为“未证明”,不能使用“响应已验证”的笼统状态。
缺失、重复或非法 challenge 时,流仍可能正常透传,但不会生成证明 headers。当前 Bedrock 流式路径也只透传、不签名。
生产客户端必须做到什么
- 策略与响应分离:issuer、audience、digest、实例、域名、path 和 model 必须来自本地策略。
- 严格解析:拒绝重复 JSON key、重复或缺失 header、非法 base64url、异常 Unicode 和不支持的 profile。
- 先验证再使用:Google token 验签前不信任证明包公钥;Ed25519 验签前不接受 header 中的 path/model 为事实。
- 确定性重建:按对应 extractor 归一化角色、顺序、消息边界和文本,再使用 RFC 8785 JCS。
- 防重放:限制 timestamp 最大年龄,并把已验证 nonce 原子写入持久化缓存;重复 nonce 直接失败。
- proof 缓存:按
proof_ref缓存已验证公钥与过期时间;公钥轮换不能只看 key ID。 - 失败可见:“未证明”“验证失败”“已验证请求但响应未证明”必须是三个不同状态。
- 资源限制:对证明包、headers、JSON body、网络超时和 JWKS 缓存设置上限。
完整字段顺序、JCS claims 和各协议消息提取规则,以仓库中的 客户验证指南与参考实现为准。
失败如何处理
| 情况 | 客户端结论 |
|---|---|
| 缺少任一必需 header | 未证明;禁止显示验证通过。 |
| Google token 签名/策略不匹配 | 环境不可信;丢弃公钥与该 proof。 |
| Ed25519 签名不通过 | 内容或证明被改变;响应无效。 |
| timestamp 超窗或 nonce 重复 | 可能重放;响应无效。 |
| domain/path/model 不在本地允许策略 | 签名可以正确,但业务策略不接受。 |
| 证明端点 404 | 未知 proof_ref;不能建立信任。 |
| 证明端点 410 | 持有对应私钥的副本不在线;稍后重新发起业务请求。 |
| 证明端点 503 | 签发或跨副本路由临时失败;可以重试,但不能降级为已验证。 |
| 流式 request-upstream 验签通过 | 仅请求和上游 metadata 已证明;流式正文仍未证明。 |
常见问题
我需要安装 MITM CA 吗?
不需要。MITM CA 只存在于中转站到 TAP 的内部链路。最终用户通过正常 HTTPS 调用服务,并验证 Google attestation 与 Ed25519 签名。
为什么不能直接信任服务方发来的公钥?
因为服务方可以随时生成另一把密钥。你必须先验证 Google token 确认公钥绑定到批准的 Confidential Space 镜像与实例策略。
Google attestation 每个请求都要获取吗?
不需要。按 proof_ref 缓存已经验证的证明和公钥,直到 expires_at、公钥变化、workload 重启或本地策略更新。业务签名仍需逐条验证。
验证通过就能证明一定调用了“正版模型”吗?
它能证明 TAP 通过验证的 TLS 连接向指定上游域名/path 发送了签名覆盖的模型与消息,并观察到签名覆盖的回复文本。它不证明模型厂商内部的生成机制,也不覆盖 profile 之外的业务字段。