Online-Akzeptanz digitaler Anmeldedaten

In diesem Leitfaden wird erläutert, wie sich vertrauende Parteien (Relying Parties, RPs) technisch in die Digital Credentials API einbinden können, um digitale Führerscheine (Mobile Driver's Licenses, mDL) und Ausweise in Google Wallet in Android-Apps und im Web anzufordern und zu validieren.

Registrierungsprozess und Voraussetzungen

Bevor Sie Ihre Anwendung für die vertrauende Partei in der Produktion veröffentlichen, müssen Sie sie offiziell bei Google registrieren.

  1. In der Sandbox testen:Sie können sofort mit der Entwicklung beginnen, indem Sie unsere Sandbox-Umgebung und die Anleitung zum Erstellen einer Test-ID verwenden. Das Akzeptieren der Nutzungsbedingungen ist für das Testen nicht erforderlich.
  2. Aufnahmeformular einreichen:Füllen Sie das Einrichtungsformular für das Partnerprogramm aus. Das Onboarding dauert in der Regel 3–5 Arbeitstage. Ihr Produktname und Ihr Logo werden auf dem Zustimmungsbildschirm für Nutzer angezeigt, damit diese erkennen können, wer ihre Daten anfordert.
  3. Nutzungsbedingungen akzeptieren:Du musst die Nutzungsbedingungen unterzeichnen, bevor du live gehst.
streng verboten.

Unterstützte Formate und Funktionen

Google Wallet unterstützt digitale Ausweise, die auf ISO mdoc basieren.

Anfrage formatieren

Wenn Sie Anmeldedaten aus einer beliebigen Wallet anfordern möchten, müssen Sie Ihre Anfrage mit OpenID4VP formatieren. Sie können bestimmte Anmeldedaten oder mehrere Anmeldedaten in einem einzelnen dcql_query-Objekt anfordern.

Beispiel für JSON-Anfrage

Hier ist ein Beispiel für eine requestJson-Anfrage für ein mobiles Dokument, um Identitätsnachweise von einem beliebigen Wallet auf einem Android-Gerät oder im Web abzurufen.

{
      "requests" : [
        {
          "protocol": "openid4vp-v1-signed",
          "data": {<signed_credential_request>} // This is an object, shouldn't be a string.
        }
      ]
}

Verschlüsselung anfordern

Das client_metadata enthält den öffentlichen Verschlüsselungsschlüssel für jede Anfrage. Sie müssen private Schlüssel für jede Anfrage speichern und damit das Token authentifizieren und autorisieren, das Sie von der Wallet App erhalten.

Der Parameter credential_request in requestJson enthält die folgenden Felder.

Spezifische Anmeldedaten

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

Alle infrage kommenden Anmeldedaten

Hier ist die Beispielanfrage für sowohl mDL als auch ID-Karte. Der Nutzer kann mit einer der beiden Optionen fortfahren.

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

Sie können eine beliebige Anzahl von unterstützten Attributen aus einem beliebigen in Google Wallet gespeicherten Identitätsnachweis anfordern.

Signierte Anfragen

Signierte Anfragen (JWT-gesicherte Autorisierungsanfragen) kapseln Ihre Anfrage für die überprüfbare Präsentation in einem kryptografisch signierten JSON-Webtoken (JWT) unter Verwendung Ihrer PKI-Infrastruktur. So wird die Integrität der Anfrage sichergestellt und Ihre Identität gegenüber Google Wallet nachgewiesen.

Vorbereitung

Bevor Sie die Codeänderungen für signierte Anfragen implementieren, müssen Sie Folgendes sicherstellen:

  • Privater Schlüssel:Sie benötigen einen privaten Schlüssel (z. B. Elliptic Curve ES256), um die Anfrage zu signieren. Dieser wird auf Ihrem Server verwaltet.
  • Zertifikat:Sie benötigen ein Standard-X.509-Zertifikat, das von Ihrem Schlüsselpaar abgeleitet ist.
  • Registrierung:Ihr öffentliches Zertifikat muss bei Google Wallet registriert sein. Wenden Sie sich an unser Supportteam unter wallet-identity-rp-support@google.com.

Logik für die Erstellung von Anfragen

Um eine Anfrage zu erstellen, müssen Sie Ihren privaten Schlüssel verwenden und die Nutzlast in eine JWS einbetten.

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

API auslösen

Die gesamte API-Anfrage sollte serverseitig generiert werden. Je nach Plattform übergeben Sie das generierte JSON an die nativen APIs.

In-App (Android)

So fordern Sie Identitätsanmeldedaten von Ihren Android-Apps an:

Abhängigkeiten aktualisieren

Aktualisieren Sie in der Datei „build.gradle“ Ihres Projekts die Abhängigkeiten, um Credential Manager (Beta) zu verwenden:

dependencies {
    implementation("androidx.credentials:credentials:1.5.0-beta01")
    implementation("androidx.credentials:credentials-play-services-auth:1.5.0-beta01")
}

Credential Manager konfigurieren

Fügen Sie zum Konfigurieren und Initialisieren eines CredentialManager-Objekts eine ähnliche Logik wie die folgende hinzu:

// Use your app or activity context to instantiate a client instance of CredentialManager.
val credentialManager = CredentialManager.create(context)

Attribute zur Identität anfordern

Anstatt einzelne Parameter für Identitätsanfragen anzugeben, stellt die App sie alle zusammen als JSON-String innerhalb von CredentialOption bereit. Der Credential Manager übergibt diesen JSON-String an die verfügbaren digitalen Wallets, ohne seinen Inhalt zu prüfen. Jede Wallet ist dann für Folgendes verantwortlich: - Parsen des JSON-Strings, um die Identitätsanfrage zu verstehen. – Es wird ermittelt, welche der gespeicherten Anmeldedaten die Anfrage erfüllen.

Wir empfehlen Partnern, ihre Anfragen auch für Android-App-Integrationen auf dem Server zu erstellen.

Sie verwenden die requestJson aus dem Anfrageformat als request im GetDigitalCredentialOption()-Funktionsaufruf.

// 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)
    }
}

