使用您的持证人应用出示凭证

借助 Credential Manager Holder API,您的 Android 持有者(也称为“钱包”)应用可以管理数字凭证并将其出示给验证者。本指南介绍了如何向 Credential Manager 注册凭证持有者应用的凭证,以及如何在用户选择凭证时处理展示请求。Android 6(API 级别 23)及更高版本支持 Holder API。

添加依赖项

如需使用 Credential Manager 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"
}

注册凭证

首先,创建一个 RegistryManager 实例,用于注册 OpenId4VpRegistry 请求。

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

构建 OpenId4VpRegistry 请求

您需要注册一个 OpenId4VpRegistry 来处理来自验证者的 OpenID4VP 请求。假设您已加载包含钱包凭据的本地数据类型(例如 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 字段的一种方法是注册加密凭证标识符,这样只有您才能解密该值。
  • 本地化:这两种格式的界面显示字段都应进行本地化。

合并并注册您的请求

合并转换后的条目,并向 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.
)

可选:内嵌签发

如果用户没有所请求类型的现有凭据,您可以显示一个选项,以便向凭据夹添加凭据。为此,请向 OpenId4VpRegistry 添加一个包含受支持凭据的 inlineIssuanceEntries 列表:

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 下多次调用注册表,则最新调用会覆盖所有先前记录。如需更新,请重新注册,无需先清除旧记录。

可选:创建匹配器

匹配器是 Credential Manager 在沙盒中运行的 Wasm 二进制文件,用于根据传入的验证器请求过滤已注册的凭证。

  • 默认匹配器:当您实例化 OpenId4VpRegistry 类时,该类会自动包含默认的 OpenId4VP 匹配器 (OpenId4VpDefaults.DEFAULT_MATCHER)。对于所有标准 OpenID4VP 用例,该库会为您处理匹配。
  • 自定义匹配器:只有在支持需要其自身匹配逻辑的非标准协议时,您才需要实现自定义匹配器。

处理所选凭证

当用户选择凭证时,您的凭证持有者应用需要处理该请求。 您需要定义一个监听 androidx.credentials.registry.provider.action.GET_CREDENTIAL intent 过滤器的 Activity。我们的示例钱包演示了此过程。

该 intent 会启动您的 activity,并附带验证器请求和调用来源,您可以使用 PendingIntentHandler.retrieveProviderGetCredentialRequest 函数提取这些信息。此方法会返回一个 ProviderGetCredentialRequest,其中包含与验证器请求关联的所有信息。主要有三个关键组成部分:

您可以使用以下代码处理展示请求:

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

您可以在示例钱包中查看相关示例。

检查验证者身份

首先,从 intent 中提取 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"

呈现 Holder 界面

选择凭证后,系统会调用持有者应用,引导用户完成应用的界面。您可以通过以下两种标准方式来处理此工作流:

  • 如果需要额外的用户身份验证才能释放凭据,请使用 BiometricPrompt API。示例中对此进行了演示。
  • 否则,许多钱包会选择静默返回,即呈现一个空 activity,该 activity 会立即将数据传回调用方应用。这样可以最大限度地减少用户点击次数,并提供更顺畅的体验。

返回凭证响应

当您的凭证持有者应用准备好发回结果时,请使用凭证响应完成 activity:

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

如需查看在上下文中返回凭据响应的完整示例,请参阅示例应用。