← All Articles
Technical 15 min read

How Zero-Knowledge Proofs Protect Privacy

A developer's introduction to ZKP fundamentals and a new way to think about identity

Denys Popov

You’ve been leaking your identity your whole life

Think about how often you are asked to share personal information. Most of the time, we hand it over without asking whether all of it is actually required.

A simple example: you walk into a bar, and the bouncer asks for your ID. That card shows your full name, address, date of birth, and often an ID number. The bouncer only needs one fact, whether you meet the age requirement. But the way IDs work today, the proof and the data come bundled together.

In a bar, that oversharing is mostly harmless. The bouncer is not writing down your address. They do not care what your ID number is.

Online, the same problem scales up fast. Every time you need to prove something: your age, where you live, whether you qualify for a service, the system needs the actual data to verify it (there is no ‘glance and move on’). The data must be handed over, stored, processed. Proving something and exposing something become the same action. What you actually need is a way to prove a claim is true without the data ever leaving your hands.

This is the fundamental problem with credentials today. They’re designed to be shown, not proven. You don’t prove a fact - you reveal the data behind it. The information and the proof are the same object.

Zero-knowledge proofs change that.

What is a zero-knowledge proof?

A zero-knowledge proof (ZKP) is a cryptographic method that lets you prove a statement is true without revealing anything beyond the truth of that statement. In other words, you can prove a property without exposing the underlying data. For example, you can prove you are over 18 without revealing your date of birth.

Instead of sending raw information, you generate proof that a specific claim is valid. The verifier checks the proof and learns only the answer to the question they asked, nothing else.

This is enforced mathematically. The verifier can be confident the claim is true, without needing to see or store the underlying data, and proofs can be generated and verified in milliseconds by modern systems.

From theory to practice: zk-SNARKs

The type of ZKP most relevant to developers right now is the zk-SNARK, Zero-Knowledge Succinct Non-Interactive Argument of Knowledge.

“Succinct” means the proof is small and fast to verify (typically a few hundred bytes, verified in under a millisecond). “Non-interactive” means the prover sends one message to the verifier, no back-and-forth.

The most widely deployed zk-SNARK scheme is Groth16 (2016), used in Zcash and hundreds of other privacy applications. It produces proofs of just 3 group elements, around 192 bytes regardless of how complex the statement being proven is.

For developers building on Ethereum or EVM-compatible chains, the primary tooling is Circom, a domain-specific language for writing arithmetic circuits, paired with SnarkJS for generating and verifying proofs in JavaScript.

ZKP-friendly cryptography

Standard cryptography such as SHA-256, ECDSA, and RSA was built for CPUs, not zero-knowledge circuits. CPUs are efficient at bitwise operations and binary logic, but zk-SNARK circuits work best when computation can be expressed as arithmetic constraints over a finite field. That mismatch makes familiar primitives expensive to prove inside a circuit. A single SHA-256 hash can cost around 25,000 constraints, and a real credential flow may require several hashes plus signature verification. The result is slower proof generation, higher memory use, and circuits that are too heavy for practical identity use cases. This is why ZKP systems rely on primitives designed for circuits from the start.

This led to ZKP-native building blocks such as Poseidon for hashing and Baby JubJub for signatures. Both are implemented in https://github.com/affinidi/affinidi-zkp-crypto-rs - a standalone Rust library with FFI exports for iOS and Android, and in the protocol’s cryptographic primitives section: https://github.com/affinidi/affinidi-vc-zkp-dart/blob/main/doc/protocol.md#cryptographic-primitives, if you want the formal spec.

Poseidon hash: a hash function designed natively for prime fields. Instead of the binary operations that make SHA-256 expensive in a circuit, Poseidon uses simple arithmetic operations that are near-free inside a zk-SNARK. The result: around 240 constraints per hash, roughly 100× cheaper than SHA-256. It’s now used in Semaphore, Polygon ID, Mina Protocol, etc.

Baby JubJub: an elliptic curve defined over the BN254 scalar field, meaning operations on it can be expressed natively in Circom circuits. Signature verification with Baby JubJub costs around 3,000 constraints, practical to include in any circuit that needs to verify a credential signature.

These two primitives together are what make it possible to do identity verification, credential checking, and selective disclosure entirely inside a ZK circuit.