Anmeldedatenantwort verarbeiten

Sobald Sie eine Antwort vom Wallet erhalten haben, prüfen Sie, ob die Antwort erfolgreich war und die credentialJson-Antwort enthält.

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

Die credentialJson-Antwort enthält ein verschlüsseltes identityToken (JWT), das vom W3C definiert wird. Die Wallet App ist für die Erstellung dieser Antwort verantwortlich.

Beispiel:

{
  "protocol" : "openid4vp-v1-signed",
  "data" : {
    <encrpted_response>
  }
}

Sie geben diese Antwort an den Server zurück, um ihre Authentizität zu bestätigen. Hier finden Sie eine Anleitung zum Validieren von Anmeldedatenantworten.

Web

Wenn Sie Identitätsanmeldedaten mit der Digital Credentials API in Chrome oder anderen unterstützten Browsern anfordern möchten, stellen Sie die folgende Anfrage.

const credentialResponse = await navigator.credentials.get({
          digital : {
          requests : [
            {
              protocol: "openid4vp-v1-signed",
              data: {<credential_request>} // This is an object, shouldn't be a string.
            }
          ]
        }
      })

Senden Sie die Antwort von dieser API zurück an Ihren Server, um die Anmeldedatenantwort zu validieren.

Antwort validieren

Sobald das Wallet das verschlüsselte identityToken (JWT) zurückgibt, müssen Sie eine strenge serverseitige Validierung durchführen, bevor Sie den Daten vertrauen.

Antwort entschlüsseln

Verwenden Sie den privaten Schlüssel, der dem im client_metadata der Anfrage gesendeten öffentlichen Schlüssel entspricht, um das JWE zu entschlüsseln. Dadurch wird ein vp_token erstellt.

Python-Beispiel:

  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 führt zu einer vp_token-JSON-Datei mit den Anmeldedaten.

  {
    "vp_token":
    {
      "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. Sitzungstranskript erstellen

    Als Nächstes erstellen Sie das SessionTranscript aus ISO/IEC 18013-5:2021 mit einer Android- oder webspezifischen Übergabestruktur:

    SessionTranscript = [
      null,                // DeviceEngagementBytes not available
      null,                // EReaderKeyBytes not available
      [
        "OpenID4VPDCAPIHandover",
        AndroidHandoverDataBytes   // BrowserHandoverDataBytes for Web
      ]
    ]
    

    Sowohl für Android- als auch für Web-Handovers müssen Sie dieselbe Nonce verwenden, mit der Sie credential_request generiert haben.

    Android-Übergabe

        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()
        

    Browserübergabe

        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()
        

    Mit der SessionTranscript muss die Geräteantwort gemäß ISO/IEC 18013-5:2021, Abschnitt 9, validiert werden. Dazu sind mehrere Schritte erforderlich, z. B.:

  2. Prüfen Sie das Zertifikat des Bundesstaatsausstellers. Hier finden Sie die IACA-Zertifikate der unterstützten Aussteller.

  3. MSO-Signatur überprüfen (18013-5, Abschnitt 9.1.2)

  4. ValueDigests für Datenelemente berechnen und prüfen (18013-5 Abschnitt 9.1.2)

  5. Signatur von deviceSignature überprüfen (18013-5, Abschnitt 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
}

Datenschutzfreundliche Altersüberprüfung (ZKP)

Wenn Sie Zero-Knowledge-Beweise unterstützen möchten (z.B. um zu überprüfen, ob ein Nutzer über 18 Jahre alt ist, ohne sein genaues Geburtsdatum zu sehen), ändern Sie das Anfrageformat in mso_mdoc_zk und geben Sie die erforderliche zk_system_type-Konfiguration an.

  ...
  "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
            {
              ...

Sie erhalten einen verschlüsselten Zero-Knowledge-Beweis vom Wallet zurück. Sie können diesen Nachweis anhand von IACA-Zertifikaten von Ausstellern mit der longfellow-zk-Bibliothek von Google validieren.

Der verifier-service enthält einen einsatzbereiten, Docker-basierten Server, mit dem Sie die Antwort anhand bestimmter IACA-Zertifikate des Ausstellers validieren können.

Sie können die Datei certs.pem ändern, um IACA-Ausstellerzertifikate zu verwalten, denen Sie vertrauen möchten.

Ressourcen und Support

  • Referenzimplementierung:Sehen Sie sich unsere Referenzimplementierung für Identitätsüberprüfer auf GitHub an.
  • Website testen:Testen Sie den End-to-End-Ablauf unter verifier.multipaz.org.
  • OpenID4VP-Spezifikation:Hier finden Sie die technische Spezifikation für OpenID4VP.
  • Support:Wenn Sie Hilfe beim Debuggen benötigen oder Fragen zur Integration haben, wenden Sie sich an wallet-identity-rp-support@google.com.