Skip to main content

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

Choose Your Learning Path

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:

  1. Client knows birthdate B
  2. Client generates proof: "Today - B ≥ 18 years"
  3. Server verifies proof
  4. 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:

  1. WASM Program: Computes age from birthdate, verifies ≥ 18 (C++ or Rust)
  2. Web Interface: Date picker, quick-fill buttons, metrics display
  3. 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:

  1. Calculate the date 18 years before today
  2. Check if birthdate is before or equal to that date
  3. If yes, assert success (proof will verify)
  4. 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

Download index.html

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

Download app.js

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

Download server.py

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-proof endpoint that accepts the user's birthdate, invokes the WASM prover, and returns proof metadata
  • /verify-proof endpoint 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

  1. Click "25 years old" quick-fill button
  2. Observe calculated age: 25 years
  3. Click "Generate Proof"
  4. 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

  1. Click "17 years old" quick-fill button
  2. Observe calculated age: 17 years
  3. Click "Generate Proof"
  4. 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

PartyKnows
ClientFull 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