Cryptographic Security in Provii's Privacy-Preserving Age Verification

Technical whitepaper on zk-SNARK based age verification with Groth16, RedJubjub signatures, and Pedersen commitments. Published by Maelstrom AI.

Public

Cryptographic Security in Provii’s Age Verification System

Abstract

This whitepaper presents the cryptographic architecture of Provii’s privacy-preserving age verification system, built on zero knowledge proof technology. The system employs Groth16 zk-SNARKs on the BLS12-381 curve to enable users to prove they meet age requirements without revealing their date of birth or other personally identifiable information. Core cryptographic primitives include Pedersen commitments on the JubJub curve for hiding date of birth, a custom RedJubjub signature scheme for credential issuance, BLAKE2s hashing for domain separation, and SHA-256 for relying party challenge binding. The system is designed to achieve strong privacy properties including unlinkability across verifications, replay prevention via nullifiers, and protection against credential forgery through cryptographic binding to trusted issuers. With approximately 99,000 circuit constraints (99,084 exact), proof generation completes in 2-15 seconds depending on device capabilities, while verification remains constant at 5-20ms. This document provides technical depth on the cryptographic constructions, security analysis, implementation details, and acknowledges current limitations including the need for formal third-party cryptographic audit of custom components.

Keywords: Zero knowledge proofs, Groth16, age verification, privacy-preserving protocols, zk-SNARKs, RedJubjub signatures, Pedersen commitments


1. Introduction

1.1 Motivation

Age verification presents a fundamental tension between regulatory compliance and individual privacy. Traditional identity verification systems require disclosure of government-issued documents containing extensive personal information (full names, addresses, dates of birth, identity numbers), creating privacy risks, potential for age discrimination, and attack surfaces for data breaches. Regulatory frameworks including COPPA (Children’s Online Privacy Protection Act), GDPR (General Data Protection Regulation), and emerging age assurance standards like ISO 27566-1 demand both effective age verification and privacy protection.

Provii addresses this tension through cryptographic innovation. Our system enables users to prove they exceed an age threshold (e.g., “over 18”) without revealing their exact date of birth or any other personally identifiable information. This is achieved through zero knowledge proof technology, specifically Groth16 zk-SNARKs, combined with commitment schemes and digital signatures that together create a privacy-preserving credential system.

The system is designed to satisfy three critical properties:

  1. Privacy: Verifiers learn only that a user exceeds the required age threshold
  2. Unlinkability: Different verifications by the same user are designed to be unlinkable
  3. Soundness: Users cannot forge proofs claiming they meet age requirements they do not satisfy

1.2 Threat Model

We consider the following threat actors and attack scenarios:

Adversarial Users (Provers):

  • Attempt to generate valid proofs without legitimate credentials
  • Try to reuse credentials across multiple verifications
  • Attempt to create fake credentials with fabricated dates of birth
  • Seek to bypass age thresholds by manipulating proof parameters

Adversarial Verifiers (Relying Parties):

  • Attempt to learn users’ exact dates of birth or ages beyond the binary threshold result
  • Try to correlate different verification sessions from the same user
  • Attempt to replay captured proofs to different relying parties
  • Seek to extract private credential information from public proof data

Adversarial Issuers:

  • Malicious issuers creating credentials without proper identity verification
  • Compromised issuer signing keys enabling credential forgery
  • Issuers attempting to track users across verification sessions

Network Adversaries:

  • Man-in-the-middle attacks attempting to capture and replay proofs
  • Traffic analysis to correlate verification sessions
  • Timing attacks on proof verification

Out of Scope:

  • Quantum adversaries (system is not quantum-resistant)
  • Physical device compromise or malware on user devices
  • Social engineering attacks on users
  • Compromise of trusted setup participants (mitigated through parameter fingerprinting and verification)

1.3 Security Goals

The Provii age verification system is designed to achieve the following security properties:

Privacy Properties:

  1. Age Threshold Verification Without DOB Disclosure: Proofs reveal only that age ≥ threshold, nothing more
  2. Unlinkability Across Verifications: Different verification sessions are designed to be unlinkable by verifiers
  3. Unlinkability Across Relying Parties: Different websites are expected to be unable to track the same user
  4. Commitment Hiding: Date of birth commitments reveal no information about the underlying date

Security Properties:

  1. Soundness: Computationally infeasible to create valid proofs for false age claims
  2. Credential Unforgeability: Only authorised issuers can create valid credentials
  3. Replay Prevention: Proofs are designed to prevent reuse within the same relying party context
  4. Challenge Binding: Proofs are cryptographically bound to specific verification sessions
  5. Issuer Trust Without Centralization: Multiple independent issuers can operate without central coordination

Implementation Properties:

  1. Cryptographically Secure Randomness: All random values generated using OS-level entropy
  2. Deterministic Nonce Generation: Signature nonces derived deterministically to reduce leakage risk
  3. Constant-Time Verification: Verification time independent of private inputs

2. Cryptographic Primitives

2.1 Groth16 zk-SNARKs

Standard: Groth16 proof system (Jens Groth, Eurocrypt 2016) Library: bellman 0.14 (Zcash cryptography library) Curve: BLS12-381 pairing-friendly elliptic curve Security Level: ~128-bit computational security

Properties:

  • Succinctness. Constant-size proofs (192 bytes) regardless of circuit complexity
  • Zero knowledge. Proofs reveal only the truth of the statement, no additional information
  • Soundness. Computationally infeasible to create valid proofs for false statements under q-SDH assumption

Why Groth16?

Groth16 is the most widely deployed zk-SNARK construction in production systems (Zcash, Filecoin, zkSync), offering:

  • Smallest proof size (3 group elements: 192 bytes)
  • Fastest verification time (single pairing check: 5-20ms)
  • Well-studied security assumptions
  • Mature implementation libraries

Trade-offs:

  • Requires trusted setup (addressed via parameter generation procedures and fingerprint verification)
  • Not quantum-resistant (acceptable for current threat model)
  • Circuit-specific setup (parameters tied to specific constraint system)

Circuit Complexity:

  • Total constraints: 99,084 (v7 circuit, optimised from initial ~117,000)
  • Public inputs: 8 field elements
  • Constraint domains: Age comparison (with direction), signature verification, commitment verification, nullifier derivation

2.2 Pedersen Commitments

Curve: JubJub twisted Edwards curve (embedded in BLS12-381 scalar field) Library: sapling-crypto (Zcash Sapling protocol) Personalization: NoteCommitment (Zcash-compatible generator points)

Construction:

C = PedersenHash(dob_bits || r_bits)

where:

  • dob_bits: 32-bit little-endian representation of date of birth (days since Unix epoch, stored as i32 biased to u32 via XOR with 0x8000_0000 for unsigned comparison in circuit)
  • r_bits: exactly 128 bits of cryptographic randomness (enforced by the circuit; r_bits.len()!= 128 causes synthesis failure)
  • PedersenHash: Zcash Pedersen hash with NoteCommitment generators

Properties:

  1. Perfectly Hiding: P(C | dob) = P(C) (information-theoretic hiding)
  • Given commitment C, all possible dates of birth are equally likely
  • Randomness reduces to uniform distribution over commitment space
  1. Computationally Binding: Infeasible to find (dob₁, r₁) ≠ (dob₂, r₂) with same commitment
  • Security reduction to discrete logarithm problem on JubJub
  • Breaking binding property equivalent to solving discrete log
  1. Homomorphic Structure: Enables efficient circuit verification
  • Pedersen commitments compose linearly
  • Circuit-friendly verification using fixed-base scalar multiplication

Randomness Requirements:

The circuit enforces exactly 128 bits (16 bytes) of commitment randomness. While the underlying Sapling Pedersen hash supports up to 1,096 bits of randomness input, the protocol fixes r_bits at 128 bits to:

  • Match the 128-bit security level of the underlying curves
  • Minimise circuit constraint overhead
  • Ensure deterministic circuit structure (fixed R1CS shape)

Randomness is generated using OsRng (operating system cryptographic RNG) on native platforms and getrandom (Web Crypto API) on WebAssembly.

Implementation:

/// Bias function: maps signed i32 ordering to unsigned u32 ordering
/// so that bias(-3652) < bias(0) < bias(13880) holds as unsigned comparison.
pub fn bias_for_circuit(days: i32) -> u32 {
    (days as u32) ^ 0x8000_0000
}

pub fn pedersen_commit_dob(dob_days: i32, r_bits: &[bool]) -> [u8; 32] {
    // Bias i32 to u32 for unsigned comparison in circuit
    let biased = bias_for_circuit(dob_days);

    // Convert biased value to little-endian bits
    let mut dob_bits = vec![];
    let mut value = biased;
    for _ in 0..32 {
        dob_bits.push((value & 1) != 0);
        value >>= 1;
    }

    // Concatenate date bits with randomness bits (exactly 128 bits)
    let mut in_bits = dob_bits;
    in_bits.extend_from_slice(r_bits);   // 32 + 128 = 160 bits

    // Hash with Pedersen generators (Zcash NoteCommitment)
    let point = pedersen_hash(
        Personalization::NoteCommitment,
        in_bits.into_iter()
    );

    point.to_bytes()
}

2.3 RedJubjub Signatures

Type: Custom Schnorr-style signature scheme Curve: JubJub (prime-order subgroup) Key Size: 32 bytes (JubJub scalar field) Signature Size: 64 bytes (32-byte R point + 32-byte s scalar)

