Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Installing the Age Verification App on Android Studio

What is the Age Verification App?

The Age Verification (AV) App is the official wallet application for the EU Age Verification Solution, developed under the EU Digital Identity framework. It is purpose-built for age verification only, implementing the EU Age Verification Profile (Annex A).

The AV App allows users to:

  • Obtain Proof of Age attestations — via document scanning (NFC passport) or online issuance
  • Present proof of age — to Relying Parties requesting age verification
  • Preserve privacy — only reveals whether the user meets an age threshold, not their exact birth date

The AV App is a fork of the EUDI Wallet but configured specifically for the Annex A profile, with different trust settings, client ID schemes, and URL schemes.

The AV App Android source code is open source and available on GitHub.

Important: Age Verification App vs. EUDI Wallet

These are two separate wallet applications with fundamentally different profiles:

FeatureAge Verification AppEUDI Wallet
ProfileAnnex A (Age Verification)HAIP (High Assurance)
Client ID Schemeredirect_urix509_san_dns, x509_hash
JAR (Signed Requests)Not requiredRequired (JWT with x5c header)
URL Schemesav://, avsp://, openid4vp://eudi-openid4vp://, openid4vp://
CredentialsProof of Age onlyPID, mDL, various
Response Modedirect_postdirect_post.jwt
LoASubstantialHigh
PAR / DPoPDisabledSupported

If your Relying Party targets the EUDI Wallet using x509_hash (or legacy x509_san_dns) with signed JARs, see the EUDI Wallet guide instead.

ewQwe Demo Setup

This section describes how to run the AV App together with the ewQwe Relying Party Demo Webapp for end-to-end Annex A testing on a local Android emulator.

Repository: https://github.com/eu-digital-identity-wallet/av-app-android-wallet-ui

Prerequisites

Tip: Use the same EUDI_Dev_Device emulator as the HAIP wallet. Both wallet apps can be installed simultaneously on the same emulator, and demo.ewqwe.local will already be mapped to the host machine if you previously ran ./start_ewqwe_eudi_emulator.sh from the HAIP repo. See the EUDI Wallet ewQwe Demo Setup for emulator setup instructions.

Step 1: Clone the Repository

git clone https://github.com/eu-digital-identity-wallet/av-app-android-wallet-ui.git
cd av-app-android-wallet-ui

Step 2: Build and Run the App

  1. Open the project in Android Studio
  2. Select the app module and the EUDI_Dev_Device emulator (or any emulator with demo.ewqwe.local mapped)
  3. Select the devDebug build variant (Build → Select Build Variant)
  4. Click Run (▶️) to deploy and start the AV App

Step 3: Initialize Documents

