Skip to content

Latest commit

 

History

History
104 lines (73 loc) · 3.85 KB

File metadata and controls

104 lines (73 loc) · 3.85 KB

Z005 — Missing Verifying-Key Integrity Check Before Use

Category: zk-trusted-setup
Severity: High
Rule name: missing_vk_integrity_check


What it detects

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.


Why it matters

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.


Vulnerable example

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)
}

Safe example

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)
}

How the check works

Within each function the rule walks statements in order and tracks three things:

  1. A verifying key loaded from storage — a let binding whose initializer reads contract storage and whose name or key mentions a verifying key, or the same read inlined directly into the verifier call.
  2. 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).
  3. The verifier callverify_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.

Out of scope by construction

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.


Relationship to Z010

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.


Fixture

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).


References