错误信息
TypeError: "ES256" signatures must be "64" bytes, saw "71"
也可能输出为
- "ES256" signatures must be "64" bytes, saw "70"
- "ES256" signatures must be "64" bytes, saw "72"
- "ES384" signatures must be "96" bytes, saw "103"
- "ES512" signatures must be "132" bytes, saw "139"
对于 ES256,“saw”后面的数字在 70 到 72 之间变化(DER 长度取决于 r 和 s 的高位);ES384 要求 96 字节,ES512 要求 132 字节。这个错误是普通的 `TypeError`,而不是 `JsonWebTokenError`,因此针对 jsonwebtoken 错误的 `instanceof` 检查捕获不到它。
jsonwebtoken / jws / jwa (Node.js) — TypeError from ecdsa-sig-formatter
含义
令牌的签名段解码后为 71 字节,但 ES256 JWS 签名必须恰好是 64 字节,即 RFC 7518 §3.4 定义的原始拼接 r‖s。以 `0x30` 开头、长度为 70~72 字节的值,是同一签名的 ASN.1 DER 编码——OpenSSL、Java、KMS 服务和 Node 的 `crypto.sign()` 默认生成的就是这种格式。签发该令牌的签名方跳过了从 DER 到原始格式的转换;jsonwebtoken(通过 ecdsa-sig-formatter)在任何验证之前就拒绝了它。
原因
自行实现的签名代码输出了 DER
常见自行构建 JWT 并用 `crypto.sign("sha256", data, key)` 或 `createSign().sign(key)` 签名的代码,除非传入 `dsaEncoding: "ieee-p1363"`,否则得到的是 DER 输出。这样的令牌在各处看起来都没问题,直到遇到符合标准的验证方检查长度。
令牌由 KMS 或 HSM 签名
常见AWS KMS 的 `Sign` 和 Google Cloud KMS 的 `asymmetricSign` 返回 DER 格式的 ECDSA 签名,因此基于它们实现 JWT 签名需要一个显式的转换步骤。(Azure Key Vault 和 PKCS#11 `CKM_ECDSA` 返回的已经是原始 r‖s——转换之前先查看提供商文档中的说明。)
其他语言的默认编码
偶见Java 的 `SHA256withECDSA`、Python cryptography 库的 `sign()`、Go 的 `ecdsa.SignASN1` 以及 OpenSSL CLI 都输出 DER。专为 JOSE 设计的库会自动转换;通用的密码学调用则不会。
签名段已损坏
罕见如果报告的长度不在 70~72 范围内(例如“saw 2”),说明第三段被截断或替换了,而不是编码方式不同。
修复方法
- 1.
让 node:crypto 输出原始 r‖s
`dsaEncoding: "ieee-p1363"` 会让 Node 输出 JWS 要求的定长格式。更好的做法是用 JOSE 库签名(jsonwebtoken 的 `jwt.sign`、jose 的 `SignJWT`),它们会替你完成这一步。
js import { sign } from "node:crypto"; const signature = sign("sha256", Buffer.from(signingInput), { key: privateKey, dsaEncoding: "ieee-p1363", // raw r||s: 64 bytes for P-256, as RFC 7518 §3.4 requires }); const token = signingInput + "." + signature.toString("base64url"); - 2.
组装令牌之前,先转换 KMS 返回的 DER
`ecdsa-sig-formatter`(随 jsonwebtoken 经由 jws/jwa 一并安装)可以把 DER 转换为 JOSE 格式。DER ⇄ 原始格式转换器会显示两种形式下的 r 和 s,方便你确认转换结果。
js import ecdsaSigFormatter from "ecdsa-sig-formatter"; // AWS KMS and Google Cloud KMS return DER; convert once before assembling the JWS. const derSignature = Buffer.from(kmsResponse.Signature); const joseSignature = ecdsaSigFormatter.derToJose(derSignature, "ES256"); // base64url, 64 bytes decoded const token = signingInput + "." + joseSignature;