Ce guide explique comment les parties de confiance peuvent intégrer techniquement l'API Digital Credentials pour demander et valider des identifiants numériques dans les applications Android et sur 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. Nous utilisons un processus de signature de certificat dans lequel Google signe votre certificat pour établir la confiance.
- Test dans le bac à sable : vous pouvez commencer le développement immédiatement sans envoyer de formulaire d'inscription. Vous pouvez effectuer des tests directement à l'aide des clés de test pré-approuvées et des exemples de métadonnées publiés sur la page Mode Sandbox.
- Enregistrez le flux de bout en bout : une fois les tests terminés et l'intégration fonctionnelle dans le bac à sable, enregistrez une vidéo de bout en bout montrant l'intégralité de votre flux d'intégration.
- Envoyez le formulaire d'inscription et acceptez les conditions d'utilisation : remplissez et envoyez le formulaire d'inscription de la partie de confiance.
Le formulaire nécessite les informations suivantes :
- Votre demande de signature de certificat (CSR) de production.
- Vos assets à afficher : URL du logo, nom à afficher, URL des règles de confidentialité et URL des conditions d'utilisation.
- Vidéo de bout en bout de votre intégration dans le bac à sable.
Une fois votre demande approuvée, Google vous fournira un certificat signé et vos métadonnées uniques encodées en Base64URL (gw_rp_metadata_bytes).
Détails de l'intégration technique
Les sections suivantes couvrent les détails techniques de l'intégration pour les parties de confiance qui s'intègrent directement à l'API Digital Credentials (y compris le formatage des requêtes, le chiffrement des requêtes, le déclenchement de l'API, la validation des réponses et l'implémentation des preuves à divulgation nulle de connaissance).
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 compatibles : 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 sur la compatibilité avec l'écosystème.
- Questions fréquentes : pour toute question concernant les pays acceptés et le calendrier des nouvelles régions, consultez les questions fréquentes sur les identifiants et les données.
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 requestJson mdoc 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.
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 vérifier votre identité et afficher votre image de marque à l'utilisateur.
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
],
"issuerauth_alg_values": [
-7
]
}
},
"gw_rp_metadata_bytes": "<base64url encoded metadata string>"
}
}
Tout identifiant éligible
Voici un exemple de requête pour la mDL et le pass d'identité. 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
],
"issuerauth_alg_values": [
-7
]
}
},
"gw_rp_metadata_bytes": "<base64url encoded metadata string>"
}
}
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.
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 de la plate-forme.
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 Credential Manager (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 lesquels de ses 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 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.Cette validation comprend plusieurs étapes :
Vérifiez le certificat de l'émetteur : extrayez la chaîne de certificats de signature de l'émetteur à partir de
issuerAuthet validez-la par rapport aux certificats racine IACA approuvés. 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.
Pour obtenir une présentation générale de la preuve à divulgation nulle de connaissance et de ses capacité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
{
...
Le portefeuille vous renverra une preuve à divulgation nulle de connaissance chiffrée. 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
- Questions fréquentes : pour obtenir des réponses aux questions fréquentes sur l'intégration technique, consultez les questions fréquentes sur les identités et les identifiants numériques.
- 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.