SP1 与 zkVMs:安全审计员指南
本文是对SP1(一个零知识虚拟机)的全面安全审计指南,重点介绍了SP1的架构、安全注意事项和审计方法,SP1使用户能够证明任意Rust程序的执行,审计需要关注 untrusted host 和 trusted guest 之间的边界,需要对输入进行严格的校验,以防止恶意输入破坏系统,同时,还需要进行外部验证。
零知识虚拟机(zkVMs),如 SP1,在区块链基础设施中变得越来越普遍,尤其是在 rollups 和跨链协议中。作为一名安全审计员,理解这些系统对于识别潜在的漏洞,并确保零知识证明系统的完整性至关重要。
本指南提供了 SP1 架构、常见安全考虑因素以及专门为安全专业人员量身定制的实用审计方法的全面概述。
前提条件
除了本博客外,要能够审查 SP1 程序,你还需要具备:
- 对源代码语言的理解。这通常是 Rust,但 SP1 程序可以用任何可以编译成 RISC-V 的语言编写。例如,如果项目是用 C++ 编写的,你需要了解 C++...
- 对源代码语言特有的安全考量的深刻理解。例如,在 Rust 中,这是错误处理、panics、内存分配等。
你不需要完全了解零知识证明系统的底层数学原理,但了解会有所帮助!
什么是 SP1 和 Succinct Prover Network?
SP1 是由 Succinct Labs 开发的零知识虚拟机(zkVM),允许开发人员证明任意 Rust 程序的执行。与需要用专用语言编写电路的传统零知识证明系统不同,SP1 使开发人员能够编写标准 Rust 代码,并生成其正确执行的密码学证明。
Succinct Prover Network 是一种协议,请求者可以将昂贵的证明外包给专门的提供商。这些提供商因其工作而获得 Succinct 代币 ($PROVE) 的奖励。
SP1 的架构:用于零知识的类 CPU 设计
SP1 的架构类似于执行标准 RISC-V 程序的 CPU,但有一个关键的区别:每个指令的执行都经过密码学证明。以下是该系统的工作方式:
1. 编译:Rust 代码被编译成标准的 RISC-V ELF 二进制文件(与真实 RISC-V 处理器使用的格式相同)。
2. 执行和证明:SP1 zkVM 逐条指令地执行这些二进制文件,生成 STARK 证明,以证明程序的正确执行并证明公开值。
3. 证明优化:然后将 STARK 证明“包装”到 SNARK 中,SNARK 要小得多,并且在链上验证的成本更低。
4. 验证:任何人都可以验证最终的 SNARK 证明,以确认程序已正确执行,而无需重新运行计算。
SP1 管道:
| 阶段 | 输入 | 过程 | 输出 | 优势 |
|---|---|---|---|---|
| 1 | Rust 代码 | 标准编译 | RISC-V ELF | 开发人员熟悉 |
| 2 | RISC-V ELF | SP1 zkVM 执行 | STARK 证明 | 经过验证的执行 |
| 3 | STARK 证明 | 证明包装 | SNARK | 快速证明 |
| 4 | SNARK | 数学验证 | 信任保证 | 廉价验证 |
主要优点:
- 开发人员熟悉:使用标准 Rust(或其他语言)工具链和库
- 性能:STARK 证明是为 RISC-V 指令高效生成的(虽然仍然不如特定于应用程序的电路有效)
- 性价比高:SNARK 包装使链上验证变得经济实惠
- 兼容性:现有的 Rust 代码库可以通过减少开销进行调整
这种方法使零知识证明对于开发人员来说更容易访问,同时保持了正确性和隐私的密码学保证。
Prover 与 Verifier:证明系统的两个方面
理解 provers 和 verifiers 之间的区别是 zkVM 安全的基础:
Prover
- 角色:执行 guest 程序并生成零知识证明
- 信任模型:被认为是潜在的恶意或已泄露的
- 能力:可以为 guest 程序提供任意输入,控制主机环境
- 输出:生成证明正确执行的密码学证明(收据)
- 安全影响:不应能够伪造有效的证明,但可以操纵输入和主机行为
Verifier
- 角色:验证密码学证明,而无需重新执行程序
- 信任模型:仅依赖于密码学假设,而不依赖于 prover 的诚实性
- 输入:证明(收据)和程序的验证密钥
- 输出:指示证明有效性的布尔结果
- 安全保证:如果验证成功,则所声称的计算肯定会正确发生
Host 与 Guest:架构分离
SP1 程序采用可信组件和非可信组件之间的明确分离进行构建。至关重要的是,主机和 guest 代码都仅在证明期间执行,verifier 执行纯粹的数学验证,而不执行任何源代码。
Host 程序
主机是非可信的 协调器,它:
- 在标准执行环境(你的操作系统)中运行
- 为 guest 程序准备输入
- 调用 SP1 zkVM 以生成证明
- NOT 是密码学证明的一部分
- 可以由恶意的 prover 控制
安全影响:主机所做的任何事情都在密码学保证之外。控制主机的恶意行为者可能会为 guest 程序提供任意输入。
Guest 程序
Guest 程序在 VM 中运行,它们无权访问操作系统。这意味着没有互联网连接、数据库、文件或操作系统调用。这些任务都必须由主机完成,并作为非可信输入共享。
guest 是 可信的 程序,它:
- 包含你要证明的逻辑
- 在 SP1 zkVM(RISC-V 环境)中运行
- 派生唯一的验证密钥(程序提交)
- 其执行经过密码学证明
- 在 32 位 RISC-V 环境中运行
安全保证:仅证明 guest 程序的执行。verifier 的数学检查仅验证 guest 代码的执行,主机代码行为永远不会经过可密码学验证。如果验证成功,你可以信任此特定 guest 代码已正确执行以生成所声称的输出。
SP1 代码模式和安全影响
在深入研究具体的安全考虑事项之前,让我们检查 SP1 程序中使用的基本代码模式及其安全影响。
入口点声明 - Guest 程序起始点
sp1_zkvm::entrypoint!(main);
- 目的:指定 guest 程序开始执行并将被证明的函数
- 安全提示:只有从此入口点可访问的代码才包含在证明中
读取非信任输入
let input_data = sp1_zkvm::io::read::<MyStruct>();
- 目的:从主机读取私有输入数据
- 关键安全考虑事项:此数据 完全不受信任
- 最佳实践:始终在 guest 程序中验证输入数据
提交公共输出
sp1_zkvm::io::commit_slice(&output_data);
- 目的:使数据作为证明的一部分公开可验证
- 安全保证:提交的数据以密码学方式绑定到证明
- 用例:状态转换、需要公开验证的计算结果
审计员的安全考虑
1. 所有输入数据都不可信
风险:主机可以为 guest 程序提供任意输入。
缓解措施:Guest 程序必须验证所有输入数据,包括:
- 数值的范围检查
- 集合的长度限制
- 结构化数据的格式验证
- 业务逻辑约束
示例 - 输入验证模式:
sp1_zkvm::entrypoint!(main);
pub fn main() {
// 读取潜在的恶意输入
let user_id = sp1_zkvm::io::read::<u32>();
let transfer_amount = sp1_zkvm::io::read::<u64>();
let recipient_address = sp1_zkvm::io::read::<[u8; 20]>();
let data_buffer = sp1_zkvm::io::read::<Vec<u8>>();
// 关键:验证所有输入
assert!(user_id > 0 && user_id <= 1_000_000, "无效的用户 ID 范围");
assert!(transfer_amount > 0 && transfer_amount <= 1_000_000_000, "无效的转账金额");
assert!(!recipient_address.iter().all(|&b| b == 0), "不允许零地址");
assert!(data_buffer.len() <= 1024, "数据缓冲区太大");
// 额外的业务逻辑验证
assert!(transfer_amount >= 1000, "未达到最低转账金额");
// 现在安全地处理已验证的输入
process_transfer(user_id, transfer_amount, recipient_address, data_buffer);
}
2. 仅证明 Guest 代码
风险:主机代码可能包含不在证明范围内的 bug 或恶意逻辑。
审计重点:确保关键逻辑在 guest 程序中实现,而不是在主机中实现。
示例 - 正确的逻辑分离:
// ❌ 错误:主机中存在关键逻辑(未证明)
fn host_main() {
let balance = get_user_balance(); // 主机逻辑 - 未证明!
let amount = 1000;
if balance >= amount { // 主机中的关键检查 - 易受攻击!
let proof_input = ProofInput { balance, amount };
generate_proof(proof_input);
}
}
// ✅ 正确:guest 中存在关键逻辑(已证明)
sp1_zkvm::entrypoint!(main);
pub fn main() {
let balance = sp1_zkvm::io::read::<u64>(); // 非信任输入
let amount = sp1_zkvm::io::read::<u64>();
// 关键验证发生在 guest 中 - 经过密码学证明
assert!(balance >= amount, "余额不足");
let new_balance = balance.checked_sub(amount).unwrap();
sp1_zkvm::io::commit(&new_balance);
}
3. 32 位与 64 位架构差异
风险:SP1 使用 32 位 RISC-V,这可能导致从 64 位系统移植时出现问题。
重要提示:SP1 的密码学系统使用的 Baby Bear 字段 (~2^31) 不会影响 guest 程序的整数类型,usize 仍然是一个完整的 32 位无符号整数(0 到 2^32 - 1)。
常见问题:
usize在 guest 中始终为 32 位(无论密码学字段大小如何)- 指针算法差异
- 内存寻址限制
- 在 64 位系统上运行良好的计算中出现整数溢出
示例 - 32 位架构陷阱:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let large_array_size = sp1_zkvm::io::read::<u64>();
// ❌ 危险:32 位系统上可能截断
let vec_size = large_array_size as usize; // 如果 > 2^32,则静默截断
let mut data = Vec::with_capacity(vec_size);
// ✅ 安全:32 位环境的显式边界检查
assert!(large_array_size <= u32::MAX as u64, "数组大小对于 32 位系统而言太大");
let safe_size: usize = large_array_size as usize;
let mut safe_data = Vec::with_capacity(safe_size);
// ❌ 危险:大型指针算法
let base_ptr = safe_data.as_ptr();
let offset = large_array_size; // 可能 > usize::MAX
// let dangerous_ptr = unsafe { base_ptr.add(offset as usize) }; // 未定义的行为
// ✅ 安全:在指针算法之前检查边界
assert!(offset < safe_data.len() as u64, "偏移量超过数组边界");
let safe_ptr = unsafe { base_ptr.add(offset as usize) };
}
4. 第三方依赖项和兼容性
风险:为常规平台编写的库和依赖项可能会假定可以访问操作系统、64 位架构或其他在 SP1 zkVM 内部不成立的系统行为。未经修改地使用这些依赖项可能会在为 RISC-V 编译并在 zkVM 中执行时引入细微的错误、未定义的行为或安全问题。
常見问题模式:
- 调用操作系统(I/O、线程、文件访问、随机性)的依赖项在 zkVM 中不可用或行为不同
- 假定 64 位类型、指针大小或依赖于未定义的整数宽度行为的库
- 依赖于本机平台 ABI 的不安全代码或 FFI
- 使用系统特定功能(时钟/计时器、环境变量、文件描述符)
缓解措施:
- 审查依赖项:更喜欢你可以检查和调整的小型、易于理解的 crates 或 vendored 副本
- 使用与 zkVM 兼容的实现替换或存根 OS 级别行为(例如,使用由主机或框架 ABI 提供的确定性 RNG)
- 添加为 RISC-V 目标编译的显式测试,并在 SP1 工具或模拟器下运行它们
- 避免或仔细审查不安全/FFI 代码;确保 ABI 兼容性和确定性行为
- 记录任何已接受的平台假设并将其包含在验证过程中
审计重点:将第三方代码视为相对于 SP1 约束不可信的代码 — 验证其是否具有确定性的、32 位安全行为,并删除任何隐藏的平台假设。
5. 整数溢出漏洞
风险:Rust 的默认整数溢出行为在调试模式和发布模式之间有所不同。
SP1 特定的建议:由于在 guest 代码中需要 panic,因此通过添加到 guest 程序的 Cargo.toml 中(而不是主机程序中)来在发布模式下启用溢出检查:
[profile.release]
overflow-checks = true
最佳实践:
- 使用经过检查的算术(
checked_add,checked_mul等) - 显式处理溢出情况
- 尤其要注意用户提供的数值输入
类型转换漏洞:溢出检查不会捕获类型转换问题,这些静默截断必须手动验证:
// ?️ 危险:静默截断
let large_value: u64 = u64::MAX;
let truncated: u32 = large_value as u32; // 静默数据丢失
// 安全:显式验证
let safe_cast: u32 = large_value.try_into()
.expect("值对于 u32 而言太大"); // 将适当地 panic
6. 验证密钥管理
什么是验证密钥:当 SP1 程序被编写和编译时,编译的一部分会生成两个密钥(两者都是公钥,任何人都可以找到它们)。第一个密钥相当大,并提供给 prover,它称为 proving key。proving key 用于证明生成,并且仅对特定程序有效。第二个密钥小得多,称为验证密钥,在验证 SP1 程序时需要此密钥。类似地,验证密钥仅对特定程序有效。验证密钥和 proving key 都是从源代码派生的,更改源代码会更改密钥。只有在使用 proving key 进行的证明生成是针对正确的验证密钥进行验证时,证明才能正确验证。
关键安全属性:每个 guest 程序都有一个唯一的验证密钥,该密钥从其编译的二进制文件派生。
风险:
- 在证明验证期间使用错误的验证密钥
- 接受过时或易受攻击的程序版本的证明
- 密钥替换攻击
审计清单:
- 验证是否使用了正确的验证密钥 - 密钥的派生对于每个程序都是确定性的,作为审计员,你应该通过重新生成它们来检查这一点。
- 确保密钥是常量或遵循正确的更新过程 - 更新你的密钥与替换整个 SP1 程序相同
7. 公共与私有数据泄漏
风险:通过公共输出意外泄露私人信息。
最佳实践:
- 仅将必要的数据提交给公共输出
- 在需要时使用零知识友好的数据结构
- 审核所有
commit和commit_slice调用
示例 - 信息泄露模式:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let private_key = sp1_zkvm::io::read::<[u8; 32]>();
let user_balance = sp1_zkvm::io::read::<u64>();
let user_age = sp1_zkvm::io::read::<u32>();
let transaction_amount = sp1_zkvm::io::read::<u64>();
let salt = sp1_zkvm::io::read::<u128>();
// 验证用户是否可以进行交易(私有计算)
let has_sufficient_balance = user_balance >= transaction_amount;
let is_adult = user_age >= 18;
let can_transact = has_sufficient_balance && is_adult;
// ❌ 危险:泄露私人信息
sp1_zkvm::io::commit(&user_balance); // 显示确切的余额!
sp1_zkvm::io::commit(&user_age); // 显示确切的年龄!
sp1_zkvm::io::commit(&private_key); // 灾难性的泄漏!
// ✅ 正确:仅提交必要的公共信息
sp1_zkvm::io::commit(&can_transact); // 仅显示布尔结果
// ✅ 替代方法:提交交易哈希而不是详细信息(添加随机 salt 以防止 preimage 暴力攻击)
let tx_hash = hash_transaction(transaction_amount, salt);
sp1_zkvm::io::commit(&tx_hash);
// ✅ 正确:提交范围证明而不显示确切的值
let balance_sufficient = user_balance >= 1000; // 证明余额 > 阈值
sp1_zkvm::io::commit(&balance_sufficient);
}
8. 证明重放和唯一性
风险:有效的证明可能会在非预期的上下文中重放。
缓解措施:
- 在证明中包含特定于上下文的数据(块号、时间戳)并将这些数据提交到公共值
- 使用 nonce 或唯一标识符
- 实施正确的证明失效机制
9. Liveness Bugs 和 Panic 处理
关键区别:主机与 guest 代码中的 panic 具有根本不同的安全含义。
主机 Panic:
- 风险:导致证明过程失败,从而导致 liveness 问题
- 影响:阻止生成有效的证明
- 最佳实践:在主机代码中实施正确的错误处理
Guest Panic:
- 安全功能:panic 是在 guest 代码中处理无效条件的 最佳 方式
- 好处:阻止为无效计算生成证明
- 设计模式:使用
assert!,panic!或unwrap()在无效输入上快速失败
关键平衡:
- 需要:对无效输入进行 Guest Panic(安全性)
- 不需要:对有效输入进行 Guest Panic(liveness bug)
审计重点:
// ✅ 正确:在无效条件下 Panic
assert!(user_balance >= withdrawal_amount, "资金不足");
// ❌ 错误:在应处理的有效边缘情况下 Panic
let result = valid_computation().unwrap(); // 可能在有效但意外的输入上 Panic
10. 资源耗尽攻击
证明在时间上通常是昂贵的,而且通常是金钱。Succinct Prover Network 仍然向请求者收取证明费用,即使请求无效。这意味着DoS 向量可能会给协议带来财务成本。
风险:恶意输入可能导致过多的内存或计算消耗。
常见攻击媒介:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let size = sp1_zkvm::io::read::<usize>();
// ❌ 危险:不检查的内存分配
let mut vec = Vec::with_capacity(size); // 可能分配千兆字节!
// ✅ 安全:有界分配
const MAX_SIZE: usize = 1_000_000; // 1MB 限制
assert!(size <= MAX_SIZE, "输入大小超过限制");
let mut safe_vec = Vec::with_capacity(size);
}
11. 外部验证要求
关键原则:某些属性不能或不应在 guest 程序内部进行验证。
为什么需要外部验证:
- 计算效率(某些检查在 zkVM 中代价高昂)
- 访问限制(guest 无法访问外部系统)
- 信任边界(某些数据源本质上是外部的)
常见示例 - 以太坊区块哈希验证:
sp1_zkvm::entrypoint!(main);
pub fn main() {
// 从主机读取输入
let block_hash = sp1_zkvm::io::read::<[u8; 32]>();
let merkle_proof = sp1_zkvm::io::read::<MerkleProof>();
let account_data = sp1_zkvm::io::read::<AccountData>();
// 可以验证:针对状态根验证 Merkle 证明
assert!(merkle_proof.verify(&block_hash, &account_data), "无效的 merkle 证明");
// 无法验证:block_hash 是否为有效的以太坊区块
// 恶意 prover 可以提供带有精心制作的状态根的伪造 block_hash
// 必须将 block_hash 提交到公共输出以进行外部验证
sp1_zkvm::io::commit(&block_hash);
sp1_zkvm::io::commit(&account_data);
}
需要 Verifier 端验证:
// 在 Solidity 智能合约 verifier 中
function verifyAccountProof(bytes32 proofData) external {
(bytes32 blockHash, AccountData memory account) = abi.decode(proofData, (bytes32, AccountData));
// 外部验证:检查区块哈希是否为最近且有效的
require(blockHash == blockhash(block.number - 1), "无效或过时的区块哈希");
require(block.number - 1 > 0, "不支持创世块");
// 现在我们可以信任 SP1 证明已正确验证 merkle 包含
}
审计方法:
- 识别 SP1 中无法验证的属性:查找需要外部验证的数据
- 审查公共提交:验证这些值是否已提交到公共输出
- 检查 Verifier 逻辑:确认验证代码中存在外部验证
- 常见的无法验证的属性:
- 区块链状态(区块哈希、状态根哈希)
- 外部 API 响应
- 真实世界的事件或预言机
- 跨链状态
- 公钥
安全风险:如果无法验证的属性未公开提交并进行外部验证,则恶意 prover 可以提供任意值,并且仍然可以生成有效的证明。
实用审计方法
在审核 SP1 程序时:
- 确定信任边界:明确区分主机代码和 guest 代码
- 跟踪输入验证:遵循所有
io::read()调用并确保正确的验证 - 审查公共输出:检查所有
io::commit()调用是否存在信息泄漏 - 检查算术:查找潜在的溢出,尤其是在财务计算中
- 验证确定性:确保所有操作都产生一致的结果
- 验证密钥管理:确认验证器中正确的验证密钥处理
- 测试边缘情况:特别注意边界条件和错误处理
来自安全审计经验的实用见解
SP1 程序结构模式
大多数 SP1 程序都遵循三阶段逻辑结构(尽管不一定以线性方式实现):
(a) 加载初始状态和私有输入
由于 zkVM 无法访问外部数据库或 API,因此所有初始状态都必须从主机加载到 guest 中。此阶段对于安全至关重要。
sp1_zkvm::entrypoint!(main);
pub fn main() {
// 阶段 1:加载和验证所有输入
let merkle_root = sp1_zkvm::io::read::<[u8; 32]>();
let account_proofs = sp1_zkvm::io::read::<Vec<AccountProof>>();
let transactions = sp1_zkvm::io::read::<Vec<Transaction>>();
// 关键:验证初始状态完整性
assert!(verify_merkle_root(&merkle_root, &account_proofs), "无效的状态根");
// 提交初始状态哈希以进行外部验证
sp1_zkvm::io::commit_slice(&merkle_root);
}
(b) 状态跃迁逻辑
这是核心计算发生的地方,转换初始状态 + 私有输入 → 输出状态。需要仔细验证边缘情况和业务逻辑。
// 阶段 2:执行状态跃迁
let mut new_state = initial_state.clone();
for transaction in transactions {
// 验证每个跃迁步骤
assert!(transaction.amount > 0, "无效的交易金额");
assert!(new_state.get_balance(transaction.from) >= transaction.amount, "余额不足");
// 应用状态跃迁
new_state.transfer(transaction.from, transaction.to, transaction.amount);
}
(c) 输出公共值
最后阶段将结果提交到公共输出,然后必须由 verifier 与 SP1 数学证明一起验证。
// 阶段 3:提交输出以进行验证
let final_state_root = new_state.compute_root();
sp1_zkvm::io::commit_slice(&final_state_root);
sp1_zkvm::io::commit(&transaction_count);
zkVM 中的“约束不足”问题
零知识电路中最常见的漏洞模式是“约束不足的电路”,即允许恶意输入产生有效证明的验证不足。同样的问题也适用于 zkVM 程序。
常见的约束不足模式:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let user_balance = sp1_zkvm::io::read::<u64>();
let withdrawal = sp1_zkvm::io::read::<u64>();
// ❌ 约束不足:缺少关键验证
let new_balance = user_balance - withdrawal; // 没有溢出检查!
sp1_zkvm::io::commit(&new_balance);
// ✅ 正确约束:综合验证
assert!(user_balance >= withdrawal, "余额不足");
assert!(withdrawal > 0, "无效的提款金额");
assert!(withdrawal <= MAX_WITHDRAWAL, "超过提款限额");
let new_balance = user_balance.checked_sub(withdrawal)
.expect("余额计算中出现算术溢出");
sp1_zkvm::io::commit(&new_balance);
}
审计策略:系统地验证每个私有输入都具有适当的约束,以防止恶意的状态跃迁。询问:“如果恶意制作此输入会发生什么?”
安全审计快速参考
常见的漏洞模式清单
输入验证:
所有 sp1_zkvm::io::read() 调用后都进行验证
数值输入的范围检查
集合和字符串的长度限制
强制执行业务逻辑约束
架构:
安全地管理程序验证密钥
在 guest 中(而不是在主机中)实现的关键逻辑
正确分离可信/不受信任的组件
主机代码具有适当的错误处理
经过审计的第三方依赖项是否与 SP1/32 位兼容
数据处理:
没有通过 commit() 调用泄露的私有数据
针对不可验证属性的外部验证
适当使用公共值与私有输入
资源管理:
内存分配有界
计算循环具有合理的限制
没有资源耗尽攻击的潜力
溢出保护:
Guest Cargo.toml 具有 overflow-checks = true
在适当情况下使用经过检查的算术
小心处理类型转换(u64 → u32)
要注意的危险信号
// 需要注意的直接安全问题:
sp1_zkvm::io::read::<Vec<T>>(); // 没有长度验证
user_input as usize; // 潜在的静默截断
Vec::with_capacity(size); // 无界分配
balance - amount; // 没有溢出检查
sp1_zkvm::io::commit(&secret); // 信息泄漏
审计的数学知识要求
好消息:深入了解 STARK 和 SNARK 的数学知识对于有效审核 SP1 程序 不是必需的。大多数安全漏洞都发生在应用程序逻辑级别,而不是在密码学原语中。
你需要知道什么:
- 基本概念:零知识证明允许证明计算的正确性,而无需显示私有输入
- 信任模型:密码学数学是健全的(假设没有密码学中断),但是 程序逻辑 仍然可能存在缺陷
- 证明范围:仅证明 guest 程序的执行,输入验证和业务逻辑是你的责任
你不需要什么:
- 有限域算术详细信息
- 多项式承诺方案
- STARK/SNARK 结构内部结构
- 密码学协议安全证明
将你的审计重点放在:
// 这是 bug 的所在地 - 而不是密码学
sp1_zkvm::entrypoint!(main);
pub fn main() {
let input = sp1_zkvm::io::read::<UserInput>();
// BUG 领域:应用程序逻辑漏洞
if input.user_type == "admin" { // 字符串比较漏洞?
grant_admin_privileges(); // 逻辑缺陷?
}
let result = process_payment(input.amount); // 整数溢出?
sp1_zkvm::io::commit(&result); // 信息泄漏?
}
// SP1 zkVM 自动处理密码学证明
关键见解:像任何其他关键系统代码一样对待 SP1 程序,专注于输入验证、业务逻辑正确性和正确的错误处理。零知识密码学由 SP1 框架处理,通常不是出现安全问题的地方。
额外的资源
SP1 文档和学习材料(推荐)
- 「SP1 官方文档」(https://docs.succinct.xyz/docs/sp1/introduction) - SP1 开发的全面指南
- 「SP1 GitHub 存储库」(https://github.com/succinctlabs/sp1) - 源代码、示例和问题跟踪
- 「Succinct 博客」(https://blog.succinct.xyz/) - 最新的更新、案例研究和技术深入探讨
- 「SP1 示例存储库」 (https://github.com/succinctlabs/sp1/tree/main/examples) - 实际的代码示例和模板
- 「RareSkills ZK Bootcamp」 (https://rareskills.io/zk-bootcamp) - 通用零知识开发课程
- 「ZK 播客」 (https://zeroknowledge.fm/) - 关于零知识技术的访谈和讨论
零知识安全研究(可选 - 数学 + 电路)
- 「ZK 证明系统中常见漏洞」 (https://blog.oxor.io/common-vulnerabilities-in-zk-proof-5ba7620dfa2f) - 注意:主要是电路特定的错误,由 SP1 的设计团队处理,而不是应用程序开发人员
- 「ZK 安全数据库」(https://github.com/0xPARC/zk-bug-tracker) - 社区维护的 ZK 漏洞和漏洞利用数据库
- 「Trail of Bits ZK 安全研究」 (https://blog.trailofbits.com/categories/zero-knowledge/) - 对零知识系统的专业安全研究
- 「0xPARC ZK 学习资源」(https://0xparc.org/) - 关于 ZK 应用程序的教育资料和研究
- 「A16Z Crypto ZK Canon」 (https://a16zcrypto.com/posts/article/zero-knowledge-canon/) - 精选的 ZK 论文和资源列表
- 「ZK-STARKs 概述」 (https://medium.com/coinmonks/zk-starks-create-verifiable-trust-even-against-quantum-computers-dd9c6a2bb13d) - STARK 技术背景(可选阅读)
- 「ZK 白板会议」 (https://zkhack.dev/whiteboard/) - ZK 概念的技术视频说明
密码学基础知识(可选 - 数学)
- 密码学入门 - 适合初学者的密码学基础知识介绍(无需任何先验知识)
- 月亮数学手册 - 你能梦到的所有 ZK 数学知识
- Vitalik 的 PLONK 解释 - 深入研究 SNARK 构造
- Groth16 论文 - 许多系统中使用的基础 SNARK 构造
- STARK 论文 - STARK 学术论文
注意:提供的密码学资源仅供参考,但请记住,有效的 SP1 程序审计不需要深厚的数学知识。请将学习时间集中在 Rust 安全模式和本指南中概述的 SP1 特定注意事项上。
结论
SP1 和 zkVM 为创建可验证的计算系统提供了强大的工具,但它们需要仔细的安全考虑。受信任的 guest 程序和不受信任的 host 环境之间的分离创建了独特的攻击媒介,而传统的代码审计可能会遗漏这些攻击媒介。
核心要点:密码学证明仅保证 guest 程序已正确执行,不保证程序的逻辑是安全的或输入是合法的。彻底的验证和适当的架构设计仍然是构建安全 zkVM 应用程序的关键。
通过理解这些架构模式和潜在的陷阱,安全审计员可以确保基于 zkVM 的系统保持其预期的安全属性。
- 原文链接: blog.sigmaprime.io/sp1-z...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~