Integra tu app de titular con Credential Manager

La API de Credential Manager Holder permite que tu app de titular (también llamada "billetera") de Android administre y presente credenciales digitales a los verificadores.

Imagen que muestra la IU de credenciales digitales en el Administrador de credenciales
Figura 1: Es la IU del selector de credenciales digitales.

Conceptos básicos

Es importante que te familiarices con los siguientes conceptos antes de utilizar la API de Holder.

Formatos de credenciales

Las credenciales se pueden almacenar en apps de titular en diferentes formatos. Estos formatos son especificaciones sobre cómo se debe representar una credencial, y cada uno contiene la siguiente información sobre la credencial:

  • Tipo: Es la categoría, como un título universitario o una licencia de conducir para dispositivos móviles.
  • Propiedades: Son atributos como el nombre y el apellido.
  • Codificación: Es la forma en que se estructura la credencial, por ejemplo, SD-JWT o mdoc.
  • Validez: Método para verificar de forma criptográfica la autenticidad de la credencial.

Cada formato de credencial realiza la codificación y la validación de manera ligeramente diferente, pero, funcionalmente, son iguales.

El registro admite dos formatos:

Un verificador puede realizar una solicitud de OpenID4VP para SD-JWT y mdocs cuando usa Credential Manager. La elección varía según el caso de uso y la industria.

Registro de metadatos de credenciales

Credential Manager no almacena las credenciales de un titular directamente, sino los metadatos de las credenciales. Primero, una app de titular debe registrar los metadatos de la credencial en Credential Manager con RegistryManager. Este proceso de registro crea un registro de registro que tiene dos propósitos clave:

  • Coincidencia: Los metadatos de credenciales registradas se usan para establecer coincidencias con futuras solicitudes del verificador.
  • Pantalla: Los elementos de la IU personalizados se muestran al usuario en la interfaz del selector de credenciales.

Usarás la clase OpenId4VpRegistry para registrar tus credenciales digitales, ya que admite los formatos de credenciales mdoc y SD-JWT. Los verificadores enviarán solicitudes de OpenID4VP para solicitar estas credenciales.

Registra las credenciales de tu app

Para usar la API de Credential Manager Holder, agrega las siguientes dependencias a la secuencia de comandos de compilación del módulo de tu 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")

}

Cómo crear el RegistryManager

Crea una instancia de RegistryManager y registra una solicitud de OpenId4VpRegistry con ella.

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

Crea una solicitud de OpenId4VpRegistry

Como se mencionó anteriormente, deberás registrar un OpenId4VpRegistry para controlar una solicitud de OpenID4VP de un verificador. Supondremos que tienes algunos tipos de datos locales cargados con tus credenciales de la billetera (por ejemplo, sdJwtsFromStorage). Ahora los convertirás en nuestros equivalentes de Jetpack DigitalCredentialEntry según su formato: SdJwtEntry o MdocEntry para SD-JWT o mdoc, respectivamente.

Agrega Sd-JWT al registro

Asigna cada credencial SD-JWT local a un SdJwtEntry para el 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
}

Agrega mdocs al registro

Asigna tus credenciales de mdoc locales al tipo de 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
}

Puntos clave sobre el código

  • Una forma de configurar el campo id es registrar un identificador de credencial encriptado para que solo tú puedas desencriptar el valor.
  • Los campos de visualización de la IU para ambos formatos deben estar localizados.

Registra tus credenciales

Combina las entradas convertidas y registra la solicitud con 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.
)

Ahora, ya puedes registrar tus credenciales con CredentialManager.

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

Ahora registraste tus credenciales con el Administrador de credenciales.

Administración de metadatos de la app

Los metadatos que tu app de titular registra en Credential Manager tienen las siguientes propiedades:

  • Persistencia: La información se guarda de forma local y persiste después de los reinicios.
  • Almacenamiento aislado: Los registros de cada app se almacenan por separado, lo que significa que una app no puede cambiar los registros de otra.
  • Actualizaciones con clave: Los registros de cada app se indexan con una id, lo que permite volver a identificar, actualizar o borrar registros.
  • Actualización de metadatos: Es una buena práctica actualizar los metadatos persistentes cada vez que se carga tu app por primera vez o cuando cambia. Si se llama a un registro varias veces en el mismo id, la llamada más reciente anula todos los registros anteriores. Para actualizarlo, vuelve a registrarte sin necesidad de borrar el registro anterior.

