5.1 安全模型与攻击向量:它会怎么被骗 本节摘要:安全声明是一份条件合同,条件之外皆是攻击面。本节盘点零知识证明系统的五类主要攻击向量——挑战操纵、上下文重放、随机数失守、约束缺失、侧信道——逐条给出防线与真实事故原型,并用"缺约束放走伪证"的演算展示最阴险的一类缺陷。承接第 5 章审计主线,通往 5.2 的性能账本。 安全员的第一课:读懂条件合同 翻开任何一份 ZK 方案的安全声明,你会看到一堆限定词:在随机预言机模型下、在离散对数假设下、对多项式时间敌手、以 128 位安全等级。这些不是免责套话,而是防线的精确坐标——假设是买来的防线,模型是防线的安装位置,参数是防线的厚度。读声明的正确动作是逐词翻译成风险:假设被攻破会怎样(量子威胁,7.
本节摘要:安全声明是一份条件合同,条件之外皆是攻击面。本节盘点零知识证明系统的五类主要攻击向量——挑战操纵、上下文重放、随机数失守、约束缺失、侧信道——逐条给出防线与真实事故原型,并用"缺约束放走伪证"的演算展示最阴险的一类缺陷。承接第 5 章审计主线,通往 5.2 的性能账本。
翻开任何一份 ZK 方案的安全声明,你会看到一堆限定词:在随机预言机模型下、在离散对数假设下、对多项式时间敌手、以 128 位安全等级。这些不是免责套话,而是防线的精确坐标——假设是买来的防线,模型是防线的安装位置,参数是防线的厚度。读声明的正确动作是逐词翻译成风险:假设被攻破会怎样(量子威胁,7.3 节),模型失真会怎样(哈希与真随机函数的偏差,3.2 节),参数配薄会怎样(枚举成本降低)。
把攻击者也想清楚:ZK 场景里的敌手通常是两类角色的合体——想伪造证明的"假证者"与想从证明里提取信息的"窥探者"。前者攻击可靠性与知识性,后者攻击零知识性。下面这张攻击向量对照表,按"攻击目标"组织,覆盖了公开事故记录里绝大多数翻车点:
| 攻击向量 | 打击对象 | 典型手法 | 防线 |
|---|---|---|---|
| 挑战操纵(grinding) | 可靠性 | 海选可选输入直到哈希挑战落在可应付子集 | 自由度入哈希、删除可选性 |
| 上下文重放 | 可靠性 | 把甲语境的证明搬到乙语境播放 | 哈希绑定全部上下文与版本号 |
| 随机数复用 | 知识性 | 两份记录反解秘密(1.3 节演算) | 硬件熵源、禁用确定性盲化量 |
| 约束缺失 | 可靠性 | 电路少写约束,伪见证直接通过 | 负向测试、等价性校验、双电路交叉验证 |
| 侧信道 | 零知识性 | 证明时间/功耗/内存波动泄露见证信息 | 恒定时间电路、盲化、噪声注入 |
表格里前两行已在 3.2 节展开,第三行的演算在 1.3 节跑过。本节把镜头对准第四行——它最阴险,因为约束缺失的电路对诚实用户完全正常,所有正向测试都是绿的。
约束系统的逻辑是"满足全部约束即承认见证正确",那么少写一条约束,就等于给某类伪见证开了白名单。危险在于这种缺陷没有症状:诚实路径的每个输入都通过,一切功能测试都绿,缺陷只在攻击者构造特定输入时兑现。历史上有过著名先例——某隐私支付系统曾被独立研究者发现铸造电路存在约束缺失,理论上可无限增发而链上无人察觉,最终靠紧急升级封堵。跑一个缩小版实验,看看缺陷的形态:
# 缺约束实验:同一业务,两版电路,一版放走骗子 P = 97 # 业务:证明知道 (x, y) 满足 x * y = 12 且结果为正数被登记为 out # —— 有缺陷的电路:写手忘了约束 2 —— A_bug = [[0, 1, 0, 0]] # 只有一条约束:x * y = out B_bug = [[0, 0, 1, 0]] C_bug = [[0, 0, 0, 1]] # —— 修正后的电路:补上"out 确实等于 12"的检查 —— A_fix = [[0, 1, 0, 0], [0, 0, 0, 1]] B_fix = [[0, 0, 1, 0], [1, 0, 0, 0]] C_fix = [[0, 0, 0, 1], [12, 0, 0, 0]] # 第二条:out * 1 = 12 def satisfied(M1, M2, M3, z): return all( sum(a * b for a, b in zip(m1, z)) % P * sum(a * b for a, b in zip(m2, z)) % P == sum(a * b for a, b in zip(m3, z)) % P for m1, m2, m3 in zip(M1, M2, M3)) honest = [1, 3, 4, 12] # 诚实见证:3 乘 4 等于 12 liar = [1, 6, 5, 30] # 骗子的见证:6 乘 5 等于 30,并非 12 print("缺陷电路 收下诚实见证:", satisfied(A_bug, B_bug, C_bug, honest)) # True print("缺陷电路 收下骗子见证:", satisfied(A_bug, B_bug, C_bug, liar)) # True —— 放行了! print("修正电路 收下骗子见证:", satisfied(A_fix, B_fix, C_fix, liar)) # False —— 拦截 print("修正电路 收下诚实见证:", satisfied(A_fix, B_fix, C_fix, honest)) # True —— 误伤为零
缩小实验放大了现实的难度:真实电路的约束以万计,人眼审查不现实,所以防线必须工程化。三类互补做法已成行业惯例:负向测试——对每个约束手工构造"恰好违反它"的见证,违反者必须被拒,一条约束一条测试;等价性校验——用独立工具(另一套电路 DSL 或形式化方法)从业务逻辑重新生成电路,与被审电路比对约束集合;双电路交叉——关键业务用两家实现分别证明再互验,同时埋雷的概率骤降。把这三条写进 CI,比任何评审会议都可靠。
最后一类攻击向量最容易被理论派忽略:证明器本身是运行在物理世界里的程序。它生成证明耗时与见证的相关性、内存访问模式、缓存命中分布,都可能向共宿主进程或机房侧泄露"见证大概长什么样"。对策从软件到硬件三档:证明时间恒定化(牺牲平均速度)、对见证做随机盲化(数学上加噪声)、敏感场景物理隔离(专用证明机)。还有一个常被遗漏的组织性防线——证明服务不要多租户混部:把不同客户的见证放在同一台证明机上互相挤兑,等于把侧信道攻击的靶子并排摆好。
安全审计做完,把这份清单连同方案声明一起归档:它不仅是防线记录,更是 7.4 节工程化 checklist 的安全分册。下一节换账房帽子,算钱。
安全审计通过只说明系统"不被骗",商业上能不能成立还要看"养不养得起"。证明生成烧多少算力、验证花多少时间、证明体积占多少带宽——下一节三本账并排算。
把本节的攻击向量折成评审现场的检查单,逐条留痕。声明层:假设、模型、参数三要素是否齐备,参数档位与业务价值是否匹配。协议层:哈希输入清单是否完整(3.2 的两问)、挑战来源是否不可预知、随机数生成是否有熵源与复用检测。电路层:负向测试覆盖率(每条约束至少一条违反用例)、独立重生成比对是否通过。实现层:恒定时间路径的关键点、证明服务的租户隔离、日志中是否意外记录了见证材料。流程层:升级与电路变更的复审流程、钥匙治理、应急迁移预案。检查单的价值在留痕:每条的结论写"通过、豁免、待办"与复核人,未来出事时,这份文档就是定责与复盘的第一手材料。
**问:敌手模型里的"多项式时间"对实际防御有什么指导意义?**它把防御目标从"绝对安全"转成"经济不划算":攻击成本按安全参数指数增长,防御方只需确保成本高过攻击收益。这条思路让安全预算变成可讨论的商务问题——参数从一档提到另一档,攻击成本翻若干个数量级,防御成本增加多少,两笔账对冲着算。
**问:开源实现一定比闭源安全吗?**开源解决的是"可审计性",不等于"已审计"——代码公开但无人认真读过,与闭源没有本质区别。正确的问法是"有没有高质量的外部审计记录、缺陷响应历史如何"。选型时把审计报告的质量与数量当作硬指标,比纠结开源闭源更贴近风险本质。