一行代码缺失,千万美元蒸发: 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. 使用经过审计的库
永远不要自己造加密轮子。使用成熟、审计过的库:
- Rust:
ed25519-dalek(标准且经过审计的实现) - JavaScript/TypeScript:
@noble/ed25519或tweetnacl - Python:
pynacl(libsodium 绑定)
对于 Cardano,请使用Cardano Serialization Library(@emurgo/cardano-serialization-lib),它经过审计并遵循正确的扩展 Ed25519 规范。
2. 在所有层级保留秘密前缀
在通过适配器层传递密钥时,确保完整的扩展私钥(kL和kR两者)都保留:
// 正确:传递完整的 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。
所有这一切,只是因为少写了一行代码。
