本指南說明信賴方 (RP) 和讀取器開發人員如何根據國際 ISO/IEC 18013-5 標準,實體 (離線) 驗證 Google 錢包中的數位憑證。
在實體環境中 (例如銷售點終端機、活動場地、大眾運輸閘門、執法機關讀取器和行動讀取器應用程式),系統可以安全地驗證 Google 錢包中的數位憑證,且出示憑證時不需連上網路。
離線簡報 (ISO/IEC 18013-5) 總覽
ISO/IEC 18013-5 標準定義了標準化的互通通訊協定,供持有人 (執行 Google 錢包的使用者行動裝置) 和讀取器 / 驗證者 (實體終端或隨附行動應用程式) 離線出示憑證。
簡報流程分為不同階段:
- 裝置互動:讀卡機和錢包會透過 NFC 輕觸 (靜態或協商交接) 或掃描 QR code 建立初始連線。在這個階段,裝置會交換裝置參與中繼資料和讀取器的暫時公開金鑰 (
EReaderKey)。 - 資料傳輸連線:系統會協商安全的加密藍牙低功耗 (BLE) 管道 (讀取器會以中央用戶端或周邊伺服器模式運作)。
- 裝置要求:讀取器會傳輸 CBOR 編碼的
DeviceRequest,指定要求的檔案類型 (例如org.iso.18013.5.1.mDL) 和要求的特定命名空間和資料元素。 - 使用者同意聲明和裝置驗證:Google 錢包會提示使用者查看要求提供的資料元素,並使用生物特徵辨識驗證或螢幕鎖定確認分享。
- 裝置回應和密碼驗證:錢包會傳回 CBOR 編碼的
DeviceResponse,內含簽署的行動安全物件 (MSO) 和裝置簽署的資料元素。讀取器會根據信任的根憑證驗證加密簽章。
Multipaz 開放原始碼 SDK
如要實作讀取器或驗證器應用程式,Google 建議使用 Multipaz,這是 Google 原先開發的開放原始碼 Kotlin Multiplatform (KMP) SDK,並貢獻給 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 互動、相機掃描 (用於 QR code 互動) 和藍牙低功耗資料傳輸的硬體權限。在 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 程式庫提供標準憑證的預先定義架構模型:
| 文件類型 ID | 標準 / 範圍 | 一般命名空間 | 常見元素 |
|---|---|---|---|
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 錢包身分證件 / 測試 ID | 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. 發卡機構驗證 | 根據信任的 IACA 根憑證驗證 IssuerAuth (COSE_Sign1) |
| 2. 效期檢查 | 請確保 validFrom ≤ 目前時間 ≤ validUntil |
| 3. 資料完整性檢查 | 計算傳回元素的 SHA-256 摘要,並與 MSO ValueDigests 比對 |
| 4. 裝置驗證 | 使用繫結至 SessionTranscript 的 DeviceKey 驗證 DeviceSigned 簽章或 MAC |
1. 4 步驟驗證管道
- 發卡機構驗證 (
IssuerAuth):- 行動安全物件 (MSO) 由發行機構 (
IssuerAuth酬載) 簽署。 - 讀取器會使用文件簽署者憑證驗證
COSE_Sign1簽章,並確保憑證鏈結至信任的簽發機構 CA (IACA) 根憑證。
- 行動安全物件 (MSO) 由發行機構 (
- 效期驗證:
- 讀取器會檢查 MSO 中的
validityInfo.validFrom和validityInfo.validUntil時間戳記,並與讀取器目前的時鐘進行比較,確保憑證未過期。
- 讀取器會檢查 MSO 中的
- 資料完整性驗證 (
ValueDigests):- 讀取器收到每個
IssuerSignedItem時,都會計算其摘要 (例如 SHA-256),並驗證摘要是否與 MSOValueDigests字典中的對應雜湊項目相符。
- 讀取器收到每個
- 裝置驗證 (
DeviceSigned):- 讀取器會驗證提供憑證的裝置是否持有與簽署 MSO 內發布的
DeviceKey相對應的私密金鑰。 - 具體做法是透過
SessionTranscript驗證DeviceAuth(DeviceSignature或DeviceMac),將工作階段繫結至讀取器的暫時性金鑰,並防範重送和中間人攻擊。
- 讀取器會驗證提供憑證的裝置是否持有與簽署 MSO 內發布的
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
}
讀取器驗證 (建議)
讀取器應用程式可使用授權的 X.509 讀取器憑證簽署 ReaderAuthentication 結構,以加密方式向 Google 錢包證明自己的身分。
- 建議原因:讀取器驗證功能可讓讀取器應用程式或終端機向使用者顯示可信的身分。雖然基本公開屬性 (例如驗證
age_over_21) 為選填,但強烈建議讀者整合功能填寫這項資訊,以提升使用者信任度。此外,要求提供敏感屬性 (例如完整社會安全號碼、居住地址或特定州別的背書) 時,可能需要遵守法律或政策規定。 - 運作方式:讀者會加入憑證鏈結,並簽署工作階段轉錄稿。在同意畫面上,Google 錢包會向使用者顯示讀取者的已驗證身分和機構名稱,然後才會發布資料。
測試與開發工具
如要加快整合速度,請使用下列開發人員工具和參考實作項目:
- Multipaz 參考應用程式:
- 複製 Multipaz 存放區,然後執行
IdentityReaderAndroid 範例應用程式,測試實體驗證流程。
- 複製 Multipaz 存放區,然後執行
- 在 Google 錢包中建立測試 ID:
- 按照「在 Google 錢包中建立測試身分憑證」指南,使用 Utopia ePassport Simulator 將模擬測試憑證佈建到 Google 錢包。
- 網頁式驗證器測試:
- 使用 verifier.multipaz.org 檢查 CBOR 要求、探索聲明查詢,以及測試 W3C / ISO 18013-7 網頁版憑證。
疑難排解與現場診斷
下表列出離線驗證期間常見的問題和建議解決方法:
| 問題 / 症狀 | 根本原因 | 建議解決方法 |
|---|---|---|
| BLE 連線逾時 / 連線失敗 |
|
|
| NFC 感應互動失敗或中斷 | 在 BLE 交接記錄完全轉移前,使用者將行動裝置移開讀取器天線。 |
|
UNTRUSTED_ISSUER / 憑證鏈結失敗 |
文件簽署者憑證未連結至讀取器本機信任存放區中的任何可信任 IACA 憑證。 |
|
INVALID_VALIDITY_INFO / 過期 MSO |
|
|
DEVICE_AUTHENTICATION_FAILED |
讀取器和錢包之間的會話轉錄稿不符,或暫時性裝置簽章無效。 |
|
| Android 12 以上版本權限導致當機 | 應用程式嘗試透過 BLE 掃描或放送廣告,但沒有執行階段權限。 |
|
實體讀者適用的使用者體驗和隱私權指南
設計實體閱讀器和隨附應用程式時:
- 在使用者介面中實行選擇性揭露:只向操作人員顯示決策或最低必要屬性 (例如,顯示顯眼的綠色勾號和「年滿 21 歲」,而非顯示使用者的完整出生日期、地址和駕照號碼)。
- 清楚的實體互動指標:清楚標示 NFC 目標區域,並顯示視覺提示 (例如動畫或進度列),說明各個階段:輕觸 / 掃描 → 連線 → 驗證 → 完成。
- 暫時性資料處理:除非適用法律明確要求,且已透過
intentToRetain = true揭露,否則請勿儲存或記錄從錢包收到的個人資料元素。