Passaggio 3: esegui l'integrazione come componente di supporto

Questa guida spiega come le parti attendibili (RP) dirette, ovvero singoli commercianti, attività e sviluppatori di app, integrano l'API Digital Credentials per richiedere e convalidare le credenziali di identità digitale nelle app per Android e sul web.

Come funziona l'integrazione della terza parte attendibile

Ogni integrazione della Relying Party prevede quattro passaggi. I passaggi A e C vengono eseguiti sul tuo server; il passaggio B viene eseguito nell'app per Android o nel browser. Seleziona un passaggio per passare ai dettagli di implementazione:

Crea e testa in sandbox prima di richiedere le credenziali di produzione

Durante l'implementazione dei passaggi A-D, utilizza gli asset sandbox pre-approvati di Google per creare e testare immediatamente:

Firma delle richieste

Firma le richieste OpenID4VP utilizzando le chiavi e i metadati di test della sandbox di Google.

Test Credential

Esegui il test su un dispositivo Android in modalità Sandbox con un pass ID di test Utopia.

Convalida della risposta

Convalida issuerAuth rispetto al certificato Utopia Sandbox.

Sequenza end-to-end tra il server di backend, l'app client o il browser e Google Wallet per la verifica online

Formati, credenziali e funzionalità supportati

Google Wallet supporta i documenti di identità digitali basati su mdoc ISO tramite OpenID4VP (versione 1.0) su Android 9 (livello API 28) e versioni successive e sui browser supportati.

Credenziali e attributi supportati

Cerca gli identificatori doctype_value (patente di guida digitale, pass ID, documento di identità con foto) e i percorsi di rivendicazione degli attributi che puoi richiedere a Google Wallet.

Emittenti supportati e certificati IACA

Scarica i certificati radice IACA ufficiali per gli stati degli Stati Uniti, gli emittenti internazionali e l'emittente Sandbox Utopia per convalidare issuerAuth.


Passaggio A: formatta e firma la richiesta (lato server)

Per richiedere le credenziali da qualsiasi wallet, devi formattare la richiesta utilizzando OpenID4VP sul tuo server. Puoi richiedere una o più credenziali specifiche in un singolo oggetto dcql_query.

Esempio di richiesta JSON

Ecco un esempio di richiesta mdoc requestJson per ottenere le credenziali di identità da qualsiasi wallet su un dispositivo Android o sul web.

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

Richiedi crittografia

Il client_metadata contiene la chiave pubblica di crittografia per ogni richiesta. Dovrai archiviare le chiavi private per ogni richiesta e utilizzarle per autenticare e autorizzare il token che ricevi dall'app Wallet.

Metadati OpenID4VP integrati

Quando formatti la richiesta di credenziali, devi includere il campo gw_rp_metadata_bytes all'interno dell'oggetto client_metadata (come mostrato nel codice di richiesta di esempio di seguito). Questo campo contiene i metadati della relying party con codifica Base64URL richiesti da Google Wallet per verificare la tua identità e mostrare il tuo branding all'utente.

Payload della richiesta di credenziali

Il parametro credential_request in requestJson contiene i seguenti campi.

Credenziale specifica

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

Qualsiasi credenziale idonea

Ecco l'esempio di richiesta per mDL, pass ID e pass ID (utilizzando il tipo di documento di identità con foto). L'utente può procedere con una delle due opzioni.

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

Puoi richiedere un numero qualsiasi di attributi supportati da qualsiasi credenziale di identità memorizzata in Google Wallet.

Richieste firmate

Le richieste firmate (richieste di autorizzazione protette da JWT) incapsulano la richiesta di presentazione verificabile all'interno di un token web JSON (JWT) firmato in modo crittografico utilizzando la tua infrastruttura PKI, garantendo l'integrità della richiesta e dimostrando la tua identità a Google Wallet.

Prerequisiti

