签名随机数复用:一次重用的手滑怎么倒推出私钥 图 1
签名随机数复用:一次重用的手滑怎么倒推出私钥 · 图 1

区块链安全里最反直觉的一课:泄露私钥不需要偷到私钥,只需要偷到一次“运气”。ECDSA 签名有个一次性配角,通常记作 k,工程圈也叫它 nonce。同一个私钥签的两笔名如果撞了同一个 k,攻击者用两条小学代数方程就能把私钥算出来。这不是理论演习,历史上真金白银地发生过。

签名方程里的一次性秘密

ECDSA 的骨架是椭圆曲线乘法。签名时先选一个只对这一笔生效的随机数 k,算出曲线上的点 k·G,取其横坐标作为 r;再用 k 的模逆把消息哈希 z 和私钥 d 缝进第二个分量:s = k⁻¹ (z + r·d)。验签方拿公钥反推等式是否成立。设计意图很清楚:k 是一次性的,用完即弃,别人从 r 反推 k 要再解一次离散对数。安全链条最薄的一环也在这里:算法本身不检查 k 是否真的每笔都新——它假设实现者会诚实抽样。

撞车的代价:两行解出私钥

设想同一把私钥的两笔签名用了相同的 k(消息可以不同)。两式相减:s1 − s2 = k⁻¹ (z1 − z2),右边只剩公开量,于是 k = (z1 − z2) ÷ (s1 − s2),随机数当场暴露;回代任一式,d = (s1·k − z1) ÷ r,私钥现形。整个过程只用签名数据里的公开字段,不需要接触签名设备。同一笔消息若被不同参数(比如不同哈希上下文)签了两次也同理。更隐蔽的是弱随机:k 不必完全相等才危险,只要取样范围比曲线要求的窄——比如只有低 32 位是真随机——攻击者就能在算力可行的范围内穷举与统计推断。复用是极端情形,偏差是慢性版本。

历史案例:代数课在真实世界开班

公开档案里有几堂著名的课。其一是游戏主机:索尼 PlayStation 3 固件用 ECDSA 校验程序签名,实现里 k 值近乎恒定,2010 年前后被外部研究团队(fail0verflow 成员在公开会议与博客中)按上述代数直接解出签名私钥,等于拿到了给任意代码背书的印章——这次翻车发生在 DRM 世界,与加密币无关,但成了教科书引用最多的算例。其二是安卓比特币钱包:2013 年披露的实现缺陷让钱包生成密钥后的签名 k 只有部分位真随机,安全研究者据此证明了从公开交易里恢复私钥的可行性,并记录到部分早期地址的币被转移到新地址;社区另有更早的报告显示,有用户因 JVM 随机源缺陷导致签名随机数问题而损失大额比特币,具体数额流传版本不一,这里不当精确史实引用。其三是一般化的提醒:浏览器与嵌入式设备的熵源贫乏问题(开机初期缺少环境噪声)是所有软件签名共同的雷区,历史上不止一种密码库因此翻车。

防御线:从“求随机靠谱”到“不需要随机”

第一代对策是把随机源做扎实:操作系统熵池、硬件噪声源、签名器内产生 k。第二代对策更彻底:干脆取消对随机的依赖。2013 年的 IETF 标准 RFC 6979 规定用 HMAC 结构把私钥与消息哈希派生成确定性的 k——同一把密钥签同一条消息永远得到同一签名,k 不随机,因此永不复用,且不泄露可推导信息。今天确定性签名已被主流钱包与签名库广泛采用,比特币的 BIP-340 在 Schnorr 签名语境下同样规定了确定性的辅助随机值生成路径。用户侧的实践结论很朴素:选择有审计与历史包袱的钱包实现;老钱包把币迁到新地址组合时,本质上是把签名随机性的历史包袱留在原地;把私钥导出到不信任的工具里重签大量交易前,先小额验证。顺带澄清一个高频混淆:以太坊账户里那个递增的 nonce 是交易计数器,管重放与顺序;本文的 k 是签名代数里的一次性秘密,两者同名不同命,泄露了前者只是浪费序号,撞上后者才可能丢币。

一句话带走

ECDSA 的信任结构里,私钥是墙,随机数是门缝:墙不会被正面推倒,门缝一次没关严就够了。理解这一点,才算理解了为什么钱包软件“看起来一样的签名按钮”,工程上的差别可以值多少钱。签名与私钥机制科普不涉及任何收益或价格判断,不构成投资建议。