La API de Credential Manager Holder permite que tu app de titular de Android (también llamada "billetera") administre y presente credenciales digitales a los verificadores. En esta guía, se describe cómo registrar las credenciales de tu app de titular con el Administrador de credenciales y cómo controlar una solicitud de presentación cuando un usuario selecciona una credencial. La API de Holder es compatible con Android 6 (nivel de API 23) y versiones posteriores.
Cómo agregar dependencias
Para usar la API de Credential Manager Holder, agrega las siguientes dependencias a la secuencia de comandos build.gradle del módulo de tu 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"
}
Registra tus credenciales
Primero, crea una instancia de RegistryManager que usarás para registrar una solicitud de OpenId4VpRegistry.
// Create the registry manager
val registryManager = RegistryManager.create(context)
Crea una solicitud OpenId4VpRegistry
Debes registrar un OpenId4VpRegistry para controlar las solicitudes de OpenID4VP de
los verificadores. Suponiendo que tienes tipos de datos locales cargados con tus credenciales de la billetera (p.ej., sdJwtsFromStorage y mdocsFromStorage), debes convertirlos en sus equivalentes de DigitalCredentialEntry de Jetpack: SdJwtEntry y MdocEntry.
Agrega SD-JWT al registro
Asigna cada credencial de 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
- IDs encriptados: Un método para configurar el campo
IDes registrar un identificador de credencial encriptado, de modo que solo tú puedas desencriptar el valor. - Localización: Los campos de visualización de la IU para ambos formatos deben estar localizados.
Combina y registra tu solicitud
Combina tus entradas convertidas y registra la solicitud con el
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: Emisión intercalada
Si no hay credenciales existentes de un tipo solicitado disponibles para un usuario, puedes mostrar una opción para agregar una credencial a tu titular. Para ello, agrega una lista de inlineIssuanceEntries que contenga las credenciales admitidas a tu 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"
)
Registra tus credenciales con CredentialManager.
try {
val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
// Handle failure
}
Administración de metadatos de la app
Los metadatos que tu app de titular registra con CredentialManager tienen las siguientes
propiedades:
- Persistencia: La información se guarda de forma local y persiste tras 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 identifican con una clave
id, lo que permite volver a identificar, actualizar o borrar registros. - Actualiza los metadatos: Se recomienda actualizar los metadatos persistentes
cada vez que tu app cambie o se cargue por primera vez. Si se llama a un registro varias
veces con el mismo
id, la llamada más reciente reemplaza todos los registros anteriores. Para actualizar, vuelve a registrarte sin necesidad de borrar primero el registro anterior.
Opcional: Crea un comparador
Un comparador es un objeto binario de Wasm que Credential Manager ejecuta en una zona de pruebas para filtrar tus credenciales registradas en función de una solicitud entrante del verificador.
- Comparador predeterminado: La clase
OpenId4VpRegistryincluye automáticamente el comparadorOpenId4VPpredeterminado (OpenId4VpDefaults.DEFAULT_MATCHER) cuando creas una instancia de ella. En todos los casos de uso estándar de OpenID4VP, la biblioteca controla la coincidencia por ti. - Comparador personalizado: Solo implementarías un comparador personalizado si admites un protocolo no estándar que requiere su propia lógica de coincidencia.
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 muestra demuestra este procedimiento.
El intent inicia tu actividad con la solicitud del verificador y el origen de la llamada,
que extraes con la
PendingIntentHandler.retrieveProviderGetCredentialRequest
función. Esto devuelve un objeto ProviderGetCredentialRequest
que contiene toda la información asociada con la solicitud del verificador. Hay
tres componentes clave:
- La app de llamadas: Es la app que realizó la solicitud, que se puede recuperar con
getCallingAppInfo. Puedes usarlo para verificar la identidad del verificador. - La credencial seleccionada: Información sobre el candidato que eligió el usuario, recuperada a través de
selectedCredentialSet extension method; coincidirá con el ID de credencial que registraste. - Solicitudes específicas: Es la solicitud específica que realiza el verificador, que se recupera
del método
getCredentialOptions. Para un flujo de solicitud de credenciales digitales, puedes esperar encontrar un soloGetDigitalCredentialOptionen esta lista.
Puedes procesar la solicitud de presentación con el siguiente código:
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
Primero, extrae el ProviderGetCredentialRequest del intent:
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
Verifica si hay apps con privilegios: Las apps con privilegios (como los navegadores web) pueden realizar llamadas en nombre de otros verificadores si configuran el parámetro de origen. Para recuperar este origen, pasa una lista de emisores con privilegios y de confianza (una lista de entidades permitidas en formato JSON) a la API de CallingAppInfo getOrigin.
val origin = request?.callingAppInfo?.getOrigin(
privilegedAppsJson // Your allow list JSON
)
Si origin no está vacío, se devuelve el origen si el packageName y las huellas digitales del certificado coinciden con una app de tu lista de entidades permitidas. Considera que esta es una llamada con privilegios y establece este origen en la respuesta de OpenID4VP en lugar de calcular el origen con la firma de la app que realiza la llamada.
Si origin está vacío, la solicitud del verificador proviene de una app para Android. El origen
asignado a la respuesta de OpenID4VP se debe calcular 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"
Renderiza la IU del titular
Cuando se selecciona una credencial, se invoca la app de titular, que guía al usuario a través de la IU de la app. Existen dos formas estándares de controlar este flujo de trabajo:
- Si se necesita autenticación de usuario adicional para divulgar la credencial, usa la API de BiometricPrompt. Esto se demuestra en el ejemplo.
- De lo contrario, muchas billeteras optan por una devolución silenciosa renderizando una actividad vacía que devuelve los datos de inmediato a la app de llamadas. Esto minimiza los clics del usuario y proporciona una experiencia más fluida.
Devuelve la respuesta de la credencial
Una vez que tu app de titular esté lista para devolver el resultado, finaliza la actividad con la respuesta de credencial:
PendingIntentHandler.setGetCredentialResponse(
resultData,
GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()
Si hay una excepción, puedes enviar la excepción de credencial 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.