Where existing credential standards hit their limits in ZKP systems

Existing credential standards enhance privacy—but they hit hard limits in zero-knowledge systems. Standards like SD-JWT enable selective disclosure, allowing a holder to reveal individual attributes without exposing the entire credential. However, selective disclosure is not equivalent to zero-knowledge verification. When applications require proving claims within a cryptographic circuit, current credential formats encounter two key challenges.

1. They use cryptography that does not work well in ZK circuits

SD-JWT relies on primitives such as SHA-256 and standard signature schemes that were not designed for zk-SNARK circuits. That makes them expensive, or impractical, to verify inside systems such as Circom. In other words, the format may support selective disclosure at the application layer, but the underlying cryptography still blocks efficient zero-knowledge proofs.

2. They still leak signals at presentation time

Even when only part of a credential is disclosed, the presentation can still expose stable identifiers. The holder’s key may appear in visible metadata, and the credential itself is deterministic, so presenting it more than once can leave a recognisable fingerprint. That weakens privacy across sessions, because repeated use becomes easier to correlate.

For simple disclosure use cases, those trade-offs may be acceptable. But for privacy-preserving identity systems built around zero-knowledge proofs, they become hard limits. The issue is not selective disclosure itself. The issue is that the underlying primitives were never designed for proof-first, un-linkable verification.

What a Credential Needs to Work with ZKP

The limitation is not the idea of verifiable credentials - it’s the cryptography and presentation model underneath. If a credential relies on primitives that are expensive inside ZK circuits, or if presenting it exposes stable identifiers across sessions, then proving anything about it privately becomes difficult in practice.

A credential designed for zero-knowledge proofs has to solve both problems at once. It needs primitives that can be verified efficiently inside a circuit, and it needs a presentation model that does not expose the holder or leave reusable fingerprints. That is the design goal behind Affinidi ZK VC, a verifiable credential model built specifically for proof-first, privacy-preserving verification.

What changes in a ZKP-native credential model

Circuit-friendly primitives throughout. Instead of relying on conventional hashes and signatures that are awkward inside zk-SNARKs, the model uses Poseidon for commitments and digests, and Baby JubJub for signatures. These are practical to verify in Circom, which keeps proof generation small enough for real credential flows.

The holder is not exposed to the verifier. In many credential systems, the holder’s public key appears somewhere in the presentation flow. Here, the holder proves control of the credential inside the circuit, without exposing their public key as visible metadata. The verifier learns that the holder is legitimate, but not who the holder is.

Presentations are untraceable across sessions. Each proof is generated with fresh randomness, so two presentations of the same credential do not produce the same cryptographic output. That means a verifier cannot compare proof artifacts across sessions and use them as a tracking signal.

The circuit design is modular. Credential validation and claim checking are split into separate circuits, linked by a session nonce. This makes the system easier to reuse: one circuit can validate the credential itself, while other lightweight circuits prove specific facts about it without needing to process the entire credential structure each time.

These principles in practice: the credential format

Those four properties aren’t abstract design goals: they’re reflected directly in how the credential is structured on disk.

The holder stores their credential in a JSON format defined in the protocol spec:
https://github.com/affinidi/affinidi-vc-zkp-dart/blob/main/doc/protocol.md#credential-structure

{
    "header": {
        "version": "1",
        "issued_at": 1712345678,
        "expires_at": 1743881678,
        "issuer": "BabyJubJub Ax, Ay of issuer",
        "holderAx": "BabyJubJub x-coordinate of holder",
        "holderAy": "BabyJubJub y-coordinate of holder",
        "schema": "0xabcd...hash of credential schema"
    },
    "disclosures": [
        { "field": "age", "value": 28 },
        { "field": "nationality", "value": "UA" }
    ],
    "header_commitments": [
        "Poseidon(fieldName_as_felt, value)",
        "..."
    ],
    "payload_commitments": [
        "Poseidon(fieldName_as_felt, value)",
        "..."
    ],
    "signature": {
        "R8": ["0x...", "0x..."],
        "S": "0x..."
    }
}

Each claim is committed individually as Poseidon ([fieldNameAsFelt, value]). The field name is packed into a single BN254 field element — a 31-byte UTF-8 string packs cleanly into the ~254-bit field element, so there’s no variable-length encoding problem at the circuit level. Including the field name in the preimage prevents two different fields with the same value from producing the same commitment.

