Skip to content

Latest commit

 

History

History
115 lines (84 loc) · 3.79 KB

File metadata and controls

115 lines (84 loc) · 3.79 KB

Z002: Insecure Randomness in ZK Circuit Inputs

Severity

HIGH - Can completely break privacy guarantees

Description

Detects usage of predictable on-chain data (ledger timestamp, sequence numbers) as secret input to ZK commitment or nullifier construction. An attacker can predict or brute-force these values, breaking the privacy or uniqueness guarantee.

Vulnerable Pattern

// ❌ BAD: Ledger timestamp as commitment secret
let secret = env.ledger().timestamp();
let commitment = poseidon_hash(&env, &[secret.into(), amount.into()]);

The attacker can observe the ledger timestamp and reconstruct the commitment, revealing the hidden amount.

Secure Patterns

User-Supplied Secret (Recommended)

// ✅ GOOD: User-supplied cryptographic secret
pub fn create_note(env: Env, secret: BytesN<32>, amount: u64) {
    // User provides cryptographically random secret (e.g., from secure wallet)
    let commitment = poseidon_hash(&env, &[secret, amount.into()]);
    env.storage().set(&COMMITMENTS_KEY, commitment);
}

Cryptographically Secure PRNG

// ✅ GOOD: Proper CSPRNG
pub fn generate_secret(env: Env) -> u64 {
    // Use env.prng() which provides cryptographically secure randomness
    let secret = env.prng().gen_range(0..u64::MAX);
    secret
}

Combined Entropy Sources

// ✅ GOOD: Combine user input with contract-generated entropy
pub fn create_note_hybrid(env: Env, user_seed: BytesN<32>, amount: u64) {
    let contract_entropy = env.prng().gen::<[u8; 32]>();
    let combined_secret = keccak256(&env, &[user_seed, contract_entropy.into()]);
    let commitment = poseidon_hash(&env, &[combined_secret, amount.into()]);
    env.storage().set(&COMMITMENTS_KEY, commitment);
}

Why This Matters

ZK systems rely on secrets being unpredictable. Predictable randomness:

  • Breaks privacy: Attacker can reconstruct commitments by trying all possible timestamp values
  • Enables double-spending: Attacker can predict nullifiers before they're published
  • Defeats purpose: The ZK scheme provides no security if secrets are known
  • Undermines anonymity: Linkability analysis becomes trivial

Real-World Impact

Consider a private payment system:

  1. Alice creates a commitment using ledger timestamp T
  2. Commitment = H(T, 100 tokens)
  3. Bob observes the transaction occurred at time T
  4. Bob brute-forces: H(T, amount) for all reasonable amounts
  5. Bob discovers Alice transferred 100 tokens, defeating privacy

Detection Method

This rule uses taint analysis:

  1. Taint sources: Track variables derived from:

    • env.ledger().timestamp()
    • env.ledger().sequence()
    • env.ledger().protocol_version()
    • Other low-entropy sources
  2. Taint propagation: Follow data flow through:

    • Variable assignments
    • Function calls
    • Arithmetic operations
    • Type conversions
  3. Sink detection: Flag when tainted data reaches:

    • poseidon_hash, pedersen_commit
    • nullifier_derive, commitment_create
    • Functions with commitment or nullifier in name
  4. Context verification: Ensure it's a ZK context, not general hashing

Related Rules

  • S018 (unsafe-prng): General PRNG security (reuses taint infrastructure)
  • Z001: Missing nullifier checks
  • Z003: Public input leakage

Implementation Dependencies

  • #1192: ZK taint-tracking infrastructure foundation
  • #1194: Commitment/nullifier sink function identification

References

Examples

See test fixtures in contracts/fixtures/finding-codes/z002_insecure_randomness.rs for comprehensive examples.