Saltar al contenido principal

Emisión de credenciales OpenID4VCI explicada: de la oferta a la credencial aceptada

OpenID4VCI, OpenID for Verifiable Credential Issuance, define cómo una wallet solicita y recibe una credencial de un emisor. Una única emisión atraviesa varios pasos distintos antes de que la wallet tenga realmente una credencial utilizable, y cada uno tiene su propio modo de fallo. Esta página recorre ese ciclo de vida completo, con ejemplos desarrollados originales.

Dos formas de empezar: pre-authorized code y authorization code

La emisión suele iniciarse a partir de una Credential Offer enviada por el emisor, y el grant que indica decide cómo se autoriza la wallet. Una wallet también puede iniciar la emisión por sí misma, sin ninguna oferta, mediante el flujo authorization code. Ambos flujos terminan en el mismo punto: la wallet con un token de acceso que puede usar para solicitar la credencial.

Flujo de pre-authorized code

1. Emisor

Ya conoce al titular y entrega una Credential Offer con un pre-authorized_code

2. Wallet

Canjea el código en el endpoint de token, opcionalmente con un código de transacción

3. Wallet

Solicita la credencial con una prueba de posesión de su clave

Flujo de authorization code

1. Wallet

Escanea una Credential Offer que indica un grant authorization_code, o inicia el flujo por sí misma sin oferta

2. Servidor de autorización

Lleva al titular a través del inicio de sesión y el consentimiento, y luego emite un código

3. Wallet

Canjea el código por un token y a continuación solicita la credencial

El ciclo de vida completo de una oferta de credencial

La especificación no define estados con nombre, pero un flujo iniciado por el emisor en el que la credencial se emite de inmediato es el más fácil de seguir, como en la secuencia siguiente. Cada paso puede fallar a su manera, y una implementación de wallet debe gestionar esas rutas, no solo la favorable.

Oferta creada
el código QR o el enlace abre la wallet
Oferta recibida
grant canjeado
Token obtenido
nonce y prueba de posesión
Credencial solicitada
credencial generada
Credencial emitida
la wallet valida y almacena
Credencial aceptada
Dónde puede fallar, en el orden en que se ejecutan los pasos:
Código caducado antes de canjearse
Solicitud de token denegada
Solicitud de credencial rechazada
Credencial no almacenada

Ejemplo desarrollado: un certificado de aptitud técnica para una flota de transporte

Un organismo de inspección técnica de vehículos emite un certificado de aptitud técnica a la wallet empresarial de una empresa de transporte tras una inspección rutinaria. El inspector ya autenticó al gestor de la flota en el punto de inspección, así que el emisor usa el flujo pre-authorized code. Los pasos siguientes recorren esa única emisión, desde la oferta hasta la credencial aceptada.

El ejemplo muestra el OpenID4VCI base. Los despliegues de alta garantía, como la EUDI Wallet, siguen además el perfil HAIP, que añade tokens de acceso vinculados con DPoP, wallet attestation en el endpoint de token y key attestation para las claves de la credencial. Se omiten aquí para mantener cada paso legible.

1. Credential offer

El terminal del organismo de inspección muestra un código QR. Contiene una URI que empieza por openid-credential-offer:// y transporta la oferta siguiente, codificada en URL en un parámetro credential_offer, o una credential_offer_uri desde la que la wallet la obtiene. La wallet la escanea y lee qué credencial se ofrece y cómo obtenerla.

Emisor

Muestra un código QR con una Credential Offer

Indica la configuración de credencial y 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. Descubrimiento de metadatos del emisor

Antes de solicitar nada, la wallet obtiene los metadatos del emisor para saber qué contiene roadworthiness_certificate y qué endpoints llamar. Los metadatos no listan servidores de autorización separados, por lo que el emisor es su propio servidor de autorización, y la wallet lee el endpoint de token de los metadatos de ese servidor.

Wallet

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

Conoce el formato de la credencial, las claims y los tipos de proof aceptados, además de los endpoints de nonce, credencial, deferred y notification

Metadatos del emisor de credenciales (extracto)

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

Metadatos del servidor de autorización (extracto), de /.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. Solicitud de token

La wallet canjea el pre-authorized code en el endpoint de token, junto con el código de transacción que el organismo de inspección envió al teléfono del gestor de la flota. Enviar ese código por un segundo canal implica que alguien que fotografía el código QR por encima del hombro aun así no puede canjearlo.