Prima di implementare le modifiche al codice per le richieste firmate, assicurati di avere:

  • Chiave privata:per firmare la richiesta, devi avere una chiave privata (ad esempio, Elliptic Curve ES256) gestita sul tuo server. Per i test della sandbox come componente di supporto, utilizza la chiave privata di test della sandbox.
  • Certificato: è necessario un certificato X.509 standard derivato dalla coppia di chiavi.
  • Registrazione:assicurati che il tuo certificato pubblico (o la CA radice per i registrar di verifica) sia considerato attendibile da Google Wallet.

Logica di creazione delle richieste

Quando crei e firmi la richiesta OpenID4VP, presta molta attenzione al formato di codifica esatto previsto per ogni campo. Se confondi base64 e base64url o il padding (=), la convalida della firma o dell'origine non andrà a buon fine:

Campo Codifica / formato richiesti Caratteri e spaziatura interna
client_id (x509_hash:<hash>) base64url senza spaziatura interna Utilizza l'alfabeto sicuro per URL (-, _); rimuovi i caratteri finali =
expected_origins (android:apk-key-hash:<hash>) Base64 standard senza spaziatura interna Utilizza l'alfabeto standard (+, /); rimuovi il carattere = finale
expected_origins (https://...) URL in minuscolo senza barre finali Esempio: https://example.com (senza / finale)
Intestazione JWS x5c Base64 standard con spaziatura interna Utilizza l'alfabeto standard (+, /); mantieni il padding = finale

Per creare una richiesta, utilizza la tua chiave privata e inserisci il payload in 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

Passaggio B: attiva l'API (lato client)

L'intera richiesta API deve essere generata lato server. A seconda della piattaforma, passa il JSON generato alle API della piattaforma.

In-app (Android)

Per richiedere le credenziali di identità dalle tue app per Android, segui questi passaggi:

Aggiorna le dipendenze

Nel file build.gradle del progetto, aggiorna le dipendenze per utilizzare Gestore delle credenziali (beta):

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

Configura Gestore delle credenziali

Per configurare e inizializzare un oggetto CredentialManager, aggiungi una logica simile alla seguente:

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

Attributi di identità della richiesta

Anziché specificare singoli parametri per le richieste di identità, l'app li fornisce tutti insieme come stringa JSON all'interno di CredentialOption. Gestore delle credenziali trasmette questa stringa JSON ai portafogli digitali disponibili senza esaminarne i contenuti. Ogni wallet è quindi responsabile di:

  • Analisi della stringa JSON per comprendere la richiesta di identità.
  • Determinare quali delle sue credenziali memorizzate, se presenti, soddisfano la richiesta.

Consigliamo ai partner di creare le richieste sul server anche per le integrazioni di app per Android.

Utilizzerai il requestJson di Formatta e firma la richiesta come request nella chiamata di funzione 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)
    }
}

Gestisci la risposta alle credenziali

Una volta ricevuta una risposta dal wallet, verifica se la risposta è andata a buon fine e contiene la risposta 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 risposta credentialJson contiene un identityToken (JWT) criptato, definito dal W3C. L'app Wallet è responsabile della creazione di questa risposta.

Esempio:

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

Invia questa risposta al tuo server per convalidarne l'autenticità. Consulta Decripta e convalida la risposta.

Web

Per richiedere le credenziali di identità utilizzando l'API Digital Credentials su Chrome o altri browser supportati, effettua la seguente richiesta:

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

Invia la risposta di questa API al tuo server per convalidare la risposta della credenziale.


Passaggio C: decripta e convalida la risposta (lato server)

Una volta che il wallet restituisce il identityToken (JWT) criptato, devi eseguire una convalida rigorosa lato server prima di considerare attendibili i dati.

Decripta la risposta

Utilizza la chiave privata corrispondente alla chiave pubblica inviata nel campo client_metadata della richiesta per decriptare il JWE. In questo modo si ottiene un vp_token.

Esempio di 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 genererà un file JSON vp_token contenente la credenziale:

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

