借助 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,其中包含与验证器请求关联的所有信息。主要有三个关键组成部分:
- 发起调用的应用:发出请求的应用,可通过
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)
检查特权应用:特权应用(例如网络浏览器)可以通过设置 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()
如需查看在上下文中返回凭据响应的完整示例,请参阅示例应用。