← BACK TO THE WIRE
N°0448ZK Tech9 MIN8 SOURCES

Longfellow ZK: how zero-knowledge age checks work on mdoc IDs

Longfellow ZK lets a wallet prove an mdoc attribute such as age_over_18 without sending the issuer signature or other data. Pin the circuit hash, use fresh nonces, keep an IACA trust list, and plan for plain-mdoc fallback.

SHARE
ZK Tech
Longfellow ZK: how zero-knowledge age checks work on mdoc IDs
IMAGE: AI-GENERATED

Longfellow ZK, the open-source library that Google points relying parties to for verifying zero-knowledge ID proofs from Google Wallet, is specified in draft-google-cfrg-libzk-03, an individual IETF Internet-Draft published on 23 September 2026. If you accept mobile IDs for age or residency checks, use this guide to see what a Longfellow proof shows, what you still trust, and what to set up on the verifier side.

Who this guide is for

This guide is for backend, wallet and identity developers who need to check a claim such as "over 18" or "lives in country X" without collecting a name, a birth date or a document scan. At the end you will know how an ISO mdoc credential is built, what a Longfellow proof states, what a request looks like, which checks your verifier must run, and where the privacy guarantee stops.

Prerequisites:

  • A Linux or macOS machine with clang and cmake. The README says the package "depends on cmake, openssl, zstd, clang, googletest and googlebenchmark".
  • The longfellow-zk repository, main branch. The commands below come from its README as of October 2026. The project is licensed Apache-2.0.
  • Basic knowledge of ECDSA on P-256, SHA-256 and the ISO/IEC 18013-5 mobile driving licence (mDL) data model.

For background on how this approach reuses existing government IDs, see our earlier piece on ZK proofs over existing IDs.

Step 1: Understand the credential you start from

An ISO 18013-5 credential (an "mdoc") has three parts that matter here:

  1. Data elements. Each attribute (family name, birth date, portrait and so on) is stored with a random salt. The mDL namespace also defines ready-made boolean elements such as age_over_18, and residence fields such as resident_country.
  2. The Mobile Security Object (MSO). The issuer hashes every salted element, puts the digests and the holder's device public key into the MSO, and signs the MSO with ECDSA.
  3. The device key. It lives in the phone's secure hardware. At presentation time it signs a session transcript that includes data from the verifier. This stops simple replay.

Plain mdoc already supports selective disclosure. The wallet sends only age_over_18 and its salt, plus the MSO and the issuer signature, and the verifier recomputes the digest. The problem is that the MSO and the issuer signature are the same bytes in every presentation. Two websites, or a website and the issuer, can match those bytes and link the visits to one person.

Step 2: Know what the zero-knowledge proof states

Longfellow replaces "send the MSO and signature" with "prove you have them". The paper behind it is Anonymous credentials from ECDSA (IACR ePrint 2024/2010, last revised 27 April 2026; the ePrint page notes the version that appears in Communications in Cryptology). It adds ZK arguments for SHA-256 and for parsing ISO identity formats, so a proof can be deployed "without changing any issuer processes, without changes to mobile devices".

In plain terms, the mdoc proof says:

  • I hold an MSO that has a valid ECDSA signature under this issuer's public key.
  • The MSO contains the digest of the element age_over_18 with the value true (or whichever elements the verifier asked for).
  • The device key listed in that MSO signed this session's transcript, which includes the verifier's challenge.
  • The credential's validity period covers the current time.

The verifier learns the requested element values and that the statement holds. It does not see the MSO, the issuer signature, the device public key, the salts or any other attribute.

The IETF draft describes the proof system itself. It combines the Ligero MPC-in-the-head argument with a sumcheck protocol to prove that a circuit C(x, w) evaluates to 0. The draft assumes there is no common reference string or trusted setup. The paper says the proof system "only relies on SHA-256 as its complexity assumption".

Step 3: Build the library

These commands are copied from the README on the main branch. On Ubuntu or Debian:

bash
$ sudo apt install -y build-essential clang cmake libssl-dev libzstd-dev libgtest-dev libbenchmark-dev zlib1g-dev
$ CXX=clang++ cmake -D CMAKE_BUILD_TYPE=Release -S lib -B clang-build-release --install-prefix ${PWD}/install
$ cd clang-build-release && make -j 16 && ctest -j 16