The issuer signs the digest Poseidon ([…all header and payload commitments…]) using Baby JubJub. This flat array approach saves 2–3 hashes compared to a two-level Merkle root, with no security tradeoff.

The presentation protocol enabling ZKP

When a holder presents their credential to a verifier, no identifying information crosses the wire. The full flow is specified in the protocol documentation.

Verifier → Holder:   challenge  (fresh random nonce per session)

Holder:
  1. Generates a session nonce (random felt, never reused)
  2. Signs the challenge with holder_privkey (Baby JubJub)
  3. Computes blinded_root = Poseidon([digest, nonce])
  4. Generates Circuit 1 proof

Holder → Verifier:   proof + { challenge, issuer_pubkey, blinded_root, nonce }

The verifier sees challenge (which they generated), issuer_pubkey (public knowledge, the issuer is a known entity), blinded_root (a hash, reveals nothing), and nonce (random felt, reveals nothing). Everything else, the holder’s key, the credential contents, the signature stays private.

Multi-circuit architecture

The multi-circuit architecture eliminates the need to create a dedicated circuit for each new credential request and allows flexible combination of pre-built circuits to cover most use cases. The Circom source files for both circuits are included in the affinidi-vc-zkp-dart repository under example/circuits/

Circuit 1 — Identity and credential validation

This circuit proves three things simultaneously:

  • The holder controls the private key corresponding to the holder public key committed in the credential
  • The challenge was signed by that same holder
  • The credential was signed by the claimed issuer and has not been tampered with
Public:   challenge, issuer_pubkey, nonce, blinded_root
Private:  commitments[], doc_signature, challenge_sig, holder_pubkey

The circuit internally recomputes the Poseidon digest over all commitments, verifies the Baby JubJub signature against issuer_pubkey, verifies the challenge signature against the derived holder_pubkey, and calculates the blinded_root which matches Poseidon ([digest, nonce]).

Circuit 2 — Claim predicate

This circuit proves that a specific claim inside the credential satisfies a condition — without revealing the claim value.

Public:   threshold, satisfies (bool), field_name [optionally], nonce, blinded_root
Private:  commitments[], value

The verifier checks that blinded_root and nonce match Circuit 1’s outputs — that’s the cryptographic link between the two proofs. Then the circuit proves the commitment at field_name contains value via Poseidon ([field_blinding, value]), and that value satisfies the predicate.

The verifier learns only one bit: the claim satisfies the threshold. Not the value. Not anything about the other claims.

ZK VC - High Level Flow

What each party actually learns

SignalVerifier seesCross-session linkable?
Holder public keyNeverNo
Raw claim valuesNeverNo
Credential contentsNeverNo
blinded_rootYes — randomised per sessionNo
nonceYes — ephemeralNo
challengeYes — they generated itNo
issuer_pubkeyYes — public knowledgeIssuer-level only
ZK proof bytesYes — Groth16-randomisedNo

Unlike existing solutions, the holder does not share any data that could be used as a fingerprint to identify them. Even if verifiers coordinate, share information with each other, and attempt to reuse the same signing nonce challenge, they will still have no repeating data that could be traced back to the same holder or credential.

Performance

The constraint counts are small by Groth16 standards:
https://github.com/affinidi/affinidi-vc-zkp-dart/blob/main/doc/protocol.md#performance-notes

CircuitApproximate constraints
Circuit 1 (N=10 claims)~7,300
Circuit 2 predicate (N=10 claims)~1,400
Baby JubJub signature verify~3,000
Poseidon (N inputs)~240 + 60N

For comparison, a single SHA-256 hash verification inside a circuit costs ~25,000 constraints. The entire Circuit 1 here — two signature verifications plus all hashing — costs less than a third of that.

During testing on the iPhone 13, we observed execution times of approximately 211 ms for the main circuit and around 85 ms for the predicate check circuit.

Built on established foundations

Nothing in this design is experimental:

  • Poseidon hash: published at USENIX Security 2021, used in Semaphore, Polygon ID, Mina Protocol

  • Baby JubJub: standardised as EIP-2494, the default curve in the Ethereum ZK identity stack

  • Groth16: published at Eurocrypt 2016, the most widely deployed zk-SNARK

  • Circom / circomlib: reference implementations of all the above, maintained by Iden3

  • SD-JWT: IETF draft, structural inspiration for the commitment-based disclosure model

  • Polygon ID: prior art for Baby JubJub based VC systems