⚠️ CRITICAL NOTE: This is a custom cryptographic construction, NOT compatible with Zcash’s RedJubjub implementation. The customisation is necessary for circuit compatibility (scalar field alignment), but requires formal cryptographic audit.

Why Custom RedJubjub?

Standard signature schemes (Ed25519, ECDSA) operate on different curves than BLS12-381, making circuit verification expensive. RedJubjub operates on JubJub, embedded in BLS12-381’s scalar field, enabling efficient in-circuit signature verification.

The custom variant differs from Zcash RedJubjub in:

  • Deterministic nonce generation (RFC 6979 style, not Zcash’s approach)
  • Domain separation tags (“ProviiRJ” prefix)
  • Scalar field reduction method (must match circuit exactly)

Signature Generation:

1. Derive nonce: r = H("ProviiRJ/nonce" || sk || msg_hash)
2. Compute R = r · G (where G is JubJub generator)
3. Compute challenge: c = H("ProviiRJ" || R || VK || msg_hash)
4. Compute s = r + c · sk (in JubJub scalar field)
5. Output signature: σ = (R, s)

Signature Verification:

1. Parse signature: σ = (R, s)
2. Recompute challenge: c = H("ProviiRJ" || R || VK || msg_hash)
3. Verify equation: s · G == R + c · VK
4. Accept if equation holds, reject otherwise

Deterministic Nonce Generation (reduces risk of catastrophic nonce reuse):

fn nonce_from(sk_bytes: &[u8; 32], msg_hash: &[u8]) -> JubjubScalar {
    let mut hasher = Blake2s256::new();
    hasher.update(PROVII_RJ_NONCE_TAG); // "ProviiRJ/nonce"
    hasher.update(sk_bytes);
    hasher.update(msg_hash);
    let digest: [u8; 32] = hasher.finalize().into();

    // Wide reduction into JubJub scalar field
    let mut wide = [0u8; 64];
    wide[..32].copy_from_slice(&digest);
    JubjubScalar::from_bytes_wide(&wide)
}

Security Analysis:

Unforgeability: Security reduces to discrete logarithm hardness on JubJub curve. An adversary who can forge signatures can compute discrete logarithms.

Non-Malleability: Challenge includes R, VK, and message, reducing signature malleability risk.

Nonce Reuse Prevention: Deterministic nonce derivation is designed to reduce the risk of nonce reuse (which would leak the signing key in Schnorr-style schemes).

⚠️ Audit Gap: While the construction follows sound cryptographic principles, the custom implementation has not undergone formal third-party cryptographic audit. This is the highest-priority security gap in the system.

2.4 Hash Functions

BLAKE2s-256:

  • Standard. RFC 7693
  • Usage. RedJubjub signature challenge hash (with personalization), deterministic nonce derivation, credential prehashing, nullifier derivation (Pedersen), parameter fingerprinting
  • Properties. 256-bit output, collision-resistant, efficient on 32-bit platforms
  • Personalization. Domain separation via personalization parameter (8 bytes, used for RedJubjub challenge)

SHA-256:

  • Standard. FIPS 180-4
  • Usage. PKCE code challenge (RFC 7636), origin hashing
  • Properties. 256-bit output, NIST-standardized

Domain Separation Tags:

  • "ProviiRJ": RedJubjub signature challenge computation
  • "ProviiRJ/nonce": RedJubjub deterministic nonce derivation
  • "provii.nullifier.pedersen.v1": Pedersen-based nullifier derivation
  • "zerokp.challenge.v1": Relying party challenge binding
  • "zerokp.vk.id.v1": Verifying key fingerprinting

Domain separation is designed to prevent cross-protocol attacks where signatures or commitments from one context might be valid in another.


3. System Architecture

3.1 Components

Issuers (Credential Authorities):

  • Verify user identity and date of birth through traditional KYC processes
  • Generate credentials binding date of birth to cryptographic commitments
  • Sign credentials using RedJubjub with issuer signing key
  • Publish verifying keys via JWKS (JSON Web Key Set)

Wallets (User Agents):

  • Store credentials securely on user devices
  • Generate zero knowledge proofs in response to verification challenges
  • Manage commitment randomness and private witness data
  • Never transmit date of birth or credential secrets

Verifiers (Proof Validation Services):

  • Validate zero knowledge proofs against age thresholds
  • Check proof binding to verification challenges
  • Maintain nullifier databases to reduce replay attack risk
  • Return binary verification results (pass/fail)

Relying Parties (Age-Restricted Services):

  • Generate verification challenges with origin binding
  • Request proofs from users via QR codes or challenge codes
  • Consume verification results from verifier services
  • Integrate age gates into application flows

3.2 Credential Structure

Credentials follow the CredMsgV2 format:

pub struct CredMsgV2 {
    v: u8,                  // Version (2)
    kid: Vec<u8>,           // Key ID (14 bytes, e.g., "provii:2025-11")
    c: [u8; 32],            // Pedersen commitment to DOB
    iat: u64,               // Issued at (Unix timestamp)
    exp: u64,               // Expiration (Unix timestamp)
    schema: Vec<u8>,        // Credential schema (12 bytes)
}

Signed Credential Package (transmitted to user after issuance):

{
    credential: CredMsgV2,
    signature: [u8; 64],     // RedJubjub signature
    witness: {
        dob_days: i32,       // Date of birth (days since epoch, signed)
        r_bits: Vec<bool>,   // Commitment randomness (128 bits)
    }
}

Key Properties:

  • Commitment c binds to date of birth without revealing it
  • Signature covers all credential fields (v, kid, c, iat, exp, schema)
  • Key ID (kid) enables key rotation and multi-issuer support
  • Expiration (exp) limits credential lifetime
  • Witness data (dob_days, r_bits) kept private, never transmitted during verification

3.3 Proof Structure

Public Inputs (8 BLS12-381 field elements):

  1. direction (1 element): Age comparison direction (32 LE bits, 1 significant bit)
  • 1 = Over (AgeDirection::Over): user is at least min_age years old (cutoff_days >= dob_days)
  • 0 = Under (AgeDirection::Under): user is at most max_age years old (dob_days >= cutoff_days)
  • The circuit uses a conditional swap (mux) to select operand order based on this bit
  1. cutoff_days (1 element): Age threshold in days since Unix epoch
  • Example: For “over 18” check on 2025-11-08, cutoff = days(2007-11-08) = 13,826
  1. rp_hash (2 elements): Relying party challenge hash (32 bytes, multipacked)
  • Binds proof to specific verification session and origin
  • Computed as: Blake2s-256(rp_challenge), where rp_challenge = SHA-256(origin || nonce || "zerokp.challenge.v1")
  1. issuer_vk_bytes (2 elements): Issuer verifying key (32 bytes, multipacked)
  • Binds proof to trusted issuer
  • Enables multi-issuer platform without central coordination
  1. cred_nullifier (2 elements): Credential nullifier (32 bytes, multipacked)
  • Derived as: PedersenHash("provii.nullifier.pedersen.v1" || commitment)
  • Enables replay prevention without revealing commitment

Packing: direction(1) + cutoff_days(1) + rp_hash(2) + issuer_vk(2) + nullifier(2) = 8 field elements total. Bellman adds an implicit 1 input at index 0, so the verifying key has ic.len() == 9.

Private Witness (not revealed in proof):

  • dob_days: Actual date of birth (i32, biased to unsigned range via XOR with 0x8000_0000 for unsigned comparison in circuit)
  • r_bits: Commitment randomness (128 bool vector)
  • issuer_signature: RedJubjub signature (R, s)
  • Credential metadata: version, key ID, timestamps, schema

Proof Size: 192 bytes (constant, regardless of circuit complexity)

Multipack Encoding:

BLS12-381 field elements are 255 bits, but only 254 bits are safe to pack (bit 254 must be zero to ensure canonical field representation). The system packs 32-byte values into field elements using bit-packing:

// Each 32-byte value → 2 field elements (127 bits each)
// Bit 254 = 0 for safety (prevents field overflow)

This encoding is critical for security: improper packing could allow field element overflow, breaking soundness.


4. Cryptographic Protocols

4.1 Issuance Protocol

The credential issuance flow involves identity verification followed by cryptographic credential generation.

Protocol Steps:

1. User → Issuer: Identity verification (government ID, biometric, etc.)
   - Issuer verifies identity through KYC process
   - Issuer extracts date of birth: dob_days (days since Unix epoch, i32)
   - Issuer creates Ed25519-signed DobAttestation containing dob_days

2. Issuer → User: Transmit DobAttestation
   attestation = { dob_days, issuer_id, timestamp, nonce, ed25519_signature }

3. User Wallet: Generate commitment randomness locally
   r_bits ← OsRng.random_bits(128)    // Exactly 128 bits, enforced by circuit

4. User Wallet → Provii provii-issuer: Send attestation + r_bits
   { attestation: DobAttestation, r_bits: Vec<bool> }

5. provii-issuer: Verify Ed25519 attestation signature
   Ed25519_Verify(ed25519_vk, attestation_message, attestation.signature) == OK

6. provii-issuer: Compute Pedersen commitment server-side
   C ← PedersenCommit(dob_days, r_bits)   // dob_days biased via XOR 0x8000_0000

7. provii-issuer: Construct and sign credential
   cred ← CredMsgV2 {
       v: 2,
       kid: "provii:2025-11",
       c: C,
       iat: current_timestamp(),
       exp: current_timestamp() + 1_year,   // Configurable; may be reduced to 60-90 days
       schema: "age.v2"
   }
   prehash ← CredPrehash(v, kid, C, iat, exp, schema)   // includes CRED_V2_DST prefix
   msg_hash ← Blake2s(prehash)
   σ ← SignRedJubjub(msg_hash, sk_issuer)

