跳到主要内容
ecdsa.com

确定性 ECDSA vs 随机化 ECDSA:RFC 6979、随机与对冲 nonce

每个 ECDSA 签名都需要一个保密、唯一且无偏差的 nonce。三种生成方式的失效方式各不相同,而验证者无法区分它们。

确定性 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.2RFC 6979 §3.2;FIPS 186-5 §6.3.2 和附录 A.3.3RFC 6979 §3.6 中的可选项;已过期的 CFRG 草案;BIP-340 辅助随机数
同一消息签名两次签名不同签名相同签名不同
随机数生成器较弱或已损坏私钥可能泄露不受影响不受影响
重复签名时遭受故障注入难以利用可能泄露私钥难以利用
可重现的测试向量否是(RFC 6979 附录 A.2)仅在额外输入固定时
验证者需要的改动无无无
默认采用OpenSSL、Node.js、Java、Python cryptographylibsecp256k1、@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 就能看出区别:

Python:用同一个 P-256 密钥生成随机化和确定性 ECDSA 签名
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 策略只影响签名者。密钥、签名格式和验证者都保持不变。

  1. 检查库的支持情况和版本:OpenSSL 3.2 或更高版本、cryptography 43 或更高版本,或内置 RFC 6979 的库。
  2. 确保显式设置摘要算法,因为启用 RFC 6979 后,派生过程依赖于它。
  3. 添加回归测试:对同一消息签名两次,并断言结果符合预期(确定性签名应相同,对冲签名应不同)。
  4. 仍要保证随机性良好:对冲签名依然受益于优质的随机数生成器,而密钥生成始终离不开它。
  5. 检查所有比较或缓存签名的地方,因为确定性签名首次让“相同输入、相同输出”成立。

参考资料

全部对比