ホルダーアプリを認証情報マネージャーと統合する

Credential Manager Holder API を使用すると、Android ホルダー(「ウォレット」とも呼ばれます)アプリでデジタル認証情報を管理し、検証ツールに提示できます。

認証情報マネージャーのデジタル認証情報 UI を示す画像
図 1. デジタル認証情報セレクタの UI。

基本コンセプト

Holder API を使用する前に、次のコンセプトをよく理解しておくことが重要です。

認証情報の形式

認証情報は、さまざまな認証情報形式でホルダーアプリに保存できます。これらの形式は、認証情報の表現方法の仕様です。各形式には、認証情報に関する次の情報が含まれています。

  • タイプ: 大学の学位やモバイル運転免許証などのカテゴリ。
  • プロパティ: 名や姓などの属性。
  • エンコード: 認証情報の構造化方法(SD-JWT、mdoc など)
  • 有効性: クライアント認証情報の信頼性を暗号技術で検証する方法。

各認証情報の形式では、エンコードと検証が若干異なりますが、機能的には同じです。

レジストリは次の 2 つの形式をサポートしています。

検証ツールは、Credential Manager を使用している場合、SD-JWT と mdoc の OpenID4VP リクエストを行うことがあります。選択肢は、ユースケースと業界の選択によって異なります。

認証情報のメタデータの登録

Credential Manager は、ホルダーの認証情報を直接保存するのではなく、認証情報のメタデータを保存します。ホルダーアプリは、まず RegistryManager を使用して Credential Manager に認証情報メタデータを登録する必要があります。この登録プロセスでは、次の 2 つの主要な目的を果たすレジストリ レコードが作成されます。

  • 照合: 登録された認証情報のメタデータは、将来の検証ツール リクエストとの照合に使用されます。
  • 表示: カスタマイズされた UI 要素が、認証情報セレクタ インターフェースでユーザーに表示されます。

OpenId4VpRegistry クラスは mdoc と SD-JWT の両方の認証情報形式をサポートしているため、このクラスを使用してデジタル認証情報を登録します。検証ツールは、これらの認証情報をリクエストするために OpenID4VP リクエストを送信します。

アプリの認証情報を登録する

Credential Manager Holder API を使用するには、アプリ モジュールのビルド スクリプトに次の依存関係を追加します。

Groovy

dependencies {
    // Use to implement credentials registrys

    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-digitalcredentials-preview:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha04"

}

Kotlin

dependencies {
    // Use to implement credentials registrys

    implementation("androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-digitalcredentials-preview:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-provider:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha04")

}

RegistryManager を作成する

RegistryManager インスタンスを作成し、それに OpenId4VpRegistry リクエストを登録します。

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

// The guide covers how to build this out later
val registryRequest = OpenId4VpRegistry(credentialEntries, id)

try {
    registryManager.registerCredentials(registryRequest)
} catch (e: Exception) {
    // Handle exceptions
}

OpenId4VpRegistry リクエストをビルドする

前述のように、検証ツールからの OpenID4VP リクエストを処理するには、OpenId4VpRegistry を登録する必要があります。ウォレット認証情報(sdJwtsFromStorage など)が読み込まれたローカル データ型があるとします。これらのデータ型を、形式(SD-JWT の場合は SdJwtEntry、mdoc の場合は MdocEntry)に基づいて Jetpack DigitalCredentialEntry の同等のデータ型に変換します。

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 フィールドを構成する 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.
)

これで、CredentialManager に認証情報を登録する準備が整いました。

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

これで、認証情報が認証情報マネージャーに登録されました。

アプリのメタデータの管理

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

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

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

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

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

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

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

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

  • 呼び出し元のアプリ: リクエストを行ったアプリ。getCallingAppInfo で取得できます。
  • 選択された認証情報: ユーザーが選択した候補に関する情報。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)
    }
}

この例は、サンプルウォレットで確認できます。

検証ツールの ID を確認する

  1. インテントから ProviderGetCredentialRequest を抽出します。
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
  1. 特権オリジンを確認する: 特権アプリ(ウェブブラウザなど)は、オリジン パラメータを設定することで、他の検証ツールの代わりに呼び出しを行うことができます。このオリジンを取得するには、権限のある信頼できる呼び出し元のリスト(JSON 形式の許可リスト)を CallingAppInfogetOrigin() API に渡す必要があります。
val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

オリジンが空でない場合: packageNamesigningInfo から取得した証明書フィンガープリントが、getOrigin() API に渡された許可リストにあるアプリのフィンガープリントと一致する場合、オリジンが返されます。このオリジン値を取得すると、プロバイダ アプリはこれを特権呼び出しと見なし、呼び出しアプリの署名を使ってオリジンを計算する代わりに、OpenID4VP レスポンスにこのオリジンを設定する必要があります。

Google パスワード マネージャーは、getOrigin() の呼び出しに、一般公開されている許可リストを使用します。認証情報プロバイダとして、このリストを使用するか、API で記述された JSON 形式で独自のリストを指定できます。使用するリストの選択はプロバイダが行います。サードパーティの認証情報プロバイダで特権アクセスを取得するには、サードパーティが提供するドキュメントをご覧ください。

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

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

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

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