为 JWT 在 ES256 与 EdDSA 之间做选择,比笼统地比较 ECDSA 与 EdDSA 范围更窄。两者的令牌大小相同,速度都很快,对新的颁发者来说也都比 RS256 更值得推荐。区别在于支持情况:许多平台和密钥库要求或默认采用 ES256,而 Ed25519 更容易正确实现,并且在 JOSE 中现在的名称是 Ed25519,而不是 EdDSA。
ES256 与 RS256 对比指南讨论的是同一决策中 RSA 的一面;本页比较的是两种椭圆曲线方案。
概览
| ES256 | EdDSA / Ed25519 | |
|---|---|---|
JOSE alg | ES256(RFC 7518 §3.4) | Ed25519(RFC 9864);较早的 EdDSA(RFC 8037),现已弃用 |
| 曲线与哈希 | P-256 配合 SHA-256 | Edwards25519 配合 SHA-512 |
| JWK | kty: EC、crv: P-256、x、y | kty: OKP、crv: Ed25519、x |
| 公钥 JWK(实测,紧凑 JSON) | 126 个字符 | 79 个字符 |
| 令牌中的签名 | 64 字节原始 r‖s,86 个字符 | 64 字节,86 个字符 |
| 示例令牌(实测见下文) | 232 个字符 | 234 个字符(alg 取值更长) |
| 确定性 | 否(大多数库使用随机 nonce) | 是 |
| JWA 要求级别 | 推荐+(Recommended+) | 可选(Optional) |
| COSE 标识符 | −7(或 RFC 9864 中的 −9 ESP256) | −19(较早为 −8) |
| 云 KMS | AWS、Google、Azure:支持 | AWS(2025年11月起)和 Google:支持;Azure:不支持 |
| 浏览器 WebCrypto | 所有浏览器 | Firefox 129、Safari 17、Chrome 137 及更高版本 |
大小与速度
两种签名都是 64 字节,因此两种令牌的长度只差在头部。下面的脚本用 Node.js 26.10 运行,只用 node:crypto 以两种方式对同一组声明签名(实际代码应使用 JOSE 库)。
import { generateKeyPairSync, sign } from "node:crypto";
const b64url = (value) => Buffer.from(value).toString("base64url");
const payload = b64url(JSON.stringify({ iss: "https://auth.example.com", sub: "user-42", aud: "api", exp: 1792500000 }));
const es = generateKeyPairSync("ec", { namedCurve: "P-256" });
const esInput = `${b64url(JSON.stringify({ alg: "ES256", kid: "es-1" }))}.${payload}`;
const es256 = `${esInput}.${b64url(sign("sha256", Buffer.from(esInput), { key: es.privateKey, dsaEncoding: "ieee-p1363" }))}`;
const ed = generateKeyPairSync("ed25519");
const edInput = `${b64url(JSON.stringify({ alg: "Ed25519", kid: "ed-1" }))}.${payload}`;
const ed25519 = `${edInput}.${b64url(sign(null, Buffer.from(edInput), ed.privateKey))}`;
console.log("ES256 token", es256.length, "chars, public JWK", JSON.stringify(es.publicKey.export({ format: "jwk" })).length, "chars");
console.log("Ed25519 token", ed25519.length, "chars, public JWK", JSON.stringify(ed.publicKey.export({ format: "jwk" })).length, "chars");
ES256 token 232 chars, public JWK 126 chars
Ed25519 token 234 chars, public JWK 79 chars
速度不是决定因素:在 Apple M4 Max 的单个核心上,OpenSSL 3.6.5 每秒约生成 87,000 个 P-256 签名和 60,000 个 Ed25519 签名。由于 OKP 密钥只有一个坐标,包含 Ed25519 密钥的 JWKS 更小。
编码与正确性
JWS 中的 ES256 签名必须采用 64 字节的原始 r‖s 形式(RFC 7518 §3.4)。许多 ECDSA API 默认输出 DER:OpenSSL、Java 的 SHA256withECDSA、Go 的 ecdsa.SignASN1、未指定 dsaEncoding 时 Node 的 crypto.sign(),以及 AWS KMS 和 Google Cloud KMS 的签名 API。手写的 ES256 代码如果忘了转换,就会生成 70~72 字节的签名,合规的验证方会拒绝这些签名;反方向的错误则会让正确的令牌无法通过验证。ECDSA 验证方还必须拒绝等于零的 r 或 s;Java 15 到 18 没有这样做(CVE-2022-21449),在打上补丁之前,任何人都能伪造 ES256 令牌。
Ed25519 只有一种 64 字节编码,签名是确定性的,也没有会出错的 nonce。它的陷阱在别处:有的库把私钥当作 32 字节的种子,有的则当作 64 字节;较早的 JOSE 库只识别 "alg": "EdDSA"。
alg 名称变了
RFC 8037(2017年)把 EdDSA 注册为多态的 JOSE 算法,具体曲线由密钥决定(crv: Ed25519 或 Ed448)。RFC 9864(2025年10月)弃用了多态标识符,并注册了 Ed25519 和 Ed448,这样仅凭 alg 就能确定具体运算,ES256 一向如此。2026年各库的支持参差不齐:jose 6 同时接受 EdDSA 和 Ed25519,joserfc 和 Nimbus JOSE+JWT 支持新名称,而 PyJWT 2.15 和 golang-jwt v5 仍然只接受 EdDSA。如果令牌由多方使用,切换头部取值之前要逐一确认每个使用方;过渡期内,验证方可以对同一个密钥同时接受这两个名称。
平台与密钥库支持
ES256 是各方都支持的共同基线。它是 DPoP 证明(RFC 9449)的常见选择,是 FAPI 2.0 允许的三种算法之一(PS256、ES256,或使用 Ed25519 的 EdDSA),是 Apple 的 App Store Server API 和 APNs 提供方令牌的必选算法,所有 OpenID Connect 提供商都支持它,它还是 WebAuthn 中默认的 COSE 算法。所有 HSM、TPM、Secure Enclave 和云 KMS 都提供 P-256 密钥。
在软件层面,Ed25519 已经追了上来:Node.js、Go、Java 15 及更高版本、.NET(借助库)、Python 的 cryptography、浏览器 WebCrypto,以及主流 JOSE 库都支持它。在密钥库中,它的支持出现得较晚:AWS KMS 于2025年11月新增了 Ed25519 签名密钥,Google Cloud KMS 提供 EC_SIGN_ED25519,而部分 HSM 型号和 Azure Key Vault 不支持。如果令牌必须在托管密钥库内签名,先查看它支持的算法列表。
如何选择
- 公共身份提供商,依赖方众多:ES256(如有需要,为旧的使用方保留 RS256)。
- 密钥存放在多个云的 HSM 或 KMS 中:ES256,各处支持一致。
- 内部服务,两端都由你掌控,使用现代库:Ed25519;如果所有验证方都支持,就使用
alg: Ed25519,否则使用EdDSA。 - 来自浏览器和移动应用的发送方约束令牌(DPoP):ES256,所有平台密钥库都能以不可导出的方式生成这种密钥。
- 规定了算法的规范(FAPI、政府、金融):按规范列出的算法选择。
迁移说明
- 把新密钥添加到 JWKS 中,为它设置独立的
kid和明确的alg,并给使用方留出刷新缓存的时间。 - 按密钥配置验证方:每个密钥只接受一种算法(对于 Ed25519,如果你的各个使用方情况不同,就同时接受
EdDSA和Ed25519两种头部取值)。切勿在没有这种绑定的情况下接受令牌自带的alg。 - 让颁发者切换到新密钥;已有令牌在过期之前仍用旧密钥验证。
- 删除旧密钥,但要等到最长的令牌有效期过去之后。
- 端到端测试编码:来自 KMS 的 ES256 签名在放入令牌之前,必须从 DER 转换为原始格式;DER ⇄ 原始格式转换器和 JWT 调试器可以显示令牌实际包含的内容。