diff --git a/oid4vc/docs/admin-api-mso-mdoc.md b/oid4vc/docs/admin-api-mso-mdoc.md new file mode 100644 index 000000000..60b07a0bc --- /dev/null +++ b/oid4vc/docs/admin-api-mso-mdoc.md @@ -0,0 +1,200 @@ +# Admin API Reference — mso_mdoc (ISO 18013-5 mDOC Management) + +All routes are served on the **ACA-Py Admin Server** (`http://:`). The Swagger UI for the admin server is available at `/api/doc`. + +All mutating endpoints (`POST`, `PUT`, `PATCH`, `DELETE`) require an authenticated request in multi-tenant deployments (Bearer token or API key, depending on ACA-Py configuration). + +## Error Responses + +| HTTP Status | Meaning | +|---|---| +| `400 Bad Request` | Invalid or missing request parameters, validation error, storage error | +| `401 Unauthorized` | Missing or invalid authentication | +| `404 Not Found` | Requested record does not exist | +| `500 Internal Server Error` | Signing failure, key/certificate error, unexpected server error | + +Error response body: + +```json +{ "reason": "human-readable error message" } +``` + +--- + +## Tag: `mso_mdoc` — ISO 18013-5 mDOC Management + +Requires the `mso_mdoc` plugin to be loaded. See [Credential Formats — mso_mdoc](credential-formats.md#mso_mdoc) for background. + +### Key and Certificate Management + +On startup the `mso_mdoc` plugin auto-generates a default EC P-256 signing key and a self-signed certificate. You can inspect and extend these via the following endpoints. + +#### `GET /mso_mdoc/keys` + +List all mDOC signing keys. + +**Example request:** + +```bash +curl http://localhost:8021/mso_mdoc/keys +``` + +--- + +#### `GET /mso_mdoc/certificates` + +List all mDOC signing certificates. + +--- + +#### `GET /mso_mdoc/certificates/default` + +Get the default (active) signing certificate. + +--- + +#### `POST /mso_mdoc/generate-keys` + +Generate a new mDOC signing key and a self-signed certificate. + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/generate-keys +``` + +**Response `200`:** The newly created key and certificate records. + +--- + +### Trust Anchors + +Trust anchors are root CA certificates used to verify mDOC credentials received from holders. A trust anchor chain must be established for `POST /oid4vp/response` to verify mDOC presentations. + +#### `POST /mso_mdoc/trust-anchors` + +Add a trust anchor certificate. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `certificate_pem` | string | **Yes** | PEM-encoded X.509 root CA certificate | +| `anchor_id` | string | No | Custom ID. If not provided, a UUID is generated. | +| `metadata` | object | No | Arbitrary metadata attached to the trust anchor record | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/trust-anchors \ + -H "Content-Type: application/json" \ + -d '{ + "certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n", + "anchor_id": "iso-test-iaca-2024", + "metadata": {"description": "ISO test IACA root"} + }' +``` + +**Response `200`:** Trust anchor record. + +--- + +#### `GET /mso_mdoc/trust-anchors` + +List all stored trust anchors. + +--- + +#### `GET /mso_mdoc/trust-anchors/{anchor_id}` + +Fetch a trust anchor by ID. + +--- + +#### `DELETE /mso_mdoc/trust-anchors/{anchor_id}` + +Remove a trust anchor. + +--- + +### mDOC Signing and Verification + +These endpoints are available for manual signing/verification operations, independent of the OID4VCI issuance flow. + +#### `POST /mso_mdoc/sign` + +Manually sign a payload as an mDOC CBOR binary per ISO 18013-5. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `payload` | object | **Yes** | Claims organized by namespace (e.g. `{"org.iso.18013.5.1": {"given_name": "Alice"}}`) | +| `headers` | object | No | Additional COSE header parameters | +| `did` | string | No | DID to use for signing | +| `verificationMethod` | string | No | Specific verification method to use | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/sign \ + -H "Content-Type: application/json" \ + -d '{ + "payload": { + "org.iso.18013.5.1": { + "given_name": "Alice", + "family_name": "Smith", + "birth_date": "1990-01-01" + } + } + }' +``` + +**Response `200`:** CBOR hex-encoded mDOC binary. + +--- + +#### `POST /mso_mdoc/verify` + +Verify a CBOR-encoded mDOC binary. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `mso_mdoc` | string | **Yes** | CBOR hex-encoded mDOC device response | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/verify \ + -H "Content-Type: application/json" \ + -d '{"mso_mdoc": "a36776657273...cbor-hex..."}' +``` + +**Response `200`:** + +| Field | Type | Description | +|---|---|---| +| `valid` | boolean | Whether verification succeeded | +| `error` | string | Error message if `valid` is `false` | +| `kid` | string | Key ID of the signing key | +| `headers` | object | COSE headers from the signed document | +| `payload` | object | Decoded claims organized by namespace | + +**Example response:** + +```json +{ + "valid": true, + "error": null, + "kid": "did:jwk:eyJ...#0", + "headers": {"alg": "ES256"}, + "payload": { + "org.iso.18013.5.1": { + "given_name": "Alice", + "family_name": "Smith" + } + } +} +``` diff --git a/oid4vc/docs/admin-api-oid4vci.md b/oid4vc/docs/admin-api-oid4vci.md new file mode 100644 index 000000000..3c36bf72c --- /dev/null +++ b/oid4vc/docs/admin-api-oid4vci.md @@ -0,0 +1,499 @@ +# Admin API Reference — OID4VCI (Credential Issuance) + +All routes are served on the **ACA-Py Admin Server** (`http://:`). The Swagger UI for the admin server is available at `/api/doc`. + +All mutating endpoints (`POST`, `PUT`, `PATCH`, `DELETE`) require an authenticated request in multi-tenant deployments (Bearer token or API key, depending on ACA-Py configuration). + +## Error Responses + +| HTTP Status | Meaning | +|---|---| +| `400 Bad Request` | Invalid or missing request parameters, validation error, storage error | +| `401 Unauthorized` | Missing or invalid authentication | +| `404 Not Found` | Requested record does not exist | +| `500 Internal Server Error` | Signing failure, key/certificate error, unexpected server error | + +Error response body: + +```json +{ "reason": "human-readable error message" } +``` + +--- + +## Tag: `oid4vci` — Credential Issuance Management + +### DID Management + +#### `POST /did/jwk/create` + +Create a `did:jwk` DID backed by the specified key type. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `key_type` | string | **Yes** | Key algorithm. One of: `ed25519`, `p256` | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/did/jwk/create \ + -H "Content-Type: application/json" \ + -d '{"key_type": "p256"}' +``` + +**Response `200`:** + +| Field | Type | Description | +|---|---|---| +| `did` | string | The created `did:jwk:...` DID | + +**Example response:** + +```json +{ + "did": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6Ii4uLiIsInkiOiIuLi4ifQ" +} +``` + +--- + +### Supported Credentials + +Supported credential records define which credential types the issuer can issue — the format, display metadata, and credential schema. They appear in the `credential_configurations_supported` field of the credential issuer metadata (`/.well-known/openid-credential-issuer`). + +#### `POST /oid4vci/credential-supported/create` + +Register a supported credential using a generic schema (format-agnostic). + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `format` | string | **Yes** | Credential format (e.g. `jwt_vc_json`, `vc+sd-jwt`, `mso_mdoc`) | +| `id` | string | **Yes** | Identifier for this credential configuration (e.g. `UniversityDegreeCredential`) | +| `cryptographic_binding_methods_supported` | array of strings | No | Supported binding methods (e.g. `["did:jwk", "jwk"]`) | +| `cryptographic_suites_supported` | array of strings | No | Supported cryptographic suites (e.g. `["ES256"]`) | +| `proof_types_supported` | object | No | Supported proof types (e.g. `{"jwt": {"proof_signing_alg_values_supported": ["ES256"]}}`) | +| `display` | array of objects | No | Display metadata (name, logo, locale) per language | +| `format_data` | object | No | Format-specific metadata (merged into issuer metadata output) | +| `vc_additional_data` | object | No | Additional VC data such as `@context` and `type` arrays | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/credential-supported/create \ + -H "Content-Type: application/json" \ + -d '{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "cryptographic_binding_methods_supported": ["did:jwk", "jwk"], + "display": [{"name": "Employee Credential", "locale": "en-US"}] + }' +``` + +**Response `200`:** `SupportedCredentialSchema` — the created record. + +--- + +#### `POST /oid4vci/credential-supported/create/jwt` + +Register a JWT VC credential configuration (`jwt_vc_json` format). Provides a typed schema for the W3C Verifiable Credential structure. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `format` | string | **Yes** | Must be `jwt_vc_json` | +| `id` | string | **Yes** | Credential configuration identifier | +| `type` | array of strings | **Yes** | W3C VC `type` array (e.g. `["VerifiableCredential", "UniversityDegreeCredential"]`) | +| `@context` | array | **Yes** | JSON-LD contexts (e.g. `["https://www.w3.org/2018/credentials/v1"]`) | +| `cryptographic_binding_methods_supported` | array of strings | No | Supported binding methods | +| `cryptographic_suites_supported` | array of strings | No | Supported suites | +| `proof_types_supported` | object | No | Supported proof types | +| `display` | array of objects | No | Display metadata | +| `credentialSubject` | object | No | Display metadata per claim (shown in wallets) | +| `order` | array of strings | No | Display ordering of claims | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/credential-supported/create/jwt \ + -H "Content-Type: application/json" \ + -d '{ + "format": "jwt_vc_json", + "id": "UniversityDegreeCredential", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + "cryptographic_binding_methods_supported": ["did:jwk"], + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256", "EdDSA"]} + }, + "display": [{"name": "University Degree", "locale": "en-US"}], + "credentialSubject": { + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Family Name", "locale": "en-US"}]}, + "degree": {"display": [{"name": "Degree", "locale": "en-US"}]} + } + }' +``` + +**Response `200`:** + +```json +{ + "supported_cred_id": "3f1a2b4c-...", + "format": "jwt_vc_json", + "identifier": "UniversityDegreeCredential", + ... +} +``` + +--- + +#### `POST /oid4vci/credential-supported/create/sd-jwt` + +Register an SD-JWT VC credential configuration. Requires the `sd_jwt_vc` plugin to be loaded. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `format` | string | **Yes** | `vc+sd-jwt` or `dc+sd-jwt` | +| `id` | string | **Yes** | Credential configuration identifier | +| `vct` | string | **Yes** | Verifiable Credential Type string (e.g. `EmployeeCredential`) | +| `cryptographic_binding_methods_supported` | array of strings | No | | +| `cryptographic_suites_supported` | array of strings | No | | +| `display` | array of objects | No | | +| `claims` | object | No | Per-claim display metadata (keyed by claim name) | +| `order` | array of strings | No | Display ordering of claims | +| `sd_list` | array of strings | No | JSON Pointer paths to claims that should be selectively disclosable (e.g. `["/given_name", "/address/street_address"]`). Claims not in this list are always disclosed. | + +**Protected claims** (cannot be in `sd_list`): `/iss`, `/exp`, `/vct`, `/nbf`, `/cnf`, `/status` + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/credential-supported/create/sd-jwt \ + -H "Content-Type: application/json" \ + -d '{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "vct": "EmployeeCredential", + "cryptographic_binding_methods_supported": ["did:jwk", "jwk"], + "display": [{"name": "Employee Credential", "locale": "en-US"}], + "claims": { + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Family Name", "locale": "en-US"}]}, + "department": {"display": [{"name": "Department", "locale": "en-US"}]} + }, + "sd_list": ["/given_name", "/family_name", "/department"] + }' +``` + +**Response `200`:** `SupportedCredentialSchema` + +--- + +#### `GET /oid4vci/credential-supported/records` + +List all supported credential configurations. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `supported_cred_id` | string | Filter by record ID | +| `format` | string | Filter by format (e.g. `jwt_vc_json`) | + +**Example request:** + +```bash +curl http://localhost:8021/oid4vci/credential-supported/records +``` + +**Response `200`:** + +```json +{ + "results": [ + { + "supported_cred_id": "3f1a2b4c-...", + "format": "jwt_vc_json", + "identifier": "UniversityDegreeCredential", + ... + } + ] +} +``` + +--- + +#### `GET /oid4vci/credential-supported/records/{supported_cred_id}` + +Fetch a single supported credential configuration by ID. + +**Path parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `supported_cred_id` | string | **Yes** | Supported credential record ID | + +**Example request:** + +```bash +curl http://localhost:8021/oid4vci/credential-supported/records/3f1a2b4c-... +``` + +**Response `200`:** `SupportedCredentialSchema` + +--- + +#### `PUT /oid4vci/credential-supported/records/jwt/{supported_cred_id}` + +Replace a JWT VC supported credential record (complete replacement). + +**Path parameters:** `supported_cred_id` + +**Request body:** Same as `POST /oid4vci/credential-supported/create/jwt` + +**Response `200`:** + +```json +{ + "supported_cred": { ... }, + "supported_cred_id": "3f1a2b4c-..." +} +``` + +--- + +#### `PUT /oid4vci/credential-supported/records/sd-jwt/{supported_cred_id}` + +Replace an SD-JWT supported credential record. Requires `sd_jwt_vc` plugin. + +**Path parameters:** `supported_cred_id` + +**Request body:** Same as `POST /oid4vci/credential-supported/create/sd-jwt` + +**Response `200`:** + +```json +{ + "supported_cred": { ... }, + "supported_cred_id": "3f1a2b4c-..." +} +``` + +--- + +#### `DELETE /oid4vci/credential-supported/records/jwt/{supported_cred_id}` + +Remove a supported credential record. + +**Example request:** + +```bash +curl -X DELETE http://localhost:8021/oid4vci/credential-supported/records/jwt/3f1a2b4c-... +``` + +**Response `200`:** `{}` + +--- + +### Exchange Records + +An exchange record represents a single credential issuance lifecycle. + +#### `POST /oid4vci/exchange/create` + +Create a new exchange record. This is the primary step that binds a specific holder to a supported credential type before generating an offer. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `supported_cred_id` | string | **Yes** | ID of the supported credential to issue | +| `credential_subject` | object | **Yes** | The claims/values to include in the issued credential | +| `did` | string | No | DID of the issuer. If omitted, ACA-Py's default DID is used | +| `verification_method` | string (URI) | No | Specific verification method URI to use for signing | +| `pin` | string | No | User PIN to be delivered out of band to the holder. Required at token request time if set. | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/exchange/create \ + -H "Content-Type: application/json" \ + -d '{ + "supported_cred_id": "3f1a2b4c-...", + "credential_subject": { + "given_name": "Alice", + "family_name": "Smith", + "degree": "Bachelor of Science" + }, + "did": "did:jwk:eyJjcnYiOiJQLTI1NiIs..." + }' +``` + +**Response `200`:** `OID4VCIExchangeRecordSchema` + +| Field | Description | +|---|---| +| `exchange_id` | Unique record ID | +| `state` | `created` | +| `supported_cred_id` | Linked supported credential | +| `credential_subject` | Provided claim values | +| `issuer_id` | DID used for signing | +| `verification_method` | Verification method URI | +| `pin` | User PIN (if set) | + +--- + +#### `GET /oid4vci/exchange/records` + +List exchange records with optional filtering. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `exchange_id` | string (UUID) | Filter by exchange ID | +| `supported_cred_id` | string | Filter by supported credential ID | +| `state` | string | Filter by state. One of: `created`, `offer`, `issued`, `failed`, `accepted`, `deleted`, `superceded` | + +**Example request:** + +```bash +curl "http://localhost:8021/oid4vci/exchange/records?state=issued" +``` + +**Response `200`:** + +```json +{ + "results": [ + { + "exchange_id": "abc123-...", + "state": "issued", + ... + } + ] +} +``` + +--- + +#### `GET /oid4vci/exchange/records/{exchange_id}` + +Fetch a single exchange record by ID. + +**Example request:** + +```bash +curl http://localhost:8021/oid4vci/exchange/records/abc123-... +``` + +**Response `200`:** `OID4VCIExchangeRecordSchema` + +--- + +#### `DELETE /oid4vci/exchange/records/{exchange_id}` + +Delete an exchange record. + +**Example request:** + +```bash +curl -X DELETE http://localhost:8021/oid4vci/exchange/records/abc123-... +``` + +**Response `200`:** `{}` + +--- + +### Credential Offers + +#### `GET /oid4vci/credential-offer` + +Generate a credential offer by value. The entire offer JSON is embedded in the `openid-credential-offer://` URI. Moves the exchange to state `offer`. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `exchange_id` | string | Exchange record ID | +| `user_pin_required` | boolean | Whether the holder must supply the PIN at token time | + +**Example request:** + +```bash +curl "http://localhost:8021/oid4vci/credential-offer?exchange_id=abc123-..." +``` + +**Response `200`:** + +| Field | Description | +|---|---| +| `credential_offer` | Full `openid-credential-offer://?credential_offer=...` URI (can be shown as QR code) | +| `offer.credential_issuer` | Base URL of the issuer | +| `offer.credential_configuration_ids` | Array of credential type identifiers | +| `offer.grants.pre_authorized_code` | The pre-authorized code | +| `offer.grants.user_pin_required` | Whether a PIN is required | + +**Example response:** + +```json +{ + "credential_offer": "openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22...", + "offer": { + "credential_issuer": "https://issuer.example.com", + "credential_configuration_ids": ["UniversityDegreeCredential"], + "grants": { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": "SplxlOBeZQQYbYS6WxSbIA", + "user_pin_required": false + } + } + } +} +``` + +--- + +#### `GET /oid4vci/credential-offer-by-ref` + +Generate a credential offer by reference. Returns a `credential_offer_uri` pointing to an endpoint where the wallet can retrieve the offer. This is useful when the offer JSON is too large to embed in a QR code. + +**Query parameters:** Same as `/oid4vci/credential-offer` + +**Response `200`:** + +| Field | Description | +|---|---| +| `credential_offer_uri` | `openid-credential-offer://?credential_offer_uri=...` URI | +| `offer` | The offer object (same structure as above) | + +--- + +### Credential Refresh + +#### `PATCH /oid4vci/credential-refresh/{refresh_id}` + +Issue a refreshed credential for an existing exchange. Creates a new exchange record that supersedes the original (original state → `superceded`). Returns the new credential offer. + +**Path parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `refresh_id` | string | **Yes** | Refresh identifier from the original exchange record | + +**Example request:** + +```bash +curl -X PATCH http://localhost:8021/oid4vci/credential-refresh/refresh-abc123 +``` + +**Response `200`:** `OID4VCIExchangeRecordSchema` (new exchange) + +--- diff --git a/oid4vc/docs/admin-api-oid4vp.md b/oid4vc/docs/admin-api-oid4vp.md new file mode 100644 index 000000000..498e10573 --- /dev/null +++ b/oid4vc/docs/admin-api-oid4vp.md @@ -0,0 +1,493 @@ +# Admin API Reference — OID4VP (Presentation & Verification) + +All routes are served on the **ACA-Py Admin Server** (`http://:`). The Swagger UI for the admin server is available at `/api/doc`. + +All mutating endpoints (`POST`, `PUT`, `PATCH`, `DELETE`) require an authenticated request in multi-tenant deployments (Bearer token or API key, depending on ACA-Py configuration). + +## Error Responses + +| HTTP Status | Meaning | +|---|---| +| `400 Bad Request` | Invalid or missing request parameters, validation error, storage error | +| `401 Unauthorized` | Missing or invalid authentication | +| `404 Not Found` | Requested record does not exist | +| `500 Internal Server Error` | Signing failure, key/certificate error, unexpected server error | + +Error response body: + +```json +{ "reason": "human-readable error message" } +``` + +--- + +## Tag: `oid4vp` — Presentation / Verification Management + +### Presentation Definitions (PEX v2) + +Presentation definitions specify which credentials a verifier wants to receive and what constraints they must satisfy. + +#### `POST /oid4vp/presentation-definition` + +Create and store a PEX v2 presentation definition. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `pres_def` | object | **Yes** | A valid PEX v2 presentation definition JSON object | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vp/presentation-definition \ + -H "Content-Type: application/json" \ + -d '{ + "pres_def": { + "id": "employee-credential-request", + "input_descriptors": [ + { + "id": "employee-credential", + "format": { + "jwt_vc_json": {"alg": ["ES256", "EdDSA"]} + }, + "constraints": { + "fields": [ + { + "path": ["$.vc.type"], + "filter": { + "type": "array", + "contains": {"const": "UniversityDegreeCredential"} + } + } + ] + } + } + ] + } + }' +``` + +**Response `200`:** + +```json +{ + "pres_def": { ... }, + "pres_def_id": "550e8400-..." +} +``` + +--- + +#### `GET /oid4vp/presentation-definitions` + +List all stored presentation definitions. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `pres_def_id` | string | Filter by ID | + +**Response `200`:** + +```json +{ + "results": [ + { + "pres_def_id": "550e8400-...", + "pres_def": { ... } + } + ] +} +``` + +--- + +#### `GET /oid4vp/presentation-definition/{pres_def_id}` + +Fetch a stored presentation definition by ID. + +--- + +#### `PUT /oid4vp/presentation-definition/{pres_def_id}` + +Replace a presentation definition. + +**Request body:** `{ "pres_def": { ... } }` + +**Response `200`:** `{ "pres_def": {...}, "pres_def_id": "..." }` + +--- + +#### `DELETE /oid4vp/presentation-definition/{pres_def_id}` + +Delete a presentation definition. + +**Response `200`:** `{}` + +--- + +### DCQL Queries + +DCQL (Digital Credentials Query Language) is an alternative to PEX for specifying credential requests. Use DCQL for mDOC/SD-JWT queries where PEX constraints may be insufficient. + +#### `POST /oid4vp/dcql/queries` + +Create and store a DCQL query. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `credentials` | array | **Yes** | Array of `CredentialQuery` objects describing the required credentials | +| `credential_sets` | array | No | Optional grouping of credential queries with logical AND/OR semantics | + +**`CredentialQuery` object:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `id` | string | **Yes** | Identifier for this credential query (used as key in `vp_token` response) | +| `format` | string | **Yes** | Expected credential format (e.g. `mso_mdoc`, `vc+sd-jwt`, `jwt_vc_json`) | +| `meta` | object | No | Format-specific metadata (see below) | +| `claims` | array | No | Array of `ClaimsQuery` objects specifying required claims | +| `claim_sets` | array of arrays | No | Alternative claim combinations | + +**`meta` object** (format-specific): + +| Field | Format | Description | +|---|---|---| +| `doctype_value` | `mso_mdoc` | Required mDoc docType string (e.g. `org.iso.18013.5.1.mDL`) | +| `doctype_values` | `mso_mdoc` | List of acceptable docType strings | +| `vct_values` | `vc+sd-jwt` | List of acceptable `vct` values | + +**`ClaimsQuery` object:** + +| Field | Description | +|---|---| +| `id` | Claim query identifier | +| `namespace` | mDOC namespace (for `mso_mdoc`) | +| `claim_name` | Claim name (for `mso_mdoc`) | +| `path` | JSON Path array (for JWT/SD-JWT, e.g. `["$.given_name"]`) | +| `values` | Acceptable values (optional constraint) | + +**Example request (mDOC driver's license):** + +```bash +curl -X POST http://localhost:8021/oid4vp/dcql/queries \ + -H "Content-Type: application/json" \ + -d '{ + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + }, + "claims": [ + { + "namespace": "org.iso.18013.5.1", + "claim_name": "given_name" + }, + { + "namespace": "org.iso.18013.5.1", + "claim_name": "family_name" + }, + { + "namespace": "org.iso.18013.5.1", + "claim_name": "birth_date" + } + ] + } + ] + }' +``` + +**Example request (SD-JWT employee credential):** + +```bash +curl -X POST http://localhost:8021/oid4vp/dcql/queries \ + -H "Content-Type: application/json" \ + -d '{ + "credentials": [ + { + "id": "employee", + "format": "vc+sd-jwt", + "meta": { + "vct_values": ["EmployeeCredential"] + }, + "claims": [ + {"path": ["$.given_name"]}, + {"path": ["$.department"]} + ] + } + ] + }' +``` + +**Response `200`:** + +```json +{ + "dcql_query": { + "credentials": [...], + "credential_sets": null + } +} +``` + +> Note: the query `dcql_query_id` is returned in the `Location` header and can be retrieved via `GET /oid4vp/dcql/queries`. + +--- + +#### `GET /oid4vp/dcql/queries` + +List all stored DCQL queries. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `dcql_query_id` | string | Filter by ID | + +**Response `200`:** + +```json +{ + "results": [ + { + "dcql_query_id": "...", + "credentials": [...], + "credential_sets": null + } + ] +} +``` + +--- + +#### `GET /oid4vp/dcql/query/{dcql_query_id}` + +Fetch a DCQL query by ID. + +**Response `200`:** + +```json +{ + "dcql_query_id": "...", + "credentials": [...], + "credential_sets": null +} +``` + +--- + +#### `DELETE /oid4vp/dcql/query/{dcql_query_id}` + +Delete a DCQL query. + +--- + +### VP Requests + +A VP request initiates a presentation exchange. It generates the `openid://` URI that the holder scans. + +#### `POST /oid4vp/request` + +Create an OID4VP authorization request. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `vp_formats` | object | **Yes** | Format constraints for the expected VP. Keys are format strings; values are alg constraints. | +| `pres_def_id` | string | No | ID of a stored presentation definition (mutually exclusive with `dcql_query_id`) | +| `dcql_query_id` | string | No | ID of a stored DCQL query (mutually exclusive with `pres_def_id`) | + +Either `pres_def_id` or `dcql_query_id` must be provided. + +**Example request (PEX):** + +```bash +curl -X POST http://localhost:8021/oid4vp/request \ + -H "Content-Type: application/json" \ + -d '{ + "pres_def_id": "550e8400-...", + "vp_formats": { + "jwt_vc_json": {"alg": ["ES256", "EdDSA"]}, + "jwt_vp_json": {"alg": ["ES256", "EdDSA"]} + } + }' +``` + +**Example request (DCQL):** + +```bash +curl -X POST http://localhost:8021/oid4vp/request \ + -H "Content-Type: application/json" \ + -d '{ + "dcql_query_id": "dcql-abc123-...", + "vp_formats": { + "mso_mdoc": {"alg": ["ES256"]} + } + }' +``` + +**Response `200`:** + +| Field | Description | +|---|---| +| `request_uri` | The `openid://?client_id=...&request_uri=...` URI (present to holder as QR code) | +| `request.request_id` | ID of the VP request record | +| `presentation.presentation_id` | ID of the presentation record to poll for results | +| `presentation.state` | Initial state: `request-created` | + +**Example response:** + +```json +{ + "request_uri": "openid://?client_id=did:jwk:...&request_uri=https://verifier.example.com/oid4vp/request/abc123", + "request": { + "request_id": "abc123-...", + "pres_def_id": "550e8400-...", + "vp_formats": { ... } + }, + "presentation": { + "presentation_id": "def456-...", + "state": "request-created", + "request_id": "abc123-..." + } +} +``` + +--- + +#### `GET /oid4vp/requests` + +List VP request records. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `request_id` | string (UUID) | Filter by request ID | +| `pres_def_id` | string | Filter by presentation definition ID | +| `dcql_query_id` | string | Filter by DCQL query ID | + +--- + +#### `GET /oid4vp/request/{request_id}` + +Fetch a VP request record by ID. Note: the request record is **deleted after the holder retrieves the JAR** (signed request object), so this endpoint returns `404` after the holder scans the QR code. + +--- + +### Presentations + +Presentation records track the lifecycle of a VP exchange from request creation to verification result. + +#### `GET /oid4vp/presentations` + +List presentation records. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `presentation_id` | string (UUID) | Filter by ID | +| `pres_def_id` | string | Filter by presentation definition | +| `state` | string | Filter by state. One of: `request-created`, `request-retrieved`, `presentation-received`, `presentation-invalid`, `presentation-valid` | + +--- + +#### `GET /oid4vp/presentation/{presentation_id}` + +Fetch a presentation record by ID. Poll this endpoint after creating a VP request to check whether the holder has responded and whether verification succeeded. + +**Response `200`:** + +| Field | Type | Description | +|---|---|---| +| `presentation_id` | string | Record ID | +| `state` | string | Current state | +| `errors` | array of strings | Validation errors (non-empty when state is `presentation-invalid`) | +| `verified_claims` | object | Verified claim values (non-empty when state is `presentation-valid`) | +| `matched_credentials` | object | Full matched credential records | + +**Presentation states:** + +| State | Meaning | +|---|---| +| `request-created` | VP request generated; waiting for holder to scan | +| `request-retrieved` | Holder retrieved the signed request JAR | +| `presentation-received` | Holder submitted a VP (processing underway) | +| `presentation-valid` | VP signature and constraints verified successfully | +| `presentation-invalid` | VP failed verification (see `errors`) | + +**Example poll loop:** + +```bash +PRES_ID="def456-..." +while true; do + STATE=$(curl -s http://localhost:8031/oid4vp/presentation/$PRES_ID | python3 -c "import json,sys; print(json.load(sys.stdin)['state'])") + echo "State: $STATE" + if [[ "$STATE" == "presentation-valid" || "$STATE" == "presentation-invalid" ]]; then + break + fi + sleep 2 +done +``` + +--- + +#### `DELETE /oid4vp/presentation/{presentation_id}` + +Delete a presentation record. + +--- + +### X.509 Identity + +When a verifier needs to use `x509_san_dns` client authentication (required by some wallet protocols), register an X.509 certificate chain here. Once registered, all VP requests use the DNS name as the `client_id` and include the `x5c` header in the signed JAR. + +#### `POST /oid4vp/x509-identity` + +Register an X.509 identity. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `cert_chain_pem` | string | **Yes** | PEM-encoded certificate chain, leaf certificate first | +| `verification_method` | string | **Yes** | Verification method ID used for signing the JAR (e.g. `did:jwk:...#0`) | +| `client_id` | string | **Yes** | DNS name that will be used as the OID4VP `client_id` (e.g. `verifier.example.com`) | + +**Example request:** + +```bash +curl -X POST http://localhost:8031/oid4vp/x509-identity \ + -H "Content-Type: application/json" \ + -d '{ + "cert_chain_pem": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n", + "verification_method": "did:jwk:eyJ...#0", + "client_id": "verifier.example.com" + }' +``` + +**Response `200`:** `{}` + +--- + +#### `GET /oid4vp/x509-identity` + +Retrieve the registered X.509 identity. + +**Response `200`:** The stored identity record. + +--- + +#### `DELETE /oid4vp/x509-identity` + +Remove the X.509 identity. Subsequent VP requests will revert to using `did:jwk` as the `client_id`. + +--- diff --git a/oid4vc/docs/admin-api-reference.md b/oid4vc/docs/admin-api-reference.md new file mode 100644 index 000000000..5f56f53e5 --- /dev/null +++ b/oid4vc/docs/admin-api-reference.md @@ -0,0 +1,1227 @@ +# Admin API Reference + +The OID4VC plugin provides a comprehensive Admin API formanaging credential issuance, presentation verification, and DID management. All routes are served on the **ACA-Py Admin Server** (`http://:`). + +The Swagger UI for the admin server is available at `/api/doc`. + +## API Categories + +The Admin API is organized into three main categories: + +### [OID4VCI — Credential Issuance Management](admin-api-oid4vci.md) + +Endpoints for managing credential issuance using OpenID for Verifiable Credential Issuance (OID4VCI): + +- **DID Management** — Create `did:jwk` DIDs for credential signing +- **Supported Credentials** — Configure credential types, formats, and display metadata +- **Exchange Records** — Track credential issuance lifecycle for individual holders +- **Credential Offers** — Generate QR codes and deep links for holder wallets +- **Credential Refresh** — Issue updated credentials to existing holders + +**Supported credential formats:** `jwt_vc_json`, `vc+sd-jwt`, `mso_mdoc` + +[View OID4VCI API Reference →](admin-api-oid4vci.md) + +--- + +### [OID4VP — Presentation & Verification Management](admin-api-oid4vp.md) + +Endpoints for requesting and verifying credential presentations using OpenID for Verifiable Presentations (OID4VP): + +- **Presentation Definitions (PEX v2)** — Define credential requirements and constraints +- **DCQL Queries** — Alternative query language optimized for mDOC and SD-JWT +- **VP Requests** — Generate authorization requests that holders scan as QR codes +- **Presentations** — Poll for presentation results and verified claims +- **X.509 Identity** — Configure DNS-based client authentication for wallet compatibility + +**Verification protocols:** PEX v2, DCQL (Digital Credentials Query Language) + +[View OID4VP API Reference →](admin-api-oid4vp.md) + +--- + +### [mso_mdoc — ISO 18013-5 mDOC Management](admin-api-mso-mdoc.md) + +Endpoints specific to mobile documents (mDOC) per ISO 18013-5, including mobile driver's licenses (mDL): + +- **Key & Certificate Management** — Generate signing keys and certificates for mDOC issuance +- **Trust Anchors** — Manage root CA certificates for verifying holder mDOCs +- **Manual Signing & Verification** — Sign and verify CBOR-encoded mDOC credentials + +**Note:** Requires the `mso_mdoc` plugin to be loaded. See [Credential Formats](credential-formats.md#mso_mdoc) for setup instructions. + +[View mso_mdoc API Reference →](admin-api-mso-mdoc.md) + +--- + +## Common Patterns + +### Authentication + +All mutating endpoints (`POST`, `PUT`, `PATCH`, `DELETE`) require authentication in multi-tenant deployments. Use Bearer tokens or API keys as configured in ACA-Py. + +### Error Responses + +All endpoints follow a consistent error response format: + +| HTTP Status | Meaning | +|---|---| +| `400 Bad Request` | Invalid or missing request parameters, validation error, storage error | +| `401 Unauthorized` | Missing or invalid authentication | +| `404 Not Found` | Requested record does not exist | +| `500 Internal Server Error` | Signing failure, key/certificate error, unexpected server error | + +Error response body: + +```json +{ "reason": "human-readable error message" } +``` + +### Typical Issuance Flow + +1. **Configure** — Create supported credential configurations ([OID4VCI](admin-api-oid4vci.md#supported-credentials)) +2. **Exchange** — Create exchange record with holder's claims ([OID4VCI](admin-api-oid4vci.md#exchange-records)) +3. **Offer** — Generate credential offer QR code ([OID4VCI](admin-api-oid4vci.md#credential-offers)) +4. **Issue** — Holder wallet completes the flow automatically + +### Typical Verification Flow + +1. **Define Requirements** — Create presentation definition or DCQL query ([OID4VP](admin-api-oid4vp.md#presentation-definitions-pex-v2)) +2. **Request** — Generate VP request QR code ([OID4VP](admin-api-oid4vp.md#vp-requests)) +3. **Poll** — Check presentation status until verified ([OID4VP](admin-api-oid4vp.md#presentations)) +4. **Extract** — Retrieve verified claims from the result + +--- + +## Additional Resources + +- [Getting Started](getting-started.md) — Installation and initial configuration +- [Architecture](architecture.md) — Plugin design and credential format registry +- [Cookbook — Issuance](cookbook-issuance.md) — Step-by-step issuance examples with curl +- [Cookbook — Verification](cookbook-verification.md) — Complete verification scenarios +- [Credential Formats](credential-formats.md) — Format-specific implementation details +- [Troubleshooting](troubleshooting.md) — Common errors and debugging tips + +### DID Management + +#### `POST /did/jwk/create` + +Create a `did:jwk` DID backed by the specified key type. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `key_type` | string | **Yes** | Key algorithm. One of: `ed25519`, `p256` | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/did/jwk/create \ + -H "Content-Type: application/json" \ + -d '{"key_type": "p256"}' +``` + +**Response `200`:** + +| Field | Type | Description | +|---|---|---| +| `did` | string | The created `did:jwk:...` DID | + +**Example response:** + +```json +{ + "did": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6Ii4uLiIsInkiOiIuLi4ifQ" +} +``` + +--- + +### Supported Credentials + +Supported credential records define which credential types the issuer can issue — the format, display metadata, and credential schema. They appear in the `credential_configurations_supported` field of the credential issuer metadata (`/.well-known/openid-credential-issuer`). + +#### `POST /oid4vci/credential-supported/create` + +Register a supported credential using a generic schema (format-agnostic). + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `format` | string | **Yes** | Credential format (e.g. `jwt_vc_json`, `vc+sd-jwt`, `mso_mdoc`) | +| `id` | string | **Yes** | Identifier for this credential configuration (e.g. `UniversityDegreeCredential`) | +| `cryptographic_binding_methods_supported` | array of strings | No | Supported binding methods (e.g. `["did:jwk", "jwk"]`) | +| `cryptographic_suites_supported` | array of strings | No | Supported cryptographic suites (e.g. `["ES256"]`) | +| `proof_types_supported` | object | No | Supported proof types (e.g. `{"jwt": {"proof_signing_alg_values_supported": ["ES256"]}}`) | +| `display` | array of objects | No | Display metadata (name, logo, locale) per language | +| `format_data` | object | No | Format-specific metadata (merged into issuer metadata output) | +| `vc_additional_data` | object | No | Additional VC data such as `@context` and `type` arrays | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/credential-supported/create \ + -H "Content-Type: application/json" \ + -d '{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "cryptographic_binding_methods_supported": ["did:jwk", "jwk"], + "display": [{"name": "Employee Credential", "locale": "en-US"}] + }' +``` + +**Response `200`:** `SupportedCredentialSchema` — the created record. + +--- + +#### `POST /oid4vci/credential-supported/create/jwt` + +Register a JWT VC credential configuration (`jwt_vc_json` format). Provides a typed schema for the W3C Verifiable Credential structure. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `format` | string | **Yes** | Must be `jwt_vc_json` | +| `id` | string | **Yes** | Credential configuration identifier | +| `type` | array of strings | **Yes** | W3C VC `type` array (e.g. `["VerifiableCredential", "UniversityDegreeCredential"]`) | +| `@context` | array | **Yes** | JSON-LD contexts (e.g. `["https://www.w3.org/2018/credentials/v1"]`) | +| `cryptographic_binding_methods_supported` | array of strings | No | Supported binding methods | +| `cryptographic_suites_supported` | array of strings | No | Supported suites | +| `proof_types_supported` | object | No | Supported proof types | +| `display` | array of objects | No | Display metadata | +| `credentialSubject` | object | No | Display metadata per claim (shown in wallets) | +| `order` | array of strings | No | Display ordering of claims | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/credential-supported/create/jwt \ + -H "Content-Type: application/json" \ + -d '{ + "format": "jwt_vc_json", + "id": "UniversityDegreeCredential", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + "cryptographic_binding_methods_supported": ["did:jwk"], + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256", "EdDSA"]} + }, + "display": [{"name": "University Degree", "locale": "en-US"}], + "credentialSubject": { + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Family Name", "locale": "en-US"}]}, + "degree": {"display": [{"name": "Degree", "locale": "en-US"}]} + } + }' +``` + +**Response `200`:** + +```json +{ + "supported_cred_id": "3f1a2b4c-...", + "format": "jwt_vc_json", + "identifier": "UniversityDegreeCredential", + ... +} +``` + +--- + +#### `POST /oid4vci/credential-supported/create/sd-jwt` + +Register an SD-JWT VC credential configuration. Requires the `sd_jwt_vc` plugin to be loaded. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `format` | string | **Yes** | `vc+sd-jwt` or `dc+sd-jwt` | +| `id` | string | **Yes** | Credential configuration identifier | +| `vct` | string | **Yes** | Verifiable Credential Type string (e.g. `EmployeeCredential`) | +| `cryptographic_binding_methods_supported` | array of strings | No | | +| `cryptographic_suites_supported` | array of strings | No | | +| `display` | array of objects | No | | +| `claims` | object | No | Per-claim display metadata (keyed by claim name) | +| `order` | array of strings | No | Display ordering of claims | +| `sd_list` | array of strings | No | JSON Pointer paths to claims that should be selectively disclosable (e.g. `["/given_name", "/address/street_address"]`). Claims not in this list are always disclosed. | + +**Protected claims** (cannot be in `sd_list`): `/iss`, `/exp`, `/vct`, `/nbf`, `/cnf`, `/status` + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/credential-supported/create/sd-jwt \ + -H "Content-Type: application/json" \ + -d '{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "vct": "EmployeeCredential", + "cryptographic_binding_methods_supported": ["did:jwk", "jwk"], + "display": [{"name": "Employee Credential", "locale": "en-US"}], + "claims": { + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Family Name", "locale": "en-US"}]}, + "department": {"display": [{"name": "Department", "locale": "en-US"}]} + }, + "sd_list": ["/given_name", "/family_name", "/department"] + }' +``` + +**Response `200`:** `SupportedCredentialSchema` + +--- + +#### `GET /oid4vci/credential-supported/records` + +List all supported credential configurations. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `supported_cred_id` | string | Filter by record ID | +| `format` | string | Filter by format (e.g. `jwt_vc_json`) | + +**Example request:** + +```bash +curl http://localhost:8021/oid4vci/credential-supported/records +``` + +**Response `200`:** + +```json +{ + "results": [ + { + "supported_cred_id": "3f1a2b4c-...", + "format": "jwt_vc_json", + "identifier": "UniversityDegreeCredential", + ... + } + ] +} +``` + +--- + +#### `GET /oid4vci/credential-supported/records/{supported_cred_id}` + +Fetch a single supported credential configuration by ID. + +**Path parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `supported_cred_id` | string | **Yes** | Supported credential record ID | + +**Example request:** + +```bash +curl http://localhost:8021/oid4vci/credential-supported/records/3f1a2b4c-... +``` + +**Response `200`:** `SupportedCredentialSchema` + +--- + +#### `PUT /oid4vci/credential-supported/records/jwt/{supported_cred_id}` + +Replace a JWT VC supported credential record (complete replacement). + +**Path parameters:** `supported_cred_id` + +**Request body:** Same as `POST /oid4vci/credential-supported/create/jwt` + +**Response `200`:** + +```json +{ + "supported_cred": { ... }, + "supported_cred_id": "3f1a2b4c-..." +} +``` + +--- + +#### `PUT /oid4vci/credential-supported/records/sd-jwt/{supported_cred_id}` + +Replace an SD-JWT supported credential record. Requires `sd_jwt_vc` plugin. + +**Path parameters:** `supported_cred_id` + +**Request body:** Same as `POST /oid4vci/credential-supported/create/sd-jwt` + +**Response `200`:** + +```json +{ + "supported_cred": { ... }, + "supported_cred_id": "3f1a2b4c-..." +} +``` + +--- + +#### `DELETE /oid4vci/credential-supported/records/jwt/{supported_cred_id}` + +Remove a supported credential record. + +**Example request:** + +```bash +curl -X DELETE http://localhost:8021/oid4vci/credential-supported/records/jwt/3f1a2b4c-... +``` + +**Response `200`:** `{}` + +--- + +### Exchange Records + +An exchange record represents a single credential issuance lifecycle. + +#### `POST /oid4vci/exchange/create` + +Create a new exchange record. This is the primary step that binds a specific holder to a supported credential type before generating an offer. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `supported_cred_id` | string | **Yes** | ID of the supported credential to issue | +| `credential_subject` | object | **Yes** | The claims/values to include in the issued credential | +| `did` | string | No | DID of the issuer. If omitted, ACA-Py's default DID is used | +| `verification_method` | string (URI) | No | Specific verification method URI to use for signing | +| `pin` | string | No | User PIN to be delivered out of band to the holder. Required at token request time if set. | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vci/exchange/create \ + -H "Content-Type: application/json" \ + -d '{ + "supported_cred_id": "3f1a2b4c-...", + "credential_subject": { + "given_name": "Alice", + "family_name": "Smith", + "degree": "Bachelor of Science" + }, + "did": "did:jwk:eyJjcnYiOiJQLTI1NiIs..." + }' +``` + +**Response `200`:** `OID4VCIExchangeRecordSchema` + +| Field | Description | +|---|---| +| `exchange_id` | Unique record ID | +| `state` | `created` | +| `supported_cred_id` | Linked supported credential | +| `credential_subject` | Provided claim values | +| `issuer_id` | DID used for signing | +| `verification_method` | Verification method URI | +| `pin` | User PIN (if set) | + +--- + +#### `GET /oid4vci/exchange/records` + +List exchange records with optional filtering. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `exchange_id` | string (UUID) | Filter by exchange ID | +| `supported_cred_id` | string | Filter by supported credential ID | +| `state` | string | Filter by state. One of: `created`, `offer`, `issued`, `failed`, `accepted`, `deleted`, `superceded` | + +**Example request:** + +```bash +curl "http://localhost:8021/oid4vci/exchange/records?state=issued" +``` + +**Response `200`:** + +```json +{ + "results": [ + { + "exchange_id": "abc123-...", + "state": "issued", + ... + } + ] +} +``` + +--- + +#### `GET /oid4vci/exchange/records/{exchange_id}` + +Fetch a single exchange record by ID. + +**Example request:** + +```bash +curl http://localhost:8021/oid4vci/exchange/records/abc123-... +``` + +**Response `200`:** `OID4VCIExchangeRecordSchema` + +--- + +#### `DELETE /oid4vci/exchange/records/{exchange_id}` + +Delete an exchange record. + +**Example request:** + +```bash +curl -X DELETE http://localhost:8021/oid4vci/exchange/records/abc123-... +``` + +**Response `200`:** `{}` + +--- + +### Credential Offers + +#### `GET /oid4vci/credential-offer` + +Generate a credential offer by value. The entire offer JSON is embedded in the `openid-credential-offer://` URI. Moves the exchange to state `offer`. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `exchange_id` | string | Exchange record ID | +| `user_pin_required` | boolean | Whether the holder must supply the PIN at token time | + +**Example request:** + +```bash +curl "http://localhost:8021/oid4vci/credential-offer?exchange_id=abc123-..." +``` + +**Response `200`:** + +| Field | Description | +|---|---| +| `credential_offer` | Full `openid-credential-offer://?credential_offer=...` URI (can be shown as QR code) | +| `offer.credential_issuer` | Base URL of the issuer | +| `offer.credential_configuration_ids` | Array of credential type identifiers | +| `offer.grants.pre_authorized_code` | The pre-authorized code | +| `offer.grants.user_pin_required` | Whether a PIN is required | + +**Example response:** + +```json +{ + "credential_offer": "openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22...", + "offer": { + "credential_issuer": "https://issuer.example.com", + "credential_configuration_ids": ["UniversityDegreeCredential"], + "grants": { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": "SplxlOBeZQQYbYS6WxSbIA", + "user_pin_required": false + } + } + } +} +``` + +--- + +#### `GET /oid4vci/credential-offer-by-ref` + +Generate a credential offer by reference. Returns a `credential_offer_uri` pointing to an endpoint where the wallet can retrieve the offer. This is useful when the offer JSON is too large to embed in a QR code. + +**Query parameters:** Same as `/oid4vci/credential-offer` + +**Response `200`:** + +| Field | Description | +|---|---| +| `credential_offer_uri` | `openid-credential-offer://?credential_offer_uri=...` URI | +| `offer` | The offer object (same structure as above) | + +--- + +### Credential Refresh + +#### `PATCH /oid4vci/credential-refresh/{refresh_id}` + +Issue a refreshed credential for an existing exchange. Creates a new exchange record that supersedes the original (original state → `superceded`). Returns the new credential offer. + +**Path parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `refresh_id` | string | **Yes** | Refresh identifier from the original exchange record | + +**Example request:** + +```bash +curl -X PATCH http://localhost:8021/oid4vci/credential-refresh/refresh-abc123 +``` + +**Response `200`:** `OID4VCIExchangeRecordSchema` (new exchange) + +--- + +## Tag: `oid4vp` — Presentation / Verification Management + +### Presentation Definitions (PEX v2) + +Presentation definitions specify which credentials a verifier wants to receive and what constraints they must satisfy. + +#### `POST /oid4vp/presentation-definition` + +Create and store a PEX v2 presentation definition. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `pres_def` | object | **Yes** | A valid PEX v2 presentation definition JSON object | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/oid4vp/presentation-definition \ + -H "Content-Type: application/json" \ + -d '{ + "pres_def": { + "id": "employee-credential-request", + "input_descriptors": [ + { + "id": "employee-credential", + "format": { + "jwt_vc_json": {"alg": ["ES256", "EdDSA"]} + }, + "constraints": { + "fields": [ + { + "path": ["$.vc.type"], + "filter": { + "type": "array", + "contains": {"const": "UniversityDegreeCredential"} + } + } + ] + } + } + ] + } + }' +``` + +**Response `200`:** + +```json +{ + "pres_def": { ... }, + "pres_def_id": "550e8400-..." +} +``` + +--- + +#### `GET /oid4vp/presentation-definitions` + +List all stored presentation definitions. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `pres_def_id` | string | Filter by ID | + +**Response `200`:** + +```json +{ + "results": [ + { + "pres_def_id": "550e8400-...", + "pres_def": { ... } + } + ] +} +``` + +--- + +#### `GET /oid4vp/presentation-definition/{pres_def_id}` + +Fetch a stored presentation definition by ID. + +--- + +#### `PUT /oid4vp/presentation-definition/{pres_def_id}` + +Replace a presentation definition. + +**Request body:** `{ "pres_def": { ... } }` + +**Response `200`:** `{ "pres_def": {...}, "pres_def_id": "..." }` + +--- + +#### `DELETE /oid4vp/presentation-definition/{pres_def_id}` + +Delete a presentation definition. + +**Response `200`:** `{}` + +--- + +### DCQL Queries + +DCQL (Digital Credentials Query Language) is an alternative to PEX for specifying credential requests. Use DCQL for mDOC/SD-JWT queries where PEX constraints may be insufficient. + +#### `POST /oid4vp/dcql/queries` + +Create and store a DCQL query. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `credentials` | array | **Yes** | Array of `CredentialQuery` objects describing the required credentials | +| `credential_sets` | array | No | Optional grouping of credential queries with logical AND/OR semantics | + +**`CredentialQuery` object:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `id` | string | **Yes** | Identifier for this credential query (used as key in `vp_token` response) | +| `format` | string | **Yes** | Expected credential format (e.g. `mso_mdoc`, `vc+sd-jwt`, `jwt_vc_json`) | +| `meta` | object | No | Format-specific metadata (see below) | +| `claims` | array | No | Array of `ClaimsQuery` objects specifying required claims | +| `claim_sets` | array of arrays | No | Alternative claim combinations | + +**`meta` object** (format-specific): + +| Field | Format | Description | +|---|---|---| +| `doctype_value` | `mso_mdoc` | Required mDoc docType string (e.g. `org.iso.18013.5.1.mDL`) | +| `doctype_values` | `mso_mdoc` | List of acceptable docType strings | +| `vct_values` | `vc+sd-jwt` | List of acceptable `vct` values | + +**`ClaimsQuery` object:** + +| Field | Description | +|---|---| +| `id` | Claim query identifier | +| `namespace` | mDOC namespace (for `mso_mdoc`) | +| `claim_name` | Claim name (for `mso_mdoc`) | +| `path` | JSON Path array (for JWT/SD-JWT, e.g. `["$.given_name"]`) | +| `values` | Acceptable values (optional constraint) | + +**Example request (mDOC driver's license):** + +```bash +curl -X POST http://localhost:8021/oid4vp/dcql/queries \ + -H "Content-Type: application/json" \ + -d '{ + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + }, + "claims": [ + { + "namespace": "org.iso.18013.5.1", + "claim_name": "given_name" + }, + { + "namespace": "org.iso.18013.5.1", + "claim_name": "family_name" + }, + { + "namespace": "org.iso.18013.5.1", + "claim_name": "birth_date" + } + ] + } + ] + }' +``` + +**Example request (SD-JWT employee credential):** + +```bash +curl -X POST http://localhost:8021/oid4vp/dcql/queries \ + -H "Content-Type: application/json" \ + -d '{ + "credentials": [ + { + "id": "employee", + "format": "vc+sd-jwt", + "meta": { + "vct_values": ["EmployeeCredential"] + }, + "claims": [ + {"path": ["$.given_name"]}, + {"path": ["$.department"]} + ] + } + ] + }' +``` + +**Response `200`:** + +```json +{ + "dcql_query": { + "credentials": [...], + "credential_sets": null + } +} +``` + +> Note: the query `dcql_query_id` is returned in the `Location` header and can be retrieved via `GET /oid4vp/dcql/queries`. + +--- + +#### `GET /oid4vp/dcql/queries` + +List all stored DCQL queries. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `dcql_query_id` | string | Filter by ID | + +**Response `200`:** + +```json +{ + "results": [ + { + "dcql_query_id": "...", + "credentials": [...], + "credential_sets": null + } + ] +} +``` + +--- + +#### `GET /oid4vp/dcql/query/{dcql_query_id}` + +Fetch a DCQL query by ID. + +**Response `200`:** + +```json +{ + "dcql_query_id": "...", + "credentials": [...], + "credential_sets": null +} +``` + +--- + +#### `DELETE /oid4vp/dcql/query/{dcql_query_id}` + +Delete a DCQL query. + +--- + +### VP Requests + +A VP request initiates a presentation exchange. It generates the `openid://` URI that the holder scans. + +#### `POST /oid4vp/request` + +Create an OID4VP authorization request. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `vp_formats` | object | **Yes** | Format constraints for the expected VP. Keys are format strings; values are alg constraints. | +| `pres_def_id` | string | No | ID of a stored presentation definition (mutually exclusive with `dcql_query_id`) | +| `dcql_query_id` | string | No | ID of a stored DCQL query (mutually exclusive with `pres_def_id`) | + +Either `pres_def_id` or `dcql_query_id` must be provided. + +**Example request (PEX):** + +```bash +curl -X POST http://localhost:8021/oid4vp/request \ + -H "Content-Type: application/json" \ + -d '{ + "pres_def_id": "550e8400-...", + "vp_formats": { + "jwt_vc_json": {"alg": ["ES256", "EdDSA"]}, + "jwt_vp_json": {"alg": ["ES256", "EdDSA"]} + } + }' +``` + +**Example request (DCQL):** + +```bash +curl -X POST http://localhost:8021/oid4vp/request \ + -H "Content-Type: application/json" \ + -d '{ + "dcql_query_id": "dcql-abc123-...", + "vp_formats": { + "mso_mdoc": {"alg": ["ES256"]} + } + }' +``` + +**Response `200`:** + +| Field | Description | +|---|---| +| `request_uri` | The `openid://?client_id=...&request_uri=...` URI (present to holder as QR code) | +| `request.request_id` | ID of the VP request record | +| `presentation.presentation_id` | ID of the presentation record to poll for results | +| `presentation.state` | Initial state: `request-created` | + +**Example response:** + +```json +{ + "request_uri": "openid://?client_id=did:jwk:...&request_uri=https://verifier.example.com/oid4vp/request/abc123", + "request": { + "request_id": "abc123-...", + "pres_def_id": "550e8400-...", + "vp_formats": { ... } + }, + "presentation": { + "presentation_id": "def456-...", + "state": "request-created", + "request_id": "abc123-..." + } +} +``` + +--- + +#### `GET /oid4vp/requests` + +List VP request records. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `request_id` | string (UUID) | Filter by request ID | +| `pres_def_id` | string | Filter by presentation definition ID | +| `dcql_query_id` | string | Filter by DCQL query ID | + +--- + +#### `GET /oid4vp/request/{request_id}` + +Fetch a VP request record by ID. Note: the request record is **deleted after the holder retrieves the JAR** (signed request object), so this endpoint returns `404` after the holder scans the QR code. + +--- + +### Presentations + +Presentation records track the lifecycle of a VP exchange from request creation to verification result. + +#### `GET /oid4vp/presentations` + +List presentation records. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `presentation_id` | string (UUID) | Filter by ID | +| `pres_def_id` | string | Filter by presentation definition | +| `state` | string | Filter by state. One of: `request-created`, `request-retrieved`, `presentation-received`, `presentation-invalid`, `presentation-valid` | + +--- + +#### `GET /oid4vp/presentation/{presentation_id}` + +Fetch a presentation record by ID. Poll this endpoint after creating a VP request to check whether the holder has responded and whether verification succeeded. + +**Response `200`:** + +| Field | Type | Description | +|---|---|---| +| `presentation_id` | string | Record ID | +| `state` | string | Current state | +| `errors` | array of strings | Validation errors (non-empty when state is `presentation-invalid`) | +| `verified_claims` | object | Verified claim values (non-empty when state is `presentation-valid`) | +| `matched_credentials` | object | Full matched credential records | + +**Presentation states:** + +| State | Meaning | +|---|---| +| `request-created` | VP request generated; waiting for holder to scan | +| `request-retrieved` | Holder retrieved the signed request JAR | +| `presentation-received` | Holder submitted a VP (processing underway) | +| `presentation-valid` | VP signature and constraints verified successfully | +| `presentation-invalid` | VP failed verification (see `errors`) | + +**Example poll loop:** + +```bash +PRES_ID="def456-..." +while true; do + STATE=$(curl -s http://localhost:8031/oid4vp/presentation/$PRES_ID | python3 -c "import json,sys; print(json.load(sys.stdin)['state'])") + echo "State: $STATE" + if [[ "$STATE" == "presentation-valid" || "$STATE" == "presentation-invalid" ]]; then + break + fi + sleep 2 +done +``` + +--- + +#### `DELETE /oid4vp/presentation/{presentation_id}` + +Delete a presentation record. + +--- + +### X.509 Identity + +When a verifier needs to use `x509_san_dns` client authentication (required by some wallet protocols), register an X.509 certificate chain here. Once registered, all VP requests use the DNS name as the `client_id` and include the `x5c` header in the signed JAR. + +#### `POST /oid4vp/x509-identity` + +Register an X.509 identity. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `cert_chain_pem` | string | **Yes** | PEM-encoded certificate chain, leaf certificate first | +| `verification_method` | string | **Yes** | Verification method ID used for signing the JAR (e.g. `did:jwk:...#0`) | +| `client_id` | string | **Yes** | DNS name that will be used as the OID4VP `client_id` (e.g. `verifier.example.com`) | + +**Example request:** + +```bash +curl -X POST http://localhost:8031/oid4vp/x509-identity \ + -H "Content-Type: application/json" \ + -d '{ + "cert_chain_pem": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n", + "verification_method": "did:jwk:eyJ...#0", + "client_id": "verifier.example.com" + }' +``` + +**Response `200`:** `{}` + +--- + +#### `GET /oid4vp/x509-identity` + +Retrieve the registered X.509 identity. + +**Response `200`:** The stored identity record. + +--- + +#### `DELETE /oid4vp/x509-identity` + +Remove the X.509 identity. Subsequent VP requests will revert to using `did:jwk` as the `client_id`. + +--- + +## Tag: `mso_mdoc` — ISO 18013-5 mDOC Management + +Requires the `mso_mdoc` plugin to be loaded. See [Credential Formats — mso_mdoc](credential-formats.md#mso_mdoc) for background. + +### Key and Certificate Management + +On startup the `mso_mdoc` plugin auto-generates a default EC P-256 signing key and a self-signed certificate. You can inspect and extend these via the following endpoints. + +#### `GET /mso_mdoc/keys` + +List all mDOC signing keys. + +**Example request:** + +```bash +curl http://localhost:8021/mso_mdoc/keys +``` + +--- + +#### `GET /mso_mdoc/certificates` + +List all mDOC signing certificates. + +--- + +#### `GET /mso_mdoc/certificates/default` + +Get the default (active) signing certificate. + +--- + +#### `POST /mso_mdoc/generate-keys` + +Generate a new mDOC signing key and a self-signed certificate. + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/generate-keys +``` + +**Response `200`:** The newly created key and certificate records. + +--- + +### Trust Anchors + +Trust anchors are root CA certificates used to verify mDOC credentials received from holders. A trust anchor chain must be established for `POST /oid4vp/response` to verify mDOC presentations. + +#### `POST /mso_mdoc/trust-anchors` + +Add a trust anchor certificate. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `certificate_pem` | string | **Yes** | PEM-encoded X.509 root CA certificate | +| `anchor_id` | string | No | Custom ID. If not provided, a UUID is generated. | +| `metadata` | object | No | Arbitrary metadata attached to the trust anchor record | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/trust-anchors \ + -H "Content-Type: application/json" \ + -d '{ + "certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n", + "anchor_id": "iso-test-iaca-2024", + "metadata": {"description": "ISO test IACA root"} + }' +``` + +**Response `200`:** Trust anchor record. + +--- + +#### `GET /mso_mdoc/trust-anchors` + +List all stored trust anchors. + +--- + +#### `GET /mso_mdoc/trust-anchors/{anchor_id}` + +Fetch a trust anchor by ID. + +--- + +#### `DELETE /mso_mdoc/trust-anchors/{anchor_id}` + +Remove a trust anchor. + +--- + +### mDOC Signing and Verification + +These endpoints are available for manual signing/verification operations, independent of the OID4VCI issuance flow. + +#### `POST /mso_mdoc/sign` + +Manually sign a payload as an mDOC CBOR binary per ISO 18013-5. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `payload` | object | **Yes** | Claims organized by namespace (e.g. `{"org.iso.18013.5.1": {"given_name": "Alice"}}`) | +| `headers` | object | No | Additional COSE header parameters | +| `did` | string | No | DID to use for signing | +| `verificationMethod` | string | No | Specific verification method to use | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/sign \ + -H "Content-Type: application/json" \ + -d '{ + "payload": { + "org.iso.18013.5.1": { + "given_name": "Alice", + "family_name": "Smith", + "birth_date": "1990-01-01" + } + } + }' +``` + +**Response `200`:** CBOR hex-encoded mDOC binary. + +--- + +#### `POST /mso_mdoc/verify` + +Verify a CBOR-encoded mDOC binary. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `mso_mdoc` | string | **Yes** | CBOR hex-encoded mDOC device response | + +**Example request:** + +```bash +curl -X POST http://localhost:8021/mso_mdoc/verify \ + -H "Content-Type: application/json" \ + -d '{"mso_mdoc": "a36776657273...cbor-hex..."}' +``` + +**Response `200`:** + +| Field | Type | Description | +|---|---|---| +| `valid` | boolean | Whether verification succeeded | +| `error` | string | Error message if `valid` is `false` | +| `kid` | string | Key ID of the signing key | +| `headers` | object | COSE headers from the signed document | +| `payload` | object | Decoded claims organized by namespace | + +**Example response:** + +```json +{ + "valid": true, + "error": null, + "kid": "did:jwk:eyJ...#0", + "headers": {"alg": "ES256"}, + "payload": { + "org.iso.18013.5.1": { + "given_name": "Alice", + "family_name": "Smith" + } + } +} +``` diff --git a/oid4vc/docs/architecture.md b/oid4vc/docs/architecture.md new file mode 100644 index 000000000..b576d96ca --- /dev/null +++ b/oid4vc/docs/architecture.md @@ -0,0 +1,170 @@ +# Architecture Overview + +## Two-Server Design + +The plugin runs two separate HTTP servers alongside ACA-Py: + +``` +┌──────────────────────────────────────────────────┐ +│ ACA-Py Process │ +│ │ +│ ┌─────────────────────┐ ┌────────────────────┐ │ +│ │ ACA-Py Admin API │ │ DIDComm Messaging │ │ +│ │ (built-in server) │ │ Server │ │ +│ │ :admin_port │ │ :inbound_port │ │ +│ │ │ │ │ │ +│ │ /oid4vci/* │ └────────────────────┘ │ +│ │ /oid4vp/* │ │ +│ │ /mso_mdoc/* │ ┌────────────────────┐ │ +│ │ /did/* │ │ OID4VCI Public │ │ +│ │ │ │ Server (plugin) │ │ +│ │ Swagger at │ │ OID4VCI_PORT │ │ +│ │ /api/doc │ │ │ │ +│ └─────────────────────┘ │ /.well-known/* │ │ +│ │ /token │ │ +│ ← Controller (admin) │ /credential │ │ +│ │ /nonce │ │ +│ │ /oid4vp/request/* │ │ +│ │ /oid4vp/response/*│ │ +│ │ /status/* │ │ +│ │ │ │ +│ │ Swagger at │ │ +│ │ /api/doc │ │ +│ └────────────────────┘ │ +│ ↑ Wallet / Holder │ +└──────────────────────────────────────────────────┘ +``` + +| Server | Audience | Purpose | +|---|---|---| +| **ACA-Py Admin API** | Controller (back-end application) | Manage credentials, exchanges, presentation definitions, DCQL queries, keys | +| **OID4VCI Public Server** | Wallets / Holders / Verifiers | OID4VCI protocol endpoints — token, credential issuance, metadata, OID4VP response | + +The two servers share ACA-Py's wallet/storage layer via a shared `Profile` context. + +--- + +## Plugin Lifecycle + +The plugin entry point is `oid4vc/__init__.py`, which registers three hooks with ACA-Py: + +### `setup(context)` + +Called once at startup before the ACA-Py event loop begins. Registers: + +- **`JwkResolver`** — DID resolver that can resolve `did:jwk` DIDs +- **`DID_JWK`** method — enables `did:jwk` creation via `/wallet/did/create` +- **`P256`** key type — EC P-256 key support +- **`jwt_vc_json` credential processor** — the default issuance/verification format +- **`CredProcessors`** instance — the format registry (see below) +- **`StatusHandler`** — optional status list handler (if `OID4VCI_STATUS_HANDLER` configured) + +### `startup(profile, event)` + +Called when the first wallet profile is opened. Starts: + +- **`Oid4vciServer`** — the separate aiohttp public server bound to `OID4VCI_HOST:OID4VCI_PORT` +- **`AppResources`** — shared HTTP client for auth server communication (when `OID4VCI_AUTH_SERVER_URL` is configured) + +### `shutdown(profile, event)` + +Called on graceful shutdown. Stops the public server and shuts down the HTTP client. + +--- + +## Credential Format Registry (`CredProcessors`) + +The `CredProcessors` class (in `oid4vc/cred_processor.py`) is a runtime registry that maps credential format strings to handler implementations. Each handler implements one or more of three protocols: + +| Protocol | Method | Responsibility | +|---|---|---| +| `Issuer` | `issue(profile, exchange)` | Sign and return a credential | +| `CredVerifier` | `verify_credential(profile, cred)` | Verify a single credential | +| `PresVerifier` | `verify_presentation(profile, pres)` | Verify a presentation | + +### Registered Formats + +| Format String | Module | Issuer | CredVerifier | PresVerifier | +|---|---|---|---|---| +| `jwt_vc_json`, `jwt_vc` | `jwt_vc_json` | ✓ | ✓ | — | +| `jwt_vp_json`, `jwt_vp` | `jwt_vc_json` | — | ✓ | ✓ | +| `vc+sd-jwt`, `dc+sd-jwt` | `sd_jwt_vc` | ✓ | ✓ | ✓ | +| `mso_mdoc` | `mso_mdoc` | ✓ | ✓ | ✓ | + +Format modules register themselves by calling `setup(context)` during their own plugin initialization. The `jwt_vc_json` module is registered unconditionally by `oid4vc/__init__.py`; `sd_jwt_vc` and `mso_mdoc` only register when their respective plugins are loaded (`--plugin sd_jwt_vc`, `--plugin mso_mdoc`). + +--- + +## Admin API — Swagger Integration + +ACA-Py discovers route modules from plugins using a naming convention: any module named `routes` (or a package with a `routes/__init__.py`) that exports `register()` and `post_process_routes()` is loaded automatically. + +The `oid4vc` plugin uses `aiohttp_apispec` to generate Swagger documentation from decorators on every handler: + +```python +@docs(tags=["oid4vci"], summary="Create a credential exchange record") +@request_schema(ExchangeRecordCreateRequestSchema()) +@response_schema(OID4VCIExchangeRecordSchema(), 200) +async def exchange_create(request: web.BaseRequest): + ... +``` + +The `post_process_routes()` function injects tag descriptions and links to the OID4VCI/OID4VP specifications into the swagger dictionary so they appear in the Swagger UI's tag section. + +--- + +## Verification Engines + +### PEX — Presentation Exchange v2 + +Used when a verifier creates a `presentation-definition` and includes it in a VP request. + +- **Class:** `PresentationExchangeEvaluator` in `oid4vc/pex.py` +- **Flow:** `compile(definition)` → `verify(profile, submission, presentation)` → `PexVerifyResult` +- **Standards:** [DIF Presentation Exchange v2](https://identity.foundation/presentation-exchange/spec/v2.0.0/) + +### DCQL — Digital Credentials Query Language + +Used when a verifier creates a `dcql_query` and includes it in a VP request instead of a presentation definition. + +- **Class:** `DCQLQueryEvaluator` in `oid4vc/dcql.py` +- **Flow:** `compile(query)` → `verify(profile, vp_token, presentation)` → `DCQLVerifyResult` +- **Standards:** [OID4VP §E.1 DCQL](https://openid.net/specs/openid-4-verifiable-presentations-1_0-ID2.html) +- **Key difference from PEX:** No `presentation_submission` in the response; `vp_token` is a JSON object keyed by `credential_query_id`. + +--- + +## Records and Storage + +All plugin records use ACA-Py's Askar wallet storage. + +| Record Type | Storage Key | Description | +|---|---|---| +| `OID4VCIExchangeRecord` | `"oid4vci"` | One per credential issuance attempt. Tracks state from `created` → `offer` → `issued` → `accepted`. | +| `SupportedCredential` | `"supported_cred"` | Configuration for a credential type that can be issued (format, display metadata, VC schema). | +| `OID4VPPresentation` | `"oid4vp"` | Tracks one VP request+response cycle. States: `request-created` → `request-retrieved` → `presentation-valid`/`presentation-invalid`. | +| `OID4VPRequest` | `"oid4vp"` | Temporary record holding VP request parameters (pres_def_id or dcql_query_id, vp_formats). Deleted after the holder retrieves the JAR. | +| `OID4VPPresDef` | `"oid4vp-pres-def"` | Stored PEX v2 presentation definitions. | +| `DCQLQuery` | `"oid4vp-dcql"` | Stored DCQL query definitions. | +| `Nonce` | `"nonce"` | Short-lived nonces used for proof-of-possession. Atomic mark-used prevents replay. | + +--- + +## Webhook Events + +The plugin emits webhook events on the following topics: + +| Topic | When Emitted | +|---|---| +| `oid4vci` | Exchange state changes: `created`, `offer`, `issued`, `accepted`, `failed`, `deleted` | +| `oid4vp` | Presentation state changes: `request-created`, `request-retrieved`, `presentation-valid`, `presentation-invalid` | + +Controllers subscribe to these via the standard ACA-Py webhook mechanism. + +--- + +## Multitenant Support + +In a multitenant ACA-Py deployment, the OID4VCI public server injects the per-wallet `Profile` into each request via a `setup_context` middleware. Wallet-specific routes are prefixed with `/tenant/{wallet_id}`, allowing multiple tenants to share a single public server. + +The admin routes use `tenant_authentication` (a decorator from ACA-Py's multitenant module) to authenticate and resolve the requesting tenant's profile. diff --git a/oid4vc/docs/cookbook-issuance.md b/oid4vc/docs/cookbook-issuance.md new file mode 100644 index 000000000..37c0af543 --- /dev/null +++ b/oid4vc/docs/cookbook-issuance.md @@ -0,0 +1,410 @@ +# Issuance Cookbook + +This guide walks through the complete credential issuance flow for each supported format using step-by-step `curl` commands. + +**Base URLs used in examples:** + +```bash +ADMIN="http://localhost:8021" # ACA-Py admin API +PUBLIC="http://localhost:8022" # OID4VCI public server +``` + +--- + +## Overview: The Pre-Authorized Code Flow + +```mermaid +sequenceDiagram + autonumber + actor Controller + participant Admin as Admin API (:8021) + participant Public as Public Server (:8022) + actor Wallet + + Controller ->> Admin: POST /did/jwk/create + Admin -->> Controller: did + + Controller ->> Admin: POST /oid4vci/credential-supported/create/jwt + Admin -->> Controller: supported_cred_id + + Controller ->> Admin: POST /oid4vci/exchange/create + Admin -->> Controller: exchange_id (state: created) + + Controller ->> Admin: GET /oid4vci/credential-offer?exchange_id=... + Admin -->> Controller: openid-credential-offer:// URI (state: offer) + + Controller -->> Wallet: Present QR code + + Wallet ->> Public: GET /.well-known/openid-credential-issuer + Public -->> Wallet: issuer metadata + + Wallet ->> Public: POST /token (pre-authorized_code) + Public -->> Wallet: access_token + c_nonce + + Wallet ->> Public: POST /credential (Bearer token + proof) + Public -->> Wallet: signed credential (state: issued) + + Wallet ->> Public: POST /notification (credential_accepted) + Public -->> Wallet: 204 (state: accepted) + + Public -->> Controller: Webhook POST /topic/oid4vci +``` + +--- + +## jwt_vc_json + +W3C Verifiable Credentials in JWT format. The issued credential is a JWT with the `vc` claim following the W3C VCDM v1 structure. + +### Step 1: Create a DID + +```bash +DID=$(curl -s -X POST $ADMIN/did/jwk/create \ + -H "Content-Type: application/json" \ + -d '{"key_type": "p256"}' | python3 -c "import json,sys; print(json.load(sys.stdin)['did'])") +echo "DID: $DID" +``` + +### Step 2: Create a Supported Credential Record + +```bash +SUPP_ID=$(curl -s -X POST $ADMIN/oid4vci/credential-supported/create/jwt \ + -H "Content-Type: application/json" \ + -d "{ + \"format\": \"jwt_vc_json\", + \"id\": \"UniversityDegreeCredential\", + \"type\": [\"VerifiableCredential\", \"UniversityDegreeCredential\"], + \"@context\": [\"https://www.w3.org/2018/credentials/v1\"], + \"cryptographic_binding_methods_supported\": [\"did:jwk\"], + \"proof_types_supported\": { + \"jwt\": {\"proof_signing_alg_values_supported\": [\"ES256\", \"EdDSA\"]} + }, + \"display\": [{\"name\": \"University Degree\", \"locale\": \"en-US\"}], + \"credentialSubject\": { + \"given_name\": {\"display\": [{\"name\": \"Given Name\", \"locale\": \"en-US\"}]}, + \"family_name\": {\"display\": [{\"name\": \"Family Name\", \"locale\": \"en-US\"}]}, + \"degree\": {\"display\": [{\"name\": \"Degree\", \"locale\": \"en-US\"}]} + } + }" | python3 -c "import json,sys; print(json.load(sys.stdin)['supported_cred_id'])") +echo "Supported credential ID: $SUPP_ID" +``` + +### Step 3: Create an Exchange Record + +```bash +EXCHANGE=$(curl -s -X POST $ADMIN/oid4vci/exchange/create \ + -H "Content-Type: application/json" \ + -d "{ + \"supported_cred_id\": \"$SUPP_ID\", + \"did\": \"$DID\", + \"credential_subject\": { + \"given_name\": \"Alice\", + \"family_name\": \"Smith\", + \"degree\": \"Bachelor of Science in Computer Science\" + } + }") +EXCHANGE_ID=$(echo $EXCHANGE | python3 -c "import json,sys; print(json.load(sys.stdin)['exchange_id'])") +echo "Exchange ID: $EXCHANGE_ID" +``` + +### Step 4: Generate a Credential Offer + +```bash +OFFER=$(curl -s "$ADMIN/oid4vci/credential-offer?exchange_id=$EXCHANGE_ID") +OFFER_URI=$(echo $OFFER | python3 -c "import json,sys; print(json.load(sys.stdin)['credential_offer'])") +echo "Offer URI (show as QR code): $OFFER_URI" +``` + +The `OFFER_URI` looks like: + +``` +openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22... +``` + +Display this as a QR code for the wallet to scan, or deep-link to the wallet app. + +### Step 5: Check Exchange State + +```bash +curl -s "$ADMIN/oid4vci/exchange/records/$EXCHANGE_ID" | python3 -c \ + "import json,sys; r=json.load(sys.stdin); print('State:', r['state'])" +``` + +Expected progression: `created` → `offer` → `issued` → `accepted` + +### Wallet-Side Steps (for reference) + +The wallet performs these steps automatically: + +```bash +# 1. Retrieve issuer metadata +curl -s "$PUBLIC/.well-known/openid-credential-issuer" + +# 2. Extract pre-authorized code from offer and request token +PRE_AUTH_CODE="SplxlOBeZQQYbYS6WxSbIA" +curl -X POST "$PUBLIC/token" \ + --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code" \ + --data-urlencode "pre-authorized_code=$PRE_AUTH_CODE" + +# 3. Request credential (wallet builds proof JWT internally) +curl -X POST "$PUBLIC/credential" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ "credential_identifier": "UniversityDegreeCredential", "proof": { "proof_type": "jwt", "jwt": "" } }' +``` + +--- + +## sd_jwt_vc + +SD-JWT Verifiable Credentials with selective disclosure. Requires `--plugin sd_jwt_vc` to be loaded. + +### Step 1: Create a DID + +Same as above. Use `p256` or `ed25519` key type. + +### Step 2: Create a Supported Credential Record + +```bash +SUPP_ID=$(curl -s -X POST $ADMIN/oid4vci/credential-supported/create/sd-jwt \ + -H "Content-Type: application/json" \ + -d '{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "vct": "EmployeeCredential", + "cryptographic_binding_methods_supported": ["did:jwk", "jwk"], + "display": [{"name": "Employee Credential", "locale": "en-US"}], + "claims": { + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Family Name", "locale": "en-US"}]}, + "department": {"display": [{"name": "Department", "locale": "en-US"}]}, + "employee_id": {"display": [{"name": "Employee ID", "locale": "en-US"}]} + }, + "sd_list": ["/given_name", "/family_name", "/department"] + }' | python3 -c "import json,sys; print(json.load(sys.stdin)['supported_cred_id'])") +echo "Supported credential ID: $SUPP_ID" +``` + +> **About `sd_list`:** Claims listed in `sd_list` (as JSON Pointer paths) will be selectively disclosable. The wallet holder can choose to reveal only a subset. Claims *not* in `sd_list` are always included in the credential. +> +> Protected claims that can never be in `sd_list`: `/iss`, `/exp`, `/vct`, `/nbf`, `/cnf`, `/status`. + +### Step 3: Create an Exchange Record + +```bash +EXCHANGE=$(curl -s -X POST $ADMIN/oid4vci/exchange/create \ + -H "Content-Type: application/json" \ + -d "{ + \"supported_cred_id\": \"$SUPP_ID\", + \"did\": \"$DID\", + \"credential_subject\": { + \"given_name\": \"Alice\", + \"family_name\": \"Smith\", + \"department\": \"Engineering\", + \"employee_id\": \"EMP-12345\" + } + }") +EXCHANGE_ID=$(echo $EXCHANGE | python3 -c "import json,sys; print(json.load(sys.stdin)['exchange_id'])") +``` + +### Step 4: Generate a Credential Offer + +```bash +OFFER_URI=$(curl -s "$ADMIN/oid4vci/credential-offer?exchange_id=$EXCHANGE_ID" | \ + python3 -c "import json,sys; print(json.load(sys.stdin)['credential_offer'])") +echo "Offer URI: $OFFER_URI" +``` + +### Issued Credential Structure + +The issued SD-JWT has the form: + +``` +
..~~~...~ +``` + +The payload contains: +- `vct`: `"EmployeeCredential"` (always disclosed) +- `iss`: issuer DID (always disclosed) +- `employee_id`: `"EMP-12345"` (always disclosed — not in `sd_list`) +- Selective disclosures for `given_name`, `family_name`, `department` as `_sd` hashes + +--- + +## mso_mdoc + +ISO 18013-5 mobile Documents (mDOC) in CBOR/COSE format. Requires `--plugin mso_mdoc` to be loaded and the `isomdl-uniffi` native library to be installed. + +### Initial Setup: Verify Keys Exist + +On first startup, the `mso_mdoc` plugin auto-generates a default signing key and self-signed certificate. Confirm they exist: + +```bash +curl -s $ADMIN/mso_mdoc/certificates/default | python3 -m json.tool +``` + +To generate additional keys (e.g. for key rotation): + +```bash +curl -X POST $ADMIN/mso_mdoc/generate-keys | python3 -m json.tool +``` + +### Step 1: Create a Supported Credential Record + +For mDOC, use the generic `POST /oid4vci/credential-supported/create` endpoint: + +```bash +SUPP_ID=$(curl -s -X POST $ADMIN/oid4vci/credential-supported/create \ + -H "Content-Type: application/json" \ + -d '{ + "format": "mso_mdoc", + "id": "org.iso.18013.5.1.mDL", + "cryptographic_binding_methods_supported": ["cose_key"], + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "display": [{"name": "Mobile Driving Licence", "locale": "en-US"}], + "format_data": { + "doctype": "org.iso.18013.5.1.mDL" + } + }' | python3 -c "import json,sys; print(json.load(sys.stdin)['supported_cred_id'])") +echo "Supported credential ID: $SUPP_ID" +``` + +### Step 2: Create an Exchange Record + +For mDOC, `credential_subject` is organized by **namespace**: + +```bash +EXCHANGE=$(curl -s -X POST $ADMIN/oid4vci/exchange/create \ + -H "Content-Type: application/json" \ + -d "{ + \"supported_cred_id\": \"$SUPP_ID\", + \"credential_subject\": { + \"org.iso.18013.5.1\": { + \"given_name\": \"Alice\", + \"family_name\": \"Smith\", + \"birth_date\": \"1990-01-15\", + \"document_number\": \"DL-1234567890\", + \"expiry_date\": \"2030-01-01\", + \"issuing_country\": \"US\", + \"driving_privileges\": [ + {\"vehicle_category_code\": \"B\", \"issue_date\": \"2020-01-01\"} + ] + } + } + }") +EXCHANGE_ID=$(echo $EXCHANGE | python3 -c "import json,sys; print(json.load(sys.stdin)['exchange_id'])") +``` + +### Step 3: Generate a Credential Offer + +```bash +OFFER_URI=$(curl -s "$ADMIN/oid4vci/credential-offer?exchange_id=$EXCHANGE_ID" | \ + python3 -c "import json,sys; print(json.load(sys.stdin)['credential_offer'])") +echo "Offer URI: $OFFER_URI" +``` + +### Issued Credential Structure + +The issued mDOC is a CBOR-encoded device response following ISO 18013-5. The credential is base64url-encoded in the response. + +--- + +## PIN-Protected Issuance + +To require the holder to enter a PIN (transaction code) at token time, set the `pin` field when creating the exchange: + +```bash +EXCHANGE=$(curl -s -X POST $ADMIN/oid4vci/exchange/create \ + -H "Content-Type: application/json" \ + -d "{ + \"supported_cred_id\": \"$SUPP_ID\", + \"credential_subject\": { ... }, + \"pin\": \"493536\" + }") +``` + +Then generate the offer with `user_pin_required=true`: + +```bash +OFFER_URI=$(curl -s "$ADMIN/oid4vci/credential-offer?exchange_id=$EXCHANGE_ID&user_pin_required=true" | \ + python3 -c "import json,sys; print(json.load(sys.stdin)['credential_offer'])") +``` + +Deliver the PIN to the holder via a separate channel (email, SMS, etc.). The wallet must include `tx_code=493536` in the token request. + +--- + +## Credential Refresh + +When a credential expires or needs to be refreshed, use the `refresh_id` from the original exchange record: + +```bash +# Get the refresh_id from the original exchange +REFRESH_ID=$(curl -s $ADMIN/oid4vci/exchange/records/$EXCHANGE_ID | \ + python3 -c "import json,sys; print(json.load(sys.stdin).get('refresh_id', 'N/A'))") + +# Create a new exchange superseding the original +NEW_EXCHANGE=$(curl -s -X PATCH "$ADMIN/oid4vci/credential-refresh/$REFRESH_ID") +NEW_EXCHANGE_ID=$(echo $NEW_EXCHANGE | python3 -c "import json,sys; print(json.load(sys.stdin)['exchange_id'])") + +# Generate new offer for the refreshed credential +OFFER_URI=$(curl -s "$ADMIN/oid4vci/credential-offer?exchange_id=$NEW_EXCHANGE_ID" | \ + python3 -c "import json,sys; print(json.load(sys.stdin)['credential_offer'])") +``` + +The original exchange state becomes `superceded`. + +--- + +## Status List Integration + +When the Status List Plugin is configured (`OID4VCI_STATUS_HANDLER=status_list.v1_0.status_handler`), bind a status list definition to a supported credential ID. All credentials issued with that `supported_cred_id` will include a `credentialStatus` entry: + +```bash +# Bind the status list to a supported credential (via Status List Plugin API) +curl -X POST $ADMIN/status-list/defs \ + -H "Content-Type: application/json" \ + -d "{ + \"issuer_did\": \"$DID\", + \"list_type\": \"ietf\", + \"list_size\": 131072, + \"shard_size\": 1024, + \"status_purpose\": \"revocation\", + \"supported_cred_id\": \"$SUPP_ID\", + \"verification_method\": \"${DID}#0\" + }" +``` + +--- + +## Webhook Events + +Subscribe to the `oid4vci` topic to monitor exchange state changes: + +```bash +# ACA-Py webhook payload example: +{ + "topic": "oid4vci", + "wallet_id": "...", + "payload": { + "exchange_id": "abc123-...", + "state": "issued", + "supported_cred_id": "...", + ... + } +} +``` + +Exchange states in order: + +| State | Trigger | +|---|---| +| `created` | `POST /oid4vci/exchange/create` | +| `offer` | `GET /oid4vci/credential-offer` (offer generated) | +| `issued` | Wallet successfully called `POST /credential` | +| `accepted` | Wallet called `POST /notification` with `credential_accepted` | +| `failed` | Error during issuance | +| `deleted` | Exchange record deleted via API | +| `superceded` | Exchange replaced by a refresh | diff --git a/oid4vc/docs/cookbook-verification.md b/oid4vc/docs/cookbook-verification.md new file mode 100644 index 000000000..f86f21c29 --- /dev/null +++ b/oid4vc/docs/cookbook-verification.md @@ -0,0 +1,445 @@ +# Verification Cookbook + +This guide walks through the complete credential presentation (VP) verification flow using both PEX (Presentation Exchange) and DCQL (Digital Credentials Query Language) query methods. + +**Base URLs used in examples:** + +```bash +ADMIN="http://localhost:8031" # ACA-Py verifier admin API +PUBLIC="http://localhost:8032" # OID4VP public server +``` + +--- + +## Overview: The OID4VP direct_post Flow + +```mermaid +sequenceDiagram + autonumber + actor Controller + participant Admin as Admin API (:8031) + participant Public as Public Server (:8032) + actor Wallet + + Controller ->> Admin: POST /oid4vp/presentation-definition (or DCQL query) + Admin -->> Controller: pres_def_id (or dcql_query_id) + + Controller ->> Admin: POST /oid4vp/request + Admin -->> Controller: presentation_id + openid:// URI (state: request-created) + + Controller -->> Wallet: Present QR code / deep link + + Wallet ->> Public: GET /oid4vp/request/{request_id} + Note right of Public: Returns signed JWT (JAR) containing
pres_def or dcql_query + Public -->> Wallet: Signed request JWT (state: request-retrieved) + + Wallet ->> Wallet: Select matching credentials, build VP + + Wallet ->> Public: POST /oid4vp/response/{presentation_id} + Public ->> Public: Verify VP + evaluate constraints + Public -->> Wallet: {} (state: presentation-valid or presentation-invalid) + + Public -->> Controller: Webhook POST /topic/oid4vp + + Controller ->> Admin: GET /oid4vp/presentation/{presentation_id} + Admin -->> Controller: { state, verified_claims, errors } +``` + +--- + +## PEX — Presentation Definition + +Use PEX (Presentation Exchange v2) for flexible credential queries with constraint filters for any credential format. + +### Step 1: Create a Presentation Definition + +#### Example: Request a JWT VC University Degree + +```bash +PRES_DEF_ID=$(curl -s -X POST $ADMIN/oid4vp/presentation-definition \ + -H "Content-Type: application/json" \ + -d '{ + "pres_def": { + "id": "university-degree-request", + "input_descriptors": [ + { + "id": "degree", + "format": { + "jwt_vc_json": {"alg": ["ES256", "EdDSA"]} + }, + "constraints": { + "fields": [ + { + "path": ["$.vc.type"], + "filter": { + "type": "array", + "contains": {"const": "UniversityDegreeCredential"} + } + }, + { + "path": ["$.vc.credentialSubject.degree"], + "filter": {"type": "string"} + } + ] + } + } + ] + } + }' | python3 -c "import json,sys; print(json.load(sys.stdin)['pres_def_id'])") +echo "Presentation definition ID: $PRES_DEF_ID" +``` + +#### Example: Request an SD-JWT Employee Credential + +```bash +PRES_DEF_ID=$(curl -s -X POST $ADMIN/oid4vp/presentation-definition \ + -H "Content-Type: application/json" \ + -d '{ + "pres_def": { + "id": "employee-credential-request", + "input_descriptors": [ + { + "id": "employee", + "format": { + "vc+sd-jwt": {"alg": ["ES256"]} + }, + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.vct"], + "filter": {"type": "string", "const": "EmployeeCredential"} + }, + { + "path": ["$.given_name"], + "filter": {"type": "string"} + }, + { + "path": ["$.department"], + "filter": {"type": "string"} + } + ] + } + } + ] + } + }' | python3 -c "import json,sys; print(json.load(sys.stdin)['pres_def_id'])") +``` + +### Step 2: Create a VP Request + +```bash +RESPONSE=$(curl -s -X POST $ADMIN/oid4vp/request \ + -H "Content-Type: application/json" \ + -d "{ + \"pres_def_id\": \"$PRES_DEF_ID\", + \"vp_formats\": { + \"jwt_vc_json\": {\"alg\": [\"ES256\", \"EdDSA\"]}, + \"jwt_vp_json\": {\"alg\": [\"ES256\", \"EdDSA\"]} + } + }") +REQUEST_URI=$(echo $RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin)['request_uri'])") +PRES_ID=$(echo $RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin)['presentation']['presentation_id'])") +echo "Request URI (show as QR code): $REQUEST_URI" +echo "Presentation ID (poll for result): $PRES_ID" +``` + +The `REQUEST_URI` looks like: + +``` +openid://?client_id=did:jwk:...&request_uri=https://verifier.example.com/oid4vp/request/abc123 +``` + +### Step 3: Poll for Result + +```bash +while true; do + RESULT=$(curl -s "$ADMIN/oid4vp/presentation/$PRES_ID") + STATE=$(echo $RESULT | python3 -c "import json,sys; print(json.load(sys.stdin)['state'])") + echo "State: $STATE" + if [[ "$STATE" == "presentation-valid" ]]; then + echo "Verified claims:" + echo $RESULT | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('verified_claims', {}), indent=2))" + break + elif [[ "$STATE" == "presentation-invalid" ]]; then + echo "Errors:" + echo $RESULT | python3 -c "import json,sys; print(json.load(sys.stdin).get('errors', []))" + break + fi + sleep 2 +done +``` + +**Example successful response:** + +```json +{ + "presentation_id": "def456-...", + "state": "presentation-valid", + "errors": [], + "verified_claims": { + "degree": { + "given_name": "Alice", + "degree": "Bachelor of Science in Computer Science" + } + } +} +``` + +--- + +## DCQL Queries + +DCQL (Digital Credentials Query Language) is better suited for mDOC and SD-JWT queries where you want to match on `doctype` or `vct` values and specify individual claim paths directly. + +### DCQL vs PEX + +| Feature | PEX | DCQL | +|---|---|---| +| Constraint language | JSONPath with filter schemas | Claim-level path matching | +| Presentation submission | Required (descriptor map) | Not required | +| `vp_token` format | JWT VP (for JWT formats) | JSON object keyed by query `id` | +| mDOC support | Limited | Native (namespace + claim_name) | +| SD-JWT support | Via `limit_disclosure: required` | Via `path` + `vct_values` | + +### DCQL — mDOC Driver's License + +#### Step 1: Create a DCQL Query + +```bash +DCQL_ID=$(curl -s -X POST $ADMIN/oid4vp/dcql/queries \ + -H "Content-Type: application/json" \ + -d '{ + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + }, + "claims": [ + { + "namespace": "org.iso.18013.5.1", + "claim_name": "given_name" + }, + { + "namespace": "org.iso.18013.5.1", + "claim_name": "family_name" + }, + { + "namespace": "org.iso.18013.5.1", + "claim_name": "birth_date" + }, + { + "namespace": "org.iso.18013.5.1", + "claim_name": "document_number" + } + ] + } + ] + }' | python3 -c "import json,sys; r=json.load(sys.stdin); print(list(r.get('results',[r]))[0].get('dcql_query_id','') if 'results' in r else '')" 2>/dev/null) + +# Alternatively, list to find the ID: +DCQL_ID=$(curl -s $ADMIN/oid4vp/dcql/queries | python3 -c \ + "import json,sys; results=json.load(sys.stdin)['results']; print(results[-1]['dcql_query_id'])") +echo "DCQL query ID: $DCQL_ID" +``` + +#### Step 2: Create a VP Request + +```bash +RESPONSE=$(curl -s -X POST $ADMIN/oid4vp/request \ + -H "Content-Type: application/json" \ + -d "{ + \"dcql_query_id\": \"$DCQL_ID\", + \"vp_formats\": { + \"mso_mdoc\": {\"alg\": [\"ES256\"]} + } + }") +REQUEST_URI=$(echo $RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin)['request_uri'])") +PRES_ID=$(echo $RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin)['presentation']['presentation_id'])") +echo "Request URI: $REQUEST_URI" +echo "Presentation ID: $PRES_ID" +``` + +The signed JAR returned by the wallet fetch endpoint (`GET /oid4vp/request/{request_id}`) contains: + +```json +{ + "dcql_query": { + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": {"doctype_value": "org.iso.18013.5.1.mDL"}, + "claims": [...] + } + ] + }, + "response_uri": "https://verifier.example.com/oid4vp/response/def456", + "nonce": "...", + "client_id": "did:jwk:..." +} +``` + +The wallet's DCQL `vp_token` response is a **JSON object** keyed by credential query `id`: + +```json +{ + "mdl": "" +} +``` + +#### Step 3: Poll for Result + +Same as PEX: + +```bash +curl -s "$ADMIN/oid4vp/presentation/$PRES_ID" | python3 -c \ + "import json,sys; r=json.load(sys.stdin); print('State:', r['state'])" +``` + +--- + +### DCQL — SD-JWT Employee Credential + +```bash +DCQL_ID=$(curl -s -X POST $ADMIN/oid4vp/dcql/queries \ + -H "Content-Type: application/json" \ + -d '{ + "credentials": [ + { + "id": "employee", + "format": "vc+sd-jwt", + "meta": { + "vct_values": ["EmployeeCredential"] + }, + "claims": [ + {"path": ["$.given_name"]}, + {"path": ["$.department"]} + ] + } + ] + }' | python3 -c "import json,sys; r=json.load(sys.stdin); print(list(r.get('results',[r]))[0].get('dcql_query_id','') if 'results' in r else '')" 2>/dev/null) +echo "DCQL query ID: $DCQL_ID" +``` + +For DCQL with SD-JWT credentials, the wallet returns `vp_token` as a JSON object keyed by credential query `id`: + +```json +{ + "employee": "" +} +``` + +--- + +### DCQL — Multi-Credential Request with `credential_sets` + +Request alternative credential combinations using `credential_sets`. This allows "give me a driving licence OR an employee badge": + +```bash +curl -X POST $ADMIN/oid4vp/dcql/queries \ + -H "Content-Type: application/json" \ + -d '{ + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": {"doctype_value": "org.iso.18013.5.1.mDL"}, + "claims": [ + {"namespace": "org.iso.18013.5.1", "claim_name": "given_name"}, + {"namespace": "org.iso.18013.5.1", "claim_name": "birth_date"} + ] + }, + { + "id": "employee", + "format": "vc+sd-jwt", + "meta": {"vct_values": ["EmployeeCredential"]}, + "claims": [{"path": ["$.given_name"]}] + } + ], + "credential_sets": [ + { + "options": [["mdl"], ["employee"]], + "required": true, + "purpose": "Identity verification" + } + ] + }' +``` + +--- + +## X.509 Identity for Verifiers + +Some wallets (particularly mobile document wallets such as ISO 18013-5 mDOC wallets) require the verifier to authenticate via X.509 (`x509_san_dns`) rather than a DID. Register your certificate chain once and all subsequent VP requests will use DNS-based client authentication. + +### Register X.509 Identity + +```bash +# Generate or load your certificate chain (leaf cert first) +CERT_PEM="-----BEGIN CERTIFICATE----- +MIIBxTCCAW... +-----END CERTIFICATE-----" + +curl -X POST $ADMIN/oid4vp/x509-identity \ + -H "Content-Type: application/json" \ + -d "{ + \"cert_chain_pem\": \"$CERT_PEM\", + \"verification_method\": \"did:jwk:eyJ...#0\", + \"client_id\": \"verifier.example.com\" + }" +``` + +After registration: + +- `client_id` in all VP requests becomes `x509_san_dns:verifier.example.com` +- The JAR is signed with an `x5c` header containing the certificate chain +- Wallets validate the certificate chain against their trust store + +### Verify Registration + +```bash +curl -s $ADMIN/oid4vp/x509-identity | python3 -m json.tool +``` + +### Remove Registration + +```bash +curl -X DELETE $ADMIN/oid4vp/x509-identity +``` + +After deletion, VP requests revert to `did:jwk` client authentication. + +--- + +## Presentation States + +| State | Description | +|---|---| +| `request-created` | VP request created. QR code ready to scan. | +| `request-retrieved` | Wallet fetched the signed JAR. VP request record deleted. | +| `presentation-received` | Wallet submitted a VP (internal processing state) | +| `presentation-valid` | VP verified successfully. `verified_claims` populated. | +| `presentation-invalid` | VP failed verification. `errors` array populated. | + +--- + +## Webhook Events + +Subscribe to the `oid4vp` topic: + +```json +{ + "topic": "oid4vp", + "wallet_id": "...", + "payload": { + "presentation_id": "def456-...", + "state": "presentation-valid", + "pres_def_id": "550e8400-...", + "verified_claims": { ... }, + "errors": [] + } +} +``` diff --git a/oid4vc/docs/credential-formats.md b/oid4vc/docs/credential-formats.md new file mode 100644 index 000000000..17eb3ac8e --- /dev/null +++ b/oid4vc/docs/credential-formats.md @@ -0,0 +1,338 @@ +# Credential Formats + +The `oid4vc` plugin supports three credential formats, each implemented as a separate module. This page describes the format-specific behaviour, data structures, and configuration for each. + +--- + +## Format Comparison + +| Feature | `jwt_vc_json` | `sd_jwt_vc` | `mso_mdoc` | +|---|---|---|---| +| Standard | W3C VCDM 1.0 as JWT | SD-JWT VC spec | ISO 18013-5 | +| Encoding | JWT (base64url) | SD-JWT with disclosures | CBOR + COSE | +| Selective disclosure | No | Yes | Yes (per namespace) | +| Plugin flag | (built-in) | `--plugin sd_jwt_vc` | `--plugin mso_mdoc` | +| Extra dependency | None | `jsonpointer` | `cbor2`, `pycose`, `isomdl-uniffi` | +| Format string | `jwt_vc_json` | `vc+sd-jwt` or `dc+sd-jwt` | `mso_mdoc` | +| Admin route | `/create/jwt` | `/create/sd-jwt` | `/create` (generic) | +| Key types | `ed25519`, `p256` | `ed25519`, `p256` | `p256` (COSE EC2) | + +--- + +## jwt_vc_json + +### Overview + +Issues W3C Verifiable Credentials as JWTs following [W3C VCDM 1.0](https://www.w3.org/TR/vc-data-model/). The JWT payload uses the `vc` claim to carry the credential body. + +### Module + +`jwt_vc_json/` — automatically registered by `oid4vc/__init__.py`. No separate `--plugin` flag required. + +``` +Issuer: jwt_vc_json, jwt_vc +CredVerifier: jwt_vc_json, jwt_vc +PresVerifier: jwt_vp_json, jwt_vp +``` + +### Issued Credential Structure + +```json +{ + "alg": "ES256", + "kid": "did:jwk:eyJ...#0", + "typ": "JWT" +} +. +{ + "iss": "did:jwk:eyJ...", + "sub": "did:jwk:", + "nbf": 1700000000, + "jti": "urn:uuid:abc123", + "vc": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "credentialSubject": { + "id": "did:jwk:", + "given_name": "Alice", + "family_name": "Smith", + "degree": "Bachelor of Science" + } + } +} +``` + +Key JWT claims: + +| Claim | Source | +|---|---| +| `iss` | Issuer DID (`exchange.issuer_id`) | +| `sub` | Holder DID (extracted from proof of possession) | +| `nbf` | Issuance time (Unix timestamp) | +| `jti` | Credential ID (`urn:uuid:`) | +| `vc.credentialSubject` | `exchange.credential_subject` | + +### Supported Credential `create/jwt` Schema + +The `@context` and `type` arrays defined in the supported credential record are used to populate `vc.@context` and `vc.type` in every issued credential. + +```json +{ + "format": "jwt_vc_json", + "id": "UniversityDegreeCredential", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + "credentialSubject": { + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "degree": {"display": [{"name": "Degree", "locale": "en-US"}]} + } +} +``` + +### Status List Integration + +When `OID4VCI_STATUS_HANDLER` is configured, the plugin calls `StatusHandler.get_credential_status(profile, exchange)` before issuing. The returned `credentialStatus` object is merged into `vc.credentialStatus`. + +--- + +## sd_jwt_vc + +### Overview + +Issues SD-JWT Verifiable Credentials following the [SD-JWT VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-08.html). Selective disclosure allows holders to reveal only a chosen subset of claims when presenting. + +### Module + +`sd_jwt_vc/` — requires `--plugin sd_jwt_vc` in ACA-Py configuration. + +``` +Issuer: vc+sd-jwt, dc+sd-jwt +CredVerifier: vc+sd-jwt, dc+sd-jwt +PresVerifier: vc+sd-jwt, dc+sd-jwt +``` + +### SD-JWT Structure + +``` +
..~~~...~ +``` + +**Header:** + +```json +{ + "alg": "ES256", + "kid": "did:jwk:eyJ...#0", + "typ": "vc+sd-jwt" +} +``` + +**Payload (simplified):** + +```json +{ + "iss": "did:jwk:eyJ...", + "iat": 1700000000, + "vct": "EmployeeCredential", + "cnf": { + "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } + }, + "_sd": ["hash1", "hash2", "hash3"], + "_sd_alg": "sha-256", + "employee_id": "EMP-12345" +} +``` + +- Claims in `sd_list` appear as `_sd` hashes in the payload +- Claims *not* in `sd_list` appear in plain text in the payload +- `cnf.jwk` holds the holder's public key (from proof of possession) +- `vct` is the type identifier and is always disclosed + +### Protected Claims + +The following claims can never be in `sd_list` and are always disclosed: + +``` +/iss /exp /vct /nbf /cnf /status +``` + +Attempting to include these in `sd_list` will result in a validation error during issuance. + +### Selective Disclosure via `sd_list` + +`sd_list` contains [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) paths relative to the credential subject: + +```json +"sd_list": [ + "/given_name", + "/family_name", + "/address/street_address", + "/driving_privileges/0/vehicle_category_code" +] +``` + +Nested paths and array element paths are supported. + +### Supported Credential `create/sd-jwt` Schema + +```json +{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "vct": "EmployeeCredential", + "display": [{"name": "Employee Credential", "locale": "en-US"}], + "claims": { + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Family Name", "locale": "en-US"}]}, + "department": {"display": [{"name": "Department", "locale": "en-US"}]} + }, + "sd_list": ["/given_name", "/family_name", "/department"] +} +``` + +### X.509 Certificate Chain in Holder Binding + +When the holder's proof of possession includes an `x5c` header (rather than `kid` or `jwk`), the issuer embeds the holder's certificate chain in the `cnf` claim: + +```json +"cnf": { + "x5c": ["", ""] +} +``` + +--- + +## mso_mdoc + +### Overview + +Issues mobile Documents (mDOC) following [ISO 18013-5](https://www.iso.org/standard/69084.html) in CBOR encoding with COSE signing. This format is used for digital driving licences, government IDs, and travel documents. + +### Module + +`mso_mdoc/` — requires `--plugin mso_mdoc` and the `isomdl-uniffi` native library. + +``` +Issuer: mso_mdoc +CredVerifier: mso_mdoc +PresVerifier: mso_mdoc +``` + +See [mso_mdoc/README.md](../mso_mdoc/README.md) for detailed installation instructions. + +### mDOC Structure + +An mDOC is CBOR-encoded data organised into: + +- **DocType:** Identifies the document type (e.g. `org.iso.18013.5.1.mDL`) +- **Namespaces:** Groups of related claims. Standard namespaces: + - `org.iso.18013.5.1` — core driving licence fields + - `org.iso.18013.5.1.aamva` — North American extension fields +- **MSO (Mobile Security Object):** COSE-signed data structure containing digests of each claim, issuer signature, validity period +- **IssuerSigned:** Per-claim signed data structures + +### Credential Subject Structure + +When creating an exchange for mDOC, `credential_subject` must be organized by namespace: + +```json +{ + "org.iso.18013.5.1": { + "given_name": "Alice", + "family_name": "Smith", + "birth_date": "1990-01-15", + "document_number": "DL-1234567890", + "expiry_date": "2030-01-01", + "issue_date": "2020-01-01", + "issuing_country": "US", + "issuing_authority": "Dept. of Motor Vehicles", + "portrait": "", + "driving_privileges": [ + { + "vehicle_category_code": "B", + "issue_date": "2020-01-01", + "expiry_date": "2030-01-01" + } + ] + }, + "org.iso.18013.5.1.aamva": { + "DHS_compliance": "F", + "EDL_credential": 1 + } +} +``` + +### Key and Certificate Management + +The `mso_mdoc` plugin manages its own signing keys and certificates, separate from ACA-Py's main wallet keys. + +**Startup:** Automatically generates a default EC P-256 key and self-signed certificate if none exist. + +**Key lifecycle:** + +```bash +# Inspect the default certificate (includes public key, validity dates) +curl -s $ADMIN/mso_mdoc/certificates/default | python3 -m json.tool + +# Generate a new key + self-signed cert (e.g. for rotation) +curl -X POST $ADMIN/mso_mdoc/generate-keys + +# List all keys +curl -s $ADMIN/mso_mdoc/keys | python3 -m json.tool +``` + +The signing certificate's public key forms the IACA leaf in the document signer certificate chain used in the MSO. Verifiers must have the corresponding root CA in their trust store. + +### Trust Anchors + +Trust anchors are root CA certificates used when **verifying** received mDOC presentations. Without trust anchors, mDOC verification will fail. + +For testing, you can add your own self-signed CA (generated by `POST /mso_mdoc/generate-keys`): + +```bash +# Export the default self-signed cert PEM (this acts as its own root for testing) +DEFAULT_CERT=$(curl -s $ADMIN/mso_mdoc/certificates/default | python3 -c \ + "import json,sys; print(json.load(sys.stdin)['certificate_pem'])") + +# Add it as a trust anchor +curl -X POST $ADMIN/mso_mdoc/trust-anchors \ + -H "Content-Type: application/json" \ + -d "{ + \"certificate_pem\": \"$DEFAULT_CERT\", + \"anchor_id\": \"local-self-signed\" + }" +``` + +For production, add the IACA (Issuing Authority Certificate Authority) root certificate from the official issuer. + +### Trust Store Configuration + +| `OID4VC_MDOC_TRUST_STORE_TYPE` | Behaviour | +|---|---| +| `file` (default) | Reads PEM files from `OID4VC_MDOC_TRUST_ANCHORS_PATH` directory on startup | +| `wallet` | Reads trust anchors stored via `POST /mso_mdoc/trust-anchors` API | +| `none` or `disabled` | Disables trust anchor verification (do not use in production) | + +### Manual Sign/Verify + +Test the mDOC signing flow without going through OID4VCI: + +```bash +# Sign a payload manually +SIGNED=$(curl -s -X POST $ADMIN/mso_mdoc/sign \ + -H "Content-Type: application/json" \ + -d '{ + "payload": { + "org.iso.18013.5.1": { + "given_name": "Alice", + "birth_date": "1990-01-15" + } + } + }') +echo $SIGNED + +# Verify the signed mDOC +curl -X POST $ADMIN/mso_mdoc/verify \ + -H "Content-Type: application/json" \ + -d "{\"mso_mdoc\": \"$SIGNED\"}" +``` diff --git a/oid4vc/docs/getting-started.md b/oid4vc/docs/getting-started.md new file mode 100644 index 000000000..a85d87cc7 --- /dev/null +++ b/oid4vc/docs/getting-started.md @@ -0,0 +1,179 @@ +# Getting Started + +## Prerequisites + +| Requirement | Version | +|---|---| +| Python | `^3.12` | +| ACA-Py | `~1.4.0` | +| `mso_mdoc` extra (optional) | `cbor2`, `cbor-diag`, `cwt`, `pycose` | +| `sd_jwt_vc` extra (optional) | `jsonpointer` | + +## Installation + +Add the plugin to your ACA-Py deployment using the `--plugin` flag and load the appropriate sub-plugin(s) for the credential format(s) you need. + +**Minimal issuance (JWT VC only):** + +``` +--plugin oid4vc +``` + +**With SD-JWT VC support:** + +``` +--plugin oid4vc +--plugin sd_jwt_vc +``` + +**With all formats (including mDOC):** + +``` +--plugin oid4vc +--plugin sd_jwt_vc +--plugin mso_mdoc +``` + +### Installing with pip + +```bash +# JWT VC only +pip install "oid4vc" + +# With SD-JWT support +pip install "oid4vc[sd_jwt_vc]" + +# With mDOC support +pip install "oid4vc[mso_mdoc]" + +# All formats +pip install "oid4vc[sd_jwt_vc,mso_mdoc]" +``` + +> **Note:** The `mso_mdoc` format additionally requires the `isomdl-uniffi` native library. See [mso_mdoc/README.md](../mso_mdoc/README.md) for separate installation instructions. + +--- + +## Configuration Reference + +Configuration is supplied via environment variables or equivalent plugin config keys in the ACA-Py configuration file. + +### Core Plugin Configuration + +| Environment Variable | Plugin Config Key | Required | Description | +|---|---|---|---| +| `OID4VCI_HOST` | `oid4vci.host` | **Yes** | Hostname the OID4VCI public server binds to (e.g. `0.0.0.0`) | +| `OID4VCI_PORT` | `oid4vci.port` | **Yes** | Port the OID4VCI public server listens on (e.g. `8022`) | +| `OID4VCI_ENDPOINT` | `oid4vci.endpoint` | **Yes** | Publicly-reachable base URL for the credential issuer (e.g. `https://issuer.example.com`). This value is advertised in `/.well-known/openid-credential-issuer` as `credential_issuer`. Supports `${VAR:-default}` variable expansion. | +| `OID4VP_ENDPOINT` | — | No | Override the public base URL for OID4VP endpoints. Falls back to `OID4VCI_ENDPOINT` if not set. | +| `OID4VCI_STATUS_HANDLER` | `oid4vci.status_handler` | No | Python module path of a status list handler (e.g. `status_list.v1_0.status_handler`). Enables `credentialStatus` in issued credentials. | +| `OID4VCI_AUTH_SERVER_URL` | `oid4vci.auth_server_url` | No | URL of an external OAuth 2.0 authorization server. When set, the plugin delegates token issuance to this server. | +| `OID4VCI_AUTH_SERVER_CLIENT` | `oid4vci.auth_server_client` | No | JSON string describing the auth server client credentials. Supports `client_secret_basic` and `private_key_jwt` auth methods. Example: `{"client_id": "acapy", "client_secret": "secret"}` | + +### mso_mdoc Configuration + +| Environment Variable | Default | Description | +|---|---|---| +| `OID4VC_MDOC_TRUST_STORE_TYPE` | `file` | Trust anchor storage mechanism. Valid values: `file`, `wallet`, `none` (or `disabled`) | +| `OID4VC_MDOC_TRUST_ANCHORS_PATH` | `/etc/acapy/mdoc/trust-anchors/` | Directory path for file-based trust anchor storage. X.509 PEM files are read from this directory on startup. | + +### Status List Integration + +When using the [Status List Plugin](https://github.com/openwallet-foundation/acapy-plugins/blob/main/status_list/README.md) together with `OID4VCI_STATUS_HANDLER`: + +| Environment Variable | Description | +|---|---| +| `STATUS_LIST_SIZE` | Number of entries in each status list (e.g. `131072`) | +| `STATUS_LIST_SHARD_SIZE` | Shard granularity (e.g. `1024`) | +| `STATUS_LIST_PUBLIC_URI` | URL template for status list resources (e.g. `https://issuer.example.com/tenant/{tenant_id}/status/{list_number}`) | +| `STATUS_LIST_FILE_PATH` | File system path template for bitstring storage (e.g. `/tmp/bitstring/{tenant_id}/{list_number}`) | + +### Endpoint Variable Expansion + +`OID4VCI_ENDPOINT` (and `OID4VP_ENDPOINT`) support shell-style default-value expansion: + +```bash +OID4VCI_ENDPOINT="https://${NGROK_HOST:-localhost:8022}" +``` + +If `NGROK_HOST` is unset, the endpoint resolves to `https://localhost:8022`. + +--- + +## Docker Quick-Start + +The plugin ships with ready-to-use Docker Compose configurations. + +### Issuer Stack + +```bash +cd oid4vc +cp docker/dev.yml docker-compose.override.yml # optional, for customisation + +# Export required env vars +export OID4VCI_ENDPOINT=http://localhost:8022 + +docker compose -f docker/dev.yml up +``` + +Key ports (from `docker/dev.yml`): + +| Service | Admin API | OID4VCI Public Server | +|---|---|---| +| ACA-Py Issuer | `http://localhost:8021` | `http://localhost:8022` | + +### Verifier Stack + +```bash +docker compose -f docker/dev-verifier.yml up +``` + +| Service | Admin API | OID4VP Public Server | +|---|---|---| +| ACA-Py Verifier | `http://localhost:8031` | `http://localhost:8032` | + +--- + +## Verifying the Setup + +Once running, confirm Swagger is accessible at both endpoints: + +```bash +# Admin Swagger UI — shows oid4vci, oid4vp, mso_mdoc, did tag groups +open http://localhost:8021/api/doc + +# Public server Swagger UI — shows wallet-facing endpoints +open http://localhost:8022/api/doc +``` + +Check the plugin is active: + +```bash +curl -s http://localhost:8021/plugins | python3 -m json.tool | grep oid4vc +``` + +Expected output: + +```json +"oid4vc", +``` + +--- + +## Multitenant Deployments + +In a multi-wallet (multitenant) ACA-Py deployment the OID4VCI public server prefixes wallet-specific routes with `/tenant/{wallet_id}`. The `OID4VCI_ENDPOINT` value must therefore include a `{tenant_id}` placeholder or be set per-tenant: + +```bash +OID4VCI_ENDPOINT="https://issuer.example.com/tenant/${WALLET_ID}" +``` + +The `credential_issuer` value in issuer metadata is computed per-wallet from this template at request time. + +--- + +## Next Steps + +- [Architecture Overview](architecture.md) — understand how the two servers and credential format registry work +- [Issuance Cookbook](cookbook-issuance.md) — issue your first credential with step-by-step curl commands +- [Admin API Reference](admin-api-reference.md) — full endpoint reference diff --git a/oid4vc/docs/index.md b/oid4vc/docs/index.md new file mode 100644 index 000000000..97570cc50 --- /dev/null +++ b/oid4vc/docs/index.md @@ -0,0 +1,53 @@ +# OID4VC Plugin — Developer Documentation + +This section covers everything needed to integrate the OID4VC plugin into an ACA-Py deployment, configure it, and call its APIs. + +## Quick Navigation + +| Document | Description | +|---|---| +| [Getting Started](getting-started.md) | Prerequisites, installation, configuration, Docker quick-start | +| [Architecture](architecture.md) | Two-server design, plugin lifecycle, credential format registry | +| [Admin API Reference](admin-api-reference.md) | All `/oid4vci/*`, `/oid4vp/*`, `/mso_mdoc/*`, `/did/*` endpoints | +| [Public API Reference](public-api-reference.md) | OID4VCI/OID4VP wallet-facing endpoints (token, credential, well-known, …) | +| [Issuance Cookbook](cookbook-issuance.md) | Step-by-step curl walkthroughs for `jwt_vc_json`, `sd_jwt_vc`, `mso_mdoc` | +| [Verification Cookbook](cookbook-verification.md) | PEX and DCQL-based VP flows with curl examples | +| [Credential Formats](credential-formats.md) | Format-specific schema details, selective disclosure, mDOC namespaces | +| [Troubleshooting](troubleshooting.md) | Error codes, common failures, debugging tips | + +## Finding Endpoints in the Swagger UI + +The plugin automatically registers all admin endpoints in the ACA-Py Swagger UI. No extra configuration is required. + +| Server | URL | Contents | +|---|---|---| +| ACA-Py Admin Server | `http://:/api/doc` | All `oid4vci`, `oid4vp`, `mso_mdoc`, and `did` tag groups | +| OID4VCI Public Server | `http://:/api/doc` | Public wallet-facing endpoints (token, credential, well-known, …) | + +The tags that appear in the admin Swagger UI are: + +- **`oid4vci`** — Credential issuance management (exchange records, supported credentials, credential offers) +- **`oid4vp`** — Presentation/verification management (presentation definitions, VP requests, DCQL queries, X.509 identity) +- **`mso_mdoc`** — ISO 18013-5 mDOC signing/verification and key management +- **`did`** — DID:JWK creation + +## Common Starting Points + +**I want to issue a JWT VC credential:** +→ [Issuance Cookbook — jwt_vc_json](cookbook-issuance.md#jwt_vc_json) + +**I want to issue an SD-JWT credential:** +→ [Issuance Cookbook — sd_jwt_vc](cookbook-issuance.md#sd_jwt_vc) + +**I want to issue an mDOC (ISO 18013-5) credential:** +→ [Issuance Cookbook — mso_mdoc](cookbook-issuance.md#mso_mdoc) + +**I want to verify a credential presentation (VP):** +→ [Verification Cookbook — PEX](cookbook-verification.md#pex-presentation-definition) +→ [Verification Cookbook — DCQL](cookbook-verification.md#dcql-queries) + +**I want to look up a specific endpoint:** +→ [Admin API Reference](admin-api-reference.md) + +**Something is failing and I don't know why:** +→ [Troubleshooting](troubleshooting.md) diff --git a/oid4vc/docs/production.md b/oid4vc/docs/production.md new file mode 100644 index 000000000..f7b472a2d --- /dev/null +++ b/oid4vc/docs/production.md @@ -0,0 +1,522 @@ +# Production Deployment Guide + +This guide covers best practices for deploying the OID4VC plugin in production environments, including security considerations, configuration, and operational concerns. + +## Prerequisites + +Before deploying to production, ensure you have: + +- ACA-Py 1.0.0+ installed and configured +- Required plugins loaded: `oid4vc`, and optionally `sd_jwt_vc`, `mso_mdoc` +- SSL/TLS certificates for HTTPS endpoints +- Proper authentication configured (API keys or JWT tokens for multi-tenant) +- Database backend configured (PostgreSQL recommended for production) + +--- + +## Security Checklist + +### 1. Transport Security + +**⚠️ Critical:** All OID4VC endpoints MUST be served over HTTPS in production. + +```yaml +# docker-compose.yml or deployment config +environment: + ACAPY_ENDPOINT: "https://issuer.example.com" # Public HTTPS endpoint + ACAPY_ADMIN_URL: "https://admin.example.com" # Admin HTTPS endpoint +``` + +**Why:** OID4VC exchanges include sensitive data (credential offers, proofs of possession, presentations). HTTP exposes this data to tampering and eavesdropping. + +### 2. Authentication & Authorization + +Enable authentication for all admin endpoints: + +```bash +--admin-api-key +# Or for multi-tenancy: +--jwt-secret +--multitenant-admin +``` + +**Generate secure keys:** + +```bash +# Generate 32-byte random key +python3 -c "import secrets; print(secrets.token_urlsafe(32))" +``` + +### 3. Key Management + +#### Issuer Signing Keys + +**Best practices:** +- Use hardware security modules (HSM) or key management services (KMS) for signing keys +- Rotate signing keys periodically (e.g., annually) +- Maintain key backup and recovery procedures +- Never hard-code private keys in configuration files + +**For production `did:jwk` issuance:** + +1. Generate signing keys with proper entropy: + ```bash + curl -X POST https://admin.example.com/did/jwk/create \ + -H "X-API-KEY: $ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{"key_type": "p256"}' + ``` + +2. Store the DID and use it consistently: + ```jsonc + { + "did": "did:jwk:eyJjcnYiOiJQLTI1NiIs...", // Save this + "verification_method": "did:jwk:...#0" + } + ``` + +3. Document key rotation procedures in your runbooks + +#### mDOC Signing Certificates + +For mDOC credentials, use proper certificate hierarchies: + +```bash +# Generate production signing key and certificate +curl -X POST https://admin.example.com/mso_mdoc/generate-keys \ + -H "X-API-KEY: $ADMIN_KEY" +``` + +**⚠️ Self-signed certificates are NOT suitable for production.** Use certificates issued by a trusted Certificate Authority (CA) recognized by holder wallets. + +### 4. Trust Anchor Management (mDOC) + +Establish proper trust anchors for verifying holder-presented mDOCs: + +```bash +# Add production root CA certificate +curl -X POST https://admin.example.com/mso_mdoc/trust-anchors \ + -H "X-API-KEY: $ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "anchor_id": "production-root-ca-2026", + "metadata": { + "issuer": "National ID Authority", + "valid_from": "2026-01-01", + "valid_until": "2046-01-01", + "purpose": "Verify government-issued mDL credentials" + } + }' +``` + +**Best practices:** +- Only add trust anchors from verified, authoritative sources +- Document the provenance of each trust anchor +- Implement trust anchor rotation procedures +- Monitor trust anchor expiration dates +- Test trust anchor chains before accepting production credentials + +### 5. Credential Status Management + +Integrate with the Status List plugin for revocation support: + +```bash +# Install status_list plugin +pip install git+https://github.com/openwallet-foundation/acapy-plugins.git#subdirectory=status_list + +# Load plugin in ACA-Py +--plugin status_list +``` + +**Configure status list issuance:** + +```jsonc +{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "vct": "EmployeeCredential", + // ... other fields ... + "status": { + "status_list": { + "idx": "auto", // Assign status list index automatically + "uri": "https://issuer.example.com/status-lists/1" + } + } +} +``` + +**⚠️ Important:** Publish status lists at stable, public HTTPS URLs. Wallets and verifiers must be able to fetch status lists to check revocation. + +--- + +## Environment Configuration + +### Production Environment Variables + +```bash +# Core ACA-Py settings +ACAPY_ENDPOINT=https://issuer.example.com +ACAPY_ADMIN_URL=https://admin-internal.example.com +ACAPY_ADMIN_API_KEY= + +# Database (PostgreSQL recommended) +ACAPY_WALLET_STORAGE_TYPE=postgres_storage +ACAPY_WALLET_STORAGE_CONFIG='{"url":"postgres://user:pass@db:5432/acapy"}' + +# Plugin configuration +ACAPY_PLUGIN=oid4vc +ACAPY_PLUGIN=sd_jwt_vc +ACAPY_PLUGIN=mso_mdoc +ACAPY_PLUGIN=status_list + +# Public server for OID4VCI/OID4VP +ACAPY_OID4VCI_PUBLIC_URL=https://issuer.example.com + +# Logging +ACAPY_LOG_LEVEL=WARNING # or INFO for detailed logs +ACAPY_LOG_FILE=/var/log/acapy/acapy.log + +# Multi-tenancy (if applicable) +ACAPY_MULTITENANT=true +ACAPY_MULTITENANT_ADMIN=true +ACAPY_JWT_SECRET= +``` + +### Docker Compose Example + +```yaml +version: '3.8' + +services: + acapy: + image: ghcr.io/openwallet-foundation/acapy:latest + environment: + ACAPY_ENDPOINT: "https://issuer.example.com" + ACAPY_ADMIN_URL: "http://0.0.0.0:8021" + ACAPY_ADMIN_API_KEY: "${ADMIN_API_KEY}" + ACAPY_WALLET_STORAGE_TYPE: "postgres_storage" + ACAPY_WALLET_STORAGE_CONFIG: '{"url":"postgres://acapy:${DB_PASSWORD}@postgres:5432/acapy"}' + ACAPY_PLUGIN: "oid4vc,sd_jwt_vc,mso_mdoc,status_list" + ACAPY_LOG_LEVEL: "INFO" + volumes: + - ./certs:/certs:ro + - ./logs:/var/log/acapy + ports: + - "8020:8020" # Public server + - "8021:8021" # Admin server (internal only) + depends_on: + - postgres + restart: unless-stopped + + postgres: + image: postgres:16 + environment: + POSTGRES_DB: acapy + POSTGRES_USER: acapy + POSTGRES_PASSWORD: "${DB_PASSWORD}" + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + +volumes: + pgdata: +``` + +### Reverse Proxy Configuration (Nginx) + +```nginx +# /etc/nginx/sites-available/issuer.example.com + +server { + listen 443 ssl http2; + server_name issuer.example.com; + + ssl_certificate /etc/nginx/certs/issuer.example.com.crt; + ssl_certificate_key /etc/nginx/certs/issuer.example.com.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + # Public OID4VCI/OID4VP endpoints + location / { + proxy_pass http://acapy:8020; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# Admin interface - internal network only +server { + listen 443 ssl http2; + server_name admin-internal.example.com; + + ssl_certificate /etc/nginx/certs/admin-internal.crt; + ssl_certificate_key /etc/nginx/certs/admin-internal.key; + + # IP whitelist + allow 10.0.0.0/8; # Internal network + deny all; + + location / { + proxy_pass http://acapy:8021; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +--- + +## Operational Considerations + +### 1. Monitoring & Logging + +**Key metrics to monitor:** +- Credential issuance rate (exchanges created, offers generated, credentials issued) +- Presentation verification rate (requests created, valid presentations, invalid presentations) +- Error rates by endpoint +- Response times (p50, p95, p99) +- Database connection pool usage +- Storage growth rate + +**Recommended logging configuration:** + +```bash +# Production: WARNING level for general operation, INFO for troubleshooting +ACAPY_LOG_LEVEL=WARNING + +# Enable structured logging for log aggregation +ACAPY_LOG_JSON=true +``` + +**Example log monitoring with Prometheus:** + +```yaml +# Add custom metrics exporter +- name: acapy-exporter + image: custom/acapy-exporter:latest + environment: + ACAPY_ADMIN_URL: http://acapy:8021 + ACAPY_ADMIN_KEY: "${ADMIN_API_KEY}" +``` + +### 2. Backup & Disaster Recovery + +**Critical data to backup:** +- ACA-Py wallet database (contains signing keys) +- Supported credential configurations +- Status list data (if using status_list plugin) +- mDOC trust anchors and certificates + +**Backup strategy:** + +```bash +# Daily automated PostgreSQL backups +pg_dump -h localhost -U acapy acapy > backup-$(date +%Y%m%d).sql + +# Encrypt backups before storage +openssl enc -aes-256-cbc -salt -in backup-$(date +%Y%m%d).sql \ + -out backup-$(date +%Y%m%d).sql.enc -k "$BACKUP_PASSWORD" + +# Store encrypted backups off-site +aws s3 cp backup-$(date +%Y%m%d).sql.enc s3://backups/acapy/ +``` + +**Recovery testing:** +- Test backup restoration quarterly +- Document recovery time objective (RTO) and recovery point objective (RPO) +- Maintain runbooks for disaster recovery scenarios + +### 3. Scalability + +**Horizontal scaling:** + +OID4VC endpoints are stateless and can be load-balanced: + +```yaml +# docker-compose.yml - multiple ACA-Py instances +services: + acapy-1: + image: ghcr.io/openwallet-foundation/acapy:latest + # ... config ... + + acapy-2: + image: ghcr.io/openwallet-foundation/acapy:latest + # ... config ... + + load-balancer: + image: nginx:alpine + volumes: + - ./nginx-lb.conf:/etc/nginx/nginx.conf:ro + ports: + - "443:443" + depends_on: + - acapy-1 + - acapy-2 +``` + +**Database tuning:** + +```sql +-- Optimize PostgreSQL for high-throughput credential issuance +ALTER SYSTEM SET max_connections = 200; +ALTER SYSTEM SET shared_buffers = '256MB'; +ALTER SYSTEM SET effective_cache_size = '1GB'; +ALTER SYSTEM SET work_mem = '16MB'; + +-- Add indexes for common queries +CREATE INDEX idx_exchange_state ON oid4vci_exchanges(state); +CREATE INDEX idx_presentation_state ON oid4vp_presentations(state); +``` + +### 4. Security Monitoring + +**Enable audit logging:** + +```bash +# Log all admin API calls +ACAPY_ADMIN_AUDIT_LOG=true +ACAPY_AUDIT_LOG_FILE=/var/log/acapy/audit.log +``` + +**Monitor for suspicious activity:** +- Unusual credential issuance volumes +- Failed authentication attempts +- Invalid presentation submissions +- Unexpected trust anchor modifications + +### 5. Incident Response + +**Credential revocation procedure:** + +```bash +# Immediately revoke a compromised credential +curl -X POST https://admin.example.com/credentials/{cred_id}/revoke \ + -H "X-API-KEY: $ADMIN_KEY" + +# Update and publish status list +curl -X POST https://admin.example.com/status-lists/1/publish \ + -H "X-API-KEY: $ADMIN_KEY" +``` + +**Key compromise response:** +1. Generate new signing key immediately +2. Issue credentials with new key +3. Revoke all credentials signed with compromised key +4. Notify credential holders to request new credentials +5. Document incident and root cause + +--- + +## Integration with Status List Plugin + +The Status List plugin enables credential revocation and suspension: + +### Configuration + +```bash +# Install alongside oid4vc +pip install git+https://github.com/openwallet-foundation/acapy-plugins.git#subdirectory=status_list + +# Load plugin +--plugin status_list +``` + +### Creating Status Lists + +```bash +# Create a new status list +curl -X POST https://admin.example.com/status-list \ + -H "X-API-KEY: $ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "purpose": "revocation", + "capacity": 100000 + }' +``` + +### Issuing Credentials with Status + +```bash +# Supported credential with status support +curl -X POST https://admin.example.com/oid4vci/credential-supported/create/sd-jwt \ + -H "X-API-KEY: $ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "format": "vc+sd-jwt", + "id": "EmployeeCredential", + "vct": "EmployeeCredential", + "status": { + "status_list": { + "idx": "auto", + "uri": "https://issuer.example.com/status-lists/1" + } + } + }' +``` + +### Revoking Credentials + +```bash +# Update credential status +curl -X PATCH https://admin.example.com/oid4vci/exchange/records/{exchange_id}/status \ + -H "X-API-KEY: $ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{"status": "revoked"}' + +# Publish updated status list (makes revocation effective) +curl -X POST https://admin.example.com/status-lists/1/publish \ + -H "X-API-KEY: $ADMIN_KEY" +``` + +--- + +## Testing Production Deployment + +### Pre-Deployment Checklist + +- [ ] HTTPS enabled for all public endpoints +- [ ] Admin API authentication configured +- [ ] Database backups configured and tested +- [ ] Monitoring and alerting configured +- [ ] Security scanning completed (container images, dependencies) +- [ ] Load testing performed +- [ ] Disaster recovery procedures documented +- [ ] Incident response runbooks prepared + +### Smoke Tests + +```bash +# Test OID4VCI credential issuer metadata +curl -s https://issuer.example.com/.well-known/openid-credential-issuer | jq + +# Test admin API authentication +curl -H "X-API-KEY: $ADMIN_KEY" https://admin.example.com/status/ready + +# Test credential issuance flow (end-to-end) +# 1. Create supported credential +# 2. Create exchange +# 3. Generate offer +# 4. Complete issuance with test wallet +``` + +### Load Testing + +```bash +# Use k6 or similar for load testing +k6 run --vus 100 --duration 5m load-test.js +``` + +--- + +## Additional Resources + +- [Getting Started](getting-started.md) — Initial setup and configuration +- [Architecture](architecture.md) — Understanding the plugin design +- [Admin API Reference](admin-api-reference.md) — Complete endpoint documentation +- [Troubleshooting](troubleshooting.md) — Common issues and solutions +- [ACA-Py Production Guide](https://aca-py.org/latest/deploying/) — General ACA-Py deployment best practices diff --git a/oid4vc/docs/public-api-reference.md b/oid4vc/docs/public-api-reference.md new file mode 100644 index 000000000..f501fa0cd --- /dev/null +++ b/oid4vc/docs/public-api-reference.md @@ -0,0 +1,337 @@ +# Public API Reference + +These endpoints are served on the **OID4VCI Public Server** (`http://:`). They implement the protocol-level interfaces specified by OID4VCI and OID4VP, consumed by wallets and holder agents — not by the controller. + +The Swagger UI for the public server is available at `http://:/api/doc`. + +> **Multitenant note:** In a multitenant deployment, all paths below are prefixed with `/tenant/{wallet_id}`. + +--- + +## Well-Known Metadata Endpoints + +### `GET /.well-known/openid-credential-issuer` + +Returns the credential issuer metadata as defined by OID4VCI §10.2. Wallets call this first to discover what credentials are available and where to request tokens and credentials. + +**Accepts:** `application/json` (default) or `application/jwt` (returns a signed JWT metadata object per the JWT Issuer Metadata spec) + +**Response `200`:** + +| Field | Description | +|---|---| +| `credential_issuer` | The canonical URL of this issuer (from `OID4VCI_ENDPOINT`) | +| `authorization_servers` | List of authorization server URLs (when `OID4VCI_AUTH_SERVER_URL` is configured) | +| `credential_endpoint` | URL for the credential issuance endpoint | +| `nonce_endpoint` | URL for the nonce endpoint (OID4VCI §8) | +| `credential_configurations_supported` | Object mapping credential configuration IDs to their metadata (populated from `SupportedCredential` records) | +| `batch_credential_issuance` | Optional batch issuance parameters | + +**Example response:** + +```json +{ + "credential_issuer": "https://issuer.example.com", + "credential_endpoint": "https://issuer.example.com/credential", + "nonce_endpoint": "https://issuer.example.com/nonce", + "credential_configurations_supported": { + "UniversityDegreeCredential": { + "format": "jwt_vc_json", + "scope": "UniversityDegreeCredential", + "cryptographic_binding_methods_supported": ["did:jwk"], + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256", "EdDSA"]} + }, + "display": [{"name": "University Degree", "locale": "en-US"}], + "credential_definition": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiableCredential", "UniversityDegreeCredential"] + } + } + } +} +``` + +--- + +### `GET /.well-known/openid_credential_issuer` + +Deprecated variant with an underscore. Adds `Deprecation` and `Link` response headers pointing to the hyphenated form. Wallets should use `/.well-known/openid-credential-issuer` instead. + +--- + +### `GET /.well-known/openid-configuration` + +Returns combined OpenID Connect Discovery + OID4VCI authorization server metadata (RFC 8414 §2 + OID4VCI §10.1). + +**Example response:** + +```json +{ + "issuer": "https://issuer.example.com", + "token_endpoint": "https://issuer.example.com/token", + "credential_issuer": "https://issuer.example.com", + "credential_endpoint": "https://issuer.example.com/credential", + "grant_types_supported": ["urn:ietf:params:oauth:grant-type:pre-authorized_code"], + "response_types_supported": ["token"] +} +``` + +--- + +### `GET /.well-known/oauth-authorization-server` + +Same content as `/.well-known/openid-configuration`. Provided for OAuth 2.0 AS metadata discovery (RFC 8414). + +--- + +## Token Endpoint + +### `POST /token` + +Exchange a pre-authorized code for an access token (OID4VCI §4.1). This is the second step in the issuance flow after the wallet scans the credential offer QR code. + +**Content-Type:** `application/x-www-form-urlencoded` + +**Request parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `grant_type` | string | **Yes** | Must be `urn:ietf:params:oauth:grant-type:pre-authorized_code` | +| `pre-authorized_code` | string | **Yes** | The pre-authorized code from the credential offer (also accepted as `pre_authorized_code`) | +| `tx_code` | string | Conditional | User PIN/transaction code (required when `user_pin_required: true` in the offer). Also accepted as `user_pin`. | + +**Example request:** + +```bash +curl -X POST https://issuer.example.com/token \ + -d "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code" \ + -d "pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA" +``` + +**Response `200`:** + +```json +{ + "access_token": "eyJhbGciOi...", + "token_type": "Bearer", + "expires_in": 86400, + "c_nonce": "tZignsnFbp", + "c_nonce_expires_in": 86400 +} +``` + +**Error responses** (per OID4VCI §4.1.3): + +| Error Code | Meaning | +|---|---| +| `invalid_request` | Missing or malformed parameters | +| `invalid_grant` | Pre-authorized code is invalid or expired | +| `unsupported_grant_type` | Grant type not supported | + +--- + +## Nonce Endpoint + +### `POST /nonce` or `GET /nonce` + +Request a fresh server-generated nonce for use in proof of possession (OID4VCI §8). Wallets that do not retain the `c_nonce` from the token response should call this endpoint to obtain a fresh nonce. + +**Response `200`:** + +```json +{ + "c_nonce": "tZignsnFbp", + "c_nonce_expires_in": 86400 +} +``` + +--- + +## Credential Endpoint + +### `POST /credential` + +Issue a credential (OID4VCI §7.2). Called by the wallet after obtaining an access token. + +**Content-Type:** `application/json` + +**Authorization:** `Bearer ` + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `credential_identifier` | string | Recommended | Identifies which credential to issue (matches a key in `credential_configurations_supported`). Use this for OID4VCI 1.0 compliance. | +| `format` | string | Alternative | Credential format (older/draft implementations). | +| `proof` | object | **Yes** | Proof of possession of the holder's key | +| `proof.proof_type` | string | **Yes** | Must be `jwt` | +| `proof.jwt` | string | **Yes** | A JWT signed by the holder's key, containing `aud` (issuer URL), `iat`, `nonce` (the `c_nonce`), and `iss`/`kid` identifying the holder key | + +**Example request:** + +```bash +ACCESS_TOKEN="eyJhbGciOi..." +C_NONCE="tZignsnFbp" + +# Build proof JWT (typically done by wallet SDK): +PROOF_JWT="eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDpqd2s6ZXlKai4uLiMwIn0.eyJhdWQiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwibm9uY2UiOiJ0WmlnbnNuRmJwIn0.signature" + +curl -X POST https://issuer.example.com/credential \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"credential_identifier\": \"UniversityDegreeCredential\", + \"proof\": { + \"proof_type\": \"jwt\", + \"jwt\": \"$PROOF_JWT\" + } + }" +``` + +**Response `200`:** + +```json +{ + "credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6ey...", + "credentials": [ + { + "credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." + } + ], + "notification_id": "3fwe98j..." +} +``` + +**Error responses** (per OID4VCI §7.3.2): + +| Error Code | Meaning | +|---|---| +| `invalid_credential_request` | Missing or malformed request | +| `unsupported_credential_type` | Requested credential type not supported | +| `invalid_proof` | Proof JWT is invalid, missing, or signature verification failed | +| `invalid_nonce` | `nonce` in proof does not match a valid server nonce | +| `invalid_credential_identifier` | `credential_identifier` not found in `credential_configurations_supported` | +| `invalid_credential_configuration` | Internal configuration error | + +--- + +## Notification Endpoint + +### `POST /notification` + +Send a lifecycle notification to the issuer (OID4VCI §11). Wallets call this to report the outcome of credential processing. + +**Authorization:** `Bearer ` + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `notification_id` | string | **Yes** | The `notification_id` from the credential response | +| `event` | string | **Yes** | One of: `credential_accepted`, `credential_failure`, `credential_deleted` | +| `event_description` | string | No | Human-readable description of the event | + +**Example request:** + +```bash +curl -X POST https://issuer.example.com/notification \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "notification_id": "3fwe98j...", + "event": "credential_accepted" + }' +``` + +**Response `204`:** No content on success. + +**Effect:** Updates the exchange record state to `accepted` (for `credential_accepted`) and emits a `oid4vci` webhook event. + +--- + +## Credential Offer Dereference + +### `GET /oid4vci/dereference-credential-offer` + +Dereference a credential offer by reference. Called by wallets that receive a `credential_offer_uri` (by-reference offer) rather than an inline `credential_offer`. + +**Query parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `exchange_id` | string | The exchange ID embedded in the reference URI | + +**Response `200`:** The full credential offer JSON object. + +--- + +## OID4VP — Presentation Endpoints + +### `GET /oid4vp/request/{request_id}` + +Retrieve a signed OID4VP authorization request (JAR — JWT Authorization Request). Called by wallets after scanning the `openid://` QR code. + +**Response:** A signed JWT containing the authorization request parameters: + +| JWT Claim | Description | +|---|---| +| `response_uri` | URL where the wallet should POST the VP response (`/oid4vp/response/{presentation_id}`) | +| `nonce` | Server-generated nonce for binding the response | +| `client_id` | Verifier DID or `x509_san_dns:` | +| `presentation_definition` | PEX v2 presentation definition (when using PEX flow) | +| `dcql_query` | DCQL query object (when using DCQL flow) | +| `response_type` | `"vp_token"` | +| `response_mode` | `"direct_post"` | + +**Effect:** Moves the presentation record from `request-created` → `request-retrieved`. The VP request record is deleted. + +> **Note:** After calling this endpoint, the `GET /oid4vp/request/{request_id}` admin endpoint will return `404` since the request record is deleted. + +--- + +### `POST /oid4vp/response/{presentation_id}` + +Submit a verifiable presentation (OID4VP direct_post response mode). Called by wallets after constructing the VP from the authorization request. + +**Content-Type:** `application/x-www-form-urlencoded` + +**Path parameters:** + +| Parameter | Description | +|---|---| +| `presentation_id` | From the `response_uri` in the JAR | + +**Form parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `vp_token` | string | **Yes** | The verifiable presentation. For PEX flows: a JWT VP. For DCQL flows: a JSON object keyed by `credential_query_id`. | +| `presentation_submission` | string | Conditional | JSON string containing the PEX presentation submission descriptor. Required for PEX flows; omitted for DCQL flows. | +| `state` | string | No | Optional state parameter from the JAR | + +**PEX example `vp_token`:** A JWT VP + +**DCQL example `vp_token`:** + +```json +{ + "mdl": "" +} +``` + +where `mdl` is the `id` from the matching `CredentialQuery`. + +**Response `200`:** `{}` on success + +**Effect:** The plugin verifies the VP signature and evaluates the PEX/DCQL constraints. Updates the presentation record state to `presentation-valid` or `presentation-invalid`. Emits a `oid4vp` webhook event. + +--- + +## Status List Endpoint + +### `GET /status/{list_number}` + +Return a status list by number. Only available when `OID4VCI_STATUS_HANDLER` is configured (requires the Status List Plugin). + +**Response `200`:** Status list token (JWT or bit-encoded format depending on the configured handler). diff --git a/oid4vc/docs/troubleshooting.md b/oid4vc/docs/troubleshooting.md new file mode 100644 index 000000000..258ab1897 --- /dev/null +++ b/oid4vc/docs/troubleshooting.md @@ -0,0 +1,269 @@ +# Troubleshooting + +## Common Issues + +### Plugin Not Loading + +**Symptom:** No `oid4vci`, `oid4vp`, or `mso_mdoc` routes appear in Swagger. + +**Checks:** + +```bash +# Verify oid4vc plugin is registered +curl -s http://localhost:8021/plugins | python3 -m json.tool | grep oid4vc + +# Check ACA-Py startup logs for plugin load errors +docker logs acapy-issuer 2>&1 | grep -i "plugin\|oid4vc\|error" +``` + +**Fix:** Ensure `--plugin oid4vc` is in the ACA-Py startup arguments and the package is installed in the same Python environment. + +--- + +### OID4VCI Public Server Not Reachable + +**Symptom:** `GET /.well-known/openid-credential-issuer` returns connection refused. + +**Checks:** + +```bash +# Check if OID4VCI_HOST and OID4VCI_PORT are configured +docker inspect acapy-issuer | grep -A2 "OID4VCI" + +# Verify the public server started +docker logs acapy-issuer 2>&1 | grep -i "oid4vci\|server" +``` + +**Fix:** Both `OID4VCI_HOST`, `OID4VCI_PORT`, and `OID4VCI_ENDPOINT` must be set. `OID4VCI_ENDPOINT` must be a publicly reachable URL. See [Getting Started — Configuration](getting-started.md#configuration-reference). + +--- + +### `ConfigError` on Startup + +**Error:** + +``` +oid4vc.config.ConfigError: Required configuration key 'oid4vci.endpoint' is missing +``` + +**Fix:** Set the `OID4VCI_ENDPOINT` environment variable to the public base URL of the OID4VCI server. + +--- + +## OID4VCI Token Errors + +### `invalid_grant` + +**Error response:** + +```json +{"error": "invalid_grant", "error_description": "Pre-authorized code is invalid or expired"} +``` + +**Causes:** + +1. The pre-authorized code was already used (codes are single-use) +2. The exchange record was deleted before token issuance +3. The offer was generated from an exchange in a non-`offer` state (e.g. `issued`) + +**Fix:** Generate a new credential offer for a new or refreshed exchange record. + +--- + +### `invalid_proof` + +**Error response:** + +```json +{"error": "invalid_proof", "error_description": "Proof verification failed"} +``` + +**Causes:** + +1. The holder's proof JWT signature is invalid +2. The `nonce` in the proof JWT does not match the `c_nonce` from the token response +3. The `aud` in the proof JWT does not match the `credential_issuer` URL +4. The proof JWT is missing `kid`, `jwk`, or `x5c` header — the issuer cannot extract a holder key + +**Fix:** Ensure the wallet: +- Signs the proof JWT with the holder's private key +- Sets `aud` to exactly the `credential_issuer` URL from issuer metadata +- Includes the `c_nonce` as the `nonce` claim +- Sets `iat` to the current time +- Includes either `kid` (DID URL), `jwk` (raw JWK), or `x5c` (certificate chain) in the JWT header + +--- + +### `invalid_nonce` + +**Error response:** + +```json +{"error": "invalid_nonce", "error_description": "Nonce has already been used or has expired"} +``` + +**Cause:** Nonce replay protection — each `c_nonce` can only be used once. Card wallets that retry a failed credential request with the same nonce will hit this error. + +**Fix:** Call `POST /nonce` or re-request a token to get a fresh `c_nonce` before retrying. + +--- + +### `invalid_credential_identifier` + +**Error response:** + +```json +{"error": "invalid_credential_identifier", "error_description": "Credential identifier not found"} +``` + +**Cause:** The `credential_identifier` in the credential request does not match any key in `credential_configurations_supported`. + +**Fix:** Use the `id` value from a `SupportedCredential` record (visible in issuer metadata under `credential_configurations_supported`). + +--- + +## OID4VP Errors + +### `404` on `GET /oid4vp/request/{request_id}` + +**Cause:** The VP request record is **deleted after the wallet retrieves the signed JAR**. This is correct — the admin `GET` endpoint reflects that the request was successfully consumed. Use `GET /oid4vp/presentation/{presentation_id}` to check the outcome. + +--- + +### Presentation State Stuck at `request-created` + +**Cause:** The wallet has not scanned/fetched the QR code, or the `openid://` deep link was not handled. + +**Check:** + +```bash +curl -s http://localhost:8031/oid4vp/presentation/$PRES_ID | \ + python3 -c "import json,sys; r=json.load(sys.stdin); print(r['state'], r.get('errors'))" +``` + +--- + +### `presentation-invalid` with Errors + +**Example errors:** + +```json +["Descriptor 'degree' did not match any credential in the presentation"] +``` + +**Causes for PEX:** + +- The holder's credential does not satisfy the `constraints.fields` filter (e.g. missing type, wrong value) +- The `format` in the input descriptor does not match the submitted credential format +- `limit_disclosure: required` is set but the credential format doesn't support selective disclosure + +**Causes for DCQL:** + +- The submitted `vp_token` key does not match the credential query `id` +- The mDOC `doctype` does not match `meta.doctype_value` +- The SD-JWT `vct` does not match `meta.vct_values` +- A required claim path is absent from the presented credential + +--- + +### mDOC Trust Anchor Verification Failure + +**Error in logs:** + +``` +mso_mdoc verification failed: certificate chain not trusted +``` + +**Fix:** Add the issuing authority's root certificate as a trust anchor: + +```bash +curl -X POST $ADMIN/mso_mdoc/trust-anchors \ + -H "Content-Type: application/json" \ + -d '{"certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"}' +``` + +Or set `OID4VC_MDOC_TRUST_STORE_TYPE=none` to disable trust anchor verification during development (not for production). + +--- + +## Configuration Issues + +### `sd_jwt_vc` or `mso_mdoc` Routes Not Available + +**Symptom:** `POST /oid4vci/credential-supported/create/sd-jwt` or `/mso_mdoc/*` routes return `404`. + +**Fix:** The `sd_jwt_vc` and `mso_mdoc` sub-plugins must be explicitly loaded: + +``` +--plugin oid4vc --plugin sd_jwt_vc --plugin mso_mdoc +``` + +--- + +### Auth Server Connection Errors + +**Error in logs:** + +``` +AppResources: failed to connect to auth server: Connection refused +``` + +**Cause:** `OID4VCI_AUTH_SERVER_URL` is set but the auth server is not running or not reachable. + +**Fix:** Either start the auth server (see [auth_server/README.md](../auth_server/README.md)) or remove `OID4VCI_AUTH_SERVER_URL` to use the built-in token endpoint. + +--- + +## Error Code Reference + +### Admin API + +| HTTP Status | Common Cause | Fix | +|---|---|---| +| `400 Bad Request` | Missing required field, schema validation failure | Check request body against the schema in [Admin API Reference](admin-api-reference.md) | +| `404 Not Found` | Record ID does not exist | Verify the ID; records may have been deleted or belong to a different wallet | +| `500 Internal Server Error` | Signing key not found, ` mso_mdoc` library error | Check ACA-Py logs for the full traceback | + +### OID4VCI Public Server + +| Error Code | HTTP Status | Description | +|---|---|---| +| `invalid_request` | 400 | Malformed request body or missing parameters | +| `invalid_grant` | 400 | Pre-authorized code is invalid, expired, or already used | +| `unsupported_grant_type` | 400 | Grant type other than `pre-authorized_code` was used | +| `invalid_proof` | 400 | Proof JWT signature invalid, wrong `aud`, or holder key cannot be extracted | +| `invalid_nonce` | 400 | Nonce already used or expired | +| `invalid_credential_identifier` | 400 | `credential_identifier` not in issuer metadata | +| `invalid_credential_request` | 400 | Other credential request validation failure | +| `invalid_credential_configuration` | 500 | Internal server configuration error | + +--- + +## Debugging Tips + +### Enable Debug Logging + +The OID4VCI public server includes a `debug_middleware` that logs all requests and responses when ACA-Py is started with `--log-level debug`: + +```bash +aca-py start --log-level debug --plugin oid4vc ... +``` + +### Inspect Exchange Records + +```bash +# List all recent exchanges +curl -s "http://localhost:8021/oid4vci/exchange/records" | \ + python3 -c "import json,sys; [print(r['exchange_id'], r['state']) for r in json.load(sys.stdin)['results']]" + +# Get a specific exchange with all fields +curl -s "http://localhost:8021/oid4vci/exchange/records/$EXCHANGE_ID" | python3 -m json.tool +``` + +### Inspect Issuer Metadata + +Verify the credential configurations are correctly populated: + +```bash +curl -s "http://localhost:8022/.well-known/openid-credential-issuer" | python3 -m json.tool +```