Age Verification
This demo builds a privacy-preserving age verification system using Ligetron. Learn how to prove you're 18 or older without revealing your exact birthdate or age.
🔗 Complete source code: github.com/ligeroinc/ligero-prover/tree/main/demo/age-verification
New to Ligetron? Start with quick experiments on the Ligero Platform - write and test zero-knowledge proofs directly in your browser with no setup required.
For a more in-depth exploration, this demo walks you through building a comprehensive mini-app project with a full stack (WASM programs, web interface, and server) - perfect for learning how to integrate Ligetron into real-world applications.
The Problem
Age verification is required for many services (alcohol sales, adult content, gambling), but current solutions have serious privacy issues:
- ID uploads: Send photo of driver's license (exposes full birthdate, address, photo)
- Age gates: Simple "Are you 18?" checkboxes (no verification)
- Third-party services: Share birthdate with external validators
- Credit card checks: Proxy for age but expensive and excludes users
All of these either collect too much data or provide no real verification.
What if you could prove "I am 18+" without revealing when you were born?
The Solution: Zero-Knowledge Proofs
With Ligetron, we can create a system where:
- Client knows birthdate
B - Client generates proof: "Today - B ≥ 18 years"
- Server verifies proof
- Server learns only: "Yes, 18+" or "No, under 18"
Your exact age and birthdate never leave your device.
What We're Building
An interactive web application demonstrating selective disclosure:
- WASM Program: Computes age from birthdate, verifies ≥ 18 (C++ or Rust)
- Web Interface: Date picker, quick-fill buttons, metrics display
- Server: Generates and verifies proofs
Project Structure
You'll create the following directory structure:
demo/age-verification/
├── cpp/
│ ├── age_verify.cpp # C++ WASM program
│ ├── CMakeLists.txt # Build configuration
│ └── build/
│ └── age_verify.wasm # Compiled WASM (created after build)
├── rust/
│ ├── src/
│ │ └── lib.rs # Rust WASM program
│ ├── Cargo.toml # Rust dependencies
│ └── target/
│ └── wasm32-wasip1/
│ └── release/
│ └── age_verify_demo.wasm
├── web/
│ ├── index.html # Web interface
│ ├── app.js # Client-side JavaScript
│ ├── server.py # Python server
│ └── proof_data.gz # Generated proof (created at runtime)
└── README.md
Create the base directories before starting:
mkdir -p demo/age-verification/{cpp,rust/src,web}
Prerequisites
- Ligetron built and installed (Installation Guide)
- Emscripten for WASM compilation
- Python 3 for the demo server
- Web browser with WebGPU support
Step 1: The WASM Program (C++)
Our program must verify age without leaking the exact birthdate. We'll compare dates directly.
Create cpp/age_verify.cpp:
#include <ligetron/api.h>
// Compare two dates: returns 1 if date1 < date2
int is_date_before(int year1, int month1, int day1,
int year2, int month2, int day2) {
if (year1 < year2) return 1;
if (year1 > year2) return 0;
if (month1 < month2) return 1;
if (month1 > month2) return 0;
if (day1 < day2) return 1;
return 0;
}
int main(int argc, char *argv[]) {
// Get birthdate (private inputs)
int birth_year = *reinterpret_cast<int*>(argv[1]);
int birth_month = *reinterpret_cast<int*>(argv[2]);
int birth_day = *reinterpret_cast<int*>(argv[3]);
// Get current date (public inputs)
int current_year = *reinterpret_cast<int*>(argv[4]);
int current_month = *reinterpret_cast<int*>(argv[5]);
int current_day = *reinterpret_cast<int*>(argv[6]);
// Calculate date 18 years ago
int min_year = current_year - 18;
int min_month = current_month;
int min_day = current_day;
// Check if birthdate is before/equal to minimum date
int is_18_or_older = is_date_before(
birth_year, birth_month, birth_day,
min_year, min_month, min_day
);
// Handle equal date case
if (birth_year == min_year && birth_month == min_month &&
birth_day == min_day) {
is_18_or_older = 1;
}
// Assert user is 18 or older
assert_one(is_18_or_older);
// Sanity checks
assert_one(birth_month >= 1);
assert_one(birth_month <= 12);
assert_one(birth_day >= 1);
assert_one(birth_day <= 31);
assert_one(birth_year >= 1900);
assert_one(birth_year <= current_year);
}
Understanding the Code
Private Inputs (never revealed):
birth_year,birth_month,birth_day
Public Inputs (known to everyone):
current_year,current_month,current_day
Logic:
- Calculate the date 18 years before today
- Check if birthdate is before or equal to that date
- If yes, assert success (proof will verify)
- If no, assert fails (proof verification will fail)
Key Insight: We're not computing exact age. We're checking a boolean: "Is this person old enough?" This is all the server needs to know!
Building the C++ Version
Create cpp/CMakeLists.txt:
cmake_minimum_required(VERSION 3.24)
project(AgeVerificationDemo)
set(CMAKE_CXX_STANDARD 20)
set(SDK_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../sdk/cpp/include")
set(SDK_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../sdk/cpp/build")
include_directories(${SDK_INCLUDE_DIR})
link_directories(${SDK_LIB_DIR})
add_executable(age_verify age_verify.cpp)
target_link_libraries(age_verify ligetron)
set_target_properties(age_verify PROPERTIES
SUFFIX ".wasm"
LINK_FLAGS "-O2 -sWASM=1 -sSTANDALONE_WASM=1"
)
Build it:
cd demo/age-verification/cpp
mkdir -p build && cd build
emcmake cmake ..
make
This produces age_verify.wasm.
Step 2: The WASM Program (Rust)
Here's the same logic in Rust, showcasing the language-agnostic nature of Ligetron.
Create rust/src/lib.rs:
use ligetron::*;
fn is_date_before(year1: i64, month1: i64, day1: i64,
year2: i64, month2: i64, day2: i64) -> bool {
if year1 < year2 { return true; }
if year1 > year2 { return false; }
if month1 < month2 { return true; }
if month1 > month2 { return false; }
day1 < day2
}
fn main() {
let args = get_args();
// Get birthdate (private)
let birth_year = args.get_as_int(1);
let birth_month = args.get_as_int(2);
let birth_day = args.get_as_int(3);
// Get current date (public)
let current_year = args.get_as_int(4);
let current_month = args.get_as_int(5);
let current_day = args.get_as_int(6);
// Calculate date 18 years ago
let min_year = current_year - 18;
// Check if user is 18 or older
let mut is_18_or_older = is_date_before(
birth_year, birth_month, birth_day,
min_year, current_month, current_day
);
if birth_year == min_year && birth_month == current_month &&
birth_day == current_day {
is_18_or_older = true;
}
// Assert age requirement
assert_one(is_18_or_older as u8);
// Sanity checks
assert_one((birth_month >= 1) as u8);
assert_one((birth_month <= 12) as u8);
assert_one((birth_day >= 1) as u8);
assert_one((birth_day <= 31) as u8);
assert_one((birth_year >= 1900) as u8);
assert_one((birth_year <= current_year) as u8);
}
Create rust/Cargo.toml:
[package]
name = "age-verify-demo"
version = "1.0.0"
edition = "2021"
[dependencies]
ligetron = { path = "../../../sdk/rust" }
[lib]
crate-type = ["cdylib"]
Build it:
cd demo/age-verification/rust
cargo build --target wasm32-wasip1 --release
Step 3: The Web Interface
The demo includes a complete web application that makes age verification intuitive and transparent. We provide three files that work together to create a smooth user experience - you can use them as-is or customize them to match your application's design and requirements.
index.html - The User Interface
This is a standalone HTML file with embedded CSS that provides a modern, responsive interface. It features:
- Clean, gradient background with a centered card layout that works on desktop and mobile
- Date picker input for entering birthdates with a simple, native interface
- Quick-fill test buttons ("25 years old" and "17 years old") to instantly test successful and failed verification scenarios
- Live metrics display showing calculated age, proof size, generation time, and verification time
- Visual feedback with loading states, success/error messages, and color-coded results
- Console output section that logs each step of the proof workflow for transparency
The styling is intentionally self-contained so you can easily integrate it into your existing application or modify the design to match your brand.
app.js - The Client Logic
This JavaScript file handles all client-side interactions and orchestrates the proof workflow:
- DOM management connecting UI elements to their event handlers
- Age calculation that properly handles month and day offsets for accurate results
- Quick-fill helpers that generate test birthdates relative to today's date
- Proof workflow using fetch API to communicate with the server endpoints
- Real-time logging to the console section for debugging and transparency
- User feedback with formatted metrics display and error handling
The code is well-structured and commented, making it easy to adapt to your specific needs - whether you want to add additional validation, integrate with existing authentication systems, or modify the UI interactions.
Step 4: The Python Server
server.py - The Proof Orchestrator
The server ties everything together by coordinating between the web interface and the Ligetron prover/verifier. It provides:
- Static file serving for the HTML, JavaScript, and CSS assets
/generate-proofendpoint that accepts the user's birthdate, invokes the WASM prover, and returns proof metadata/verify-proofendpoint that runs the verifier and returns a simple success/failure result- Intelligent error handling that distinguishes between constraint failures (user under 18) and technical errors (missing WASM file, etc.)
- Structured logging with timestamps and clear status messages for monitoring and debugging
The Key Privacy Feature: Notice that the verifier endpoint uses 1900-01-01 as a dummy birthdate when verifying the proof. This demonstrates the power of zero-knowledge proofs - the verifier doesn't need to know the user's actual birthdate to confirm they meet the age requirement. The server learns only one bit of information: "yes" or "no."
The server implementation is kept simple and focused, making it easy to understand the proof workflow. In a production environment, you might want to extend it with features like rate limiting, authentication, proof caching, or integration with your existing backend infrastructure.
Step 5: Running the Demo
Start the server:
cd demo/age-verification/web
python3 -u server.py
The -u flag runs Python in unbuffered mode so you can see the server logs. Open http://localhost:8000 in your browser.
Test Case 1: User is 25 Years Old
- Click "25 years old" quick-fill button
- Observe calculated age: 25 years
- Click "Generate Proof"
- Results:
- Proof generation: ~2-10 seconds
- Proof size: ~10-100 KB
- Verification: ~0.5-2 seconds
- Result: ✅ User is 18+
The server learned: "This person is at least 18." Nothing more!
Test Case 2: User is 17 Years Old
- Click "17 years old" quick-fill button
- Observe calculated age: 17 years
- Click "Generate Proof"
- Results:
- Proof generation completes
- Verification runs
- Result: ❌ User is Under 18
Even though a proof was generated, verification fails because the age constraint isn't satisfied.
Understanding Selective Disclosure
This demo showcases selective disclosure: revealing only what's necessary.
What's Revealed
| Party | Knows |
|---|---|
| Client | Full birthdate (1995-03-15), exact age (29) |
| Server (before proof) | Nothing |
| Server (after proof) | "User is 18+" OR "User is under 18" |
What's NOT Revealed
- Exact birthdate
- Exact age
- Birth year, month, or day individually
- Any other personal information
The server gains only 1 bit of information: a yes/no answer!
Next Steps
This demo demonstrates the core concepts of selective disclosure with zero-knowledge proofs. You've learned how to verify age requirements without revealing sensitive personal information.
Ready to build it yourself? The complete source code with detailed setup instructions is available in the ligetron repository.
Want to extend this demo? Consider exploring:
- Different age thresholds (21+, 13+, etc.)
- Age range verification (18-65)
- Timestamp validation to prevent replay attacks
- Multi-tier age verification systems