Skip to content
Registry StackDocsDevelopment (unreleased)

Configure OID4VCI wallet delivery

View as Markdown

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.

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.

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.

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.

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: 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:

Terminal window
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.

Terminal window
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.

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

EndpointAuthNotes
GET /.well-known/openid-credential-issuernonePublishes 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-servernonePublishes the token endpoint, the pre-authorized-code grant, none for token endpoint authentication, and anonymous pre-authorized access.
POST /offersRegistry Mint bearer tokenAdopter-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 /tokenpre-authorized code, plus the transaction code when the offer set onegrant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code. Returns access_token, token_type, expires_in. No c_nonce.
POST /noncenoneEmpty 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 /credentialbearer access tokenTakes credential_configuration_id and proofs.jwt[]. Returns credentials: [{credential: ...}].
GET /health, GET /readynoneOperational 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.

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.