错误信息
signature verification failed
jose (JavaScript) — JWSSignatureVerificationFailed, code ERR_JWS_SIGNATURE_VERIFICATION_FAILED
含义
jose 成功解析了 JWS 并执行了密码学检查,但用它选中的密钥无法验证该签名。结构、算法和密钥类型都没有问题——只是数学运算对不上,这说明问题出在密钥上,或者被签名的字节遭到了改动。
原因
密钥与该令牌不匹配
常见验证方持有的密钥与颁发者签名所用的密钥不同:JWKS 已轮换且旧 kid 已被移除、硬编码的密钥已经过时,或者环境混用(预发布环境的验证方,生产环境的令牌)。
被签名的字节在签名方与验证方之间发生了变化
常见JWS 签名的是头部和载荷的原样 base64url 文本(header.payload)。任何重新编码这些段的操作——网关规范化 base64 填充、重新序列化 JSON、传输中截掉了字符——都会导致验证失败,即使解码后的载荷看起来完全相同。
自行实现的 ES256 签名代码输出了 DER
偶见Node 的 crypto.sign() 默认为 ECDSA 输出 ASN.1 DER,但 RFC 7518 要求 ES256 JWS 签名为原始 r‖s(恰好 64 字节)。如果令牌的第三段解码后约为 70~72 字节并以 0x30 开头,说明它是用错误的格式签名的。
修复方法
- 1.
按 kid 从颁发者的 JWKS 中查找密钥
让 jose 为每个令牌挑选正确的密钥,而不是手动固定某一个密钥;createRemoteJWKSet 会缓存密钥,并在密钥轮换时刷新。
js import { createRemoteJWKSet, jwtVerify } from "jose"; const jwks = createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: "https://issuer.example", }); - 2.
如果用 node:crypto 签名,就输出原始 r‖s
对于自定义的签名代码,请求 IEEE P1363 格式的输出,使签名成为 JWS 要求的 64 字节格式——或者通过检查第三段的长度,核实签名代码实际输出的是什么。
js import { sign } from "node:crypto"; const sig = sign("sha256", Buffer.from(signingInput), { key: privateKey, dsaEncoding: "ieee-p1363", // raw r||s, 64 bytes for P-256 }); - 3.
检查签名段的结构
解码令牌的第三段:64 字节表示原始 r‖s(对 ES256 来说是正确的);以 0x30 开头、约 70 字节则表示 DER。下方的 DER ⇄ 原始格式转换器会显示 r 和 s 的值,并在两种格式之间转换。