当前位置: 首页 > news >正文

一行代码缺失,千万美元蒸发: SecondFi 钱包 Ed25519 签名漏洞全解密

SecondFi 钱包事件:一份面向开发者的加密学事后剖析

背景

2026 年 6 月 21 日至 23 日,SecondFi(原 Yoroi)Cardano 钱包中的一个致命漏洞导致约 1610 万 ADA(当时价值约 240 万美元)从 374 个用户钱包中被盗。根源?仅仅是在 Ed25519 签名实现中少写了一行代码。

作为开发者,我们常常把加密库当作黑盒来用。这次事件残酷地提醒我们:如何使用加密原语,和原语本身同样重要。让我们一步步拆解到底出了什么问题、为什么会发生,以及如何避免重蹈覆辙。


技术拆解

Ed25519 签名本该怎么做

Ed25519 是 RFC 8032 定义的一种确定性签名方案。与 ECDSA(其随机数重用已是众所周知的灾难)不同,Ed25519 的设计初衷就是彻底消除随机数失败的风险, 它从私钥材料和消息中确定性地派生出临时随机数(nonce)。

对于 Cardano 的扩展 Ed25519 变体,钱包存储一个 64 字节的扩展私钥,分为两部分:

  • kL(第 0–31 字节):签名标量
  • kR(第 32–63 字节):秘密随机数后缀(每个钱包独有,永不公开)

正确的签名流程如下:

r = SHA-512(kR || M) mod L # 随机数由秘密 + 消息派生 R = r * B # R 是公开的 k = SHA-512(R || A || M) mod L # 挑战标量 S = (r + k * kL) mod L # 签名分量

最终签名是(R, S)对。这个设计安全的原因:

  • 确定性:同一消息 + 同一密钥 = 同一签名(无需依赖 RNG)
  • 秘密参与:随机数r依赖kR,而kR永远不会离开设备
  • 可验证:任何人都可通过[S]B = R + [k]A验证,无需知道私钥

SecondFi 实际做了什么

SecondFi Android 10.0.3 版本在从 BIP32-Ed25519 派生层向签名适配器层传递数据时,遗漏了关键的kR秘密前缀。只有签名标量被传给了底层签名器。

有缺陷的实现计算随机数的方式是:

r = SHA-512(M) mod L

仅此而已。没有任何秘密成分,只有交易体哈希, 而这是链上公开可见的

为什么这是灾难性的

当随机数可以公开计算时,攻击者只需一条链上签名就能恢复私钥。

给定任何交易中的公开值:

  • M:被签名的消息(交易体)
  • R:公开随机数点
  • A:公钥
  • S:签名分量

攻击者计算:

r = SHA-512(M) mod L # 和漏洞签名器一样 k = SHA-512(R || A || M) mod L # 和验证器一样 s = (S - r) * inverse(k) mod L # 恢复私钥

一条签名,一笔交易,你的私钥就归别人了

安全研究员 Charles Guillemet 现场演示了这一点:他从主网上拉取签名,仅凭链上信息就重建了私钥。


时间线

日期事件
6 月 8 日SecondFi Android 10.0.3 发布,包含有缺陷的签名器
6 月 12 日早期用户交易已经开始泄露密钥
6 月 21–23 日两个独立攻击者团伙系统性清空 374 个钱包
6 月 22–23 日SecondFi 承认问题,进入维护模式
6 月 22 日SecondFi 紧急将约 1.29 亿 ADA 转移至第三方托管机构
6 月 24 日+补丁发布
7 月 22 日SecondFi 宣布永久关闭

根本原因:未经审计的 SDK

漏洞是在 6 月 8 日引入的,当时一个名为trantor的未经审计的第三方实验性 SDK(由一名独立开发者发布在 npm 上)替换了 EMURGO 此前审计过的签名模块。

这是一个关键的教训:绝不能在生产环境中用未经审计的代码替换经过审计的加密代码, 尤其不能跳过完整的安全审查。

Tibane Labs 的取证报告证实,有漏洞的签名器正是trantorSDK,它替换了经过验证的 EMURGO 构建版本。问题不在于 SDK 本身的意图,而在于它如何与 Cardano 扩展 Ed25519 签名流程集成, 秘密前缀根本没有通过适配器层传递。


代码级分析