8. provii-issuer: Discard r_bits (not stored after commitment computation)

9. provii-issuer → User Wallet: Return signed credential
   {
       credential: cred,
       signature: σ
   }

10. User Wallet: Verify signature before storage
    VerifyRedJubjub(σ, msg_hash, vk_issuer) == OK
    Store credential + private witness (dob_days, r_bits) in encrypted wallet

Security Properties:

  • Wallet generates r_bits locally, contributing user entropy to the commitment
  • provii-issuer discards r_bits after computing the Pedersen commitment
  • DOB is processed transiently by provii-issuer during commitment computation, then discarded
  • User receives provably valid credential (signature verification)
  • Commitment hides date of birth from anyone without randomness
  • Signature is designed to prevent credential tampering

4.2 Verification Protocol

The zero knowledge verification flow enables users to prove age thresholds without revealing dates of birth.

Protocol Steps:

1. Relying Party → User: Generate verification challenge
   nonce ← random_bytes(32)
   origin ← "https://example.com"
   rp_challenge ← SHA-256(origin || nonce || "zerokp.challenge.v1")
   cutoff_age ← 18 (years)
   cutoff_days ← days_since_epoch(today - 18_years)

   Challenge QR code or challenge code displayed to user

2. User Wallet: Parse and validate challenge
   - Verify cutoff_age is reasonable (e.g., ≤ 25 years)
   - Verify origin matches current website
   - Check credential not expired (iat ≤ now ≤ exp)

3. User Wallet: Prepare witness data
   witness ← {
       dob_days: user's actual DOB,
       r_bits: commitment randomness from credential,
       issuer_signature: σ from credential
   }

   Verify age threshold satisfied:
   assert(dob_days ≤ cutoff_days)

4. User Wallet: Compute nullifier
   cred_nullifier ← PedersenHash(
       "provii.nullifier.pedersen.v1" || credential.c
   )

5. User Wallet: Generate zero knowledge proof
   public_inputs ← {
       direction,          // Over (1) or Under (0)
       cutoff_days,
       rp_challenge,
       issuer_vk_bytes,
       cred_nullifier
   }

   π ← Groth16Prove(circuit, public_inputs, witness)

   // Proof generation: 2-15 seconds depending on device

6. User → Verifier: Submit proof
   {
       proof: π (192 bytes),
       direction,
       cutoff_days,
       rp_challenge,
       issuer_vk_bytes,
       cred_nullifier
   }

7. Verifier: Check nullifier replay
   if nullifier_db.contains(cred_nullifier, rp_id):
       return REJECT // Replay attack detected

8. Verifier: Verify proof cryptographically
   result ← Groth16Verify(π, public_inputs, verifying_key)

   // Verification: 5-20ms (constant time)

9. Verifier: Store nullifier (prevent future replay)
   nullifier_db.insert(cred_nullifier, rp_id, timestamp)

10. Verifier → Relying Party: Return verification result
    {
        status: "verified",
        cutoff_days,
        timestamp
    }

    // NO date of birth or age transmitted

11. Relying Party: Grant access
    Grant user access to age-restricted content/service

Security Properties:

  • Zero knowledge. Verifier learns only the direction-dependent comparison result (Over: cutoff >= dob, Under: dob >= cutoff), nothing about actual DOB
  • Challenge Binding. Proof is designed to be invalid for a different relying party (rp_challenge differs)
  • Replay Prevention. Nullifier database is designed to prevent credential reuse
  • Unlinkability. Different verifications use random session IDs, designed to be unlinkable
  • Issuer Binding. Proof includes issuer_vk_bytes, designed to ensure credential from trusted issuer

4.3 Challenge Generation (PKCE-like Construction)

To reduce risk of proof interception and replay attacks, the system uses a challenge-response mechanism inspired by OAuth 2.0 PKCE (Proof Key for Code Exchange).

Challenge Generation:

1. Relying Party: Generate cryptographically secure nonce
   nonce ← OsRng.random_bytes(32)

2. Relying Party: Compute origin hash
   origin_hash ← SHA256(origin)

3. Relying Party: Compute RP challenge
   rp_challenge ← SHA-256(
       origin ||
       nonce ||
       "zerokp.challenge.v1"
   )

4. Relying Party: Generate challenge ID (for tracking)
   challenge_id ← random_uuid()

5. Relying Party: Store challenge state
   challenge_db.insert(challenge_id, {
       rp_challenge,
       origin,
       nonce,
       cutoff_days,
       created_at: timestamp(),
       status: "pending"
   })

6. Relying Party: Generate QR code or challenge code
   qr_payload ← {
       challenge_id,
       cutoff_days,
       origin
   }

   Display QR code or 12-character challenge code

Challenge Verification (when proof is submitted):

1. Verifier: Lookup challenge state
   state ← challenge_db.get(challenge_id)

2. Verifier: Verify challenge freshness
   if timestamp() - state.created_at > 5_minutes:    // 300 seconds
       return REJECT // Challenge expired

3. Verifier: Verify challenge not already used
   if state.status != "pending":
       return REJECT // Already redeemed

4. Verifier: Recompute expected rp_challenge
   expected_rp_challenge ← SHA-256(
       state.origin ||
       state.nonce ||
       "zerokp.challenge.v1"
   )

5. Verifier: Verify proof's rp_challenge matches
   if proof.rp_challenge != expected_rp_challenge:
       return REJECT // Challenge mismatch

6. Continue with proof verification...

Security Properties:

  • Origin Binding. Proof bound to specific origin, designed to reduce cross-site replay risk
  • Nonce Freshness. Each verification session has unique nonce
  • Challenge Expiration. 5-minute TTL (300 seconds) limits replay window. The replay tag tracking window (REPLAY_WINDOW_SECONDS) extends to 10 minutes to catch late-arriving duplicates.
  • One-Time Use. Challenge status tracking is designed to reduce reuse risk

5. Security Analysis

5.1 Zero knowledge Property

Claim: Proofs reveal only that the age comparison holds in the declared direction (either cutoff_days >= dob_days for Over, or dob_days >= cutoff_days for Under), and nothing else about the prover’s date of birth or credential.

Analysis:

Groth16 provides perfect zero knowledge: there exists a simulator that can generate proofs indistinguishable from real proofs, without access to the witness (dob_days, r_bits, signature). This is proven in the original Groth16 paper (Eurocrypt 2016).

What Public Inputs Reveal:

  • direction: Whether the check is “at least X” (Over) or “at most X” (Under)
  • cutoff_days: Age threshold (chosen by relying party, not private)
  • rp_hash: Challenge hash (random session data, not linked to user)
  • issuer_vk_bytes: Issuer identity (public key, not private)
  • cred_nullifier: Pseudorandom value derived from commitment (unlinkable to user without commitment randomness)

What Remains Private:

  • Exact date of birth (dob_days)
  • Commitment randomness (r_bits)
  • Credential signature
  • Issuer signing key

Privacy Design: An adversarial verifier who sees multiple proofs from the same user (different sessions, different relying parties) is expected to gain no information beyond:

  1. Each proof satisfies its respective age threshold
  2. Each proof came from a credential signed by a trusted issuer

The verifier cannot:

  • Determine the user’s exact age or date of birth
  • Correlate proofs across different relying parties (designed to be unlinkable)
  • Learn anything about the commitment randomness

5.2 Soundness

Claim: A dishonest prover cannot create a valid proof that they satisfy an age threshold they do not actually satisfy.

Analysis:

Groth16 soundness relies on the q-Strong Diffie-Hellman (q-SDH) assumption on the BLS12-381 curve. Under this assumption, the probability that an adversary can create a valid proof for a false statement is negligible (≤ 2^-128 for our security level).

Constraint System Enforcement:

The circuit enforces critical constraints:

  1. Age Check: cutoff_days ≥ dob_days
  • Constraint: cutoff_days - dob_days ≥ 0 (range check)
  • Cannot be violated without breaking soundness
  1. Commitment Verification: C == PedersenHash(dob_days || r_bits)
  • Circuit recomputes commitment from witness
  • Binds proof to specific date of birth
  1. Signature Verification: VerifyRedJubjub(σ, msg_hash, vk_issuer) == true
  • Circuit verifies issuer signature
  • Designed to prevent fabrication of credentials
  1. Nullifier Derivation: nullifier == PedersenHash(DST || C)
  • Designed to ensure nullifier matches commitment
  • Reduces nullifier manipulation risk

Attack Resistance:

Scenario 1: User tries to claim older age than actual

  • Requires finding different dob_days' that satisfies commitment constraint
  • Breaking commitment binding property (discrete log on JubJub)
  • Computationally infeasible (~2^128 operations)

Scenario 2: User forges issuer signature

  • Requires forging RedJubjub signature without signing key
  • Breaking signature unforgeability (discrete log on JubJub)
  • Computationally infeasible (~2^128 operations)

Scenario 3: User fabricates credential without issuer

  • Requires creating valid commitment and signature
  • Circuit verifies signature against issuer’s verifying key (public input)
  • Cannot forge without breaking signature scheme

Soundness Probability: ≤ 2^-128 (negligible under q-SDH assumption)

5.3 Replay Prevention

Mechanism: Two-layer nullifier system

Layer 1: Credential-Level Nullifiers

