Passer au contenu principal

Émission de credentials OpenID4VCI expliquée : de l'offre au credential accepté

OpenID4VCI, OpenID for Verifiable Credential Issuance, définit comment un wallet demande et reçoit un credential auprès d'un émetteur. Une seule émission traverse plusieurs étapes distinctes avant que le wallet ne détienne réellement un credential utilisable, et chacune a son propre mode d'échec. Cette page parcourt ce cycle de vie complet, avec des exemples détaillés originaux.

Deux façons de démarrer : pre-authorized code et authorization code

L'émission démarre souvent à partir d'une Credential Offer envoyée par l'émetteur, et le grant qu'elle indique décide comment le wallet est autorisé. Un wallet peut aussi démarrer l'émission lui-même, sans aucune offre, via le flux authorization code. Les deux flux aboutissent au même point : le wallet détient un token d'accès qu'il peut utiliser pour demander le credential.

Flux pre-authorized code

1. Émetteur

Connaît déjà le détenteur et remet une Credential Offer avec un pre-authorized_code

2. Wallet

Échange le code auprès du token endpoint, éventuellement avec un code de transaction

3. Wallet

Demande le credential avec une preuve de possession de sa clé

Flux authorization code

1. Wallet

Scanne une Credential Offer indiquant un grant authorization_code, ou démarre le flux elle-même sans offre

2. Serveur d'autorisation

Fait passer le détenteur par la connexion et le consentement, puis émet un code

3. Wallet

Échange le code contre un token, puis demande le credential

Le cycle de vie complet d'une offre de credential

La spécification ne définit pas d'états nommés, mais un flux initié par l'émetteur où le credential est émis immédiatement est le plus simple à suivre, comme dans la séquence ci-dessous. Chaque étape peut échouer à sa manière, et une implémentation de wallet doit gérer ces chemins, pas seulement le scénario favorable.

Offre créée
le QR code ou le lien ouvre le wallet
Offre reçue
grant échangé
Token obtenu
nonce et preuve de possession
Credential demandé
credential généré
Credential émis
le wallet valide et stocke
Credential accepté
Où cela peut échouer, dans l'ordre où se déroulent les étapes :
Code expiré avant l'échange
Demande de token refusée
Demande de credential rejetée
Credential non stocké

Exemple détaillé : un certificat de contrôle technique pour une flotte de transport

Un organisme de contrôle technique des véhicules émet un certificat de contrôle technique au wallet professionnel d'une entreprise de transport après un contrôle de routine. L'inspecteur a déjà authentifié le gestionnaire de flotte au point de contrôle, l'émetteur utilise donc le flux pre-authorized code. Les étapes ci-dessous suivent cette émission unique, de l'offre au credential accepté.

L'exemple présente l'OpenID4VCI de base. Les déploiements à haute garantie, comme l'EUDI Wallet, appliquent en plus le profil HAIP, qui ajoute des tokens d'accès liés par DPoP, une wallet attestation au token endpoint et une key attestation pour les clés du credential. Ces éléments sont omis ici pour garder chaque étape lisible.

1. Credential offer

Le terminal de l'organisme de contrôle affiche un QR code. Il contient une URI commençant par openid-credential-offer:// qui transporte l'offre ci-dessous, encodée en URL dans un paramètre credential_offer, ou une credential_offer_uri à partir de laquelle le wallet la récupère. Le wallet la scanne et lit quel credential est proposé et comment l'obtenir.

Émetteur

Affiche un QR code avec une Credential Offer

Indique la configuration de credential et 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. Découverte des métadonnées de l'émetteur

Avant de demander quoi que ce soit, le wallet récupère les métadonnées de l'émetteur pour savoir ce que contient roadworthiness_certificate et quels endpoints appeler. Les métadonnées ne listent pas de serveur d'autorisation séparé, donc l'émetteur est son propre serveur d'autorisation, et le wallet lit le token endpoint dans les métadonnées de ce serveur.

Wallet

GET /.well-known/openid-credential-issuer

Découvre le format du credential, les claims et les types de proof acceptés, ainsi que les endpoints nonce, credential, deferred et notification

Métadonnées de l'émetteur de credentials (extrait)

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

Métadonnées du serveur d'autorisation (extrait), depuis /.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. Requête de token

Le wallet échange le pre-authorized code auprès du token endpoint, avec le code de transaction que l'organisme de contrôle a envoyé au téléphone du gestionnaire de flotte. Envoyer ce code par un second canal signifie que quelqu'un qui photographie le QR code par-dessus l'épaule ne peut toujours pas l'échanger.

Wallet

POST /token

Envoie le pre-authorized_code et le tx_code, reçoit un token d'accès limité à cette offre

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

Réponse

{
  "access_token": "fi-at-3d91e0",
  "token_type": "Bearer",
  "expires_in": 86400
}

