线下接受数字凭证

本指南介绍了信赖方 (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,这是一款最初由 Google 开发并贡献给 OpenWallet Foundation (OWF) 的开源 Kotlin Multiplatform (KMP) SDK。

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 互动、摄像头扫描(用于 QR 互动)和低功耗蓝牙数据传输。将以下权限添加到 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_namegiven_namebirth_dateissue_dateexpiry_dateissuing_authoritydocument_numberportraitdriving_privilegesage_over_18age_over_21
eu.europa.ec.eudi.pid.1 欧盟数字身份钱包 (EUDIW) 个人身份数据 eu.europa.ec.eudi.pid.1 family_namefirst_namebirth_datenationalityissuing_countryissuing_authoritypersonal_administrative_number
com.google.wallet.idcard.1 Google 钱包身份证件 / 测试身份证件 com.google.wallet.idcard.1 given_namefamily_namebirth_datedocument_numberportrait

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. 发卡机构身份验证 根据可信的 IACA 根证书验证 IssuerAuth (COSE_Sign1)
2. 有效期限检查 确保 validFrom ≤ 当前时间 ≤ validUntil
3. 数据完整性检查 计算返回元素的 SHA-256 摘要,并与 MSO ValueDigests 进行匹配
4. 设备身份验证 使用绑定到 SessionTranscriptDeviceKey 验证 DeviceSigned 签名或 MAC

1. 4 步验证流水线

  1. 发卡机构身份验证 (IssuerAuth)
    • 移动安全对象 (MSO) 由签发机构(IssuerAuth 载荷)签名。
    • 读卡器使用证件签名者证书验证 COSE_Sign1 签名,并确保证书链一直延伸到受信任的签发机构 CA (IACA) 根证书。
  2. 有效性窗口验证
    • 读取器会检查 MSO 中的 validityInfo.validFromvalidityInfo.validUntil 时间戳,并将其与读取器的当前时钟进行比较,以确保凭据未过期。
  3. 数据完整性验证 (ValueDigests)
    • 对于收到的每个 IssuerSignedItem,读取器都会计算其摘要(例如 SHA-256),并验证该摘要是否与 MSO 的 ValueDigests 字典中的相应哈希条目匹配。
  4. 设备身份验证 (DeviceSigned)
    • 读卡器验证提供凭据的设备是否持有与签名 MSO 中发布的 DeviceKey 对应的私钥。
    • 这是通过以下方式实现的:通过 SessionTranscript 验证 DeviceAuthDeviceSignatureDeviceMac),将会话绑定到读卡器的临时密钥,并防止重放攻击和中间人攻击。

2. 管理可信的 IACA 根证书

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

  • 生产 IACA 证书:从官方签发机构下载并配置根证书。请参阅我们的支持的签发机构和 IACA 证书列表。
  • AAMVA VICAL:对于美国管辖区,读取器系统可以与美国机动车辆管理协会 (AAMVA) 验证的签发者证书授权机构列表 (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
}

读卡器身份验证(推荐)

借助 Reader Authentication,读取器应用可以使用授权的 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 Sandbox IACA Root
  • 确保定期更新 IACA 证书列表(例如 AAMVA VICAL)。
INVALID_VALIDITY_INFO / MSO 已过期
  • 阅读器系统时钟未同步。
  • MSO 签名已过期。
  • 确保读卡器设备通过 NTP 定期同步其系统时间。
  • 提示用户在连接到互联网时打开 Google 钱包,以刷新凭据令牌。
DEVICE_AUTHENTICATION_FAILED 读卡器和钱包之间的会话转写不匹配,或临时设备签名无效。
  • 确保 DeviceEngagementBytesEReaderKeyBytes 的确切原始字节保留在 SessionTranscript 结构中,而无需重新编码。
Android 12 及更高版本中的权限崩溃问题 应用尝试通过 BLE 进行扫描或广播,但未获得运行时权限。
  • 在开始读取器会话之前,在运行时检查并请求 BLUETOOTH_SCANBLUETOOTH_CONNECTBLUETOOTH_ADVERTISE

面向实体读者的用户体验和隐私权准则

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

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