签名里的随机数 k 为什么致命?从 k 值复用到确定性签名 图 1
签名里的随机数 k 为什么致命?从 k 值复用到确定性签名 · 图 1

一笔能在白板上算完的灾难

先摆结论:ECDSA 签名不是“对消息签一次名”,而是“对这条消息做一次临时数学实验”。每次签名,钱包先抽一个只此一次使用的秘密随机数 k,由它算出曲线上的点 R,签名由 R 的坐标 r 和一个值 s 组成,其中 s 满足方程 s = k⁻¹(z + r·d),d 是私钥,z 是消息哈希。注意 k 与 d 一起出现在式子里——这个结构埋着一颗地雷。假设有人用同一个 k 签了两条不同消息,得到 (r, s₁)(r, s₂),两式相减:s₁ − s₂ = k⁻¹(z₁ − z₂),攻击者立刻解出 k = (z₁ − z₂)/(s₁ − s₂),代回任意一式再解出 d = (s₁·k − z₁)/r。全程只需要两条公开签名和公开的哈希值,私钥就出现在纸上。这不是理论演习:主机游戏界早年正是靠收集两组重复 r 值的签名,反解出了厂商标签系统的私钥;2012 年发表的研究《Mining Your Ps and Qs》则用同样手法,从嵌入式设备、路由器和若干早期钱包里批量回收了数千把真实在用的密钥,论文里明确把病根指向弱随机源和实现缺陷——同一条链上出现两个相同 r 值的签名,就是开火信号。

为什么“好运气不够”,协议要改写

人类会问:那把随机数发生器做好不就行了?问题在于“做差”的姿势太多了:熵池冷启动、固件升级丢掉种子、时钟同步错误、调试分支复用缓存——2012 年那批中招设备大量是量产嵌入式硬件,随机数质量问题在出厂后多年才被发现,而发现之时资产早已易主。密码学的应对是干脆取消“抽随机数”这一步的义务:RFC 6979 把 k 定义为对私钥与消息哈希做确定性派生的结果,同样的钥匙签同样的消息永远得到同一个 k,不再有可失败的随机源;为了在批量场景防跨用户相关性,派生函数还允许掺入附加数据。比特币生态采纳了这条思路:早期客户端的签名实现使用 RFC 6979 风格的确定性派生(叠加附加随机输入以对抗特定侧信道),2021 年随 Taproot 生效的 BIP 340 Schnorr 签名则把这一点写进规范正文——nonce 由私钥、消息与辅助输入做带标签哈希得出,实现即使提供全零辅助值也保持正确与安全,等于把“随机性”降级为可有可无的加固层而非承重墙。

旁观者能做什么

对普通用户,这个知识点最实用的一面是被动预警。链上数据公开:任何时候观察到某个地址的两个未花费签名共享同一个 r 值,就等于公开宣告这把私钥已经作废,正确动作是立刻把该地址余额扫到全新密钥,而不是等“对方哪天想起来”。监控工具把这类事件称为地址的“死亡宣告”。反过来也提醒自己:任何要求“手动填写随机数”或用不可信固件签名的教程,都在把这笔账交给运气。

顺带一句:Schnorr 与 ECDSA 在数学上同根(k 复用同样致命),所以确定性化在 BIP 340 里被同等坚持。随机数的问题从来不是“会不会出错”,而是“出错是否可被旁观者立即发现并止损”——确定性签名输出一条确定性,随机实现输出一条静默泄露,这就是两种工程哲学的分界线。

把账算到底

为什么 k 的泄露半径这么小、后果却这么大?回到方程:私钥 d 被一次签名“切碎”后藏在两个未知数里,单条签名解不出任何信息;第二条同 k 的签名恰好在两条方程里消掉了同一个未知 k,方程组从“两未知一条方程”变成“两未知两条方程”,可解性就此翻转。这也是加固方向的答案雏形:任何让第二条签名“换一个新 k”的机制都在恢复欠定状态。RFC 6979 的派生式 k、BIP 340 的带标签哈希,乃至对 k 做盲化的防侧信道设计,本质都在做同一件事——让“同一个 k 出现在两条消息上”成为工程上几乎不可能、出事时也能从签名本身看出的事件。用户侧的对应操作只有一条:签名异常提示出现时,把该地址当成已泄露处理,先搬家再查原因。

快速问答

问:同一个地址对同一消息签两次会泄露吗? 确定性实现下签名完全相同,k 复用同一 k 同一消息的代数式无新信息,不构成泄露条件。

问:怎么查链上有没有 r 值相撞? 区块浏览器开发者接口或开源监控脚本都能扫描重复 r;日常层面,钱包若提示“该地址签名异常”务必当真。

风险提示:本文是密码学机制科普,不构成投资建议;私钥操作请以所使用钱包与库的官方安全文档为准。