cred_nullifier = PedersenHash("provii.nullifier.pedersen.v1" || commitment)
  • Each credential has unique commitment, producing a unique nullifier
  • Same credential always produces same nullifier
  • Verifier maintains nullifier database per relying party
  • Second proof with same nullifier is rejected

Security:

  • Nullifier deterministically derived from commitment
  • Without commitment randomness, adversary cannot link nullifier to user
  • Designed to detect credential sharing (same credential produces same nullifier)

Privacy Implication:

  • Same credential produces same nullifier at same relying party
  • Different relying parties maintain separate nullifier databases
  • Cross-site tracking is designed to be prevented (nullifiers not shared between RPs)

Layer 2: Session-Level Nonce Tracking

replay_tag = base64url(origin_hash || ":" || nonce)
  1. Each verification challenge has unique nonce
  2. Verifier tracks nonce usage with 5-minute replay window (nonce TTL = 300s)
  3. Designed to prevent duplicate redemption of same challenge

Security:

  • Nonces generated using OsRng (cryptographically secure)
  • Challenge expiration limits replay window
  • Constant-time comparison is designed to reduce timing attack risk

Combined Protection:

  • Session layer is designed to prevent immediate replay (same challenge reused)
  • Credential layer is designed to prevent long-term replay (credential reused after challenge expires)

5.4 Unlinkability Across Sites

Mechanism: Origin binding + random session identifiers

Security Properties:

  1. Proofs Bound to Origin:
    rp_hash = Blake2s-256(rp_challenge) where rp_challenge = SHA-256(origin || nonce || "zerokp.challenge.v1")
  • Proof for example.com invalid for other-site.com
  • Origin included in public inputs, verified in circuit
  1. Random Session IDs:
  • Each verification uses random challenge_id
  • No persistent user identifiers transmitted
  • Session IDs unlinkable across verifications
  1. Nullifier Database Separation:
  • Each relying party maintains separate nullifier database
  • Nullifiers not shared between relying parties
  • Designed to prevent correlation of nullifier_A at RP₁ with nullifier_A at RP₂

Unlinkability Design:

An adversary controlling multiple relying parties (RP₁, RP₂,…, RPₙ) who sees proofs from the same user is expected to be unable to determine they came from the same user, except:

  • If user reuses same credential at same RP (detected via nullifier)
  • If user’s IP address is tracked (out of scope for cryptographic protocol)

Comparison to Traditional Systems:

PropertyTraditional IDProvii
Cross-site trackingEmail/username sharedNo shared identifiers
Proof reuseCredentials reusableNullifier reduces reuse risk
Age disclosureExact DOB revealedOnly threshold (yes/no)
LinkabilityHigh (same ID everywhere)Low (unlinkable sessions)

5.5 Commitment Security

Hiding Property: Information-theoretic hiding under the Pedersen construction on JubJub

Analysis:

Pedersen commitments on JubJub provide:

  • Perfectly Hiding. Information-theoretically, determining dob_days from C without randomness is not feasible
  • Computationally Binding. Finding collision (dob₁, r₁) ≠ (dob₂, r₂) with same commitment requires discrete log solution

Randomness Requirements:

The system uses exactly 128 bits (16 bytes) of commitment randomness, enforced by the circuit:

  • Provides 2^128 possible commitments for any given date of birth
  • Matches the 128-bit security level of the underlying BLS12-381 and JubJub curves
  • Generated using OsRng (OS-level cryptographic RNG) on the wallet device

Attack Resistance:

Scenario 1: Adversary tries to guess date of birth from commitment

  • Without randomness: Must try all possible (dob, r) pairs
  • Search space: 50,000 possible dates x 2^128 randomness values
  • Computationally infeasible

Scenario 2: Adversary tries to find commitment collision

  • Requires solving discrete log on JubJub
  • ~2^128 group operations
  • Computationally infeasible with current technology

Commitment Verification in Circuit:

The circuit verifies commitment consistency:

commitment_from_witness = PedersenHash(dob_days || r_bits)
assert(commitment_from_witness == public_commitment)

This is designed to ensure the prover cannot use different dates of birth than what was committed during issuance.

5.6 Signature Security

Custom RedJubjub Security:

Our RedJubjub variant is designed to provide:

  • Unforgeability under chosen-message attacks (EU-CMA)
  • Non-malleability (challenge includes R, VK, message)
  • Deterministic nonces (designed to reduce nonce reuse vulnerabilities)

Security Reduction:

Signature security reduces to discrete logarithm hardness on JubJub:

  • Breaking unforgeability would require solving discrete log
  • Breaking non-malleability would require finding Blake2s collisions (computationally infeasible)

Deterministic Nonce Benefits:

Traditional Schnorr signatures using random nonces risk catastrophic failure if nonces are:

  • Reused (leaks signing key)
  • Biased (statistical attacks possible)
  • Generated from weak RNG

Our deterministic nonce derivation:

nonce = Blake2s("ProviiRJ/nonce" || sk || msg_hash)

Is designed to address these risks:

  • Same message always produces same nonce
  • No randomness source required during signing
  • Signing becomes deterministic and reproducible
  • Reduced side-channel exposure (no secret random generation)

⚠️ Critical Limitation:

While the construction follows sound cryptographic principles (deterministic Schnorr signatures are well-studied), our custom variant has NOT undergone formal third-party cryptographic audit. This is the highest-priority security gap.

Mitigation: Extensive property-based testing, fuzzing (25 targets), and self-validation against test vectors. However, formal audit is required before high-stakes production deployment.


6. Implementation Security

6.1 Randomness Sources

Native Platforms (Linux, macOS, Windows):

use rand::rngs::OsRng;
use rand::RngCore;

let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);

Randomness Sources by Platform:

  • Linux. getrandom() syscall, falling back to /dev/urandom
  • macOS. SecRandomCopyBytes(), drawing from the system entropy pool
  • Windows. BCryptGenRandom(), using CNG (Cryptography Next Generation)

All sources are cryptographically secure, continuously seeded from hardware entropy.

WebAssembly Platforms (Browser, Node.js):

#[cfg(target_arch = "wasm32")]
{
    // Uses Web Crypto API via getrandom crate
    getrandom::getrandom(&mut bytes)
        .expect("failed to generate random nonce");
}

WebAssembly Sources:

  • Browser. crypto.getRandomValues() (Web Crypto API standard)
  • Node.js. crypto.randomBytes() (Node.js crypto module)

Both provide cryptographically secure randomness per W3C Web Crypto API specification.

Entropy Validation:

All generated nonces undergo entropy quality checks:

pub fn has_sufficient_entropy(nonce: &[u8; 32]) -> bool {
    let mut seen = [false; 256];
    let mut unique = 0u8;

    for &b in nonce {
        if !seen[b as usize] {
            seen[b as usize] = true;
            unique += 1;
        }
    }

    unique >= 8  // At least 8 unique bytes required
}

Designed to reduce risk of weak RNG implementations by requiring minimum byte diversity.

6.2 Constant-Time Operations

Where Applicable:

  1. PKCE Verification. Constant-time comparison of code_verifier is designed to reduce timing attack risk
  2. Proof Verification. Groth16 verification time independent of private witness
  3. Nullifier Lookup. Database queries use constant-time comparison for sensitive comparisons

Implementation Example (PKCE validation):

fn validate_pkce(verifier: &str, challenge: &str) -> bool {
    let computed = sha256_base64url(verifier);
    constant_time_eq(&computed, challenge)
}

fn constant_time_eq(a: &str, b: &str) -> bool {
    if a.len() != b.len() {
        return false;
    }

    let mut result = 0u8;
    for (x, y) in a.bytes().zip(b.bytes()) {
        result |= x ^ y;
    }

    result == 0
}

Limitations:

Not all operations are constant-time:

  • Commitment generation varies with input size
  • Signature generation includes variable-time scalar multiplication (acceptable for signing operations)
  • Circuit synthesis time varies with witness complexity

These variable-time operations do not leak sensitive information in our threat model (proof verification is public, signing keys are protected).

6.3 Memory Safety

Rust Memory Safety:

The entire cryptographic implementation is written in Rust, providing:

  • Memory safety. No buffer overflows, use-after-free, or dangling pointers
  • Thread safety. Data races prevented by compiler
  • Type safety. Strong typing reduces type confusion attack risk

Unsafe Code Policy:

# Cargo.toml configuration (5 of 9 crates)
[lints.rust]
unsafe_code = "forbid"

5 of 9 cryptographic crates enforce #![forbid(unsafe_code)]. The exception is crypto-sig-redjubjub, which contains one documented unsafe block for the Zeroize implementation on SigningKey. This is necessary because jubjub::Fr (the JubJub scalar type) does not implement Zeroize, and the signing key scalar is secret material that must be securely erased on drop. The safety invariant is documented inline: the type is a 32-byte value with no padding, exclusive mutable access is held, and volatile writes are used to reduce compiler optimisation risk. All other crates in the workspace use #![forbid(unsafe_code)].

Sensitive Data Handling:

use zeroize::Zeroize;

pub struct SigningKey {
    scalar: JubjubScalar,
}

impl Drop for SigningKey {
    fn drop(&mut self) {
        self.scalar.zeroize();
    }
}

The zeroize crate is used to securely erase sensitive data (signing keys, commitment randomness) from memory when dropped.

Dependency Security:

All cryptographic dependencies are:

  • Well-audited. bellman, bls12_381, jubjub from Zcash platform
  • Actively maintained. Regular security updates
  • Version-pinned. Exact version dependencies designed to reduce supply chain attack risk

