线下接受数字凭证

本指南介绍了依赖方 (RP) 和读取器开发者如何根据国际 ISO/IEC 18013-5 标准,对 Google 钱包中出示的数字凭证进行现场(离线)验证。

Google 钱包中的数字凭证可在实体环境(例如销售终端、活动场地、公交闸机、执法机构读取器和移动读取器应用)中安全验证,出示时无需连接到互联网。

离线呈现 (ISO/IEC 18013-5) 概览

ISO/IEC 18013-5 标准定义了一种标准化的可互操作协议,用于在持有者(运行 Google 钱包的用户移动设备)和读取器 / 验证器(实体终端或配套移动应用)之间进行离线出示。

演示流程分为以下几个阶段:

  1. 设备互动:读卡器和钱包通过触碰 NFC(静态或协商切换)或扫描二维码建立初始联系。在此阶段,系统会交换设备互动元数据和读取器的临时公钥 (EReaderKey)。
  2. 数据传输连接:协商建立安全的加密低功耗蓝牙 (BLE) 通道(读取器在中央客户端或外围服务器模式下运行)。
  3. 设备请求:读取器传输采用 CBOR 编码的 DeviceRequest,其中指定了所请求的文档类型(例如 org.iso.18013.5.1.mDL)以及所请求的特定命名空间和数据元素。
  4. 用户同意和设备身份验证:Google 钱包会提示用户查看请求的数据元素,并使用生物识别验证或屏幕锁定确认共享。
  5. 设备响应和加密验证:钱包会发回一个 CBOR 编码的 DeviceResponse,其中包含已签名的移动安全对象 (MSO) 和设备签名的数据元素。读取器会根据可信根证书验证加密签名。

Multipaz 开源 SDK

如需实现读取器或验证器应用,Google 建议使用 Multipaz,这是一款开源 Kotlin Multiplatform (KMP) SDK,最初由 Google 开发,并贡献给了 OpenWallet Foundation (OWF)。

Multipaz 提供了可用于生产用途的 ISO/IEC 18013-5 读取器和钱包协议实现、加密验证流水线、CBOR 编码/解码以及可扩展的文档类型架构。

将 Multipaz 集成到您的读取器应用中

以下步骤演示了如何将 Multipaz SDK 集成到 Android 读取器应用中。

第 1 步:添加依赖项

Multipaz 库发布在 Maven Central 上。将所需模块添加到应用的 build.gradle.kts 文件中:

// 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")
}

第 2 步:配置 Android 权限

当面验证需要硬件权限,以便进行 NFC 互动、摄像头扫描(用于二维码互动)和低功耗蓝牙数据传输。将以下权限添加到 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>

第 3 步:初始化验证引擎

使用 Multipaz 的 VerificationHelper 或 Reader 连接抽象来处理互动事件并管理 BLE 通信生命周期:

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()
    }
}

第 4 步:构建 DeviceRequest

指定应用需要验证的文件类型和各个数据元素。始终遵循最小披露原则(例如,在验证年龄时,仅请求 age_over_21,而非完整的 birth_date):

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)
    }
}

支持其他文档类型 (DocType)

Multipaz 开箱即支持标准化凭证,并提供可扩展的架构来请求自定义或特定于网域的文档类型。

1. 内置标准文档类型

multipaz-doctypes 库为标准凭证提供了预定义的架构模型:

文档类型标识符 标准 / 范围 典型命名空间 常见元素
org.iso.18013.5.1.mDL ISO/IEC 18013-5 数字驾照 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 欧盟数字身份钱包 (EUDIW) 个人身份数据 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 钱包身份证件 / 测试身份证件 com.google.wallet.idcard.1 given_name、family_name、birth_date、document_number、portrait

2. 请求自定义文档类型

如需请求自定义文档类型,请在构建 DeviceRequest 时定义目标 docType 字符串和相应的命名空间映射:

// 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)
        )
    )
)

加密验证和信任管理

接收响应载荷只是第一步。读取器必须执行四步加密验证,以验证所出示凭证的真实性和完整性。

验证步骤 验证目标
1. Issuer Authentication 根据可信 IACA 根证书验证 IssuerAuth (COSE_Sign1)
2. 有效期检查 确保 validFrom ≤ 当前时间 ≤ validUntil
3. 数据完整性检查 计算返回元素的 SHA-256 摘要,并与 MSO ValueDigests 进行匹配
4. 设备身份验证 使用绑定到 SessionTranscript 的 DeviceKey 验证 DeviceSigned 签名或 MAC

1. 4 步验证流程

  1. 颁发者身份验证 (IssuerAuth):
    • 移动安全对象 (MSO) 由签发机构签名(IssuerAuth 载荷)。
    • 读取器使用文档签名者证书验证 COSE_Sign1 签名,并确保该证书链可追溯到受信任的签发机构 CA (IACA) 根证书。
  2. 有效期验证:
    • 读取器会根据读取器的当前时钟检查 MSO 中的 validityInfo.validFrom 和 validityInfo.validUntil 时间戳,以确保凭证未过期。
  3. 数据完整性验证 (ValueDigests):
    • 对于收到的每个 IssuerSignedItem,读取器都会计算其摘要(例如 SHA-256),并验证其是否与 MSO 的 ValueDigests 字典中的相应哈希条目匹配。
  4. 设备身份验证 (DeviceSigned):
    • 读取器会验证出示凭证的设备是否持有与已签名 MSO 中发布的 DeviceKey 对应的私钥。
    • 为此,系统会通过 SessionTranscript 验证 DeviceAuth(DeviceSignature 或 DeviceMac),将相应会话绑定到读取器的临时密钥,并防范重放攻击和中间人攻击。

