ホルダーアプリで認証情報を提示する

認証情報マネージャー Holder API を使用すると、Android ホルダー(「ウォレット」とも呼ばれます)アプリでデジタル認証情報を管理し、検証者に提示できます。このガイドでは、認証情報マネージャーにホルダーアプリの認証情報を登録する方法と、ユーザーが認証情報を選択したときに提示リクエストを処理する方法について説明します。Holder API は、Android 6(API レベル 23)以降でサポートされています。

依存関係を追加する

認証情報マネージャー Holder API を使用するには、アプリ モジュールの build.gradle スクリプトに次の依存関係を追加します。

dependencies {
    // Used to implement credential registries
    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-openid:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-sdjwtvc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha05"
}

認証情報を登録する

まず、OpenId4VpRegistry リクエストの登録に使用する RegistryManager インスタンスを作成します。

// Create the registry manager
val registryManager = RegistryManager.create(context)

OpenId4VpRegistry リクエストを作成する

検証ツールからの OpenID4VP リクエストを処理するには、OpenId4VpRegistry を登録する必要があります。ウォレットの認証情報(sdJwtsFromStorage、mdocsFromStorage など)が読み込まれたローカル データ型があると仮定すると、それらを Jetpack DigitalCredentialEntry の同等物(SdJwtEntry、MdocEntry)に変換する必要があります。

Sd-JWT をレジストリに追加する

各ローカル SD-JWT 認証情報をレジストリの SdJwtEntry にマッピングします。

