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.
Scegli il tuo percorso RP, Verifier Registrar o di persona
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:

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.
org.iso.23220.photoid.1com.google.wallet.idcard.1dcql_query
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
- 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:
- Controlla il certificato dell'emittente: estrai la catena di certificati di firma dell'emittente da
issuerAuthe 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). - Verifica della firma MSO (ISO/IEC 18013-5 Sezione 9.1.2).
- Calcola e controlla
ValueDigestsper gli elementi di dati (ISO/IEC 18013-5 Sezione 9.1.2). - 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 |