Apresentar credenciais com o app de titular

A API Credential Manager Holder permite que seu app de titular do Android (também chamado de "carteira") gerencie e apresente credenciais digitais aos verificadores. Este guia descreve como registrar as credenciais do app de armazenamento no Gerenciador de credenciais e como processar uma solicitação de apresentação quando um usuário seleciona uma credencial. A API Holder pode ser usada no Android 6 (nível 23 da API) e versões mais recentes.

Adicionar dependências

Para usar a API Credential Manager Holder, adicione as seguintes dependências ao script build.gradle do módulo do app:

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"
}

Registrar suas credenciais

Primeiro, crie uma instância RegistryManager que será usada para registrar uma solicitação OpenId4VpRegistry.

// Create the registry manager
val registryManager = RegistryManager.create(context)

Criar uma solicitação OpenId4VpRegistry

É necessário registrar um OpenId4VpRegistry para processar solicitações OpenID4VP de verificadores. Supondo que você tenha tipos de dados locais carregados com as credenciais da carteira (por exemplo, sdJwtsFromStorage e mdocsFromStorage), é necessário convertê-los nos equivalentes do Jetpack DigitalCredentialEntry: SdJwtEntry e MdocEntry.

Adicionar Sd-JWTs ao registro

Associe cada credencial SD-JWT local a um SdJwtEntry para o registro:

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
}

Adicionar mdocs ao registro

Mapeie suas credenciais mdoc locais para o tipo MdocEntry do Jetpack:

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
}

Pontos principais sobre o código

  • IDs criptografados: um método de configuração do campo ID é registrar um identificador de credencial criptografado para que só você possa descriptografar o valor.
  • Localização: os campos de exibição da interface para os dois formatos precisam ser localizados.

Combinar e registrar sua solicitação

Combine as entradas convertidas e registre a solicitação com o 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.
)

Opcional: emissão inline

Se não houver credenciais do tipo solicitado disponíveis para um usuário, você poderá mostrar uma opção para adicionar uma credencial ao titular. Para fazer isso, adicione uma lista inlineIssuanceEntries com as credenciais aceitas ao seu 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"
)

Registre suas credenciais com o CredentialManager.

try {
    val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
    // Handle failure
}

Gerenciamento de metadados de apps

Os metadados que o app de carteira registra com o CredentialManager têm as seguintes propriedades:

  • Persistência: as informações são salvas localmente e mantidas após as reinicializações.
  • Armazenamento em silos: os registros de cada app são armazenados separadamente, ou seja, um app não pode mudar os registros de outro.
  • Atualizações com chave: os registros de cada app são identificados por um id, o que permite reidentificar, atualizar ou excluir registros.
  • Atualização de metadados: é uma boa prática atualizar os metadados persistentes sempre que o app for alterado ou carregado pela primeira vez. Se um registro for chamado várias vezes no mesmo id, a chamada mais recente vai substituir todos os registros anteriores. Para atualizar, registre novamente sem precisar limpar o registro antigo primeiro.

Opcional: criar um correspondente

Um matcher é um binário Wasm que o Gerenciador de credenciais executa em um sandbox para filtrar suas credenciais registradas em relação a uma solicitação de verificador recebida.

  • Matcher padrão: a classe OpenId4VpRegistry inclui automaticamente o matcher OpenId4VP padrão (OpenId4VpDefaults.DEFAULT_MATCHER) quando você o instancia. Para todos os casos de uso padrão do OpenID4VP, a biblioteca processa a correspondência para você.
  • Correspondência personalizada: você só implementará uma correspondência personalizada se estiver oferecendo suporte a um protocolo não padrão que exija uma lógica de correspondência própria.

Processar uma credencial selecionada

Quando um usuário seleciona uma credencial, o app de carteira precisa processar a solicitação. Você precisa definir uma atividade que detecte o filtro de intent androidx.credentials.registry.provider.action.GET_CREDENTIAL. Nossa carteira de exemplo demonstra esse procedimento.

A intent inicia sua atividade com a solicitação do Verificador e a origem da chamada, que você extrai com a função PendingIntentHandler.retrieveProviderGetCredentialRequest. Isso retorna um ProviderGetCredentialRequest com todas as informações associadas à solicitação do verificador. Há três componentes principais:

Você pode processar a solicitação de apresentação com o seguinte código:

request.credentialOptions.forEach { option ->
    if (option is GetDigitalCredentialOption) {
        Log.i(TAG, "Got DC request: ${option.requestJson}")
        processRequest(option.requestJson)
    }
}

Confira um exemplo disso na carteira de exemplo.

Verificar a identidade do verificador

Primeiro, extraia o ProviderGetCredentialRequest da intent:

val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

Verificar se há apps privilegiados: apps privilegiados (como navegadores da Web) podem fazer chamadas em nome de outros verificadores definindo o parâmetro de origem. Para recuperar essa origem, transmita uma lista de autores de chamadas privilegiados e confiáveis (uma lista de permissões no formato JSON) para a API getOrigin CallingAppInfo.

val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

Se origin não estiver vazio: a origem será retornada se o packageName e as impressões digitais do certificado corresponderem a um app na sua lista de permissões. Considere isso uma chamada privilegiada e defina essa origem na resposta do OpenID4VP em vez de calculá-la usando a assinatura do app de chamada.

Se origin estiver vazio: a solicitação do verificador é de um app Android. A origem mapeada para a resposta do OpenID4VP precisa ser calculada como 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"

Renderizar a interface do titular

Quando uma credencial é selecionada, o app de armazenamento é invocado, guiando o usuário pela interface do app. Há duas maneiras padrão de lidar com esse fluxo de trabalho:

  • Se for necessária uma autenticação adicional do usuário para liberar a credencial, use a API BiometricPrompt. Isso é demonstrado no exemplo.
  • Caso contrário, muitas carteiras optam por um retorno silencioso, renderizando uma atividade vazia que transmite imediatamente os dados de volta ao app de chamada. Isso minimiza os cliques do usuário e oferece uma experiência mais integrada.

Retornar a resposta da credencial

Quando o app de carteira estiver pronto para enviar o resultado de volta, encerre a atividade com a resposta da credencial:

PendingIntentHandler.setGetCredentialResponse(
    resultData,
    GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()

Se houver uma exceção, você poderá enviar a exceção de credencial de maneira semelhante:

PendingIntentHandler.setGetCredentialException(
    resultData,
    GetCredentialUnknownException() // Configure the proper exception
)
setResult(RESULT_OK, resultData)
finish()

Consulte o app de exemplo para ver um exemplo completo de como retornar a resposta da credencial no contexto.