Back to blog
·5 min read·BitAtlas Team

Zero-Knowledge Proofs for Private Credential Verification

How to implement ZKPs for verifying credentials without revealing sensitive data. A developer's guide to privacy-preserving authentication systems.

zero-knowledge proofscredentialsverificationprivacyattributescryptographyauthentication

When a user logs into a banking app, they prove their identity. But how much must they reveal? Today's systems leak metadata, timestamps, and often more than necessary. Zero-knowledge proofs (ZKPs) offer a better path: prove you have the credentials without exposing them.

This post walks through the fundamentals, real-world patterns, and implementation considerations for credential verification using ZKPs—essential knowledge for anyone building privacy-forward authentication systems.

Why Zero-Knowledge Proofs Matter for Credentials

Traditional credential verification is binary: either you hand over your password/certificate, or the verifier trusts a central authority. Both leak information. ZKPs introduce a third option: prove that you satisfy a condition (e.g., "my age is over 18") without revealing the underlying data (your exact age or identity).

For enterprises and consumer applications, this unlocks:

  • Selective disclosure: Prove one attribute without exposing others. A music streaming service learns you're 16+ but never your birthdate.
  • Decoupled verification: Prove a credential issued by one party directly to another, without requiring the issuer to participate in the verification flow.
  • Audit trails without privacy leaks: Services can log "verified user with valid credential" without storing sensitive attributes.

The Mathematics (Simplified for Engineers)

ZKPs rely on a prover convincing a verifier of a statement's truth with three properties:

  • Completeness: If the statement is true, an honest prover can always convince the verifier.
  • Soundness: A dishonest prover cannot convince the verifier (except with negligible probability).
  • Zero-knowledge: The verifier learns nothing about the hidden information—only that the statement is true.

A canonical example is the Schnorr proof, used in many credential systems. Suppose a user knows a secret x and wants to prove they know it without revealing x. They can:

  1. Commit to a random value derived from x.
  2. Respond to a challenge from the verifier with a proof of knowledge.
  3. Verifier checks the math without learning x.

For credentials, this scales to complex statements: "this credential is signed by a trusted issuer, has not expired, and the holder's age attribute is >= 18."

Practical Implementation Patterns

1. Credential Issuance

Before a ZKP, the issuer creates a signed credential. A common format is a JWT or CWT (CBOR Web Token) containing:

const credential = {
  issuer: "example.com",
  subject: "user-id-123",
  age: 25,
  country: "US",
  issued_at: 1700000000,
  expires_at: 1800000000,
  signature: "<issuer_signature>"
};

The issuer signs this with their private key. The holder stores it locally.

2. Proof Generation

When proving age >= 18, the holder generates a ZKP locally. This requires:

  • The credential (already held).
  • A proof system library (e.g., libsnark, circom + snarkjs, or Hyperledger Indy).
  • The statement to prove: age >= 18.

Example pseudocode (using a fictional library):

const zkp = await ProveCredential({
  credential,
  statement: "age >= 18",
  revealedAttributes: ["issuer"], // which fields the verifier can see
  nonce: randomNonce() // prevent replay attacks
});
// zkp is now a compact proof, ~256–512 bytes

The proof is mathematically bound to:

  • The credential's content.
  • The specific statement.
  • A nonce (preventing reuse for different transactions).

3. Proof Verification

The verifier (service provider) receives the proof and does not see the credential itself—only the proof and any revealed attributes (like issuer: "example.com"). Verification:

const isValid = await VerifyProof({
  proof: zkp,
  statement: "age >= 18",
  revealedAttributes: { issuer: "example.com" },
  issuerPublicKey: getIssuerPublicKey("example.com"),
  nonce: retrieveStoredNonce() // for replay prevention
});

If valid, the user gains access. The issuer never participates; the verifier never learns the age value.

Design Considerations

Credential Revocation

ZKPs don't inherently handle revocation. If a credential is compromised or must be revoked (e.g., identity theft), you need a fallback:

  • Revocation list: Maintain a public list of revoked credential IDs. Proofs bind to the credential ID, which the verifier checks against the list.
  • Expiration: Short credential lifetimes (hours/days) reduce the impact of compromise. Holders re-request credentials periodically.

Computational Cost

Generating ZKPs is non-trivial. Schnorr proofs are fast (<10ms on modern hardware), but more complex statements (multi-attribute range proofs, signature verification within the proof) can take 100–500ms. On-device verification is typically <5ms.

For user-facing applications, generate proofs asynchronously or pre-compute on background threads.

Nonce and Replay Prevention

Every proof must bind to a unique context (nonce). Without it, an attacker could:

  1. Observe a valid proof.
  2. Replay it later to impersonate the user.

Include a nonce from the verifier in each challenge:

// Verifier generates and stores nonce
const nonce = crypto.randomUUID();
// Holder includes nonce in proof
const proof = await ProveCredential({ credential, statement, nonce });
// Verifier checks nonce hasn't been used before

Credential Formats

Several standards exist:

  • ISO/IEC 18013-5 (mDL, mobile driver's license): CBOR-based, supports selective disclosure natively.
  • W3C Verifiable Credentials (VC): JSON-LD format, flexible but requires integration with proof systems.
  • Hyperledger Indy: Ledger-based, built-in revocation and schema management.

Choose based on your ecosystem and interoperability needs.

Real-World Example: Age-Gated Service

Here's a simplified flow:

  1. Issuer (Government): Issues a credential with DOB, address, ID number, signed with their key.
  2. Holder (User): Stores credential on-device.
  3. Verifier (Music Streaming Service): Needs to verify user is 18+.
  4. User requests access to age-restricted content.
  5. Service generates nonce, sends challenge.
  6. User's device: Generates ZKP proving "I hold a credential from [issuer] attesting age >= 18, issued in the last 365 days, nonce=[X]."
  7. Service verifies proof: If valid, grant access. Logs "age-verified user" (no age stored).
  8. No identity leakage: The issuer never learns who accessed what; the service never sees the user's actual age.

Challenges and Limitations

  • Ecosystem adoption: Most identity infrastructure assumes central verification. Decentralized ZKP-based systems are immature.
  • Quantum risk: Current ZKP schemes may be vulnerable to quantum computers. Post-quantum alternatives are under research.
  • Debugging: When a proof fails, pinpointing the issue (bad credential, wrong statement, tampered data) requires instrumentation.

Getting Started

  • Circom + snarkjs: JavaScript-friendly circuit language and proving toolkit. Great for learning.
  • libsnark: C++ library, more mature, steeper learning curve.
  • Hyperledger Indy SDK: Full-stack credential system with ZKPs, revocation, and ledger integration.
  • W3C VC Data Model: Standard for credential representation; pair with a proof system like BBS+ or ZKP2.

Zero-knowledge proofs unlock a new dimension of privacy in authentication. The tooling is maturing, and early adopters are solving credential verification at scale. Start with a simple statement (age, role membership), validate the user experience, then expand.

The next generation of identity systems will measure trust in bits of information revealed, not in credentials handed over. ZKPs are how we get there.

Encrypt your agent's data today

BitAtlas gives your AI agents AES-256-GCM encrypted storage with zero-knowledge guarantees. Free tier, no credit card required.