デジタル認証情報の対面式での受け入れ(オフライン)

このガイドでは、証明書利用者(RP)とリーダー デベロッパーが、国際規格 ISO/IEC 18013-5 に従って、Google ウォレットから提示されたデジタル認証情報の対面式(オフライン)検証を実装する方法について説明します。

Google ウォレットのデジタル認証情報は、提示時にインターネット接続がなくても、物理的な環境(POS 端末、イベント会場、交通機関の改札、法執行機関のリーダー、モバイル リーダー アプリなど)で安全に検証できます。

オフライン プレゼンテーション(ISO/IEC 18013-5)の概要

ISO/IEC 18013-5 標準は、ホルダー(Google ウォレットを実行しているユーザーのモバイル デバイス)とリーダー / 検証者(物理端末またはコンパニオン モバイルアプリ)間のオフライン プレゼンテーションに関する、標準化された相互運用可能なプロトコルを定義しています。

プレゼンテーション フローは、次の個別のフェーズで行われます。

  1. デバイス エンゲージメント: リーダーとウォレットは、NFC タップ(静的またはネゴシエートされたハンドオーバー)または QR コードスキャナを介して最初の接続を確立します。このフェーズでは、デバイス エンゲージメント メタデータと、リーダーのエフェメラル公開鍵(EReaderKey)が交換されます。
  2. データ転送接続: 安全で暗号化された Bluetooth Low Energy(BLE)チャネルがネゴシエートされます(リーダーはセントラル クライアント モードまたはペリフェラル サーバー モードで動作します)。
  3. デバイス リクエスト: リーダーは、リクエストされたドキュメント タイプ(org.iso.18013.5.1.mDL など)と、リクエストされた特定の名前空間とデータ要素を指定する CBOR エンコードされた DeviceRequest を送信します。
  4. ユーザーの同意とデバイス認証: Google ウォレットは、リクエストされたデータ要素を確認し、生体認証または画面ロックを使用して共有を確定するようユーザーに求めます。
  5. デバイスのレスポンスと暗号検証: ウォレットは、署名済みのモバイル セキュリティ オブジェクト(MSO)とデバイス署名済みのデータ要素を含む、CBOR エンコードされた DeviceResponse を返します。リーダーは、信頼できるルート証明書と照合して暗号署名を検証します。

Multipaz オープンソース SDK

リーダーまたは検証ツール アプリケーションを実装するには、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 エンゲージメント用)、Bluetooth Low Energy データ転送のためのハードウェア権限が必要です。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: Verification Engine を初期化する

Multipaz の VerificationHelper またはリーダー接続の抽象化を使用して、エンゲージメント イベントを処理し、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 を作成する

アプリケーションで検証する必要があるドキュメント タイプと個々のデータ要素を指定します。常に最小限の開示の原則を適用します(例: 年齢確認時に birth_date 全体ではなく age_over_21 のみをリクエストする)。

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 ライブラリには、標準の認証情報用に事前定義されたスキーマモデルが用意されています。

ドキュメント タイプ識別子 基準 / 範囲 一般的な Namespace 共通要素
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 EU デジタル ID ウォレット(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 パス / テスト 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)
        )
    )
)

暗号検証と信頼管理

レスポンス ペイロードの受信は最初のステップにすぎません。読み取りデバイスは、提示された認証情報の真正性と完全性を検証するために、4 段階の暗号検証を行わなければなりません。

確認手順 検証対象
1. カード発行会社による認証 信頼できる 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 ウォレットに対して自身の ID を暗号的に証明できます。

  • 推奨される理由: リーダー認証を使用すると、リーダーアプリまたはリーダー端末は信頼できる ID をユーザーに提示できます。基本的な公開属性(age_over_21 の確認など)では省略可能ですが、ユーザーの信頼を高めるために、リーダーの統合では強く推奨されます。また、機密性の高い属性(社会保障番号全体、居住地、特定の州の承認など)をリクエストする場合は、法律またはポリシーで義務付けられていることがあります。
  • 仕組み: リーダーは証明書チェーンを組み込み、セッション トランスクリプトに署名します。Google ウォレットは、データがリリースされる前に、同意画面で読み取りデバイスの確認済みの ID と組織名をユーザーに表示します。

テストツールとデベロッパー ツール

統合を加速するには、次のデベロッパー ツールとリファレンス実装を使用します。

  1. Multipaz リファレンス アプリ:
    • Multipaz リポジトリのクローンを作成し、IdentityReader Android サンプルアプリを実行して、物理的な検証フローをテストします。
  2. Google ウォレットでテスト ID を作成する:
  3. ウェブベースの検証ツールによるテスト:
    • verifier.multipaz.org を使用して、CBOR リクエストの検査、クレームクエリの確認、W3C / ISO 18013-7 ウェブベースのプレゼンテーションのテストを行います。

トラブルシューティングとフィールド診断

次の表に、オフライン確認中に発生する一般的な問題と、推奨される解決策を示します。

問題 / 現象 原因 推奨の解決策
BLE 接続のタイムアウト / 接続できない
  • 高密度環境での RF 干渉。
  • 特定のリーダー ハードウェアにおけるペリフェラル モードとセントラル モードの非互換性。
  • スキャンのタイムアウト。
  • リーダーが BLE セントラル クライアント モードとペリフェラル サーバー モードの両方をサポートしていることを確認します。
  • アクティブなエンゲージメント中に積極的なスキャンを行うように、BLE スキャン ウィンドウと間隔を調整します。
  • MTU サイズのネゴシエーションが正常に完了したことを確認します。
NFC タップ エンゲージメントが失敗する、または中断する BLE ハンドオーバー レコードが完全に転送される前に、ユーザーがモバイル デバイスをリーダーのアンテナから離した。
  • NFC のエンゲージメントが開始されるとすぐに、端末に視覚、音声、触覚によるフィードバックが表示されます。
  • BLE 接続が確立されるまで、スマートフォンを NFC ターゲットにしっかりと近づけておくようお客様に伝えます。
UNTRUSTED_ISSUER / 証明書チェーンの失敗 ドキュメント署名者の証明書が、リーダーのローカル トラストストア内の信頼できる IACA 証明書にチェーンされていない。
  • 発行元のルート証明書がリーダーのトラストストアに読み込まれていることを確認します。
  • サンドボックスでテストする場合は、Google Sandbox IACA Root が読み込まれていることを確認します。
  • 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 を確認してリクエストします。

対面式カードリーダーの UX とプライバシー ガイドライン

物理リーダーとコンパニオン アプリを設計する際は、次の点に注意してください。

  • UI で選択的開示を実践する: オペレーターには、判定または必要最小限の属性のみを表示します(例: ユーザーの生年月日、住所、免許証番号をすべて表示するのではなく、目立つ緑色のチェックマークと「21 歳以上確認済み」を表示します)。
  • 物理的な操作のインジケーターを明確にする: NFC ターゲット ゾーンに明確なラベルを付け、各ステージ(タップ / スキャン → 接続中 → 確認中 → 完了)を示す視覚的なキュー(アニメーションや進行状況バーなど)を表示します。
  • エフェメラル データの取り扱い: 適用される法律で明示的に義務付けられ、intentToRetain = true を通じて開示されている場合を除き、ウォレットから受信した個人データ要素を保存またはログに記録しないでください。