Opcional: Crea un comparador

Un comparador es un archivo binario de Wasm que Credential Manager ejecuta en un entorno de pruebas para filtrar tus credenciales registradas en función de una solicitud entrante del verificador.

  • Coincidencia predeterminada: La clase OpenId4VpRegistry incluye automáticamente la coincidencia predeterminada OpenId4VP (OpenId4VpDefaults.DEFAULT_MATCHER) cuando la instancias. Para todos los casos de uso estándar de OpenID4VP, la biblioteca controla la coincidencia por ti.
  • Correlacionador personalizado: Solo implementarías un correlacionador personalizado si admites un protocolo no estándar que requiere su propia lógica de correlación.

Cómo controlar una credencial seleccionada

Cuando un usuario selecciona una credencial, tu app de titular debe controlar la solicitud. Deberás definir una actividad que escuche el filtro de intents androidx.credentials.registry.provider.action.GET_CREDENTIAL. Nuestra billetera de ejemplo demuestra este procedimiento.

El intent inicia tu actividad con la solicitud del verificador y el origen de la llamada, que extraes con la función PendingIntentHandler.retrieveProviderGetCredentialRequest. Devuelve un objeto ProviderGetCredentialRequest que contiene toda la información asociada con la solicitud del verificador. Hay tres componentes clave:

  • La app que hace la llamada: Es la app que realizó la solicitud y se puede recuperar con getCallingAppInfo.
  • La credencial seleccionada: Es la información sobre qué candidato eligió el usuario, que se recupera a través de selectedCredentialSet extension method. Coincidirá con el ID de credencial que registraste.
  • Solicitudes específicas: Es la solicitud específica que realizó el verificador y que se recuperó del método getCredentialOptions. En el flujo de solicitudes de credenciales digitales, puedes esperar encontrar un solo GetDigitalCredentialOption en esta lista.

Por lo general, el verificador realiza una solicitud de presentación de credenciales digitales, que puedes procesar con el siguiente código de muestra:

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

Puedes ver un ejemplo de esto en la billetera de muestra.

Verifica la identidad del verificador

  1. Extrae el ProviderGetCredentialRequest del intent:
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
  1. Verifica el origen con privilegios: Las apps con privilegios (como los navegadores web) pueden realizar llamadas en nombre de otros verificadores configurando el parámetro de origen. Para recuperar este origen, debes pasar una lista de emisores con privilegios y de confianza (una lista de entidades permitidas en formato JSON) a la API de getOrigin() de CallingAppInfo.
val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

Si el origen no está vacío: Se devuelve el origen si el packageName y las huellas digitales del certificado obtenidas de signingInfo coinciden con las de una app que se encuentra en la lista de entidades permitidas que se pasa a la API de getOrigin(). Después de obtener el valor del origen, la app del proveedor debe considerar esta llamada como una llamada con privilegios y establecer este origen en la respuesta de OpenID4VP, en lugar de calcular el origen con la firma de la app que realiza la llamada.

El Administrador de contraseñas de Google usa una lista de entidades permitidas disponible para las llamadas a getOrigin(). Como proveedor de credenciales, puedes usar esta lista o proporcionar la tuya en el formato JSON que describe la API. Depende del proveedor seleccionar qué lista se usará. Para obtener acceso privilegiado con proveedores de credenciales externos, consulta la documentación proporcionada por el tercero.

Si el origen está vacío, la solicitud del verificador proviene de una app para Android. El origen de la app que se debe incluir en la respuesta de OpenID4VP se debe calcular 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"

Renderiza la IU de Holder

Cuando se selecciona una credencial, se invoca la app de titular, que guía al usuario por la IU de la app. Existen dos formas estándares de controlar este flujo de trabajo:

  • Si se necesita autenticación adicional del usuario para liberar la credencial, usa la API de BiometricPrompt. Esto se demuestra en la muestra.
  • De lo contrario, muchas billeteras optan por un retorno silencioso renderizando una actividad vacía que pasa inmediatamente los datos a la app que realiza la llamada. Esto minimiza los clics del usuario y proporciona una experiencia más fluida.

Devuelve la respuesta de credenciales

Una vez que tu app de titular esté lista para enviar el resultado, finaliza la actividad con la respuesta de credenciales:

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

Si hay una excepción, puedes enviar la excepción de credenciales de manera similar:

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

Consulta la app de ejemplo para ver un ejemplo completo de cómo devolver la respuesta de credenciales en contexto.