Integrar o app Holder ao Gerenciador de credenciais

A API Credential Manager Holder permite que seu app de carteira (também chamado de "carteira") do Android gerencie e apresente credenciais digitais a verificadores.

Imagem mostrando a interface de credenciais digitais no Credential Manager
Figura 1. A interface do seletor de credenciais digitais.

Principais conceitos

É importante se familiarizar com os seguintes conceitos antes de usar a API Holder.

Formatos de credenciais

As credenciais podem ser armazenadas em apps de carteira digital em diferentes formatos. Esses formatos são especificações de como uma credencial deve ser representada, e cada um contém as seguintes informações sobre ela:

  • Tipo:a categoria, como um diploma universitário ou uma carteira de motorista digital.
  • Propriedades:atributos como nome e sobrenome.
  • Codificação:a forma como a credencial é estruturada, por exemplo, SD-JWT ou mdoc
  • Validade:método para verificar criptograficamente a autenticidade da credencial.

Cada formato de credencial faz a codificação e a validação de maneira um pouco diferente, mas funcionalmente eles são iguais.

O registro é compatível com dois formatos:

Um verificador pode fazer uma solicitação OpenID4VP para SD-JWTs e mdocs ao usar o Credential Manager. A escolha varia de acordo com o caso de uso e o setor.

Registro de metadados de credenciais

O Gerenciador de credenciais não armazena as credenciais de um titular diretamente, mas sim os metadados delas. Um app de suporte precisa primeiro registrar metadados de credenciais com o Gerenciador de credenciais usando RegistryManager. Esse processo de registro cria um registro que tem duas finalidades principais:

  • Correspondência:os metadados de credenciais registradas são usados para corresponder a futuras solicitações do verificador.
  • Tela:os elementos personalizados da interface são mostrados ao usuário na interface do seletor de credenciais.

Você vai usar a classe OpenId4VpRegistry para registrar suas credenciais digitais, já que ela é compatível com os formatos de credenciais mdoc e SD-JWT. Os verificadores vão enviar solicitações OpenID4VP para pedir essas credenciais.

Registrar as credenciais do app

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

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

}

Criar o RegistryManager

Crie uma instância RegistryManager e registre uma solicitação OpenId4VpRegistry com ela.

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

Criar uma solicitação OpenId4VpRegistry

Como mencionado anteriormente, você precisa registrar um OpenId4VpRegistry para processar uma solicitação OpenID4VP de um verificador. Vamos presumir que você tenha alguns tipos de dados locais carregados com suas credenciais da carteira (por exemplo, sdJwtsFromStorage). Agora, você vai convertê-los em nossos equivalentes do Jetpack DigitalCredentialEntry com base no formato deles: SdJwtEntry ou MdocEntry para SD-JWT ou mdoc, respectivamente.

Adicionar Sd-JWTs ao registro

Mapeie cada credencial SD-JWT local para um SdJwtEntry do 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 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
}

Pontos principais sobre o código

  • Uma maneira de configurar o campo id é registrar um identificador de credencial criptografado para que apenas você possa descriptografar o valor.
  • Os campos de exibição da interface dos dois formatos precisam ser localizados.

Registrar suas credenciais

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

Agora, vamos registrar suas credenciais com o CredentialManager.

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

Agora você registrou suas credenciais no Gerenciador de credenciais.

Gerenciamento de metadados de apps

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

  • Persistência:as informações são salvas localmente e permanecem disponíveis mesmo após reinicializações.
  • Armazenamento isolado:os registros de cada app são armazenados separadamente. Isso significa que 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, que permite reidentificar, atualizar ou excluir registros.
  • Atualização de metadados:é recomendável atualizar os metadados persistentes sempre que o app mudar ou for 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 matcher

Um comparador é um binário Wasm que o Gerenciador de credenciais executa em uma 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ê instancia essa classe. Para todos os casos de uso padrão do OpenID4VP, a biblioteca processa a correspondência para você.
  • Correspondência personalizada:você só implementaria uma correspondência personalizada se estivesse oferecendo suporte a um protocolo não padrão que exige uma lógica de correspondência própria.

Processar uma credencial selecionada

Quando um usuário seleciona uma credencial, o app de carteira digital 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 que contém todas as informações associadas à solicitação do verificador. Há três componentes principais:

  • O app de chamada:o app que fez a solicitação, recuperável com getCallingAppInfo.
  • A credencial selecionada:informações sobre qual candidato o usuário escolheu, recuperadas pelo selectedCredentialSet extension method. Isso vai corresponder ao ID da credencial que você registrou.
  • Solicitações específicas:a solicitação específica feita pelo verificador, recuperada do método getCredentialOptions. Para um fluxo de solicitação de credenciais digitais, você vai encontrar um único GetDigitalCredentialOption nessa lista.

Normalmente, o verificador faz uma solicitação de apresentação de credencial digital, que pode ser processada com o seguinte exemplo de 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

  1. Extraia o ProviderGetCredentialRequest da intent:
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
  1. Verificar a origem privilegiada: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() do CallingAppInfo.
val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

Se a origem não estiver vazia:a origem será retornada se o packageName e as impressões digitais do certificado fornecidas por signingInfo corresponderem às de um app encontrado na lista de permissões transmitida para a API getOrigin(). Depois que o valor de origem é recebido, o app do provedor precisa considerar isso como uma chamada privilegiada e definir essa origem na resposta do OpenID4VP, em vez de calcular a origem usando a assinatura do app de chamada.

O Gerenciador de senhas do Google usa uma lista de permissões (link em inglês) disponível para chamadas para getOrigin(). Como provedor de credenciais, use essa lista ou forneça a sua no formato JSON descrito pela API. Cabe ao provedor selecionar qual lista será usada. Para ter acesso privilegiado com provedores de credenciais terceirizados, consulte a documentação fornecida por eles.

Se a origem estiver vazia,a solicitação de verificação será de um app Android. A origem do app a ser colocada na resposta OpenID4VP deve ser calculada como 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"

Renderizar a interface do Holder

Quando uma credencial é selecionada, o app de carteira digital é 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 na amostra.
  • Caso contrário, muitas carteiras optam por um retorno silencioso renderizando uma atividade vazia que passa imediatamente os dados de volta para o app de chamada. Isso minimiza os cliques do usuário e oferece uma experiência mais perfeita.

Retornar a resposta de credencial

Quando o app de contêiner estiver pronto para enviar o resultado de volta, encerre a atividade com a resposta de credencial:

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

Se houver uma exceção, envie a exceção de credencial da mesma forma:

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

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