Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/dev/llms.txt
Full corpus: https://docs.registrystack.org/dev/llms-full.txt

# Configure OID4VCI wallet delivery

> Configure the OID4VCI 1.0 adapter for holder-bound Evidence Gateway credentials.

Configure `evidence-oid4vci` when a deployment needs to expose a holder-bound Evidence Gateway
credential to an OID4VCI client rather than through the Evidence Gateway response contract.
This page documents the adapter's protocol and deployment contract, not compatibility with a
specific wallet.

## When to use evidence-oid4vci

`evidence-oid4vci` is a supporting service, not a product pattern of its own, in the same sense
Registry Mint is. It exposes Evidence Gateway credentials over OID4VCI 1.0 Final, the
wallet-facing protocol Evidence Gateway deliberately refuses to speak.

Three properties bound the service, and none is negotiable:

- **It never signs a credential.** Evidence Gateway signs. `evidence-oid4vci` holds no Evidence
  Gateway signing key and has no code path that could use one.
- **It never holds a holder private key.** It receives holder public keys inside wallet-signed
  proof JWTs and passes them to Evidence Gateway unchanged.
- **It adds no Evidence Gateway semantics.** It is a protocol adapter. Every authorization
  decision, every source acquisition, and every signature stays behind the Evidence Gateway
  runtime contract.

The dependency between the two services runs one way: `evidence-oid4vci` is a client of Evidence
Gateway and, for its adopter-facing endpoint, a resource server for Registry Mint tokens. No
Evidence Gateway crate depends on `evidence-oid4vci` at runtime.

Use `evidence-oid4vci` only for credentials issued under Evidence Gateway's `holder-bound` subject
binding mode. A requirement whose subject binding is audience-scoped is not deliverable through
this service.

The repository does not yet carry a real-wallet or external OID4VCI conformance run. Verify the
chosen wallet against the deployment before making a compatibility claim.

## Before you start

You need:

- A complete Evidence Gateway deployment with at least one requirement declared `holder-bound`,
  permitted for the SD-JWT VC batch format at bundle and grant scope.
- A Registry Mint deployment, or another token issuer, that can grant `evidence-oid4vci` its own
  access token to Evidence Gateway through the `private_key_jwt` client assertion flow, and this
  service's own private JWK for that assertion.
- A published `https` origin for the credential issuer identifier this service will serve. A
  wallet compares this origin exactly against the `aud` of every proof it signs.

## Configure the deployment

`evidence-oid4vci` reads one YAML document, validated as a whole with `deny_unknown_fields`: an
unrecognized key fails the load rather than being silently ignored. Every relative path in it
resolves against the document's own directory.

```yaml
version: 1
validationMode: strict
credentialIssuer: https://wallet.example.org
listener:
  address: 127.0.0.1
  port: 8090
  maximumRequestBytes: 16384
  requestTimeoutMilliseconds: 5000
evidence:
  baseUrl: https://evidence.example.org
mint:
  tokenEndpoint: https://mint.example.org/token
  clientId: evidence-oid4vci
  privateKeyFile: keys/delivery-client.jwk.json
  clientAssertionAudience: https://mint.example.org/token
offers:
  issuer: https://mint.example.org
  jwksUri: https://mint.example.org/.well-known/jwks.json
  audiences: [evidence-oid4vci]
  algorithms: [EdDSA]
  authorizedClients: [adopter-front-end]
  maximumTokenLifetimeSeconds: 900
store:
  maximumOffers: 4096
  offerLifetimeSeconds: 300
  accessTokenLifetimeSeconds: 300
  nonceLifetimeSeconds: 120
  maximumTransactionCodeAttempts: 3
```

`credentialIssuer` is the `credential_issuer` identifier this service publishes, and the exact
value a wallet proof must carry as its own `aud`. A trailing slash is removed as the document
loads, so the identifier the metadata publishes, the identifier an offer names, and the audience a
proof is compared against are one string. `listener.maximumRequestBytes` is bounded to
`1024..=1048576` and `listener.requestTimeoutMilliseconds` to `1..=30000`; a zero body limit or a
zero timeout would leave the service reporting itself ready while every request fails. The timeout
bounds a whole request, from reading its body to the call behind it, and a request that outlives it
is refused with `408 Request Timeout` rather than held open. `evidence.baseUrl`
is the base URL of the Evidence Gateway deployment this service requests credentials from; which
credentials may be requested is the Evidence Gateway bundle's decision, never restated here.
There is deliberately no configuration member for the credential configurations this service
publishes: every entry is derived from the Evidence Gateway bundle, so published metadata cannot
describe a credential Evidence Gateway would refuse to issue.

`mint.privateKeyFile` is this service's own private JWK, read owner-only when the outbound client
to Registry Mint is built, and never logged or rendered. `mint.clientAssertionAudience` defaults to
`mint.tokenEndpoint` when absent, which is the usual registration.

`offers` is the authorization boundary of the adopter-facing `POST /offers` endpoint, and it is a
required, separate document from `mint`: the identity this service authenticates to Mint with has
nothing to do with the identities it accepts offer tokens from. `offers.issuer` and
`offers.jwksUri` name the authorization server that issues offer tokens, compared exactly against a
token's `iss` and read for its keys; both must be `https` in `strict` mode. `offers.audiences`
names the resource identifiers this service answers to, so a token minted for another resource
server cannot be replayed here; at least one is required. `offers.algorithms` defaults to
`[EdDSA]` and accepts exactly one signature algorithm family per deployment. `offers.authorizedClients`
defaults to empty, which accepts any client the issuer vouched for; naming one or more client
identifiers narrows that further. `offers.maximumTokenLifetimeSeconds` defaults to `900` and is
bounded to `60..=3600` seconds.

