This is the third article in our series exploring the security of zkVMs and the applications built on top of them.
zkVM security series:
📚 Part I: Introduction to Zero-Knowledge Virtual Machines (zkVMs)
📚 Part II: Identifying common vulnerabilities in zkVMs
📚 Part III: Writing and auditing secure zkVM applications: A practical example with RISC Zero
Writing and auditing a zkVM application
A general purpose zkVM greatly eases the pains associated with manual constraint writing, as it removes much of the difference in mindset that is required for manually writing constraints. Rather than requiring developers to think in terms of field arithmetic and constraint systems, zkVMs allow applications to be written in familiar languages and compiled into circuits automatically. However, as we’ve explored in earlier sections, this abstraction doesn’t eliminate all risks. Let’s walk through an example.
RISC Zero is one of the most popular zkVMs, and that is the system we will use for this example. You can see a previous blog post of ours on how we assisted RISC Zero in securing their zkVM implementation. For this example, we will mimic the bug in section “Example: Public key derivation vulnerability in ZK UTXO model” (in Part II blog post) and only focus on the guest portion of the code. Like the above example, we omit the generation of the new note (so this code is only relevant to spending).
For our toy example, we will assume that two dependencies exist:
merkletreeexposes averify(root, leaf, path) -> boolfunction that validates a given leaf exists inside of the provided merkle root and pathzkcoinexposes agenerate(id, amount, pubkey, privkey) -> (coin, nullifier)function that generates the coin and nullifier for a givenidandamount.
Let’s write our simple implementation:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
use merkletree::verify; use zkcoin::generate; use risc0_zkvm::guest::env; // Assume the following types exist: // - U256: A 32 byte integer, the output of hash functions // - PrivateKey: A private key // - PublicKey: A public key struct Input { from_priv: PrivateKey, from_pub: PublicKey, from_id: U256, from_amount: U256, coin_merkle_root: U256 coin: U256, coin_merkle_path: U256 } pub async fn main() { let Input { from_priv, from_pub, from_id, from_amount, coin_merkle_root, coin, coin_merkle_path } = env::read(); if !verify(coin_merkle_root, coin, coin_merkle_path) { panic!("Invalid merkle proof!"); } let (gen_coin, gen_nullifier) = generate(from_id, from_amount, from_pub, from_priv); if gen_coin != coin { panic!("Invalid coin"); } env::commit(coin_merkle_root); env::commit(gen_nullifier); } |
Of the four mentioned vulnerabilities in the above sections, we can be confident that two of them are addressed.
| Vulnerability | Addressed? |
|---|---|
| Underconstrained circuit | ✅ |
| Weak input validation | ❌ |
| Replay & Frontrunning | ✅ |
| External dependency relaxed threat model | ❌ |
The underconstrained circuit issue does not apply here, as we are working with the RISC Zero zkVM and assuming the correctness of their components. Additionally, replay attacks are not an issue as the nullifier is publicly committed to the proof, and therefore an attacker “stealing” the proof does no harm to the protocol as long as the nullifier is checked in the smart contract and cannot be double spent. Similarly, frontrunning does not apply in this example (assuming the dependencies are correct), as the update of the smart contract’s state does not rely upon the submitter of the transaction.
One may be led to believe the validity of the coin supply and the enforcement of nullifier relationships to UTXOs are correct. This assumption may be made if the developer expected the zkcoin crate to correctly verify the from_pub is derived from the from_priv. However, if this assertion is not made in the dependency, then the vulnerability in the original section “Example: Public key derivation vulnerability in ZK UTXO model” (in Part II blog post) exists here as well. This bug arises from both “weak input validation” and the “relaxed threat model” of dependencies.
Recommendations for secure zkVM application development
Use dependencies sparingly and validate their behavior
In the “Dangers specific to zkVMs” section (in Part II blog post) I detailed how issues can arise by introducing dependencies into a ZK program that were not designed for the adversarial environment zkVMs operate under. Due to these dangers, it is strongly recommended to minimize dependency usage as much as possible. When it is not feasible to reimplement a dependency’s function then there are two points we greatly emphasize:
- Validate the inputs to the circuit that are directly/indirectly utilized by the dependency strictly match a defined structure, such as the intended behavioral specification. For example a bytestring input that is parsed as an HTTP request should be validated to conform to the corresponding RFC.
- Investigate the behavior and guarantees provided by the dependency. This task is twofold and is time consuming. One must first figure out the intentions of the developers of the library, and what guarantees are documented surrounding the validity of the outputs. After understanding this goal, one should investigate the library itself to confirm it meets both the library developer’s goals and the goals of the consuming ZK application. This includes looking for open issues in the library’s repository and even reviewing the code of the library itself. This is tedious but, again, we have found issues in external libraries that opened critical vulnerabilities in the consuming ZK application.
Map inputs to outputs
A good exercise for preventing input validation bugs and other insufficiently validated/deterministic behavior is to map how the inputs of a program effect its outputs/public inputs. This will require one to make strict documentation surrounding the influence an input has on the execution of a ZK application/circuit and assist in validating the intended behavior is correctly implemented. We have uncovered multiple issues during security reviews during this method and believe it serves as a good model for understanding a program more deeply, regardless if any issues are found.
zkVM security series:
📚 Part I: Introduction to Zero-Knowledge Virtual Machines (zkVMs)
📚 Part II: Identifying common vulnerabilities in zkVMs
📚 Part III: Writing and auditing secure zkVM applications: A practical example with RISC Zero