This is the second 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
Pitfalls zkVMs are designed to eliminate
Due to the nature of ZK applications, the set of constraints must be strictly architected to only validate the exact parameters the application requires of its input. A significant source of bugs in ZK circuits correspond to cases where computation is non-deterministic or under constrained. As mentioned in the previous article, VMs are typically designed to be deterministic. Therefore, the implementation of the zkVM provides guardrails to achieve the required determinism, and any application running in the zkVM will inherit this property. Due to the zkVM developers having created the circuits for every operation that can exist inside of the VM, then users can rely on the consistency guarantees provided.
Understanding underconstrained circuits: The leading cause of ZK circuit bugs
The term “underconstrained circuit” generally refers to situations in which the circuit’s constraints leave room for manipulation. This can range from improperly implementing arithmetic operations, all the way to issues similar to those mentioned below in “Weak input validation” section. In terms of vanilla ZK applications, the responsibility of correctly encoding the constraints of a circuit relies on the application developer. Conversely, it is the responsibility of zkVM developers to ensure that the underlying circuit is properly constrained, allowing zkVM application developers to focus entirely on the application logic. While this removes the largest source of bugs commonly found in a ZK circuit, other common ZK vulnerabilities remain such as weak input validation as well as replay & front-running attacks, which will be covered in this blog post. For the interested reader, more information can be found about underconstrained circuits in a previous blogpost.
ZK circuit-level bugs that still affect zkVM applications
While zkVMs assist the developer in creating a secure application without as much in-depth knowledge, developing zkVM-based application will inherit certain issues from general ZK circuit development.
Weak input validation
Input validation issues occur due to the inputs of a circuit not having proper constraints with respect to what the circuit is validating. Generally, these can be distinguished between two separate issues:
- Missing validation of untrusted input relationships can occur when inputs are trusted to be factual. An example of this is assuming that an input representing a public key is correctly derived from another input representing the private key.
- Missing validation of untrusted input authenticity occurs when the zkVM application must assume that the inputs provide factual information as the VM itself does not have the resources necessary to validate the information. In such a case, these inputs must be reported publicly so that can be validated externally from the zkVM application. As an example, consider a blockchain state that is passed to a zkVM application. The zkVM cannot validate if this state is actually consistent with the intended chain and must instead emit a block hash that must be validated against the canonical chain upon verification of the proof.
Example: Public key derivation vulnerability in ZK UTXO model
We can expand on the first example mentioned in the previous section to illustrate this classification of bug. We have observed similar vulnerabilities during security reviews for clients. Let’s assume that the ZK application in question operates with the UTXO model pioneered by Bitcoin. This is very common with ZK applications that operate with monetary values, as it provides a method of spending the created coins in the system without revealing which coins are spent in a transaction.
A user can publish a public key in order to receive coins, with each coin containing a unique ID. This coin can then be spent once by “nullifying it” – publishing a unique identifier that is deterministically derived from from the coin’s ID (there is a 1:1 relationship between a coin and its nullifier). In made-up pseudocode definition, we can define the following relationships (this is a simple, insecure example):
Private-Key = <secret random bytes>
Public-Key = Hash(Private-Key)
Coin = Hash(Unique-ID || Public-Key)
Nullifier = Hash(Unique-ID || Private-Key)
Notice that the Nullifier
contains the Private-Key
corresponding to the coin, as this proves ownership of the Public-Key
embedded in a Coin
. The Public-Key
is simply the hash of the Private-Key
, as the ZK settings allows us to not require asymmetric signatures (which are expensive to verify in the circuit). Let’s write an example circuit that verifies spending a Coin
in our made-up psuedocode ZK language (we will ignore the functionality of minting coins to our destination public key):
Input:
private Private-Key from-priv,
private Public-Key from-pub,
private Unique-ID from-id,
private Hash from-coin,
public Hash coin-root,
public Nullifier from-nullifier
Logic:
verify_coin_is_in_root(coin-root, from-coin)
from-coin == Hash(from-id || from-pub)
from-nullifier == Hash(from-id || from-priv)
First, the circuit will check that we are spending a legitimate Coin
that has been created and inserted into the Merkle tree of all Coins
(The coin-root
should be validated by a smart contract to avoid the second type of weak input validation issue). We can then see that the ZK circuit correctly verifies the public input from-nullifier
for the from-coin
is correctly derived, which will assist in preventing double spending of coins. Smart contracts that consume proofs in this zk-UTXO model will confirm the correct coin-root
is utilized, and that the from-nullifier
has never been used. However, the ZK circuit forgot to check that Hash(Private-key) == Public-Key
! This is catastrophic and is the equivalent of Bitcoin forgetting to check the digital signature of a spent transaction output. This allows an attacker to generate an arbitrary amount of Nullifiers for a given Coin
since there is no constraint that the from-pub
is derived from the from-priv
. Not only that, but this flaw completely breaks the soundness of the coin spending logic, as anyone can spend a Coin
as long as they know the preimage of it. This example illustrates how even a single lack of constraint on an input can completely break a given application. As mentioned above, outside observers would not even know an exploit is occurring, as the differing nullifiers will appear to be different coins being spent.
Replay & front-running attacks
ZK applications often go hand-in-hand with smart contracts. The privacy offered by ZK applications pairs well with the permissionless and psuedoanonymous execution of smart-contracts. However, developers of these applications must be aware that since these proofs are published over a peer-to-peer network and publicly provided, there must be methods that prevent “stealing” a proof from the intended origin of the transaction.
For example, assume there is a smart contract giveaway in which the first person to prove they know two prime factors p
and q
of a number z
get a monetary reward. One could easily write a ZK-proof that takes in a public input z
and the private inputs p
and q
that simply verifies p*q==z
. When a user creates this proof and publishes a transaction with it, once it becomes publicly visible in the mempool a malicious actor could simply copy that proof and create their own transaction utilizing it. If their transaction is successfully confirmed before the original creator’s (such as by paying a larger fee to the block composer) then they have successfully frontran the transaction and stolen the monetary reward without ever learning p
and q
.
Likewise, a malicious actor may be able to duplicate a user’s proof that has already been used in a confirmed transaction if sufficient safeguards are not in place. For example, if a ZK proof proves the authorization of an action without publishing a unique identifier of the authorization attempt (such as a nullifier as introduced in the Example: Public key derivation vulnerability in ZK UTXO model) then the malicious actor can replay the user’s proof to cause unintended consequences.
The first frontrunning issue can be addressed by pinning a proof to a user, such as adding a public input of the receiver’s Ethereum address. This will cause the proof generator to be credited, regardless if they are the sender of the smart contract call. One solution for the authorization replay issue is by adding a public input nonce, such that the smart contract does not allow duplicate nonces. Note that the proof itself should not be used as replay protection, as multiple unique proofs can be generated from the same input.
Dangers specific to zkVMs: The double-edged sword of abstraction
Although zkVMs protect developers against some issues, one of its greatest strengths is also one of its greatest weaknesses: dependency usage. Due to the fact that zkVMs can run (almost) any compiled program, this enables developers to use normal, non-zk dependencies within their program. While this is great for the developer experience, it is not fully compatible with the mindset that ZK application developers need to possess. Remember, ZK applications look to validate and verify a set of constraints on input data. Developers of these normal, ZK-unaware libraries often do not perform the strict validation that ZK applications often require, as they are more focused on generality and flexibility.
The relaxed threat model: Why general-purpose libraries can undermine zkVM security
In the context of normal application development, many libraries have a much more relaxed threat model compared to ZK applications. ZK applications are developed under the knowledge that every input to the circuit is controlled by a potential attacker and therefore no input should be considered trustworthy without creating constraints surrounding its desired properties. However, normal libraries may expect consumers of their libraries to do any required validation and target a more general use case than strict conformation to standards. For example, an HTTP library may consider any string as a valid URL, without performing strict validation that it conforms to its relevant specification.
This difference in threat model can provide ZK application developers with a “footgun”, in which they can develop applications much easier without thinking of the implications regarding input sanitization. Veridise analysts have witnessed this on several security reviews: usage of dependencies does not conform to expected behavior. This led to undesirable consequences in the corresponding ZK application. Libraries for ZK-specific languages do not usually suffer from this behavior, as they are designed knowing that strict validation of the inputs must be performed.
This concludes Part II of our zkVM security series. In Part III, we’ll explore how to write and audit secure zkVM applications, with a practical example using RISC Zero. Stay tuned!
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