7. Trusted Setup

7.1 Parameter Generation

Groth16 requires a trusted setup to generate proving and verifying keys. This process generates random “toxic waste” that must be destroyed.

Parameter Generation Process:

// 1. Define circuit shape (no witness)
let param_gen_circuit = AgeCircuit {
    public: public_shape,
    witness: None,  // No witness during parameter generation
};

// 2. Generate random parameters using cryptographically secure RNG
let mut rng = OsRng;
let params: Parameters<Bls12> = generate_random_parameters::<Bls12, _, _>(
    param_gen_circuit,
    &mut rng
)?;

// 3. Extract proving key and verifying key
let proving_key = params.pk;
let verifying_key = params.vk;

// 4. Toxic waste (randomness used in setup) is discarded
// OsRng randomness never persisted

Security Properties:

  • Randomness. Generated using OsRng (OS-level entropy)
  • Circuit-Specific. Parameters valid only for this specific constraint system
  • Toxic Waste. Ephemeral randomness, never written to disk

Toxic Waste Handling:

The trusted setup generates secret randomness (α, β, γ, δ, τ) that, if retained, could enable creation of fake proofs. The current implementation:

  1. Generates randomness in memory using OsRng
  2. Creates parameters
  3. Randomness goes out of scope and is zeroised
  4. No persistence of toxic waste to disk

Limitation: Single-party setup means trust in the parameter generator. If the generator retains toxic waste, they could create fake proofs.

7.2 Parameter Fingerprinting

To reduce risk of parameter tampering and ensure prover/verifier use matching parameters:

Verifying Key Fingerprint:

pub fn vk_fingerprint(vk: &VerifyingKey<Bls12>) -> [u8; 32] {
    let mut bytes = Vec::new();
    vk.write(&mut bytes).expect("Failed to serialize verifying key");

    let mut hasher = Blake2s256::new();
    hasher.update(&bytes);
    hasher.finalize().into()
}

Verifying Key ID (with domain separation):

pub fn compute_vk_id(vk: &VerifyingKey<Bls12>) -> u32 {
    let mut bytes = Vec::new();
    vk.write(&mut bytes).expect("Failed to serialize verifying key");

    let mut hasher = Blake2s256::new();
    hasher.update(b"zerokp.vk.id.v1");  // Domain separator
    hasher.update(&bytes);
    let result = hasher.finalize();

    u32::from_le_bytes([result[0], result[1], result[2], result[3]])
}

Circuit Constants Hash:

pub fn compute_circuit_constants_hash() -> String {
    let mut hasher = Blake2s256::new();
    hasher.update(b"AgeCircuitV2");
    hasher.update(DOMAIN_SEPARATORS);
    hasher.update(GENERATOR_POINTS);
    hex::encode(hasher.finalize())
}

Parameter Manifest (generated during setup):

{
  "vk_id": 0x12345678,
  "vk_fingerprint_blake2s": "abc123...",
  "circuit_constants_hash": "def456...",
  "pk_filename": "age_pk.12345678.bin",
  "pk_size_bytes": 45678901,
  "pk_blake2s_hash": "789abc...",
  "vk_filename": "vk.12345678.bin",
  "vk_size_bytes": 1234,
  "shape": {
    "kid_bytes": 14,
    "schema_bytes": 12,
    "constraints": 99084,
    "public_inputs": 8
  },
  "generated_at": "2025-01-15T12:34:56Z",
  "bellman_version": "0.14.0"
}

Validation During Initialisation:

// Verifier checks VK fingerprint matches expected value
let vk = load_verifying_key()?;
let fingerprint = vk_fingerprint(&vk);

assert_eq!(
    fingerprint,
    EXPECTED_VK_FINGERPRINT,
    "Verifying key fingerprint mismatch - parameter tampering detected"
);

This is designed to prevent:

  • Using mismatched proving/verifying keys
  • Parameter substitution attacks
  • Accidental version mismatches

7.3 Multi-Party Ceremony Considerations

For higher-assurance deployments, a multi-party trusted setup ceremony would distribute trust so that security holds if at least one participant is honest. This is standard practice for high-value zk-SNARK deployments (e.g., Zcash Powers of Tau with 200+ participants).

Current Approach: Single-party parameter generation with cryptographic fingerprinting. Parameters are generated using OsRng, toxic waste is zeroised immediately, and the resulting proving/verifying keys are fingerprinted for integrity verification.

Why This Is Acceptable:

  • Age verification is a lower-stakes use case than cryptocurrency
  • Parameter integrity is verified via cryptographic fingerprints
  • The risk is theoretical (requires parameter generator to retain toxic waste AND use it maliciously)
  • A multi-party ceremony may be considered if the system is adopted for higher-value use cases

8. Performance Analysis

8.1 Proof Generation Performance

Note: The performance figures below are approximate and should be verified against the current 99,084-constraint v7 circuit. Actual times may be up to 60% longer than figures originally benchmarked against earlier circuit versions.

Desktop/Laptop (x86-64, 8+ threads):

  • Time. 2-5 seconds
  • Constraints. 99,084
  • Memory. ~500MB peak
  • Parallelization. Multi-threaded FFT for polynomial operations

Mobile Devices (ARM, 4-8 threads):

  • Time. 5-15 seconds
  • Constraints. 99,084
  • Memory. ~500MB peak
  • Limitation. Fewer cores, lower clock speeds

WebAssembly (Browser):

  • Time. 10-20 seconds (single-threaded)
  • Constraints. 99,084
  • Memory. ~500MB peak
  • Limitation. No multi-threading (web workers add complexity)

Performance Factors:

  1. Circuit Size: 99,084 constraints (v7 circuit)
  2. FFT Operations: Dominant cost (polynomial arithmetic)
  3. Multi-Scalar Multiplication: ~200ms on desktop, ~500ms on mobile
  4. Memory Access Patterns: Cache-friendly or thrashing determines variance

8.2 Proof Verification Performance

All Platforms:

  • Time. 5-20ms (constant time)
  • Operations. Single pairing check (e(A, B) = e(α, β) · e(L, γ) · e(C, δ))
  • Parallelization. Not applicable (verification is already fast)

Verification is Constant-Time:

  • Independent of circuit size
  • Independent of private witness complexity
  • Depends only on pairing operations (fixed cost)

Comparison:

OperationDesktopMobileServer
Proof Generation2-5s5-15s1-3s
Proof Verification5-20ms5-20ms5-20ms
Commitment Generation<1ms<1ms<1ms
Signature Generation<1ms<1ms<1ms
Signature Verification<1ms<1ms<1ms

8.3 Optimisation Techniques

Circuit Optimisation (99,084 constraints in v7, down from 117k initial):

  1. RP Hash Off-Circuit: Compute rp_hash = Blake2s-256(rp_challenge) where rp_challenge = SHA-256(origin || nonce || DST) outside circuit, pass as public input
  • Savings: ~20,000 constraints (avoids in-circuit Blake2s)
  • Trade-off: Trust that prover honestly computed hash (acceptable, rp_hash is a public input verified by the verifier)
  1. Direct VK Bytes: Use raw issuer verifying key bytes instead of hash
  • Savings: ~10,000 constraints
  • Trade-off: Larger public inputs (32 bytes vs 32 bytes hash, no practical difference)
  1. Pedersen Nullifier: Replace Blake2s with Pedersen hash for nullifier derivation
  • Savings: ~25,000 constraints
  • Trade-off: Custom nullifier construction (requires audit)

Memory Optimisation:

  • Streaming FFT (reduce peak memory from 1GB to 500MB)
  • Parameter file compression (proving key from 60MB to 45MB)

Future Optimisations:

  • GPU Acceleration. Offload FFT/MSM to GPU (10-50x speedup)
  • Smaller Circuits. Further constraint reduction (target: <40k)
  • PLONK Migration. More flexible proof system (removes trusted setup requirement, faster proving)

9. Comparison with Alternatives

9.1 vs. Attribute-Based Credentials (ABCs)

PropertyGroth16 ZK (Provii)ABCs (e.g., Idemix)
Proof Size192 bytes (constant)500-2000 bytes (variable)
Verification Time5-20ms (constant)50-200ms (variable)
Proving Time2-15s1-5s
UnlinkabilityPerfect (via nullifiers)Perfect (via randomization)
Selective DisclosureLimited (binary threshold)Flexible (any attribute subset)
Trusted SetupRequiredNot required
Mobile SupportGood (WASM support)Limited (complex crypto)
Standard LibrariesMature (bellman)Limited (Idemix patent issues)

When to Choose ABCs:

  • Need selective disclosure of multiple attributes
  • Want to avoid trusted setup
  • Privacy more important than proof size

When to Choose Groth16 (Provii):

  • Need smallest proof size (bandwidth-constrained)
  • Want fastest verification (high-throughput verifiers)
  • Binary threshold sufficient (age yes/no)

9.2 vs. Blind Signatures

PropertyGroth16 ZK (Provii)Blind Signatures
Privacy from IssuerModerate (issuer sees DOB once for commitment, then discards)None (issuer sees attributes)
Privacy from VerifierHigh (zero knowledge)Moderate (reveals token)
UnlinkabilityPerfect (cross-site)Limited (same token linkable)
Threshold ProofsNativeRequires additional crypto
Credential RevocationVia nullifiersVia revocation lists
ComplexityHigh (ZK circuits)Low (simple signatures)

