已签名请求能证明什么?
大多数自动化流量会用 user agent string 进行自我标识。这个字符串只是声明,复制它几乎没有成本。已签名请求改变了这一点。当自动化客户端使用私钥对其 HTTP 请求签名,并在与其运营方绑定的位置发布匹配的公钥时,你的服务器就可以检查该请求是否确实由持有该密钥的人生成。
其背后的标准是 HTTP Message Signatures,以 RFC 9421 形式发布。IETF 的 Web Bot Auth 工作组 在此基础上为自动化客户端制定约定: 代理如何指向自己的密钥目录,以及应对哪些请求组件签名。截至 2026 年 11 月,该工作组的协议文档是 draft-ietf-webbotauth-httpsig-protocol,其中定义了 Signature-Agent 头部、基于 JSON Web Key Sets 的密钥目录格式,以及用于提供该目录的 well-known URI。理解这些内容很重要,因为验证很容易在细节上出错。
在讨论机制之前,先准确界定结果。有效签名表明,请求是由与已发布标识关联的密钥签署的,例如运营方域名,并且请求中被签名的部分没有被篡改。它不能说明是谁指示了该代理,不能说明是否有人将权限委托给它,也不能说明该操作是否安全。身份证据和授权是两个不同的问题,而你的应用要回答的是第二个。我们的博客文章 已签名的 AI 代理请求能证明什么 用示例说明了这条边界。
涉及哪些头部
已签名请求通常包含三个头部:
Signature-Input(RFC 9421, Section 4.1) 描述每个签名: 标签、受覆盖组件列表,以及created、expires、keyid、alg、nonce和tag等参数。Signature(RFC 9421, Section 4.2) 以结构化字段的形式携带每个标签的签名字节,使用 base64 编码。Signature-Agent(来自 Web Bot Auth 约定) 指向客户端公钥的发布位置,通常是运营方域名上的一个 HTTPS URL。
一个简化示例:
GET /api/catalog?page=2 HTTP/1.1
Host: shop.example
Signature-Agent: "https://agent.example"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"
Signature: sig1=:Base64SignatureBytes==:为便于阅读,示例已做简化。在当前工作组草案中,Signature-Agent 本身就是一个按签名标签键控的结构化字段(例如 sig1="https://agent.example"),因此请确认你的验证库期望的格式。
第 1 步: 正确解析结构化字段
Signature-Input 和 Signature 是 HTTP Structured Fields(RFC 9651,其已使 RFC 8941 失效)。不要用字符串切分来解析它们。带引号字符串、参数和内联列表都有严格语法,草率的解析器既会导致误拒绝,也会留下真实的安全缺口。
请使用维护中的 structured-field 解析器,或者更好地使用同时处理解析与验证的维护中的 RFC 9421 库。从零实现该规范是很好的学习练习,但不是好的生产选择。下面的步骤描述的是这类库的工作方式,便于你正确配置并审查其输出。
第 2 步: 选择要评估的签名
一个请求可能包含多个签名标签。选择符合你策略的那个,例如其 tag 为 web-bot-auth 的标签。如果出现以下情况,则出于验证目的拒绝该请求(不是为了提供服务而拒绝):
- 没有签名包含你所需的参数,
- 算法不在你的 allowlist 中(例如
ed25519),或 - 受覆盖组件不包含你要求的最小集合。
对于自动化客户端,一个合理的最小集合是 @method、@authority 和 @path,如果该头部存在,还应包含 signature-agent。没有 @authority,为一个站点捕获的签名可能会被重放到另一个站点。没有 @path,它可能会在多个 endpoint 之间复用。Web Bot Auth 协议草案 要求代理覆盖 @authority 或 @target-uri,并警告说,仅覆盖 authority 的签名,在到期前可以对该主机上的任何方法或 path 都有效。
第 3 步: 检查时间参数
读取 created 和 expires,它们是 RFC 9421, Section 2.3 中定义的时间参数。然后:
- 拒绝
created时间在未来且超出小范围时钟偏差容限的签名。 - 拒绝已经超过
expires的签名。 - 如果缺少
expires,则基于created强制执行你自己的最大时长。
较短的有效期可以限制被捕获请求可被重放的时间长度。
第 4 步: 发现密钥
Signature-Agent 头部指向运营方的密钥目录。通过 HTTPS 获取它,而且只能通过 HTTPS。Web Bot Auth 协议草案 将该目录描述为一个 JSON Web Key Set(以 JWK 格式表示的密钥),并从该 origin 的 http-message-signatures-directory well-known URI 提供。然后找到其标识符与 keyid 参数匹配的密钥。
type Jwk = { kid: string; kty: string; crv?: string; x?: string };
const keyCache = new Map<string, { keys: Jwk[]; fetchedAt: number }>();
const KEY_TTL_MS = 60 * 60 * 1000;
export async function getKey(directoryUrl: string, keyId: string): Promise<Jwk | null> {
const url = new URL(directoryUrl);
if (url.protocol !== "https:") return null;
const cached = keyCache.get(url.origin);
const fresh = cached && Date.now() - cached.fetchedAt < KEY_TTL_MS;
const keys = fresh ? cached.keys : await fetchKeys(url.origin);
return keys.find((k) => k.kid === keyId) ?? null;
}
async function fetchKeys(origin: string): Promise<Jwk[]> {
const res = await fetch(`${origin}/.well-known/http-message-signatures-directory`, {
headers: { accept: "application/http-message-signatures-directory+json, application/json" },
signal: AbortSignal.timeout(2000),
});
if (!res.ok) return [];
const body = (await res.json()) as { keys?: Jwk[] };
const keys = Array.isArray(body.keys) ? body.keys : [];
keyCache.set(origin, { keys, fetchedAt: Date.now() });
return keys;
}密钥发现的实用规则:
- 缓存密钥,并设置有限的有效期,这样验证就不会为每个请求都增加一次网络调用。
- 设置较短的超时,并将获取失败视为“无法验证”,绝不要视为“已验证”。
- 如果可以,限制可获取的 origin,或者至少阻止私有地址段,以避免服务器端请求伪造。协议草案在安全注意事项中专门列出服务器端请求伪造,就是出于这个原因。
- 将身份绑定到 origin,而不是绑定到 user agent 中的自由文本名称。你记录的标识符应是目录 origin。
第 5 步: 重建签名基
签名者并未对原始请求进行签名。它签的是一个称为 signature base 的规范化字符串(RFC 9421, Section 2.5), 该字符串按顺序由受覆盖组件构建,然后追加 @signature-params 行。你的验证器需要从收到的请求中重建相同的字符串。
对于上面的示例,signature base 看起来像这样:
"@method": GET
"@authority": shop.example
"@path": /api/catalog
"signature-agent": "https://agent.example"
"@signature-params": ("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"注意那些会在代理后破坏验证的细节:
@authority必须是客户端所针对的 host。如果负载均衡器改写了Host,请使用它转发的原始值。@path不包含查询字符串。如果客户端覆盖了@query,请按收到时的样子精确重建。- 标头值会按规范进行规范化: 去除首尾空白,并合并多个值。好的库会替你完成这些工作。
第 6 步: 验证签名
有了 base 和公钥之后,验证只是一次调用,按照 RFC 9421, Section 3.2 中的步骤执行。对于 Node.js 中的 Ed25519,使用内置的 crypto module:
import { createPublicKey, verify } from "node:crypto";
export function verifyEd25519(jwk: Jwk, base: string, signatureB64: string): boolean {
const key = createPublicKey({ key: jwk as any, format: "jwk" });
return verify(null, Buffer.from(base, "utf8"), key, Buffer.from(signatureB64, "base64"));
}如果检查失败,请求就未通过验证。它仍然可以被提供服务,只是不会获得“verified” basis。
第 7 步: 防止重放
有效签名在到期前可以被捕获并重新发送。RFC 9421, Section 7.2.2 讨论了签名重放,而 Web Bot Auth 协议草案则直接引用该部分,没有另行增加自己的 nonce 规则。两个控制措施可以关闭大部分此类窗口:
- 较短的过期时间,在第 3 步中强制执行。
- nonce 跟踪。 当出现
nonce时,在其密钥标识符下将其存储到签名到期为止,并拒绝第二次使用。
const seen = new Map<string, number>();
export function acceptNonce(keyId: string, nonce: string, expiresAt: number): boolean {
const id = `${keyId}:${nonce}`;
const now = Date.now() / 1000;
for (const [k, exp] of seen) if (exp < now) seen.delete(k);
if (seen.has(id)) return false;
seen.set(id, expiresAt);
return true;
}在多实例部署中,请使用共享存储(例如 Redis),并将 time to live 设为剩余有效期。
第 8 步: 记录带有 basis 的结果
验证产生的是证据,而不是裁决。请以你的分析和策略能够使用的形式记录它:
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};当验证失败或无法运行时,也要记录下来,并写明原因: 缺少头部、不支持的算法、未找到密钥、已过期、已重放,或签名不匹配。这些原因很有价值。来自某个运营方的“key not found”激增,往往意味着他们轮换了密钥,而不是他们在攻击你。
你应从签名验证中记录什么?
记录运营方 origin、密钥标识符、受覆盖组件、时间参数、结果以及失败原因。不要记录完整的签名字节或整个请求 body。日志保留时间应足够长,以便研究模式,同时又足够短,以尊重你的隐私承诺。
已验证签名应如何改变你的策略?
一旦请求可以被验证,你就可以用有意义的方式区别对待它们。我们关于 为人类和 AI 代理设计应用策略 的文章介绍了更广泛的策略模型:
- 对已验证的运营方开放只读 route,并赋予比未验证自动化更高的 rate limit。
- 为它们提供结构化响应,例如 JSON 或 Markdown,而不是完整的 HTML 页面。
- 将高影响操作保留在你的常规授权之后。已验证签名告诉你是谁在调用,不告诉你它是否可以修改账户。
请先从 observation mode 开始。在任何策略依赖它之前,先衡量你的自动化流量中有多少是已签名的、出现了哪些运营方,以及验证失败的频率。
Proof of Actor 在哪里发挥作用
签名验证只是多个证据来源之一。Proof of Actor 会将其与其他信号结合,并在每个结果上保持 basis 可见,这样已验证的代理就不会被误认为只是自我声明的代理。接下来由你的应用决定发生什么。
如需在生产应用中与我们的工程师一起构建这些能力,请 申请早期访问。若想全面了解这些证据能说明什么、不能说明什么,请阅读 Detection & Limitations。
来源
- RFC 9421: HTTP Message Signatures: 签名与验证标准,包括签名参数、signature base 和重放相关考量
- RFC 9651: Structured Field Values for HTTP:
Signature-Input和Signature使用的语法 - IETF Web Bot Auth working group: 自动化客户端认证工作的章程与状态
- draft-ietf-webbotauth-httpsig-protocol: 定义
Signature-Agent、JWKS 密钥目录及其 well-known URI 的 Internet-Draft(截至 2026 年 11 月仍在进行中) - RFC 7517: JSON Web Key (JWK): 已发布密钥目录中使用的密钥格式
- RFC 8615: Well-Known URIs: 如何注册诸如密钥目录之类的 well-known path
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): 示例中使用的 Ed25519 算法
- Node.js crypto documentation: 第 6 步中使用的
createPublicKey和verify