谈到 ECDSA 与 EdDSA,通常指的是 P-256 上的 ECDSA 与 Ed25519 上的 EdDSA:当今几乎所有的椭圆曲线部署都在使用这两种签名方案。两者强度相当、同样紧凑,因此选择取决于两个因素:一边是实现的安全性,一边是生态系统的支持。
概览
| ECDSA(P-256) | EdDSA(Ed25519) | |
|---|---|---|
| 标准 | FIPS 186-5 §6、SEC 1、ANSI X9.62 | RFC 8032、FIPS 186-5 §7(2023年起获批准) |
| 曲线形式 | 短 Weierstrass 形式,素数阶(余因子为 1) | 扭曲 Edwards 形式,余因子为 8 |
| 安全强度 | 约 128 位 | 约 128 位 |
| 私钥 | 32 字节标量 | 32 字节种子 |
| 公钥 | 未压缩形式 65 字节,压缩形式 33 字节,SPKI DER 91 字节 | 32 字节,SPKI DER 44 字节 |
| 签名 | 原始 r‖s 为 64 字节,DER 为 70~72 字节 | R‖S 为 64 字节,只有一种编码 |
| nonce | 每次签名随机生成,或按 RFC 6979 确定性派生 | 按设计由密钥和消息派生 |
| 延展性 | 有:(r, n − s) 同样能通过验证 | 无,前提是验证者检查 S 小于群的阶 |
| 每秒签名 / 验证次数(OpenSSL 3.6.5) | 约 87,000 / 29,000 | 约 60,000 / 25,000 |
| 每秒签名 / 验证次数(@noble/curves 2.3) | 约 3,800 / 870 | 约 3,100 / 1,280 |
| JOSE / COSE | ES256 / −7 | Ed25519(RFC 9864)/ −19,较早的 EdDSA / −8 |
| TLS 1.3 签名方案 | ecdsa_secp256r1_sha256(0x0403) | ed25519(0x0807) |
| 公开信任的 TLS 证书 | 允许 | 不允许(《基线要求》只允许 RSA 和 P-256/384/521) |
速度在 Apple M4 Max 的单个核心上测得,使用 OpenSSL 3.6.5(openssl speed);纯 JavaScript 的那一行则使用 Node.js 26.10 上的 @noble/curves 2.3.0。
设计上的差异
两者都是椭圆曲线上的离散对数签名,但设计时间相隔约二十年——ECDSA 出现于1992年,EdDSA 出现于2011年——这一差距在设计中处处可见。
nonce
每生成一个 ECDSA 签名,都要消耗一个秘密的 nonce k。如果两条消息使用了相同的 k,或者 nonce 在大量签名中哪怕只有几位存在偏差,就能算出私钥;PlayStation 3 的签名密钥就是这样泄露的,许多加密货币钱包也因此被盗空。FIPS 186-5 允许两种方式:由经批准的生成器产生随机的 k,或者按 RFC 6979 确定性地派生;大多数现代库默认采用其中之一,但这种失效模式是方案本身固有的。
EdDSA 消除了这种失效模式:nonce 是对私钥的一半和消息计算出的哈希(RFC 8032 §5.1.6)。对同一消息签名两次会得到相同的签名,不同的消息总会得到不同的 nonce,整个过程不涉及随机数生成器。代价是面临故障攻击:在对同一消息签名两次的过程中,只要出现一次毛刺(glitch),就可能泄露密钥,因此一些硬件钱包和后量子标准 ML-DSA 更倾向于采用同时混入新鲜随机性的对冲签名。
编码与延展性
ECDSA 有两种签名编码:原始 r‖s(JOSE、WebCrypto、COSE、PKCS #11)和 ASN.1 DER SEQUENCE(X.509、TLS、OpenSSL、Java、WebAuthn 断言)。把两者弄混,是出现“invalid signature”错误最常见的原因,DER 与原始格式指南对此有详细介绍。ECDSA 还具有延展性:任何人都能把 (r, s) 变成同样有效的 (r, n − s),这正是比特币和以太坊强制要求 low-S 的原因。
Ed25519 只有一种 64 字节的编码,而且 RFC 8032 §5.1.7 要求 S 小于群的阶,从而消除了明显的延展性。
验证中的边界情况
ECDSA 验证有明确的定义:检查 r 和 s 是否在 1…n−1 范围内,计算 u₁·G + u₂·Q,并将其 x 坐标与 r 比较。跳过范围检查是致命的:Java 15 到 18 曾对任意消息都接受全零签名(CVE-2022-21449)。
EdDSA 验证的问题更隐蔽。由于 Edwards25519 的余因子为 8,RFC 8032 既允许带余因子的检查 [8][S]B = [8]R + [8][k]A,也允许不带余因子的 [S]B = R + [k]A;各个库对小阶点和非规范编码的处理也不一致。对于诚实生成的签名,结果一致;对于恶意构造的输入,结果则不一致。区块链等需要多方就有效性达成一致的系统会固定一套规则,常见的选择是 Zcash 的 ZIP 215。
各自的支持情况
在硬件和 Web 平台上,ECDSA P-256 是各方都支持的共同基线。所有浏览器的 WebCrypto 都支持它;Apple 的 Secure Enclave 以及大多数 TPM、HSM、智能卡和云 KMS 密钥类型都提供它(而且往往只提供它);通行密钥默认使用它(COSE −7);CA/Browser Forum 的《基线要求》(§6.1.5)只允许公开信任的 TLS 证书使用 RSA,以及 P-256、P-384 和 P-521 上的 ECDSA。
在由软件定义的生态系统中,Ed25519 占主导地位。OpenSSH 自 9.5 版(2023年10月)起默认生成 Ed25519 密钥;Signal、WireGuard、Tor、age 以及大多数新设计的协议都基于 25519 系列曲线(X25519 用于密钥交换,Ed25519 用于签名);OpenPGP v6 密钥原生支持它(RFC 9580);WebCrypto 则在 Firefox 129、Safari 17、Chrome 137 及更高版本中支持它。在 JOSE 中,RFC 9864 用完全指定的 Ed25519 取代了多态的 EdDSA 标识符。
对于经过 FIPS 验证的部署,两者都已获 FIPS 186-5 批准,但较早通过验证的模块可能还不支持 EdDSA。从长远看,两者都会被量子计算机攻破,NIST IR 8547(仍是草案)提议在2035年之后禁止使用两者。
性能
坊间流传的说法是 Ed25519 比 ECDSA 快得多。在 P-256 实现还都是通用实现的年代,这话没错;但对如今的 OpenSSL 来说已不成立,因为它自带手工优化的 P-256 代码。在 Apple M4 Max 的单个核心上,OpenSSL 3.6.5 每秒约生成 87,000 个 P-256 签名,Ed25519 约为 60,000 个;验证次数则约为 29,000 对 25,000。在纯 JavaScript(@noble/curves 2.3.0)中,Ed25519 的验证约快 50%,而 P-256 的签名略快。无论哪种情况,开销都只在微秒级,很少成为决定因素;真正重要的是,两者都比 RSA 签名快一到两个数量级。
下面的脚本用 Node.js 26.10 运行,只用几行代码就展示了实际差异。
import { generateKeyPairSync, sign, verify } from "node:crypto";
const message = Buffer.from("transfer 100 to alice");
const ed = generateKeyPairSync("ed25519");
const ec = generateKeyPairSync("ec", { namedCurve: "P-256" });
// Ed25519: the algorithm argument is null because the hash is fixed (SHA-512).
const ed1 = sign(null, message, ed.privateKey);
const ed2 = sign(null, message, ed.privateKey);
// ECDSA P-256 with SHA-256, raw r||s output as used by JOSE and WebCrypto.
const ec1 = sign("sha256", message, { key: ec.privateKey, dsaEncoding: "ieee-p1363" });
const ec2 = sign("sha256", message, { key: ec.privateKey, dsaEncoding: "ieee-p1363" });
console.log("Ed25519 signature bytes:", ed1.length, "deterministic:", ed1.equals(ed2));
console.log("ECDSA P-256 signature bytes:", ec1.length, "deterministic:", ec1.equals(ec2));
console.log("Ed25519 public key (SPKI DER) bytes:", ed.publicKey.export({ type: "spki", format: "der" }).length);
console.log("P-256 public key (SPKI DER) bytes:", ec.publicKey.export({ type: "spki", format: "der" }).length);
console.log("both verify:", verify(null, message, ed.publicKey, ed1) && verify("sha256", message, { key: ec.publicKey, dsaEncoding: "ieee-p1363" }, ec1));
Ed25519 signature bytes: 64 deterministic: true
ECDSA P-256 signature bytes: 64 deterministic: false
Ed25519 public key (SPKI DER) bytes: 44
P-256 public key (SPKI DER) bytes: 91
both verify: true
Node 的 ECDSA 签名实现使用随机 nonce,因此对同一消息的两个签名并不相同,但两者都能通过验证。签名验证工具接受这两种方案,并显示它检测到的编码。
如何选择
- SSH 用户密钥和主机密钥:Ed25519,或配合 FIDO 安全密钥使用
ed25519-sk。只有面对不支持 Ed25519 的旧服务器或硬件时才使用 ECDSA。 - 通行密钥和 WebAuthn:两种都要接受(COSE −7,以及 −8 或 −19),但要预料到平台认证器给出的会是 ES256。
- 公开信任的 TLS 证书:ECDSA P-256(或 P-384);《基线要求》不允许使用 Ed25519。
- JWT 和 API 令牌:追求最广泛的兼容性或密钥存放在云 KMS 中时,使用 ES256;所有使用方都支持 RFC 8037 或 RFC 9864 时,使用 Ed25519。JWT 中的 ES256 与 EdDSA 一文有更深入的讨论。
- 存放在 Secure Enclave、TPM、智能卡或大多数 HSM 中的密钥:ECDSA P-256,因为硬件提供的就是它。
- 区块链:选择权不在你,按链的要求来(比特币和以太坊用 secp256k1 ECDSA 或 Schnorr,Solana 用 Ed25519)。
- 验证结果关系到共识的新协议:Ed25519,并明确规定验证规则,例如 ZIP 215。
迁移说明
P-256 密钥和 Ed25519 密钥之间无法互相转换:曲线不同,密钥格式也不同。从一种迁移到另一种,就是一次密钥轮换。
- 在密钥最终存放的位置生成新密钥。使用
openssl genpkey -algorithm ED25519或ssh-keygen -t ed25519;如果 HSM 支持 Ed25519,就在 HSM 内部生成。制订计划前先确认固件是否支持。 - 过渡期内同时发布两个密钥。在 JWKS 中为每个密钥分配各自的
kid和alg;在authorized_keys中先添加新行,再删除旧行;对于证书,为新密钥申请新证书。 - 先更新验证者,再更新签名者。验证者必须接受新的算法标识符(JOSE 中为
Ed25519或EdDSA,X.509 中为 OID1.3.101.112,SSH 中为ssh-ed25519),同时仍要用允许列表固定可接受的算法,绝不能让令牌或消息自行选择。 - 切换签名,然后停用旧密钥——要等到用旧密钥签名的所有内容都过期之后。
- 注意编码。ECDSA 代码经常在 DER 和原始签名之间转换;Ed25519 代码则不得这样做,而且 Ed25519“私钥”的大小因 API 而异(32 字节的种子,或种子加公钥共 64 字节)。
反方向的迁移,即从 Ed25519 迁到 ECDSA,主要发生在密钥迁入硬件的时候。使用确定性或对冲 nonce;如果签名被用作标识符,就强制要求 low-S;并用 DER ⇄ 原始格式转换器测试 DER 和原始格式的处理。