sota.io
Join the waitlist
2026-09-07·15 min read·sota.io team

The EU's Age Verification App Was Bypassed Twice in 2026 -- What DSA Article 28 Actually Requires From Your Integration

The EU's Age Verification App Was Bypassed Twice in 2026 -- What DSA Article 28 Actually Requires From Your Integration

On 15 April 2026, the European Commission released an open-source reference implementation of the EU's official age-verification app and urged member states to roll it out fast (European Commission, 29 April 2026). Within days, security researcher Paul Moore had defeated it: editing a plain-text XML configuration file reset the app's PIN, flipped a boolean flag to disable biometric authentication, and kept the underlying credential access intact -- under two minutes, no exploit chain required (Gataca). The project's own GitHub repository had already flagged the build as unfit for production before the Commission's press conference.

Four months later, in August 2026, Moore was back with a second, more interesting bypass: a small Chrome extension that intercepted a genuine "over 18" proof once and then replayed it to every site that asked, without a fresh verification each time (EFF, August 2026). This one wasn't sloppy config-file security -- it worked because the app's zero-knowledge-proof credential is designed to be reusable within its validity window. That distinction matters if you are building the other side of this handshake: a platform integrating age checks under Article 28 DSA, the EU's protection-of-minors provision.

This post covers what the EU's Age Verification Solution actually is (it is not the same thing as the EU Digital Identity Wallet, even though the two get merged in most coverage), what both bypasses actually exploited, and what a Relying Party integration needs to check to avoid inheriting either failure mode.

It's a "mini-wallet," and that distinction is load-bearing

The Age Verification Solution is a separate, narrower project from the full EU Digital Identity (EUDI) Wallet that member states must offer under eIDAS 2.0. The Commission's own technical specification repository states the goal directly:

"The proposed solution is intended to bridge the gap until the EU Digital Identity (EUDI) Wallets become available by the end of 2026, enabling the incorporation of the age verification functionality in them." (eu-digital-identity-wallet/av-doc-technical-specification, GitHub)

In other words: the full EUDI Wallet (the thing our eIDAS 2.0 relying-party guide and OpenID4VP cross-device guide cover) is the long-term infrastructure. The age-verification app is a fast-tracked, narrower "mini-wallet" that a member state can ship standalone or fold into its EUDI Wallet once that's ready -- built specifically to satisfy Article 28 DSA and the 2024 Louvain-la-Neuve Declaration on child safety online, ahead of the full wallet's timeline (av-doc-technical-specification README). Seven member states -- Denmark, France, Greece, Italy, Spain, Cyprus, and Ireland -- are piloting it in 2026, with more building their own implementations of the same spec (ageverification.dev).

Three legal instruments overlap here, with three different deadlines, which is the single most common source of confusion in secondary coverage:

InstrumentBinding?What it requiresDeadline
DSA Article 28 (Regulation (EU) 2022/2065)Yes, enforceableOnline platforms accessible to minors must take "appropriate and proportionate measures" for privacy, safety, and security of minors; no profiling-based ads to known minorsGeneral DSA application since 17 Feb 2024; Commission Art.28(1) guidelines published 14 July 2025
Commission Recommendation on age verification (29 April 2026)No, non-bindingUrges member states to make an age-verification app available to citizens"By the end of 2026" (European Commission)
eIDAS 2.0, Art.5a (Regulation (EU) 2024/1183)Yes, bindingEvery member state must provide at least one EU Digital Identity Wallet to citizensWithin 24 months of the relevant implementing acts (due 21 Nov 2024) -- target window ~end of 2026, not a single fixed calendar date in the Regulation's own text

Read that middle row carefully: the specific "31 December 2026" date you'll see cited everywhere for the age-verification app comes from a Recommendation, not a Regulation. A Recommendation under EU law creates no enforceable obligation and carries no penalty for a member state that misses it. The actual binding pressure on your business to do age verification at all comes from DSA Article 28 -- which has applied for over two years already. The Recommendation just tells member states which tool the Commission wants them to offer you.

How it's actually supposed to work

