在公钥密码学的版图中,数字签名(Digital Signature)与加密并列为两大基石。如果说加密解决的是”别人看不到”的问题,那么签名解决的就是”这的确是我说的”以及”我说完之后没有被篡改”的问题。在互联网协议、软件分发、区块链交易、电子合同等几乎所有涉及信任传递的场景中,数字签名都扮演着不可替代的角色。
本文将系统梳理三大主流签名方案——ECDSA、EdDSA 与 Schnorr 签名。我们不仅关注算法本身的数学结构,还将深入探讨工程实践中最容易出错的环节:随机数 k 的致命重要性、确定性签名的演进、以及面向未来的多重签名与聚合签名技术。
一、数字签名的形式化定义
一个数字签名方案由三个多项式时间算法组成:
- 密钥生成(Gen):输入安全参数 n,输出一对密钥 (pk, sk),其中 pk 为公钥(public key),sk 为私钥(secret key)。
- 签名(Sign):输入私钥 sk 与消息 m,输出签名 σ = Sign(sk, m)。
- 验证(Verify):输入公钥 pk、消息 m 与签名 σ,输出布尔值 Verify(pk, m, σ) ∈ {0, 1}。
正确性要求非常直观:对任何由 Gen 生成的密钥对 (pk, sk) 和任意消息 m,都有 Verify(pk, m, Sign(sk, m)) = 1。
安全性:EUF-CMA
数字签名最核心的安全模型是存在性不可伪造性——自适应选择消息攻击(Existential Unforgeability under Adaptive Chosen Message Attack, EUF-CMA)。在此模型下,攻击者可以获取公钥 pk,并且可以自适应地请求签名预言机对任意消息进行签名。攻击者的目标是输出一对 (m, σ),使得 Verify(pk, m, σ) = 1,且 m* 不在之前查询过的消息集合中。若没有多项式时间的攻击者能以不可忽略的概率赢得此博弈,则称该方案是 EUF-CMA 安全的。
签名提供的三重保证
数字签名在实际应用中提供三种核心安全保证:
- 完整性(Integrity):签名与消息绑定,任何对消息的篡改都会导致验证失败。
- 身份认证(Authentication):只有持有私钥的实体才能产生有效签名,验证者可以确认签名者的身份。
- 不可否认性(Non-repudiation):签名者事后不能否认自己曾签署过某条消息——这一点是数字签名与消息认证码(MAC)的根本区别。MAC 使用对称密钥,发送方与接收方共享同一密钥,因此接收方同样有能力伪造 MAC,不可否认性无从谈起。
二、DSA 与 ECDSA
先看一张图,把这一节的关键关系串起来。
flowchart LR
A[消息] --> B[哈希]
B --> C[nonce]
C --> D[生成签名]
P[公钥] --> E[验签]
D --> E
DSA 的历史
数字签名算法(Digital Signature Algorithm, DSA)由 NIST 于 1991 年提出,并在 1994 年成为美国联邦信息处理标准 FIPS 186。DSA 基于有限域上离散对数问题的困难性,其设计受到 ElGamal 签名方案和 Schnorr 签名方案的启发。DSA 的提出在密码学社区引发了广泛讨论,部分原因是其设计避开了当时仍受 Schnorr 专利保护的方案。
ECDSA 算法
椭圆曲线数字签名算法(Elliptic Curve Digital Signature Algorithm, ECDSA)是 DSA 在椭圆曲线群上的类比。给定椭圆曲线 E 定义在有限域 F_p 上,基点 G 的阶为素数 n,ECDSA 的完整流程如下:
密钥生成:随机选取整数 d ∈ [1, n-1] 作为私钥,计算公钥 Q = dG。
签名过程:对消息 m 进行签名:
- 计算消息的哈希值 e = H(m),取其左边 L_n 位(L_n 是 n 的比特长度),记为 z。
- 随机选取 k ∈ [1, n-1]。
- 计算椭圆曲线点 (x_1, y_1) = kG。
- 计算 r = x_1 mod n。若 r = 0,返回步骤 2。
- 计算 s = k^{-1}(z + r·d) mod n。若 s = 0,返回步骤 2。
- 输出签名 (r, s)。
验证过程:给定公钥 Q、消息 m 和签名 (r, s):
- 检查 r, s ∈ [1, n-1]。
- 计算 z(同签名过程)。
- 计算 u_1 = z·s^{-1} mod n,u_2 = r·s^{-1} mod n。
- 计算椭圆曲线点 (x_1, y_1) = u_1·G + u_2·Q。
- 检查 r ≡ x_1 (mod n),若成立则签名有效。
验证的正确性可以直接推导:u_1·G + u_2·Q = u_1·G + u_2·dG = (u_1 + u_2·d)G。将 u_1 和 u_2 的定义代入,得 (z·s^{-1} + r·d·s^{-1})G = (z + r·d)·s^{-1}·G。由于 s = k^{-1}(z + r·d),故 s^{-1} = k·(z + r·d)^{-1},代入得 (z + r·d)·k·(z + r·d)^{-1}·G = kG,其 x 坐标恰好就是 r。
随机数 k 的致命重要性
在上述签名过程中,随机数 k(通常称为 nonce)是整个方案安全性的命脉。k 必须满足三个条件:真正随机、每次不同、绝对保密。违反其中任何一条,私钥 d 都可以被直接恢复。这并非理论上的可能性——历史上已经发生了多起因 k 的处理不当而导致的灾难性密钥泄露事件。
三、ECDSA 的 nonce 灾难
PlayStation 3 事件:固定 k
2010 年,fail0verflow 黑客团队公开披露了索尼 PlayStation 3 主机代码签名系统的一个致命漏洞:索尼在使用 ECDSA 对所有固件进行签名时,每次签名都使用了相同的 k 值。
当两次签名 (r_1, s_1) 和 (r_2, s_2) 使用相同的 k 时,由于 r_1 = r_2(相同的 k 产生相同的椭圆曲线点),攻击者可以直接计算:
s_1 - s_2 = k^{-1}(z_1 - z_2) mod n
由此立即得到:
k = (z_1 - z_2)(s_1 - s_2)^{-1} mod n
一旦 k 被恢复,从签名方程 s = k^{-1}(z + r·d) 中可以直接解出私钥:
d = (s·k - z)·r^{-1} mod n
索尼的这个错误使得任何人都可以为 PS3 签署任意代码,整个安全模型彻底崩溃。这是密码学工程史上最经典的反面教材之一。
Bitcoin 钱包泄露:有偏 k
即便 k 没有被完全重用,只要其分布存在微小的偏差(bias),攻击者也能利用格攻击(Lattice Attack)恢复私钥。2013 年,研究者发现部分 Android 比特币钱包中的随机数生成器存在缺陷,产生的 k 值不够均匀。通过收集多个签名,利用隐藏数问题(Hidden Number Problem, HNP)的格归约算法(如 LLL 或 BKZ),攻击者成功提取了用户的私钥,并窃取了大量比特币。
格攻击的基本思路如下:每个签名方程 s_i = k_i^{-1}(z_i + r_i·d) mod n 可以改写为 k_i = s_i^{-1}(z_i + r_i·d) mod n。如果已知每个 k_i 的高若干比特(即 k_i 存在偏差),这就构成了一个 HNP 实例。通过构造适当的格,并使用 LLL 算法求最短向量,可以在收集足够多签名(通常仅需几十到几百个)后恢复私钥 d。
教训
这些事件深刻地揭示了一个根本性问题:ECDSA 的安全性在理论上依赖于密钥 d 和消息哈希 z,但在工程实践中却悬挂于一个”额外的”随机量 k 之上。每一次签名操作都是一次高风险事件——任何一次 k 的失败都可能导致整个密钥对永久作废。这种将安全性寄托于运行时随机数质量的设计模式,在嵌入式设备、受限环境或异常系统状态下尤其危险。
从工程实践来看,nonce k 的问题是已部署密码学中最危险的单点故障之一。笔者认为,理解这一点需要把它放在更大的背景下:密码学方案的安全性证明通常假设所有随机数都是完美均匀的,但现实世界的随机数生成器可能在虚拟机快照恢复后重复状态、在嵌入式设备启动早期缺乏熵源、在系统负载异常时退化为可预测的输出。RFC 6979 的确定性 nonce 生成和 EdDSA 的内建确定性签名,本质上是将”运行时每次都要正确”的要求转化为”实现时一次正确即可”——这是密码学工程中最有价值的设计范式转换之一。一个依赖运行时随机数的签名方案,其真实安全性上限不是数学证明给出的界限,而是部署环境中最差的那次随机数生成的质量。
一个值得深思的现象是,EdDSA(Ed25519)相对于 ECDSA 的改进,代表了密码学标准化理念的一次根本性转变:从”数学规范”到”完整实现规范”。ECDSA 的标准(FIPS 186)定义了数学运算,但将大量实现细节留给开发者:哈希函数的选择、点的编码方式、nonce 的生成方法、乃至标量乘法的实现策略——这些”留白”中的每一处都是潜在的安全陷阱。相比之下,RFC 8032 对 Ed25519 的规范细致到了字节级别:它精确定义了私钥种子如何通过 SHA-512 哈希和 clamping 转化为标量,公钥如何编码(y 坐标加 x 的奇偶位),nonce 如何从私钥后半部分和消息确定性地派生——实现者几乎没有做决策的空间,因而也没有犯错的空间。这种”零歧义规范”哲学正是 Bernstein 设计 NaCl/libsodium 的核心原则在标准化层面的延伸。
从工程实践来看,Schnorr 签名的专利历史(US Patent 4,995,082,1990-2008)是密码学标准化中知识产权壁垒造成系统性危害的最惨痛案例。Schnorr 签名在数学上比 DSA/ECDSA 更简洁(签名方程 \(s = k - e \cdot d\) 是线性的,而 ECDSA 的 \(s = k^{-1}(z + r \cdot d)\) 需要模逆)、安全证明更直接(在随机预言模型下可严格归约到离散对数假设)、且天然支持多签聚合(线性结构使得多个签名可以直接相加)。然而,NIST 在 1991 年制定数字签名标准时被迫绕开 Schnorr 专利,设计了结构更复杂、更容易出错的 DSA。从 1991 年到 2008 年专利过期,整整十七年间,全球部署了数十亿个 ECDSA 密钥——每一个都承载着 nonce 重用导致私钥泄露的风险,而这个风险在 Schnorr 的确定性变体中本可以从设计层面消除。如果没有这项专利,Ed25519 级别的签名方案可能在 1990 年代就已经出现。密码学标准的选择影响深远,而专利制度在这一过程中造成的扭曲,其代价可能要数十年后才能被完全清算。
四、RFC 6979:确定性签名
为了从根本上消除对运行时随机数的依赖,Thomas Pornin 于 2013 年提出了 RFC 6979 标准。其核心思想极为精妙:将 nonce k 的生成从随机过程变为确定性过程,利用私钥和消息本身作为种子推导 k。
HMAC-DRBG 状态机
RFC 6979 的核心是一个基于 HMAC 的确定性随机比特生成器(HMAC-DRBG)。其内部维护两个状态变量 V(值)和 K(密钥),通过两阶段状态机完成 k 的推导:初始化阶段将私钥与消息哈希注入状态,生成阶段从状态中提取候选值并验证有效性。完整伪代码如下:
def rfc6979_generate_k(d, message, q, hash_func):
"""
RFC 6979 确定性 nonce 生成
d: 私钥(整数)
message: 待签名消息
q: 群的阶
hash_func: 哈希函数(如 SHA-256)
"""
hlen = hash_func.digest_size # 哈希输出字节数
qlen = bit_length(q) # q 的比特长度
# ── 阶段一:初始化 ──────────────────────────────
# 步骤 a:计算消息哈希
h1 = hash_func(message)
# 步骤 b-c:V 置全 1,K 置全 0
V = b'\x01' * hlen
K = b'\x00' * hlen
# 步骤 d-e:第一轮注入(带 0x00 分隔符)
K = HMAC(K, V || 0x00 || int2octets(d) || bits2octets(h1))
V = HMAC(K, V)
# 步骤 f-g:第二轮注入(带 0x01 分隔符)
K = HMAC(K, V || 0x01 || int2octets(d) || bits2octets(h1))
V = HMAC(K, V)
# ── 阶段二:生成与范围检查 ─────────────────────
while True:
# 步骤 h.1:累积足够比特构成候选 k
T = b''
while len(T) < (qlen + 7) // 8:
V = HMAC(K, V)
T = T || V
# 步骤 h.2:转为整数,检查 1 <= k <= q-1
k = bits2int(T, qlen)
if 1 <= k <= q - 1:
return k
# 步骤 h.3:候选无效,更新状态后重试
K = HMAC(K, V || 0x00)
V = HMAC(K, V)两轮注入(步骤 d-e 与 f-g)使用不同的分隔字节(0x00 和 0x01),目的是确保 HMAC-DRBG 的内部状态充分混合私钥和消息哈希。步骤 h 的循环在绝大多数情况下仅执行一次——候选 k 落在 [1, q-1] 之外的概率不超过 2^{-128}。
为何消除了 k 重用灾难
回顾第三节中的 PlayStation 3 事件:索尼对所有固件使用固定的 k 值进行 ECDSA 签名,攻击者仅需两个签名就能通过 k = (z_1 - z_2)(s_1 - s_2)^{-1} 恢复 k,进而解出私钥。在 RFC 6979 下,k 由 HMAC-DRBG(d, H(m)) 确定性派生——只要消息不同,k 就必然不同;而相同的消息产生完全相同的签名,不会泄露额外信息。即使攻击者能观察到任意数量的签名,在不知道私钥 d 的前提下也无法预测或关联任何 k 值。
RFC 6979 的工程优势可以总结为三点:
- 确定性与可测试性:相同的私钥和消息总是产生相同的签名。开发者可以使用 RFC 6979 附录 A 的测试向量逐步验证 V、K 的中间状态,确保实现正确。
- 向后兼容:生成的签名在格式上与标准 ECDSA 完全相同,验证端无需任何修改。
- 消除随机源依赖:嵌入式设备、虚拟机等熵源不可靠的环境中,RFC 6979 将安全性锚定在 HMAC 的伪随机性上,而非操作系统的随机数生成器质量。
RFC 6979 的推出是 ECDSA 工程实践的一个重要转折点。此后,OpenSSL、libsecp256k1(比特币核心使用的密码库)、BoringSSL 等主流实现均默认采用确定性 nonce 生成。然而,RFC 6979 本质上是对已有方案的修补——更现代的签名方案从设计之初就将确定性 nonce 内建于协议之中。
五、Schnorr 签名
最简洁的签名方案
Schnorr 签名方案由德国密码学家 Claus-Peter Schnorr 于 1989 年提出,其安全性直接基于离散对数假设,可以在随机预言模型(Random Oracle Model)下严格证明 EUF-CMA 安全性。与 ECDSA 相比,Schnorr 签名的数学结构极为简洁。
在椭圆曲线群上,Schnorr 签名的流程如下:
密钥生成:随机选取 d ∈ [1, n-1] 作为私钥,Q = dG 为公钥。
签名:对消息 m:
- 选取随机 k ∈ [1, n-1],计算 R = kG。
- 计算 e = H(R || Q || m)。
- 计算 s = k - e·d mod n。
- 输出签名 (R, s) 或等价地 (e, s)。
验证:
- 计算 R’ = sG + eQ。
- 计算 e’ = H(R’ || Q || m)。
- 检查 e’ = e。
验证的正确性一目了然:R’ = sG + eQ = (k - e·d)G + e·dG = kG = R,因此 e’ = H(R’ || Q || m) = H(R || Q || m) = e。
以下是在小素数群上实现 Schnorr 签名的 Python 示例代码:
import hashlib
import secrets
# 使用一个小素数阶群进行演示(实际应用中需使用标准椭圆曲线)
# 这里用模素数 p 的乘法群的一个素数阶子群来模拟
p = 2357 # 素数模数
q = 131 # 子群的素数阶(q | p-1)
g = 2 ** ((p - 1) // q) % p # 阶为 q 的生成元
def schnorr_keygen():
"""生成 Schnorr 密钥对"""
sk = secrets.randbelow(q - 1) + 1 # 私钥 d ∈ [1, q-1]
pk = pow(g, sk, p) # 公钥 y = g^d mod p
return sk, pk
def schnorr_hash(r_bytes, pk_bytes, msg_bytes):
"""计算哈希 e = H(R || pk || m) mod q"""
h = hashlib.sha256(r_bytes + pk_bytes + msg_bytes).digest()
return int.from_bytes(h, 'big') % q
def schnorr_sign(sk, pk, message):
"""Schnorr 签名"""
msg_bytes = message.encode('utf-8')
pk_bytes = pk.to_bytes(16, 'big')
k = secrets.randbelow(q - 1) + 1 # 随机 nonce
r = pow(g, k, p) # R = g^k mod p
r_bytes = r.to_bytes(16, 'big')
e = schnorr_hash(r_bytes, pk_bytes, msg_bytes)
s = (k - e * sk) % q
return (e, s)
def schnorr_verify(pk, message, signature):
"""Schnorr 验证"""
e, s = signature
msg_bytes = message.encode('utf-8')
pk_bytes = pk.to_bytes(16, 'big')
# 重建 R' = g^s * pk^e mod p
r_prime = (pow(g, s, p) * pow(pk, e, p)) % p
r_bytes = r_prime.to_bytes(16, 'big')
e_prime = schnorr_hash(r_bytes, pk_bytes, msg_bytes)
return e_prime == e
# 演示
sk, pk = schnorr_keygen()
msg = "Schnorr signatures are elegant"
sig = schnorr_sign(sk, pk, msg)
print(f"私钥: {sk}")
print(f"公钥: {pk}")
print(f"签名: (e={sig[0]}, s={sig[1]})")
print(f"验证结果: {schnorr_verify(pk, msg, sig)}")
# 篡改消息后验证失败
print(f"篡改后验证: {schnorr_verify(pk, msg + '!', sig)}")线性性质
Schnorr 签名最重要的代数性质是线性性(Linearity)。观察签名方程 s = k - e·d:它是关于 k 和 d 的线性组合。这意味着多个签名者可以各自生成自己的 (k_i, R_i, s_i),然后简单地将 R 值和 s 值相加,得到一个对联合公钥有效的聚合签名。这种天然的可聚合性是 Schnorr 签名相较于 ECDSA 的核心优势之一,我们将在第八节详细讨论。
专利历史
Schnorr 于 1990 年为其签名方案申请了美国专利(US Patent 4,995,082),该专利直到 2008 年才到期。这一长达 18 年的专利保护期深刻影响了密码学标准的发展轨迹。NIST 在 1991 年制定 DSA 标准时,刻意绕开了 Schnorr 专利,设计了结构更为复杂的 DSA/ECDSA。这段历史颇具讽刺意味:一个数学上更优美、安全性证明更直接的方案,因为专利壁垒而被束之高阁近二十年,取而代之的是一个工程上更容易出错的方案。直到 Schnorr 专利过期后,密码学社区才得以自由地在新标准中采用 Schnorr 签名的思想。
六、EdDSA 与 Ed25519
确定性的设计哲学
Edwards 曲线数字签名算法(Edwards-curve Digital Signature Algorithm, EdDSA)由 Daniel J. Bernstein 等人于 2011 年提出,其设计目标是从根本上避免 ECDSA 中由 nonce 管理引发的所有问题。EdDSA 并非事后修补,而是从第一个设计决策开始就将确定性签名作为核心原则。
EdDSA 的 nonce 生成方式极为简洁:
r = H(h_b || ... || h_{2b-1} || M)
其中 h_b, …, h_{2b-1} 是将私钥种子哈希后的后半部分(作为”前缀密钥”使用),M 是消息。这种方式不需要任何外部随机源:nonce 完全由私钥和消息确定。相同的私钥对相同的消息签名,永远产生相同的签名——这既消除了随机数的风险,也使得实现的正确性易于测试和验证。
Ed25519 参数
Ed25519 是 EdDSA 最广泛部署的实例,其参数选择如下:
- 曲线:扭曲 Edwards 曲线(Twisted Edwards Curve) -x^2 + y^2 = 1 + d·x2·y2,其中 d = -121665/121666,定义在素数域 F_p 上,p = 2^{255} - 19。
- 基点:选取特定的点 B,其阶为 l = 2^{252} + 27742317777372353535851937790883648493,是一个大素数。
- 哈希函数:SHA-512。
- 私钥:32 字节随机种子。
- 公钥:32 字节(基点的标量乘结果的 y 坐标加上 x 坐标奇偶位的编码)。
- 签名:64 字节(R 的编码 32 字节 + s 的编码 32 字节)。
余因子处理
Curve25519 的余因子(cofactor)为 8,这意味着曲线上的点的总数是基点阶 l 的 8 倍。余因子的存在引入了小阶子群的问题:存在 8 个阶整除 8 的”小阶点”。在签名验证中,如果不正确处理余因子,可能导致签名的可延展性(malleability)问题——同一个逻辑上的签名可能存在多种有效表示。
Ed25519 的原始规范通过在验证等式中引入余因子乘法来解决此问题:验证 8·(sB) = 8·(R + eA) 而非 sB = R + eA。后来的变体(如 Ristretto255)通过群构造在数学层面直接消除了小阶子群,提供了更干净的抽象。
Ed448
Ed448 是 EdDSA 的另一个标准实例,使用 Goldilocks 曲线(Edwards448-Goldilocks),定义在 p = 2^{448} - 2^{224} - 1 上,提供约 224 比特的安全强度。Ed448 适用于需要更高安全裕度的场景,但在性能上不如 Ed25519。RFC 8032 同时定义了 Ed25519 和 Ed448 的完整规范。
性能特征
Ed25519 的性能令人印象深刻。在现代 x86-64 处理器上,典型的性能指标为:
- 密钥生成:约 50 微秒
- 签名:约 70 微秒
- 验证:约 200 微秒
- 批量验证(每个签名平摊):约 100 微秒
这些数字使得 Ed25519 成为吞吐量要求最高的应用(如区块链节点)中的首选方案之一。
以下是使用 Python 标准库进行 Ed25519 签名与验证的示例:
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey
)
# 生成密钥对
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
# 签名
message = "EdDSA 确定性签名示例".encode("utf-8")
signature = private_key.sign(message)
print(f"公钥 ({len(public_key.public_bytes_raw())} 字节): "
f"{public_key.public_bytes_raw().hex()}")
print(f"签名 ({len(signature)} 字节): {signature.hex()}")
# 验证——成功时无异常
try:
public_key.verify(signature, message)
print("签名验证成功")
except Exception as e:
print(f"签名验证失败: {e}")
# 篡改消息后验证
try:
public_key.verify(signature, b"tampered message")
print("篡改后验证成功(不应发生)")
except Exception:
print("篡改后验证失败(符合预期)")
# 确定性验证:相同消息产生相同签名
signature2 = private_key.sign(message)
print(f"两次签名相同: {signature == signature2}")七、Ed25519 vs ECDSA P-256
在实际工程中,Ed25519 和 ECDSA over P-256 是最常见的两个选项。以下从多个维度进行详细对比。
密钥与签名尺寸
| 指标 | ECDSA P-256 | Ed25519 |
|---|---|---|
| 私钥 | 32 字节 | 32 字节 |
| 公钥 | 33 字节(压缩) / 65 字节(未压缩) | 32 字节 |
| 签名 | 64 字节(r, s 各 32 字节) | 64 字节(R, s 各 32 字节) |
| 安全强度 | ~128 比特 | ~128 比特 |
两者的密钥和签名尺寸非常接近。Ed25519 的公钥固定为 32 字节,略优于 P-256 的压缩公钥(33 字节)。
性能
Ed25519 在大多数平台上都显著快于 ECDSA P-256。这主要归功于以下因素:
- Curve25519 的素数 p = 2^{255} - 19 是一个伪梅森素数(pseudo-Mersenne prime),模运算可以用简单的加减法和移位操作实现,无需通用的大数除法。
- 扭曲 Edwards 曲线的加法公式是完备的(complete)——对任意两个输入点都有效,不存在需要特殊处理的例外情况(如无穷远点)。这消除了条件分支,天然适合常时间实现。
- Ed25519 不需要计算模逆(签名过程中没有 k^{-1} 这样的操作),而 ECDSA 的签名和验证都需要模逆运算。
侧信道抗性与常时间实现
这是 Ed25519 相对于 ECDSA P-256 的最大优势之一。常时间实现(constant-time implementation)是指算法的执行时间不依赖于任何秘密数据(如私钥或 nonce)的值。非常时间的实现容易遭受计时攻击(timing attack)、缓存攻击(cache attack)等侧信道攻击。
Ed25519 从设计之初就考虑了常时间实现的便利性:
- 完备的加法公式消除了因点的特殊位置而产生的条件分支。
- 固定基点标量乘可以使用预计算查找表,结合常时间的表查询实现。
- 没有模逆运算,避免了扩展欧几里得算法中数据依赖的循环次数。
相比之下,NIST P-256 曲线使用短 Weierstrass 形式,其加法公式在处理恒等元(无穷远点)和点加与倍点时需要不同的公式,实现常时间操作需要更多工程技巧。历史上,许多 P-256 的实现被发现存在侧信道漏洞。
确定性
如前所述,Ed25519 的签名过程天然是确定性的,不需要任何外部随机源。ECDSA P-256 在原始规范中需要随机 nonce,但可以通过 RFC 6979 实现确定性签名。从结果上看,两者都可以实现确定性签名,但 Ed25519 将其作为强制要求而非可选附加。
生态系统支持
ECDSA P-256 拥有更广泛的历史生态系统支持——它是 TLS 1.2 中最常用的签名算法,是 NIST 推荐的标准曲线,也是许多政府和企业合规框架的要求。Ed25519 的支持正在快速增长:OpenSSH 自 6.5 版本起支持 Ed25519 密钥;TLS 1.3 将 Ed25519 纳入标准;GnuPG、WireGuard、Signal 协议等现代系统均采用 Ed25519。
选择建议
对于新系统的设计,如果没有特定的合规性要求,Ed25519 几乎在所有方面都是更优的选择。如果必须使用 ECDSA(例如因为遗留系统兼容性或合规要求),务必确保使用 RFC 6979 确定性 nonce 生成,并采用经过审计的常时间实现库。
八、多重签名与聚合签名
在许多实际场景中,我们需要多个参与方共同对一条消息进行签名。例如,加密货币的多签钱包(multisig wallet)要求 m-of-n 个密钥持有者的授权才能执行交易。传统做法是将多个独立签名拼接在一起,但这导致签名尺寸随参与者数量线性增长。更优雅的方案是将多个签名压缩为一个与单签名尺寸相同的聚合签名。
Schnorr 签名的天然线性性
Schnorr 签名的线性结构使得多重签名的构造异常自然。考虑 n 个签名者,各自持有私钥 d_i 和公钥 Q_i = d_i·G。一个朴素的多签方案如下:
- 每个签名者选取随机 k_i,计算 R_i = k_i·G,并广播 R_i。
- 计算聚合的 R = R_1 + R_2 + … + R_n。
- 计算挑战 e = H(R || Q_1 + Q_2 + … + Q_n || m)。
- 每个签名者计算 s_i = k_i - e·d_i。
- 聚合签名为 (R, s) 其中 s = s_1 + s_2 + … + s_n。
验证时只需检查 sG + e(Q_1 + … + Q_n) = R,与普通 Schnorr 验证完全相同。这意味着在链上或协议中,多签交易与单签交易在验证成本和占用空间上没有区别。
然而,上述朴素方案存在”流氓密钥攻击(Rogue Key Attack)“的安全隐患:恶意参与者可以选择一个精心构造的公钥,使得聚合公钥实际上等于其独自控制的密钥。MuSig 和 MuSig2 协议正是为了解决此问题而设计的。
MuSig 与 MuSig2
MuSig 协议由 Maxwell, Poelstra, Seurin 和 Wuille 于 2018 年提出,通过在聚合公钥时对每个参与者的公钥施加基于所有公钥集合的哈希系数来防御流氓密钥攻击。具体而言,聚合公钥不再是简单的 Q_1 + … + Q_n,而是:
Q_agg = a_1·Q_1 + a_2·Q_2 + ... + a_n·Q_n
其中 a_i = H(L || Q_i),L = H(Q_1 || Q_2 || … || Q_n) 是所有公钥的承诺。由于 a_i 依赖于整个公钥集合,攻击者无法事先选择一个公钥来抵消其他参与者的贡献。
原始 MuSig 需要三轮交互(承诺、nonce 交换、部分签名)。MuSig2 将交互轮次减少到两轮,通过让每个签名者预先发送两个 nonce(而非一个)来实现。这显著降低了协议的通信复杂度,使其更适合实际部署。比特币的 Taproot 升级(2021 年激活)正是利用了 Schnorr 签名的这一特性。
BLS 签名
BLS 签名(Boneh-Lynn-Shacham, 2001)基于双线性配对(Bilinear Pairing),提供了更强大的聚合能力。给定配对 e: G_1 × G_2 → G_T,BLS 签名的核心思想是:
- 私钥 sk ∈ Z_q,公钥 pk = sk·G_2 ∈ G_2。
- 签名 σ = sk·H(m) ∈ G_1,其中 H 将消息映射到 G_1 上的点。
- 验证:检查 e(σ, G_2) = e(H(m), pk)。
BLS 签名最引人注目的特性是非交互式聚合:给定 n 个不同消息上的 n 个签名 σ_1, …, σ_n,任何人(无需签名者参与)都可以计算聚合签名 σ_agg = σ_1 + … + σ_n。验证者只需一次配对计算即可验证整个批次。这种特性在以太坊 2.0 的权益证明共识中得到了大规模应用——每个 epoch 需要验证数千个验证者的签名,BLS 聚合使得验证成本保持在可控范围内。
BLS 签名的主要缺点是配对运算的计算成本较高:单次 BLS 签名验证比 Schnorr 或 EdDSA 慢一个数量级。但在需要聚合大量签名的场景中,聚合验证的总体效率远超逐一验证。
多签与聚合签名的场景边界
虽然 Schnorr 多签(MuSig2)和 BLS 聚合签名都提供了将多个签名压缩为一个的能力,但它们的适用场景存在清晰的边界,不能混为一谈:
Schnorr 多签(MuSig2) 适用于一组已知签名者对同一条消息达成共识的场景。它需要两轮交互式通信,所有参与者必须在线协同。最典型的部署是比特币 Taproot——多签交易在链上看起来与单签交易完全相同,节省了空间和验证成本。MuSig2 也适用于需要 m-of-n 阈值签名的场景(结合 FROST 协议)。但它不适合签名者之间无法直接通信的场景,也不适合对不同消息的签名进行事后聚合。
BLS 聚合签名 的独特价值在于非交互式聚合:任何第三方都可以将 n 个不同签名者对不同消息的签名合并为一个聚合签名,且不需要签名者之间有任何通信。这使得 BLS 在大规模共识系统中不可替代——以太坊信标链每个 epoch 需要验证数千个验证者的签名,BLS 聚合将验证成本从 O(n) 降到接近 O(1)。但 BLS 的配对运算成本使其不适合延迟敏感的场景(如 TLS 握手)。
部署边界总结:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 区块链多签钱包 | MuSig2(Schnorr) | 链上空间宝贵;签名者已知且可交互 |
| 大规模共识验证 | BLS 聚合 | 需要非交互式聚合数千签名 |
| TLS / SSH 认证 | 单签名(Ed25519 / ECDSA) | 延迟敏感;通常只需单方签名 |
| 固件签名 | 阈值签名(FROST) | 私钥不应存在于单一设备;签名可离线 |
| 跨链桥验证 | BLS 聚合 | 需要紧凑地验证多链的多个签名 |
批量验证
除了签名聚合,Ed25519 还原生支持批量验证(Batch Verification):给定 n 个独立的 (消息, 公钥, 签名) 三元组,可以通过随机线性组合在一次多标量乘法中完成所有验证,计算成本约为 n 次单独验证的一半。这对需要高吞吐量签名验证的系统(如区块链全节点)非常有价值。
九、从 DSA 到聚合签名:历史演进
数字签名的技术演进并非线性的”更好算法替代更差算法”,而是一段交织着数学突破、专利壁垒、工程教训与标准博弈的复杂历史。将这条时间线梳理清楚,有助于理解今天各方案并存的格局是如何形成的。
1989-1990:Schnorr 签名与专利封锁。 Claus-Peter Schnorr 于 1989 年提出了基于离散对数的签名方案,其签名方程 s = k - e·d 是线性的,安全性可在随机预言模型下严格归约到离散对数假设。然而 Schnorr 在 1990 年获得美国专利(US Patent 4,995,082),有效期至 2008 年。这项专利成为此后近二十年密码学标准化的最大障碍。
1991-1994:DSA 的诞生——专利的副产品。 NIST 在制定联邦数字签名标准时,被迫绕开 Schnorr 专利。结果是结构更复杂的 DSA:签名方程变为 s = k^{-1}(z + r·d),引入了模逆运算,安全性证明也不如 Schnorr 直接。DSA 于 1994 年成为 FIPS 186 标准。从数学角度看,这是一次”降级”——更优美的方案因法律原因被搁置,取而代之的方案在工程上更容易出错。
1999-2005:ECDSA 继承了 DSA 的一切。 随着椭圆曲线密码学的成熟,ECDSA 作为 DSA 的椭圆曲线类比被标准化(ANSI X9.62, 1999; FIPS 186-2, 2000)。ECDSA 继承了 DSA 的所有数学结构——包括对随机 nonce k 的致命依赖。此后十余年间,数十亿个 ECDSA 密钥被部署在 TLS、SSH、比特币等系统中,每一次签名都承载着 nonce 失败导致私钥泄露的风险。
2008:专利过期,枷锁解除。 Schnorr 专利到期,密码学社区终于可以自由使用 Schnorr 签名的思想。这一事件直接催生了此后数年的一系列创新。
2011:EdDSA——从头设计的确定性签名。 Daniel J. Bernstein 等人提出 EdDSA,其核心改进是将 Schnorr 签名与确定性 nonce 生成结合:nonce 从私钥和消息确定性派生,彻底消除了对运行时随机源的依赖。同时,EdDSA 选用扭曲 Edwards 曲线,加法公式完备,天然适合常时间实现。Ed25519 作为 EdDSA 的旗舰实例,迅速被 OpenSSH、WireGuard、Signal 等现代系统采纳。
2013:RFC 6979——为 ECDSA 打补丁。 在 EdDSA 的启发下,Thomas Pornin 提出了 RFC 6979,通过 HMAC-DRBG 为 ECDSA 实现确定性 nonce。这是一次重要的工程修补,但相比 EdDSA 的”从设计之初就正确”,RFC 6979 终究是事后补救。
2018-2020:MuSig 与 MuSig2——Schnorr 的多签潜力释放。 Schnorr 签名的线性结构(s = k - e·d)使得多个签名可以直接相加。MuSig 协议(2018)通过哈希系数解决了流氓密钥攻击问题,实现了安全的 n-of-n 聚合签名;MuSig2(2020)将交互轮次从三轮降到两轮。这些成果为比特币 Taproot 升级奠定了基础。
2021 至今:BLS 与非交互式聚合。 BLS 签名(Boneh-Lynn-Shacham, 2001)虽然提出较早,但其大规模部署始于以太坊 2.0 信标链(2020)。BLS 基于双线性配对,支持非交互式聚合——任何第三方都可以将 n 个签名合并为一个,无需签名者之间通信。这使得 BLS 成为大规模共识系统中验证者签名聚合的唯一可行方案。
纵观这段历史,每一次演进都在解决前一代方案的具体缺陷:DSA 绕开了专利但引入了工程复杂性;RFC 6979 修补了 nonce 风险但无法改变底层结构;EdDSA 从头设计消除了整类漏洞;MuSig/BLS 则将签名从”单人操作”扩展为”多方协作”。理解这条线索,才能理解为什么”更旧的方案”并非简单地”更差”——它们是特定历史约束下的最优选择。
十、区块链中的签名实践
区块链是数字签名最密集的应用场景之一:每一笔交易都需要签名,每一个区块都需要验证数百到数千个签名。不同区块链在签名方案的选择上做出了截然不同的权衡,这些选择深刻影响了各自的性能、隐私性和可扩展性。
比特币:从 ECDSA 到 Schnorr
比特币自 2009 年创世区块起就使用 secp256k1 曲线上的 ECDSA 签名。Satoshi Nakamoto 选择 secp256k1 而非 NIST P-256 的原因至今仍有争议,但一个被广泛接受的解释是:secp256k1 的参数选择具有可验证的随机性(verifiably random),而 P-256 的种子来源不明,存在潜在的后门风险。
2021 年 11 月,比特币激活了 Taproot 升级(BIP 340/341/342),引入了基于 secp256k1 的 Schnorr 签名。BIP 340 对标准 Schnorr 签名做了若干工程优化:
- 仅使用 x 坐标:公钥和 nonce 点 R 只编码 x 坐标(32 字节),通过隐含地选择偶数 y 坐标来消除歧义。这将公钥从 33 字节(压缩 ECDSA)缩减到 32 字节。
- tagged hash:哈希计算使用 SHA-256(tag || tag || data) 的形式,其中 tag 是特定上下文标签的哈希,防止不同协议上下文之间的哈希碰撞。
- 批量验证友好:验证方程 sG = R + eP 的形式天然支持高效的批量验证,多个签名可以通过随机线性组合在一次多标量乘法中完成验证。
Taproot 最深远的影响在于多签场景:通过 MuSig2 协议,一个 n-of-n 多签交易在链上的表现与普通单签交易完全相同——相同的签名尺寸、相同的验证成本、相同的链上占用空间。这不仅节省了区块空间,还显著提升了隐私性,因为观察者无法区分单签与多签交易。
以太坊:ECDSA 与预编译合约
以太坊同样使用 secp256k1 曲线上的 ECDSA 签名,但其签名体系与比特币存在几个关键差异:
ecrecover 预编译合约。 以太坊在地址 0x01 部署了 ecrecover 预编译合约,允许智能合约从签名中恢复签名者的公钥(进而得到以太坊地址)。这一设计使得链上签名验证的 gas 成本大幅降低,是 ERC-20 permit、EIP-712 结构化签名、元交易(meta-transaction)等模式的基础。ecrecover 的函数签名为:
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
其中 v 是恢复标识符(recovery id, 27 或 28),用于从签名 (r, s) 中唯一确定公钥。
EIP-7212:P-256 曲线预编译。 2024 年,以太坊通过 EIP-7212 引入了 P-256(secp256r1)曲线的签名验证预编译合约。这一看似”回退”到 NIST 曲线的举措,实际上是为了支持硬件安全模块(HSM)和移动设备中广泛部署的 P-256 密钥——Apple Secure Enclave、Android Keystore、WebAuthn/Passkeys 均使用 P-256。EIP-7212 使得用户可以直接用手机的生物识别硬件签名以太坊交易,无需管理独立的 secp256k1 私钥。
以太坊 2.0 信标链:BLS 签名聚合
以太坊共识层(信标链)使用 BLS12-381 曲线上的 BLS 签名来处理验证者的投票与证明(attestation)。这一选择源于一个核心的可扩展性需求:信标链每个 epoch(约 6.4 分钟)需要处理数十万个验证者的签名。
信标链的 BLS 聚合流程如下:
- 每个验证者使用自己的 BLS 私钥对当前 epoch 的检查点(checkpoint)进行签名,生成单独的 BLS 签名 σ_i。
- 同一委员会(committee)内的签名由聚合者(aggregator)合并为一个聚合签名 σ_agg = σ_1 + σ_2 + … + σ_k。
- 区块提议者(proposer)将多个委员会的聚合签名打包进区块。
- 验证节点对每个聚合签名执行一次配对运算即可验证整个委员会的投票。
这种分层聚合使得信标链的签名验证成本从 O(n)(n 为验证者数量)降到接近 O(c)(c 为委员会数量),是支撑以太坊数十万验证者规模的关键技术。BLS12-381 曲线的选择则兼顾了安全强度(约 128 比特)和配对运算效率。
十一、签名方案选型
何时使用哪种方案
Ed25519 是大多数新系统的默认推荐:
- SSH 密钥认证(已成为 OpenSSH 默认)。
- 软件包签名(如 Minisign、signify)。
- 需要高性能签名验证的应用。
- 嵌入式或受限环境(确定性签名消除了对高质量随机源的依赖)。
ECDSA P-256 适用于:
- 需要与 NIST 标准或政府合规框架兼容的场景。
- TLS 1.2 及更早的生态系统。
- 已有大量 P-256 基础设施的遗留系统。
- 务必搭配 RFC 6979 使用。
Schnorr 签名 适用于:
- 比特币及基于比特币的区块链系统(Taproot/BIP 340)。
- 需要原生多重签名支持的协议设计。
- 需要在协议层面利用签名线性性质的场景。
BLS 签名 适用于:
- 大规模共识系统(如以太坊 2.0 信标链)。
- 需要非交互式签名聚合的分布式系统。
- 阈值签名方案。
量子抗性考量
上述所有方案——ECDSA、EdDSA、Schnorr、BLS——都基于椭圆曲线离散对数问题或配对问题的困难性,而这些问题都可以被量子计算机上的 Shor 算法在多项式时间内求解。因此,它们在后量子时代都是不安全的。
NIST 后量子密码学标准化进程已经选定了基于格的签名方案 Dilithium(现更名为 ML-DSA)和基于哈希的签名方案 SPHINCS+(现更名为 SLH-DSA)。对于需要长期安全性保证的系统(例如需要在 2030 年后仍然安全的数字签名),应当开始规划向后量子签名方案的迁移。
一种渐进的迁移策略是使用混合签名(Hybrid Signature):同时使用一个经典方案(如 Ed25519)和一个后量子方案(如 ML-DSA)进行签名。验证时要求两个签名都通过。这种方式确保即便后量子方案存在未知弱点,经典方案仍然提供保护;反之亦然。
未来方向
签名方案的发展远未停滞。几个值得关注的前沿方向包括:
- 阈值签名(Threshold Signature):(t, n) 阈值方案允许 n 个参与者中的任意 t 个合作生成有效签名,而少于 t 个参与者无法获取任何关于私钥的信息。FROST(Flexible Round-Optimized Schnorr Threshold)是 Schnorr 阈值签名的最新成果。
- 盲签名(Blind Signature):签名者在不知道消息内容的情况下进行签名,用于隐私保护的数字现金和匿名凭证系统。
- 环签名(Ring Signature):签名者可以证明自己属于某个群体,但不暴露具体身份,用于 Monero 等隐私加密货币。
- 可验证随机函数(Verifiable Random Function, VRF):在 Schnorr/EdDSA 基础上构造的可验证伪随机输出,用于区块链的领导者选举和随机信标。
数字签名是公钥密码学的核心组件,其设计与实现的每一个细节都可能影响整个系统的安全性。从 DSA 到 ECDSA,从 RFC 6979 的修补到 EdDSA 的从头设计,从单一签名到多重签名与聚合签名——这条演进路径清晰地展现了密码学工程的一条核心教训:安全性不仅取决于数学假设的困难性,更取决于协议设计是否为实现者留下了犯错的空间。最好的密码学方案是那些让正确的实现成为最简单实现的方案。
密码学百科系列 · 第 18 篇
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【密码学百科】区块链中的密码学原语
聚焦区块链实际使用的密码学原语——哈希链、默克尔树与 MPT、ECDSA/Schnorr/BLS 签名、Taproot 与 MuSig2 多方签名、VRF 与 RANDAO 共识随机性,以及承诺方案在链上的典型模式
【国密算法与国密 TLS 系列】SM2 vs ECDSA/X25519:椭圆曲线的国产方案到底怎么样
从曲线参数、签名/加密/密钥交换算法、标量乘法实现到侧信道防护,全方位对比 SM2、P-256 和 Curve25519 三条主流椭圆曲线,拆解 simple_gmsm 源码中的点乘实现,分析国密方案在安全性与工程生态上的优劣。
【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算
椭圆曲线密码学用更短的密钥实现了与 RSA 等价的安全性——本文从实数域上的几何直觉出发,逐步过渡到有限域上的离散点群,深入曲线选择、标量乘法优化与安全曲线标准
【密码学百科】零知识证明入门:如何证明你知道而不泄露
零知识证明是密码学中最反直觉的概念之一——本文从阿里巴巴洞穴的直觉出发,建立完备性、可靠性、零知识性的形式化理解,并实现 Schnorr 身份认证与 Fiat-Shamir 变换