This guide explains how Relying Parties (RPs) and reader developers can implement in-person (offline) verification of digital credentials presented from Google Wallet according to the international **ISO/IEC 18013-5** standard.

Digital credentials in Google Wallet can be securely verified in physical environments (such as point-of-sale terminals, event venues, transit gates, law enforcement readers, and mobile reader apps) without requiring an active internet connection at the time of presentation.

> [!NOTE]
> **No allowlisting required for mDL (`org.iso.18013.5.1.mDL`):**
>
> - **Works out of the box with ISO/IEC 18013-5 terminals:** You don't need to wait for Google allowlisting or complete an onboarding intake form to request mobile Driver's Licenses (mDLs) in person. If your reader terminal or verifier app already supports standard **ISO/IEC 18013-5 mdoc retrieval** (NFC tap or QR code device engagement with BLE data transport) and validates responses against trusted [IACA root certificates](https://developers.google.com/wallet/identity/verify/supported-issuers-iaca-certs), it can accept mDLs from Google Wallet immediately.
> - **ID passes are not currently supported in person:** Google Wallet **ID passes** (`com.google.wallet.idcard.1` and `org.iso.23220.photoid.1`) are currently supported for [online presentation](https://developers.google.com/wallet/identity/verify/choose-your-path) only and are not supported for in-person presentation.

## Overview of Offline Presentation (ISO/IEC 18013-5)

The ISO/IEC 18013-5 standard defines a standardized, interoperable protocol for offline presentation between a **Holder** (the user's mobile device running Google Wallet) and a **Reader / Verifier** (a physical terminal or companion mobile app).

The presentation flow occurs in distinct phases:

1. **Device Engagement:** The reader and wallet establish initial contact via **NFC Tap** (Static or Negotiated Handover) or **QR Code scan** . During this phase, device engagement metadata and the reader's ephemeral public key (`EReaderKey`) are exchanged.
2. **Data Transport Connection:** A secure, encrypted **Bluetooth Low Energy (BLE)** channel is negotiated (with the reader acting in Central Client or Peripheral Server mode).
3. **Device Request:** The reader transmits a CBOR-encoded `DeviceRequest` specifying the requested document type (such as `org.iso.18013.5.1.mDL`) and the specific namespaces and data elements requested.
4. **User Consent \& Device Authentication:** Google Wallet prompts the user to review the requested data elements and confirm sharing using biometric authentication or screen lock.
5. **Device Response \& Cryptographic Verification:** The wallet sends back a CBOR-encoded `DeviceResponse` containing the signed Mobile Security Object (MSO) and device-signed data elements. The reader verifies the cryptographic signatures against trusted root certificates.

[Video](https://www.youtube.com/watch?v=aIrXNm2XKH4)

## The Multipaz Open-Source SDK

To implement a reader or verifier application, Google recommends using **[Multipaz](https://github.com/openwallet-foundation/multipaz)** , an open-source Kotlin Multiplatform (KMP) SDK originally developed by Google and contributed to the **[OpenWallet Foundation (OWF)](https://openwallet.foundation/)**.

Multipaz provides a production-ready implementation of ISO/IEC 18013-5 reader and wallet protocols, cryptographic verification pipelines, CBOR encoding/decoding, and extensible document type schemas.

> [!NOTE]
> **Developer Resources:**
>
> - **GitHub Repository:** [openwallet-foundation/multipaz](https://github.com/openwallet-foundation/multipaz)
> - **Documentation:** [developer.multipaz.org](https://developer.multipaz.org)
> - **Web Verifier Tool:** [verifier.multipaz.org](https://verifier.multipaz.org)

## Integrating Multipaz into Your Reader App

The following steps demonstrate how to integrate the Multipaz SDK into an Android reader application.

### Step 1: Add Dependencies

Multipaz libraries are published on **Maven Central** . Add the required modules to your application's `build.gradle.kts` file:

    // build.gradle.kts
    dependencies {
        // Core Multipaz library (protocol engine, CBOR, crypto)
        implementation("org.multipaz:multipaz:0.100.0")

        // Android-specific platform bindings (NFC, BLE, Keystore)
        implementation("org.multipaz:multipaz-android:0.100.0")

        // Standardized document types (mDL, EU PID, etc.)
        implementation("org.multipaz:multipaz-doctypes:0.100.0")
    }

### Step 2: Configure Android Permissions

In-person verification requires hardware permissions for NFC engagement, camera scanning (for QR engagement), and Bluetooth Low Energy data transport. Add the following permissions to your `AndroidManifest.xml`:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <!-- NFC Engagement -->
        <uses-permission android:name="android.permission.NFC" />
        <uses-feature android:name="android.hardware.nfc" android:required="false" />

        <!-- Camera for QR Code Engagement -->
        <uses-permission android:name="android.permission.CAMERA" />
        <uses-feature android:name="android.hardware.camera" android:required="false" />

        <!-- Bluetooth Low Energy Transport (Android 12+) -->
        <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
                         android:usesPermissionFlags="neverForLocation" />
        <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
        <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

        <!-- Legacy Bluetooth Permissions for Android 11 and lower -->
        <uses-permission android:name="android.permission.BLUETOOTH"
                         android:maxSdkVersion="30" />
        <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
                         android:maxSdkVersion="30" />
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
                         android:maxSdkVersion="30" />
    </manifest>

**Android 12+ Runtime Permissions:** On Android 12 (API level 31) and higher, you must prompt the user for `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, and `BLUETOOTH_ADVERTISE` permissions at runtime before initiating device engagement.

### Step 3: Initialize the Verification Engine

Use Multipaz's `VerificationHelper` or Reader connection abstractions to handle engagement events and manage the BLE communication lifecycle:

    import org.multipaz.verification.VerificationHelper
    import org.multipaz.cbor.Cbor
    import org.multipaz.crypto.Crypto

    class ReaderManager(private val context: android.content.Context) {

        private var verificationHelper: VerificationHelper? = null

        fun startListeningForEngagement() {
            verificationHelper = VerificationHelper.Builder(
                context = context,
                listener = object : VerificationHelper.Listener {
                    override fun onDeviceConnected() {
                        // BLE channel established, send request
                        sendDeviceRequest()
                    }

                    override fun onResponseReceived(deviceResponseBytes: ByteArray) {
                        // Process and verify the received credential payload
                        handleDeviceResponse(deviceResponseBytes)
                    }

                    override fun onError(error: Throwable) {
                        // Handle transport or protocol errors
                    }

                    override fun onDeviceDisconnected(transportTransportSpecificTermination: Boolean) {
                        // Connection closed
                    }
                }
            ).build()
        }
    }

### Step 4: Construct the DeviceRequest

Specify the document type and individual data elements your application needs to verify. Always apply the principle of **minimal disclosure** (e.g., requesting only `age_over_21` rather than full `birth_date` when verifying age):

    fun sendDeviceRequest() {
        // Specify the DocType and requested elements
        val docType = "org.iso.18013.5.1.mDL"
        val namespace = "org.iso.18013.5.1"

        // Map of requested elements: elementName -> intentToRetain
        val requestedElements = mapOf(
            "family_name" to false,
            "given_name" to false,
            "age_over_21" to false,
            "portrait" to false,
            "driving_privileges" to false
        )

        // Build ISO/IEC 18013-5 DeviceRequest structure
        val deviceRequestBytes = verificationHelper?.buildDeviceRequest(
            docType = docType,
            itemsToRequest = mapOf(namespace to requestedElements)
        )

        if (deviceRequestBytes != null) {
            verificationHelper?.sendDeviceRequest(deviceRequestBytes)
        }
    }

## Supporting Additional Document Types (DocTypes)

Multipaz supports standardized credentials out-of-the-box and provides an extensible architecture to request custom or domain-specific document types.

### 1. Built-in Standard Document Types

The `multipaz-doctypes` library provides predefined schema models for standard credentials:

| Document Type Identifier | Standard / Scope | Typical Namespace | Common Elements |
|---|---|---|---|
| `org.iso.18013.5.1.mDL` | ISO/IEC 18013-5 Mobile Driver's License | `org.iso.18013.5.1` | `family_name`, `given_name`, `birth_date`, `issue_date`, `expiry_date`, `issuing_authority`, `document_number`, `portrait`, `driving_privileges`, `age_over_18`, `age_over_21` |
| `eu.europa.ec.eudi.pid.1` | EU Digital Identity Wallet (EUDIW) Person Identification Data | `eu.europa.ec.eudi.pid.1` | `family_name`, `first_name`, `birth_date`, `nationality`, `issuing_country`, `issuing_authority`, `personal_administrative_number` |
| `com.google.wallet.idcard.1` | Google Wallet ID Pass / Test IDs | `com.google.wallet.idcard.1` | `given_name`, `family_name`, `birth_date`, `document_number`, `portrait` |

### 2. Requesting Custom Document Types

To request custom document types, define the target `docType` string and corresponding namespace mappings when constructing the `DeviceRequest`:

    // Example: Requesting a custom event ticket credential
    val customDocType = "com.example.events.ticket"
    val customNamespace = "com.example.events.ticket.1"

    val customRequestedItems = mapOf(
        "ticket_id" to false,
        "event_name" to false,
        "seat_section" to false,
        "vip_access" to false
    )

    val multiDocRequestBytes = verificationHelper?.buildMultiDocDeviceRequest(
        documents = listOf(
            DocumentRequest(
                docType = customDocType,
                namespaces = mapOf(customNamespace to customRequestedItems)
            )
        )
    )

## Cryptographic Verification \& Trust Management

Receiving a response payload is only the first step. Readers **must** perform a four-step cryptographic verification to validate the authenticity and integrity of the presented credential.

| Verification Step | Verification Target |
|---|---|
| **1. Issuer Authentication** | Verify `IssuerAuth` (`COSE_Sign1`) against trusted IACA root certificates |
| **2. Validity Window Check** | Ensure `validFrom` ≤ current time ≤ `validUntil` |
| **3. Data Integrity Check** | Compute SHA-256 digests of returned elements and match against MSO `ValueDigests` |
| **4. Device Authentication** | Verify `DeviceSigned` signature or MAC using the `DeviceKey` bound to the `SessionTranscript` |

### 1. The 4-Step Verification Pipeline

1. **Issuer Authentication (`IssuerAuth`):**
   - The Mobile Security Object (MSO) is signed by the Issuing Authority (`IssuerAuth` payload).
   - The reader verifies the `COSE_Sign1` signature using the Document Signer certificate and ensures the certificate chains up to a trusted **Issuing Authority CA (IACA)** root certificate.
2. **Validity Window Verification:**
   - The reader checks `validityInfo.validFrom` and `validityInfo.validUntil` timestamps in the MSO against the reader's current clock to ensure the credential is not expired.
3. **Data Integrity Verification (`ValueDigests`):**
   - For every received `IssuerSignedItem`, the reader computes its digest (e.g. SHA-256) and verifies that it matches the corresponding hash entry in the MSO's `ValueDigests` dictionary.
4. **Device Authentication (`DeviceSigned`):**
   - The reader validates that the device presenting the credential holds the private key corresponding to the `DeviceKey` published inside the signed MSO.
   - This is accomplished by verifying the `DeviceAuth` (either `DeviceSignature` or `DeviceMac`) over the `SessionTranscript`, binding the session to the reader's ephemeral key and preventing replay and man-in-the-middle attacks.

### 2. Managing Trusted IACA Root Certificates

Production readers must maintain a secure, local trust store containing trusted IACA root certificates:

- **Production IACA Certificates:** Download and configure root certificates from official issuing authorities. Refer to our [Supported Issuers and IACA Certificates](https://developers.google.com/wallet/identity/verify/supported-issuers-iaca-certs) list.
- **AAMVA VICAL:** For US jurisdictions, reader systems can integrate with the American Association of Motor Vehicle Administrators (AAMVA) Verified Issuer Certificate Authority List (VICAL) service to automatically synchronize state trust anchors.
- **Sandbox Testing Roots:** When testing against sandbox credentials, ensure the reader trusts the [Google Sandbox IACA Root](https://developers.google.com/wallet/identity/verify/sandbox).

    import org.multipaz.crypto.X509Cert

    // Configure trusted IACA certificates in the trust store
    val trustedCertificates = mutableListOf<X509Cert>()

    // Add official state IACA certificates
    trustedCertificates.add(X509Cert.fromPem(sampleStateIacaPem))

    // Add Google Sandbox IACA root certificate for testing
    trustedCertificates.add(X509Cert.fromPem(googleSandboxIacaPem))

    val verifier = MultipazVerifier(trustStore = trustedCertificates)
    val verificationResult = verifier.verify(deviceResponseBytes, sessionTranscript)

    if (verificationResult.isIssuerAuthorized && verificationResult.isDeviceAuthenticated) {
        // Credential is valid and authentic
    } else {
        // Reject presentation: cryptographic validation failed
    }

## Reader Authentication (Recommended)

Reader Authentication allows the reader application to cryptographically prove its identity to Google Wallet by signing the `ReaderAuthentication` structure using an authorized X.509 reader certificate.

- **Why It Is Recommended:** Reader Authentication allows your reader app or terminal to present a trusted identity to the user. While optional for basic public attributes (e.g., verifying `age_over_21`), it is strongly recommended for reader integrations to increase user trust and may be legally or policy-required when requesting sensitive attributes (such as full Social Security Number, residential address, or specific state endorsements).
- **How It Works:** The reader includes its certificate chain and signs the session transcript. Google Wallet displays the verified identity and organizational name of the reader to the user on the consent screen prior to data release.

## Testing \& Development Tools

To accelerate integration, use the following developer tools and reference implementations:

1. **Multipaz Reference Apps:**
   - Clone the **[Multipaz repository](https://github.com/openwallet-foundation/multipaz)** and run the `IdentityReader` Android sample app to test physical verification flows.
2. **Create a Test ID in Google Wallet:**
   - Follow our [Create a Test ID Pass in Google Wallet](https://developers.google.com/wallet/identity/verify/create-test-id) guide to provision a simulated test credential into Google Wallet using the Utopia ePassport Simulator.
3. **Web-Based Verifier Testing:**
   - Use [verifier.multipaz.org](https://verifier.multipaz.org) to inspect CBOR requests, explore claim queries, and test W3C / ISO 18013-7 web-based presentations.

## Troubleshooting \& Field Diagnostics

The following table lists common issues encountered during offline verification and recommended resolutions:

| Issue / Symptom | Root Cause | Recommended Resolution |
|---|---|---|
| **BLE connection timeout / Failure to connect** | - RF interference in high-density environments. - Peripheral vs. Central mode incompatibility on specific reader hardware. - Scanning timeouts. | - Ensure the reader supports both BLE Central Client and Peripheral Server modes. - Adjust BLE scan window and interval for aggressive scanning during active engagement. - Verify MTU size negotiation completes successfully. |
| **NFC tap engagement fails or drops** | User pulls mobile device away from the reader antenna before the BLE handover record is fully transferred. | - Provide immediate visual/audio/haptic feedback on the terminal as soon as NFC engagement starts. - Instruct users to hold the phone steady against the NFC target until the BLE connection is established. |
| **`UNTRUSTED_ISSUER` / Certificate chain failure** | The Document Signer certificate does not chain to any trusted IACA certificate in the reader's local trust store. | - Check that the issuer's root certificate is loaded into the reader trust store. - If testing in sandbox, verify that the [Google Sandbox IACA Root](https://developers.google.com/wallet/identity/verify/sandbox) is loaded. - Ensure IACA certificate lists (e.g. AAMVA VICAL) are updated periodically. |
| **`INVALID_VALIDITY_INFO` / Expired MSO** | - Reader system clock is out of sync. - The MSO signature has expired. | - Ensure the reader device synchronizes its system time regularly via NTP. - Prompt the user to open Google Wallet while connected to the internet to refresh credential tokens. |
| **`DEVICE_AUTHENTICATION_FAILED`** | Session transcript mismatch between reader and wallet, or invalid ephemeral device signature. | - Ensure the exact raw bytes of `DeviceEngagementBytes` and `EReaderKeyBytes` are preserved in the `SessionTranscript` structure without re-encoding. |
| **Android 12+ permission crash** | Application attempted to scan or advertise over BLE without runtime permissions. | - Check and request `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, and `BLUETOOTH_ADVERTISE` at runtime before starting reader sessions. |

## UX \& Privacy Guidelines for In-Person Readers

When designing physical readers and companion apps:

- **Practice Selective Disclosure in the UI:** Only display the decision or minimum required attribute to the operator (e.g. show a prominent green checkmark and "**Age 21+ Verified**" rather than displaying the user's full date of birth, address, and license number).
- **Clear Physical Interaction Indicators:** Label the NFC target zone clearly and display visual cues (such as animations or progress bars) showing each stage: *Tap / Scan → Connecting → Verifying → Complete*.
- **Ephemeral Data Handling:** Do not store or log personal data elements received from the wallet unless explicitly required by applicable law and disclosed via `intentToRetain = true`.