Credential Manager Holder API 可让您的 Android 持有者(也称为“钱包”)应用管理数字凭据并将其呈现给验证者。
核心概念
在使用 Holder API 之前,请务必先熟悉以下概念。
凭据格式
凭证可以以不同的凭证格式存储在持有者应用中。这些格式是凭据应如何表示的规范,每种格式都包含有关凭据的以下信息:
- 类型:类别,例如大学学位或移动驾驶执照。
- 属性:例如名字和姓氏等属性。
- 编码:凭据的结构方式,例如 SD-JWT 或 mdoc
- 有效性:以加密方式验证凭据真实性的方法。
每种凭据格式的编码和验证方式略有不同,但功能相同。
注册表支持两种格式:
- SD-JWT:符合 IETF 基于 SD-JWT 的可验证凭据 (SD-JWT VC) 规范。
- 移动证件或 mdoc:符合 ISO/IEC 18013-5:2021 规范。
使用凭据管理器时,验证方可以针对 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)。现在,您将根据这些数据类型的格式(SdJwtEntry 或 MdocEntry,分别对应 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,其中包含与验证器请求关联的所有信息。主要有三个关键组成部分:
- 调用应用:发出请求的应用,可通过
getCallingAppInfo进行检索。 - 所选凭据:用户选择的候选凭据的相关信息,通过
selectedCredentialSet extension method检索;此信息将与您注册的凭据 ID 相匹配。 - 具体请求:验证方发出的具体请求,从
getCredentialOptions方法中检索。对于数字凭据请求流程,您应该会在该列表中找到一个GetDigitalCredentialOption。
最常见的情况是,验证方会发出数字凭据出示请求,您可以使用以下示例代码进行处理:
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)
- 检查特权来源:特权应用(如网络浏览器)可以通过设置来源参数代表其他验证方进行调用。如需检索此来源,您必须将可信的特权调用方列表(采用 JSON 格式的许可名单)传递给
CallingAppInfo的getOrigin()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()
如需查看在上下文中返回凭据响应的完整示例,请参阅示例应用。