Legit: Rethinking KYC Verification — Your Documents Never Leave

    How Legit fundamentally changes document verification by keeping your data server-side and sharing only YES/NO results with cryptographic proof. Zero knowledge, maximum security.

    ·12 min read·Firoj Siddiquie
    securitykyckotlincryptographyprivacyblockchain

    Legit: Rethinking KYC Verification

    When was the last time you felt comfortable sharing your Aadhaar card, PAN, or passport with a random service? Probably never. Yet every time you sign up for a new financial service, rent a place, or apply for a loan, you're forced to hand over your most sensitive documents and then pray they don't get leaked.

    That's broken.

    Legit — a secure KYC verification platform where your documents never leave the server. Only YES/NO verification results with cryptographic proof get shared.

    DigiLocker solved storage. Legit solves verification. Your documents stay yours.


    The Problem with DigiLocker and Traditional KYC

    Let's talk about the elephant in the room: DigiLocker.

    DigiLocker was supposed to solve document verification in India. It did make things convenient — you can share documents digitally instead of carrying physical copies. But here's the issue: it shares actual documents.

    Traditional KYC Flow:

    ┌─────────────────────────────────────┐
    │  Company requests documents         │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  User shares via DigiLocker         │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  Documents stored in company DB     │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  Company gets hacked                │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  YOUR documents leaked              │
    └─────────────────────────────────────┘
    

    Every service provider you share with becomes a potential data breach waiting to happen. And once your documents are out there, you have zero control.

    The Real-World Impact

    • 2023: Air India data breach — 4.5 million customers' passport details leaked
    • 2022: Domino's India — customer data including documents sold on dark web
    • 2021: MobiKwik — 110 million users' KYC data exposed

    The problem isn't just technology. It's the fundamental design of how we do verification.


    Enter Legit: A New Paradigm

    Legit flips the script. Instead of sharing documents, we share verification results.

    Legit Verification Flow:

    ┌─────────────────────────────────────┐
    │  Company requests VERIFICATION      │
    │  (not documents)                    │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  User approves in app               │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  Disposable key generated           │
    │  (single-use, 5-min TTL)            │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  Server verifies internally         │
    │  (documents never leave)            │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  Company gets: PASS/FAIL + proof    │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  Key BURNED (can't be replayed)     │
    └──────────────┬──────────────────────┘
                   ↓
    ┌─────────────────────────────────────┐
    │  Documents stay encrypted on server │
    └─────────────────────────────────────┘
    

    No documents ever leave the server. Period.


    How Legit Actually Works

    1. The Contractual Verification Pipeline

    Instead of document sharing, Legit uses contracts. A service provider creates a verification contract specifying:

    • Who they want to verify (user ID)
    • What document types they need (Aadhaar, PAN, Passport, etc.)
    • Which fields to verify (name, DOB, address, age 18+, etc.)
    • Why they need it (purpose for audit trail)
    POST /api/v1/pipeline/contracts
    {
      "userId": "user123",
      "requiredDocuments": ["AADHAAR_CARD", "PAN_CARD"],
      "verificationFields": ["FULL_NAME", "DATE_OF_BIRTH", "AGE_VERIFICATION"],
      "purpose": "Loan application KYC"
    }

    The contract enters a PENDING_APPROVAL state.


    2. User Review & Approval

    The user sees the contract in their app:

    GET /api/v1/pipeline/user/contracts/pending

    They see:

    • Who is asking (XYZ Bank)
    • What they want to verify (Name, DOB, Age 18+)
    • Why they need it (Loan application)

    The user can:

    • Approve → Generate disposable key
    • Reject → Contract marked REJECTED
    • Ignore → Auto-expires in 24 hours

    When approved, a disposable key is generated:

    {
      "contractId": "contract_abc123",
      "disposableKey": "lgk_xyz789_single_use",
      "expiresAt": "2025-03-20T12:35:00Z",
      "ttl": 300
    }

    This key is:

    • Single-use — Burned after one verification
    • Short-lived — 5-minute default TTL
    • Cryptographically secure — SHA-256 hash stored
    • Non-replayable — Can't be reused even if intercepted

    3. Service Provider Executes Verification

    The service provider uses the disposable key to trigger verification:

    POST /api/v1/pipeline/verify
    {
      "contractId": "contract_abc123",
      "disposableKey": "lgk_xyz789_single_use"
    }

    The key is immediately burned — it cannot be used again. Even if the request fails, the key is gone.


    4. Server-Side Verification (The Magic)

    This is where Legit fundamentally differs from everything else. The verification happens entirely on the server:

    Step 1: Fetch encrypted documents from MongoDB
       ↓
    Step 2: Decrypt using AES-256-GCM (in-memory only)
       ↓
    Step 3: Parse document metadata
       ↓
    Step 4: Verify each requested field
            • FULL_NAME → PASS
            • DATE_OF_BIRTH → PASS
            • AGE_VERIFICATION → PASS
       ↓
    Step 5: Generate SHA-256 proof hash
       ↓
    Step 6: Create signed JWT token
       ↓
    Step 7: Return results to service provider
    

    The documents are never transmitted to the service provider. Only verification results:

    {
      "status": "VERIFIED",
      "verificationResults": {
        "FULL_NAME": "PASS",
        "DATE_OF_BIRTH": "PASS",
        "AGE_VERIFICATION": "PASS"
      },
      "proofHash": "sha256:abc123...",
      "verificationToken": "eyJhbGc...",
      "verifiedAt": "2025-03-20T12:30:45Z",
      "expiresAt": "2026-03-20T12:30:45Z"
    }

    5. Cryptographic Proof & Audit Trail

    Every verification generates:

    1. Proof Hash — SHA-256 hash of verification data

      sha256(contractId + timestamp + results + salt + secret)
      
    2. Signed Token — JWT signed with platform secret

      {
        "contractId": "...",
        "verifiedFields": [...],
        "timestamp": "...",
        "signature": "..."
      }
    3. Audit Log — Immutable record of verification

      Who verified, what was verified, when, proof hash, result
      

    The service provider can prove they did verification without ever having seen your documents.


    The Architecture

    Legit is built with security as the foundation:

    ┌──────────────────────────────────────────────────┐
    │                  API GATEWAY                     │
    │         Route Aggregation · Auth · Health       │
    └─────────────┬────────────────────────────────────┘
                  │
        ┌─────────┼─────────┐
        │         │         │
        ↓         ↓         ↓
    ┌────────┐ ┌────────┐ ┌────────┐
    │ USER   │ │ DOCS   │ │  DATA  │
    │SERVICE │ │SERVICE │ │PIPELINE│
    ├────────┤ ├────────┤ ├────────┤
    │ JWT    │ │MongoDB │ │Contracts│
    │ RBAC   │ │AES-256 │ │Disp.Key│
    │Sessions│ │Encrypt │ │Verify  │
    │BCrypt  │ │Masking │ │Proof   │
    └────┬───┘ └────┬───┘ └────┬───┘
         │          │          │
         └──────────┼──────────┘
                    │
              ┌─────▼─────┐
              │  MongoDB  │
              ├───────────┤
              │  · users  │
              │  · docs   │
              │  · contracts
              └───────────┘
    

    Tech Stack

    • Backend: Kotlin + Ktor 3.4.0 (Netty engine)
    • Database: MongoDB with encrypted document storage
    • Encryption: AES-256-GCM for documents
    • Auth: JWT (Auth0 java-jwt) with refresh tokens
    • Hashing: BCrypt for passwords, SHA-256 for disposable keys and proofs

    Security Deep Dive

    Document Encryption

    Every document uploaded goes through this pipeline:

    1. User uploads document → Base64 encoded
    2. Generate unique salt (16 bytes)
    3. Derive encryption key using PBKDF2:
       PBKDF2WithHmacSHA256(masterSecret, salt, 65536 iterations, 256-bit)
    4. Encrypt with AES-256-GCM (authenticated encryption)
    5. Store: {
         encryptedContent: "...",
         encryptionSalt: "...",
         iv: "...",
         authTag: "..."
       }

    AES-256-GCM gives us:

    • Confidentiality — Can't read without key
    • Authenticity — Can't tamper without detection
    • Integrity — Any modification breaks auth tag

    Disposable Key System

    The disposable key system prevents replay attacks:

    // Generation
    val key = generateSecureKey() // Cryptographically random
    val hash = SHA256(key)
     
    // Storage
    {
      keyHash: "...",
      expiresAt: timestamp,
      used: false
    }
     
    // Verification attempt
    1. Check if expired → Reject
    2. Check if used → Reject
    3. Verify key hash matches (SHA-256)
    4. Mark as used = true (atomic)
    5. Proceed with verification

    Even if someone intercepts the key, they can only use it once within 5 minutes.

    Zero-Knowledge Verification

    The core principle: Service providers learn nothing except YES/NO.

    Traditional KYC:

    Service gets: Full Aadhaar PDF with photo, DOB, address, etc.
    Service learns: Everything about you
    

    Legit:

    Service gets: { "AGE_VERIFICATION": "PASS" }
    Service learns: You're 18+ (nothing else)
    

    This is inspired by zero-knowledge proofs — prove a statement without revealing the data.


    Contract Lifecycle

    A contract goes through this state machine:

                        PENDING_APPROVAL
                               │
            ┌──────────────────┼──────────────────┐
            │                  │                  │
            ↓                  ↓                  ↓
        REJECTED          APPROVED            EXPIRED
                              │              (24hr timeout)
                              │
            ┌─────────────────┼─────────────────┐
            │                 │                 │
            ↓                 ↓                 ↓
        REVOKED    VERIFICATION_IN_PROGRESS  EXPIRED
      (user revoked)          │             (key timeout)
                              │
                        ┌─────┴─────┐
                        │           │
                        ↓           ↓
                    VERIFIED     FAILED
    

    Each state transition is logged for audit.


    Supported Document Types & Verifications

    Document Types

    • AADHAAR_CARD — UIDAI unique identity (12 digits)
    • PAN_CARD — Income Tax PAN (ABCDE1234F format)
    • PASSPORT — Travel document (A1234567 format)
    • DRIVING_LICENSE — RTO license
    • VOTER_ID — Election Commission ID
    • BANK_STATEMENT — Account statement
    • ADDRESS_PROOF — Any address proof
    • INCOME_PROOF — Salary slips, ITR
    • EDUCATION_CERTIFICATE — Degrees, diplomas

    Verification Fields

    • FULL_NAME — Name matches across documents
    • DATE_OF_BIRTH — DOB exists and matches
    • ADDRESS — Address present in address-bearing docs
    • GENDER — Gender field exists
    • FATHER_NAME — Father's name exists
    • DOCUMENT_VALIDITY — Not expired, integrity intact
    • DOCUMENT_NUMBER_MATCH — Doc numbers valid
    • IDENTITY_PROOF — Valid identity document present
    • ADDRESS_PROOF — Valid address proof present
    • AGE_VERIFICATION — User is 18+ based on DOB

    The Mobile App (legit.kt)

    The Android companion app is built with Jetpack Compose and provides:

    Features

    • Biometric Authentication — Fingerprint/Face unlock
    • Document Vault — Encrypted local storage
    • Real-time Approvals — Push notifications for contract requests
    • Offline-first Sync — Works without internet, syncs later
    • Audit Dashboard — See all verifications you've approved
    • Revocation — Instantly revoke any active contract

    Tech

    • Kotlin for type safety
    • Jetpack Compose for modern UI
    • Room DB for local encrypted storage
    • WorkManager for background sync
    • Retrofit for API calls

    Note: The Android app is in early-stage development as part of the hackathon prototype.


    Why This Matters

    For Users

    • Your documents stay yours — Never shared, never exposed
    • You control access — Approve/reject/revoke anytime
    • Full transparency — See exactly what's being verified
    • Audit trail — Complete history of all verifications
    • Reduced risk — Even if a service provider gets hacked, your documents are safe

    For Service Providers

    • No liability — You never store user documents
    • Compliance-friendly — Easier to meet GDPR/privacy laws
    • Cost savings — No need for document storage infrastructure
    • Cryptographic proof — Verifiable proof you did KYC
    • Faster integration — Simple REST API

    For Society

    • Reduced data breaches — Documents aren't floating around in databases
    • Privacy by design — Zero-knowledge approach
    • User empowerment — People control their own data
    • Trust building — Cryptographic proof instead of "trust us"

    Comparison: Legit vs DigiLocker

    FeatureDigiLockerLegit
    Document SharingShares actual documentsNever shares documents
    Data ExposureHigh — docs leave systemZero — only YES/NO results
    Replay AttacksPossible with shared linksImpossible — disposable keys
    User ControlLimitedFull — approve/reject/revoke
    Audit TrailBasicComplete with crypto proof
    Data Leak RiskHigh (at every provider)Zero (docs never leave)
    PrivacyDocuments exposedZero-knowledge verification
    ComplianceProvider must secure docsProvider never sees docs

    Real-World Use Cases

    1. Loan Applications

    Bank: "Verify user is 18+, has PAN, and income proof"
    User: Approves
    Legit: { "AGE": PASS, "IDENTITY_PROOF": PASS, "INCOME_PROOF": PASS }
    Bank: Loan approved (never saw actual documents)
    

    2. Rental Agreements

    Landlord: "Verify identity and address proof"
    User: Approves
    Legit: { "IDENTITY_PROOF": PASS, "ADDRESS_PROOF": PASS }
    Landlord: Lease signed (never saw Aadhaar/Passport)
    

    3. Job Onboarding

    Company: "Verify education and identity"
    User: Approves
    Legit: { "EDUCATION": PASS, "IDENTITY": PASS }
    Company: Onboarding complete (zero PII stored)
    

    The Code (Simplified)

    Here's a peek at the verification logic:

    fun verifyContract(contractId: String, disposableKey: String): VerificationResult {
        // 1. Validate and burn key
        val contract = contractRepository.findById(contractId)
        if (!keyService.validateAndBurnKey(contract.id, disposableKey)) {
            throw InvalidKeyException("Key expired or already used")
        }
     
        // 2. Fetch encrypted documents
        val documents = documentRepository.findByUserId(contract.userId)
     
        // 3. Decrypt in-memory (never persisted)
        val decryptedDocs = documents.map { 
            encryptionService.decrypt(it.encryptedContent, it.salt, it.iv)
        }
     
        // 4. Verify each field
        val results = contract.verificationFields.map { field ->
            field to verifyField(field, decryptedDocs, contract.requiredDocuments)
        }.toMap()
     
        // 5. Generate cryptographic proof
        val proofHash = generateProofHash(contract.id, results)
        val verificationToken = generateSignedToken(contract.id, results)
     
        // 6. Clear decrypted data
        decryptedDocs.clear()
     
        // 7. Return results
        return VerificationResult(
            status = if (results.all { it.value == PASS }) VERIFIED else FAILED,
            results = results,
            proofHash = proofHash,
            verificationToken = verificationToken
        )
    }

    Challenges & Solutions

    Challenge 1: Key Replay Attacks

    Problem: What if someone intercepts the disposable key and reuses it?

    Solution: Atomic key burning. The key is marked as used in the database in an atomic operation. Even if two requests come simultaneously, only one succeeds.

    Challenge 2: Document Integrity

    Problem: How do we ensure documents haven't been tampered with?

    Solution: AES-256-GCM authenticated encryption. Any tampering breaks the authentication tag, and decryption fails.

    Challenge 3: Proof Verification

    Problem: How can a third party verify the verification happened?

    Solution: Signed JWT tokens with 1-year validity. Anyone can verify the signature using the public key.

    Challenge 4: Revocation

    Problem: User approves, then changes mind. What now?

    Solution: Real-time revocation. User can revoke a contract, which immediately invalidates any unused keys and marks the contract as REVOKED.


    The Hackathon Build: Graph-E-Thon 3.0

    This project was built for Graph-E-Thon 3.0 at Graphic Era University Dehradun, April 2-5, 2025 (72 hours). It fits under Track 3: Digital Infrastructure & Security (SDG 9 + SDG 16).

    What Was Built

    • Core verification pipeline with disposable keys
    • MongoDB with AES-256-GCM encrypted storage
    • REST API with JWT authentication
    • Contract approval/rejection flow
    • SHA-256 proof hash generation

    Known Limitations

    • No actual document file upload yet — currently using metadata only
    • Proof hash is tamper-evident but not cryptographically signed with public key verification
    • No frontend demo — all testing done via curl commands
    • Android app is early-stage prototype

    The Philosophy

    Legit is built on a simple belief:

    Your data should work for you, not against you.

    In a world where data breaches are the norm, we need a fundamental shift. Instead of distributing your sensitive documents to hundreds of services, we centralize verification while distributing the results.

    DigiLocker solved storage. Legit solves verification. Your documents stay yours.

    It's zero-knowledge, maximum security.


    Try It Out

    For Developers

    git clone https://github.com/sonusid1325/legit
    cd legit
    ./gradlew build
    docker-compose up -d  # Start MongoDB
    ./gradlew run

    API available at http://localhost:8080

    For Users

    Download the Android app (legit.kt) from GitHub releases.


    Blockchain Integration: Immutable Audit Trail

    One of Legit's core differentiators is on-chain verification logging. While the current hackathon build uses MongoDB for audit logs, the design includes blockchain integration for tamper-proof verification records.

    What Goes On-Chain

    Every verification event is logged to a smart contract:

    struct VerificationLog {
        bytes32 contractId;       // Unique contract identifier
        address serviceProvider;  // Who requested verification
        bytes32 userIdHash;       // Hashed user ID (privacy)
        uint256 timestamp;        // When verification occurred
        bytes32 proofHash;        // SHA-256 of verification results
        bool result;              // PASS/FAIL
    }

    What Does NOT Go On-Chain

    • No user documents
    • No PII (personally identifiable information)
    • No actual verification data
    • Only hashes and metadata

    The Flow

    User approves contract
       ↓
    Disposable key generated
       ↓
    Service provider triggers verification
       ↓
    Legit server performs verification (off-chain)
       ↓
    Verification result + proof hash logged to blockchain
       ↓
    Key burn event logged to blockchain
       ↓
    Service provider gets result
    

    Why Blockchain?

    1. Immutability - Once written, cannot be altered or deleted
    2. Transparency - Anyone can verify a verification happened
    3. Accountability - Service providers can't deny requesting verification
    4. Audit Trail - Complete history of all verification events
    5. Verifier Reputation - Track service provider behavior over time

    Verifier Reputation System

    The smart contract tracks service provider behavior:

    struct Verifier {
        address verifierAddress;
        uint256 totalVerifications;
        uint256 revokedCount;        // How many users revoked access
        uint256 excessiveRequestCount; // Requesting too much data
        uint256 reputationScore;     // Calculated score
    }

    Reputation factors:

    • High revocation rate → Lower score (users don't trust them)
    • Excessive data requests → Lower score (asking for more than needed)
    • Clean history → Higher score

    Users can see verifier reputation before approving contracts.

    Planned Implementation

    Chain: Polygon Mumbai testnet (low gas fees, fast finality) Contract: Simple event logger with reputation tracking Storage: IPFS for larger proof data if needed Cost: ~$0.0001 per verification log

    Current Status

    Hackathon build: MongoDB-based audit logs (working) Post-hackathon: Migrate to hybrid MongoDB + Polygon Long-term: Full decentralized audit with IPFS


    Three Key Differentiators

    1. Documents never leave — verification happens server-side
    2. Every verification audited on-chain — immutable proof trail
    3. Verifiers have public reputation scores — shady providers get flagged

    Open Questions

    I'm still exploring:

    1. Decentralized Storage? Should documents live on-chain or IPFS?
    2. Multi-sig Approvals? Should high-value verifications require multiple approvers?
    3. Biometric Verification? Can we tie verification to liveness detection?
    4. International Standards? How do we map this to eIDAS, ISO standards?

    If you have thoughts, drop a GitHub issue or reach out.


    Conclusion

    KYC doesn't have to mean "give us all your documents and pray we don't lose them."

    With Legit, you keep your documents. Service providers get verification. Everyone wins.

    It's time we rethought how identity works on the internet.

    Your documents never leave. Only verification does.


    Links


    By Firoj Siddiquie. If you find this interesting, star the repo or share it with someone who cares about privacy.