`validationMode` defaults to `strict`, which requires every published and called origin
(`credentialIssuer`, `evidence.baseUrl`, `mint.tokenEndpoint`, the client assertion audience,
`offers.issuer`, and `offers.jwksUri`) to be an absolute `https` URL with a host and no embedded
credentials, query, or fragment. Set it to `supervised-local-development` only for a disposable
local developer environment; that mode admits one supervised process group on loopback, requiring
the listener to match the credential issuer's origin exactly and every other endpoint to be a
canonical `http://127.0.0.1:<port>` origin.

`store` is optional and defaults as shown. Every bound is a memory limit or a window on the
in-memory store described in [Operational limits](#operational-limits): `maximumOffers` is bounded
to `256..=1048576`; `offerLifetimeSeconds` and `accessTokenLifetimeSeconds` to `60..=900` seconds
each; `nonceLifetimeSeconds` to `30..=900` seconds and must not exceed
`accessTokenLifetimeSeconds`; `maximumTransactionCodeAttempts`, how many wrong transaction codes an
offer survives before it locks out for the rest of its life, to `1..=10`.

Validate the deployment before opening a socket:

```sh
evidence-oid4vci check --config /etc/evidence-oid4vci/oid4vci.yaml
```

`check` does everything `serve` does except bind a socket: it loads and validates the
configuration document and the Mint client key, then exits. `check` and `serve` accept
`EVIDENCE_OID4VCI_CONFIG` in place of `--config`.

## Start evidence-oid4vci

```sh
evidence-oid4vci serve --config /etc/evidence-oid4vci/oid4vci.yaml
```

Every field in the configuration document is startup-only; changing the credential issuer, the
listener, the Evidence Gateway or Mint endpoints, or the store bounds means restarting the process.

## Protocol surface

`evidence-oid4vci` pins OID4VCI 1.0 Final, one version, with no draft-13 compatibility mode.

| Endpoint | Auth | Notes |
| --- | --- | --- |
| `GET /.well-known/openid-credential-issuer` | none | Publishes the issuer and credential endpoints, authorization server, batch ceiling, and supported credential configurations. The token endpoint is not published here. |
| `GET /.well-known/oauth-authorization-server` | none | Publishes the token endpoint, the pre-authorized-code grant, `none` for token endpoint authentication, and anonymous pre-authorized access. |
| `POST /offers` | Registry Mint bearer token | Adopter-facing, not wallet-facing. Takes `credentialConfigurationId`, `subjects`, and a boolean `transactionCode`. Returns the offer object, its `openid-credential-offer://` URI, and, when one was asked for, the generated transaction code, once. |
| `POST /token` | pre-authorized code, plus the transaction code when the offer set one | `grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code`. Returns `access_token`, `token_type`, `expires_in`. No `c_nonce`. |
| `POST /nonce` | none | Empty body in, `{"c_nonce": ...}` out, `Cache-Control: no-store`. The nonce is a keyed freshness challenge and is not bound to an access token. |
| `POST /credential` | bearer access token | Takes `credential_configuration_id` and `proofs.jwt[]`. Returns `credentials: [{credential: ...}]`. |
| `GET /health`, `GET /ready` | none | Operational probes. Readiness deliberately reports nothing about the Evidence Gateway or Registry Mint deployments behind it, so a probe cannot be used to survey them. |

The transaction code is generated by this service and returned to the adopter once, on the `POST
/offers` response, over the channel that request was authorized on. There is no way to supply one,
which is what keeps a caller from choosing a guessable code.

`POST /offers` is the real authorization boundary of this service. It accepts selector values and
will cause a credential to be issued for whoever they identify, so protect it at least as strongly
as you would protect a direct call to Evidence Gateway. It is a resource server for Registry
Mint-issued tokens, verified the same way Evidence Gateway's own authenticator verifies them, and
that verification never shares a code path with this service's own outbound calls to Evidence
Gateway or Registry Mint.

Only a `holder-bound` requirement can be named as a `credential_configuration_id`. A request
naming an audience-scoped requirement is refused, so this service cannot be used to launder an
audience-scoped credential into a wallet.

Each proof uses ES256 and nominates exactly one P-256 public key. It can carry the public JWK
inline, or carry a `kid` in the self-contained `did:jwk:<base64url-public-JWK>#0` form. The service
decodes `did:jwk` locally and performs no remote DID resolution. `aud`, `iat`, and `nonce` are
required. A proof may also carry `exp`; an expired proof is refused, and a far-future `exp` never
widens the five-minute `iat` freshness window.

## Operational limits

Everything this service remembers lives in one bounded in-memory store, and none of it survives a
restart or a second replica. State these limits plainly to anyone operating the deployment:

- **State is in-memory only.** A restart invalidates every offer outstanding in a minutes-wide
  window. There is no database anywhere beneath this service.
- **Single-use is enforced per process.** A redeemed pre-authorized code and a redeemed access
  token are each refused on reuse, but only within the one process that redeemed them, which is why
  a deployment runs exactly one replica. Adding a second replica breaks single-use unless it also
  adds a shared store, which this service does not have.
- **Single-use is mostly a detection property, not a prevention property.** It turns a stolen
  offer's silent duplicate issuance into one party getting an error and complaining. The short
  offer and access-token lifetimes and the `tx_code` are the primary defenses; single-use marks the
  attempt after the fact rather than stopping it before.

## Next

- [Configure Registry Mint](../mint/) to issue the access tokens `evidence-oid4vci` uses to
  authenticate to Evidence Gateway and to protect its own `POST /offers` endpoint.
- [Configure Evidence Gateway](../evidence/) for the deployment this service requests credentials
  from.