When to Choose Blind Signatures:

  • Simpler implementation requirements
  • Issuer privacy not critical
  • Performance more important than unlinkability

When to Choose Groth16 (Provii):

  • Strong unlinkability required
  • Threshold proofs needed (age ≥ X)
  • Privacy from issuer important

9.3 vs. Traditional Identity Verification

PropertyTraditional IDProvii ZK
Data RevealedFull PII (name, DOB, address)Only threshold result (yes/no)
ReusabilitySingle-use (privacy risk)Multi-use (unlinkable)
Cross-Site TrackingEasy (same ID everywhere)Designed to prevent (unlinkable sessions)
Verification Speed30-60s (human review)5-20ms (cryptographic)
Cost per Verification$0.50-$2.00 (third-party)$0.001-$0.01 (computational)
ComplianceGDPR data minimization issuesPrivacy-by-design compliant
User ExperienceIntrusive (document upload)(QR code scan)

Traditional ID Advantages:

  • Universally understood
  • No special infrastructure required
  • Works offline (document inspection)

Provii ZK Advantages:

  • Data minimization (GDPR compliant)
  • Unlinkability (privacy-preserving)
  • Reusability (better UX)
  • Lower marginal cost (no per-verification fees)

10. Known Limitations and Future Work

10.1 Critical Gap: No Formal Cryptographic Audit

Status: Custom RedJubjub signature scheme and age verification circuit have NOT undergone formal third-party cryptographic audit.

Risk: Potential undiscovered vulnerabilities in custom cryptographic constructions.

Components Requiring Audit:

  1. Custom RedJubjub Signature Scheme (Highest Priority)
  • Signature generation algorithm
  • Deterministic nonce derivation
  • Challenge computation
  • Scalar field reduction
  • Circuit-side verification compatibility
  1. Age Verification Circuit (High Priority)
  • Constraint system completeness
  • Soundness of age check
  • Commitment verification logic
  • Signature verification gadget
  • Public input assembly
  1. Pedersen Commitment Implementation (Medium Priority)
  • Host-circuit consistency
  • Randomness generation
  • Nullifier derivation

Recommended Audit Firms:

  • Trail of Bits. Expertise in ZK proofs, blockchain cryptography (Zcash, Filecoin audits)
  • NCC Group. Cryptographic implementations, secure systems
  • Kudelski Security. Cryptographic libraries, ZK systems
  • Least Authority. Privacy tech, zero knowledge systems (Zcash audits)

Timeline: Will pursue when commercially viable (estimated $50,000-$150,000 for engagement)

Interim Mitigations:

  • Extensive testing (200+ unit tests, 25 fuzz targets)
  • Property-based testing (hiding/binding properties)
  • Internal code review by experienced cryptographic engineers
  • Limited deployment to low-stakes use cases pending audit

10.2 Trusted Setup

Current Status: Single-party parameter generation with cryptographic fingerprinting

Limitation: Requires trust in parameter generator to have destroyed toxic waste

Mitigation: Parameters are fingerprinted and verified at startup. Toxic waste is zeroised immediately after generation using the zeroize crate. A multi-party ceremony may be considered for higher-assurance use cases in the future.

Participant Selection:

  • Independent security researchers
  • Community members
  • Partner organisations
  • Geographic diversity

Budget: $20,000-$50,000 (ceremony coordination, participant incentives, verification tooling)

10.3 Quantum Resistance

Current Status: NOT quantum-resistant (relies on discrete log hardness)

Threat Timeline: Cryptographically relevant quantum computers estimated 10-30 years away

Vulnerability:

  • Shor’s algorithm breaks discrete log in polynomial time
  • Affects: RedJubjub signatures, Pedersen commitments, Groth16 proofs
  • Consequence: Quantum adversary could forge signatures, create fake proofs

Future Work:

  1. Monitor: Track quantum computing developments, NIST post-quantum standards
  2. Research: Investigate post-quantum ZK proof systems (STARKs, lattice-based SNARKs)
  3. Prepare: Design migration strategy to post-quantum cryptography

Post-Quantum ZK Candidates:

  • STARKs. Transparent setup (no trusted setup), quantum-resistant, but larger proofs (100KB+)
  • Lattice-based SNARKs. Emerging research, smaller proofs than STARKs
  • Hash-based Schemes. Quantum-resistant, but limited to signature schemes (not ZK)

Migration Strategy (when quantum threat becomes imminent):

  1. Implement post-quantum signature scheme for credential issuance
  2. Migrate ZK proofs to post-quantum construction (likely STARKs)
  3. Gradual credential rotation to new scheme
  4. Maintain backward compatibility during transition

10.4 Mobile Performance

Challenge: Proof generation on mobile devices takes 5-15 seconds, affecting user experience

Causes:

  • Limited CPU cores (4-8 vs 8-16 on desktop)
  • Lower clock speeds (battery conservation)
  • WebAssembly single-threaded (no web worker parallelization)
  • Memory constraints (proof generation uses ~500MB peak)

Future Optimisations:

  1. GPU Acceleration (iOS Metal, Android Vulkan)
  • Offload FFT/MSM to GPU
  • Expected speedup: 10-50x
  • Challenge: GPU API complexity, compatibility
  1. Smaller Circuits (target: <40,000 constraints)
  • Further constraint reduction through optimisations
  • Trade-off: May limit flexibility for future features
  1. Progressive Proof Generation
  • Start proof generation during QR code scan
  • Display progress UI to user
  • Perception: Feels faster even if same time
  1. Caching and Precomputation
  • Cache proving key in memory (45MB)
  • Precompute FFT domain elements
  • Expected improvement: 20-30% faster repeat proofs

Target Performance: <3 seconds on modern mobile devices (expected with GPU acceleration + circuit optimisation)

10.5 Revocation Mechanisms

Current Status: Limited revocation support (relies on issuer key rotation)

Limitations:

  • No per-credential revocation (only per-issuer via key rotation)
  • Revocation requires all users to obtain new credentials
  • No real time revocation checks

Future Work: Cryptographic Accumulators

Accumulator-Based Revocation:

1. Issuer maintains cryptographic accumulator of valid credentials
2. Proof includes witness that credential is in accumulator
3. Revocation: Remove credential from accumulator
4. Next proof fails (credential no longer in accumulator)

Benefits:

  • Per-credential revocation (fine-grained)
  • No need to reissue all credentials
  • Privacy-preserving (revoked credential not linked to user)

Challenges:

  • Accumulator updates require re-computation
  • Witnesses need updating (communication overhead)
  • Circuit constraints increase (~10,000-20,000 additional)

Timeline: 2026+ (research maturity required)


11. Security Assumptions

The security of Provii’s age verification system relies on the following assumptions:

11.1 Cryptographic Hardness Assumptions

  1. Discrete Logarithm Problem (DLP) on JubJub:
  • Assumption. Computing x from P = x·G is computationally infeasible
  • Security Level. ~128 bits
  • Affects. Pedersen commitment binding, RedJubjub signature unforgeability
  1. Discrete Logarithm Problem (DLP) on BLS12-381:
  • Assumption. DLP hard in both G1 and G2 groups
  • Security Level. ~128 bits
  • Affects. Groth16 soundness
  1. q-Strong Diffie-Hellman (q-SDH) on BLS12-381:
  • Assumption. Computing (c, 1/(x+c)) given (1, x, x²,..., x^q) is infeasible
  • Security Level. ~128 bits
  • Affects. Groth16 soundness (primary assumption)
  1. Collision Resistance of Blake2s-256:
  • Assumption. Finding m₁ ≠ m₂ with Blake2s(m₁) = Blake2s(m₂) is infeasible
  • Security Level. ~128 bits
  • Affects. Challenge binding, nonce derivation, domain separation

11.2 Trusted Setup Assumptions

  1. Parameter Generation Honesty:
  • Assumption. Parameter generator destroyed toxic waste
  • Current Status. Single-party setup (trust required in generator)
  • Mitigation. Cryptographic fingerprinting, immediate zeroisation of toxic waste
  1. Parameter Integrity:
  • Assumption. Proving/verifying keys not tampered with
  • Mitigation. Cryptographic fingerprinting, VK ID verification

11.3 Issuer Trust Assumptions

  1. Identity Verification:
  • Assumption. Issuers correctly verify user identity and date of birth before issuing credentials
  • Risk. Malicious issuer could issue credentials with false DOB
  • Mitigation. Issuer reputation, regulatory oversight, audits
  1. Key Management:
  • Assumption. Issuers protect signing keys from compromise
  • Risk. Compromised issuer key enables credential forgery
  • Mitigation. HSM storage (recommended), key rotation, revocation
  1. Issuer Non-Collusion:
  • Assumption. Issuers do not collude to track users across sites
  • Risk. Issuers sharing user metadata could enable tracking
  • Mitigation. Minimal metadata in credentials, unlinkability of proofs

11.4 User Device Assumptions

  1. Wallet Security:
  • Assumption. User’s device is not compromised by malware
  • Risk. Malware could extract credentials, signing keys, randomness
  • Mitigation. OS-level security, secure enclaves (future), user education
  1. Private Key Protection:
  • Assumption. Users protect wallet access (passwords, biometrics)
  • Risk. Unauthorized access could enable credential theft
  • Mitigation. Strong authentication, device encryption