有漏洞的实现(简化版)

importhashlib# 漏洞:完全省略了秘密前缀 kRdefflawed_sign(private_key_scalar,message):# 缺失:kR(秘密随机数前缀)# 本应是:r = SHA512(kR || message) mod L# 有漏洞:随机数只由消息派生r=SHA512(message)%L# 可公开计算!R=r*B k=SHA512(R||public_key||message)%L S=(r+k*private_key_scalar)%Lreturn(R,S)

正确的实现(RFC 8032)

importhashlibdefcorrect_sign(extended_secret_key,message):# extended_secret_key = kL (32 字节) + kR (32 字节)kL=extended_secret_key[0:32]kR=extended_secret_key[32:64]# 正确:随机数由秘密前缀 + 消息派生r=SHA512(kR||message)%L# 秘密且不可预测R=r*B k=SHA512(R||public_key||message)%L S=(r+k*kL)%Lreturn(R,S)

概念验证(玩具示例)

下面这个简化的 PoC 展示了漏洞原理:

importhashlib L=2**127-1# 不是真正的 Ed25519 阶,仅为演示defH(*parts):h=hashlib.sha512()forpinparts:h.update(pifisinstance(p,bytes)elsestr(p).encode())returnint.from_bytes(h.digest(),"little")%Ldefinv(x):returnpow(x,-1,L)# 受害者的私钥(玩具示例)secret_s=98765432123456789public_A=secret_s# 真实 Ed25519 中 A = s * BM=b"cardano tx body hash, toy example"# 漏洞签名器:前缀被丢弃,随机数仅由消息决定r=H(M)R=r# 真实 Ed25519 中 R = r * Bk=H(R,public_A,M)S=(r+k*secret_s)%Lprint("[受害者签名]")print("A =",public_A)print("R =",R)print("S =",S)# 攻击者:从公开数据恢复私钥r_public=H(M)recovered_s=((S-r_public)*inv(k))%Lprint("\n[攻击者]")print("恢复出的私钥:",recovered_s)print("匹配:",recovered_s==secret_s)

修复方案:应该怎么做

1. 使用经过审计的库

永远不要自己造加密轮子。使用成熟、审计过的库:

  • Rusted25519-dalek(标准且经过审计的实现)
  • JavaScript/TypeScript@noble/ed25519tweetnacl
  • Pythonpynacl(libsodium 绑定)

对于 Cardano,请使用Cardano Serialization Library@emurgo/cardano-serialization-lib),它经过审计并遵循正确的扩展 Ed25519 规范。

2. 在所有层级保留秘密前缀

在通过适配器层传递密钥时,确保完整的扩展私钥kLkR两者)都保留:

// 正确:传递完整的 64 字节扩展私钥fnsign_transaction(extended_secret:&[u8;64],message:&[u8])->Signature{letkL=&extended_secret[0..32];letkR=&extended_secret[32..64];// kR 必须用于随机数派生letr=sha512(kR,message);// ...}

3. 绝不用未经审计的代码替换已审计代码

trantorSDK 在 6 月 8 日替换了 EMURGO 的审计实现。这种变更本应触发:

  • 完整的安全审查
  • 加密正确性验证测试
  • 分阶段发布并伴随监控

4. 实施加密正确性的回归测试

测试随机数派生是否正确:

#[test]fntest_nonce_derivation_includes_secret_prefix(){letsecret=generate_test_secret();letmessage=b"test message";letsig1=sign(secret,message);letsig2=sign(secret,message);// 确定性:同一密钥 + 同一消息 = 同一签名assert_eq!(sig1,sig2);// 不同消息应产生不同随机数letsig3=sign(secret,b"different message");assert_ne!(sig1,sig3);// 关键:验证随机数不是简单的 SHA512(message)// (这需要访问内部状态或已知答案测试)}

5. 审计所有第三方依赖

trantorSDK 由一名独立开发者发布,从未被审计。在集成任何加密依赖之前:

  • 审查源代码
  • 检查已知漏洞
  • 验证维护者的信誉
  • 考虑进行全面安全审计

