Vai al contenuto principale

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.

Offerta creata
Il codice QR o il link apre il wallet
Offerta ricevuta
grant riscattato
Token ottenuto
nonce e prova di possesso
Credenziale richiesta
credenziale generata
Credenziale emessa
il wallet convalida e memorizza
Credenziale accettata
Dove può fallire, nell'ordine in cui si susseguono i passaggi:
Codice scaduto prima del riscatto
Richiesta di token rifiutata
Richiesta di credenziale respinta
Credenziale non memorizzata

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 JWT
  • alg: l'algoritmo di firma, uno di quelli elencati dall'issuer in proof_signing_alg_values_supported
  • jwk: 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 diverso
  • iat: l'istante in cui la prova è stata creata, in secondi dal 1970
  • nonce: 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

  1. Credential Issuer verso Wallet: Credential Offer, mostrata come codice QR
  2. Credential Issuer verso Wallet: tx_code, inviato al telefono del responsabile flotta via SMS
  3. Wallet verso Credential Issuer: GET /.well-known/openid-credential-issuer
  4. Credential Issuer verso Wallet: metadati del credential issuer
  5. Wallet verso Authorization Server: GET /.well-known/oauth-authorization-server
  6. Authorization Server verso Wallet: metadati dell'authorization server
  7. Wallet verso Authorization Server: POST /token: pre-authorized_code, tx_code
  8. Authorization Server verso Wallet: access_token
  9. Wallet verso Credential Issuer: POST /nonce
  10. Credential Issuer verso Wallet: c_nonce
  11. Wallet: firma il JWT di prova
  12. Wallet verso Credential Issuer: POST /credential: access token, prove
  13. Credential Issuer verso Wallet: credentials, notification_id
  14. Wallet: convalida e memorizza
  15. Wallet verso Credential Issuer: POST /notify: credential_accepted
  16. 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.

transaction_idinterroga deferred_credential_endpoint

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

credential_accepted

Memorizzata nel wallet

credential_failure

L'emissione è fallita per qualsiasi altro motivo, ad esempio la credenziale non ha superato la convalida

credential_deleted

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

  1. OpenID for Verifiable Credential Issuance 1.0
  2. OpenID4VC High Assurance Interoperability Profile 1.0
  3. EUDI Wallet Architecture and Reference Framework

Questa pagina ha scopo informativo e non costituisce consulenza legale. Per indicazioni autorevoli rivolgersi direttamente alla OpenID Foundation e alla Commissione europea.

Parlaci della tua integrazione con l'EUDI Wallet