4. Fiat-Shamir 变换漏洞:启发式转换中的随机预言机模型滥用、签名伪造攻击
Fiat-Shamir 变换,这个名字听起来挺唬人的。说白了,它就是把一个交互式的零知识证明,变成非交互式的。你想想看,如果每次证明都要来回对话好几轮,那在实际系统里根本跑不动。所以这个变换,几乎是所有实用零知识证明系统的基石。
但问题来了——这个变换本身,其实是个「启发式」方法。它假设存在一个随机预言机,也就是一个完美的哈希函数。嗯,现实世界里哪有这种东西?于是漏洞就来了。
4.1 随机预言机模型:理想很丰满
随机预言机模型,你可以把它想象成一个「神谕」。你问它一个问题,它给你一个完全随机的答案。而且同样的输入,永远得到同样的输出。这个模型在理论上非常漂亮,因为它能保证证明者无法预测哈希结果,也就没法作弊。
但实际代码里,我们用的是 SHA256、Keccak 这些具体的哈希函数。它们不是随机的,它们是确定性的算法。我个人习惯把这种差距叫做「理想与现实的鸿沟」。很多攻击,就是利用这条鸿沟打进来的。
核心问题: Fiat-Shamir 变换的安全性,完全依赖于「挑战值」必须是证明者无法控制的。如果证明者能预测或操纵挑战值,那整个证明就废了。
4.2 签名伪造攻击:一个经典案例
我记得有一次审计一个 Schnorr 签名方案。代码看起来没什么问题,但仔细一看,发现挑战值的计算方式有猫腻。具体来说,签名者把公钥、消息、还有随机点都哈希到一起,生成挑战。这看起来没问题对吧?
但问题在于,这个哈希的输入顺序和格式,被攻击者利用了。攻击者可以构造一个特殊的公钥,使得哈希结果恰好等于他想要的值。这样一来,他就能伪造签名。
为什么会这样?因为哈希函数的输入空间太大了,攻击者可以暴力搜索一个「坏」的公钥。你想想看,如果挑战值不是真正随机的,而是可以被反向工程出来的,那签名还有什么安全性可言?
4.2.1 攻击流程
- 攻击者选择任意消息 m 和任意随机数 r
- 计算 R = g^r
- 攻击者想要伪造挑战 e,于是反向计算公钥 PK,使得 Hash(PK, m, R) = e
- 攻击者现在可以构造签名 (R, s),其中 s = r + e * sk(但 sk 是伪造的)
- 验证者收到 (R, s) 和伪造的 PK,计算 e' = Hash(PK, m, R),发现 e' = e,验证通过
警告: 这个攻击成立的前提是,挑战值的计算没有绑定到「证明者身份」或「公钥的合法性」。如果哈希输入里包含了公钥,但公钥本身可以被攻击者任意选择,那这个绑定就是无效的。
4.3 随机预言机模型滥用:常见的坑
在实际项目中,我见过太多滥用随机预言机模型的案例。这里列几个最常见的:
- 挑战值重用: 同一个挑战值被用在多个证明里。攻击者可以通过交叉计算,恢复出秘密信息。
- 挑战值可预测: 哈希函数的输入只包含公开信息,但攻击者可以提前计算所有可能的挑战值,然后选择对自己有利的。
- 缺少域分离: 不同的协议阶段使用了相同的哈希函数,没有加区分标签。攻击者可以把一个阶段的输出,当作另一个阶段的输入来利用。
- 哈希输入顺序不一致: 证明者和验证者计算挑战值时,输入的顺序不同。这会导致挑战值不一致,验证失败,或者更糟——被攻击者利用。
避坑指南: 我曾经在一个 zk-SNARKs 项目里,发现开发者把多个公开输入直接拼接后哈希。结果攻击者可以通过调整输入顺序,构造出相同的哈希值,从而伪造证明。从那以后,我建议所有项目都使用「域分离标签 + 固定编码格式」来计算挑战值。
4.4 如何正确实现 Fiat-Shamir 变换
嗯,这里要注意几个关键点。我总结了一个 checklist,每次做代码审查时都会过一遍:
| 检查项 | 说明 | 常见错误 |
|---|---|---|
| 挑战值唯一性 | 每个证明会话必须使用不同的挑战值 | 使用固定种子或时间戳作为挑战 |
| 输入完整性 | 哈希输入必须包含所有公开参数和证明者的承诺 | 遗漏了公钥或公共参考串 |
| 域分离 | 不同协议阶段使用不同的哈希标签 | 所有阶段共用同一个哈希函数 |
| 编码一致性 | 证明者和验证者使用完全相同的编码方式 | 一个用 JSON,一个用 Protobuf |
| 抗量子安全 | 如果考虑量子攻击,哈希输出长度要足够 | 使用 128 位输出,容易被碰撞攻击 |
4.5 代码示例:安全的挑战值生成
下面是一个我常用的挑战值生成函数。它使用了域分离标签,并且强制要求输入按固定顺序编码:
// 安全的挑战值生成函数
function computeChallenge(
domainTag: string, // 域分离标签,例如 "schnorr-signature-v1"
publicKey: Uint8Array, // 公钥
message: Uint8Array, // 消息
commitment: Uint8Array // 证明者的承诺(随机点)
): Uint8Array {
// 1. 使用域分离标签,防止跨协议攻击
const tagBytes = new TextEncoder().encode(domainTag);
// 2. 固定编码顺序:tag || publicKey || message || commitment
const input = concat(tagBytes, publicKey, message, commitment);
// 3. 使用 SHA256,输出 256 位挑战值
return sha256(input);
}
关键点: 域分离标签必须包含协议名称和版本号。这样即使两个协议使用了相同的哈希函数,也不会互相干扰。我习惯在标签里加上「v1」「v2」这样的版本号,方便未来升级。
4.6 知识体系图
下面这张 SVG 图,展示了 Fiat-Shamir 变换漏洞的核心知识结构。你可以看到,从交互式证明到非交互式证明,中间的关键就是随机预言机模型。而滥用这个模型,会导致签名伪造、证明伪造等一系列问题。
4.7 总结
Fiat-Shamir 变换的漏洞,本质上是对「随机预言机模型」的滥用。你想想看,一个理论上完美的假设,放到现实世界里,到处都是妥协。哈希函数不是随机的,挑战值可能被预测,输入编码可能不一致——每一个细节都可能成为攻击面。
我个人习惯在每次实现零知识证明系统时,都专门写一个「挑战值生成模块」,单独测试、单独审计。这个模块的代码量通常不到 50 行,但它的安全性直接决定了整个系统的生死。嗯,别嫌麻烦,值得的。