Released docs. You are viewing the documentation published with v0.20.0. Development docs are available at Latest.
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.
When to use evidence-oid4vci
Section titled “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.
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.
Treat the frozen profile as the contract
Section titled “Treat the frozen profile as the contract”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 property | Supported value |
|---|---|
| Protocol | OpenID for Verifiable Credential Issuance (OID4VCI) 1.0 Final |
| Initiation and grant | Issuer-initiated pre-authorized code, with anonymous token-client access |
| Credential format | Holder-bound Evidence dc+sd-jwt only |
| Proof | jwt with ES256 over a P-256 holder key |
| Proof key source | Inline public JWK or bounded self-contained did:jwk#0 |
| Batch shape | Plural proofs.jwt in, plural credentials out |
| State | Bounded process-local memory, exactly one service replica |
| Compatibility claim | Frozen 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.
Use the pinned Inji evidence narrowly
Section titled “Use the pinned Inji evidence narrowly”The interoperability target is the metadata-driven flow in these upstream develop revisions:
| Component | Revision | Revision date |
|---|---|---|
| Inji Wallet | 2fa12c3285b6523db340c3dd2333454b750b40a4 | 2026-08-03 |
| Inji Android VCI client | f1d7ee2b14e996e18bfc7c40fbf89ec31b768951 | 2026-07-31 |
| Inji iOS VCI client | dbe60eef9a8c7b71ba58ee81cc7d0e5a92af7c7c | 2026-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.
Before you start
Section titled “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-oid4vciits own access token to Evidence Gateway through theprivate_key_jwtclient assertion flow, and this service’s own private JWK for that assertion. - A published
httpsorigin for the credential issuer identifier this service will serve. A wallet compares this origin exactly against theaudof every proof it signs.
Configure the deployment
Section titled “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.
version: 1validationMode: strictcredentialIssuer: https://wallet.example.orglistener: address: 127.0.0.1 port: 8090 maximumRequestBytes: 16384 requestTimeoutMilliseconds: 5000metricsListener: address: 127.0.0.1 port: 9090evidence: baseUrl: https://evidence.example.orgmint: tokenEndpoint: https://mint.example.org/token clientId: evidence-oid4vci privateKeyFile: keys/delivery-client.jwk.json clientAssertionAudience: https://mint.example.org/tokenoffers: issuer: https://mint.example.org jwksUri: https://mint.example.org/.well-known/jwks.json audiences: [evidence-oid4vci] algorithms: [EdDSA] authorizedClients: [adopter-front-end] maximumTokenLifetimeSeconds: 900store: maximumOffers: 4096 offerLifetimeSeconds: 300 accessTokenLifetimeSeconds: 300 nonceLifetimeSeconds: 120 maximumTransactionCodeAttempts: 3credentialIssuer 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:
evidence-oid4vci check --config /etc/evidence-oid4vci/oid4vci.yamlcheck 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:
umask 077evidence-oid4vci inspect \ --config /etc/evidence-oid4vci/oid4vci.yaml \ > oid4vci-metadata.jsonThe 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:
evidence-oid4vci openapi --output oid4vci.openapi.jsonThis command needs no deployment configuration and performs no network request.
Start evidence-oid4vci
Section titled “Start evidence-oid4vci”Run exactly one serving process for the deployment:
evidence-oid4vci serve --config /etc/evidence-oid4vci/oid4vci.yamlEvery 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:
curl --fail --silent https://wallet.example.org/healthcurl --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.
Monitor the private metrics listener
Section titled “Monitor the private metrics listener”When metricsListener is present, collect Prometheus text from its only route:
curl --fail --silent http://127.0.0.1:9090/metricsThe 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.
| Metric | Type | Meaning |
|---|---|---|
evidence_oid4vci_http_requests_total | Counter | Delivery requests by closed route, method, status, and problem class |
evidence_oid4vci_http_request_duration_seconds | Histogram | Delivery request latency with the same closed labels |
evidence_oid4vci_outcomes_total | Counter | Offer, code, token, nonce, proof, issuance, upstream Evidence, saturation, store-fault, and cleanup outcomes |
evidence_oid4vci_store_entries and evidence_oid4vci_store_capacity | Gauges | The fullest independently bounded offer, ledger, or token keyspace and its refusal threshold |
evidence_oid4vci_cleanup_expired_total | Counter | Expired 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.
Protocol surface
Section titled “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, 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-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 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 /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
Section titled “Operational limits”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.
- Configure Registry Mint to issue the access tokens
evidence-oid4vciuses to authenticate to Evidence Gateway and to protect its ownPOST /offersendpoint. - Configure Evidence Gateway for the deployment this service requests credentials from.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Bounded cause | Corrective action |
|---|---|---|
check refuses the configuration | A 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 unavailable | The 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 body | The 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 restart | Restart 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_nonce | The 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_proof | The 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_denied | Evidence 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 load | The 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 failure | Registry 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 connection | metricsListener 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.