11.5 Verifier Assumptions

  1. Nullifier Database Integrity:
  • Assumption. Verifiers maintain accurate nullifier databases
  • Risk. Corrupted nullifier DB could allow replay attacks
  • Mitigation. Database consistency checks, backups, monitoring
  1. Challenge Generation Randomness:
  • Assumption. Verifiers use cryptographically secure RNG for nonces
  • Risk. Weak RNG could enable challenge prediction
  • Mitigation. OsRng/getrandom enforcement, entropy validation

11.6 Network Assumptions

  1. TLS/HTTPS for Communication:
  • Assumption. Communications protected by TLS (out of scope for cryptographic protocol)
  • Risk. Man-in-the-middle could intercept proofs
  • Mitigation. HTTPS enforcement via Cloudflare-managed TLS 1.3 (supplier-held, via Cloudflare), App Transport Security (iOS), network security config (Android)
  1. DNS Integrity:
  • Assumption. DNS resolution not compromised
  • Risk. DNS hijacking could redirect to malicious verifier
  • Mitigation. DNSSEC (recommended), origin binding in proofs

12. Conclusion

Provii’s cryptographic architecture demonstrates that privacy-preserving age verification is not only theoretically possible but practically deployable. By combining Groth16 zk-SNARKs with Pedersen commitments and custom signature schemes, Maelstrom AI has designed a system with strong privacy properties. Users prove age thresholds without revealing dates of birth, verifications are designed to be unlinkable across sites, and credentials resist forgery.

Key Achievements

Cryptographic Design:

  • Age verification system using Groth16 zk-SNARKs applied to a privacy-preserving credential context
  • Custom RedJubjub signature scheme enabling efficient in-circuit verification
  • Nullifier-based replay prevention designed to preserve unlinkability

Performance:

  • 192-byte proofs (smallest in category)
  • 5-20ms verification (constant-time, high-throughput)
  • 2-15s proving on consumer devices (acceptable UX)

Privacy Properties:

  • Zero knowledge age threshold proofs
  • Designed for unlinkability across relying parties
  • Minimal data collection (GDPR/privacy-by-design compliant)

Security Properties:

  • Computational soundness under q-SDH assumption
  • Replay prevention via two-layer nullifier system
  • Cryptographic binding to trusted issuers and challenges

Critical Limitations Acknowledged

Maelstrom AI transparently acknowledges the following limitations:

  1. No Formal Cryptographic Audit (CRITICAL)
  • Custom RedJubjub implementation requires expert review
  • Age verification circuit needs formal security analysis
  • Will pursue when commercially viable
  1. Single-Party Trusted Setup (HIGH)
  • Current parameters require trust in generator
  • Acceptable for age verification use case; multi-party ceremony may be considered for higher-assurance deployments
  1. Not Quantum-Resistant (MEDIUM)
  • Vulnerable to future quantum computers
  • Monitoring NIST PQC standards; no implementation timeline
  1. Mobile Performance (MEDIUM)
  • 5-15s proving time on mobile devices
  • GPU acceleration and circuit optimisation in progress

Call for External Cryptographic Review

Maelstrom AI recognises that custom cryptographic constructions, no matter how carefully designed and tested, require independent third-party validation. We are actively seeking engagement with reputable cryptographic audit firms to conduct:

  1. Formal verification of the security of our custom RedJubjub signature scheme
  2. Analysis of the age verification circuit for completeness and soundness
  3. Validation of host-circuit consistency in commitment and nullifier computations
  4. Assessment of overall system architecture for cryptographic vulnerabilities

Contact: For audit firms or security researchers interested in reviewing Provii’s cryptographic implementation, please contact security@maelstrom.au.

Future Directions

  1. Formal cryptographic audit (external cryptographic review, will pursue when commercially viable)
  2. GPU acceleration for mobile proving
  3. Cryptographic accumulator-based revocation
  4. Migration path to post-quantum cryptography (monitoring NIST PQC standards)

Final Remarks

Provii represents a significant step toward privacy-preserving digital identity. By combining zero knowledge proofs with practical engineering, Maelstrom AI demonstrates that regulatory compliance, user privacy, and good user experience are not mutually exclusive. The system is designed with transparency, acknowledging limitations while providing strong cryptographic foundations where they matter most.

This whitepaper serves both as technical documentation for implementers and as an invitation to the cryptographic community to scrutinise, critique, and ultimately strengthen the foundations of privacy-preserving age verification.


References

Academic Papers

  1. Groth, J. (2016). “On the Size of Pairing-based Non-interactive Arguments.” Proceedings of Eurocrypt 2016. https://eprint.iacr.org/2016/260

  2. Ben-Sasson, E., Chiesa, A., Tromer, E., & Virza, M. (2014). “Succinct Non-Interactive Zero Knowledge for a von Neumann Architecture.” Proceedings of USENIX Security 2014.

  3. Pedersen, T. P. (1991). “Non-Interactive and Information-Theoretic Secure Verifiable Secret Sharing.” Proceedings of CRYPTO 1991.

  4. Schnorr, C. P. (1991). “Efficient Signature Generation by Smart Cards.” Journal of Cryptology, 4(3), 161-174.

  5. Sasson, E. B., Chiesa, A., Garman, C., Green, M., Miers, I., Tromer, E., & Virza, M. (2014). “Zerocash: Decentralized Anonymous Payments from Bitcoin.” IEEE Symposium on Security and Privacy 2014.

Standards and Specifications

  1. ISO/IEC 27566-1:2023. “Information security, cybersecurity and privacy protection. Framework for age assurance.”

  2. RFC 7693 (2015). “The BLAKE2 Cryptographic Hash and Message Authentication Code (MAC).” Internet Engineering Task Force (IETF).

  3. RFC 6979 (2013). “Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA).” IETF.

  4. NIST FIPS 180-4 (2015). “Secure Hash Standard (SHS).” National Institute of Standards and Technology.

Cryptographic Libraries and Implementations

  1. Zcash bellman library. https://github.com/zkcrypto/bellman

  2. Zcash jubjub library. https://github.com/zkcrypto/jubjub

  3. BLS12-381 library. https://github.com/zkcrypto/bls12_381

  4. Zcash sapling-crypto. https://github.com/zcash-hackworks/sapling-crypto

  1. Zcash Protocol Specification. “Sapling Protocol Specification.” https://zips.z.cash/protocol/protocol.pdf

  2. Filecoin Proof-of-Replication. “PoRep and PoSt specifications using zk-SNARKs.”

  3. Semaphore Protocol. “Zero knowledge signaling protocol.” https://semaphore.appliedzkp.org/

Privacy and Compliance

  1. GDPR Article 5. “Principles relating to processing of personal data.”

  2. COPPA 16 CFR Part 312. “Children’s Online Privacy Protection Rule.”

  3. UK Age Appropriate Design Code. “ICO Code of Practice for Online Services.” https://ico.org.uk/for-organisations/guide-to-data-protection/ico-codes-of-practice/age-appropriate-design-a-code-of-practice-for-online-services/

Audit Firms and Security Research

  1. Trail of Bits. https://www.trailofbits.com/

  2. NCC Group. https://www.nccgroup.com/

  3. Kudelski Security. https://www.kudelskisecurity.com/

  4. Least Authority. https://leastauthority.com/


Appendix A: Circuit Constraints Breakdown

Total Constraints: 99,084

The v7 circuit has 9 synthesis steps (Steps 0-8). Signature verification is the dominant cost.

Constraint Categories:

ComponentConstraintsPercentageNotes
Signature Verification~56,00057%RedJubjub in-circuit verification (R, s, challenge, curve arithmetic)
Commitment Verification~19,00019%Pedersen hash computation (160-bit input: 32 dob + 128 r_bits)
Age Comparison~8,0008%Direction-dependent conditional swap + unsigned range check
Nullifier Derivation~12,00012%Pedersen nullifier = Hash(DST || C) with MerkleTree(0)
Public Input Packing~4,0844%Bit-packing 8 field elements (including direction)

Signature Verification Breakdown:

  • Point addition operations: ~24,000 constraints
  • Scalar multiplication: ~19,000 constraints
  • Challenge hash computation (Blake2s in-circuit): ~8,000 constraints
  • Signature equation check: ~5,000 constraints

Commitment Verification Breakdown:

  • DOB bit decomposition: ~500 constraints
  • Randomness handling: ~500 constraints
  • Pedersen hash (160 bits input): ~18,000 constraints

Optimisation History:

VersionConstraintsOptimisations
v1.0~117,000Initial implementation
v1.5~92,000RP hash off-circuit (-25k)
v2.0~82,000Direct VK bytes (-10k)
v2.5~62,000Pedersen nullifier (-20k)
v799,084Unified direction circuit (added direction public input, conditional swap mux, i32 bias support). Constraint increase from v2.5 reflects additional features, not regression.

Appendix B: Test Coverage

Unit Tests: 200+ tests across all cryptographic crates

Test Categories:

  1. Signature Tests (40+ tests):
  • Basic signature generation/verification
  • Tamper detection (modified message, wrong key)
  • RP challenge binding
  • Deterministic nonce verification
  • Serialization round-trip
  • Edge cases (zero values, max values)
  1. Commitment Tests (30+ tests):
  • Pedersen commitment computation
  • Commitment hiding property
  • Commitment binding property
  • Nullifier derivation
  • Randomness generation
  1. Circuit Tests (50+ tests):
  • Constraint satisfiability
  • Proof generation/verification
  • Public input sensitivity
  • Age threshold edge cases
  • Credential expiration
  1. Protocol Tests (30+ tests):
  • Challenge generation
  • Nonce validation
  • Replay tag computation
  • PKCE validation
  1. Integration Tests (20+ tests):
  • End-to-end issuance flow
  • End-to-end verification flow
  • Multi-issuer scenarios
  • Key rotation scenarios

