Privacy-preserving KYC platform using zero-knowledge proofs, fully homomorphic encryption, and cryptographic commitments.
Advanced cryptographic techniques—zero-knowledge proofs, fully homomorphic encryption, Merkle trees—have existed for decades. They solve real problems: proving claims without revealing data, computing on encrypted values, verifying set membership privately.
Yet these techniques are rarely used in mainstream applications.
Why? The barrier isn't mathematical—it's practical:
- Complex setup (trusted ceremonies, circuit compilation, key generation)
- Specialized expertise required
- No reference implementations for common use cases
- Perceived performance overhead
Meanwhile, identity verification systems store millions of passport photos, birth dates, and biometric templates in plaintext databases—creating honeypots for attackers and compliance nightmares for organizations.
KYC and identity verification is the perfect domain for privacy-preserving cryptography:
- High-value PII: Names, birthdates, document numbers, face images
- Binary decisions: "Is this person over 18?" doesn't require knowing their exact birthday
- Regulatory pressure: GDPR, data minimization laws demand "privacy by design"
- Business alignment: Companies want to verify without the liability of storing
Zentity proves these cryptographic techniques can work together in a real application:
| Technique | Traditional Barrier | Zentity Approach |
|---|---|---|
| Zero-Knowledge Proofs | Complex circuit design | Pre-built circuits for age, nationality, document validity |
| Fully Homomorphic Encryption | Slow, requires expertise | TFHE-rs with optimized operations for KYC use cases |
| Cryptographic Commitments | Roll-your-own risk | Standardized SHA256 + salt with GDPR erasure support |
| Merkle Trees | Custom implementation | Ready-to-use country group trees (EU, EEA, SCHENGEN) |
The result: Complete identity verification with zero PII storage.
Note: Zentity is a demonstration project showing that privacy-preserving KYC is technically feasible today. It serves as a reference architecture for teams building production systems.
Zentity is a privacy-preserving KYC platform that enables identity verification for banks, crypto exchanges, and fintechs—without storing or accessing sensitive personal information.
- Verify age without revealing date of birth (ZK proofs + FHE)
- Verify nationality group membership without revealing country (ZK Merkle proofs)
- Verify liveness with multi-gesture challenges (smile, blink, head turns)
- Verify liveness scores without revealing biometric data (FHE threshold comparisons)
- Match faces to ID documents without storing biometrics (DeepFace/ArcFace)
- Verify document validity without exposing expiration date (ZK proofs)
- ZK Face Match Proofs - Prove face similarity inside a ZK circuit
- AML/Sanctions screening - Privacy-preserving sanctions list checking
- Accredited investor verification - Prove income thresholds without revealing amounts
- Source of funds verification - ZK proofs for financial compliance
Traditional liveness detection exposes exact anti-spoof confidence scores. Zentity encrypts liveness scores using FHE, enabling threshold comparisons without revealing the actual score:
User → Liveness Service: Submit face capture
Liveness Service → FHE: encrypt(score=0.85)
FHE → Storage: ciphertext (score hidden)
Verifier → FHE: verify(ciphertext >= 0.3)
FHE → Verifier: true/false (score never revealed)
Benefits:
- Prevents gaming the system by knowing exact thresholds
- Protects biometric scoring algorithms from reverse engineering
- Enables different threshold policies per use case
Current POC implementation (Human.js):
- Real-time challenges (smile + head turns) run in the browser for instant feedback.
- The client captures a baseline frame and one frame per challenge.
- Those frames are sent to
POST /api/liveness/verify, where Next.js re-runs Human.js on Node to make the authoritative decision and return a face embedding.
Limitations (non-production):
- Server-side re-scoring blocks simple UI tampering, but can’t prove frames came from a live camera; replayed or edited frames can still be submitted.
- Human’s antispoof/liveness models are lightweight “quick checks” and not KYC‑grade on their own.
- Model weights are bundled locally via
@vladmandic/human-modelsand served from/human-models/*; first run may still be slow while models initialize, but no external download is needed. - Liveness sessions are stored in memory and reset on server restart.
Proving citizenship often requires revealing exact nationality, which can lead to discrimination. Zentity's ZK Merkle proofs enable group membership verification:
User → Zentity: "Prove I'm EU citizen"
Zentity → ZK Service: Generate Merkle proof (nationality in EU tree)
ZK Service → Verifier: proof + merkleRoot (EU identifier)
Verifier: Knows user is EU citizen, but NOT which of 27 countries
Use Cases:
- EU Right to Work: Verify employment authorization without revealing specific nationality
- Schengen Travel: Prove travel zone eligibility without passport country disclosure
- Regional Compliance: Meet LATAM or EEA requirements without over-sharing
- Anti-Discrimination: Prevent nationality-based bias in hiring/services
Different jurisdictions require different age thresholds. Zentity generates multiple age proofs efficiently:
| Threshold | Use Case |
|---|---|
| 18+ | General adult verification (EU, most jurisdictions) |
| 21+ | US alcohol/cannabis, car rental |
| 25+ | Premium car rental, certain financial products |
All proofs use the same FHE-encrypted DOB, generating new proofs without re-verification.
Zentity uses three complementary cryptographic techniques to enable privacy-preserving identity verification:
┌─────────────────────────────────────────────────────────────────┐
│ ZERO-KNOWLEDGE PROOFS │
│ Prove claims without revealing underlying data │
│ "I am over 18" • "I am EU citizen" • "Document not expired" │
├─────────────────────────────────────────────────────────────────┤
│ FULLY HOMOMORPHIC ENCRYPTION (FHE) │
│ Perform computations on encrypted data │
│ Age comparisons • Gender matching • Liveness scores │
├─────────────────────────────────────────────────────────────────┤
│ CRYPTOGRAPHIC COMMITMENTS │
│ One-way hashes for identity verification │
│ Names • Document numbers • Nationality │
└─────────────────────────────────────────────────────────────────┘
Each cryptographic primitive solves a specific problem:
| Problem | Solution | How It Works |
|---|---|---|
| "Verify my name without storing it" | Commitment | SHA256(name + salt) stored; verify by recomputing |
| "Check if I'm over 18 without seeing my DOB" | FHE | Encrypted DOB compared homomorphically |
| "Prove I'm EU citizen without revealing country" | ZK Proof | Merkle tree membership proof |
| "Delete my data for GDPR" | Commitment | Delete salt → commitment becomes unlinkable |
A commitment is a one-way hash that binds you to a value without revealing it.
How it works:
- During verification:
commitment = SHA256("John Doe" + random_salt) - Commitment stored in database (hash, not name)
- Later verification: Recompute hash with claimed name + stored salt
- Match = verified. No name ever stored.
GDPR compliance: Deleting the salt makes the commitment cryptographically unlinkable to any identity.
FHE allows computations on encrypted data without decryption.
How it works:
- Your date of birth is encrypted:
encrypted_dob = FHE.encrypt(1990-05-15) - Server performs comparison:
is_adult = encrypted_dob <= (current_year - 18) - Only the boolean result is decrypted:
true - Server never sees your actual birthday
Library: TFHE-rs (Rust) - industry-leading FHE implementation from Zama
ZK proofs let you prove a statement is true without revealing why it's true.
How it works:
- You want to prove: "I am an EU citizen"
- Circuit checks: Is your nationality in the EU Merkle tree?
- Proof generated: Mathematical proof that passes verification
- Verifier learns: "Yes, EU citizen" but NOT which of 27 countries
Components:
- Groth16 - Proof system (compact proofs, fast verification)
- Circom - Circuit language defining provable statements
- Poseidon - ZK-optimized hash function (much faster than SHA256 in circuits)
- Powers of Tau - Trusted setup ceremony ensuring proof security
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Document │────▶│ OCR │────▶│ Commitments │
│ Image │ │ Extract │ │ (SHA256) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ FHE │ │ Database │
│ Encrypt │────▶│ (No PII) │
│ DOB/Gender │ └──────────────┘
└──────────────┘ ▲
│ │
▼ │
┌──────────────┐ │
│ ZK Proofs │────────────┘
│ Age/Nation │
└──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Selfie │────▶│ Liveness │────▶│ FHE Score │
│ Image │ │ Check │ │ + Boolean │
└──────────────┘ └──────────────┘ └──────────────┘
Result: Complete identity verification with zero PII storage.
| Data | Storage Type | Purpose |
|---|---|---|
| Birth Year | FHE ciphertext | Age verification at any threshold |
| Full DOB | FHE ciphertext (u32) | Precise age calculation (YYYYMMDD) |
| Liveness Score | FHE ciphertext (u16) | Privacy-preserving anti-spoof threshold |
| Gender | FHE ciphertext (u8) | ISO 5218 encoded, FHE comparisons |
| Name | SHA256 commitment | Verification without storage |
| Document # | SHA256 commitment | Duplicate detection |
| Nationality | SHA256 commitment | ISO 3166-1 alpha-3 code commitment |
| Age Proof | ZK proof (JSON) | Multiple thresholds: 18, 21, 25 |
| Doc Validity Proof | ZK proof | Proves document not expired |
| Nationality Group Proof | ZK Merkle proof | Proves EU/EEA/SCHENGEN membership |
| Data | Reason |
|---|---|
| Document Image | Processed transiently, discarded |
| Selfie Image | Processed transiently, discarded |
| Face Embeddings | Discarded after comparison |
| Liveness Signals | Discarded after analysis |
| Actual Name/DOB | Only commitments stored |
User → Zentity: "Verify me"
Zentity → User: age_proof (ZK), liveness_attestation
User → Retailer: "Here's my age proof"
Retailer → Verify: verify(proof) → true/false
NO PII EVER SHARED - Retailer only knows: user is over 21, is a real person
User → Zentity: Complete verification
User → Exchange: "I want to onboard"
Exchange → User (via Zentity): Request PII disclosure
Zentity → Exchange: Encrypted package (RSA-OAEP + AES-GCM)
- Name, DOB, Nationality (E2E encrypted)
- Face match result (verified, no biometrics shared)
- Liveness attestation
Exchange stores: PII (regulatory requirement)
Zentity stores: Only commitments (minimal liability)
Biometrics: NEVER stored by either party
zentity/
├── apps/
│ ├── web/ # Next.js 16 frontend (includes liveness via Human.js)
│ ├── fhe/ # Rust/Axum - Homomorphic Encryption
│ ├── zk/ # TypeScript/Express - Zero-Knowledge Proofs
│ └── ocr/ # Python/FastAPI - Document OCR (local RapidOCR)
├── tooling/
│ └── bruno-collection/ # API testing
└── docs/ # Documentation
┌───────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js 16) │
│ http://localhost:3000 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ LIVENESS (Human.js) │ │
│ │ • Multi-gesture challenges (smile, blink, head turns) │ │
│ │ • Face detection and matching │ │
│ │ • Runs locally via tfjs-node (no external API calls) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ /api/crypto/* /api/liveness/* /api/kyc/* /api/identity/*│ │
│ └──────────┬──────────────────────────────────┬───────────────────┘ │
└──────────────┼──────────────────────────────────┼─────────────────────────┘
│ │
┌──────────▼──────────┐ ┌────────────────┐ │ ┌────────────────────┐
│ FHE SERVICE │ │ ZK SERVICE │ │ │ OCR SERVICE │
│ Rust/Axum │ │ TS/Express │ │ │ Python/FastAPI │
│ Port 5001 │ │ Port 5002 │ │ │ Port 5004 │
│ │ │ │ │ │ │
│ • /encrypt │ │ • /generate │ │ │ • /process │
│ • /verify-age │ │ • /verify │ │ │ • /extract │
│ • /keys/generate │ │ • /facematch │ │ │ • /ocr │
│ • /encrypt-liveness │ │ • /docvalid │ │ │ │
│ • /verify-liveness │ │ • /national │ │ │ Local RapidOCR │
│ TFHE-rs v1.4.2 │ │ snarkjs │ │ │ (no external calls)│
└─────────────────────┘ └────────────────┘ │ └────────────────────┘
│
└──► All processing local
| Service | Language | Framework | Crypto Library | Port |
|---|---|---|---|---|
| Frontend + Liveness | TypeScript | Next.js 16, Human.js | - | 3000 |
| FHE Service | Rust | Axum | TFHE-rs v1.4.2 | 5001 |
| ZK Service | TypeScript | Express | snarkjs (Groth16) | 5002 |
| OCR | Python 3.10+ | FastAPI, RapidOCR | SHA256 | 5004 |
| Circuit | Purpose | Public Signals |
|---|---|---|
| Age Proof | Prove age >= threshold | currentYear, minAge, isValid |
| Document Validity | Prove not expired | currentDate, isValid |
| Nationality Membership | Prove nationality in group | merkleRoot, isMember |
The Nationality Membership circuit uses Merkle tree proofs to verify membership in predefined country groups without revealing the specific country:
| Group | Countries | Use Case |
|---|---|---|
| EU | 27 countries | EU citizen verification |
| EEA | 30 countries | European work authorization |
| SCHENGEN | 25 countries | Travel zone verification |
| LATAM | 7 countries | Regional compliance |
| FIVE_EYES | 5 countries | Intelligence alliance nations |
docker-compose upClick to expand manual setup instructions
- Node.js 20+ (managed via mise)
- Rust 1.91+ (managed via mise)
- Python 3.12+ (managed via mise)
- pnpm
# Install mise (https://mise.jdx.dev)
curl https://mise.run | sh
# Install project toolchain versions
mise install# Frontend
cd apps/web && pnpm install
# ZK Service
cd services/zk && pnpm install
# Python services
cd services/ocr && python -m venv venv && source venv/bin/activate && pip install -r requirements.txt
cd services/liveness && python -m venv venv && source venv/bin/activate && pip install -r requirements.txt
# FHE Service (Rust - compiles on first run)
cd services/fhe && cargo build --release# Terminal 1: Frontend
cd apps/web && pnpm dev
# Terminal 2: FHE Service
cd services/fhe && cargo run --release
# Terminal 3: ZK Service
cd services/zk && pnpm start
# Terminal 4: OCR Service
cd services/ocr && source venv/bin/activate && uvicorn app.main:app --port 5004
# Terminal 5: Liveness Service
cd services/liveness && source venv/bin/activate && uvicorn app.main:app --port 5003| Document | Description |
|---|---|
| Executive Summary | Business overview and value proposition |
| KYC Data Architecture | FHE vs ZK vs Hash decision framework |
| Liveness Architecture | Anti-spoofing and face matching design |
| ZK Nationality Proofs | Merkle tree nationality verification |
| Frontend UX | Onboarding flow UX best practices |
| API Collection | Bruno API testing collection |
MIT
Contributions welcome! Please read the contributing guidelines before submitting PRs.