OpenID4VP / Response validation and verifiers (v6.6)

Receiving a wallet response is only the first step. Before creating a user profile, pac4j checks the exchange, asks the credential verifiers to validate the presentations, and checks that the results satisfy the original DCQL query. Here is how to configure that validation in your Java application.

See also:

  ▸ OpenID4VP overview

  ▸ Clients and configuration

  ▸ SD-JWT VC verifier

  ▸ mdoc verifier

  ▸ DCQL queries, user profiles and logging


1) Response validation

A profile is created only after the whole response passes three levels of checks, always against the request saved with the transaction, even if the configuration has changed since:

Level Done by Checks
Exchange pac4j transaction expiry, response mode, response encryption, state, well-formed and consistent parameters
Presentations the credential verifier of each format signatures, issuer trust, holder binding, credential status
DCQL query pac4j credential types, requested claims and values, claim and credential sets, multiplicity, trusted authorities, and holder binding unless the query disables it

The verified credentials are available through VerifiablePresentationCredentials.getVerifiedCredentials(): each query identifier maps to a list of credentials.

The state binds the response to the request when a wallet is invoked by URL: a fresh one is sent with every request and must come back unchanged, since without holder binding no nonce is returned (OpenID4VP section 5.3). The Digital Credentials API maintains that binding itself and carries no state.

With a wallet invoked by URL (OpenId4VpClient, EudiWalletClient), the response URI is not authenticated and its transaction identifier is visible in the wallet URL, thus in the QR code. The response is therefore validated when the wallet posts it, without any session: a post which does not validate is refused with a 400 and leaves the transaction open for the wallet’s own answer (beyond what section 14.3.2 asks). The result of this validation is stored with the response: when the user’s browser calls the callback URL to claim it, its web session identifies the transaction and the profile built at reception is used as is.

With setUseRedirectUri(true), the wallet is also given a fresh response_code in the callback URL returned as redirect_uri. The browser must present that code in the session which started the transaction before the callback can consume the response and save the profile. A missing or incorrect code is refused; a code from another session cannot claim its presentation, and a transaction already consumed cannot be used again. The callback then restores the saved request automatically. See Returning to the application.

For the URL binding, keep these limits in mind:

2) Configuring credential verifiers

Register a CredentialVerifier for each credential format requested by your DCQL query using config.addCredentialVerifier(verifier). Registration replaces the verifier for that format, and a query whose format has no verifier is refused at initialization. The same registration mechanism applies to OpenId4VpClient, OpenId4VpDcApiClient and EudiWalletClient.

The configuration provides one built-in verifier per format. Neither accepts an issuer until trust is configured, and each relies on an optional library to add to your application:

Format DCQL format Built-in verifier Optional library Documentation
SD-JWT VC dc+sd-jwt SdJwtVcVerifier EUDI eudi-lib-jvm-sdjwt-kt SD-JWT VC verifier
mdoc (ISO/IEC 18013-5) mso_mdoc MdocVerifier walt.id waltid-mdoc-credentials-jvm mdoc verifier

Add only the libraries of the formats your application requests; when using both, align the Kotlin dependencies (see Kotlin alignment).

3) Trusted issuers

No issuer is trusted by default. Give each verifier its TrustedIssuers, built with trusted issuer definitions, implementations of TrustedIssuer. Two are provided, which do not trust the same thing:

  KeysTrustedIssuer CertificateTrustedIssuer
Created with new KeysTrustedIssuer(iss, jwks) new CertificateTrustedIssuer(trustStore)
What you configure the issuer’s own signing keys CA certificates, never the issuers’ keys
Where the verifying key comes from the configuration the credential: the leaf of its x5c / x5chain, accepted because its chain leads to a trust anchor
Trust direct: the key is pinned delegated: the CA vouches for issuers not known in advance
How many issuers one, named all those of the CA, including future ones
Issuer reported the configured iss the leaf certificate subject (RFC 2253)
Checks the signature only validity, key usage, basic constraints, revocation
Trusted authorities configured with setTrustedAuthorities(...) aki of the validated chain
Lookup by identifier by certificate chain
Formats SD-JWT VC SD-JWT VC, mdoc

A truststore thus holds trust anchors: one of its keys never verifies a credential directly, it verifies the certificate of an issuer, which carries the key. To pin the key of one issuer, use a KeysTrustedIssuer. Keeping both kinds apart is also what makes the resolution order safe: for a given iss, the verification process never depends on what the credential carries.

Add a definition to a verifier with addTrustedIssuer(...), or share the same TrustedIssuers between several verifiers with setTrustedIssuers(...): each one ignores the definitions which do not support its format.

var trustedIssuers = new TrustedIssuers(
    new CertificateTrustedIssuer(new KeystoreProperties("classpath:pid-providers.p12")
            .setKeyStoreType("PKCS12")
            .setKeystorePassword("changeit"))
        .setCertificateRevocationLists(List.of(new ResourceProperties("classpath:pid-providers.crl"))),
    new KeysTrustedIssuer("https://issuer.example", new JwksProperties("classpath:partner.jwks")));

config.addCredentialVerifier(new SdJwtVcVerifier().setTrustedIssuers(trustedIssuers));
config.addCredentialVerifier(new MdocVerifier().setTrustedIssuers(trustedIssuers));

3.1) Configuration

KeysTrustedIssuer: the static counterpart of the issuer metadata, which pac4j does not download.

CertificateTrustedIssuer:

3.2) Resolution order

The definitions supporting the credential format are consulted by identifier first, then the others in the order they were added. The first one which knows the issuer decides, and its decision is final:

3.3) Other sources of trust

Implement TrustedIssuer for another ecosystem (trusted list, federation, issuer metadata): from the IssuerEvidence of a credential (identifier, kid, certificate chain), return the ResolvedIssuer (name, keys, trusted authorities), empty if the issuer is unknown, or throw to reject it. Override isIdentifierBased() and supports(format) as needed.

4) Application responsibilities

The built-in verifiers perform format verification, not a complete EUDI trust-list or HAIP validation. pac4j does not download issuer metadata, keys, trust lists or credential status lists:

An application may obtain these resources separately, or register a custom verifier which manages them.

5) Custom verifiers

Implement CredentialVerifier and register it with config.addCredentialVerifier(...). Extend AbstractCredentialVerifier to get the trusted issuers of the built-in verifiers: its resolveIssuer(...) applies the resolution order to your format.