在公钥密码学的版图中,数字签名(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
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。
具体而言,RFC 6979 使用 HMAC-DRBG(一种基于 HMAC 的确定性随机比特生成器)来计算 k:
k = HMAC_DRBG(private_key, H(message))
内部过程分为以下步骤:
- 设 h1 = H(m) 为消息的哈希值。
- 初始化 V = 0x01 01 … 01(长度等于哈希输出长度的全 1 字节串)。
- 初始化 K = 0x00 00 … 00(全 0 字节串)。
- K = HMAC_K(V || 0x00 || int2octets(d) || bits2octets(h1))。
- V = HMAC_K(V)。
- K = HMAC_K(V || 0x01 || int2octets(d) || bits2octets(h1))。
- V = HMAC_K(V)。
- 循环生成候选 k,直到其落在 [1, n-1] 的有效范围内。
这个方案的优雅之处在于:
- 确定性:相同的私钥和消息总是产生相同的 k,因此产生相同的签名。这使得签名过程可测试、可复现。
- 安全性:由于私钥 d 参与了 k 的推导,攻击者在不知道 d 的前提下无法预测 k。HMAC 的伪随机性保证了 k 的分布对外部观察者而言与均匀随机不可区分。
- 向后兼容:RFC 6979 生成的签名在格式上与标准 ECDSA 完全相同,验证端无需任何修改。
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 次单独验证的一半。这对需要高吞吐量签名验证的系统(如区块链全节点)非常有价值。
九、签名方案选型
何时使用哪种方案
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 篇