Ricostruisci il SessionTranscript e convalida il DeviceResponse

  1. Crea SessionTranscript da ISO/IEC 18013-5:2021 con una struttura di trasferimento specifica per Android o web:
SessionTranscript = [
  null,                // DeviceEngagementBytes not available
  null,                // EReaderKeyBytes not available
  [
    "OpenID4VPDCAPIHandover",
    AndroidHandoverDataBytes   // BrowserHandoverDataBytes for Web
  ]
]

Sia per i trasferimenti Android che per quelli web, utilizza lo stesso nonce che hai utilizzato per generare 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()

Browser Handover

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

Utilizzando SessionTranscript, la risposta del dispositivo deve essere convalidata in base alla clausola 9 della norma ISO/IEC 18013-5:2021.

Questa convalida include quattro controlli:

  1. Controlla il certificato dell'emittente: estrai la catena di certificati di firma dell'emittente da issuerAuth e convalidala rispetto ai certificati radice IACA attendibili. Consulta la sezione Emittenti e certificati IACA supportati (e aggiungi il certificato Utopia Sandbox quando esegui i test con un ID di test della sandbox).
  2. Verifica della firma MSO (ISO/IEC 18013-5 Sezione 9.1.2).
  3. Calcola e controlla ValueDigests per gli elementi di dati (ISO/IEC 18013-5 Sezione 9.1.2).
  4. Verifica la firma di deviceSignature (ISO/IEC 18013-5 Sezione 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
}

Passaggio D (facoltativo): verifica dell'età che tutela la privacy (ZKP)

Per supportare le prove a conoscenza zero (ad esempio, verificare che un utente abbia più di 18 anni senza vedere la sua data di nascita esatta), modifica il formato della richiesta in mso_mdoc_zk e fornisci la configurazione zk_system_type richiesta.

Per una panoramica generale di cosa sia ZKP e delle sue funzionalità, consulta le domande frequenti.

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

Riceverai dal wallet una prova a conoscenza zero criptata. Puoi convalidare questa prova rispetto ai certificati IACA dell'emittente utilizzando la libreria longfellow-zk di Google.

Il servizio di verifica contiene un server basato su Docker pronto per il deployment che ti consente di convalidare la risposta rispetto ai certificati IACA dell'emittente.

Puoi modificare certs.pem per gestire i certificati dell'emittente IACA che vuoi considerare attendibili.


SDK, implementazioni di riferimento e assistenza

Utilizza le nostre librerie open source, il server di riferimento e lo strumento di verifica web per accelerare l'implementazione:

Risorsa / Libreria Cosa fornisce Link
Identity Verifiers Reference Implementation Server e client di esempio che mostrano la generazione di richieste OpenID4VP, la firma JWS, la decrittografia JWE e la convalida della risposta mdoc. Repository GitHub
SDK Multipaz Libreria open source (OpenWallet Foundation) che include DeviceResponseParser per la decodifica CBOR, la ricostruzione di SessionTranscript e la convalida della firma ISO/IEC 18013-5/7. openwallet-foundation/multipaz
Libreria Longfellow ZKP e servizio di verifica Libreria open source e server basato su Docker per la verifica delle attestazioni di età e attributi con Zero-Knowledge Proof (mso_mdoc_zk). google/longfellow-zk
Strumento di verifica web interattivo Strumento di verifica web in hosting per testare le richieste dell'API W3C Digital Credentials / OpenID4VP con Google Wallet. verifier.multipaz.org
Specifica OpenID4VP 1.0 Specifica ufficiale del protocollo OpenID for Verifiable Presentations 1.0. Specifiche OpenID4VP
Domande frequenti e assistenza per gli sviluppatori Risposte alle domande comuni sull'integrazione tecnica o invia un'email all'indirizzo wallet-identity-rp-support@google.com per ricevere assistenza per il debug. Domande frequenti sull'integrazione tecnica

Passaggio successivo