Skip to content
Registry StackDocsv0.20.0

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 frozen protocol and deployment profile, not general issuer certification or compatibility with a wallet product.

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.

Evidence Gateway signs every credential. The adapter holds no Evidence Gateway signing key and never holds a holder private key. It receives holder public keys inside wallet-signed proof JSON Web Tokens (JWTs) and passes those public keys into one Evidence Gateway batch request. Every Evidence authorization decision, source acquisition, derivation, and 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.

products/evidence/contracts/oid4vci-profile.yaml is the versioned source of truth for the adapter. Public metadata, the generated OpenAPI document, configuration, and runtime tests must stay within that profile.

products/evidence/generated/registry-evidence-oid4vci.openapi.json is the generated contract for the complete delivery listener. Reproduce it with products/evidence/scripts/check-contracts.sh; do not edit the generated document by hand.

Profile propertySupported value
ProtocolOpenID for Verifiable Credential Issuance (OID4VCI) 1.0 Final
Initiation and grantIssuer-initiated pre-authorized code, with anonymous token-client access
Credential formatHolder-bound Evidence dc+sd-jwt only
Proofjwt with ES256 over a P-256 holder key
Proof key sourceInline public JWK or bounded self-contained did:jwk#0
Batch shapePlural proofs.jwt in, plural credentials out
StateBounded process-local memory, exactly one service replica
Compatibility claimFrozen Registry profile implementation, not full issuer certification or conformance

The service performs no remote Decentralized Identifier (DID) resolution. A did:jwk#0 value contains the public JWK that the service decodes locally.

The profile does not support:

  • Authorization code flow or Pushed Authorization Requests (PAR)
  • Demonstrating Proof of Possession (DPoP)
  • Remote DID resolution or any proof-selected key lookup
  • Credential formats other than dc+sd-jwt
  • Deferred issuance, notifications, status, revocation, refresh, or reissuance
  • Draft 13 wire shapes or credential offers by reference
  • Durable protocol state, multi-replica state, or stateful nonce replay tracking
  • Wallet attestation, wallet onboarding, or plugin extensibility

Do not enable an unsupported capability to accommodate a client. A client that requires one is outside this profile.

The interoperability target is the metadata-driven flow in these upstream develop revisions:

ComponentRevisionRevision date
Inji Wallet2fa12c3285b6523db340c3dd2333454b750b40a42026-08-03
Inji Android VCI clientf1d7ee2b14e996e18bfc7c40fbf89ec31b7689512026-07-31
Inji iOS VCI clientdbe60eef9a8c7b71ba58ee81cc7d0e5a92af7c7c2026-08-04

On 2026-08-09, the checked-in sanitized runner passed the Registry-side metadata, wire-shape, negative, concurrency, and independent presentation-verification tests for this target. The separate upstream runner then reproduced the focused tests from all three exact public revisions: 41 Wallet tests, the selected Kotlin unit-test task, and 12 Swift tests passed. The sanitized receipt is committed at products/evidence/fixtures/interoperability/inji-oid4vci/receipt.json.

This is bounded interoperability evidence for exact revisions and test conditions. It does not cover the Android or iOS user interface, device behavior, wallet storage or presentation, full issuer certification, another Inji release, or general compatibility.

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
metricsListener:
address: 127.0.0.1
port: 9090
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 that the same coordinated Evidence Gateway generation would refuse to issue. The advertised batch_credential_issuance.batch_size also comes from authenticated Evidence discovery. The adapter omits that optional metadata member when the effective ceiling is one, and publishes it only for a ceiling from two through sixteen. It records the effective value with each offer, so a later metadata change cannot widen an outstanding exchange.

metricsListener is optional and absent by default. When present, it binds a separate operator-private loopback or private address. It cannot share the delivery listener’s binding. The delivery listener does not serve metrics, and the metrics listener does not serve protocol or adopter routes.

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, inspect, and serve accept EVIDENCE_OID4VCI_CONFIG in place of --config.

Inspect the metadata derived from the validated configuration and authenticated Evidence discovery before routing traffic:

Terminal window
umask 077
evidence-oid4vci inspect \
--config /etc/evidence-oid4vci/oid4vci.yaml \
> oid4vci-metadata.json

The command prints one indented JSON document. Open it and read the profile marker it claims and the deployment shape it derived:

{
"profile": "registry.evidence.oid4vci-profile/v1",
"credentialIssuerMetadata": { ... },
"authorizationServerMetadata": { ... },
"deployment": {
"replicas": 1,
"restartInvalidatesOutstandingExchanges": true,
"metricsEnabled": true
}
}

The two elided members are the credential issuer and authorization server metadata exactly as the service derives them, and they are the parts worth reading in full before routing traffic. inspect is an explicit diagnostic command, so it may print raw issuer and audience values to stdout. Keep the output owner-only and do not send it to operational logs, metrics, or a shared build artifact. Operational logging omits those raw values.

Render the deterministic OpenAPI 3.1 contract when the deployment process needs an inspectable copy of the public HTTP surface:

Terminal window
evidence-oid4vci openapi --output oid4vci.openapi.json

This command needs no deployment configuration and performs no network request.

Run exactly one serving process for the deployment:

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.

Use the two process-local probes after the listener starts:

Terminal window
curl --fail --silent https://wallet.example.org/health
curl --fail --silent https://wallet.example.org/ready
{"status":"ok"}
{"status":"ready"}

/health reports that the process answers. /ready reports that the process holds its loaded configuration, key material, and state store. Readiness does not contact or report the condition of Evidence Gateway or Registry Mint. Use the deployment’s private upstream monitoring to observe those services.

When metricsListener is present, collect Prometheus text from its only route:

Terminal window
curl --fail --silent http://127.0.0.1:9090/metrics

