Présenter des identifiants avec votre application de portefeuille

L'API Credential Manager Holder permet à votre application de titulaire Android (également appelée "portefeuille") de gérer et de présenter des identifiants numériques aux outils de validation. Ce guide explique comment enregistrer les identifiants de votre application de stockage auprès de Credential Manager et comment gérer une demande de présentation lorsqu'un utilisateur sélectionne un identifiant. L'API Holder est compatible avec Android 6 (niveau d'API 23) ou version ultérieure.

Ajouter des dépendances

Pour utiliser l'API Credential Manager Holder, ajoutez les dépendances suivantes au script build.gradle de votre module d'application :

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

Enregistrer vos identifiants

Commencez par créer une instance RegistryManager que vous utiliserez pour enregistrer une requête OpenId4VpRegistry.

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

Créer une requête OpenId4VpRegistry

Vous devez enregistrer un OpenId4VpRegistry pour gérer les requêtes OpenID4VP des vérificateurs. En supposant que vous avez chargé des types de données locaux avec vos identifiants de portefeuille (par exemple, sdJwtsFromStorage et mdocsFromStorage), vous devez les convertir en leurs équivalents Jetpack DigitalCredentialEntry : SdJwtEntry et MdocEntry.

Ajouter des SD-JWT au registre

Mappez chaque identifiant SD-JWT local à un SdJwtEntry pour le registre :

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
}

Ajouter des mdocs au registre

Mappez vos identifiants mdoc locaux au type 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
}

Points clés concernant le code

  • ID chiffrés : une méthode de configuration du champ ID consiste à enregistrer un identifiant d'identifiant chiffré, de sorte que vous seul puissiez déchiffrer la valeur.
  • Localisation : les champs d'affichage de l'interface utilisateur pour les deux formats doivent être localisés.

Combiner et enregistrer votre demande

Combinez vos entrées converties et enregistrez la requête auprès de 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.
)

Facultatif : Émission instantanée

Si aucun identifiant existant du type demandé n'est disponible pour un utilisateur, vous pouvez afficher une option permettant d'ajouter un identifiant à votre titulaire. Pour ce faire, ajoutez une liste inlineIssuanceEntries contenant les identifiants compatibles à votre OpenId4VpRegistry :

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    inlineIssuanceEntries = 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"
)

Enregistrez vos identifiants avec CredentialManager.

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

Gestion des métadonnées de l'application

Les métadonnées que votre application de support enregistre auprès de CredentialManager présentent les propriétés suivantes :

  • Persistance : les informations sont enregistrées localement et conservées après les redémarrages.
  • Stockage cloisonné : les enregistrements du registre de chaque application sont stockés séparément, ce qui signifie qu'une application ne peut pas modifier les enregistrements du registre d'une autre application.
  • Mises à jour par clé : les enregistrements du registre de chaque application sont associés à une clé id, ce qui permet de les réidentifier, de les mettre à jour ou de les supprimer.
  • Mettre à jour les métadonnées : il est recommandé de mettre à jour les métadonnées persistantes chaque fois que votre application change ou est chargée pour la première fois. Si un registre est appelé plusieurs fois sous le même id, le dernier appel écrase tous les enregistrements précédents. Pour effectuer la mise à jour, réenregistrez-vous sans avoir à effacer l'ancien enregistrement au préalable.

Facultatif : Créer un outil de mise en correspondance

Un outil de mise en correspondance est un binaire Wasm que le Gestionnaire d'identifiants exécute dans un bac à sable pour filtrer vos identifiants enregistrés par rapport à une requête de vérificateur entrante.

  • Outil de mise en correspondance par défaut : la classe OpenId4VpRegistry inclut automatiquement l'outil de mise en correspondance OpenId4VP par défaut (OpenId4VpDefaults.DEFAULT_MATCHER) lorsque vous l'instanciez. Pour tous les cas d'utilisation OpenID4VP standards, la bibliothèque gère la mise en correspondance pour vous.
  • Outil de mise en correspondance personnalisé : vous n'implémentez un outil de mise en correspondance personnalisé que si vous acceptez un protocole non standard qui nécessite sa propre logique de mise en correspondance.

Gérer un identifiant sélectionné

Lorsqu'un utilisateur sélectionne un justificatif, votre application de portefeuille doit traiter la requête. Vous devez définir une activité qui écoute le filtre d'intent androidx.credentials.registry.provider.action.GET_CREDENTIAL. Notre exemple de portefeuille illustre cette procédure.

L'intent lance votre activité avec la requête de l'outil de validation et l'origine de l'appel, que vous extrayez avec la fonction PendingIntentHandler.retrieveProviderGetCredentialRequest. Cette opération renvoie un ProviderGetCredentialRequest contenant toutes les informations associées à la demande de l'outil de validation. Il se compose de trois éléments clés :

Vous pouvez traiter la demande de présentation avec le code suivant :

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

Vous trouverez un exemple de ce type dans l'exemple de portefeuille.

Vérifier l'identité du vérificateur

Tout d'abord, extrayez le ProviderGetCredentialRequest de l'intent :

val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

Recherchez les applications privilégiées : les applications privilégiées (comme les navigateurs Web) peuvent passer des appels au nom d'autres vérificateurs en définissant le paramètre d'origine. Pour récupérer cette origine, transmettez une liste d'appelants privilégiés et approuvés (une liste d'autorisation au format JSON) à l'API CallingAppInfo getOrigin.

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

Si origin n'est pas vide : l'origine est renvoyée si packageName et les empreintes du certificat correspondent à une application de votre liste d'autorisation. Considérez cet appel comme privilégié et définissez cette origine sur la réponse OpenID4VP au lieu de calculer l'origine à l'aide de la signature de l'application appelante.

Si origin est vide : la requête du vérificateur provient d'une application Android. L'origine mappée à la réponse OpenID4VP doit être calculée comme android:apk-key-hash:<encoded SHA 256 fingerprint>.

val appSigningInfo =
    request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory?.firstOrNull()?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash =
    Base64.encodeToString(md.digest(requireNotNull(appSigningInfo)), Base64.NO_WRAP or Base64.NO_PADDING)
val computedOrigin = "android:apk-key-hash:$certHash"

Afficher l'UI du support

Lorsqu'un identifiant est sélectionné, l'application du titulaire est appelée et guide l'utilisateur dans l'UI de l'application. Il existe deux façons standards de gérer ce workflow :

  • Si une authentification utilisateur supplémentaire est nécessaire pour libérer l'identifiant, utilisez l'API BiometricPrompt. C'est ce que nous allons voir dans l'exemple.
  • Sinon, de nombreux portefeuilles optent pour un retour silencieux en affichant une activité vide qui renvoie immédiatement les données à l'application appelante. Cela réduit le nombre de clics de l'utilisateur et offre une expérience plus fluide.

Renvoyer la réponse d'identifiant

Une fois que votre application de stockage est prête à renvoyer le résultat, terminez l'activité avec la réponse d'identifiant :

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

En cas d'exception, vous pouvez également envoyer l'exception d'identifiant :

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

Consultez l'application exemple pour obtenir un exemple complet de renvoi de la réponse d'identifiant en contexte.