Signed Agent Cards:基于 JWS 防范智能体名片伪造与投毒
当 AI 智能体在公网上通过获取 agent-card.json 文件来发现彼此时,信任不能建立在“网络可信”的假设上。一张未签名的卡片,只要一次 DNS 欺骗或 CDN 配置失误,就可能把自主系统引向恶意端点。在 A2A v1.0 中,签名 Agent Card 用 JSON Web Signature 解决了这个问题。
为什么未签名卡片存在安全隐患
在 A2A 协议规范 中,发现机制被设计得轻量、静态且无需鉴权:客户端智能体只需向 /.well-known/agent-card.json 发一个简单的 HTTP GET 请求。
这种简洁性推动了生态的快速采用,同时也暴露出一个可以称为 Agent Card 投毒 的攻击面:
- 端点重定向:攻陷 DNS 记录或边缘代理的攻击者可以改写
supportedInterfaces[].url字段,把敏感的提示词载荷悄悄发送给窃听或冒充的服务器; - 能力与提示词注入:篡改
skills数组或description字段,恶意方可以诱使调用方智能体把高权限的金融或管理类任务委派给一个未经审查的模型; - 身份冒充:没有密码学验证,任何人都可以托管一张卡片,声称
provider.organization是某家知名金融机构或软件厂商。
为了让多智能体架构抵御这些风险,a2aproject/A2A 仓库维护的规范允许卡片携带一个 signatures 数组,其中的元素是 RFC 7515:JSON Web Signature (JWS) 对象(规范第 8.4 节)。
signatures 数组的结构剖析
在 A2A v1.0 的 Agent Card 中,顶层的 signatures 属性是一个 AgentCardSignature 对象数组。允许有多个条目,密钥轮换就是靠这一点实现的:轮换期间,卡片同时带有一个旧密钥的签名和一个新密钥的签名。
{
"name": "Treasury Reconciliation Agent",
"version": "1.2.0",
"supportedInterfaces": [
{
"url": "https://agents.acme-finance.com/a2a/v1",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"signatures": [
{
"protected": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJrZXktMSIsImprdSI6Imh0dHBzOi8vYWNtZS1maW5hbmNlLmNvbS8ud2VsbC1rbm93bi9qd2tzLmpzb24ifQ",
"signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ"
}
]
}
每个条目都具有扁平 JWS 的字段,此外没有其他必填项:
-
protected(必填):经 Base64URL 编码的 JSON 对象,即 JWS 受保护头。它必须包含:alg:签名算法,如ES256或RS256;kid:密钥 ID,用于标识签名所用的密钥;typ:应设为JOSE。
它还可以包含
jku,即存放公钥的 JSON Web Key Set 的 URL。上面的示例解码后是{"alg":"ES256","typ":"JOSE","kid":"key-1","jku":"https://acme-finance.com/.well-known/jwks.json"}。 -
signature(必填):经 Base64URL 编码的签名值; -
header(可选):未受保护头参数组成的 JSON 对象,不做 Base64URL 编码。
使用 ES256 为 agent-card.json 生成签名
被签名的并不是对外提供的文件本身,而是它的规范化形式。按照规范第 8.4.1 节,你需要:
- 移除
signatures字段本身,避免循环依赖; - 遵循 Protocol Buffer 的字段存在性规则:丢弃取默认值的字段(空数组、空字符串),除非它们是必填字段,或是被显式设置的 optional 字段;
- 用 JSON 规范化方案(RFC 8785) 对结果做规范化:对象键排序,无多余空白。
JWS 签名输入就是 BASE64URL(受保护头) + "." + BASE64URL(规范化后的卡片)。下面是一个按这些步骤实现、不依赖任何第三方库的 Node.js 版本。对于普通卡片,只需对键排序并使用 JSON.stringify 就足以得到 RFC 8785 形式;如果你的卡片里有特殊数字,请改用经过充分验证的 JCS 库。
import crypto from 'node:crypto';
import fs from 'node:fs';
// RFC 8785 (JCS): keys sorted by UTF-16 code units, no whitespace. JSON.stringify
// already serializes strings and numbers the way JCS requires.
const canonicalize = value =>
Array.isArray(value)
? `[${value.map(canonicalize).join(',')}]`
: value !== null && typeof value === 'object'
? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(',')}}`
: JSON.stringify(value);
// 1. Read the card and drop any existing signatures.
const card = JSON.parse(fs.readFileSync('agent-card.json', 'utf8'));
const { signatures, ...unsigned } = card;
// 2. Build the JWS protected header: alg, typ and kid are required.
const header = {
alg: 'ES256',
typ: 'JOSE',
kid: 'key-1',
jku: 'https://acme-finance.com/.well-known/jwks.json'
};
const protectedB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
const payloadB64 = Buffer.from(canonicalize(unsigned)).toString('base64url');
// 3. Sign with your P-256 private key. A JWS ES256 signature is the raw R||S
// pair (RFC 7518 section 3.4), not Node's default DER encoding.
const signature = crypto.sign('sha256', Buffer.from(`${protectedB64}.${payloadB64}`), {
key: fs.readFileSync('private-key.pem'),
dsaEncoding: 'ieee-p1363'
}).toString('base64url');
// 4. Attach the signature to the card.
const signedCard = { ...unsigned, signatures: [{ protected: protectedB64, signature }] };
fs.writeFileSync('agent-card.signed.json', JSON.stringify(signedCard, null, 2));
console.log('Agent Card signed with ES256');
客户端智能体如何验证签名
客户端智能体从一个未经审查的对等方获取 Agent Card 时,验证应当发生在检查能力或调用接口之前。规范第 8.4.3 节给出了步骤:
- 提取:从
signatures数组中取出一个签名。如果没有,则根据你的安全策略,把该卡片视为未验证或直接拒绝; - 获取密钥:获取
kid所指的公钥,来源可以是jku处的 JWKS,也可以是已知提供方的受信任密钥库; - 重建载荷:从收到的卡片中移除取默认值的属性和
signatures字段,然后用 RFC 8785 做规范化; - 验证:用公钥对
BASE64URL(protected) + "." + BASE64URL(规范化后的卡片)校验签名。
除了机制本身,规范对信任问题的态度也很明确:客户端应当在信任卡片之前至少验证一个签名,公钥应当通过 HTTPS 获取,已过期或已吊销的密钥不得用于验证。允许存在多个签名,这样轮换密钥时验证不会中断。
在此基础上,还有两条规范并未要求但值得采用的策略:拒绝低于你安全基线的算法(none 永远不可接受,对称算法同理),并检查密钥的来源与卡片声明的 provider.url 是否一致,避免某个组织的签名为另一个组织的名义背书。
密钥轮换与安全实践清单
在生产环境中管理签名 Agent Card 时,请遵循以下运维准则:
- 让密钥生命周期与部署解耦:用
kid为密钥命名,并把密钥发布在由jku指向的 JWKS(/.well-known/jwks.json)中,而不是硬编码静态密钥。轮换时,先把新密钥加入 JWKS,用两把密钥同时签名一段时间,再下线旧密钥; - 每次变更都重新签名:签名覆盖整张卡片。把签名做成构建步骤,让版本号变更和新签名一起发布;
- 全程强制 HTTPS:
supportedInterfaces中列出的每个接口以及每个密钥端点,都必须通过 HTTPS 提供,并使用有效证书; - 让域名与提供方保持一致:托管 Agent Card 的域名、
provider.url以及 JWKS 的来源应保持一致; - 发布前审计:在把完成的卡片发布到公网根路径之前,先用 校验器 检查,并替换 生成器 输出的任何占位签名。
常见问题解答
A2A v1.0 中 Agent Card 必须带 JWS 签名吗?
不必须。规范规定 Agent Card 可以(MAY)被签名,并且客户端在卡片带有签名时应当(SHOULD)至少验证其中一个,再决定是否信任。对于暴露在公网或多租户网络中的智能体,仍然建议加上签名,因为没有签名的话,客户端无法判断卡片在传输途中是否被改动。
什么是“Agent Card 投毒”?
指篡改智能体的名片,让调用方信任错误的对象:控制了 DNS 记录、CDN 配置或被攻陷存储桶的攻击者,改写卡片,把接口端点重定向到恶意基础设施,或注入伪造的技能描述。
应该使用哪种签名算法?
规范的示例使用 ES256 和 RS256。ES256(基于 P-256 曲线和 SHA-256 的 ECDSA)是不错的默认选择:签名短小,库支持广泛。切勿使用 none 算法,也要避免 HS256 这类对称算法,因为客户端是用公钥来验证 Agent Card 的。
公钥应该托管在哪里?
放在提供方自己域名下通过 HTTPS 提供的 JSON Web Key Set 中,常见路径是 /.well-known/jwks.json。受保护头通过 kid 指明密钥,并可以用 jku 指向该密钥集;客户端也可以改用已知提供方的受信任密钥库。
Agent Card 校验器如何评估签名?
只检查格式。每个 signatures 条目需要一个声明了 alg 和 kid 的 base64url 受保护头(typ 应为 JOSE),以及一个不是占位符的 base64url 签名。它不会验证签名本身,因为那需要签名者的公钥和卡片的规范化形式;请用你的 SDK 或 JWS 库来完成验证。
参考资料
相关工具: Agent Card 校验器
浏览更多指南
下一步
在正式部署前检测您的 Agent Card 完整性。
校验器会检查卡片结构,核对 JWS 签名头中的 alg、kid 和 typ,并标记占位符签名与未加密的接口端点。