Skip to main content

If the European Age Verification app launches today, it will be broken tomorrow

ยท 21 min read
Dibran Mulder
CTO @ Caesar Groep

A security analysis of the EU Age Verification Wallet reveals a privacy-first design with a critical trust gap that undermines its security guarantees.

The stakes: more than just buying alcoholโ€‹

When we talk about age verification, it's easy to think of trivial use cases: buying a beer at a festival, accessing an adult website, or purchasing cigarettes. But the EU's digital identity ambitions extend far beyond these scenarios.

The Age Verification Wallet is a stepping stone toward the broader EU Digital Identity Wallet, a system designed to become the digital equivalent of your physical ID card. Imagine using it to:

  • Open a bank account
  • Sign legal contracts
  • Access healthcare records
  • Vote in elections
  • Prove your professional qualifications

The security of this foundation matters. A bypassable age verification system isn't just a problem for alcohol vendors; it's a warning sign for the entire digital identity infrastructure Europe is building.

Introductionโ€‹

The European Commission is building an Age Verification (AV) Wallet: a mobile application that allows EU citizens to prove their age using their passport, without revealing their actual birth date or identity. It's part of the broader EU Digital Identity Wallet initiative.

The concept is compelling: scan your passport with NFC, verify you're the legitimate holder via face matching, and receive a cryptographic credential that proves "I am over 18" without revealing who you are. Privacy advocates should be pleased.

But after conducting a thorough security analysis of the Android and iOS implementations, along with the Python issuer service, we found a fundamental architectural flaw: the issuer has no way to verify that the passport verification actually happened.

If this app launches in its current state, it will be trivially bypassable by anyone with basic reverse engineering skills. Let us explain why.


Understanding the architecture: a tale of two worldsโ€‹

Before diving into the technical details, let's understand the fundamental design of this system through a simple analogy.

Imagine you're applying for a job that requires a university degree. There are two ways to verify your qualification:

Approach A (Trust the applicant): You tell the employer "I have a degree" and they believe you.

Approach B (Verify independently): The employer contacts your university directly, or you provide an official transcript with verifiable signatures.

The EU Age Verification Wallet currently uses Approach A. The app performs all the verification on your phone, then simply tells the issuer "this person is over 18." The issuer has no way to confirm this claim independently.

The official solution architecture illustrates this problem:

EU Age Verification Solution Architecture

Notice how the "Age Verification App Instance" sits between the user's passport and the Attestation Provider. All passport verification, face matching, and liveness detection happen inside this app. The Attestation Provider only receives the result, not cryptographic evidence. This creates what security researchers call a trust boundary violation: the verification happens in an environment the user controls (their phone), but the result is trusted by a system that the user shouldn't be able to manipulate.

The contradiction in the specificationsโ€‹

The official technical specification contains requirements that are mutually exclusive:

Section 3.3.2 states:

"Using this approach, the AVI does not transmit any user-related personal information to the AP."

Section 4.3 requires:

"An Attestation Provider SHALL NOT issue a Proof of Age attestation before verifying the attestation subject's age at LoA substantial or high."

These two requirements cannot both be satisfied. The Attestation Provider cannot verify the user's age at a "substantial" or "high" Level of Assurance if it receives no user-related personal information. The specification essentially mandates verification without evidence.

This isn't an implementation bug. It's an architectural contradiction baked into the requirements themselves.


The illusion of privacyโ€‹

The architecture makes a compelling promise: your biometric data never leaves your device, your passport details stay local, and the issuer only learns your birth date. On paper, this sounds privacy-preserving.

But privacy without security is meaningless. If anyone can forge an age credential without possessing a valid passport, then the "privacy" of the enrollment process becomes irrelevant. The entire system produces credentials that cannot be trusted.

This is a false sense of privacy. Users believe they're participating in a secure, privacy-respecting system, when in reality they're using an infrastructure that will be exploited the moment it goes live.

The real privacy gap: no Zero-Knowledge Proofsโ€‹

