跳转到主要内容
指南

如何验证已签名的 AI 代理请求

一份面向自动化客户端的 RFC 9421 签名验证实用指南,说明有效签名能证明什么,以及日志应记录哪些内容。

Toshendra Sharma

作者 Toshendra Sharma创始人兼 CEO,AgentCredia · Updated November 13, 2026

高级 · 11 分钟阅读

已签名请求能证明什么?

大多数自动化流量会用 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。

一个简化示例:

HTTP
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 中定义的时间参数。然后:

  1. 拒绝 created 时间在未来且超出小范围时钟偏差容限的签名。
  2. 拒绝已经超过 expires 的签名。
  3. 如果缺少 expires,则基于 created 强制执行你自己的最大时长。

较短的有效期可以限制被捕获请求可被重放的时间长度。

第 4 步: 发现密钥

Signature-Agent 头部指向运营方的密钥目录。通过 HTTPS 获取它,而且只能通过 HTTPS。Web Bot Auth 协议草案 将该目录描述为一个 JSON Web Key Set(以 JWK 格式表示的密钥),并从该 origin 的 http-message-signatures-directory well-known URI 提供。然后找到其标识符与 keyid 参数匹配的密钥。

TS
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 看起来像这样:

TEXT
"@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:

TS
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 规则。两个控制措施可以关闭大部分此类窗口:

  1. 较短的过期时间,在第 3 步中强制执行。
  2. nonce 跟踪。 当出现 nonce 时,在其密钥标识符下将其存储到签名到期为止,并拒绝第二次使用。
TS
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 的结果

验证产生的是证据,而不是裁决。请以你的分析和策略能够使用的形式记录它:

TS
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。

来源

与我们一起构建。

加入 Proof of Actor founding developer program。

想先聊聊? 联系创始人