The new contribution is the combination: a credential format designed from the start for ZKP verification, with full untraceability at the presentation layer, using only primitives that have been independently audited and deployed at scale.

What this enables

A ZKP-native credential model changes what developers can build. Instead of designing flows that collect and store sensitive user data, you can design flows that verify a fact and learn nothing else. That makes privacy-preserving identity checks practical in places where traditional credential formats still force disclosure.

This matters for beyond privacy in the abstract. It changes how identity, onboarding, and compliance flows can work in practice. A verifier no longer has to ask for the full document or the raw personal data behind it. They can ask a narrower question and get only the answer they need.

That opens up use cases like:

  • Age-gated access: prove you’re over 18 without revealing your date of birth, name, or any other field.

  • KYC without data sharing: prove a regulated issuer verified your identity without sharing which issuer, when, or any personal data.

  • Income verification: prove your income exceeds a threshold for a loan application without revealing the number, your employer, or your bank.

  • Cross-border identity: carry a credential issued by one jurisdiction and prove claims from it to verifiers in another, without either party learning anything about the other.

  • Composable privacy: stack multiple claim proofs in a single session, all linked to the same credential validation proof via the shared session nonce.

This is the direction Affinidi is building toward: making zero-knowledge identity usable in real products, through a credential model, protocol design, and open-source libraries that help developers move from document sharing to proof-based verification. The goal is simple: verify what matters, reveal nothing more than necessary.

Where this goes next

Zero-knowledge proofs are not just a privacy feature. They are becoming a basic requirement for how we exchange trust online.

As AI systems get better at extracting, correlating, and reusing data, any information you disclose today can be copied, replayed, and combined in ways you never intended.

“The safest data is the data you never had to share in the first place.”

That is what ZKPs enable: prove what needs to be true, without handing over the underlying personal or business data. Instead of sending documents, you send proofs. Instead of collecting sensitive fields, you verify a claim.

The shift is simple, but the impact is huge. If we want digital identity to scale without turning every interaction into a data leak, we need to move from disclosure to verification, and start building systems that default to privacy.

If you’re building identity, onboarding, or compliance flows, this is the moment to start experimenting. ZKPs are ready for builders, and you can explore here:

https://github.com/affinidi/affinidi-vc-zkp-dart
https://github.com/affinidi/affinidi-vc-zkp-dart/blob/main/doc/protocol.md
https://github.com/affinidi/affinidi-zkp-crypto-rs

Zero-Knowledge ProofsDigital IdentityVerifiable CredentialsPrivacyzk-SNARKsSelective DisclosureDecentralized IdentityCryptography

Build with Affinidi

Start building trust infrastructure with our open-source tools and developer-friendly APIs.

Cookie Preferences

We use cookies to enhance your experience. You can manage your preferences below. For more information, read our Cookie Policy.

Strictly Necessary Always Active

These cookies are essential for core website functions such as security, session integrity, and cookie preference storage. They cannot be disabled.

  • _cf_bm: Distinguishes humans from bots (Cloudflare) · 30m
  • _cfuvid: Ensures secure browsing (Cloudflare) · Session
  • __hs_initial_opt_in: Prevents HubSpot's banner · 7 days
  • _gtm_debug: GTM debug mode (testing only) · Session
Analytics

These cookies help us understand how visitors interact with the site so we can improve content and performance. All data is aggregated and anonymous.

  • _ga, _gid, _gat: Google Analytics · Session – 2 years
  • __hstc, hubspotutk, __hssrc: HubSpot visitor tracking · 13 months
  • __hs_opt_out: HubSpot opt-out preference · 6 months
Marketing & Targeting

These cookies allow us and our partners to serve personalised ads and measure campaign performance.

  • _gcl_au, _gcl_dc: Google Ads conversion tracking · 90 days
  • IDE: Google Display Network personalisation · 1 year
  • _fbp: Meta / Facebook remarketing · 90 days
  • li_gc, _li_fat_id, bcookie: LinkedIn tracking · 1–24 months
  • guest_id, personalization_id: Twitter/X analytics · 2 years