开发者的核心要点

  • Ed25519 设计上是确定性的——随机数由SHA-512(secret_prefix || message)派生。如果省略秘密前缀,随机数就变成了公开可计算的。

  • 当随机数可预测时,一条签名就足以恢复你的私钥。这比 ECDSA 随机数重用更糟糕, 后者通常需要两条签名。

  • 漏洞是通过用未经审计的代码替换已审计代码引入的。加密代码不是“即插即用”的, 集成方式至关重要。

  • 一旦密钥在链上被泄露,它就永远泄露了。区块链交易是不可逆的。EMURGO 警告用户,将受影响的助记词导入其他钱包并不能降低风险, 被泄露的地址必须彻底放弃。

  • 攻击窗口只有短短两周(6 月 8 日至 6 月 21 日),但已有 374 个钱包被清空。当漏洞如此容易利用时,攻击者可以行动得极快。


最后的思考

这不是一次复杂的攻击。没有零日漏洞,没有钓鱼活动,没有智能合约漏洞,也没有助记词被偷。攻击者只是读取了区块链,然后做了一些算术。

正如 MyCrypto 创始人 Taylor Monahan 所指出的,这比 2011 年早期的比特币钱包漏洞还要严重。这是一次流程上的失败, 在未审查的情况下替换审计过的加密代码,未能测试集成,然后将其推送给数百万用户。

SecondFi 将永久关闭。374 名用户失去了他们的资金。一位支持 Cardano 九年的用户损失了为退休而积攒的 998,000 ADA。

所有这一切,只是因为少写了一行代码。

http://www.jsqmd.com/news/1337021/

相关文章:

  • 本地AI项目部署实战:从环境准备到API集成的全流程指南
  • Pandas数值运算与缺失值处理实战:从原理到避坑,构建数据分析基石
  • STM32F103C8T6驱动光敏传感器+OLED 显示实验(ADC 采集光敏电阻完整教程)
  • 数字电路核心技能:T触发器与SR、JK、D触发器的相互转换原理与工程实践
  • 第 09 篇 快照读 vs 当前读:为什么 RR 下依然可能出现幻读
  • 2026 年现阶段,叶城有实力的锌钢仿竹护栏厂家哪个好,小区里那阵再也没响起的“竹子”敲门声,原来藏着这样的秘密-振昂丝网 - 企业推荐官【认证】
  • GaussDB账户锁定策略配置:从原理到生产环境实战避坑指南
  • WiFi同频干扰诊断与优化:从信道原理到实战解决网络卡顿
  • Python数据分析实战:构建足球明星生涯数据平台
  • I2C总线负载过重?五大场景解析缓冲器选型与实战部署
  • MySQL索引类型详解:B+Tree、Hash等核心原理与实战场景
  • STM32物联网项目实战:智能风扇的硬件设计、固件开发与云平台接入全解析
  • A*-第K短路
  • 蜂助手46亿元算力大单背后:30亿元采购接近蜂助手总资产八成
  • VSCode远程连接阿里云DSW:ProxyClient模式原理与实战指南
  • C++网络编程核心:Socket API、并发模型与高性能服务器实践
  • Linux系统挂载WebDAV远程存储:davfs2配置与实战指南
  • 滨海新区Q235C钢板实力公司如何选更稳妥 - 品牌优推
  • 2026 年新消息:荆门可靠的面包凹槽管定做厂家哪家好,你从没留意过的烘焙小物,竟能让面包成型快10倍? - 企业推荐官-
  • 从苹果300亿美元诉讼看人脸识别开发:BIPA合规与设备端隐私实践
  • 企业微信发Gmail失败?SPF、DKIM、DMARC配置全解析与实战排查
  • 多目标优化实战:无人机配送如何同时打赢“快“与“省“两场仗
  • 揭秘鄂尔多斯网站建设背后的真相与避坑指南,打造真正适合本地企业的数字化门面
  • 鸿蒙PC开发:自适应布局实战与优化技巧
  • 钉钉机器人Markdown消息推送实战:从Webhook到企业级应用
  • 【华为新品发布】尊界MPV+全场景新品集体登场
  • 已授权老客唤醒外呼总封号|3坐席1200包月AXB合规方案
  • 从用户到创造者:技术人如何逆向工程黑盒系统并掌握协议设计
  • 母线槽导体选材:高纯铜、铜排厚度与全长镀锡对运行温升的影响
  • Qt Windows应用管理员权限启动:UAC机制、清单配置与QMake实战