6月21日至23日,Cardano主网上数以千计的钱包遭遇攻击,约 1600 万枚 ADA 和大量 NFT 被转移,总价值约 250 万美元。此外,约 1.29 亿枚 ADA(价值约 2000 万美元)被 SecondFi 以安全原因自行转移到第三方托管机构。
漏洞分析
此次的核心问题在 Ed25519 的实现。Ed25519签名算法在设计之初就通过确定性nonce生成彻底杜绝了随机数故障。按 RFC 8032,Ed25519 签名时应从私钥派生出两部分:签名标量 s 和秘密前缀 prefix。签名 nonce r的应计算为:
r = SHA512(prefix || M) mod L
其中 prefix 是密钥扩展(SHA-512(seed))的前32字节,这正是Ed25519安全的关键。而SecondFi的实现恰恰遗漏了这个prefix。在密钥从BIP32-Ed25519派生层传递到签名适配层的过程中,nonce r的秘密前缀被彻底遗漏,只有签名标量被传递给了底层签名器。最终签名器实际执行的运算是:
r = SHA512(M) mod L
这使得nonce r 从秘密值变成公开可计算值。而在Ed25519中,还有以下标准对象:
B:基点,也叫 base point / generator。它是 Ed25519 曲线上预定义的固定点。私钥标量乘以 B 会得到公钥点。
A:公钥点。
如果私钥签名标量是 s,那么:
A = s * B
L:基点 B 所在子群的阶,也就是标量运算的模数。Ed25519 的所有标量计算都在 mod L 下进行。
M:被签名的消息。在普通 Ed25519 里,M 就是调用签名函数时传入的消息字节串。
所以签名流程可以简化理解为:
s = private key signing scalarA = s * B # public key
M = message to be signed
r = SHA512(prefix || M) mod LR = r * B
k = SHA512(R || A || M) mod LS = r + k * s mod L
最终签名通常包含 R 与 S,如果 r 公开可计算,攻击者只要拿到链上一笔由受影响钱包签出的交易,就能得到公开的M、R、A、S,并可以从公开的 M 算出 r,再用
s = (S - r) * inverse(k) mod L
直接恢复私钥签名标量 s。也就是说,某个地址只要曾经用受影响实现签过一次交易,它对应的私钥标量就已经泄露。
下面 PoC 用一个小素数阶的EdDSA代数模型复现漏洞原理,并不支持解析真实的Cardano交易和真实的链上签名:
import hashlib
L = 2**127 - 1 # not Ed25519's real group order, just for demonstration
def H(*parts): h = hashlib.sha512() for p in parts: h.update(p if isinstance(p, bytes) else str(p).encode()) return int.from_bytes(h.digest(), "little") % L
def inv(x): return pow(x, -1, L)
# Victim private/public scalar in a toy additive group.secret_s = 98765432123456789public_A = secret_s # In real Ed25519: A = s * B
M = b"cardano tx body hash, toy example"
# Buggy signer: prefix was dropped, so r is public and message-only.r = H(M)R = r # In real Ed25519: R = r * Bk = H(R, public_A, M)S = (r + k * secret_s) % L
print("[victim signature]")print("A =", public_A)print("R =", R)print("S =", S)
# Attacker only needs public A, R, S, and M.r_public = H(M)fingerprint = (R == r_public)recovered_s = ((S - r_public) * inv(k)) % L
print("\n[attacker]")print("bug fingerprint:", fingerprint)print("recovered secret:", recovered_s)print("success:", recovered_s == secret_s)
结语
本次SecondFi钱包安全事件的根本原因为Ed25519规范实现层面的错误,这一疏忽导致了灾难性的后果:任何使用SecondFi钱包在Cardano主网上签名过的地址,其私钥均已公开可推导。其开发团队应当使用测试向量对签名逻辑的实现进行严格验证,确保签名器接收到的内容与派生阶段产生的内容完全一致。

