Back to blogZero-Knowledge verification flow: the official AV app presents an encrypted proof and espuni's verifier accepts it in 317 ms

We verified a Zero-Knowledge proof from the official AV app: how it works, and what our verifier still doesn't do


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, for a relying party, isn't the wallet

To be clear: cryptographically, generating the proof is the expensive side — proving in a Zero-Knowledge system is orders of magnitude heavier than verifying it (that's why the wallet takes ~1 s to generate one and we take ~300 ms to check it). But that part is already built and shipped: wallets — Google, the reference AV app — already generate the proofs; nobody integrating verification reimplements the prover.

What nobody hands you — and what we had to build — is the relying-party side: asking for the proof in the right format, receiving the encrypted response, decrypting it, and verifying it against the right circuit and the right key. That's where the integration work is.

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 we've now closed, and what we haven't

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 now validate the Trusted List in the ZK path — and for AV that's the whole chain. When we first published this article we didn't yet; we've since closed it. AV has no "list of lists": there's a single European list of AV issuers, published and signed by the Commission. The ZK verdict now requires all three layers: that the proof is cryptographically sound, that it's fresh, and that the credential's Document Signer is among that list's recognized issuers — whose XAdES signature we verify against the scheme operator whose certificate we pin. There's no higher level to chain to: anchoring to that list is anchoring to the root. It's the same validation our classic flow does.
  • Freshness too. 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.
  • What's left is confirming and hardening it, not completing the chain. We still need to check the byte-for-byte match of the real DS against the list on a phone test (the cryptographic interop is already verified); check the list's NextUpdate to explicitly reject a stale copy (we fetch it live every few hours, so the practical risk is low); and, if the list can't be loaded, the check degrades to report-only rather than blocking. The trust chain itself, for AV, is complete.
  • 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 now validates Trusted List and freshness, with the trust chain complete for AV — what remains is confirming it in the field (the real DS match on a phone) and minor operational hardening. 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.