4. Preuve de possession

Comme les métadonnées listent un nonce_endpoint, le wallet y récupère d'abord un c_nonce récent. Il prouve ensuite qu'il détient la clé privée à laquelle le credential sera lié, en signant un proof JWT sur l'identifiant de l'émetteur et ce c_nonce.

Wallet

POST /nonce, puis signe un proof JWT avec la clé à laquelle le credential sera lié

Lie le credential à cette clé, pas simplement à qui détient le token d'accès

POST /nonce HTTP/1.1
Host: issuer.fleetinspect.example

Réponse

{
  "c_nonce": "fi-nonce-77aa"
}

Le wallet construit maintenant le proof JWT à partir de deux objets JSON, un header et un payload, et les signe avec la clé privée à laquelle le credential sera lié.

Header : ce qu'est ce JWT et quelle clé l'a signé

{
  "typ": "openid4vci-proof+jwt",
  "alg": "ES256",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
  • typ: marque ceci comme une key proof OpenID4VCI, afin qu'elle ne soit pas confondue avec un autre type de JWT
  • alg: l'algorithme de signature, l'un de ceux que l'émetteur a listés dans proof_signing_alg_values_supported
  • jwk: la clé publique à laquelle le credential sera lié ; l'émetteur vérifie la signature avec celle-ci

Payload : pour qui est la proof et quand elle a été créée

{
  "aud": "https://issuer.fleetinspect.example",
  "iat": 1789376400,
  "nonce": "fi-nonce-77aa"
}
  • aud: l'identifiant de l'émetteur, afin que la proof ne puisse pas être rejouée chez un autre émetteur
  • iat: le moment où la proof a été créée, en secondes depuis 1970
  • nonce: le c_nonce provenant du nonce endpoint, qui montre que la proof est récente

Résultat signé

Le header et le payload sont chacun encodés en base64url puis reliés par un point. Le wallet signe cette chaîne avec sa clé privée et ajoute la signature encodée en base64url après un second point. La chaîne résultante est le proof JWT que le wallet envoie dans la requête de credential à l'étape 5.

base64url(header) . base64url(payload) . base64url(signature)

eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIs...
  .eyJhdWQiOiJodHRwczovL2lzc3Vlci5mbGVldGluc3BlY3QuZXhhbXBsZSIs...
  .<ES256 signature>

5. Requête de credential

Le wallet appelle le credential endpoint avec le token d'accès et la proof, et l'émetteur génère et renvoie le credential signé.

Wallet

POST /credential

Envoie le token d'accès, l'id de configuration et le proof JWT, reçoit le credential signé et 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>"]
  }
}

Réponse

{
  "credentials": [
    { "credential": "<issuer-signed SD-JWT VC>" }
  ],
  "notification_id": "fi-notif-9012"
}

Tout le flux en un coup d'œil

Ce diagramme de séquence rassemble les cinq étapes de l'exemple détaillé, de l'offre à la notification. Les flèches pleines sont des requêtes, les flèches en pointillés sont des réponses, et la flèche en pointillé fin représente le code de transaction voyageant hors du protocole par SMS.

Wallet

Wallet professionnel de l'entreprise de transport

Serveur d'autorisation

Géré ici par l'émetteur lui-même dans cet exemple

Émetteur de credentials

Organisme de contrôle technique des véhicules

  1. Émetteur de credentials vers Wallet: Credential Offer, présentée sous forme de QR code
  2. Émetteur de credentials vers Wallet: tx_code, envoyé par SMS au téléphone du gestionnaire de flotte
  3. Wallet vers Émetteur de credentials: GET /.well-known/openid-credential-issuer
  4. Émetteur de credentials vers Wallet: métadonnées de l'émetteur de credentials
  5. Wallet vers Serveur d'autorisation: GET /.well-known/oauth-authorization-server
  6. Serveur d'autorisation vers Wallet: métadonnées du serveur d'autorisation
  7. Wallet vers Serveur d'autorisation: POST /token: pre-authorized_code, tx_code
  8. Serveur d'autorisation vers Wallet: access_token
  9. Wallet vers Émetteur de credentials: POST /nonce
  10. Émetteur de credentials vers Wallet: c_nonce
  11. Wallet: signe le proof JWT
  12. Wallet vers Émetteur de credentials: POST /credential : token d'accès, proofs
  13. Émetteur de credentials vers Wallet: credentials, notification_id
  14. Wallet: valide et stocke
  15. Wallet vers Émetteur de credentials: POST /notify: credential_accepted
  16. Émetteur de credentials vers Wallet: 204 No Content

Quand le credential n'est pas encore prêt : l'émission différée

L'exemple ci-dessus suppose que le résultat du contrôle est déjà définitif. Si l'organisme de contrôle doit au contraire faire remonter un résultat limite à un inspecteur senior, le credential endpoint ne peut pas renvoyer le credential immédiatement, il diffère donc l'émission.

