Acceptation en ligne des identifiants numériques

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.

  1. 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.
  2. 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.
  3. 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.

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.
    }
  }
  1. 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 :

  2. Vérifiez le certificat de l'émetteur de l'État. Consultez les certificats IACA des émetteurs acceptés.

  3. Valider la signature du MSO (18013-5, section 9.1.2)

  4. Calculer et vérifier ValueDigests pour les éléments de données (section 9.1.2 de la norme 18013-5)

  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.