31 July 2026
We verified a Zero-Knowledge proof from the official AV app: how it works, and what our verifier still doesn't do
By Jorge
The blueprint's privacy piece
The EU Age Verification Blueprint has one privacy piece that sits above the rest: Zero-Knowledge Proofs. With the longfellow-zk scheme — designed by Google engineers, peer-reviewed paper, IETF draft — a wallet can prove its holder is over 18 without showing the credential, without a traceable issuer signature in the clear, and without two separate verifications being linkable to each other. The blueprint's Annex A says it outright: the age verification app should generate these proofs, and the relying party should verify them.
Google Wallet has generated them since 2025; the official EU AV app since early 2026. We wanted to understand the other half — the relying-party side, the business that has to check the proof — by building it. So we built an independent ZK verifier and tested it against the real thing.
What we did, and what backs it up
On an Android phone with the official reference AV app and an age credential issued by the blueprint's own test issuer, we opened our demo, tapped "With your phone", picked the AV app, and confirmed the presentation.
The result:
✓ accepted · verify 317 ms · 353 KB
issuer: CN=Age Verification DS - 001,
O=Age Verification Reference Implementation, C=EU
That issuer is what makes the result credible. It isn't our demo issuer — ours signs as "espuni demo AV issuer". It's the Document Signer of the reference implementation: the credential was genuine, the wallet was the official one, and the cryptographic proof was checked by our code. A 353 KB proof, verified in a little over three tenths of a second. And because every run of our demo publishes the proof, anyone can download it and re-verify it with the open-source binary, outside our infrastructure.
The hard part isn't the wallet
It's tempting to assume the hard part here is the wallet. It isn't: wallets already generate the proofs. The hard part is the relying-party side — asking for the proof in the right format, receiving the encrypted response, decrypting it, and verifying the proof against the right circuit and the right key.
Every step has a trap. The request travels as an ISO 18013-7 deviceRequest carrying a zkRequest block that advertises which circuits we accept. The response comes back encrypted with HPKE (RFC 9180) to an ephemeral key we generate — so we implemented and validated HPKE against the official CFRG test vectors before trusting a single byte. The "transcript" that binds the proof to this specific session has to be reconstructed identically, byte for byte, or verification fails without telling you why. We pulled all of that out of the reference wallet's source and checked every assumption against the real thing. It's genuine integration work, and it works.
What our verifier still doesn't do
This is where we want to be precise, because in identity verification the honest half of a demo matters as much as the half that lights up green.
- We don't yet validate the Trusted List in the ZK path. We take the issuer's public key from the certificate carried in the response and verify the proof against it. That proves the proof is cryptographically sound relative to that key, but we don't yet check that the certificate chains to a trust anchor on the official AV Trusted List. Our classic (non-ZK) verification flow does do this; bringing it to the ZK path is the next step, not something already done.
- Freshness we now check. The "now" the circuit validates the credential's window against is signed inside the proof, so we can't substitute our own clock — but we do require that instant to fall within a window of our real time, which rejects a proof with an old timestamp (an expired credential asserted as current) or a future one.
- We're not the only ones. The reference ecosystem itself has a ZK verifier (the DC API backend built on Multipaz), which also validates trust anchors. Other ecosystem players verify ZK too. What we add isn't being first — it's doing it as a managed relying-party layer.
Why this matters to a platform
The ecosystem's direction is unambiguous: ZKP is in the blueprint's Annex A, in the EUDI technical specifications, in the draft second edition of ISO 18013-5, and Google already ships it. When wallets start sending ZK proofs at volume, every platform will have to decide: stand up the whole stack yourself — archived, versioned circuits, Trusted List validation, a native verification service, and tracking a scheme that has broken compatibility three times in a year — or call an API that absorbs that complexity.
That's espuni's bet: one integration, every app. Classic verification has been in production for a while with full trust validation; the ZK verifier is proven against the official wallet and on its way to matching those guarantees (Trusted List and freshness) before we call it ready. If you're building age verification, let's talk.
Technical note: the longfellow-zk scheme is under two independent security reviews whose reports haven't been published yet; we don't describe it as "audited". The verifier is real and reproducible — every run of our demo publishes the proof so anyone can re-verify it with the open-source binary — with the limits spelled out above.
Sources for this article are linked inline. For the full list of primary sources espuni relies on, see references.