In-person Acceptance of Digital Credentials (Offline)

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.

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.

The Multipaz Open-Source SDK

To implement a reader or verifier application, Google recommends using Multipaz, an open-source Kotlin Multiplatform (KMP) SDK originally developed by Google and contributed to the OpenWallet Foundation (OWF).

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.

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>

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 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.
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 and run the IdentityReader Android sample app to test physical verification flows.
  2. Create a Test ID in Google Wallet:
  3. Web-Based Verifier Testing:
    • Use 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 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.