The more significant privacy concern isn't during enrollment, but during verification. When you prove your age to a website or store, the current system reveals your credential to the Relying Party. This creates linkability: the same credential presented to multiple verifiers can potentially be correlated.

The solution is Zero-Knowledge Proofs (ZKPs): cryptographic techniques that let you prove "I am over 18" without revealing your credential or any other identifying information.

The specification acknowledges this:

"A next version of the Technical Specifications for Age Verification Solutions will include as an experimental feature the Zero-Knowledge Proof (ZKP) solution"

But ZKPs are explicitly not implemented. The specification states they were deferred because:

"Adding support for these features would introduce additional complexity, which could hinder the rapid adoption of the solution."

So the EU chose speed over privacy. The current implementation uses standard mdoc presentations where Relying Parties see your actual credential. To mitigate linkability, the system uses batch issuance: 30 single-use credentials are issued at once, and each credential is discarded after one use. This prevents tracking across verifications, but once all 30 are used, users must scan their passport again to obtain a fresh batch.


How to forge an age credentialโ€‹

The lack of server-side verification makes this system trivially exploitable. Here's a practical guide to the tools and techniques an attacker would use.

Alternative: Use Frida for runtime hooking without APK modification. Root the device, hook verification functions, same result.

Method 1: App modificationโ€‹

Tools required:

  • apktool: Decompile and recompile Android APKs
  • jadx: Decompile to readable Java/Kotlin code
  • Android Studio: For signing the modified APK

Attack steps:

  1. Download the APK from the Play Store or build from source
  2. Decompile with apktool d app.apk
  3. Locate the passport verification logic and birth date extraction
  4. Modify to skip verification and return a hardcoded birth date
  5. Recompile with apktool b app -o modified.apk
  6. Sign with your own key and install
  7. Request a credential (the issuer blindly trusts your claimed birth date)

Method 2: Runtime hookingโ€‹

Tools required:

  • Frida: Dynamic instrumentation toolkit
  • A rooted Android device or jailbroken iPhone
  • Magisk: For hiding root from detection

Attack steps:

  1. Root/jailbreak the device and install Frida
  2. Identify the verification functions using static analysis
  3. Write Frida scripts to intercept and modify return values:
// Hook passport verification: always return valid
Interceptor.attach(Module.findExportByName("libpassport.so", "verifyPassport"), {
onLeave: function(retval) {
retval.replace(1); // Force success
}
});

// Hook birth date extraction: return any date
Interceptor.attach(Module.findExportByName("libpassport.so", "extractBirthDate"), {
onLeave: function(retval) {
// Return 1990-01-01 instead of actual birth date
Memory.writeUtf8String(retval, "1990-01-01");
}
});
  1. Run the unmodified app with hooks active
  2. The issuer receives your forged birth date

Method 3: Protocol-level attackโ€‹

Tools required:

  • Postman or curl: For crafting HTTP requests
  • Basic understanding of OpenID4VCI
  • Any device capable of generating a key pair

Attack steps:

  1. Study the issuer's OpenID4VCI endpoints from the source code
  2. Generate your own cryptographic key pair
  3. Craft credential requests directly to the issuer API
  4. Submit any birth date you want
  5. Receive a valid, signed credential bound to your key

No passport required. No app required. No face matching required.


Why app attestation doesn't solve thisโ€‹

note

Google Play Integrity and Apple App Attest are not currently implemented in the EU Age Verification app. However, app attestation is likely to be raised as a counter-argument to this security analysis. We address it here to explain why it would not solve the fundamental trust gap.

A common response to client-side security issues is: "Just use Google Play Integrity or Apple App Attest." This fundamentally misunderstands the problem.

What app attestation can verifyโ€‹

CheckGoogle Play IntegrityApple App Attest
App is unmodifiedโœ“โœ“
Device is not rooted/jailbrokenโœ“โœ“
App was installed from official storeโœ“โœ“

What app attestation cannot verifyโ€‹

CheckGoogle Play IntegrityApple App Attest
Passport was actually scannedโœ—โœ—
Face matching was performedโœ—โœ—
Liveness detection passedโœ—โœ—
Birth date is genuineโœ—โœ—