Wallet

POST /token

Envía el pre-authorized_code y el tx_code, recibe un token de acceso limitado a esta oferta

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

Respuesta

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

4. Prueba de posesión

Como los metadatos listan un nonce_endpoint, la wallet primero obtiene allí un c_nonce reciente. A continuación demuestra que posee la clave privada a la que se vinculará la credencial, firmando un proof JWT sobre el identificador del emisor y ese c_nonce.

Wallet

POST /nonce, y luego firma un proof JWT con la clave a la que se vinculará la credencial

Vincula la credencial a esa clave, no simplemente a quien posea el token de acceso

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

Respuesta

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

La wallet construye ahora el proof JWT a partir de dos objetos JSON, una cabecera y un payload, y los firma con la clave privada a la que se vinculará la credencial.

Cabecera: qué es este JWT y qué clave lo firmó

{
  "typ": "openid4vci-proof+jwt",
  "alg": "ES256",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
  • typ: marca esto como una key proof de OpenID4VCI, para que no se confunda con otro tipo de JWT
  • alg: el algoritmo de firma, uno de los que el emisor listó en proof_signing_alg_values_supported
  • jwk: la clave pública a la que se vinculará la credencial; el emisor comprueba la firma contra ella

Payload: para quién es la proof y cuándo se hizo

{
  "aud": "https://issuer.fleetinspect.example",
  "iat": 1789376400,
  "nonce": "fi-nonce-77aa"
}
  • aud: el identificador del emisor, para que la proof no pueda reutilizarse ante otro emisor
  • iat: el momento en que se creó la proof, en segundos desde 1970
  • nonce: el c_nonce del nonce endpoint, que muestra que la proof es reciente

Resultado firmado

La cabecera y el payload se codifican cada uno en base64url y se unen con un punto. La wallet firma esa cadena con su clave privada y añade la firma codificada en base64url tras un segundo punto. La cadena resultante es el proof JWT que la wallet envía en la solicitud de credencial del paso 5.

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

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

5. Solicitud de credencial

La wallet llama al endpoint de credencial con el token de acceso y la proof, y el emisor genera y devuelve la credencial firmada.

Wallet

POST /credential

Envía el token de acceso, el id de configuración y el proof JWT, recibe la credencial firmada y 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>"]
  }
}

Respuesta

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

Todo el flujo de un vistazo

Este diagrama de secuencia reúne los cinco pasos del ejemplo desarrollado, desde la oferta hasta la notificación. Las flechas continuas son solicitudes, las discontinuas son respuestas, y la flecha punteada es el código de transacción viajando fuera del protocolo por SMS.

Wallet

Wallet empresarial de la empresa de transporte

Servidor de autorización

En este ejemplo lo gestiona el propio emisor

Emisor de credenciales

Organismo de inspección técnica de vehículos

  1. Emisor de credenciales a Wallet: Credential Offer, mostrada como código QR
  2. Emisor de credenciales a Wallet: tx_code, enviado por SMS al teléfono del gestor de la flota
  3. Wallet a Emisor de credenciales: GET /.well-known/openid-credential-issuer
  4. Emisor de credenciales a Wallet: metadatos del emisor de credenciales
  5. Wallet a Servidor de autorización: GET /.well-known/oauth-authorization-server
  6. Servidor de autorización a Wallet: metadatos del servidor de autorización
  7. Wallet a Servidor de autorización: POST /token: pre-authorized_code, tx_code
  8. Servidor de autorización a Wallet: access_token
  9. Wallet a Emisor de credenciales: POST /nonce
  10. Emisor de credenciales a Wallet: c_nonce
  11. Wallet: firma el proof JWT
  12. Wallet a Emisor de credenciales: POST /credential: token de acceso, proofs
  13. Emisor de credenciales a Wallet: credentials, notification_id
  14. Wallet: valida y almacena
  15. Wallet a Emisor de credenciales: POST /notify: credential_accepted
  16. Emisor de credenciales a Wallet: 204 No Content

Cuando la credencial aún no está lista: emisión diferida

El ejemplo anterior supone que el resultado de la inspección ya es definitivo. Si en cambio el organismo de inspección necesita escalar un resultado límite a un inspector superior, el endpoint de credencial no puede devolver la credencial de inmediato, así que difiere la emisión.