The architecture has three roles: an Attestation Provider (AP) that issues a "Proof of Age" credential after identity-proofing the user to eIDAS Level of Assurance "Substantial," a Relying Party (RP) -- you, if you're gating age-restricted content or products -- that requests and verifies the credential, and a centrally maintained Trusted List that lets an RP confirm which APs are legitimate (av-doc-technical-specification, architecture spec, ss3.2-3.4). Trust anchors are published via eIDAS Article 22 trusted-list infrastructure on the eIDAS Dashboard.

Two details in the spec matter more than the marketing copy about "zero-knowledge proofs" suggests:

First, the credential is explicitly allowed to be reused. The spec states it plainly: "An attestation presented as a Zero-Knowledge Proof MAY be reused within its validity period, since the proof does not permit linkage across presentations" (av-doc-technical-specification). Validity is recommended at up to three months, with no revocation mechanism required ("Implementing revocation mechanisms would significantly increase the complexity for both Proof of Age Attestation providers and Relying Parties"). This is a deliberate privacy tradeoff -- a linkable, single-use token would let colluding RPs correlate the same user across sites, which is exactly what the ZKP design is meant to prevent. But it also means the credential, once obtained, behaves less like a one-time OTP and more like a long-lived bearer token from the RP's perspective.

Second, the entire trust chain is anchored at issuance, not at verification. The spec is explicit: "Trust in Age Verification Apps is established at issuance time... If a repackaged or cloned app, a tampered execution environment, or an emulator farm can obtain Proof of Age attestations, every downstream safeguard is undermined." The mechanism meant to prevent that is called an AVS Unit Attestation (AVSUA) -- proof to the Attestation Provider that the requesting app instance is genuine and untampered, checked once, before a credential is ever issued (av-doc-technical-specification, s3.4.4).

Put those two design choices together and you can see exactly what both bypasses actually broke.

Bypass #1 (April 2026): defeating the issuance-time check locally

Moore's first bypass didn't touch cryptography at all. He edited a plain-text XML configuration file on-device to reset the app's PIN, flip a boolean flag disabling biometric authentication, and retain access to the already-issued credential -- reported at under two minutes (Gataca). This is a local-authentication bypass, not an attestation forgery: it doesn't fabricate a fake Proof of Age, it just removes the device-level gate (PIN/biometric) that's supposed to stop someone other than the enrolled user from operating an already-provisioned app instance. The European Commission's response was that this was a "demo" build, and the project's own repository had already flagged it as not production-ready before the incident became public. That's a fair point about this specific build -- but it also means the reference implementation every member state's own app is likely to fork from shipped with exactly the kind of client-side, config-file-based auth gate that a decade of mobile-security tooling knows how to defeat.

Bypass #2 (August 2026): replaying a legitimately reusable proof

The second bypass is the one worth sitting with longer, because it isn't really a bug against the spec -- it's the spec working as written, applied somewhere the spec didn't anticipate. Moore's Chrome extension intercepted one genuine "over-18" proof and replayed it automatically whenever a site requested age verification, without the app or the user being involved a second time (EFF, "Zero-Knowledge Proofs Aren't Age Verification Silver Bullets," August 2026). Multiple independent outlets reproduced and corroborated the report, including Cybernews and Cybersecurity News -- this is not a single unverified claim.

Given the spec explicitly permits ZKP-based reuse "within its validity period" precisely so the proof "does not permit linkage across presentations," a browser-level extension replaying an intercepted proof isn't exploiting a cryptographic flaw in the ZKP math. It's exploiting the fact that reusability, by design, has no built-in requirement that the human be present and consenting at each verification -- only that the credential itself remains within its (up to three-month) validity window. EFF's framing is the right one for developers: "ZKPs are not a magic bullet. They do not solve the age verification paradox; they simply push the burden of trust down the road." The trust that a linkable single-use token would have provided (this specific presentation happened now, for this specific request) is exactly the trust the privacy-preserving design gives up.

What this means for your Relying Party integration