fun mapToSdJwtEntries(sdJwtsFromStorage: List<StoredSdJwtEntry>): List<SdJwtEntry> {
    val list = mutableListOf<SdJwtEntry>()

    for (sdJwt in sdJwtsFromStorage) {
        list.add(
            SdJwtEntry(
                verifiableCredentialType = sdJwt.getVCT(),
                claims = sdJwt.getClaimsList(),
                entryDisplayPropertySet = sdJwt.toDisplayProperties(),
                id = sdJwt.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

レジストリに mdoc を追加する

ローカルの mdoc 認証情報を Jetpack タイプ MdocEntry にマッピングします。

fun mapToMdocEntries(mdocsFromStorage: List<StoredMdocEntry>): List<MdocEntry> {
    val list = mutableListOf<MdocEntry>()

    for (mdoc in mdocsFromStorage) {
        list.add(
            MdocEntry(
                docType = mdoc.retrieveDocType(),
                fields = mdoc.getFields(),
                entryDisplayPropertySet = mdoc.toDisplayProperties(),
                id = mdoc.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

コードに関する主なポイント

  • 暗号化された ID: ID フィールドを構成する方法の 1 つは、暗号化された認証情報識別子を登録することです。これにより、ユーザーのみが値を復号できます。
  • ローカライズ: どちらの形式でも、UI の表示フィールドをローカライズする必要があります。

リクエストを組み合わせて登録する

変換されたエントリを結合し、リクエストを RegistryManager に登録します。

val credentialEntries =
    mapToSdJwtEntries(sdJwtsFromStorage) + mapToMdocEntries(mdocsFromStorage)

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    id = "my-wallet-openid-registry-v1" // A stable, unique ID to identify your registry record.
)

省略可: インライン発行

リクエストされたタイプの既存の認証情報がユーザーにない場合は、認証情報をホルダーに追加するオプションを表示できます。これを行うには、サポートされている認証情報を含む inlineIssuanceEntries リストを OpenId4VpRegistry に追加します。

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    inlineIssuanceEntries = emptyList(),
                listOf(
                MdocInlineIssuanceEntry(
                    id = "Issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedMdocs = setOf(
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "eu.europa.ec.eudi.pid.1"
                        ),
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "org.iso.18013.5.1.mDL1"
                        ),
                    )
                ),
                SdJwtInlineIssuanceEntry(
                    id = "sd-jwt-issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedSdJwts = setOf(
                        SdJwtInlineIssuanceEntry.SupportedSdJwt("urn:openid:interop:id:1")
                    )
                )
            ),
    id = "my-wallet-openid-registry-v1"
)

CredentialManager に認証情報を登録します。

try {
    val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
    // Handle failure
}

アプリのメタデータ管理

ホルダーアプリが CredentialManager に登録するメタデータには、次のプロパティがあります。

  • 永続性: 情報はローカルに保存され、再起動しても保持されます。
  • サイロ化されたストレージ: 各アプリのレジストリ レコードは個別に保存されます。つまり、あるアプリが別のアプリのレジストリ レコードを変更することはできません。
  • キー付き更新: 各アプリのレジストリ レコードは id でキー設定されます。これにより、レコードの再識別、更新、削除が可能になります。
  • メタデータの更新: アプリが変更されたときや最初に読み込まれたときに、永続化されたメタデータを更新することをおすすめします。同じ id でレジストリが複数回呼び出された場合、最新の呼び出しによって以前のすべてのレコードが上書きされます。更新するには、古いレコードを消去せずに再登録します。

省略可: マッチャーを作成する

マッチャーは、認証情報マネージャーがサンドボックスで実行する Wasm バイナリで、登録済みの認証情報を、受信した検証ツールのリクエストに照らしてフィルタします。

  • デフォルトのマッチャー: OpenId4VpRegistry クラスをインスタンス化すると、デフォルトの OpenId4VP マッチャー(OpenId4VpDefaults.DEFAULT_MATCHER)が自動的に含まれます。すべての標準的な OpenID4VP ユースケースでは、ライブラリがマッチングを処理します。
  • カスタム マッチャー: 独自のマッチング ロジックを必要とする非標準プロトコルをサポートしている場合にのみ、カスタム マッチャーを実装します。

選択した認証情報を処理する

ユーザーが認証情報を選択すると、ホルダーアプリはリクエストを処理する必要があります。androidx.credentials.registry.provider.action.GET_CREDENTIAL インテント フィルタをリッスンする Activity を定義する必要があります。サンプル ウォレットでこの手順を説明しています。

このインテントは、Verifier リクエストと呼び出し元を使用してアクティビティを起動します。これらは、PendingIntentHandler.retrieveProviderGetCredentialRequest関数を使用して抽出します。これにより、検証リクエストに関連するすべての情報を含む ProviderGetCredentialRequest が返されます。主なコンポーネントは次の 3 つです。

  • 呼び出し元のアプリ: リクエストを行ったアプリ。getCallingAppInfo で取得できます。これを使用して、検証者の ID を確認できます。
  • 選択された認証情報: ユーザーが選択した候補に関する情報。selectedCredentialSet extension method を介して取得されます。これは、登録した認証情報 ID と一致します。
  • 特定のリクエスト: 検証者によって行われた特定のリクエスト。getCredentialOptions メソッドから取得されます。デジタル認証情報のリクエスト フローでは、このリストに 1 つの GetDigitalCredentialOption が含まれます。

次のコードを使用して、プレゼンテーション リクエストを処理できます。

request.credentialOptions.forEach { option ->
    if (option is GetDigitalCredentialOption) {
        Log.i(TAG, "Got DC request: ${option.requestJson}")
        processRequest(option.requestJson)
    }
}

この例については、サンプル ウォレットをご覧ください。

検証者の身元を確認する

まず、インテントから ProviderGetCredentialRequest を抽出します。

val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

特権アプリを確認する: 特権アプリ(ウェブブラウザなど)は、origin パラメータを設定することで、他の検証者の代わりに呼び出しを行うことができます。このオリジンを取得するには、権限のある信頼できる呼び出し元のリスト(JSON 形式の許可リスト)を CallingAppInfo getOrigin API に渡します。

val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

origin が空でない場合: packageName と証明書フィンガープリントが許可リスト内のアプリと一致する場合、オリジンが返されます。これを特権呼び出しと見なし、呼び出し元アプリの署名を使用してオリジンを計算する代わりに、OpenID4VP レスポンスにこのオリジンを設定します。

origin が空の場合: 検証ツールのリクエストは Android アプリからのものです。OpenID4VP レスポンスにマッピングされたオリジンは、android:apk-key-hash:<encoded SHA 256 fingerprint> として計算する必要があります。

val appSigningInfo =
    request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory?.get(0)?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash =
    Base64.encodeToString(md.digest(appSigningInfo), Base64.NO_WRAP or Base64.NO_PADDING)
val computedOrigin = "android:apk-key-hash:$certHash"

ホルダー UI をレンダリングする

認証情報が選択されると、ホルダーアプリが呼び出され、アプリの UI を通じてユーザーをガイドします。このワークフローを処理するには、次の 2 つの標準的な方法があります。

  • 認証情報を解放するために追加のユーザー認証が必要な場合は、BiometricPrompt API を使用します。これはサンプルで示されています。
  • それ以外の場合、多くのウォレットは、データを呼び出し元のアプリにすぐに返す空のアクティビティをレンダリングすることで、サイレント リターンを選択します。これにより、ユーザーのクリックが最小限に抑えられ、よりシームレスなエクスペリエンスが提供されます。

認証情報のレスポンスを返す

ホルダーアプリで結果を返す準備ができたら、認証情報レスポンスを使用してアクティビティを終了します。

PendingIntentHandler.setGetCredentialResponse(
    resultData,
    GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()

例外がある場合は、同様に認証情報の例外を送信できます。

PendingIntentHandler.setGetCredentialException(
    resultData,
    GetCredentialUnknownException() // Configure the proper exception
)
setResult(RESULT_OK, resultData)
finish()

コンテキストで認証情報レスポンスを返す完全な例については、サンプルアプリをご覧ください。