Property-Based Tests (Proptest)

Commitment Properties:

proptest! {
    // Hiding property: different randomness → different commitment
    #[test]
    fn prop_commitment_hiding(
        dob_days in 0i32..50000,
        r1 in vec(any::<bool>(), 128),
        r2 in vec(any::<bool>(), 128)
    ) {
        prop_assume!(r1 != r2);
        let c1 = pedersen_commit_dob(dob_days, &r1);
        let c2 = pedersen_commit_dob(dob_days, &r2);
        prop_assert_ne!(c1, c2);
    }

    // Binding property: different DOB → different commitment
    #[test]
    fn prop_commitment_binding(
        dob1 in 0i32..50000,
        dob2 in 0i32..50000,
        r in vec(any::<bool>(), 128)
    ) {
        prop_assume!(dob1 != dob2);
        let c1 = pedersen_commit_dob(dob1, &r);
        let c2 = pedersen_commit_dob(dob2, &r);
        prop_assert_ne!(c1, c2);
    }
}

Fuzzing Targets: 25 targets

Fuzzing Coverage by Module:

  1. crypto-sig-redjubjub (5 targets):
  • fuzz_sig_verify: Proof-of-work verification with arbitrary inputs
  • fuzz_sig_roundtrip: Signature serialization/deserialization
  • fuzz_nonce_derivation: Deterministic nonce generation
  • fuzz_challenge_computation: Challenge hash variations
  • fuzz_keypair_generation: Key generation edge cases
  1. crypto-commit (4 targets):
  • fuzz_commit: Commitment generation with arbitrary DOB/randomness
  • fuzz_nullifier: Nullifier derivation edge cases
  • fuzz_randomness_generation: Randomness quality
  • fuzz_commitment_serialization: Byte encoding/decoding
  1. crypto-verifier (6 targets):
  • fuzz_proof_verify: Proof verification with malformed proofs
  • fuzz_public_inputs_assemble: Public input packing edge cases
  • fuzz_vk_loading: Verifying key deserialization
  • fuzz_proof_deserialization: Proof parsing from arbitrary bytes
  • fuzz_multipack: Bit-packing field element overflow
  • fuzz_vk_fingerprint: Fingerprint computation variations
  1. crypto-protocol (5 targets):
  • fuzz_nonce_operations: Nonce validation, entropy checking
  • fuzz_challenge_generation: RP challenge computation
  • fuzz_replay_tag: Replay tag derivation
  • fuzz_origin_hash: Origin hashing edge cases
  • fuzz_pkce_validation: PKCE code_verifier/code_challenge
  1. crypto-circuit-age (5 targets):
  • fuzz_circuit_synthesis: Circuit synthesis with arbitrary witness
  • fuzz_public_input_packing: Field element packing overflow
  • fuzz_age_comparison: Age threshold boundary conditions
  • fuzz_credential_parsing: Credential message parsing
  • fuzz_proof_generation: Proof generation with invalid witness

Fuzzing Statistics (100M+ executions):

  • Code coverage: 85%+ in cryptographic modules
  • Crashes found: 0 (after hardening)
  • Unique edge cases discovered: 200+
  • Run time: 500+ CPU-hours

Appendix C: Formal Notation

Algebraic Structures

BLS12-381 Pairing-Friendly Curve:

  • $\mathbb{G}_1$: Base field group (order $r$)
  • $\mathbb{G}_2$: Twist field group (order $r$)
  • $\mathbb{G}_T$: Target group (order $r$)
  • $e: \mathbb{G}_1 \times \mathbb{G}_2 \rightarrow \mathbb{G}_T$: Optimal Ate pairing
  • $r$: Prime subgroup order (255 bits)
  • $\mathbb{F}_r$: Scalar field ($\mathbb{Z}/r\mathbb{Z}$)

JubJub Twisted Edwards Curve:

  • Equation: $ax^2 + y^2 = 1 + dx^2y^2$ over $\mathbb{F}_r$ (BLS12-381 scalar field)
  • Base point: $G \in \mathbb{J}$ (JubJub generator)
  • Order: $r$ (prime, same as BLS12-381 subgroup order)

Commitment Scheme

Pedersen Commitment:

$$C = \text{Commit}(m, r) = H_{\text{Ped}}(m | r)$$

where:

  • $m \in {0, 1}^{32}$: Message (date of birth in bits, biased from i32 to u32 via XOR with $\texttt{0x8000_0000}$)
  • $r \in {0, 1}^{128}$: Randomness (commitment blinding factor, exactly 128 bits enforced by circuit)
  • $H_{\text{Ped}}: {0, 1}^* \rightarrow \mathbb{J}$: Pedersen hash function
  • $C \in \mathbb{J}$: Commitment (point on JubJub curve)

Properties:

  • Hiding. $\Pr[C | m] = \Pr[C]$ (information-theoretic)
  • Binding. $\text{Adv}{\text{bind}}(A) \leq \text{Adv}{\text{DLog}}(A’)$ (computational)

Signature Scheme

RedJubjub Signature:

Key Generation: $$\text{sk} \xleftarrow{$} \mathbb{F}_r, \quad \text{vk} = \text{sk} \cdot G$$

Signing: $$\begin{align*} r &= H_{\text{nonce}}(\text{sk} | m) \ R &= r \cdot G \ c &= H_{\text{chal}}(R | \text{vk} | m) \ s &= r + c \cdot \text{sk} \pmod{r} \ \sigma &= (R, s) \end{align*}$$

Verification: $$s \cdot G \stackrel{?}{=} R + c \cdot \text{vk}$$

where:

  • $H_{\text{nonce}}: {0,1}^* \rightarrow \mathbb{F}_r$: BLAKE2s-based nonce derivation
  • $H_{\text{chal}}: {0,1}^* \rightarrow \mathbb{F}_r$: BLAKE2s-based challenge computation
  • $m$: Message hash (32 bytes)

Zero knowledge Proof System

Groth16 Proof:

Setup: $$(pk, vk) \leftarrow \text{Setup}(1^\lambda, C)$$

where:

  • $C$: Constraint system (R1CS)
  • $pk$: Proving key (circuit-specific)
  • $vk$: Verifying key (circuit-specific)

Proving: $$\pi \leftarrow \text{Prove}(pk, x, w)$$

where:

  • $x \in \mathbb{F}_r^n$: Public inputs ($n = 8$ for age circuit: direction, cutoff_days, rp_hash(2), issuer_vk(2), nullifier(2))
  • $w$: Private witness (dob, randomness, signature)
  • $\pi \in \mathbb{G}_1^2 \times \mathbb{G}_2$: Proof (192 bytes)

Verification: $$\text{Verify}(vk, x, \pi) \in {\text{accept}, \text{reject}}$$

Pairing Equation: $$e(A, B) = e(\alpha, \beta) \cdot e\left(\sum_{i=0}^n x_i \cdot IC_i, \gamma\right) \cdot e(C, \delta)$$

where:

  • $\pi = (A, B, C)$: Proof components
  • $(\alpha, \beta, \gamma, \delta)$: Verifying key parameters
  • $IC_0, \ldots, IC_n$: Input commitment points

Soundness: $$\Pr[\text{Verify}(vk, x, \pi) = \text{accept} \land C(x, w) = 0] \leq \text{negl}(\lambda)$$

under q-SDH assumption.

Nullifier Derivation

Nullifier Function: $$N = H_{\text{null}}(\text{DST} | C)$$

where:

  • $\text{DST} = \texttt{“provii.nullifier.pedersen.v1”}$: Domain separation tag
  • $C$: Credential commitment (32 bytes)
  • $H_{\text{null}}: {0, 1}^* \rightarrow \mathbb{J}$: Pedersen hash
  • $N \in \mathbb{J}$: Nullifier (32 bytes, compressed point)

Properties:

  • Uniqueness. Same credential produces same nullifier
  • Unlinkability. Without $C$, cannot link $N$ to user
  • Collision Resistance. $\Pr[H_{\text{null}}(m_1) = H_{\text{null}}(m_2)] \leq \text{negl}(\lambda)$ for $m_1 \neq m_2$

Challenge-Response Protocol

Challenge Generation: $$\begin{align*} n &\xleftarrow{$} {0, 1}^{256} \ \text{rp_challenge} &= H_{\text{SHA-256}}(\text{origin} | n | \text{DST}) \end{align*}$$

Proof Binding: $$\pi \leftarrow \text{Prove}(pk, (d, t, \text{rp_challenge}, \text{vk}_{\text{iss}}, N), w)$$

where $d$ is direction (1 = Over, 0 = Under) and $t$ is cutoff threshold (days).

Verification: $$\text{Verify}(vk, (d, t, \text{rp_challenge}, \text{vk}{\text{iss}}, N), \pi) \land N \notin \mathcal{N}{\text{db}}$$

where $\mathcal{N}_{\text{db}}$ is the nullifier database.


Document Version: 1.1 Classification: Public Last Updated: 2026-02-16 Contact: security@maelstrom.au License: Creative Commons Attribution 4.0 International (CC BY 4.0)


Acknowledgments: This work builds upon foundational research in zero knowledge proofs by the Zcash team, cryptographic primitives from the broader ZK community, and the rigorous security analysis of pairing-based SNARKs. Maelstrom AI thanks the developers of bellman, bls12_381, jubjub, and sapling-crypto for their open source contributions.