A2A JWS签名验证怎么过:从验签到排错
A2A 协议里用 JWS 给消息签名,目的是让接收方确认消息确实来自声称的代理,并且内容没有被改过。
验证失败时,先别急着换算法,大多数问题出在签名原文取错、密钥对不上或时间戳超出允许窗口这三处。
下面按验签的实际顺序拆开讲。
先确认签名原文到底签了什么
JWS 的签名不是对整个 JSON 请求体直接做哈希,而是对 BASE64URL(header) + "." + BASE64URL(payload) 这段字符串签名。
接收方验签时,必须用完全相同的字符串重新计算,任何空格、换行、字段顺序变化都会导致验签失败。
实际操作中,把收到的 JWS 按 . 切成三段,取前两段拼回签名原文:
signing_input = jws_parts[0] + "." + jws_parts[1]
然后用公钥验证第三段签名。
常见错误是先把 payload 解码成 JSON,再重新序列化去验签,这样几乎一定失败,因为序列化顺序和空白字符已经变了。
检查方法:在验签代码里打印 signing_input 的前 80 个字符,和发送方日志里的值对比。
如果对不上,问题就在取原文这一步。
密钥和算法要成对匹配
A2A 代理通常用非对称密钥签消息,常见是 ES256 或 RS256。
验签方需要拿到发送方 Agent Card 里公布的公钥,或者通过 JWKS 端点获取。
这里有几个容易踩的点:
- 算法声明和密钥类型不一致:header 里写
alg: ES256,但拿到的公钥是 RSA 密钥,验签库会直接报错。 - 用了对称密钥验非对称签名:HS256 需要共享密钥,和 ES256 的公钥验证不是一回事,不能混用。
- 公钥格式不对:PEM、JWK、裸 Base64 之间需要转换,直接塞进去会解析失败。
如果 Agent Card 里给了 jwks_uri,
优先从那里取密钥,
并核对 kid 是否和 JWS header 里的 kid 一致。kid 对不上时不要强行用第一把钥匙试,
先确认密钥轮换是否已经发生。
时间戳和消息时效性检查
JWS 的 payload 里一般会带 iat(签发时间)和 exp(过期时间)。
验签通过只说明签名有效,不代表消息还在有效期内。
接收方通常要检查当前时间是否落在允许窗口内,比如签发后 5 分钟内。
排查时先看两个时间:
- 发送方服务器的系统时间是否准确,用
date -u核对。 - 接收方和发送方是否存在明显时区或时钟偏差。
如果验签报 token expired 或类似错误,但签名本身能通过,问题就在时间窗口,不在密钥。
用最小样例复现验证过程
当线上验签失败又不好打断点时,可以拿一条失败消息的原始 JWS 字符串,在本地写一段最小验签代码复现。
步骤是:切分三段、拼签名原文、用公钥验证、再检查 payload 里的时间字段。
如果本地能通过、线上失败,重点查两处:线上是否对请求体做了二次编码,以及线上加载的公钥是不是最新版本。
反过来,本地也失败,就按前面三节逐项排除。
几个容易混淆的边界
JWS 验签通过,只能证明消息完整且来自持有对应私钥的一方,不证明对方有权限执行某个操作。
权限判断要单独做。
另外,如果 A2A 消息经过网关转发,网关如果重写了 JSON 字段顺序,接收方拿到的 payload 已经和签名原文不一致,这种场景下要么让网关透传原始 JWS,要么在网关层完成验签后再转发。
对接初期建议把验签失败的原因分类记录:原文不一致、密钥不匹配、时间过期、格式解析失败。
分类清楚后,排查会快很多。