Émission immédiate

Le credential endpoint renvoie le credential signé dans la même réponse que la requête.

Émission différée

Le credential endpoint répond avec un code HTTP 202, un transaction_id et un interval. Le wallet interroge le deferred credential endpoint avec cet id, en attendant au moins interval secondes entre les requêtes, jusqu'à ce que le credential soit prêt.

transaction_idinterroger deferred_credential_endpoint

Réponse différée de /credential

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "transaction_id": "fi-tx-55c2",
  "interval": 900
}

Interrogation de /deferred jusqu'à ce qu'il soit prêt

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.

Boucler la boucle : le notification endpoint

Après l'émission, le wallet peut informer l'émetteur de ce qu'il est advenu du credential, à l'aide du notification_id reçu dans la réponse de credential. Les wallets ne sont pas tenus d'envoyer ces notifications et la livraison n'est pas garantie, donc un émetteur ne peut rien interpréter d'une notification absente.

Wallet

Envoie un événement au notification_endpoint de l'émetteur pour le notification_id reçu, ce qui couvre tous les credentials de cette réponse

credential_accepted

Stocké dans le wallet

credential_failure

L'émission a échoué pour une autre raison, par exemple parce que le credential n'a pas été validé

credential_deleted

L'émission a échoué du fait du détenteur, par exemple parce qu'il a refusé de le stocker

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

Termes associés

Questions fréquentes

Comment un wallet sait-il quel type de grant utiliser ?

La Credential Offer indique le grant dans son objet grants. authorization_code est présent lorsque l'émetteur souhaite que le détenteur se connecte dans le cadre du flux. pre-authorized_code est présent lorsque le détenteur était déjà authentifié sur le canal où l'offre a été créée, par exemple par l'inspecteur au point de contrôle dans l'exemple détaillé ci-dessous. Une offre peut lister les deux, le wallet en choisit alors un. Si l'offre ne comporte aucun objet grants, le wallet recherche dans les métadonnées du serveur d'autorisation quels types de grant celui-ci prend en charge.

Pourquoi le wallet récupère-t-il les métadonnées de l'émetteur avant de demander quoi que ce soit ?

La Credential Offer indique seulement les credential_configuration_ids, l'URL de l'émetteur et les grants. Les métadonnées de l'émetteur, servies depuis un chemin well-known, décrivent chaque configuration : son format, ses claims et les types de proof acceptés, ainsi que les endpoints nonce, credential, deferred et notification. Elles indiquent aussi quel serveur d'autorisation utiliser, et les métadonnées de ce serveur fournissent le token endpoint. Sans les deux, le wallet ne saurait pas comment construire des requêtes valides ni quoi montrer au détenteur avant qu'il ne consente.

Que prouve réellement la preuve de possession ?

Elle prouve que le wallet demandant le credential détient la clé privée à laquelle le credential sera lié, et non simplement qu'il dispose d'un token d'accès valide. Le wallet signe un proof JWT sur l'identifiant de l'émetteur et un c_nonce récent obtenu du nonce endpoint de l'émetteur, à l'aide de cette clé. L'émetteur intègre la clé publique correspondante dans le credential. Un verifier qui exige le key binding demande au détenteur de signer à nouveau avec la même clé lors de la présentation, de sorte qu'un credential copié sans la clé échoue à ce contrôle.

Pourquoi un émetteur différerait-il l'émission au lieu de renvoyer le credential immédiatement ?

Certains contrôles que l'émetteur effectue avant de générer un credential ne peuvent pas s'achever dans une seule requête HTTP, par exemple une révision manuelle ou un appel à un registre externe lent. L'émission différée permet au credential endpoint de répondre immédiatement avec un transaction_id plutôt que de bloquer la connexion, et le wallet interroge l'endpoint deferred avec cet id jusqu'à ce que le contrôle se termine et que le credential soit prêt à être récupéré.

Le notification endpoint est-il obligatoire pour un émetteur ?

Non. Il est optionnel pour les émetteurs, et les wallets ne sont pas non plus tenus de l'utiliser. Lorsque les deux le prennent en charge, l'émetteur apprend ce qui s'est passé après l'émission : les credentials ont été stockés (credential_accepted), le détenteur a arrêté l'émission, par exemple en refusant de les stocker (credential_deleted), ou cela a échoué pour une autre raison (credential_failure). La livraison n'est pas garantie, donc un émetteur doit traiter une notification comme une information utile, jamais comme un enregistrement fiable, et ne peut rien déduire d'une notification absente.

Sources

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

Cette page est informative et ne constitue pas un avis juridique. Pour des conseils faisant autorité, consultez directement l'OpenID Foundation et la Commission européenne.

Parlez-nous de l'intégration EUDI Wallet