跳到主要内容
ecdsa.com

ECDSA vs EdDSA:签名该选 P-256 还是 Ed25519

两者都是椭圆曲线签名,输出均为 64 字节,安全强度均为 128 位。区别在于出错时有多安全、各自在哪里受支持,以及不同验证者的判定能否严格一致。

谈到 ECDSA 与 EdDSA,通常指的是 P-256 上的 ECDSA 与 Ed25519 上的 EdDSA:当今几乎所有的椭圆曲线部署都在使用这两种签名方案。两者强度相当、同样紧凑,因此选择取决于两个因素:一边是实现的安全性,一边是生态系统的支持。

概览

ECDSA(P-256)EdDSA(Ed25519)
标准FIPS 186-5 §6、SEC 1、ANSI X9.62RFC 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 / COSEES256 / −7Ed25519(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 运行,只用几行代码就展示了实际差异。

Node.js:Ed25519 与 ECDSA P-256 的大小和确定性
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 密钥之间无法互相转换:曲线不同,密钥格式也不同。从一种迁移到另一种,就是一次密钥轮换。

  1. 在密钥最终存放的位置生成新密钥。使用 openssl genpkey -algorithm ED25519 或 ssh-keygen -t ed25519;如果 HSM 支持 Ed25519,就在 HSM 内部生成。制订计划前先确认固件是否支持。
  2. 过渡期内同时发布两个密钥。在 JWKS 中为每个密钥分配各自的 kid 和 alg;在 authorized_keys 中先添加新行,再删除旧行;对于证书,为新密钥申请新证书。
  3. 先更新验证者,再更新签名者。验证者必须接受新的算法标识符(JOSE 中为 Ed25519 或 EdDSA,X.509 中为 OID 1.3.101.112,SSH 中为 ssh-ed25519),同时仍要用允许列表固定可接受的算法,绝不能让令牌或消息自行选择。
  4. 切换签名,然后停用旧密钥——要等到用旧密钥签名的所有内容都过期之后。
  5. 注意编码。ECDSA 代码经常在 DER 和原始签名之间转换;Ed25519 代码则不得这样做,而且 Ed25519“私钥”的大小因 API 而异(32 字节的种子,或种子加公钥共 64 字节)。

反方向的迁移,即从 Ed25519 迁到 ECDSA,主要发生在密钥迁入硬件的时候。使用确定性或对冲 nonce;如果签名被用作标识符,就强制要求 low-S;并用 DER ⇄ 原始格式转换器测试 DER 和原始格式的处理。

参考资料

全部对比