Category: zk-trusted-setup
Severity: High
Rule name: missing_vk_integrity_check
A contract that loads a verifying key from storage and uses it for proof verification without first checking its hash/fingerprint against a reference value committed at deployment.
Storage corruption, a malicious admin rotation (see Z010), or an upgrade bug could silently swap the verifying key to one controlled by an attacker. Without an integrity check before every verification call the contract would accept proofs generated under the attacker's fake key. The defense is cheap: a single sha256 comparison prevents the entire class.
pub fn verify(env: Env, proof: Vec<u8>, inputs: Vec<u64>) -> bool {
// Z005: key is loaded from storage and used directly — no hash check.
let vk: BytesN<64> = env.storage().persistent().get(&DataKey::VerifyingKey).unwrap();
groth16_verify(vk.as_ref(), &proof, &inputs)
}pub fn verify(env: Env, proof: Vec<u8>, inputs: Vec<u64>) -> bool {
let vk: BytesN<64> = env.storage().persistent().get(&DataKey::VerifyingKey)
.expect("VK not set");
let expected: BytesN<32> = env.storage().persistent().get(&DataKey::VkHash)
.expect("VK hash not set");
// Integrity gate: abort if the stored key has been tampered with.
assert_eq!(
env.crypto().sha256(&Bytes::from_slice(&env, vk.as_ref())),
expected,
"verifying key integrity check failed"
);
groth16_verify(vk.as_ref(), &proof, &inputs)
}Within each function the rule walks statements in order and tracks three things:
- A verifying key loaded from storage — a
letbinding whose initializer reads contract storage and whose name or key mentions a verifying key, or the same read inlined directly into the verifier call. - An integrity gate — a statement that both applies a hash or signature primitive
(
sha256,keccak256,blake2,ed25519_verify, …) to the key and compares or asserts on the result (assert_eq!,==,!=,panic!,require). - The verifier call —
verify_proof,groth16_verify,snark_verify,plonk_verify, and their variants.
A finding is raised when (1) reaches (3) with no (2) in between.
Merely reading the reference hash out of storage is not a gate — the comparison has
to happen. let expected = storage.get(&DataKey::VkHash).unwrap(); on its own leaves
the finding in place.
Verifying keys held in const or static items. They cannot be mutated at runtime,
so a runtime integrity check would be checking a value against itself. Their distinct
risk — undocumented provenance — is Z004's concern.
Z010 asks who may write the key. Z005 asks is the key sitting there right now the one we vetted. Passing Z010 does not answer Z005's question: a storage key collision, an unrelated migration, or a compromised admin account can all leave a hostile key in place through a perfectly access-controlled rotation function. The two rules are complementary, and a rotatable-key design needs both.
When you do rotate, update the reference hash in the same transaction as the key — otherwise the integrity gate rejects the new, legitimate key.
contracts/fixtures/finding-codes/z005_missing_vk_integrity_check.rs — two triggering
functions (bound and inlined storage reads) plus three clean cases (hash asserted,
immutable constant key, and a getter that never verifies).
- Z005 implementation issue #1201
- Related: Z004 (setup provenance), Z010 (rotation auth)