将持有者应用与 Credential Manager 集成

Credential Manager Holder API 可让您的 Android 持有者(也称为“钱包”)应用管理数字凭据并将其呈现给验证者。

图片:Credential Manager 中的数字凭证界面
图 1. 数字凭据选择器界面。

核心概念

在使用 Holder API 之前,请务必先熟悉以下概念。

凭据格式

凭证可以以不同的凭证格式存储在持有者应用中。这些格式是凭据应如何表示的规范,每种格式都包含有关凭据的以下信息:

  • 类型:类别,例如大学学位或移动驾驶执照。
  • 属性:例如名字和姓氏等属性。
  • 编码:凭据的结构方式,例如 SD-JWT 或 mdoc
  • 有效性:以加密方式验证凭据真实性的方法。

每种凭据格式的编码和验证方式略有不同,但功能相同。

注册表支持两种格式:

使用凭据管理器时,验证方可以针对 SD-JWT 和 mdoc 发出 OpenID4VP 请求。具体选择因使用情形和行业选择而异。

凭据元数据注册

凭据管理器不会直接存储持有者的凭据,而是存储凭据的元数据。持有者应用必须先使用 RegistryManager 向 Credential Manager 注册凭据元数据。此注册流程会创建注册记录,该记录有以下两个主要用途:

  • 匹配:注册的凭据元数据用于与未来的验证方请求进行匹配。
  • 显示:向用户显示凭据选择器界面上的自定义界面元素。

您将使用 OpenId4VpRegistry 类来注册数字凭据,因为该类同时支持 mdoc 和 SD-JWT 凭据格式。验证方将发送 OpenID4VP 请求来请求这些凭据。

注册应用的凭据

如需使用 Credential Manager Holder API,请将以下依赖项添加到应用模块的 build 脚本中:

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 请求

如前所述,您需要注册一个 OpenId4VpRegistry 来处理验证方的 OpenID4VP 请求。我们假设您已加载一些包含钱包凭据的本地数据类型(例如 sdJwtsFromStorage)。现在,您将根据这些数据类型的格式(SdJwtEntryMdocEntry,分别对应 SD-JWT 或 mdoc)将其转换为 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 字段的一种方法是注册加密的凭据标识符,这样只有您才能解密该值。
  • 这两种格式的界面显示字段都应进行本地化。

注册凭据

合并转换后的条目,并使用 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
}

您现在已向 Credential Manager 注册了凭据。

应用元数据管理

持有者应用向 CredentialManager 注册的元数据具有以下属性:

  • 持久性:信息保存在本地,在重新启动后仍会保留。
  • 隔离存储:每个应用的注册记录都单独存储,这意味着一个应用无法更改另一个应用的注册记录。
  • 键控更新:每个应用的注册记录都由 id 键控,从而可以重新识别、更新或删除记录。
  • 更新元数据:每当应用发生更改或首次加载时,最好更新持久性元数据。如果在同一 id 下多次调用注册,则最新调用会覆盖所有先前的记录。如需更新,请重新注册,无需先清除旧记录。

可选:创建匹配器

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

  • 默认匹配器:当您实例化 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)
    }
}

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

检查验证者身份

  1. 从 intent 中提取 ProviderGetCredentialRequest
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
  1. 检查特权来源:特权应用(如网络浏览器)可以通过设置来源参数代表其他验证方进行调用。如需检索此来源,您必须将可信的特权调用方列表(采用 JSON 格式的许可名单)传递给 CallingAppInfogetOrigin() API。
val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

如果来源不为空:如果 packageName 和从 signingInfo 获取的证书指纹与传递给 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"

渲染持有者界面

选择凭据后,系统会调用持有者应用,引导用户浏览该应用的界面。处理此工作流程有两种标准方法:

  • 如果需要进行额外的用户身份验证才能释放凭据,请使用 BiometricPrompt API在示例中对此进行了演示。
  • 否则,许多钱包会选择静默返回,即呈现一个空 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()

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