The listener returns 404 Not Found for every other path. With metricsListener absent, the process opens no metrics socket and the delivery listener still has no /metrics route.

MetricTypeMeaning
evidence_oid4vci_http_requests_totalCounterDelivery requests by closed route, method, status, and problem class
evidence_oid4vci_http_request_duration_secondsHistogramDelivery request latency with the same closed labels
evidence_oid4vci_outcomes_totalCounterOffer, code, token, nonce, proof, issuance, upstream Evidence, saturation, store-fault, and cleanup outcomes
evidence_oid4vci_store_entries and evidence_oid4vci_store_capacityGaugesThe fullest independently bounded offer, ledger, or token keyspace and its refusal threshold
evidence_oid4vci_cleanup_expired_totalCounterExpired process-local entries released by cleanup

The request labels are closed to the registered routes plus unmatched, the methods GET, POST, HEAD, OPTIONS, and other, three status classes, and the protocol’s fixed problem classes. The complete outcome vocabulary is offer_created, offer_authorization_refused, store_saturated, code_redeemed, code_claim_refused, token_claimed, token_claim_refused, nonce_minted, nonce_invalid, nonce_tampered, nonce_expired, proof_refused, credential_issued, evidence_refused, evidence_not_available, evidence_unavailable, store_fault, and cleanup_expired. These labels contain no request-derived or deployment-specific identifier.

Structured request logs use the same closed route, method, status, and problem classes. Logs and metrics omit codes, transaction codes, tokens, nonces, proofs, holder keys, credentials, selectors, subject identifiers, client identifiers, credential configuration identifiers, raw issuers, and raw audiences. Successful /health and /ready requests log at DEBUG; probe failures and non-probe requests remain visible at INFO.

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, supported credential configurations, and a batch ceiling only when the deployment accepts at least two proofs. 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 or empty JSON object 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.

Run exactly one replica. A pre-authorized code and an access token can each be claimed once within that process. A second replica would hold another store and would not observe the first replica’s claim, so a load-balanced or rolling multi-replica deployment is outside the profile.

A recognized live access token is claimed before the credential request body and holder proofs are accepted. Invalid JSON, configuration, nonce, proof, or duplicate holder key leaves that token spent and makes no Evidence request. Obtain a new authorized exchange before correcting and retrying the request. A failure after Evidence access begins also leaves the token spent and returns the terminal credential_request_denied error, because retrying that token cannot succeed.

Stop the old replica before starting a replacement. Restarting invalidates outstanding offers, pre-authorized codes, and access tokens. The client receives the same value-free refusal used for an unknown, expired, consumed, or locked exchange and must obtain a new offer. A nonce is a stateless, service-minted freshness challenge. The service validates the nonce’s integrity and expiry but does not store or consume it, so nonce reuse alone is not replay detection.

Treat an Evidence Gateway bundle change as a coordinated restart. Stop the adapter first, which invalidates its outstanding exchanges, restart Evidence Gateway with the new bundle, then start the adapter so its catalog cache begins from the new deployment generation. Do not restart or replace Evidence Gateway behind a running adapter whose cached metadata and outstanding offers came from the previous bundle generation.

Size store.maximumOffers for every offer created during the configured offer lifetime, including offers already redeemed whose failure ledgers remain live. The approximate sustained creation ceiling is maximumOffers / offerLifetimeSeconds per second. Saturation refuses a new offer without evicting a live authorized exchange. Short offer and access-token lifetimes and a transaction code reduce the usefulness of copied offer material.

SymptomBounded causeCorrective action
check refuses the configurationA required field, origin, bound, listener, or owner-only client key is invalid.Correct the named configuration category and rerun check. Do not weaken strict mode for a shared deployment.
Issuer metadata is unavailableThe adapter could not derive a current holder-bound catalog from Evidence Gateway.Check Evidence Gateway through operator-private monitoring, then inspect the metadata again. Do not add a hand-authored credential configuration.
/health or /ready does not return its fixed success bodyThe process or listener is unavailable, or the loaded process state is not ready.Remove the replica from traffic, correct the startup or listener failure, and start one replacement.
A code or token stops working after a restartRestart discarded the process-local exchange.Create a new authorized offer and complete a new exchange. Do not retry an outstanding exchange against the replacement process.
The credential endpoint reports invalid_nonceThe nonce is malformed, tampered, expired, or not minted by this service.Obtain a fresh nonce from the discovered nonce endpoint, obtain a new offer and access token, and sign a new proof with a synchronized clock.
The credential endpoint reports invalid_proofThe proof shape, algorithm, key source, audience, freshness, or distinct-key rule is outside the frozen profile.Compare the request with published metadata, then obtain a new offer, access token, and nonce before retrying.
The credential endpoint reports credential_request_deniedEvidence Gateway refused, lacked, or could not complete this already claimed exchange.Diagnose Evidence Gateway through private monitoring, then create a new authorized offer after the upstream is usable. Do not retry the spent token.
A new offer is refused under loadThe bounded in-memory store is at capacity.Stop creating offers, allow expired entries to be cleaned up, and size maximumOffers for the reviewed live window before the next restart.
Credential delivery reports an upstream failureRegistry Mint or Evidence Gateway did not complete the adapter’s bounded outbound call.Diagnose the upstream through its private monitoring. Start a new authorized exchange after the upstream recovers.
The private metrics address refuses a connectionmetricsListener is absent, invalid, occupied, or the process has not restarted on the edited configuration.Run check, keep the binding private and distinct from the delivery listener, then restart the single replica.

Protocol errors deliberately do not reveal whether a secret value was unknown, expired, consumed, or locked out. Operational logs must not add that distinction or print codes, tokens, nonces, proofs, holder keys, credentials, selectors, subject identifiers, raw issuers, or raw audiences.