专业零知识证明漏洞 - Trail of Bits博客
零知识(ZK)证明是近年来备受关注的密码学工具,主要应用于加密货币领域。ZK证明的核心思想是:拥有秘密信息(如加密密钥)的一方可以在不泄露秘密的情况下证明其真实性。加密货币目前利用ZK证明实现匿名性、交易隐私和“roll-up”系统(通过ZK证明批量处理交易以提高区块链效率)。ZK证明还被更广泛地应用,例如允许安全研究人员证明他们知道如何利用软件漏洞而无需透露漏洞信息。
然而,与密码学中的大多数事物一样,很难做到万无一失。本文重点讨论某些专用ZKP代码中的两个漏洞,这些漏洞允许恶意行为者欺骗流行软件接受无效证明,包括对群签名无效输入的“有效性证明”,进而导致无效签名。在使用阈值签名的区块链系统(如ThorChain和Binance)中,攻击者可能因此阻止目标交易完成,造成对整条链或特定参与者的拒绝服务攻击。
离散对数证明背景
一种专用ZK证明是离散对数知识证明。假设Bob向Alice提供RSA模数N = PQ(其中P和Q是只有Bob知道的大素数),Bob想向Alice证明他知道秘密指数x,使得s ≡ tx (mod N)。即x是s关于底数t的离散对数,且他希望在完全不透露x的情况下证明自己知道x。
协议工作流程如下:
- Bob和Alice约定安全参数k(正整数,决定协议执行次数,通常设为k=128)。
- Bob从ZΦ(N)中随机采样ai(i=1,2,…,k),计算对应值αi = tai (mod N),并将α1,α2,…,αk发送给Alice。
- Alice回复随机比特序列c1,c2,…,ck。
- Bob计算zi = ai + cix,并将z1,z2,…,zk发送给Alice。
- Alice检查是否对所有i=1,2,…,k满足tzi ≡ αisci (mod N)。若所有检查通过,则接受证明;否则拒绝。
工作原理
如果Bob不知道x,他每次有50%概率猜对Alice的ci选择。若任意一次猜测错误,Alice会拒绝证明。当k=128时,Bob成功欺骗的概率极低(约1/2^128)。
在非交互式证明中(如下述代码),双方通过哈希计算挑战值c = Hash(N ∥ s ∥ t ∥ α1 ∥ … ∥ αk),其比特位作为ci值(称为Fiat-Shamir变换)。本文讨论的漏洞不涉及Fiat-Shamir失败。
代码分析
证明结构和验证代码源自Binance的tss-lib库。Proof结构如下:
type Proof struct {Alpha, T [Iterations]*big.Int
}
包含两个大整数数组Alpha和T,分别对应数学描述中的αi和zi值。值得注意的是,该结构未包含模数N或值s和t。
验证函数Verify实现:
func (p *Proof) Verify(h1, h2, N *big.Int) bool {// 详细代码见原文
}
其中h1和h2分别对应t和s。函数首先计算挑战值c,然后对c的每个比特位ci检查:
h1ExpTi ≡ alphaIMulH2ExpCi (mod N)
若任意检查失败则拒绝证明。
漏洞详情
Verify函数未对h1、h2、p.Alpha或p.T进行有效性验证,导致可触发多种边缘情况。特别是涉及对数和指数关系时,零值需要特别注意:对于任何x ≠ 0,0x = 0;对于任何x,0 ∙ x = 0。攻击者可利用这些特性使等式检查始终通过。
第一个不可能事件:基数为0的离散对数
假设Bob创建Proof结构:
- 所有p.T元素为正(zi > 0)
- 所有p.Alpha元素为0(αi = 0)
使用参数调用Verify:
- N为两大素数乘积
- h1为任意整数(s无约束)
- h2设为0(t=0)
验证过程会检查tzi ≡ αisci (mod N)。右侧αi=0导致αisci=0;左侧tzi=0zi=0(因zi>0)。验证函数看到0=0即接受证明。但事实上,若s ∉ {0,1},不存在整数x使s ≡ 0x (mod N)成立。
修复方案:验证h2和p.Alpha所有元素非零。最佳实践应包括检查所有密码学值(例如确保椭圆曲线点位于曲线上,整数在适当区间内并满足乘法性质)。具体到此证明,应验证h1、h2、p.Alpha非零、与N互质且在区间[1,N)内,同时对N进行基本检查(如位长检查)。
离散对数加密证明
在某些阈值签名协议中,签名过程的一个步骤需要同时证明关于Bob知道的秘密整数x的两件事:
- X = Rx,其中X和R在阶为q的群G中(通常为某模数的乘法整数群或椭圆曲线群)
- 密文c = PaillierEncN(x,r)(随机值r ∈ Z⋆N,Bob公钥N),即c = (N + 1)xrN (mod N2)
证明c与X的离散对数之间的一致性可确保Bob对椭圆曲线签名的贡献值与协议早期阶段贡献的值相同,防止Bob提交导致无效签名的虚假X值。
代码实现
以下代码取自kryptology库的Paillier离散对数证明实现,用于计算v̂:
func (p PdlProof) vHatConstruct(pv *PdlVerifyParams) (*big.Int, error) {// 代码细节见原文
}
验证函数Verify使用vHatConstruct计算上述v̂值。在有效证明中一切正常。
漏洞详情
在无效证明中,Bob可设置v = s = 0。此时c值无关紧要:Alice最终检查v̂ = 0N ∙ (N+1)s1 ∙ c−e = 0 = v,并接受结果。
第二个不可能事件:任意密文
通过利用v̂ = s = 0问题,Bob可证明他知道x满足X = Rx,同时向Alice“证明”任何c ≠ 0的值都是x的有效密文。Bob甚至不需要知道N的因子分解——再次“证明”了不可能之事!
此伪造具有实际安全影响:能够伪造此证明允许Bob破坏阈值签名协议而不被检测。在某些系统中,这可能用于阻止有效交易执行。
修复方案:通过更好的输入验证防止此问题。基本验证应包括检查z和s在Z*N中(即gcd(z,N)=gcd(s,N)=1,强制z≠0且s≠0),同时确保s1≠0和s2≠0。
风险与披露
风险
这些漏洞存在于实现GG20阈值签名方案的代码库中。若攻击者利用密文可塑性漏洞,可“证明”群签名无效输入的有效性,导致无效签名。若区块链依赖阈值签名,攻击者可能因此阻止目标交易完成。
披露
已向Binance报告tss-lib问题并迅速修复。同时联系了多个依赖tss-lib(或其分支)的项目,包括已修复代码的ThorChain(Joltify和SwipeChain直接依赖ThorChain分支)。Keep Networks维护自己的tss-lib分支并集成了修复。
kryptology问题已向Coinbase报告,该GitHub项目已被归档,未发现当前项目依赖该库的阈值签名实现。
经验教训
最终,这是源于可理解的数据验证疏忽的密码学失败。使用另一方提供的值前,应始终检查所有适用约束——甚至从这些值计算出的结果也应检查所有约束。
但查看这些ZK证明的数学描述或伪代码时,这些约束在哪里明确说明?这些文档从数学而非具体实现角度描述算法。例如步骤β←$ZN,后跟v = (N+1)αβN (mod N2)。数学上理解v在ZN2中故v≠0,但编程层面没有明确指示需要检查v的约束。
Trail of Bits在zkdocs.com维护ZK证明系统资源指南,此类问题是我们提供该指南的主要动机之一——将数学和理论描述转化为软件是困难的过程。诚然,我们自己的描述本可以更清晰地解释这些检查,希望在未来的版本中修复。
Trail of Bits喜欢向审计员和密码学家提供的一条建议是:警惕两个特殊值0和1(及其类似值,如无穷远点)。与0相关的漏洞过去曾造成问题(例如此处和此处)。本例中,未检查0导致两个独立漏洞,允许攻击者在阈值签名方案中将诚实方引入歧途。
(原文包含社交媒体分享链接及博客页脚信息,此处按准则省略非技术内容)
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码