The README lists these commands for running some of the benchmarks:

bash
$ ./algebra/fft_test --benchmark_filter='BM_*'
$ ./circuits/sha/flatsha256_circuit_test --benchmark_filter=BM_ShaZK_fp2_128

The project's benchmark page says all code runs single-threaded "for deployment purposes" to save phone battery. It also says proving possession of one ECDSA signature takes "<17ms" on a Mac M4. These are project-reported numbers. The paper puts a full mdoc presentation proof at "a few hundred ms on mobile devices".

Step 4: Request a ZK proof

The Longfellow documentation proposes an OpenID4VP request with response mode dc_api. Only the format and meta parts differ from a normal mdoc request. The format is mso_mdoc_zk, and meta carries zk_system_type and verifier_message, which the docs say are defined in the ISO 18013-5 second edition, section 10.3.4.

Below is the documented example with two changes. A missing comma after doctype_value is added, and the claim path is changed from family_name to age_over_18 to show an age check. The circuit_hash is copied from the docs example for one attribute. It may not belong to a circuit that discloses age_over_18, so take the real value from your wallet or verifier documentation. The client_metadata encryption block is left out here; keep it from the original.

json
{
  "response_type": "vp_token",
  "response_mode": "dc_api",
  "nonce": "nonce",
  "dcql_query": {
    "credentials": [{
      "id": "cred1",
      "format": "mso_mdoc_zk",
      "meta": {
        "doctype_value": "org.iso.18013.5.1.mDL",
        "zk_system_type": [{
          "system": "longfellow-libzk-v1",
          "circuit_hash": "f88a39e561ec0be02bb3dfe38fb609ad154e98decbbe632887d850fc612fea6f",
          "num_attributes": 1,
          "version": 1
        }],
        "verifier_message": "challenge"
      },
      "claims": [{
        "path": ["org.iso.18013.5.1", "age_over_18"]
      }]
    }]
  }
}

For a residency check, request a residence element instead, for example resident_country, and compare the revealed value on your server. The proof reveals the element's value. It does not prove a range or set membership on its own.

Check which transport your target wallet supports. The Yivi team's longfellow-go bindings, which target the EU age verification profile, name longfellow-libzk-v1. They quote the profile's rule that a device which cannot produce a ZK proof must fall back to plain mdoc presentation.

Step 5: Verify on the server

Google's developer documentation for online ID acceptance says the wallet returns an encrypted zero-knowledge proof. You validate it against the issuers' IACA certificates using longfellow-zk. The documentation points to a Docker-based verifier-service (the repository has one in reference/verifier-service), and you edit its certs.pem to choose which issuer certificates to trust. Your verifier should do the following for each request:

  1. Generate a fresh, random nonce and challenge per session and bind them to the user's session. Reject any response for an unknown or reused nonce.
  2. Accept only the ZK systems and circuit hashes you listed in the request. A proof only means something if it is checked against the right circuit.
  3. Check the issuer key in the proof against your IACA trust list, and keep that list current.
  4. Check the revealed value, not only its presence: age_over_18 must be true.
  5. Decide in advance what happens when the wallet falls back to plain mdoc. That response is still valid, but it is linkable.
  6. Store the result ("over 18: yes, at time T"), not the raw response.

What is proven and what is trusted

QuestionProven by the ZK proof?What you still trust
Did a trusted issuer sign this attribute?Yes, ECDSA over the MSOThe issuer checked the person's age correctly before issuing
Is the issuer real?NoYour IACA certificate list
Is the holder's device present?Yes, device signature over the transcriptThe secure hardware and wallet were not cloned or shared
Is the attribute value true?YesThat the user is the person the ID was issued to
Is the statement the right one?NoThe circuit behind the pinned circuit_hash
Can a proof be forged?Not without breaking the hash assumptionSHA-256 collision resistance and a bug-free implementation

The README says the project "has completed several independent security reviews" and links the reports from its documentation.