Once the app is running:

  1. Follow the on-screen prompts to create a PIN code
  2. Obtain a Proof of Age credential from the dev issuer (https://test.issuer.dev.ageverification.dev)

Step 4: Open the Relying Party Demo Webapp

  1. Open Chrome on the Android emulator
  2. Navigate to https://demo.ewqwe.local:5174
  3. Proceed past the certificate warning (expected — the demo uses a self-signed certificate)
  4. The Demo Webapp should load

Step 5: Request Proof of Age

  1. From the webapp, select “Proof of Age” (Annex A profile) and initiate a credential request
  2. This displays a QR code or deep link that opens the AV App
  3. Thanks to the TLS trust bypass, the wallet accepts the self-signed certificate and proceeds
  4. Approve the credential sharing in the wallet
  5. The webapp displays the verified age verification result

Security Bypass (Technical Details)

The ewQwe fork adds a custom HttpClient configuration in NetworkModule.kt that trusts all TLS certificates and skips hostname verification:

val trustAllCerts = arrayOf<TrustManager>(
    object : X509TrustManager {
        override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
        override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
        override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
    }
)
// HttpClient configured with sslManager using trustAllCerts + HostnameVerifier { _, _ -> true }

The Annex A profile uses redirect_uri as client ID scheme — no x5c certificate chain or Reader Trust Store is involved, so no reader trust bypass is needed (unlike the HAIP wallet).


Quick Start: Download APK Directly (No Build Required)

The easiest way to install the AV App is to download the pre-built APK:

  1. Install Android Studio and create an emulator (see EUDI Wallet guide — Setting Up an Android Emulator)
  2. Enable Developer Mode on the emulator (see EUDI Wallet guide — Enabling Developer Mode)
  3. Open Chrome on the emulator
  4. Navigate to AV App Releases
  5. Download the latest APK (e.g., app-dev-debug.apk)
  6. Open the downloaded file and tap Install

Note: Use the dev variant for testing with the development infrastructure, or demo for the demo environment. Both flavors support redirect_uri client ID scheme.


Annex A Profile: What Your RP Needs to Know

The AV App implements the Annex A profile, which is significantly simpler than the HAIP profile used by the EUDI Wallet. Here are the key implications for your Relying Party:

Client ID Scheme: redirect_uri

Per Annex A Section A.5, the client identifier scheme MUST be redirect_uri:

“The client identifier scheme MUST be redirect_uri followed by the response_uri

This means:

  • No signed JAR required — the Authorization Request is sent as plain query parameters
  • No x5c certificate chain — the wallet identifies the RP by its redirect URI, not a certificate
  • No Reader Trust Store modification — you do not need to add your CA to the wallet’s trust store
  • Simpler implementation — no JWT signing infrastructure needed

The client_id in your Authorization Request must be the literal redirect_uri: prefix followed by the response_uri:

client_id=redirect_uri:https://your-rp.example.com/openid4vp/callback
response_uri=https://your-rp.example.com/openid4vp/callback

URL Schemes for Same-Device Flow

The AV App registers the following URL schemes for deep-linking (same-device flow):

SchemeVariablePurpose
av://AV_SCHEMEPrimary AV App scheme
avsp://AVSP_SCHEMEAV Service Provider scheme
openid4vp://OPENID4VP_SCHEMEStandard OpenID4VP scheme
eudi-openid4vp://EUDI_OPENID4VP_SCHEMEEUDI-compatible scheme
mdoc-openid4vp://MDOC_OPENID4VP_SCHEMEmDoc presentation scheme

For same-device flows, construct a deep link URL using one of these schemes:

av://?client_id=redirect_uri:https://rp.example.com/cb&response_uri=https://rp.example.com/cb&...

Response Mode: direct_post

The AV App uses response_mode=direct_post (not direct_post.jwt as in HAIP). The wallet POSTs the VP Token directly to the response_uri as plain form parameters — not wrapped in a JWT.

Credential Format

The AV App works with:

  • Document type: eu.europa.ec.av.1 (Age Verification namespace)
  • Credential format: mDoc (ISO 18013-5 / CBOR encoded)
  • Key claim: age_over_18 (boolean)

PAR and DPoP: Disabled

Per the Annex A profile, both Pushed Authorization Requests (PAR) and DPoP are explicitly disabled:

  • parUsage = NEVER
  • useDPoPIfSupported = false / DPoPUsage.Disabled

Building from Source

The AV App build process is identical to the EUDI Wallet. Follow the same Android Studio setup guide, but clone the AV App repository instead:

git clone https://github.com/eu-digital-identity-wallet/av-app-android-wallet-ui.git
cd av-app-android-wallet-ui

Build Variants (Flavors)

The AV App has two build flavors:

FlavorIssuer URLPurpose
devhttps://test.issuer.dev.ageverification.devDevelopment / testing
demohttps://issuer.ageverification.devDemo environment

Build and install:

# Dev flavor
./gradlew :app:installDevDebug

# Demo flavor
./gradlew :app:installDemoDebug

AV App Configuration

The AV App configuration is centralized in WalletCoreConfigImpl.kt, with one implementation per flavor:

  • core-logic/src/dev/java/eu/europa/ec/corelogic/config/WalletCoreConfigImpl.kt
  • core-logic/src/demo/java/eu/europa/ec/corelogic/config/WalletCoreConfigImpl.kt

OpenID4VP Configuration (Default)

Both flavors are pre-configured for Annex A:

configureOpenId4Vp {
    withClientIdSchemes(
        listOf(
            ClientIdScheme.RedirectUri  // Annex A mandated scheme
        )
    )
    withSchemes(
        listOf(
            BuildConfig.OPENID4VP_SCHEME,      // openid4vp://
            BuildConfig.EUDI_OPENID4VP_SCHEME,  // eudi-openid4vp://
            BuildConfig.MDOC_OPENID4VP_SCHEME,  // mdoc-openid4vp://
            BuildConfig.AVSP_SCHEME,            // avsp://
            BuildConfig.AV_SCHEME               // av://
        )
    )
    withFormats(
        Format.MsoMdoc.ES256
    )
}

Using the Preregistered Client Scheme (Optional)

If your verifier requires a pre-registered client ID (e.g., for self-signed certificate setups), add the Preregistered scheme to the configuration:

const val OPENID4VP_VERIFIER_API_URI = "https://your-verifier.example.com"
const val OPENID4VP_VERIFIER_LEGAL_NAME = "Your Verifier"
const val OPENID4VP_VERIFIER_CLIENT_ID = "your-client-id"

configureOpenId4Vp {
    withClientIdSchemes(
        listOf(
            ClientIdScheme.Preregistered(
                listOf(
                    PreregisteredVerifier(
                        clientId = OPENID4VP_VERIFIER_CLIENT_ID,
                        verifierApi = OPENID4VP_VERIFIER_API_URI,
                        legalName = OPENID4VP_VERIFIER_LEGAL_NAME
                    )
                )
            )
        )
    )
}

Working with Self-Signed Certificates

If your verifier or issuer uses self-signed certificates (development only), see the official AV App configuration guide for instructions on configuring a custom HttpClient that trusts all certificates.

⚠️ Security Warning: Trusting all certificates disables TLS verification and must never be used in production.

Passport Scanning Issuance

The AV App supports issuing age verification credentials by scanning a physical passport via NFC. This requires a second issuer configuration:

override val passportScanningIssuerConfig: OpenId4VciManager.Config =
    OpenId4VciManager.Config.Builder()
        .withIssuerUrl(issuerUrl = "https://passport.issuer.dev.ageverification.dev")
        .withClientAuthenticationType(
            OpenId4VciManager.ClientAuthenticationType.None(
                clientId = "wallet-dev"
            )
        )
        .withAuthFlowRedirectionURI(BuildConfig.ISSUE_AUTHORIZATION_DEEPLINK)
        .withParUsage(OpenId4VciManager.Config.ParUsage.NEVER)
        .withDPoPUsage(OpenId4VciManager.Config.DPoPUsage.Disabled)
        .build()

The passport scanning flow includes face liveness detection and face matching, configured via faceMatchConfig.

Viewing Logs

Logs from the AV App can be viewed using Logcat in Android Studio, identical to the EUDI Wallet. See Viewing EUDI Wallet Logs for instructions.

Filter by the EudiWallet tag to see wallet-core events:

tag:EudiWallet

Key Differences from the EUDI Wallet (Summary)

AspectAV App (Annex A)EUDI Wallet (HAIP)
Trust modelNo trust lists needed; redirect_uri identifies the RPRequires root CA in Reader Trust Store
Request signingPlain query parametersSigned JAR with x5c certificate chain
Certificate requirementsStandard HTTPS (any CA)Specific CA must be trusted by wallet
Implementation complexityLow — no JWT signing neededHigh — requires PKCS#8 keys, x5c chain
Credential typeseu.europa.ec.av.1 (age verification)org.iso.18013.5.1.mDL, PID, various
Privacy featuresAge threshold only (age_over_18)Selective disclosure of any attribute
ZKP supportYes (Longfellow circuits)Not in current reference implementation
DC APIEnabledEnabled

References

Official AV App Documentation

Age Verification Specifications

Issuer Services