Skip to main content
A password validation system that checks password policies and generates PBKDF2 hashes inside the zkVM, producing verifiable proofs without exposing the password.

What You’ll Learn

  • Implementing password policies in the zkVM
  • Using PBKDF2 for password hashing
  • Privacy-preserving credential verification
  • Working with password strength requirements

Overview

This example demonstrates how to:
  • Check password validity against a compiled policy
  • Generate PBKDF2-SHA256 hashes securely
  • Share only the hash while keeping the password private
  • Produce verifiable receipts of password compliance
The password never leaves your machine and is never revealed in the receipt - only the PBKDF2 hash is public.

How It Works

1
Guest Program
2
The guest enforces a password policy and generates a hash:
3
use password_checker_core::PasswordRequest;
use pbkdf2::pbkdf2_hmac_array;
use risc0_zkvm::{guest::env, sha::Digest};
use sha2::Sha256;

/// Password policy compiled into the guest
const POLICY: PasswordPolicy = PasswordPolicy {
    min_length: 3,
    max_length: 64,
    min_numeric: 2,
    min_uppercase: 2,
    min_lowercase: 2,
    min_special_chars: 1,
};

const PBKDF2_SHA256_ITERATIONS: u32 = 10;

fn main() {
    let request: PasswordRequest = env::read();

    // Validate password against policy
    if !POLICY.is_valid(&request.password) {
        panic!("Password invalid. Please try again.");
    }

    // Generate PBKDF2 hash
    let password_hash: Digest = pbkdf2_hmac_array::<Sha256, 32>(
        &request.password.as_bytes(),
        &request.salt,
        PBKDF2_SHA256_ITERATIONS,
    )
    .into();

    env::commit(&password_hash);
    env::commit(&request.salt);
}
4
Password Policy
5
The policy is embedded in the guest program and reflected in the Image ID:
6
struct PasswordPolicy {
    pub min_length: usize,
    pub max_length: usize,
    pub min_uppercase: usize,
    pub min_lowercase: usize,
    pub min_numeric: usize,
    pub min_special_chars: usize,
}

impl PasswordPolicy {
    pub fn is_valid(&self, pw: &str) -> bool {
        let metrics = PasswordMetrics::new(pw);
        self.correct_length(pw)
            && (metrics.numeric >= self.min_numeric)
            && (metrics.uppercase >= self.min_uppercase)
            && (metrics.lowercase >= self.min_lowercase)
            && (metrics.special >= self.min_special_chars)
    }
}
7
Password Metrics
8
The guest analyzes password characteristics:
9
struct PasswordMetrics {
    pub numeric: usize,
    pub special: usize,
    pub uppercase: usize,
    pub lowercase: usize,
}

impl PasswordMetrics {
    pub fn new(password: &str) -> Self {
        let mut numeric = 0;
        let mut special = 0;
        let mut uppercase = 0;
        let mut lowercase = 0;
        
        for ch in password.chars() {
            if ch.is_ascii_digit() {
                numeric += 1;
            }
            if ch.is_ascii_punctuation() {
                special += 1;
            }
            if ch.is_ascii_uppercase() {
                uppercase += 1;
            }
            if ch.is_ascii_lowercase() {
                lowercase += 1;
            }
        }
        
        PasswordMetrics { numeric, special, uppercase, lowercase }
    }
}

Running the Example

cargo run --release
The program will:
  1. Generate a salt
  2. Validate the password against the policy
  3. Compute PBKDF2-SHA256 hash
  4. Produce a receipt with the hash and salt

What Gets Proven?

The receipt proves:
  1. Policy Compliance: The password meets all requirements:
    • Length between 3-64 characters
    • At least 2 numeric characters
    • At least 2 uppercase letters
    • At least 2 lowercase letters
    • At least 1 special character
  2. Hash Validity: The PBKDF2 hash was correctly computed from a compliant password
  3. Policy Binding: The specific policy is encoded in the Image ID, preventing tampering

Why Use zkVM for This?

Traditional password validation requires trusting the server. With zkVM:
  • Local Execution: Password checking runs on your machine
  • Privacy: The password never leaves your device
  • Verifiability: The receipt proves compliance without revealing the password
  • Trust Minimization: The recipient verifies the receipt, not the password

Workflow

You (Local)                          Recipient
├─ Enter password                    │
├─ Run zkVM validation               │
├─ Generate receipt                  │
├─ Send: receipt + hash + salt ───→  ├─ Verify receipt
│                                    ├─ Check Image ID
│                                    └─ Store hash
└─ Password never shared

Security Features

PBKDF2 Key Derivation

PBKDF2 (Password-Based Key Derivation Function 2) provides:
  • Salt: Prevents rainbow table attacks
  • Iterations: Increases computational cost for attackers
  • Standard: Widely used and well-tested
let hash = pbkdf2_hmac_array::<Sha256, 32>(
    password.as_bytes(),
    salt,
    10,  // iterations (low for demo, use 10000+ in production)
);
This demo uses only 10 iterations for speed. Production systems should use 10,000+ iterations.

Image ID Binding

Changing the policy changes the Image ID:
// If you change this policy...
const POLICY: PasswordPolicy = PasswordPolicy {
    min_length: 8,  // Changed from 3 to 8
    ...
};

// ...the Image ID changes, and old receipts won't verify
This prevents accepting proofs generated with weaker policies.

Use Cases

User Registration

Prove your password meets requirements without sending it:
Password → zkVM → Receipt + Hash → Server verifies receipt

Password Reset

Prove your new password is valid before submission:
New Password → Validate locally → Send proof + hash

Compliance Auditing

Prove password policies are enforced:
Multiple Passwords → Batch Proofs → Audit compliance

Decentralized Auth

Create password-based authentication without central servers:
Password + Policy → Receipt → Blockchain verification

Integration Example

use password_checker::{validate_password, PasswordRequest};

// Client side
let password = get_user_password();
let salt = generate_salt();

let receipt = validate_password(&PasswordRequest {
    password,
    salt: salt.clone(),
})?;

// Send to server
send_registration(receipt, salt);

// Server side
fn handle_registration(receipt: Receipt, salt: Vec<u8>) {
    // Verify receipt against known Image ID
    receipt.verify(PASSWORD_CHECKER_ID)?;
    
    // Extract hash from journal
    let hash: Digest = receipt.journal.decode()?;
    
    // Store hash and salt for future authentication
    store_credentials(hash, salt);
}

Educational Notice

This is example code for educational purposes. The password policy is intentionally simple. Production systems should:
  • Use higher PBKDF2 iteration counts (10,000+)
  • Implement comprehensive password policies
  • Consider additional security measures
  • Follow industry best practices

Video Tutorial

For a detailed walkthrough, see this excerpt from our workshop at ZK HACK III.

Next Steps

Build docs developers (and LLMs) love