Ce guide explique comment les parties de confiance peuvent intégrer techniquement l'API Digital Credentials pour demander et valider les permis de conduire mobiles (mDL) et les cartes d'identité depuis Google Wallet sur les applications Android et le Web.
Processus d'inscription et prérequis
Avant de passer en production, vous devez enregistrer formellement votre application Relying Party auprès de Google.
- Test dans le bac à sable : vous pouvez commencer le développement immédiatement en utilisant notre environnement de bac à sable et en créant un ID de test. L'acceptation des conditions d'utilisation n'est pas requise pour les tests.
- Envoyez le formulaire de participation : remplissez le formulaire d'intégration au programme de partenaires recommandés. L'intégration prend généralement entre trois et cinq jours ouvrés. Le nom et le logo de votre produit s'afficheront sur l'écran de consentement destiné aux utilisateurs pour les aider à identifier qui demande leurs données.
- Accepter les conditions d'utilisation : vous devez signer les conditions d'utilisation avant de passer en direct.
Formats et fonctionnalités compatibles
Google Wallet est compatible avec les pièces d'identité numériques basées sur la norme ISO mdoc.
- Identifiants acceptés : vous pouvez consulter les identifiants et attributs acceptés.
- Protocoles acceptés : OpenID4VP (version 1.0).
- SDK Android minimal : Android 9 (niveau d'API 28) ou version ultérieure.
- Navigateurs compatibles : pour obtenir la liste complète des navigateurs compatibles avec l'API Digital Credentials, consultez la page Compatibilité de l'écosystème.
Mettre en forme la demande
Pour demander des identifiants à partir d'un portefeuille, vous devez mettre en forme votre demande à l'aide d'OpenID4VP. Vous pouvez demander des identifiants spécifiques ou plusieurs identifiants dans un seul objet dcql_query.
Exemple de requête JSON
Voici un exemple de requête mdoc requestJson permettant d'obtenir des identifiants à partir de n'importe quel portefeuille sur un appareil Android ou sur le Web.
{
"requests" : [
{
"protocol": "openid4vp-v1-signed",
"data": {<signed_credential_request>} // This is an object, shouldn't be a string.
}
]
}
Demander le chiffrement
client_metadata contient la clé publique de chiffrement pour chaque requête.
Vous devrez stocker les clés privées pour chaque requête et les utiliser pour authentifier et autoriser le jeton que vous recevez de l'application de portefeuille.
Le paramètre credential_request dans requestJson contient les champs suivants.
Certificat spécifique
{
"response_type": "vp_token",
"response_mode": "dc_api.jwt", // change this to dc_api if you want to demo with a non encrypted response.
"nonce": "1234",
"dcql_query": {
"credentials": [
{
"id": "cred1",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL" // this is for mDL. Use com.google.wallet.idcard.1 for ID pass
},
"claims": [
{
"path": [
"org.iso.18013.5.1",
"family_name"
],
"intent_to_retain": false // set this to true if you are saving the value of the field
},
{
"path": [
"org.iso.18013.5.1",
"given_name"
],
"intent_to_retain": false
},
{
"path": [
"org.iso.18013.5.1",
"age_over_18"
],
"intent_to_retain": false
}
]
}
]
},
"client_metadata": {
"jwks": {
"keys": [ // sample request encryption key
{
"kty": "EC",
"crv": "P-256",
"x": "pDe667JupOe9pXc8xQyf_H03jsQu24r5qXI25x_n1Zs",
"y": "w-g0OrRBN7WFLX3zsngfCWD3zfor5-NLHxJPmzsSvqQ",
"use": "enc",
"kid" : "1", // This is required
"alg" : "ECDH-ES", // This is required
}
]
},
"vp_formats_supported": {
"mso_mdoc": {
"deviceauth_alg_values": [
-7
],
"isserauth_alg_values": [
-7
]
}
}
}
}
Tout identifiant éligible
Voici un exemple de requête pour la carte d'identité et la carte d'identité numérique. L'utilisateur peut choisir l'une ou l'autre.
{
"response_type": "vp_token",
"response_mode": "dc_api.jwt", // change this to dc_api if you want to demo with a non encrypted response.
"nonce": "1234",
"dcql_query": {
"credentials": [
{
"id": "mdl-request",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"path": [
"org.iso.18013.5.1",
"family_name"
],
"intent_to_retain": false // set this to true if you are saving the value of the field
},
{
"path": [
"org.iso.18013.5.1",
"given_name"
],
"intent_to_retain": false
},
{
"path": [
"org.iso.18013.5.1",
"age_over_18"
],
"intent_to_retain": false
}
]
},
{ // Credential type 2
"id": "id_pass-request",
"format": "mso_mdoc",
"meta": {
"doctype_value": "com.google.wallet.idcard.1"
},
"claims": [
{
"path": [
"org.iso.18013.5.1",
"family_name"
],
"intent_to_retain": false // set this to true if you are saving the value of the field
},
{
"path": [
"org.iso.18013.5.1",
"given_name"
],
"intent_to_retain": false
},
{
"path": [
"org.iso.18013.5.1",
"age_over_18"
],
"intent_to_retain": false
}
]
}
]
credential_sets : [
{
"options": [
[ "mdl-request" ],
[ "id_pass-request" ]
]
}
]
},
"client_metadata": {
"jwks": {
"keys": [ // sample request encryption key
{
"kty": "EC",
"crv": "P-256",
"x": "pDe667JupOe9pXc8xQyf_H03jsQu24r5qXI25x_n1Zs",
"y": "w-g0OrRBN7WFLX3zsngfCWD3zfor5-NLHxJPmzsSvqQ",
"use": "enc",
"kid" : "1", // This is required
"alg" : "ECDH-ES", // This is required
}
]
},
"vp_formats_supported": {
"mso_mdoc": {
"deviceauth_alg_values": [
-7
],
"isserauth_alg_values": [
-7
]
}
}
}
}
Vous pouvez demander n'importe quel nombre d'attributs compatibles à partir de n'importe quel identifiant stocké dans Google Wallet.
Requêtes signées
Les requêtes signées (requêtes d'autorisation sécurisées par JWT) encapsulent votre demande de présentation vérifiable dans un jeton Web JSON (JWT) signé de manière cryptographique à l'aide de votre infrastructure PKI. Elles garantissent ainsi l'intégrité de la requête et prouvent votre identité à Google Wallet.
Prérequis
Avant d'implémenter les modifications de code pour les requêtes signées, assurez-vous d'avoir :
- Clé privée : vous avez besoin d'une clé privée (par exemple,
ES256à courbe elliptique) pour signer la requête gérée sur votre serveur. - Certificat : vous avez besoin d'un certificat X.509 standard dérivé de votre paire de clés.
- Enregistrement : assurez-vous que votre certificat public est enregistré auprès de Google Wallet. Contactez notre équipe d'assistance à l'adresse
wallet-identity-rp-support@google.com.
Logique de construction des requêtes
Pour créer une requête, vous devez utiliser votre clé privée et encapsuler la charge utile dans un JWS.
def construct_openid4vp_request(
doctypes: list[str],
requested_fields: list[dict],
nonce_base64: str,
jwe_encryption_public_jwk: jwk.JWK,
is_zkp_request: bool,
is_signed_request: bool,
state: dict,
origin: str
) -> dict:
# ... [Existing logic to build 'presentation_definition' and basic 'request_payload'] ...
# ------------------------------------------------------------------
# SIGNED REQUEST IMPLEMENTATION (JAR)
# ------------------------------------------------------------------
if is_signed_request:
try:
# 1. Load the Verifier's Certificate
# We must load the PEM string into a cryptography x509 object
verifier_cert_obj = x509.load_pem_x509_certificate(
CERTIFICATE.encode('utf-8'),
backend=default_backend()
)
# 2. Calculate Client ID (x509_hash)
# We calculate the SHA-256 hash of the DER-encoded certificate.
cert_der = verifier_cert_obj.public_bytes(serialization.Encoding.DER)
verifier_fingerprint_bytes = hashlib.sha256(cert_der).digest()
# Create a URL-safe Base64 hash (removing padding '=')
verifier_fingerprint_b64 = base64.urlsafe_b64encode(verifier_fingerprint_bytes).decode('utf-8').rstrip("=")
# Format the client_id as required by the spec
client_id = f'x509_hash:{verifier_fingerprint_b64}'
# 3. Update Request Payload with JAR specific fields
request_payload["client_id"] = client_id
# Explicitly set expected origins to prevent relay attacks
# Format for android origin: origin = android:apk-key-hash:<base64SHA256_ofAppSigningCert>
# Format for web origin: origin = <origin_url>
if origin:
request_payload["expected_origins"] = [origin]
# 4. Create Signed JWT (JWS)
# Load the signing private key
signing_key = jwk.JWK.from_pem(PRIVATE_KEY.encode('utf-8'))
# Initialize JWS with the JSON payload
jws_token = jws.JWS(json.dumps(request_payload).encode('utf-8'))
# Construct the JOSE Header
# 'x5c' (X.509 Certificate Chain) is critical: it allows the wallet
# to validate your key against the one registered in the console.
x5c_value = base64.b64encode(cert_der).decode('utf-8')
protected_header = {
"alg": "ES256", # Algorithm (e.g., ES256 or RS256)
"typ": "oauth-authz-req+jwt", # Standard type for JAR
"kid": "1", # Key ID
"x5c": [x5c_value] # Embed the certificate
}
# Sign the token
jws_token.add_signature(
key=signing_key,
alg=None,
protected=json_encode(protected_header)
)
# 5. Return the Request Object
# Instead of returning the raw JSON, we return the signed JWT string
# under the 'request' key.
return {"request": jws_token.serialize(compact=True)}
except Exception as e:
print(f"Error signing OpenID4VP request: {e}")
return None
# ... [Fallback for unsigned requests] ...
return request_payload
Déclencher l'API
L'intégralité de la requête API doit être générée côté serveur. Selon la plate-forme, vous transmettrez le fichier JSON généré aux API natives.
Dans l'application (Android)
Pour demander des identifiants d'identité depuis vos applications Android, procédez comme suit :
Mettre à jour les dépendances
Dans le fichier build.gradle de votre projet, mettez à jour vos dépendances pour utiliser le Gestionnaire d'identifiants (bêta) :
dependencies {
implementation("androidx.credentials:credentials:1.5.0-beta01")
implementation("androidx.credentials:credentials-play-services-auth:1.5.0-beta01")
}
Configurer le Gestionnaire d'identifiants
Pour configurer et initialiser un objet CredentialManager, ajoutez une logique semblable à celle-ci :
// Use your app or activity context to instantiate a client instance of CredentialManager.
val credentialManager = CredentialManager.create(context)
Demander des attributs d'identité
Au lieu de spécifier des paramètres individuels pour les requêtes d'identité, l'application les fournit tous ensemble sous forme de chaîne JSON dans CredentialOption.
Le Gestionnaire d'identifiants transmet cette chaîne JSON aux portefeuilles numériques disponibles sans examiner son contenu. Chaque portefeuille est ensuite responsable des éléments suivants :
- Analyser la chaîne JSON pour comprendre la demande d'identité.
- Déterminer quels identifiants stockés, le cas échéant, répondent à la demande.
Nous recommandons aux partenaires de créer leurs requêtes sur le serveur, même pour les intégrations d'applications Android.
Vous utiliserez le requestJson de la section Format de la requête comme request dans l'appel de la fonction GetDigitalCredentialOption().
// The request in the JSON format to conform with
// the JSON-ified Digital Credentials API request definition.
val requestJson = generateRequestFromServer()
val digitalCredentialOption =
GetDigitalCredentialOption(requestJson = requestJson)
// Use the option from the previous step to build the `GetCredentialRequest`.
val getCredRequest = GetCredentialRequest(
listOf(digitalCredentialOption)
)
coroutineScope.launch {
try {
val result = credentialManager.getCredential(
context = activityContext,
request = getCredRequest
)
verifyResult(result)
} catch (e : GetCredentialException) {
handleFailure(e)
}
}
Gérer la réponse d'identifiant
Une fois que vous avez reçu une réponse du portefeuille, vérifiez si elle est positive et contient la réponse credentialJson.
// Handle the successfully returned credential.
fun verifyResult(result: GetCredentialResponse) {
val credential = result.credential
when (credential) {
is DigitalCredential -> {
val responseJson = credential.credentialJson
validateResponseOnServer(responseJson) // make a server call to validate the response
}
else -> {
// Catch any unrecognized credential type here.
Log.e(TAG, "Unexpected type of credential ${credential.type}")
}
}
}
// Handle failure.
fun handleFailure(e: GetCredentialException) {
when (e) {
is GetCredentialCancellationException -> {
// The user intentionally canceled the operation and chose not
// to share the credential.
}
is GetCredentialInterruptedException -> {
// Retry-able error. Consider retrying the call.
}
is NoCredentialException -> {
// No credential was available.
}
else -> Log.w(TAG, "Unexpected exception type ${e::class.java}")
}
}
La réponse credentialJson contient un identityToken (JWT) chiffré, défini par le W3C. L'application Wallet est responsable de la création de cette réponse.
Exemple :
{
"protocol" : "openid4vp-v1-signed",
"data" : {
<encrpted_response>
}
}
Vous renverrez cette réponse au serveur pour valider son authenticité. Vous trouverez la procédure de validation de la réponse d'identifiant.
Web
Pour demander des identifiants à l'aide de l'API Digital Credentials sur Chrome ou d'autres navigateurs compatibles, envoyez la requête suivante.
const credentialResponse = await navigator.credentials.get({
digital : {
requests : [
{
protocol: "openid4vp-v1-signed",
data: {<credential_request>} // This is an object, shouldn't be a string.
}
]
}
})
Renvoyez la réponse de cette API à votre serveur pour valider la réponse des identifiants.
Valider la réponse
Une fois que le portefeuille renvoie le identityToken (JWT) chiffré, vous devez effectuer une validation stricte côté serveur avant de faire confiance aux données.
Déchiffrer la réponse
Utilisez la clé privée correspondant à la clé publique envoyée dans le client_metadata de la requête pour déchiffrer le JWE. Cela génère un vp_token.
Exemple Python :
from jwcrypto import jwe, jwk
# Retrieve the Private Key from Datastore
reader_private_jwk = jwk.JWK.from_json(jwe_private_key_json_str)
# Save public key thumbprint for session transcript
encryption_public_jwk_thumbprint = reader_private_jwk.thumbprint()
# Decrypt the JWE encrypted response from Google Wallet
jwe_object = jwe.JWE()
jwe_object.deserialize(encrypted_jwe_response_from_wallet)
jwe_object.decrypt(reader_private_jwk)
decrypted_payload_bytes = jwe_object.payload
decrypted_data = json.loads(decrypted_payload_bytes)
decrypted_data génère un fichier JSON vp_token contenant les identifiants.
{
"vp_token":
{
"cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
}
}
Créer la transcription de la session
L'étape suivante consiste à créer le SessionTranscript à partir de la norme ISO/IEC 18013-5:2021 avec une structure de transfert spécifique à Android ou au Web :
SessionTranscript = [ null, // DeviceEngagementBytes not available null, // EReaderKeyBytes not available [ "OpenID4VPDCAPIHandover", AndroidHandoverDataBytes // BrowserHandoverDataBytes for Web ] ]Pour les transferts Android et Web, vous devez utiliser le même nonce que celui utilisé pour générer
credential_request.Transfert Android
AndroidHandoverData = [ origin, // "android:apk-key-hash:<base64SHA256_ofAppSigningCert>", nonce, // nonce that was used to generate credential request, encryption_public_jwk_thumbprint, // Encryption public key (JWK) Thumbprint ] AndroidHandoverDataBytes = hashlib.sha256(cbor2.dumps(AndroidHandoverData)).digest()
Transfert vers le navigateur
BrowserHandoverData =[ origin, // Origin URL nonce, // nonce that was used to generate credential request encryption_public_jwk_thumbprint, // Encryption public key (JWK) Thumbprint ] BrowserHandoverDataBytes = hashlib.sha256(cbor2.dumps(BrowserHandoverData)).digest()
À l'aide de
SessionTranscript, la réponse de l'appareil doit être validée conformément à la clause 9 de la norme ISO/IEC 18013-5:2021. Cela inclut plusieurs étapes, par exemple :Vérifiez le certificat de l'émetteur de l'État. Consultez les certificats IACA des émetteurs acceptés.
Valider la signature du MSO (18013-5, section 9.1.2)
Calculer et vérifier
ValueDigestspour les éléments de données (section 9.1.2 de la norme 18013-5)Valider la signature
deviceSignature(18013-5, section 9.1.3)
{
"version": "1.0",
"documents": [
{
"docType": "org.iso.18013.5.1.mDL",
"issuerSigned": {
"nameSpaces": {...}, // contains data elements
"issuerAuth": [...] // COSE_Sign1 w/ issuer PK, mso + sig
},
"deviceSigned": {
"nameSpaces": 24(<< {} >>), // empty
"deviceAuth": {
"deviceSignature": [...] // COSE_Sign1 w/ device signature
}
}
}
],
"status": 0
}
Vérification de l'âge respectueuse de la confidentialité (ZKP)
Pour prendre en charge les preuves à divulgation nulle de connaissance (par exemple, pour vérifier qu'un utilisateur a plus de 18 ans sans connaître sa date de naissance exacte), définissez le format de votre requête sur mso_mdoc_zk et fournissez la configuration zk_system_type requise.
...
"dcql_query": {
"credentials": [{
"id": "cred1",
"format": "mso_mdoc_zk",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
"zk_system_type": [
{
"system": "longfellow-libzk-v1",
"circuit_hash": "f88a39e561ec0be02bb3dfe38fb609ad154e98decbbe632887d850fc612fea6f", // This will differ if you need more than 1 attribute.
"num_attributes": 1, // number of attributes (in claims) this has can support
"version": 5,
"block_enc_hash": 4096,
"block_enc_sig": 2945,
}
{
"system": "longfellow-libzk-v1",
"circuit_hash": "137e5a75ce72735a37c8a72da1a8a0a5df8d13365c2ae3d2c2bd6a0e7197c7c6", // This will differ if you need more than 1 attribute.
"num_attributes": 1, // number of attributes (in claims) this has can support
"version": 6,
"block_enc_hash": 4096,
"block_enc_sig": 2945,
}
],
"verifier_message": "challenge"
},
"claims": [{
...
"client_metadata": {
"jwks": {
"keys": [ // sample request encryption key
{
...
Vous recevrez une preuve à divulgation nulle de connaissance chiffrée du portefeuille. Vous pouvez valider cette preuve par rapport aux certificats IACA des émetteurs à l'aide de la bibliothèque longfellow-zk de Google.
Le service de validation contient un serveur Docker prêt au déploiement qui vous permet de valider la réponse par rapport à certains certificats IACA de l'émetteur.
Vous pouvez modifier certs.pem pour gérer les certificats d'émetteur IACA auxquels vous souhaitez faire confiance.
Ressources et assistance
- Mise en œuvre de référence : consultez notre mise en œuvre de référence des validateurs d'identité sur GitHub.
- Site Web de test : essayez le parcours de bout en bout sur verifier.multipaz.org.
- Spécification OpenID4VP : consultez la spécification technique pour openID4VP.
- Assistance : pour obtenir de l'aide concernant le débogage ou si vous avez des questions lors de l'intégration, contactez
wallet-identity-rp-support@google.com.