Released docs. You are viewing the documentation published with v0.46.0. Development docs are available at Latest.
If you run Registry Evidence and a holder must present a credential from a wallet for Evidence to
answer from, run evidence-oid4vp beside it. The verifier runs the wallet-facing
OpenID4VP exchange and holds the wallet’s response until
Evidence reads it. Evidence then verifies the credential and decides; the verifier only checks
protocol facts, so how Evidence works still describes every
decision.
What you need
Section titled “What you need”- A running Registry Evidence whose project has a definition with
acquisition.holderPresented(see Pair it with an Evidence definition). - A TLS proxy that publishes the verifier’s public HTTPS origin,
publicBaseUrl. - An OIDC issuer that mints two kinds of bearer token: one for the applicant who starts a session, and one, distinct from it, for the Evidence service that reads the result.
- A wallet that holds a holder-bound
dc+sd-jwtcredential and can encrypt its authorization response. Check wallet interoperability says what is and is not established about any wallet. - A build of
evidence-oid4vpfrom source.
evidence-oid4vp is in no release. No release script builds it, the release images and the
rstack toolset do not carry it, and no install command fetches it. Build it from the repository
with cargo build --locked --release -p registry-evidence-oid4vp. The evidence runtime that
reads acquisition.holderPresented does ship in releases.
What the verifier does and does not do
Section titled “What the verifier does and does not do”The verifier creates a presentation session for an authenticated applicant, serves the signed
request object to the wallet, receives the wallet’s encrypted response, and releases the
presentation to Evidence on request. It holds the presentation in memory only, bounded by
sessions.maximumHeldPresentationBytes, and drops the result after sessions.resultTtlSeconds
or when Evidence reads it, whichever comes first.
It never opens a credential, verifies an SD-JWT, or checks a key binding. It holds no Evidence signing key and signs no assertion; its only signing keys sign request objects. Evidence verifies what the wallet presented, so a compromised verifier can withhold or misroute a presentation but cannot make Evidence accept a forged one.
Write the runtime file
Section titled “Write the runtime file”The runtime file names the verifier’s public origin, the tokens it accepts, the client identifiers it signs requests as, and the policies that say what to ask a wallet for. This excerpt of the repository’s example shows the two client identifier kinds most wallets need and one policy for each; replace every origin and reference with your own:
# yaml-language-server: $schema=https://id.registrystack.org/schemas/evidence/oid4vp-runtime/oid4vp-runtime.v1alpha1.schema.jsonapiVersion: id.registrystack.org/formats/evidence/oid4vp-runtime/v1alpha1kind: EvidenceOid4vpRuntimeConfigpublicBaseUrl: https://verifier.example.orglistener: bind: 127.0.0.1:8095secretProviders: file: root: /run/secrets/evidence-oid4vpaudit: destination: file path: /var/lib/registry-evidence-oid4vp/audit.jsonl hashKeyRef: secret:file/audit-hash-keyapplicantTokens: issuer: https://mint.example.org audience: https://verifier.example.org requiredScope: oid4vp:sessionevidenceCredentials: issuer: https://mint.example.org audience: https://verifier.example.org allowedClient: evidence requiredScope: oid4vp:read-resultclientIdentifiers: - id: haip type: x509-san-dns certificateChainRef: secret:file/verifier-chain.pem signingKeyRef: secret:file/verifier-signing.jwk.json - id: inji type: decentralized-identifier did: did:web:verifier.example.org signingKeyRef: secret:file/verifier-did-signing.jwk.jsonpolicies: - id: national-id clientIdentifier: haip credentials: - id: national_id format: dc+sd-jwt vctValues: - https://credentials.example.org/national-id claims: - path: [given_name] - path: [family_name]listener.bind is the private address your proxy forwards to; the listener speaks plain HTTP.
Every policy names one client identifier, and startup refuses an unknown reference, a duplicate
id, an identifier no policy uses, and two identifiers that yield the same client_id. A policy
whose credential lists no claims requests no claim, which is how a possession-only check
discloses nothing; an empty claims list is refused. Every key has the bounds and default its
schema gives it, and the generated reference lists them.
Choose a client identifier
Section titled “Choose a client identifier”The client identifier is how a wallet recognizes the verifier, and wallets differ in which kinds they accept. Declare the kind your wallet trusts; the verifier signs every request object for that policy with it.
| Type | Wallet sees client_id | Request signed with | Also serves |
|---|---|---|---|
x509-san-dns | x509_san_dns: and the host of publicBaseUrl | ES256, certificate chain in x5c | Nothing |
x509-hash | x509_hash: and the SHA-256 of the leaf certificate | ES256, certificate chain in x5c | Nothing |
decentralized-identifier | decentralized_identifier:did:web:<host> | EdDSA | /.well-known/did.json and /v1/jwks |
pre-registered | The configured clientId, with no colon | EdDSA | /v1/jwks |
An X.509 leaf certificate must name the host of publicBaseUrl as a DNS subject alternative name
(x509-san-dns) and must hold the signing key. A did:web identifier names a host with no port and
no path, and the publicBaseUrl must have no port. A pre-registered identifier works only when
the wallet operator has registered the client identifier, the response URI, and the keys served at
/v1/jwks; the verifier cannot do that registration for you.
Response mode and credential format
Section titled “Response mode and credential format”The verifier speaks one response mode and one credential format.
direct_post.jwtis the only response mode. The wallet encrypts its authorization response to a per-session key the request publishes inclient_metadata, acceptingA128GCMorA256GCMas the policy allows. A wallet that cannot encrypt the response cannot complete the flow; the only unencrypted shape the verifier accepts is an error that echoes the session state.dc+sd-jwt(SD-JWT VC) is the only credential format, asked for through a DCQL query. An mdoc credential is not supported.- The verifier implements OpenID4VP 1.0 and the HAIP 1.0 verifier flow profile. It is not
certified, and no conformance suite result is claimed. Draft wire shapes such as
presentation_definitionare not spoken. - Only the cross-device flow exists: the wallet reads the request over a link or QR code the portal shows. A same-device redirect is not supported.
Pair it with an Evidence definition
Section titled “Pair it with an Evidence definition”The verifier decides nothing about the credential, so an Evidence definition does. Declare the
holder-presented channel on the definition and read the presentation through a Source that uses
the built-in wallet-presentation-v1 host protocol:
acquisition: holderPresented: source: presented-possession selectorProfiles: - wallet-sessionA request’s selector profile selects exactly one acquisition channel, with no fallback to the
other. The selector profile carries the session reference and the presenting principal, and the
principal must be the one that created the session in evidence-oid4vp. The caller states both the
presenting principal and the session reference. Evidence refuses a principal that did not create the
session, but it cannot tell which relying party a session was meant for, so run one relying party
per Evidence deployment that serves wallet channels. The Source’s route sends
Evidence’s read to the verifier’s POST /v1/presentations/read with the Evidence service
credential, whose token the evidenceCredentials block admits.
The wallet-presentation-v1 configuration is pinned by digest in the Adapter pack. It states the
trusted issuers and their keys, the credential types, the claims to disclose, the maximum
credential age, and the subject-link mode, and it carries an explicit issuer deny-list. Holder
binding is always required. Status checking is not available: write status: not-checked, because
required is refused. Startup refuses a holder-presented Source that does not use
wallet-presentation-v1, and a source-channel Source that does.
What Evidence signs is an ordinary assertion on the holder-presented channel; see
how Evidence works for what a
relying party sees and decides. The holderPresented keys are in the generated
Evidence configuration reference.
Check and serve
Section titled “Check and serve”Check the file before the service reads it:
evidence-oid4vp check --runtime-config runtime.yamlevidence-oid4vp --help lists the commands and flags; its command reference is not published until it
is reviewed. check reads that one file and nothing else. It exits 0 when nothing is refused, 1 when something
is refused (or, with --deny-warnings, a warning is reported), 2 for an invalid command line, and
3 when the file cannot be read. It does not prove that the certificate chain and signing key
resolve, match, or are unexpired; serve proves all three when it starts. Then start the service,
and render its HTTP contract if your portal needs it:
evidence-oid4vp serve --runtime-config runtime.yamlevidence-oid4vp openapi --output evidence-oid4vp.openapi.jsonYour portal creates a session at POST /v1/sessions with an applicant token and shows the returned
wallet link; the wallet fetches the request object and posts its encrypted response; Evidence reads
the result. The wallet presentation API reference renders
the same contract. GET /v1/sessions/{reference} reports pending, received, or failed, and
adds a failureCategory when the wallet reported an error; it never returns presented content.
/health answers while the process runs, and /ready tracks the local audit destination and the
client identifier certificates: it answers 503 once an X.509 certificate is past its notAfter,
because wallets refuse every request object signed under it, and the service logs one warning per
identifier at that transition. The certificate is checked for validity at load only, so replace it
and restart before it expires.
The first successful read of a result (200, or the 422 that reports a wallet error) consumes it
and releases its held bytes. Any later read of that reference is the same 404 as an unknown or
expired reference, and two concurrent reads yield exactly one result. A read that does not succeed
(authentication, rate limit, unknown reference, principal mismatch, session still pending, audit
unavailable) consumes nothing. Reads are not retried safely after delivery: if Evidence loses the
response, the portal creates a new session. The audit record of a delivering read has the outcome
delivered_and_consumed (failed_and_consumed for a wallet error).
A session takes one response, and sessions, presentations, and results live in process memory, so
run one replica and expect a restart to end every open session. A response that would pass
sessions.maximumHeldPresentationBytes is refused with 503 and Retry-After, and the session
stays pending. Each endpoint family has a request budget under rateLimits; an exceeded budget is
answered with 429 and Retry-After. The budgets for the request-object fetch and the wallet response
use one key for all wallets, so they bound the service as a
whole and cannot tell one client from another: a single source can exhaust them for everyone.
Rate limit per source address at your ingress in front of those two paths.
Check wallet interoperability
Section titled “Check wallet interoperability”No wallet is verified against this verifier. The repository’s gate runs the verifier and Evidence
together over loopback with a simulated wallet, which proves the flow between the two services and
nothing about a real wallet. A wallet is described as working only by a reviewed device-run receipt
under products/evidence/fixtures/interoperability/oid4vp-device-run/, and none exists yet.
What you can rely on is the wire profile the verifier speaks (above) and what the research expects
each wallet to need. The expectations in the repository’s device-run procedure are unverified for
every wallet it lists, and one is expected to fail: a wallet that presents only mdoc cannot satisfy a
dc+sd-jwt request. To confirm your own wallet, run the repository’s device-run harness against a
deployment you control, or create a session from your portal and present from the wallet while you
watch the verifier’s audit journal and Evidence’s response. A refused or failed session is the
information; the failureCategory the session reports is a closed set that names the wallet’s own
error class.
Configuration reference
Section titled “Configuration reference”evidence-oid4vp runtime configurationSource preview, pre-1.0, 89 key paths
evidence-oid4vp runtime configuration
Source preview, pre-1.0
Generated from products/evidence/generated/oid4vp-runtime/oid4vp-runtime.schema.json. 89 key paths. Explained in docs/site/src/content/docs/configure/evidence-oid4vp.mdx.
| Key path | Type | Required | Accepted values | Constraints |
|---|---|---|---|---|
apiVersionThe format version of this file. | string | Yes |
| None specified |
applicantTokensThe issuer of the applicant tokens a portal presents to create a
session. | object | Yes | Not specified | None specified |
applicantTokens.algorithmsThe signature algorithms a token may use; one family only. | array | No | Not specifiedSchema default, where declared: ["ES256"] |
|
applicantTokens.algorithms[]Array item | string | No |
| None specified |
applicantTokens.allowInsecureHttpPermit plain HTTP beyond loopback to the configured issuer and to a
JWKS URI declared in configuration. TLS remains required unless the
operator explicitly accepts this transport risk. | boolean | No | Not specifiedSchema default, where declared: false | None specified |
applicantTokens.audienceThe audience every accepted access token must carry in aud. | string | Yes | Not specified |
|
applicantTokens.issuerExact issuer accepted in access-token iss claims, an absolute
https URL, or a plain http URL when allowInsecureHttp is set. | string | Yes | Not specified |
|
applicantTokens.jwksSourceWhere the issuer's signing keys come from. Absent reads the issuer's
OpenID Connect discovery document. | object | No | Not specifiedSchema default, where declared: {"type":"discovery"} | None specified |
applicantTokens.jwksSource.documentRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared. | string | Conditional | Not specified |
|
applicantTokens.jwksSource.type | string | Yes |
| None specified |
applicantTokens.jwksSource.uriAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted. | string | Conditional | Not specified |
|
applicantTokens.maximumTokenLifetimeSecondsThe longest lifetime a token may carry, in seconds. | integer | No | Not specifiedSchema default, where declared: 900 |
|
applicantTokens.requiredScopeThe scope every token must carry to create a session. The creating
principal is the token's issuer, subject, and client. | string | Yes | Not specified |
|
auditThe required durable audit journal. | object | Yes | Not specified | None specified |
audit.destinationThe configured audit destination kind, as written in product configuration. | string | No |
"file" | None specified |
audit.hashKeyRefSecret reference to the audit hash key. | string | Yes | Not specified |
|
audit.path | string | No | Not specified |
|
audit.retentionDays | integer | No | Not specified |
|
audit.rotateBytes | integer | No | Not specified |
|
clientIdentifiersHow the service identifies itself to a wallet: one or more client
identifiers, each named by a local id. Every policy names the one it
uses, and every identifier is used by at least one policy. | array | Yes | Not specified |
|
clientIdentifiers[]Array itemOne client identifier. The type member names the choice. | object | No | Not specified | None specified |
clientIdentifiers[].certificateChainRefThe PEM certificate chain, leaf first, as a secret reference. | string | Conditional | Not specified |
|
clientIdentifiers[].clientIdThe client identifier as registered with the wallet operator. It
holds no :, which is what marks it as pre-registered. | string | Conditional | Not specified |
|
clientIdentifiers[].diddid:web: and the host of publicBaseUrl, with no port and no
path. | string | Conditional | Not specified |
|
clientIdentifiers[].idThe identifier policies name in clientIdentifier. | string | Yes | Not specified |
|
clientIdentifiers[].signingKeyRefFor x509-hash or x509-san-dns: The private JWK that signs request objects, as a secret reference.
It must be the leaf certificate's key. For decentralized-identifier or pre-registered: The private Ed25519 JWK (kty OKP, alg EdDSA) that signs request
objects, as a secret reference. | string | Yes | Not specified |
|
clientIdentifiers[].type | string | Yes |
| None specified |
evidenceCredentialsThe issuer of the Evidence service credential that reads a result. | object | Yes | Not specified | None specified |
evidenceCredentials.algorithmsThe signature algorithms a token may use; one family only. | array | No | Not specifiedSchema default, where declared: ["ES256"] |
|
evidenceCredentials.algorithms[]Array item | string | No |
| None specified |
evidenceCredentials.allowInsecureHttpPermit plain HTTP beyond loopback to the configured issuer and to a
JWKS URI declared in configuration. TLS remains required unless the
operator explicitly accepts this transport risk. | boolean | No | Not specifiedSchema default, where declared: false | None specified |
evidenceCredentials.allowedClientThe one client allowed to read results: Evidence's service client. | string | Yes | Not specified |
|
evidenceCredentials.audienceThe audience every accepted access token must carry in aud. | string | Yes | Not specified |
|
evidenceCredentials.issuerExact issuer accepted in access-token iss claims, an absolute
https URL, or a plain http URL when allowInsecureHttp is set. | string | Yes | Not specified |
|
evidenceCredentials.jwksSourceWhere the issuer's signing keys come from. Absent reads the issuer's
OpenID Connect discovery document. | object | No | Not specifiedSchema default, where declared: {"type":"discovery"} | None specified |
evidenceCredentials.jwksSource.documentRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared. | string | Conditional | Not specified |
|
evidenceCredentials.jwksSource.type | string | Yes |
| None specified |
evidenceCredentials.jwksSource.uriAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted. | string | Conditional | Not specified |
|
evidenceCredentials.maximumTokenLifetimeSecondsThe longest lifetime a token may carry, in seconds. | integer | No | Not specifiedSchema default, where declared: 900 |
|
evidenceCredentials.requiredScopeThe scope the Evidence token must carry to read a result. | string | Yes | Not specified |
|
kindThe document kind. | string | Yes |
| None specified |
listenerThe listener a wallet and a portal reach. | object | Yes | Not specified | None specified |
listener.bindSocket address the runtime listens on, written host:port with an IP address host ([addr]:port for IPv6). | string | Yes | Not specified |
|
listener.maximumRequestBytesThe largest request body accepted, in bytes. | integer | No | Not specifiedSchema default, where declared: 16384 |
|
listener.requestTimeoutMillisecondsHow long one request may take, in milliseconds. | integer | No | Not specifiedSchema default, where declared: 5000 |
|
metricsListenerThe optional operator-only metrics listener: a loopback or private
address the service listener does not use. Absent means no metrics
endpoint exists. | object | No | Not specified | None specified |
metricsListener.bindSocket address the runtime listens on, written host:port with an IP address host ([addr]:port for IPv6). | string | Yes | Not specified |
|
policiesWhat a portal may ask a wallet to present. At least one. | array | Yes | Not specified |
|
policies[]Array itemA presentation policy. | object | No | Not specified | None specified |
policies[].acceptedResponseEncryptionThe enc values of an encrypted authorization response the service
accepts and advertises. | array | No | Not specifiedSchema default, where declared: ["A128GCM","A256GCM"] |
|
policies[].acceptedResponseEncryption[]Array itemThe accepted enc values of an encrypted authorization response. | string | No |
| None specified |
policies[].clientIdentifierThe client identifier requests under this policy use: the id of an
entry in clientIdentifiers. | string | Yes | Not specified |
|
policies[].credentialsThe credential queries, 1 to 8. | array | Yes | Not specified |
|
policies[].credentials[]Array itemOne DCQL credential query. | object | No | Not specified | None specified |
policies[].credentials[].claimsThe claims asked for, 1 to 64. Ask only for what the policy needs.
Absent, the query requests no selectively disclosable claim and the
wallet presents the credential with none (OpenID4VP 1.0 section 6.4.1);
an empty list is refused. | array | No | Not specified |
|
policies[].credentials[].claims[]Array itemOne claim a credential query asks for. | object | No | Not specified | None specified |
policies[].credentials[].claims[].pathThe claims path: object member names from the credential root, 1 to 8.
A name may repeat along a path (address, address), so this is an
ordered list, not a set. | array | Yes | Not specified |
|
policies[].credentials[].claims[].path[]Array item | string | No | Not specified |
|
policies[].credentials[].formatThe credential format, dc+sd-jwt. | string | Yes |
| None specified |
policies[].credentials[].idThe query identifier the wallet keys its vp_token by. | string | Yes | Not specified |
|
policies[].credentials[].vctValuesThe credential types (vct) the wallet may present for this query, 1
to 16. | array | Yes | Not specified |
|
policies[].credentials[].vctValues[]Array itemAn identifier another system issues, written exactly as its issuer gives it. | string | No | Not specified |
|
policies[].idThe policy identifier a portal names when it creates a session. | string | Yes | Not specified |
|
policies[].trustedAuthoritiesTrusted authority hints applied to every credential query. Omit for a
wallet that does not support them. | array | No | Not specified |
|
policies[].trustedAuthorities[]Array itemA trusted authority hint passed to the wallet for minimisation. The service
still decides trust itself. | object | No | Not specified | None specified |
policies[].trustedAuthorities[].typeThe authority type. | string | Yes |
| None specified |
policies[].trustedAuthorities[].valuesAuthority Key Identifiers, base64url, 1 to 16. | array | Yes | Not specified |
|
policies[].trustedAuthorities[].values[]Array itemAn identifier another system issues, written exactly as its issuer gives it. | string | No | Not specified |
|
publicBaseUrlThe public origin wallets reach this service at: a bare https origin
with no path, query, or fragment. The response URI and the request URI
are built from it. A trailing slash is removed. | string | Yes | Not specified |
|
rateLimitsRequest-rate limits. | object | No | Not specified | None specified |
rateLimits.evidenceReadBurstThe most result reads one Evidence client may make in a burst. | integer | No | Not specifiedSchema default, where declared: 100 |
|
rateLimits.evidenceReadsPerMinuteResult reads per minute for one Evidence client. | integer | No | Not specifiedSchema default, where declared: 600 |
|
rateLimits.responseBurstPerRequestThe most response attempts against one session in a burst. | integer | No | Not specifiedSchema default, where declared: 6 |
|
rateLimits.responsesPerRequestPerMinuteResponse attempts per minute against one session. | integer | No | Not specifiedSchema default, where declared: 12 |
|
rateLimits.sessionCreationBurstThe most sessions one principal may create in a burst. | integer | No | Not specifiedSchema default, where declared: 10 |
|
rateLimits.sessionCreationsPerPrincipalPerMinuteSessions one principal may create per minute. | integer | No | Not specifiedSchema default, where declared: 30 |
|
rateLimits.walletRequestBurstThe most wallet requests admitted in a burst. | integer | No | Not specifiedSchema default, where declared: 100 |
|
rateLimits.walletRequestsPerMinuteRequest object fetches per minute, across all wallets. | integer | No | Not specifiedSchema default, where declared: 600 |
|
rateLimits.walletResponseBurstThe most wallet responses admitted in a burst. | integer | No | Not specifiedSchema default, where declared: 100 |
|
rateLimits.walletResponsesPerMinuteWallet responses per minute across all sessions. Every attempt counts,
including those for unknown request identifiers. | integer | No | Not specifiedSchema default, where declared: 600 |
|
secretProvidersThe secret providers the *Ref members may name. | object | Yes | Not specified | None specified |
secretProviders.environmentEnables secret:env/NAME references, read from the process
environment. Declared as an empty mapping: environment: {}. | object | Conditional | Not specified | None specified |
secretProviders.fileEnables secret:file/name references, read from files under root. | object | Conditional | Not specified | None specified |
secretProviders.file.rootAbsolute directory holding one file per secret. Each file must be a
regular file owned by the runtime user, mode 0400 or 0600, with exactly
one hard link. | string | Conditional | Not specified |
|
sessionsThe bounds of the in-memory session store. | object | No | Not specified | None specified |
sessions.maximumHeldPresentationBytesThe most presentation bytes the store holds across all sessions, from
the moment a response is accepted until its session ends or its result
expires. A response that would exceed it is refused with a retryable
503; the sessions already holding results are unaffected. | integer | No | Not specifiedSchema default, where declared: 268435456 |
|
sessions.maximumLiveSessionsHow many sessions the store holds. A full store refuses new sessions
rather than forgetting live ones. | integer | No | Not specifiedSchema default, where declared: 4096 |
|
sessions.resultTtlSecondsHow long an accepted presentation is held for Evidence to read, in
seconds. | integer | No | Not specifiedSchema default, where declared: 1800 |
|
sessions.sessionTtlSecondsHow long a session waits for the wallet's response, in seconds. The
request object expires with it. | integer | No | Not specifiedSchema default, where declared: 300 |
|
Cookies on Registry Stack
We use optional cookies to understand how you use the main Registry Stack website and this documentation. If you contact us, we may link this activity to your enquiry. You can change your choice at any time. Read the privacy policy.
Your browser asks sites not to track you, so optional tracking stays off.
We could not use or save that choice. Optional tracking remains off.