2. 管理可信 IACA 根证书

生产环境中的读取器必须维护一个安全的本地受信任证书存储区,其中包含受信任的 IACA 根证书:

  • 正式版 IACA 证书:下载并配置官方颁发机构提供的根证书。请参阅支持的颁发机构和 IACA 证书列表。
  • AAMVA VICAL:对于美国管辖区,读取器系统可以与美国机动车管理员协会 (AAMVA) 的 Verified Issuer Certificate Authority List (VICAL) 服务集成,以自动同步州信任锚。
  • 沙盒测试根证书:使用沙盒凭证进行测试时,请确保读取器信任 Google 沙盒 IACA 根证书。
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
}

读卡器身份验证(推荐)

借助读取器身份验证,读取器应用可以使用授权的 X.509 读取器证书对 ReaderAuthentication 结构进行签名,从而以加密方式向 Google 钱包证明其身份。

  • 推荐原因:借助读取器身份验证,您的读取器应用或终端可以向用户展示可信身份。虽然对于基本公开属性(例如验证 age_over_21)来说,此步骤是可选的,但我们强烈建议读取器集成采用此步骤,以提高用户信任度。此外,在请求敏感属性(例如完整的社会保障号、居住地址或特定州背书)时,法律或政策可能要求采用此步骤。
  • 运作方式:读取器会添加其证书链并对会话记录进行签名。在释放数据之前,Google 钱包会在权限请求页面上向用户显示读卡器经过验证的身份和组织名称。

测试和开发者工具

如需加快集成速度,请使用以下开发者工具和参考实现:

  1. Multipaz 参考应用:
    • 克隆 Multipaz 代码库并运行 IdentityReader Android 示例应用,以测试实体验证流程。
  2. 在 Google 钱包中创建测试 ID:
  3. 基于 Web 的验证程序测试:
    • 使用 verifier.multipaz.org 检查 CBOR 请求、探索声明查询,以及测试 W3C / ISO 18013-7 基于 Web 的呈现。

问题排查和现场诊断

下表列出了离线验证期间遇到的常见问题以及建议的解决方法:

问题 / 症状 根本原因 推荐的解决方法
BLE 连接超时 / 连接失败
  • 高密度环境中的射频干扰。
  • 特定读卡器硬件上的外围设备模式与中心设备模式不兼容。
  • 扫描超时。
  • 确保读卡器同时支持 BLE 中央客户端和外围服务器模式。
  • 调整 BLE 扫描窗口和间隔,以便在活跃互动期间进行主动扫描。
  • 验证 MTU 大小协商是否成功完成。
NFC 触碰互动失败或中断 在 BLE 切换记录完全传输之前,用户将移动设备从读取器天线处移开。
  • 在 NFC 互动开始后,立即在终端上提供视觉/音频/触感反馈。
  • 指示用户将手机稳定地贴在 NFC 目标上,直到 BLE 连接建立。
UNTRUSTED_ISSUER / 证书链失败 文档签名者证书未链接到读取器本地受信任证书存储区中的任何受信任 IACA 证书。
  • 检查签发者的根证书是否已加载到读取器受信任证书存储区中。
  • 如果在沙盒中测试,请验证是否已加载 Google 沙盒 IACA 根。
  • 确保 IACA 证书列表(例如 AAMVA VICAL)定期更新。
INVALID_VALIDITY_INFO / 已过期的 MSO
  • 读卡器系统时钟不同步。
  • MSO 签名已过期。
  • 确保读取器设备通过 NTP 定期同步其系统时间。
  • 提示用户在连接到网络时打开 Google 钱包,以刷新凭证令牌。
DEVICE_AUTHENTICATION_FAILED 读取器和钱包之间的会话转写不匹配,或临时设备签名无效。
  • 确保 DeviceEngagementBytes 和 EReaderKeyBytes 的确切原始字节保留在 SessionTranscript 结构中,而无需重新编码。
Android 12+ 权限崩溃 应用尝试在没有运行时权限的情况下通过 BLE 进行扫描或通告。
  • 在启动读取器会话之前,在运行时检查并请求 BLUETOOTH_SCAN、BLUETOOTH_CONNECT 和 BLUETOOTH_ADVERTISE。

面对面读卡器的用户体验和隐私权准则

在设计实体读卡器和配套应用时:

  • 在界面中实行选择性披露:仅向操作员显示验证结果或最低要求的属性(例如,显示醒目的绿色对勾标记和“已验证年满 21 周岁”,而不是显示用户的完整出生日期、地址和驾照号码)。
  • 清晰的实体互动指示:清晰地标记 NFC 目标区域,并显示视觉提示(例如动画或进度条),展示每个阶段:点按 / 扫描 → 正在连接 → 正在验证 → 完成。
  • 临时数据处理:除非适用法律明确要求并通过 intentToRetain = true 披露,否则不得存储或记录从钱包收到的个人数据元素。