Credential Manager Holder API позволяет приложению-держателю (также называемому кошельком) на Android управлять цифровыми удостоверениями личности и предоставлять их проверяющим. В этом руководстве описано, как зарегистрировать учетные данные приложения-держателя в Менеджере учетных данных и как обрабатывать запрос на предоставление, когда пользователь выбирает учетные данные. Holder API поддерживается на устройствах с Android 6 (уровень API 23) и более поздними версиями.
Добавьте зависимости
Чтобы использовать 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– зарегистрировать зашифрованный идентификатор учетных данных, чтобы расшифровать значение могли только вы. - Локализация. Поля интерфейса для обоих форматов должны быть локализованы.
Объедините и зарегистрируйте запрос
Объедините преобразованные записи и зарегистрируйте запрос с помощью 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.
)
Встроенный выпуск (необязательно)
Если у пользователя нет удостоверения запрашиваемого типа, вы можете предложить ему добавить удостоверение в кошелек. Для этого добавьте список inlineIssuanceEntries, содержащий поддерживаемые учетные данные, в OpenId4VpRegistry:
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, последний вызов перезаписывает все предыдущие записи. Чтобы обновить данные, зарегистрируйтесь повторно. Удалять старую запись не нужно.
Как создать сопоставитель (необязательно)
Сопоставитель – это двоичный файл Wasm, который Менеджер учетных данных запускает в изолированной среде, чтобы отфильтровать зарегистрированные учетные данные по входящему запросу проверяющей стороны.
- Сопоставитель по умолчанию. Класс
OpenId4VpRegistryавтоматически включает сопоставительOpenId4VPпо умолчанию (OpenId4VpDefaults.DEFAULT_MATCHER) при создании экземпляра. Для всех стандартных сценариев использования OpenID4VP библиотека выполняет сопоставление за вас. - Пользовательский сопоставитель. Его следует использовать, только если вы поддерживаете нестандартный протокол, для которого требуется собственная логика сопоставления.
Обработка выбранных учетных данных
Когда пользователь выбирает удостоверение, ваше приложение-кошелек должно обработать запрос.
Вам нужно будет определить Activity, которое прослушивает фильтр интентов androidx.credentials.registry.provider.action.GET_CREDENTIAL.
Эта процедура показана в примере кошелька.
Интент запускает действие с запросом Verifier и источником вызова, которые вы извлекаете с помощью функции PendingIntentHandler.retrieveProviderGetCredentialRequest. Возвращается объект ProviderGetCredentialRequest, содержащий всю информацию, связанную с запросом верификатора. Есть три ключевых компонента:
- Вызывающее приложение. Приложение, которое отправило запрос. Его можно получить с помощью
getCallingAppInfo. Вы можете использовать его, чтобы проверить личность проверяющего. - Выбранные учетные данные. Информация о том, какого кандидата выбрал пользователь, полученная через
selectedCredentialSet extension method. Она будет соответствовать зарегистрированному вами идентификатору учетных данных. - Конкретные запросы. Конкретный запрос, сделанный проверяющим, полученный из метода
getCredentialOptions. Для запроса цифрового удостоверения в этом списке будет один элементGetDigitalCredentialOption.
Вы можете обработать запрос на показ с помощью следующего кода:
request.credentialOptions.forEach { option ->
if (option is GetDigitalCredentialOption) {
Log.i(TAG, "Got DC request: ${option.requestJson}")
processRequest(option.requestJson)
}
}
Пример можно посмотреть в образце кошелька.
Проверьте личность проверяющего
Сначала извлеките ProviderGetCredentialRequest из намерения:
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
Проверьте привилегированные приложения. Они (например, веб-браузеры) могут выполнять вызовы от имени других верификаторов, задавая параметр origin. Чтобы получить этот источник, передайте список привилегированных и доверенных вызывающих объектов (белый список в формате JSON) в API CallingAppInfo getOrigin.
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"
Отрисовка интерфейса держателя
Когда пользователь выбирает учетные данные, запускается приложение-держатель, которое направляет пользователя через свой интерфейс. Есть два стандартных способа работы с такими данными:
- Если для передачи учетных данных требуется дополнительная аутентификация пользователя, используйте 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()
Полный пример возврата ответа с учетными данными в контексте можно найти в примере приложения.