Ce guide explique comment les parties de confiance directes (marchands, entreprises et développeurs d'applications individuels) intègrent l'API Digital Credentials pour demander et valider des identifiants d'identité numérique dans les applications Android et sur le Web.
Choisissez votre parcours RP, Verifier Registrar ou en personne
Fonctionnement de l'intégration de la partie de confiance
Chaque intégration de partie de confiance suit quatre étapes. Les étapes A et C s'exécutent sur votre serveur, tandis que l'étape B s'exécute dans votre application Android ou votre navigateur. Sélectionnez une étape pour accéder aux détails de son implémentation :

Formats, identifiants et fonctionnalités compatibles
Google Wallet est compatible avec les pièces d'identité numériques basées sur ISO mdoc via OpenID4VP (version 1.0) sur Android 9 (niveau d'API 28) et versions ultérieures et sur les navigateurs compatibles.
Identifiants et attributs compatibles
Recherchez les identifiants doctype_value (mDL, pass d'identité, pièce d'identité avec photo) et les chemins d'accès aux revendications d'attributs que vous pouvez demander à Google Wallet.
Émetteurs et certificats IACA acceptés
Téléchargez les certificats racine IACA officiels pour les États américains, les émetteurs internationaux et l'émetteur Sandbox Utopia afin de valider issuerAuth.
Étape A : Mettre en forme et signer la requête (côté serveur)
Pour demander des identifiants à partir de n'importe quel portefeuille, vous devez formater votre requête à l'aide d'OpenID4VP sur votre serveur. Vous pouvez demander un ou plusieurs identifiants spécifiques dans un seul objet dcql_query.
Exemple de requête JSON
Voici un exemple de requête mdoc requestJson permettant d'obtenir des identifiants d'identité à 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.
}
]
}
Chiffrement des requêtes
client_metadata contient la clé publique de chiffrement pour chaque requête.
Vous devez 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.
Métadonnées OpenID4VP intégrées
Lorsque vous mettez en forme votre demande d'identifiants, vous devez inclure le champ gw_rp_metadata_bytes dans l'objet client_metadata (comme indiqué dans l'exemple de code de requête ci-dessous). Ce champ contient les métadonnées de la partie de confiance encodées en Base64URL, requises par Google Wallet pour valider votre identité et afficher votre branding à l'utilisateur.
Charge utile de la demande d'identifiants
Le paramètre credential_request dans requestJson contient les champs suivants.
Certification 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": {
// Use org.iso.18013.5.1.mDL for mDL,
// com.google.wallet.idcard.1 for ID pass, or
// org.iso.23220.photoid.1 for ID pass (using photo ID document type)
"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
}
]
}
]
},
"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
],
"issuerauth_alg_values": [
-7
]
}
},
"gw_rp_metadata_bytes": "<base64url encoded metadata string>"
}
}
Tout identifiant éligible
Voici un exemple de requête pour un permis de conduire mobile, un pass d'identité et un pass d'identité (utilisant le type de document "pièce d'identité avec photo"). L'utilisateur peut continuer avec 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 pass
"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 type 3: ID pass (using photo ID document type)
"id": "photo_id-request",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.23220.photoid.1"
},
"claims": [
{
"path": [
"org.iso.23220.1",
"family_name"
],
"intent_to_retain": false // set this to true if you are saving the value of the field
},
{
"path": [
"org.iso.23220.1",
"given_name"
],
"intent_to_retain": false
},
{
"path": [
"org.iso.23220.1",
"age_over_18"
],
"intent_to_retain": false
}
]
}
]
credential_sets : [
{
"options": [
[ "mdl-request" ],
[ "id_pass-request" ],
[ "photo_id-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
],
"issuerauth_alg_values": [
-7
]
}
},
"gw_rp_metadata_bytes": "<base64url encoded metadata string>"
}
}
Vous pouvez demander autant d'attributs compatibles que vous le souhaitez à partir de n'importe quel document d'identité stocké dans Google Wallet.
Requêtes signées
Les requêtes signées (requêtes d'autorisation sécurisées par JWT) encapsulent votre requête de présentation vérifiable dans un jeton Web JSON (JWT) signé de manière cryptographique à l'aide de votre infrastructure PKI, ce qui garantit l'intégrité de la requête et prouve votre identité à Google Wallet.
Prérequis
Avant d'implémenter les modifications de code pour les requêtes signées, assurez-vous de disposer des éléments suivants :
- Clé privée : vous avez besoin d'une clé privée (par exemple, la courbe elliptique
ES256) pour signer la requête, gérée sur votre serveur. Pour les tests en bac à sable en tant que partie de confiance, utilisez la clé privée de test en bac à sable. - 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 (ou l'autorité de certification racine pour les bureaux d'enregistrement de vérificateurs) est approuvé par Google Wallet.
Logique de construction de la requête
Lorsque vous créez et signez votre requête OpenID4VP, faites attention au format d'encodage exact attendu pour chaque champ. Si vous mélangez base64 et base64url ou le remplissage (=), la validation de la signature ou de l'origine échouera :
| Champ | Encodage / Format requis | Caractères et marge intérieure |
|---|---|---|
client_id (x509_hash:<hash>) |
base64url sans marge intérieure | Utilise l'alphabet sécurisé pour les URL (-, _) ; supprime le caractère = de fin |
expected_origins (android:apk-key-hash:<hash>) |
Base64 standard sans marge intérieure | Utilise l'alphabet standard (+, /) ; supprime le = de fin |
expected_origins (https://...) |
URL en minuscules sans barre oblique finale | Exemple : https://example.com (sans / à la fin) |
En-tête JWS x5c |
Base64 standard avec marge intérieure | Utilise l'alphabet standard (+, /) ; conserve le remplissage = de fin |
Pour créer une requête, utilisez votre clé privée et encapsulez 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()
# Important: Use URL-safe Base64 (base64url: '-', '_') without padding ('=') for client_id
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
# Important: Use standard Base64 (not URL-safe) without padding for android:apk-key-hash:
# origin = f"android:apk-key-hash:{base64.b64encode(apk_cert_hash).decode('utf-8').rstrip('=')}"
# Format for web origin (lowercase URL without trailing slash): origin = "https://example.com"
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.
# Important: Use standard Base64 ('+', '/') WITH padding ('=') for x5c
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
Étape B : Déclencher l'API (côté client)
La requête API doit être entièrement générée côté serveur. Selon la plate-forme, transmettez le fichier JSON généré aux API de la plate-forme.
Dans l'application (Android)
Pour demander des identifiants à 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)
Attributs d'identité de la demande
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 le CredentialOption.
Credential Manager transmet cette chaîne JSON aux portefeuilles numériques disponibles sans examiner son contenu. Chaque portefeuille est ensuite responsable de :
- Analyse de la chaîne JSON pour comprendre la requête d'identité.
- Déterminer lesquels de ses identifiants stockés, le cas échéant, répondent à la requête.
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 Formater et signer la requête comme request dans l'appel de 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 chargée de créer cette réponse.
Exemple :
{
"protocol" : "openid4vp-v1-signed",
"data" : {
<encrypted_response>
}
}
Transmettez cette réponse à votre serveur pour valider son authenticité. Consultez Déchiffrer et valider la réponse.
Web
Pour demander des identifiants d'identité à 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 d'identification.
Étape C : Déchiffrer et valider la réponse (côté serveur)
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.
}
}
Reconstruire le SessionTranscript et valider la DeviceResponse
- Créez 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, utilisez le même nonce que celui utilisé pour générer credential_request.
Android Handover
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/CEI 18013-5:2021.
Cette validation comprend quatre vérifications :
- Vérifier le certificat de l'émetteur : extrayez la chaîne de certificats de signature de l'émetteur de
issuerAuthet validez-la par rapport aux certificats racine IACA de confiance. Consultez Émetteurs et certificats IACA compatibles (et ajoutez le certificat Utopia Sandbox lorsque vous effectuez des tests avec un ID de test de bac à sable). - Valider la signature MSO (ISO/IEC 18013-5, section 9.1.2).
- Calculez et vérifiez
ValueDigestspour les éléments de données (ISO/IEC 18013-5, section 9.1.2). - Vérifier la signature
deviceSignature(section 9.1.3 de la norme ISO/CEI 18013-5).
{
"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
}
Étape D (facultative) : Vérification de l'âge protégeant la confidentialité (ZKP)
Pour accepter les preuves à divulgation nulle de connaissance (par exemple, vérifier qu'un utilisateur a plus de 18 ans sans voir sa date de naissance exacte), remplacez le format de votre requête par mso_mdoc_zk et fournissez la configuration zk_system_type requise.
Pour une présentation générale de ZKP et de ses fonctionnalités, consultez les questions fréquentes.
...
"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 de la part du portefeuille. Vous pouvez valider cette preuve par rapport aux certificats IACA de l'émetteur à l'aide de la bibliothèque longfellow-zk de Google.
Le service de vérification contient un serveur Docker prêt pour le déploiement qui vous permet de valider la réponse par rapport aux certificats IACA de l'émetteur.
Vous pouvez modifier certs.pem pour gérer les certificats d'émetteur IACA auxquels vous souhaitez faire confiance.
SDK, implémentations de référence et assistance
Utilisez nos bibliothèques Open Source, notre serveur de référence et notre outil de validation Web pour accélérer votre implémentation :
| Ressource / Bibliothèque | Fonctionnalités proposées | Lien |
|---|---|---|
| Implémentation de référence des validateurs d'identité | Exemple de serveur et de client illustrant la génération de requêtes OpenID4VP, la signature JWS, le déchiffrement JWE et la validation des réponses mdoc. | Dépôt GitHub |
| SDK Multipaz | Bibliothèque Open Source (OpenWallet Foundation) incluant DeviceResponseParser pour le décodage CBOR, la reconstruction SessionTranscript et la validation de signature ISO/IEC 18013-5/7. |
openwallet-foundation/multipaz |
| Bibliothèque ZKP et service de vérification Longfellow | Bibliothèque Open Source et serveur basé sur Docker pour vérifier les assertions d'âge et d'attributs de preuve à divulgation nulle de connaissance (mso_mdoc_zk). |
google/longfellow-zk |
| Outil de validation Web interactif | Outil de validation Web hébergé pour tester les requêtes de l'API W3C Digital Credentials / OpenID4VP avec Google Wallet. | verifier.multipaz.org |
| Spécification OpenID4VP 1.0 | Spécification officielle du protocole OpenID for Verifiable Presentations 1.0. | Spécification OpenID4VP |
| Questions fréquentes et assistance pour les développeurs | Réponses aux questions fréquentes sur l'intégration technique, ou envoyez un e-mail à wallet-identity-rp-support@google.com pour obtenir de l'aide sur le débogage. |
Questions fréquentes sur l'intégration technique |