Emisión inmediata

El endpoint de credencial devuelve la credencial firmada en la misma respuesta que la solicitud.

Emisión diferida

El endpoint de credencial responde con HTTP 202, un transaction_id y un intervalo. La wallet consulta el deferred credential endpoint con ese id, esperando al menos interval segundos entre solicitudes, hasta que la credencial esté lista.

transaction_idconsultar deferred_credential_endpoint

Respuesta diferida de /credential

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

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

Consultando /deferred hasta que esté lista

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.

Cerrando el círculo: el notification endpoint

Tras la emisión, la wallet puede informar al emisor de lo que ocurrió con la credencial, usando el notification_id de la respuesta de credencial. Las wallets no están obligadas a enviar estas notificaciones y la entrega no está garantizada, así que un emisor no puede interpretar nada a partir de una notificación ausente.

Wallet

Envía un evento al notification_endpoint del emisor para el notification_id recibido, que cubre todas las credenciales de esa respuesta

credential_accepted

Almacenada en la wallet

credential_failure

La emisión falló por cualquier otro motivo, por ejemplo porque la credencial no se validó

credential_deleted

La emisión falló por causa del titular, por ejemplo porque rechazó almacenarla

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

Términos relacionados

Preguntas frecuentes

¿Cómo sabe una wallet qué tipo de grant usar?

La Credential Offer indica el grant en su objeto grants. authorization_code está presente cuando el emisor quiere que el titular inicie sesión como parte del flujo. pre-authorized_code está presente cuando el titular ya estaba autenticado en el canal donde se creó la oferta, por ejemplo por el inspector en el punto de inspección del ejemplo desarrollado a continuación. Una oferta puede incluir ambos, y entonces la wallet elige uno. Si la oferta no tiene ningún objeto grants, la wallet consulta en los metadatos del servidor de autorización qué tipos de grant admite.

¿Por qué la wallet obtiene los metadatos del emisor antes de solicitar nada?

La Credential Offer solo indica credential_configuration_ids, la URL del emisor y los grants. Los metadatos del emisor, servidos desde una ruta well-known, describen cada configuración: su formato, sus claims y los tipos de proof que acepta, además de los endpoints de nonce, credencial, deferred y notification. También indican qué servidor de autorización usar, y los metadatos de ese servidor proporcionan el endpoint de token. Sin ambos, la wallet no sabría cómo construir solicitudes válidas ni qué mostrar al titular antes de que dé su consentimiento.

¿Qué demuestra realmente la prueba de posesión?

Demuestra que la wallet que solicita la credencial posee la clave privada a la que se vinculará la credencial, no solo que dispone de un token de acceso válido. La wallet firma un proof JWT sobre el identificador del emisor y un c_nonce reciente obtenido del nonce endpoint del emisor, usando esa clave. El emisor incrusta la clave pública correspondiente en la credencial. Un verificador que exija key binding pide al titular que firme de nuevo con la misma clave al presentarla, de modo que una credencial copiada sin la clave no supera esa comprobación.

¿Por qué diferiría un emisor la emisión en lugar de devolver la credencial de inmediato?

Algunas comprobaciones que realiza el emisor antes de generar una credencial no pueden completarse dentro de una única solicitud HTTP, por ejemplo una revisión manual o una llamada a un registro externo lento. La emisión diferida permite que el endpoint de credencial responda de inmediato con un transaction_id en lugar de bloquear la conexión, y la wallet consulta el endpoint deferred con ese id hasta que la comprobación termina y la credencial está lista para recogerse.

¿Es obligatorio el notification endpoint para un emisor?

No. Es opcional para los emisores, y tampoco es obligatorio que las wallets lo usen. Cuando ambos lo admiten, el emisor conoce qué sucedió tras la emisión: las credenciales se almacenaron (credential_accepted), el titular detuvo la emisión, por ejemplo al rechazar almacenarlas (credential_deleted), o falló por otro motivo (credential_failure). La entrega no está garantizada, así que un emisor debe tratar una notificación como información útil, nunca como un registro fiable, y no puede sacar conclusiones de una notificación ausente.

Fuentes

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

Esta página tiene fines informativos y no constituye asesoramiento legal. Para obtener orientación autorizada, consulta directamente a la OpenID Foundation y a la Comisión Europea.

Habla con nosotros sobre la integración con EUDI Wallet