Как предъявлять удостоверения с помощью приложения-кошелька

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

Полный пример возврата ответа с учетными данными в контексте можно найти в примере приложения.