一次签名即致命:SecondFi钱包的Ed25519实现缺陷导致私钥泄露

현재 언어 번역이 없어 원문을 표시합니다.
6月21日至23日,Cardano主网上数以千计的钱包遭遇攻击,约 1600 万枚 ADA 和大量 NFT 被转移,总价值约 250 万美元。此外,约 1.29 亿枚 ADA(价值约 2000 万美元)被 SecondFi 以安全原因自行转移到第三方托管机构。

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主网上签名过的地址,其私钥均已公开可推导。其开发团队应当使用测试向量对签名逻辑的实现进行严格验证,确保签名器接收到的内容与派生阶段产生的内容完全一致。

공유하기:

작성자: Beosin

이 글은 PANews 입주 칼럼니스트의 관점으로, PANews의 입장을 대표하지 않으며 법적 책임을 지지 않습니다.

글 및 관점은 투자 조언을 구성하지 않습니다

이미지 출처: Beosin. 권리 침해가 있을 경우 저자에게 삭제를 요청해 주세요.

PANews 공식 계정을 팔로우하고 함께 상승장과 하락장을 헤쳐나가세요
PANews APP
现货黄金突破4110美元/盎司,现货白银日内大涨4%
PANews 속보