|
1 | 1 | --- |
2 | 2 | title: "OID4VCI" |
3 | | -description: "Verifiable credential issuance with SD-JWT VC and deferred delivery." |
| 3 | +description: "OpenID4VCI credential issuance with pre-authorized grants, proof binding, c_nonce freshness, and deferred delivery." |
4 | 4 | --- |
5 | 5 |
|
6 | 6 | # OID4VCI |
7 | 7 |
|
8 | 8 | ## Standards |
9 | 9 |
|
10 | | -- OpenID for Verifiable Credential Issuance (OID4VCI) 1.0 |
| 10 | +- OpenID for Verifiable Credential Issuance 1.0 (final) |
| 11 | +- OAuth 2.0 Authorization Framework (token and grant semantics) |
| 12 | +- SD-JWT VC profile (`dc+sd-jwt` credential format) |
11 | 13 |
|
12 | 14 | ## Available Flows |
13 | 15 |
|
14 | 16 | | Flow ID | Name | Description | |
15 | 17 | |---------|------|-------------| |
16 | | -| `oid4vci-pre-authorized` | Pre-Authorized Code | Credential issuance with pre-authorized grant | |
17 | | -| `oid4vci-pre-authorized-tx-code` | Pre-Authorized + tx_code | Issuance with transaction code challenge | |
18 | | -| `oid4vci-deferred-issuance` | Deferred Issuance | Credential issued asynchronously via polling | |
| 18 | +| `oid4vci-pre-authorized` | Pre-Authorized Code | Wallet resolves offer, exchanges `pre-authorized_code`, and requests credential with proof | |
| 19 | +| `oid4vci-pre-authorized-tx-code` | Pre-Authorized + tx_code | Same flow with mandatory `tx_code` at token exchange | |
| 20 | +| `oid4vci-deferred-issuance` | Deferred Issuance | Credential endpoint returns `transaction_id`, then wallet polls deferred endpoint | |
19 | 21 |
|
20 | 22 | ## Demo Scenarios |
21 | 23 |
|
22 | | -- **Pre-Authorized Issuance** -- Direct pre-authorized code flow |
23 | | -- **Pre-Authorized + tx_code** -- Issuance with user-provided transaction code |
24 | | -- **Deferred Issuance Polling** -- Deferred credential retrieval |
| 24 | +- **Pre-Authorized Issuance** -- Resolve by-reference offer, exchange token, submit proof, receive SD-JWT VC |
| 25 | +- **Pre-Authorized + tx_code** -- Enforce out-of-band `tx_code` before token issuance |
| 26 | +- **Deferred Issuance Polling** -- Receive `transaction_id`, poll until credential is ready |
25 | 27 |
|
26 | 28 | ## Credential Details |
27 | 29 |
|
28 | 30 | | Property | Value | |
29 | 31 | |----------|-------| |
30 | | -| Credential type | `UniversityDegreeCredential` | |
| 32 | +| Credential configuration ID | `UniversityDegreeCredential` | |
31 | 33 | | VCT | `https://protocolsoup.com/credentials/university_degree` | |
32 | 34 | | Format | `dc+sd-jwt` (SD-JWT VC) | |
| 35 | +| Scope in token response | `vc:issue` | |
| 36 | +| Metadata `proof_types_supported` | `jwt` with `proof_signing_alg_values_supported: ["RS256"]` | |
| 37 | + |
| 38 | +## Issuance Flow (Implementation) |
| 39 | + |
| 40 | +1. Issuer creates a credential offer (by-reference or by-value), including `credential_issuer`, `credential_configuration_ids`, and pre-authorized grant data. |
| 41 | +2. Wallet resolves `credential_offer_uri` (for by-reference offers) and retrieves issuer metadata. |
| 42 | +3. Wallet calls token endpoint with `grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code`, `pre-authorized_code`, and `tx_code` when required. |
| 43 | +4. Token response includes `access_token`, `c_nonce`, and `c_nonce_expires_in`. |
| 44 | +5. Wallet submits credential request with `credential_configuration_id` and `proof`/`proofs` (`proof_type: jwt`) bound to current `c_nonce`. |
| 45 | +6. Issuer validates proof key binding, nonce freshness, audience/subject constraints, then returns: |
| 46 | + - immediate credential (`format`, `credential`, next `c_nonce`), or |
| 47 | + - deferred response (`transaction_id`, next `c_nonce`) for later polling. |
| 48 | + |
| 49 | +## Contract Enforcement |
| 50 | + |
| 51 | +- Credential offer envelope enforces XOR: exactly one of `credential_offer` or `credential_offer_uri`. |
| 52 | +- `tx_code` is required when the offer grant includes a `tx_code` object. |
| 53 | +- Proofs are required when proof types are declared for the credential configuration. |
| 54 | +- Proof JWT header `typ` must be `openid4vci-proof+jwt`. |
| 55 | +- Proof must include `iss`, `sub`, `aud`, `nonce`, `iat`, `exp`, and `cnf.jwk`. |
| 56 | +- Nonce is freshness-bound and one-time in issuance requests (replay and expiry are rejected). |
| 57 | +- Deferred polling requires matching `transaction_id` and same access token lineage. |
33 | 58 |
|
34 | 59 | ## Endpoints |
35 | 60 |
|
36 | 61 | | Path | Methods | Purpose | |
37 | 62 | |------|---------|---------| |
38 | | -| `/oid4vci/.well-known/openid-credential-issuer` | GET | Issuer metadata | |
39 | | -| `/.well-known/openid-credential-issuer/oid4vci` | GET | Issuer metadata (alternate) | |
| 63 | +| `/oid4vci/.well-known/openid-credential-issuer` | GET | Plugin-local metadata path (supported) | |
| 64 | +| `/.well-known/openid-credential-issuer/oid4vci` | GET | Canonical issuer-derived metadata path for issuer `.../oid4vci` | |
40 | 65 | | `/oid4vci/credential-offer/{offerID}` | GET | Credential offer by reference | |
41 | 66 | | `/oid4vci/offers/pre-authorized` | POST | Create pre-authorized offer | |
42 | 67 | | `/oid4vci/offers/pre-authorized/by-value` | POST | Create offer (by value) | |
43 | 68 | | `/oid4vci/offers/pre-authorized/deferred` | POST | Create deferred offer | |
44 | | -| `/oid4vci/token` | POST | Token endpoint (`pre-authorized_code`, `authorization_code`) | |
45 | | -| `/oid4vci/nonce` | POST | Nonce endpoint | |
46 | | -| `/oid4vci/credential` | POST | Credential request | |
47 | | -| `/oid4vci/deferred_credential` | POST | Deferred credential retrieval | |
| 69 | +| `/oid4vci/token` | POST | Token exchange (`pre-authorized_code` and `authorization_code`) | |
| 70 | +| `/oid4vci/nonce` | POST | Issue fresh `c_nonce` for valid access token | |
| 71 | +| `/oid4vci/credential` | POST | Validate proof and issue credential or deferred transaction | |
| 72 | +| `/oid4vci/deferred_credential` | POST | Poll deferred transaction until credential is ready | |
| 73 | + |
| 74 | +## Implementation Profile Notes |
| 75 | + |
| 76 | +- Token endpoint expects `application/x-www-form-urlencoded`; non-form content types are rejected. If `Content-Type` is omitted, request may still parse as form data. |
| 77 | +- Metadata for proof signing algorithms advertises `RS256`. |
| 78 | +- Proof verifier currently accepts RSA and EC wallet proof keys (`cnf.jwk`) and validates algorithm family alignment (`RS*` or `ES*`). |
| 79 | +- For deterministic guided demos, tx_code challenge value is generated as `123456` and surfaced through Looking Glass event data. |
48 | 80 |
|
49 | 81 | ## What To Validate |
50 | 82 |
|
51 | | -- Credential offer: `credential_issuer`, `credential_configuration_ids`, `grants` |
52 | | -- Token exchange: `pre-authorized_code` grant type, optional `tx_code` |
53 | | -- Proof: key binding proof JWT with `nonce` from nonce endpoint |
54 | | -- Credential response: SD-JWT VC with selective disclosure claims |
55 | | -- Deferred: `transaction_id` in initial response, polling with `deferred_credential` endpoint |
56 | | -- Wallet credential store: persisted at `{SHOWCASE_DATA_DIR}/vc/wallet_credentials.json` |
| 83 | +- Credential offer envelope: exactly one of `credential_offer` or `credential_offer_uri` |
| 84 | +- Metadata retrieval: canonical issuer-derived well-known path works for non-root issuer IDs |
| 85 | +- Token exchange: `pre-authorized_code` and `authorization_code` behaviors, plus `tx_code` enforcement when offered |
| 86 | +- Proof JWT: `typ=openid4vci-proof+jwt`, `cnf.jwk` key binding, `aud` matches issuer, `nonce` matches active `c_nonce` |
| 87 | +- Nonce behavior: replayed or expired `c_nonce` is rejected; next nonce is returned after successful issuance |
| 88 | +- Credential response: `dc+sd-jwt` serialization includes issuer-signed JWT and disclosures |
| 89 | +- Deferred flow: `issuance_pending` before ready time, then final credential on successful poll |
| 90 | +- Issuance lineage: wallet credential store persistence at `{SHOWCASE_DATA_DIR}/vc/wallet_credentials.json` |
0 commit comments