感谢Trapdoor Tech李星老师和安比实验室Even的帮助
Geometry是Kobi Gurkan 等人所在的一个新成立不久的研究组织,Kobi本人曾给ZK HACK 出过9道puzzles,这次又合作给出了这个关于Groth16延展攻击的新puzzle:ZK Hack x Geometry Puzzle I
该puzzle的主要内容在这个github仓库中:zkhack-groth-puzzle,
英文题解参见这篇文章。
背景信息和中文题解,强烈推荐参考李星老师的文章。
SNARK领域中的延展攻击,是指给定敌手一个合法证明,敌手在不知道见证的条件下自己生成新的(对相同或不同公开输入的)合法证明。
对于相同公开输入的伪造证明的方法请参见这篇文章How to Generate a Groth16 Proof for Forgery,但是其中描述的两个攻击对于需要更改公开输入到特定值(如本题为攻击者的以太坊地址)时并不适用。
论文BKSV20 中的如下定理给出了Groth16抵抗延展攻击的一个条件:
定理:如果 是线性独立的,且 Span 。则Groth16在代数群模型中,离散对数假设下,可达到弱白盒模拟可抽取性质。
如果条件“ 是线性独立的,且 Span ”不满足,则可以构造更改公开输入的延展攻击。本文下文即描述在不满足该条件时如何利用该点进行延展攻击伪造证明。
固定某个, (分别是见证和公开输入的下标),使得多项式 分别和 线性相关。记:
于是和也线性相关,即存在某个线性因子使得:
Groth16的验证方程如下:
把项分离出来后如下:
如果只考虑配对操作后群中的指数,验证方程如下:
注意这种形式下未知的分母已经被消去了。
如果更改公开输入值为,结果变为:
和原始差了一项。通过修改方程右式中的如下,则可以在方程右边平衡左边原始结果多出来的:
注意上式使用了线性相关关系:
其中
在公开参数中已知可用。而公开参数里存储的是
不能被直接使用。
于是有:
依然只考虑配对操作后群中的指数,上式等于:
注意到最后一行正是原始验证方程的右式,这意味着敌手改变了公共输入,并对新的语句伪造出了合法的证明,即成功进行了延展攻击。
具体到本题电路中,公开输入a和见证b的相关约束为: , 公开输入只有,。
,
其中 = new_a , = a,
const buildMalleabeC = async (stringified_c, matching_base_index, matching_pub_input, new_public_input, lf) => {
const c = unstringifyBigInts(stringified_c)
const {fd: fdZKey, sections: sectionsZKey} = await binFileUtils.readBinFile(finalZkeyPath, "zkey", 2, 1<<25, 1<<23)
const buffBasesC = await binFileUtils.readSection(fdZKey, sectionsZKey, 8)
fdZKey.close()
const curve = await buildBn128();
const Fr = curve.Fr;
const G1 = curve.G1;
const new_pi = new Uint8Array(Fr.n8);
Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8);
const matching_pub = new Uint8Array(Fr.n8);
Scalar.toRprLE(matching_pub, 0, matching_pub_input, Fr.n8);
const sGIn = curve.G1.F.n8*2
const matching_base = buffBasesC.slice(matching_base_index*sGIn, matching_base_index*sGIn + sGIn)
const linear_factor = Fr.e(lf.toString(10))
const delta_lf = Fr.mul(linear_factor, Fr.sub(matching_pub, new_pi));
const p = await curve.G1.timesScalar(matching_base, delta_lf);
const affine_c = G1.fromObject(c);
const malleable_c = G1.toAffine(G1.add(affine_c, p))
return stringifyBigInts(G1.toObject(malleable_c))
}
const linearDep = BigInt(-1)
const matchingBase = 0;
const malleable_c = await buildMalleabeC(proof.pi_c, matchingBase, BigInt(a), new_a, linearDep)
proof.pi_c = malleable_c;
[1] Geometry: https://geometryresearch.xyz/about[2] Kobi Gurkan: https://twitter.com/kobigurk[3] ZK HACK: https://zkhack.dev/events/[4] ZK Hack x Geometry Puzzle I: https://geometryresearch.xyz/notebook/zkhack-groth-challenge[5] zkhack-groth-puzzle: https://github.com/geometryresearch/zkhack-groth-puzzle[6] 这篇: https://geometry.xyz/notebook/groth16-malleability[7] How to Generate a Groth16 Proof for Forgery: https://medium.com/ppio/how-to-generate-a-groth16-proof-for-forgery-9f857b0dcafd[8] BKSV20: https://eprint.iacr.org/2020/811