Authors:
- Mr George Padaytti (iGrant.io, Sweden)
- Mr Lal Chandran (iGrant.io, Sweden)
- Dr Andreas Abraham (ValidatedID, Spain)
Reviewers:
- Dr Nikos Triantafyllou (University of the Aegean, Greece)
- Mr Florin Coptil (Bosch, Germany)
- Mr Matteo Mirabelli (Infocert, Italy)
- Dr Mikael Linden (Vero, Finland)
- Mr Renaud Murat (Archipels, France)
- Mr. Sebastian Bickerle (Lissi ID, Germany)
Status: Approved for v1.0 release
Table of Contents
- EWC RFC001: Issue Verifiable Credential - v1.0
- 1.0 Summary
- 2.0 Motivation
- 3.0 Messages
- 4.0 Alternate response format
- 5.0 Implementers
- 6.0 Reference
- Appendix A: Public key resolution
This specification implements OID4VCI workflow for any issuer as per reference specification [1]. This minimises risks towards interoperability across the European Wallet Ecosystem with a standard specification in the EUDI wallet ecosystem as per the ARF [2] requirements.
The EWC LSP must align with the standard protocol for issuing credentials. This is the basis of interoperability between Issuers and Holders across the EWC LSPs. The assumption is that the user is familiar with the EWC-chosen protocols and standards and can refer to original standards references when necessary.
The credential issuance can be an authorisation flow or a pre-authorised one. These are depicted in the following diagrams, the assumption is that credential offer is obtained by holder wallet prior to discovery using same device (i.e. clicking on a link) or cross device (i.e scanning a QR code) flows.
sequenceDiagram
participant I as Individual using EUDI Wallet
participant O as Organisational Wallet (Issuer)
Note over I,O: Discovery issuer capabilities
I->>O: GET: /.well-known/openid-credential-issuer
O-->>I: OpenID credential issuer configuration
I->>O: GET: /.well-known/oauth-authorization-server
O-->>I: OAuth authorisation server metadata
Note over I,O: Authenticate and Authorise
I->>O: Authorisation request
O-->>I: Authorisation response
I->>O: Token request
O-->>I: Token response
Note over I,O: Issue credential
I->>O: POST: Credential request with token
O-->>I: Credential response with acceptance token
Figure 1: Issuance using Authorisation Code Flow based on [1]
sequenceDiagram
participant I as Individual using EUDI Wallet
participant O as Organisational Wallet (Issuer)
Note over I,O: Discovery issuer capabilities
I->>O: GET: /.well-known/openid-credential-issuer
O-->>I: OpenID credential issuer configuration
I->>O: GET: /.well-known/oauth-authorization-server
O-->>I: OAuth authorisation server metadata
Note over I,O: Authenticate and Authorise
I->>O: POST: Pre-authorised token request with PIN
O-->>I: Token response
Note over I,O: Issue credential
I->>O: POST: Credential request with token
O-->>I: Credential response with acceptance token
Figure 2: Issuance using Pre-Authorisation Code Flow based on [1]
The recommendation is to send the Credential Offer by Reference Using the credential_offer_uri
Parameter to avoid QR Code data overload. The endpoint that is expected to be embedded in the QR code is:
openid-credential-offer://?credential_offer_uri=https://server.example.com/credential-offer
Here, the credential_offer_uri
query param contains the URL in which the credential offer from the issuer can be resolved.
Once the credential_offer_uri
query param is resolved, the response can be either of the following formats.
{
"credential_issuer": "https://server.example.com",
"credentials": [
"VerifiablePortableDocumentA1"
],
"grants": {
"authorization_code": {
"issuer_state": "eyJhbGciOiJSU0Et...FYUaBy"
}
}
}
Note
In order to support all EBSI conformant wallets, the following format for the response is also valid, but not mandatory to be supported:
{
"credential_issuer": "https://server.example.com",
"credentials": [
{
"format": "jwt_vc_json",
"types": [
"VerifiableCredential",
"VerifiableAttestation",
"VerifiablePortableDocumentA1"
],
"trust_framework": {
"name": "ewc-issuer-trust-list",
"type": "Accreditation",
"uri": "TIR link towards accreditation"
}
}
],
"grants": {
"authorization_code": {
"issuer_state": "eyJhbGciOiJSU0Et...FYUaBy"
}
}
}
The holder wallet obtains the JSON and can process it. The format is the credential format. In the context of EWC LSPs, it can be jwt_vc
or sd-jwt_vc
.
For pre-authorised flow, the credential response is as given:
{
"credential_issuer": "https://server.example.com",
"credentials": [
{
"format": "jwt_vc_json",
"types": [
"VerifiableCredential",
"VerifiableAttestation",
"VerifiablePortableDocumentA1"
],
"trust_framework": {
"name": "ebsi",
"type": "Accreditation",
"uri": "TIR link towards accreditation"
}
}
],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJSU0Et...FYUaBy",
"user_pin_required": true
}
}
}
Here, the holder wallet requests the issuer’s authorisation server configurations.
Resolve /.well-known/openid-credential-issuer
endpoint for credential_issuer
URI in the credential offer response.
GET https://server.example.com/.well-known/openid-credential-issuer
Resolve /.well-known/oauth-authorization-server
endpoint for authorization_server
URI present in the response for the above.
GET https://server.example.com/.well-known/oauth-authorization-server
Once the well-known endpoint for issuer server configuration is resolved, the response is as given below with credentials_supported as defined by [6]:
{
"credential_issuer": "https://server.example.com",
"authorization_server": "https://server.example.com",
"credential_endpoint": "https://server.example.com/credential",
"deferred_credential_endpoint": "https://server.example.com/credential_deferred",
"display": [
{
"name": "Issuer",
"location": "Belgium",
"locale": "en-GB",
"description": "For queries about how we are managing your data please contact the Data Protection Officer."
}
],
"credentials_supported": {
"VerifiablePortableDocumentA1SdJwt": {
"format": "vc+sd-jwt",
"scope": "VerifiablePortableDocumentA1",
"cryptographic_binding_methods_supported": [
"jwk"
],
"cryptographic_suites_supported": [
"ES256"
],
"display": [
{
"name": "Portable Document A1",
"locale": "en-GB",
"background_color": "#12107c",
"text_color": "#FFFFFF"
}
],
"credential_definition": {
"type": "VerifiablePortableDocumentA1",
"claims": {
"given_name": {
"display": [
{
"name": "Given Name",
"locale": "en-GB"
},
{
"name": "Vorname",
"locale": "de-DE"
}
]
},
"last_name": {
"display": [
{
"name": "Surname",
"locale": "en-GB"
},
{
"name": "Nachname",
"locale": "de-DE"
}
]
}
}
}
}
}
}
Note
In order to support all EBSI conformant wallets, the following format for the response is also valid, but not mandatory to be supported:
{
"credential_issuer": "https://server.example.com",
"authorization_server": "https://server.example.com",
"credential_endpoint": "https://server.example.com/credential",
"deferred_credential_endpoint": "https://server.example.com/credential_deferred",
"display": {
"name": "Issuer",
"location": "Belgium",
"locale": "en-GB",
"description": "For queries about how we are managing your data please contact the Data Protection Officer."
},
"credentials_supported": [
{
"format": "jwt_vc",
"types": [
"VerifiableCredential",
"VerifiableAttestation",
"VerifiablePortableDocumentA1"
],
"trust_framework": {
"name": "ebsi",
"type": "Accreditation",
"uri": "TIR link towards accreditation"
},
"display": [
{
"name": "Portable Document A1",
"locale": "en-GB"
}
]
}
]
}
Once the well-known endpoint for authorisation server configuration is resolved, the response is as given below:
{
"issuer": "https://server.example.com",
"authorization_endpoint": "https://server.example.com/authorize",
"token_endpoint": "https://server.example.com/token",
"jwks_uri": "https://server.example.com/jwks",
"scopes_supported": [
"openid"
],
"response_types_supported": [
"vp_token",
"id_token"
],
"response_modes_supported": [
"query"
],
"grant_types_supported": [
"authorization_code"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"ES256"
],
"request_object_signing_alg_values_supported": [
"ES256"
],
"request_parameter_supported": true,
"request_uri_parameter_supported": true,
"token_endpoint_auth_methods_supported": [
"private_key_jwt"
],
"request_authentication_methods_supported": {
"authorization_endpoint": [
"request_object"
]
},
"vp_formats_supported": {
"jwt_vp": {
"alg_values_supported": [
"ES256"
]
},
"jwt_vc": {
"alg_values_supported": [
"ES256"
]
}
},
"subject_syntax_types_supported": [
"did:key:jwk_jcs-pub",
"did:ebsi:v1",
"did:ebsi:v2"
],
"subject_trust_frameworks_supported": [
"ebsi",
"ewc-issuer-trust-list"
],
"id_token_types_supported": [
"subject_signed_id_token",
"attester_signed_id_token"
]
}
Currently, we retain the trust framework specified by EBSI. Subsequently, we will specify an additional RFC defining the EWC trusted issuer list.
The authorisation request is to grant access to the credential endpoint. Below is an example of such a request:
GET https://my-issuer.rocks/auth/authorize?
&response_type=code
&scope=openid
&issuer_state=tracker%3Dvcfghhj
&state=client-state
&client_id=did%3Akey%3Az2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r
&authorization_details=%5B%7B%22format%22%3A%22jwt_vc%22%2C%22locations%22%3A%5B%22https%3A%2F%2Fissuer.example.com%22%5D%2C%22type%22%3A%22openid_credential%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22VerifiableAttestation%22%2C%22VerifiablePortableDocumentA1%22%5D%7D%5D
&redirect_uri=openid%3A
&nonce=glkFFoisdfEui43
&code_challenge=YjI0ZTQ4NTBhMzJmMmZhNjZkZDFkYzVhNzlhNGMyZDdjZDlkMTM4YTY4NjcyMTA5M2Q2OWQ3YjNjOGJlZDBlMSAgLQo%3D
&code_challenge_method=S256
&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22jwt_vp%22%3A%7B%22alg%22%3A%5B%22ES256%22%5D%7D%2C%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22ES256%22%5D%7D%7D%2C%22response_types_supported%22%3A%5B%22vp_token%22%2C%22id_token%22%5D%2C%22authorization_endpoint%22%3A%22openid%3A%2F%2F%22%7D
Host: https://server.example.com
Query params for the authorisation request are given below:
response_type
|
The value must be ‘code’ |
scope
|
The value must be ‘openid’ |
state
|
The client uses an opaque value to maintain the state between the request and callback. |
client_id
|
Decentralised identifier |
authorization_details
|
As specified in OAuth 2.0 Rich Authorization Requests specification to specify fine-grained access [4]. An example is as given below:
{
"type": "openid_credential",
"locations": [
"https://credential-issuer.example.com"
],
"format": "jwt_vc_json",
"credential_definition": {
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
]
}
} Note You may also use the earlier version as supported by EBSI. {
"format": "jwt_vc",
"locations": [
"https://issuer.example.com"
],
"type": "openid_credential",
"types": [
"VerifiableCredential",
"VerifiableAttestation",
"VerifiablePortableDocumentA1"
]
} |
redirect_uri
|
For redirection of the response |
code_challenge
|
As specified in PKCE for OAuth Public Client specification [5] |
code_challenge_method
|
As specified in PKCE for OAuth Public Client specification |
client_metadata
|
Holder wallets are non-reachable and can utilise this field in the Authorisation Request to deliver configuration |
issuer_state
|
If present in the credential offer |
The credential issuer can optionally request additional details to authenticate the client e.g. DID authentication. In this case, the authorisation response will contain a response_type
parameter with the value direct_post
. A sample response is as given:
HTTP/1.1 302 Found
Location: http://localhost:8080?state=22857405-1a41-4db9-a638-a980484ecae1&client_id=https%3A%2F%2Fapi-conformance.ebsi.eu%2Fconformance%2Fv3%2Fauth-mock&redirect_uri=https%3A%2F%2Fapi-conformance.ebsi.eu%2Fconformance%2Fv3%2Fauth-mock%2Fdirect_post&response_type=id_token&response_mode=direct_post&scope=openid&nonce=a6f24536-b109-4623-a41a-7a9be932bdf6&request_uri=https%3A%2F%2Fapi-conformance.ebsi.eu%2Fconformance%2Fv3%2Fauth-mock%2Frequest_uri%2F111d2819-9ab7-4959-83e5-f414c57fdc27
Query params for the authorisation response are given below:
state
|
The client uses an opaque value to maintain the state between the request and callback. |
client_id
|
Decentralised identifier |
redirect_uri
|
For redirection of the response |
response_type
|
The value must be id_token if the issuer requests DID authentication.
|
response_mode
|
The value must be direct_post
|
scope
|
The value must be openid
|
nonce
|
A value used to associate a client session with an ID token and to mitigate replay attacks |
request_uri
|
The authorisation server’s private key signed the request. |
The holder wallet then responds with an id_token
signed by the DID to the direct post endpoint.
POST /direct_post
Content-Type: application/x-www-form-urlencoded
&id_token=eyJraWQiOiJkaW...a980484ecae1
If additional details are not requested, the credential issuer will send an authorisation response with a code
query parameter containing the short-lived authorisation code. A sample response is given below:
HTTP/1.1 302 Found
Location: https://Wallet.example.org/cb?code=SplxlOBeZQQYbYS6WxSbIA
The token request for authorised code flow is as given:
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
&grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&redirect_uri=https%3A%2F%2FWallet.example.org%2Fcb
This request is made with the following query params:
grant_type
|
Grant type for authorisation. E.g. authorization_code
|
client_id
|
Decentralised identifier |
code
|
Authorisation code |
code_verifier
|
Wallet-generated secure random token used to validate the original code_challenge provided in the initial Authorization Request
|
The token request for pre-authorised code flow is as given:
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
&grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
&pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA
&user_pin=493536
This request is made with the following query params:
grant_type
|
Grant type for authorisation. E.g. urn:ietf:params:oauth:grant-type:pre-authorized_code
|
pre-authorized_code
|
Code representing the Credential Issuer's authorisation for the Wallet to obtain Credentials of a certain type. This code must be short-lived and single-use. |
user_pin
|
The end user pin is decided by the issuer and sent to the holder through an out-of-band process. E.g. Email, SMS |
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI4a5k..zEF",
"token_type": "bearer",
"expires_in": 86400,
"id_token": "eyJodHRwOi8vbWF0dHIvdGVuYW50L..3Mz",
"c_nonce": "PAPPf3h9lexTv3WYHZx8ajTe",
"c_nonce_expires_in": 86400
}
The Holder wallet makes a Credential Request to the Credential Endpoint as below:
POST /credential
Content-Type: application/json
Authorization: Bearer eyJ0eXAi...KTjcrDMg
{
"format": "vc+sd-jwt",
"credential_definition": {
"type": "VerifiablePortableDocumentA1"
},
"proof": {
"proof_type": "jwt",
"jwt":"eyJraW...KWjceMcr"
}
}
If specified, you can request specific fields to be included in the issued credential. If its not specified, all fields in the credential is included.
Note
In order to support all EBSI conformant wallets, the following format for the request is also valid, but not mandatory to be supported:
POST /credential
Content-Type: application/json
Authorization: Bearer eyJ0eXAi...KTjcrDMg
{
"format": "jwt_vc_json",
"proof": {
"jwt": "eyJraWQiOiJkaWQ6a2...su7UFClz9NQnw",
"proof_type": "jwt"
},
"types": [
"VerifiableCredential",
"VerifiableAttestation",
"VerifiablePortableDocumentA1"
]
}
The credential response can happen in-time or can be deferred as described below.
The In-time flow indicates that the credential is available immediately and the response format is as below:
{
"format": "vc+sd-jwt",
"credential": "eyJ0eXAiOi...F0YluuK2Cog",
"c_nonce": "fGFF7UkhLa",
"c_nonce_expires_in": 86400
}
If the credential is unavailable, the issuer responds with an acceptance token, indicating credential issuance is deferred.
{
"acceptance_token": "eyJ0eXAiOiJKV1QiLCJhbGci..zaEhOOXcifQ",
"c_nonce": "wlbQc6pCJp",
"c_nonce_expires_in": 86400
}
If the response contains acceptance_token
field, then it can be understood the credential is not available now and should be later available through the deferred credential endpoint.
POST /deferred-credential
Authorization: BEARER eyJ0eXAiOiJKV1QiLCJhbGci..zaEhOOXcifQ
Standard HTTP response codes shall be supported. Any additional ones can be formulated in the following format.
{
"error": "invalid_request",
"error_description": "The verifiable credential is expired"
}
The table below summarises the success/error responses that can be used:
Response format | Description |
invalid_request | Request failed. E.g. The verifiable credential is expired |
invalid_grant |
|
invalid_client | The Client tried to send a Token Request with a Pre-Authorized Code without Client ID, but the Authorization Server does not support anonymous access |
Please refer to the implementers table.
- OpenID Foundation (2023), 'OpenID for Verifiable Credential Issuance (OID4VCI)', Available at: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-12.html (Accessed: January 10, 2024).
- European Commission (2023) The European Digital Identity Wallet Architecture and Reference Framework (2023-04, v1.1.0) [Online]. Available at: https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework/releases (Accessed: October 16, 2023).
- OpenID Foundation (2023), 'Self-Issued OpenID Provider v2 (SIOP v2)', Available at: https://openid.net/specs/openid-connect-self-issued-v2-1_0.html (Accessed: October 01, 2023)
- OAuth 2.0 Rich Authorization Requests, Available at: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-11 (Accessed: February 01, 2024)
- Proof Key for Code Exchange by OAuth Public Clients, Available at: https://datatracker.ietf.org/doc/html/rfc7636 (Accessed: February 01, 2024)
- OpenID4VC High Assurance Interoperability Profile with SD-JWT VC - draft 00, Available at https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-sd-jwt-vc-1_0.html (Accessed: February 16, 2024)
For a JWT there are multiple ways for resolving the public key using the kid
header claim:
- If the key identifier is a DID then use a DID resolver to obtain the public key
- If the key identifier is not a DID, then resolve the JWKs endpoint in the AS configuration and match the public key from the JWK set using the key identifier.
Additionally, it is possible to specify JWK directly in the header using jwk
header claim.