If you operate a platform in scope for Article 28 DSA (or you're a vendor building age-gating for one), don't treat "the app returned verified: true" as the end of your due diligence. Concretely:

  1. Validate the attestation server-side, against the trust list -- never trust a client SDK's boolean alone. Confirm the issuing Attestation Provider's Trust Anchor (Service Digital Identifier) against the eIDAS AV trusted list for every verification, not just the first one for a given library integration.
  2. Know the interim-validation caveat. The Age Verification Scheme's own trusted list of AP providers doesn't exist yet. Until it does, the spec says validation is anchored "in the certificates lodged with the Commission by Age Verification App providers and published in the list of compliant Age Verification Apps" (av-doc-technical-specification, s3.4.4) -- a different, temporary source of truth that a naive integration built against "the trusted list" documentation alone will miss.
  3. Budget for replay as an accepted risk, not a bug you can code around. Because a ZKP-presented Proof of Age can legitimately be reused for up to three months with no revocation, your integration cannot cryptographically distinguish "the enrolled human is presenting this right now" from "someone replayed an intercepted response." If your risk profile genuinely requires per-transaction freshness (e.g., a gambling platform with regulatory exposure beyond DSA Article 28), the spec's alternative -- a plain ISO mDoc presentation, explicitly defined as single-use -- is the stronger option; requesting the ZKP variant trades that guarantee away for user privacy.
  4. Log verification metadata for your own Article 28 accountability, not the user's identity. Record which AP issued the credential, the trust-anchor ID, and the timestamp of your verification call -- enough to demonstrate you ran a real check if a Digital Services Coordinator asks, without retaining anything that would recreate the linkability the ZKP design is meant to prevent.

A minimal illustration of the difference between a naive and a correct verification call on your backend:

// NAIVE — trusts the client SDK's own verdict, no server-side validation.
// This is what both 2026 bypasses would sail straight through: a tampered
// app instance or a replayed response both produce { verified: true }.
async function gateContent(req, res) {
  const { verified } = req.body; // reported by the client-side wallet SDK
  if (verified) return res.status(200).send("access granted");
  return res.status(403).send("age verification required");
}

// CORRECT (illustrative) — the server independently validates the
// presented attestation against the trust anchor, checks it hasn't
// expired, and records the verification for Article 28 accountability.
// This still cannot detect a replayed-but-genuine ZKP presentation
// (see point 3 above) — that's an architectural limit, not a bug.
async function gateContent(req, res) {
  const { attestation, trustAnchorId } = req.body;

  const trustedAP = await lookupTrustAnchor(trustAnchorId); // eIDAS AV
  // trusted list, or the interim Commission-published compliant-apps
  // list while the dedicated AV Scheme list is not yet live
  if (!trustedAP) return res.status(403).send("unknown issuer");

  const proof = await verifyZkpAttestation(attestation, trustedAP.publicKey);
  if (!proof.valid || proof.expiresAt < Date.now()) {
    return res.status(403).send("age verification required");
  }

  await logVerification({
    trustAnchorId,
    issuedAt: proof.issuedAt,
    verifiedAt: Date.now(),
  }); // no user-identifying data retained

  return res.status(200).send("access granted");
}

The takeaway

The Commission's Age Verification Solution is a genuinely privacy-conscious design -- reusable, unlinkable, minimal-disclosure proofs are the right instinct for a system that will sit in front of a large share of EU internet traffic. But "privacy-preserving" and "replay-resistant" pull in opposite directions, and 2026's two public bypasses landed on exactly that seam: one defeated the local gate that's supposed to keep a credential tied to its enrolled user, the other exploited the reusability the spec grants on purpose. If you're building the Relying Party side of this under Article 28 DSA, the fix isn't waiting for the Commission to patch the reference app -- it's not conflating "the wallet said yes" with "this is verified," checking the actual trust anchor yourself, and being honest with your own risk owners about what a reusable credential can and can't prove.

EU-Native Hosting

Ready to move to EU-sovereign infrastructure?

sota.io is a German-hosted PaaS — no CLOUD Act exposure, no US jurisdiction, full GDPR compliance by design. Deploy your first app in minutes.