Emissione di credenziali OpenID4VCI spiegata: dall'offerta alla credenziale accettata
OpenID4VCI, OpenID for Verifiable Credential Issuance, definisce come un wallet richiede e riceve una credenziale da un issuer. Una singola emissione attraversa diversi passaggi distinti prima che il wallet disponga effettivamente di una credenziale utilizzabile, e ciascuno ha una propria modalità di fallimento. Questa pagina percorre l'intero ciclo di vita, con esempi pratici originali.
Due modi per iniziare: codice pre-autorizzato e codice di autorizzazione
L'emissione parte spesso da una Credential Offer inviata dall'issuer, e il grant che indica determina come il wallet viene autorizzato. Un wallet può anche avviare l'emissione da solo, senza alcuna offerta, usando il flusso a codice di autorizzazione. Entrambi i flussi terminano nello stesso punto: il wallet in possesso di un access token utilizzabile per richiedere la credenziale.
Flusso a codice pre-autorizzato
1. Issuer
Conosce già il titolare e consegna una Credential Offer con un pre-authorized_code
2. Wallet
Riscatta il codice presso il token endpoint, opzionalmente con un codice di transazione
3. Wallet
Richiede la credenziale con una prova di possesso della propria chiave
Flusso a codice di autorizzazione
1. Wallet
Scansiona una Credential Offer che indica un grant authorization_code, oppure avvia il flusso da solo senza offerta
2. Authorization Server
Guida il titolare attraverso login e consenso, poi emette un codice
3. Wallet
Scambia il codice con un token, poi richiede la credenziale
L'intero ciclo di vita di un'offerta di credenziale
La specifica non definisce stati nominati, ma un flusso avviato dall'issuer in cui la credenziale viene emessa subito è il più semplice da seguire, come nella sequenza qui sotto. Ogni passaggio può fallire in modo diverso, e un'implementazione del wallet deve gestire quei percorsi, non solo quello ideale.
Esempio pratico: un certificato di idoneità alla circolazione per una flotta di trasporti
Un ente di revisione veicoli emette un certificato di idoneità alla circolazione al business wallet di un'azienda di trasporti dopo una revisione di routine. L'ispettore ha già autenticato il responsabile flotta presso il punto di ispezione, quindi l'issuer usa il flusso a codice pre-autorizzato. I passaggi seguenti seguono questa singola emissione dall'offerta alla credenziale accettata.
L'esempio mostra l'OpenID4VCI di base. Implementazioni ad alta garanzia come l'EUDI Wallet seguono il profilo HAIP che si aggiunge ad esso, il quale introduce access token vincolati a DPoP, wallet attestation presso il token endpoint e key attestation per le chiavi della credenziale. Questi aspetti sono omessi qui per mantenere leggibile ogni passaggio.
1. Offerta di credenziale
Il terminale dell'ente di revisione mostra un codice QR. Contiene un URI che inizia con openid-credential-offer:// e trasporta l'offerta sottostante, codificata in URL in un parametro credential_offer, oppure un credential_offer_uri da cui il wallet la recupera. Il wallet lo scansiona e legge quale credenziale è offerta e come ottenerla.
Issuer
Mostra un codice QR con una Credential Offer
Indica la configurazione della credenziale e un grant pre-authorized_code
{
"credential_issuer": "https://issuer.fleetinspect.example",
"credential_configuration_ids": ["roadworthiness_certificate"],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "fi-8f2c0b3a",
"tx_code": {
"length": 6,
"input_mode": "numeric",
"description": "Enter the code sent to your phone by text message"
}
}
}
}2. Scoperta dei metadati dell'issuer
Prima di richiedere qualsiasi cosa, il wallet recupera i metadati dell'issuer per sapere cosa contiene roadworthiness_certificate e quali endpoint chiamare. I metadati non elencano authorization server separati, quindi l'issuer è anche il proprio authorization server, e il wallet legge il token endpoint dai metadati di quel server.
Wallet
GET /.well-known/openid-credential-issuer
Apprende il formato della credenziale, le claim e i tipi di prova accettati, oltre agli endpoint per nonce, credential, deferred e notification
Metadati del credential issuer (estratto)
{
"credential_issuer": "https://issuer.fleetinspect.example",
"nonce_endpoint": "https://issuer.fleetinspect.example/nonce",
"credential_endpoint": "https://issuer.fleetinspect.example/credential",
"deferred_credential_endpoint": "https://issuer.fleetinspect.example/deferred",
"notification_endpoint": "https://issuer.fleetinspect.example/notify",
"credential_configurations_supported": {
"roadworthiness_certificate": {
"format": "dc+sd-jwt",
"vct": "https://fleetinspect.example/vct/roadworthiness",
"cryptographic_binding_methods_supported": ["jwk"],
"credential_signing_alg_values_supported": ["ES256"],
"proof_types_supported": {
"jwt": { "proof_signing_alg_values_supported": ["ES256"] }
},
"credential_metadata": {
"claims": [
{ "path": ["vehicle_registration"] },
{ "path": ["inspection_result"] },
{ "path": ["valid_until"] }
]
}
}
}
}Metadati dell'authorization server (estratto), da /.well-known/oauth-authorization-server
{
"issuer": "https://issuer.fleetinspect.example",
"token_endpoint": "https://issuer.fleetinspect.example/token",
"pre-authorized_grant_anonymous_access_supported": true
}3. Richiesta del token
Il wallet riscatta il codice pre-autorizzato presso il token endpoint, insieme al codice di transazione che l'ente di revisione ha inviato al telefono del responsabile flotta. Inviare quel codice su un secondo canale fa sì che chi fotografa il codice QR da dietro le spalle non possa comunque riscattarlo.
Wallet
POST /token
Invia il pre-authorized_code e il tx_code, riceve un access token limitato a questa offerta
POST /token HTTP/1.1 Host: issuer.fleetinspect.example Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code &pre-authorized_code=fi-8f2c0b3a &tx_code=482913
Risposta
{
"access_token": "fi-at-3d91e0",
"token_type": "Bearer",
"expires_in": 86400
}4. Prova di possesso
Poiché i metadati elencano un nonce_endpoint, il wallet recupera prima un c_nonce recente da lì. Dimostra quindi di possedere la chiave privata a cui la credenziale sarà vincolata, firmando un JWT di prova sull'identificatore dell'issuer e su quel c_nonce.
Wallet
POST /nonce, poi firma un JWT di prova con la chiave a cui la credenziale sarà vincolata
Vincola la credenziale a quella chiave, non semplicemente a chi detiene l'access token
POST /nonce HTTP/1.1 Host: issuer.fleetinspect.example
Risposta
{
"c_nonce": "fi-nonce-77aa"
}Il wallet costruisce ora il JWT di prova a partire da due oggetti JSON, un header e un payload, e li firma con la chiave privata a cui la credenziale sarà vincolata.
Header: cos'è questo JWT e quale chiave lo ha firmato
{
"typ": "openid4vci-proof+jwt",
"alg": "ES256",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}typ: contrassegna questo come una key proof OpenID4VCI, così non può essere scambiato per un altro tipo di JWTalg: l'algoritmo di firma, uno di quelli elencati dall'issuer in proof_signing_alg_values_supportedjwk: la chiave pubblica a cui la credenziale sarà vincolata; l'issuer verifica la firma rispetto ad essa
Payload: per chi è la prova e quando è stata creata
{
"aud": "https://issuer.fleetinspect.example",
"iat": 1789376400,
"nonce": "fi-nonce-77aa"
}aud: l'identificatore dell'issuer, così la prova non può essere riutilizzata presso un issuer diversoiat: l'istante in cui la prova è stata creata, in secondi dal 1970nonce: il c_nonce ottenuto dal nonce endpoint, che dimostra che la prova è recente
Risultato firmato
Header e payload sono ciascuno codificati in base64url e uniti con un punto. Il wallet firma quella stringa con la propria chiave privata e aggiunge la firma codificata in base64url dopo un secondo punto. La stringa risultante è il JWT di prova che il wallet invia nella richiesta di credenziale al passaggio 5.
base64url(header) . base64url(payload) . base64url(signature) eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIs... .eyJhdWQiOiJodHRwczovL2lzc3Vlci5mbGVldGluc3BlY3QuZXhhbXBsZSIs... .<ES256 signature>
5. Richiesta di credenziale
Il wallet chiama il credential endpoint con l'access token e la prova, e l'issuer genera e restituisce la credenziale firmata.
Wallet
POST /credential
Invia l'access token, l'id di configurazione e il JWT di prova, riceve la credenziale firmata e un notification_id
POST /credential HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0
{
"credential_configuration_id": "roadworthiness_certificate",
"proofs": {
"jwt": ["<proof JWT from step 4>"]
}
}Risposta
{
"credentials": [
{ "credential": "<issuer-signed SD-JWT VC>" }
],
"notification_id": "fi-notif-9012"
}L'intero flusso in sintesi
Questo diagramma di sequenza riunisce i cinque passaggi dell'esempio pratico, dall'offerta alla notifica. Le frecce continue sono richieste, quelle tratteggiate sono risposte, e la freccia punteggiata rappresenta il codice di transazione che viaggia fuori dal protocollo tramite SMS.
Wallet
Business wallet dell'azienda di trasporti
Authorization Server
Gestito dall'issuer stesso in questo esempio
Credential Issuer
Ente di revisione veicoli
- Credential Issuer verso Wallet: Credential Offer, mostrata come codice QR
- Credential Issuer verso Wallet: tx_code, inviato al telefono del responsabile flotta via SMS
- Wallet verso Credential Issuer: GET /.well-known/openid-credential-issuer
- Credential Issuer verso Wallet: metadati del credential issuer
- Wallet verso Authorization Server: GET /.well-known/oauth-authorization-server
- Authorization Server verso Wallet: metadati dell'authorization server
- Wallet verso Authorization Server: POST /token: pre-authorized_code, tx_code
- Authorization Server verso Wallet: access_token
- Wallet verso Credential Issuer: POST /nonce
- Credential Issuer verso Wallet: c_nonce
- Wallet: firma il JWT di prova
- Wallet verso Credential Issuer: POST /credential: access token, prove
- Credential Issuer verso Wallet: credentials, notification_id
- Wallet: convalida e memorizza
- Wallet verso Credential Issuer: POST /notify: credential_accepted
- Credential Issuer verso Wallet: 204 No Content
Quando la credenziale non è ancora pronta: emissione differita
L'esempio sopra presume che l'esito dell'ispezione sia già definitivo. Se invece l'ente di revisione deve escalare un esito dubbio a un ispettore senior, il credential endpoint non può restituire subito la credenziale, quindi ne differisce l'emissione.
Emissione immediata
Il credential endpoint restituisce la credenziale firmata nella stessa risposta della richiesta.
Emissione differita
Il credential endpoint risponde con HTTP 202, un transaction_id e un intervallo. Il wallet interroga il deferred credential endpoint con quell'id, attendendo almeno l'intervallo di secondi indicato tra una richiesta e l'altra, finché la credenziale non è pronta.
Risposta differita da /credential
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"transaction_id": "fi-tx-55c2",
"interval": 900
}Interrogazione di /deferred finché non è pronta
POST /deferred HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0
{ "transaction_id": "fi-tx-55c2" }
// While the review is open: 202 with the same transaction_id and interval.
// Once approved: 200 with the credentials, optionally with a notification_id.
// If the review rejects the result: a credential_request_denied error.Chiudere il cerchio: il notification endpoint
Dopo l'emissione, il wallet può comunicare all'issuer cosa è successo alla credenziale, usando il notification_id ricevuto nella risposta della credenziale. I wallet non sono tenuti a inviare queste notifiche e la consegna non è garantita, quindi un issuer non può interpretare una notifica mancante come indicativa di alcunché.
Wallet
Invia un evento al notification_endpoint dell'issuer per il notification_id ricevuto, relativo a tutte le credenziali contenute in quella risposta
Memorizzata nel wallet
L'emissione è fallita per qualsiasi altro motivo, ad esempio la credenziale non ha superato la convalida
L'emissione è fallita a causa del titolare, ad esempio ha rifiutato di memorizzarla
POST /notify HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0
{
"notification_id": "fi-notif-9012",
"event": "credential_accepted"
}Termini correlati
Domande frequenti
Come fa un wallet a sapere quale tipo di grant usare?
La Credential Offer indica il grant nel proprio oggetto grants. authorization_code è presente quando l'issuer vuole che il titolare effettui il login come parte del flusso. pre-authorized_code è presente quando il titolare era già autenticato sul canale in cui è stata creata l'offerta, ad esempio dall'ispettore al punto di ispezione nell'esempio pratico riportato di seguito. Un'offerta può elencarli entrambi, e in tal caso il wallet ne sceglie uno. Se l'offerta non ha alcun oggetto grants, il wallet consulta i tipi di grant supportati dall'authorization server nei suoi metadati.
Perché il wallet recupera i metadati dell'issuer prima di richiedere qualsiasi cosa?
La Credential Offer indica solo i credential_configuration_ids, l'URL dell'issuer e i grants. I metadati dell'issuer, pubblicati su un percorso well-known, descrivono ogni configurazione: il suo formato, le sue claim e i tipi di prova accettati, oltre agli endpoint per nonce, credential, deferred e notification. Indicano anche quale authorization server usare, e i metadati di quel server forniscono a loro volta il token endpoint. Senza entrambi, il wallet non saprebbe come costruire richieste valide né cosa mostrare al titolare prima del consenso.
Cosa dimostra effettivamente la prova di possesso?
Dimostra che il wallet che richiede la credenziale possiede la chiave privata a cui la credenziale sarà vincolata, non semplicemente che dispone di un access token valido. Il wallet firma un JWT di prova sull'identificatore dell'issuer e su un c_nonce recente ottenuto dal nonce endpoint dell'issuer, usando quella chiave. L'issuer inserisce la chiave pubblica corrispondente nella credenziale. Un verifier che richiede il key binding chiede al titolare di firmare nuovamente con la stessa chiave al momento della presentazione, così una credenziale copiata senza la chiave non supera quel controllo.
Perché un issuer dovrebbe differire l'emissione invece di restituire subito la credenziale?
Alcuni controlli che l'issuer esegue prima di generare una credenziale non possono completarsi all'interno di un'unica richiesta HTTP, ad esempio una revisione manuale o una chiamata a un registro esterno lento. L'emissione differita permette al credential endpoint di rispondere subito con un transaction_id invece di bloccare la connessione, e il wallet interroga l'endpoint differito con quell'id finché il controllo non termina e la credenziale non è pronta da ritirare.
Il notification endpoint è obbligatorio per un issuer?
No. È opzionale per gli issuer, e anche i wallet non sono tenuti a usarlo. Quando entrambi lo supportano, l'issuer viene a sapere cosa è successo dopo l'emissione: le credenziali sono state memorizzate (credential_accepted), il titolare ha interrotto l'emissione, ad esempio rifiutando di memorizzarle (credential_deleted), oppure l'emissione è fallita per un altro motivo (credential_failure). La consegna non è garantita, quindi un issuer dovrebbe considerare una notifica come informazione utile, mai come un dato affidabile, e non può dedurre nulla dall'assenza di una notifica.
Fonti
Questa pagina ha scopo informativo e non costituisce consulenza legale. Per indicazioni autorevoli rivolgersi direttamente alla OpenID Foundation e alla Commissione europea.