确定性 ECDSA 与随机化 ECDSA 的区别只在于一个值:每次签名使用的 nonce k。它的质量决定了私钥能否保密,因为只要有一次 nonce 重复,或者许多签名中的 nonce 存在轻微偏差,就足以算出私钥。FIPS 186-5 允许用两种方式生成它,而第三种方式——对冲(hedged)签名——把这两种方式结合了起来。
概览
| 随机化 | 确定性(RFC 6979) | 对冲 | |
|---|---|---|---|
| nonce 来源 | 每次签名都使用新的 CSPRNG 输出 | 基于私钥和消息哈希的 HMAC-DRBG | 确定性派生与新鲜随机数混合 |
| 标准 | FIPS 186-5 §6.3.1、附录 A.3.1 和 A.3.2 | RFC 6979 §3.2;FIPS 186-5 §6.3.2 和附录 A.3.3 | RFC 6979 §3.6 中的可选项;已过期的 CFRG 草案;BIP-340 辅助随机数 |
| 同一消息签名两次 | 签名不同 | 签名相同 | 签名不同 |
| 随机数生成器较弱或已损坏 | 私钥可能泄露 | 不受影响 | 不受影响 |
| 重复签名时遭受故障注入 | 难以利用 | 可能泄露私钥 | 难以利用 |
| 可重现的测试向量 | 否 | 是(RFC 6979 附录 A.2) | 仅在额外输入固定时 |
| 验证者需要的改动 | 无 | 无 | 无 |
| 默认采用 | OpenSSL、Node.js、Java、Python cryptography | libsecp256k1、@noble/curves | 需显式启用:@noble/curves extraEntropy: true、BIP-340 签名 |
三种方式如何失效
随机 nonce 完全依赖随机数生成器。如果它重复输出同一个值(索尼的 PlayStation 3 用的是一个常数),简单的代数运算就能算出私钥。即使只是存在偏差,例如最高几位总是为零,或者时序侧信道泄露了 nonce 的长度,求解隐藏数问题的格方法也能从几百到几千个签名中恢复出私钥;Minerva 和 TPM-Fail(2019年)正是这样攻破了智能卡和 TPM。启动后不久就进行签名的嵌入式设备、克隆的虚拟机和 fork 出的进程,都是典型的高风险环境。
确定性 nonce 把随机数生成器排除在外:k 由私钥和消息哈希通过 HMAC 派生,因此相同的输入总是得到相同的 nonce,而不同的消息得到互不相关的 nonce。风险随之转移到物理攻击上。如果攻击者能在设备对同一消息签名两次时注入毛刺(glitch),使用同一 nonce 的一个正确签名和一个错误签名就可能泄露私钥;2018年,Poddebniak 等人针对确定性 ECDSA 和 EdDSA 演示了这类故障攻击。确定性签名还会让签名可以重复出现:有些协议需要这一点,另一些协议则不得泄露这一点。
对冲 nonce 把确定性输入和新鲜随机数一起送入派生过程。随机数生成器损坏时,该方案退化为确定性签名;而故障攻击每次面对的都是不同的 nonce。RFC 6979 §3.6 本就允许额外输入,BIP-340 建议使用 32 字节的辅助随机数,ML-DSA(FIPS 204 §3.4)默认采用对冲签名,而一份旨在标准化对冲 ECDSA 和 EdDSA 的 IRTF 草案已经过期(2025年),未能成为 RFC。
三种方式产生的都是普通签名。验证者无法区分它们,这也意味着你无法通过输出来审计签名者的 nonce 策略,除非注意到相同的消息产生了相同的签名。
各个库的做法
- OpenSSL 默认使用随机 nonce 签名。OpenSSL 3.2(2023年11月)新增了 RFC 6979 支持:将签名参数
nonce-type设为 1 即可启用,此时必须显式指定摘要算法;OpenSSL 3.6 又把确定性 ECDSA 引入了它的 FIPS 提供者。命令行用法:openssl pkeyutl -sign -pkeyopt nonce-type:1 -pkeyopt digest:sha256。 - Python
cryptography默认采用随机化签名,自 43.0 版(2024年7月)起提供ec.ECDSA(hashes.SHA256(), deterministic_signing=True)。 - Node.js
crypto.sign()和 WebCrypto 使用随机 nonce,且没有可以更改的选项。 - @noble/curves 默认用 RFC 6979 派生
k;调用时传入extraEntropy: true或额外字节,则采用对冲签名。 - libsecp256k1(Bitcoin Core 使用的库)采用 RFC 6979,并可选择附加额外数据。
- EdDSA 按定义就是确定性的(RFC 8032);尽管如此,一些平台实现仍会加入随机性。
用 cryptography 46.0.3 写几行 Python 就能看出区别:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
key = ec.generate_private_key(ec.SECP256R1())
message = b"invoice 2026-10-20 #4411"
randomized = [key.sign(message, ec.ECDSA(hashes.SHA256())) for _ in range(2)]
deterministic = [
key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) for _ in range(2)
]
print("randomized signatures equal: ", randomized[0] == randomized[1])
print("deterministic signatures equal:", deterministic[0] == deterministic[1])
for sig in randomized + deterministic:
key.public_key().verify(sig, message, ec.ECDSA(hashes.SHA256()))
print("all four verify with the same verifier")
randomized signatures equal: False
deterministic signatures equal: True
all four verify with the same verifier
测试与可重现性
确定性签名让已知答案测试成为可能:RFC 6979 附录 A.2 列出了 P-192 到 P-521 以及其他曲线的预期签名,测试向量生成器和 Project Wycheproof 都建立在这类向量之上。对于随机化或对冲签名,你只能测试签名能通过验证、格式错误的签名不能通过验证。在审计中,确定性模式也是检查可疑签名者的方法:对同一消息签名两次,看输出是否重复。
要在已有的签名中检测实际发生的 nonce 重用,可以比较同一密钥所产生签名的 r 值;r 重复就意味着 nonce 重复,nonce 重用检测工具可以批量完成这项检查。
如何选择
- 操作系统随机数生成器状态良好的服务器和 CI:三种方式都可以接受;优先选择确定性或对冲签名,以消除一项依赖。
- 嵌入式设备、启动早期运行的代码、克隆的虚拟机:确定性或对冲签名,切勿使用纯随机 nonce。
- 可能被攻击者接触的硬件(硬件钱包、智能卡、支付终端):对冲签名,并配合故障防护措施,例如在输出每个签名之前先验证它。
- 测试套件和互操作性工作:确定性签名,以便测试向量可以重现。
- 重复出现相同签名会泄露信息的协议(例如泄露同一消息被再次签名这一事实):随机化或对冲签名。
迁移说明
切换 nonce 策略只影响签名者。密钥、签名格式和验证者都保持不变。
- 检查库的支持情况和版本:OpenSSL 3.2 或更高版本、
cryptography43 或更高版本,或内置 RFC 6979 的库。 - 确保显式设置摘要算法,因为启用 RFC 6979 后,派生过程依赖于它。
- 添加回归测试:对同一消息签名两次,并断言结果符合预期(确定性签名应相同,对冲签名应不同)。
- 仍要保证随机性良好:对冲签名依然受益于优质的随机数生成器,而密钥生成始终离不开它。
- 检查所有比较或缓存签名的地方,因为确定性签名首次让“相同输入、相同输出”成立。