Standards and deployments today

  • IETF. draft-google-cfrg-libzk-03 (authors Matteo Frigo and abhi shelat) has intended status Informational. As of 14 October 2026 the Datatracker lists it as an individual draft with no stream. That means it is not endorsed by the IETF and has no formal standing. It expires on 27 March 2027.
  • ISO. The Longfellow docs tie the request fields to the second edition of ISO/IEC 18013-5. For the broader ISO view on ZK privacy, see our note on ISO's first ZK privacy guidance.
  • Google Wallet. Google's developer documentation describes returning a ZK proof for online ID checks and points relying parties to longfellow-zk for verification.
  • EU age verification blueprint. The European Commission says a second version, released in October 2025, added a passport and ID card method for getting a proof of age and includes zero-knowledge proof technology. The Yivi wallet team publishes Go bindings that present mdoc credentials as longfellow-libzk-v1 proofs for that profile.
  • Other implementations and stewardship. ISRG publishes a Rust implementation that follows the draft. The library is listed as a project of the Post-Quantum Cryptography Alliance. ZKProof, the industry and academic standards initiative, runs working groups on ZK proof standards but does not define this credential flow.

The README also says the library targets JWT and W3C Verifiable Credentials, but the deployments above use ISO mdoc.

Privacy limits

  • The issuer is visible. The verifier checks the proof against a specific issuer certificate, so it learns which issuer signed the credential. If that issuer has few holders, few people share the same proof profile, so the user is easier to single out.
  • Revealed values are data. "Over 18" says little on its own. A residence value plus an age flag plus a time says more. Ask for one attribute when one is enough.
  • The network still leaks. The proof does not hide IP addresses, browser fingerprints, cookies or account logins. If a site already knows who you are, it now also knows you are over 18. ZK does not change that.
  • Fallback removes unlinkability. A plain mdoc presentation carries the same MSO and signature every time.
  • The issuer and wallet see some events. Issuance is not anonymous, and the wallet sees each request it answers.
  • No proof of who holds the phone. Device binding shows the right device took part, not the right person.

Pitfalls and troubleshooting

  • JSON parse error on the docs example. The published example lacks a comma after "doctype_value": "org.iso.18013.5.1.mDL". Add it.
  • Proof rejected after a wallet update. Compare the circuit_hash and version in the response with the ones you pinned. Circuits are versioned, so a mismatch should fail.
  • Fedora build fails on a missing library. The README says newer Fedora versions also need libpfm-devel.
  • Benchmarks slower than published. The README warns that benchmarks in its GitHub Codespaces devcontainer may run slower than reported because the container runs in a VM.
  • Spec drift. The IETF draft is at revision 03 and can change. Pin the commit you build from and re-check the draft before each upgrade.

FAQ

Does a zero-knowledge age proof reveal my birth date?

No. In the mdoc flow the verifier asks for age_over_18, and the proof reveals only that element's value. The birth date, name, MSO and issuer signature stay on the device.

Does Longfellow ZK need a trusted setup?

No. The IETF draft targets a setting without a common reference string or trusted parameters, and the paper says the proof system relies only on SHA-256.

Can I use Longfellow with SD-JWT or W3C Verifiable Credentials?

The README says the library targets ISO mdoc, JWT and W3C Verifiable Credentials. The documented request format and the Google Wallet and EU deployments use ISO mdoc, so start there.

TAGSLongfellow ZKISO 18013-5 mdocage verificationdraft-google-cfrg-libzk-03
Grounded sources8 REFS
  1. [01]longfellow-zk/longfellow-zk on GitHub (README)github.com ↗
  2. [02]Longfellow docs: Using ZK in Identity Protocolslongfellow-zk.github.io ↗
  3. [03]Longfellow docs: Benchmarkslongfellow-zk.github.io ↗
  4. [04]IETF Datatracker: draft-google-cfrg-libzk-03 (Longfellow ZK)datatracker.ietf.org ↗
  5. [05]Anonymous credentials from ECDSA (IACR ePrint 2024/2010)eprint.iacr.org ↗
  6. [06]Google Wallet: Online Acceptance of Digital Credentialsdevelopers.google.com ↗
  7. [07]European Commission: Blueprint for an age verification solutiondigital-strategy.ec.europa.eu ↗
  8. [08]privacybydesign/longfellow-go: Go bindings for EUDI age verificationgithub.com ↗
Read next

Get the wire in your inbox

Every new signal, straight from the generator. No noise, unsubscribe anytime.

RSS AVAILABLE · NO SPAM