App attestation proves the app is authentic. It does not prove the app did its job.

The protocol-level bypass remainsโ€‹

Even with mandatory app attestation, an attacker can:

  1. Use a legitimate, unmodified app on a non-rooted device
  2. Intercept the network traffic between app and issuer (via a proxy on the local network)
  3. Modify the birth_date field in transit
  4. The issuer receives tampered data from an "attested" app

Or simpler: reverse-engineer the protocol and replay requests with forged data. The attestation token proves the app existed, but it doesn't cryptographically bind the verification results to the credential request.

The privacy costโ€‹

Even if app attestation could solve the security problem (it can't), it introduces significant privacy concerns:

PlatformWhat the vendor learns
Google Play IntegrityApp usage, timing, device fingerprint, linked to Google account
Apple App AttestApp usage, obfuscated device identifier

Users of privacy-focused Android distributions like GrapheneOS, who specifically avoid Google Play Services, would be excluded entirely.

You'd be trading one set of privacy problems for another, while still not fixing the fundamental security flaw.


The technical detailsโ€‹

Passport verification: client-side onlyโ€‹

The passport verification happens in PassportNFC.kt (Android) and uses the JMRTD library:

// All verification happens locally
private fun verifyHT() { /* Hash verification */ }
private fun verifyDS() { /* Signature verification */ }
private fun verifyCS() { /* Certificate chain verification */ }

These checks verify:

  • Hash Table (HT): Data hasn't been modified
  • Document Signature (DS): Issuing authority signed the document
  • Certificate Chain (CS): Certificate chains to a trusted CSCA

All of this is good cryptography. But the results never leave the device in a verifiable form.

Face verification: easily bypassableโ€‹

The face matching uses a native SDK with thresholds configured in WalletCoreConfigImpl.kt:

faceMatchConfig = FaceMatchConfig(
livenessThreshold = 0.972017, // 97.2% confidence required
matchingThreshold = 0.5 // 50% similarity required
)

The 97.2% liveness threshold sounds strict, but it doesn't matter if an attacker can simply hook the result:

// Frida script to bypass face verification
Interceptor.attach(Module.findExportByName("libavfacelib.so", "jni_match"), {
onLeave: function(retval) {
retval.replace(1); // Always return "match successful"
}
});
Security/Privacy Tradeoff

Performing face verification and liveness detection on-device is a deliberate privacy choice: biometric data never leaves the user's phone. However, this creates a fundamental security gap: an attacker can borrow or steal someone else's passport, bypass the on-device face matching using the methods described above, and obtain a credential bound to their own device.

The issuer has no defenseโ€‹

Looking at formatter_func.py in the issuer service, we can see it simply accepts the birth date and creates a credential:

def mdocFormatter(data, credential_metadata, country, device_publickey):
# No verification of passport authenticity
# Just trusts the data and signs a credential
mdoci.new(
doctype="eu.europa.ec.av.1",
data=data, # Trusts this completely
devicekeyinfo=device_publickey,
...
)

What would actually workโ€‹

The fix: server-side passport verificationโ€‹

The issuer needs cryptographic proof that a real passport was scanned. Here's what should be sent:

data class PassportProof(
val sodFile: ByteArray, // Security Object Document
val dsCertificate: ByteArray, // Document Signing Certificate
val dg1Bytes: ByteArray, // Raw DG1 data (full MRZ)
)

The issuer would then:

  1. Verify SOD signature with the DS certificate
  2. Verify certificate chain against known CSCAs (Country Signing CAs)
  3. Hash DG1 and compare to the hash in SOD
  4. Extract birth date from the now-verified DG1

This way, a modified app cannot forge a valid passport proof. It would need to forge the passport's cryptographic signatures, which is infeasible.

The tradeoff: privacy vs securityโ€‹

Here's the tension:

ApproachPrivacySecurity
Current (all on-device)ExcellentWeak
Server-side passport verificationReducedStrong

Sending DG1 to the server reveals the full MRZ content:

  • Your name
  • Document number
  • Nationality
  • Date of birth
  • Document expiry date

It does NOT reveal your face image (stored in DG2).

The recommendation: verify and discard. The issuer verifies the cryptographic proof, extracts only the birth date, and immediately discards all other passport data without storing it.


The bigger pictureโ€‹

This isn't just about age verification. The EU Digital Identity Wallet is intended to become a cornerstone of digital identity in Europe, used for everything from opening bank accounts to accessing government services.

If the architecture trusts the app to honestly report verification results, the entire system's security depends on the assumption that no one will modify the app. In 2025, that's not a reasonable assumption.

The cryptographic building blocks are all there:

  • Passports have digital signatures (ICAO 9303)
  • The SOD contains verifiable hashes
  • Certificate chains can be validated
  • OpenID4VCI supports proof mechanisms

The app just needs to use them in a way the issuer can verify.


Who should be worried?โ€‹

Businesses relying on age verificationโ€‹

If you're a business planning to accept EU Age Verification credentials (an online gambling platform, alcohol delivery service, or adult content provider), you need to understand what you're actually getting.

A credential from the current system means: "Someone using the official app (or a modified version of it) submitted this birth date."

It does not mean: "This person proved their age using a verified passport."

The liability implications are significant. If a minor obtains alcohol using a fraudulently obtained credential, who bears responsibility? The credential issuer who trusted the app? The business who trusted the credential? The platform provider who didn't implement additional checks?

Governments and regulatorsโ€‹

The eIDAS 2.0 regulation mandates that EU member states offer digital identity wallets to citizens by 2026. These wallets will be used for accessing public services, signing documents, and proving identity across borders.

If the foundational age verification component is architecturally flawed, what does that suggest about the broader technical oversight? This isn't a criticism of individual developers; the code shows thoughtful privacy engineering. It's a systems-level concern about how security requirements are specified and verified in large government technology projects.

Privacy advocatesโ€‹

Here's an uncomfortable truth: fixing the security vulnerability likely requires compromising some privacy guarantees.

The most robust fix (sending passport cryptographic proofs to the server) reveals the full MRZ content including your name and document number. This is a meaningful privacy reduction compared to the current design where only your birth date leaves your device.

This creates a genuine tension. The privacy-first design is insecure. The secure design is less private. There may not be a perfect solution, only tradeoffs that need to be made transparently and with informed consent from citizens.


How Yivi solved this problemโ€‹

The security issues we've identified aren't theoretical; they're well-known in the digital identity community. The Yivi wallet (formerly IRMA) has already implemented passport-based credentials with a fundamentally different architecture that addresses these exact vulnerabilities.

Server-side passport validationโ€‹

Yivi's go-passport-issuer validates passport data on the server, not in the app. When a user scans their passport, the cryptographic proof is sent to the issuer for verification.

The issuer performs:

  1. Passive Authentication (PA): Verifies the passport's digital signatures against the Document Signing Certificate, then validates the certificate chain against government-issued Masterlists
  2. Active Authentication (AA): Performs a challenge-response with the passport's NFC chip to prove the physical document is present, not just a copy of its data

This means a modified app cannot forge credentials. Even if an attacker controls the entire app, they cannot produce valid cryptographic signatures from a passport they don't possess.

Zero-Knowledge Proofs with Idemixโ€‹

While the EU Age Verification system deferred ZKPs as "too complex," Yivi has shipped them. Using Idemix credentials, Yivi enables:

  • Unlinkable disclosures: Each presentation is cryptographically unlinkable to previous ones
  • Predicate proofs: Prove "I am over 18" without revealing your birth date
  • Selective disclosure: Share only the attributes needed for each transaction

This is what privacy-preserving age verification actually looks like.

The architectural differenceโ€‹

ComponentEU Age VerificationYivi
Passport signature verificationClient-side onlyServer-side
Masterlist validationNot implementedGovernment masterlists
Active AuthenticationNot usedChallenge-response with chip
Zero-Knowledge Proofs"Future experimental feature"Shipped (Idemix)
Credential formatmdoc (linkable)Idemix (unlinkable) or SD-JWT VC
ProtocolOpenID4VCIOpenID4VP + IRMA protocol

Open source and battle-testedโ€‹

All Yivi components are open source and have been in production for years:

The passport credential feature has been publicly available since 2025, with support for passports, ID-cards, and driver's licenses validated against Dutch and German government Masterlists.

The solution has been audited and penetration tested by Radically Open Security, an independent security firm, with no major findings.


Conclusionโ€‹

Member states cannot adopt the EU Age Verification Wallet in its current form. The architectural flaws we've documented aren't edge cases; they're fundamental. Any system that relies on client-side verification without cryptographic proof to the server will be bypassed within days of deployment. The tools are freely available, the attack vectors are well-documented, and the incentives for fraud are enormous.

This isn't speculation. It's how mobile security works.

The JMRTD problemโ€‹

The EU Age Verification app relies on JMRTD for passport reading, a library that is poorly maintained and hasn't kept pace with the evolving landscape of Machine Readable Travel Documents. Security vulnerabilities go unpatched, compatibility issues with newer passport formats persist, and the project lacks the active development needed for critical infrastructure.

Yivi offers a working alternativeโ€‹

Member states looking for a secure, privacy-preserving age verification solution can adopt Yivi today. We maintain production-ready MRTD infrastructure that:

  • Supports all EU countries: validated against government-issued Masterlists
  • Is actively maintained: security updates, compatibility fixes, and new features ship regularly
  • Implements proper security: server-side validation with Passive and Active Authentication
  • Delivers real privacy: Zero-Knowledge Proofs via Idemix for unlinkable disclosures
  • Is fully open source: auditable, extensible, and free from vendor lock-in

The passport credential feature has been in production since 2025, supporting passports, ID-cards, and driver's licenses. The infrastructure exists. The code is battle-tested. Member states don't need to wait for the EU to fix its architecture; they can deploy a working solution now.

Digital identity infrastructure, once deployed at scale, becomes extremely difficult to change. The decisions made now will echo for decades. We urge member states to choose security over expediency.


A note on responsible disclosureโ€‹

This analysis is based entirely on publicly available source code published on GitHub by the EU Digital Identity Wallet project. The repositories are open for review, which is commendable. Transparency in government technology builds trust and enables community oversight.

We've chosen to publish this analysis publicly for several reasons:

  1. The code is already public. Anyone with security expertise can identify these issues by reviewing the repositories. Publishing an analysis doesn't reveal anything that isn't already accessible.

  2. Time pressure matters. According to EU timelines, member states should offer digital wallets by 2026. If fundamental architectural issues aren't addressed before deployment, they become much harder to fix afterward.

  3. Public scrutiny improves security. The security community has a long history of improving software through open analysis. The goal isn't to embarrass the development team, but to help build a more robust system.

  4. Citizens deserve to understand the systems that affect them. Digital identity infrastructure will become a critical part of European civic life. Transparency about its security properties is a public interest matter.

We hope the EU Digital Identity Wallet team views this analysis as constructive input. The architectural changes recommended here are well within their capabilities to implement; the cryptographic building blocks already exist in the codebase.


What you can doโ€‹

If you're a security researcher: Review the code yourself. File issues on the GitHub repositories. The more eyes on this codebase, the better.

If you're a policymaker: Ask questions about the security review process for government digital identity projects. Ensure that independent security audits are part of the deployment requirements.

If you're a citizen: Be aware that digital identity credentials, like physical documents, can potentially be forged if systems aren't properly designed. Ask questions about how the credentials you're asked to trust are verified.

If you're a business: Don't assume that any digital credential provides absolute assurance. Understand the verification chain and implement additional checks where the stakes are high.


Referencesโ€‹

EU Age Verificationโ€‹

Yivi / IRMAโ€‹

Standards & Toolsโ€‹

Security Toolsโ€‹

Platform Securityโ€‹


This analysis is based on the publicly available source code as of March 2026. The findings are intended to help improve the security of the EU Digital Identity ecosystem.