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

  ▸ DCQL queries, user profiles and logging


1) Response validation

A profile is created only after the complete response passes validation.

With OpenId4VpClient and EudiWalletClient, the response is validated twice: when the wallet posts it, and again when the browser comes back. The response URI is not authenticated, and its transaction identifier is visible in the wallet URL, thus in the QR code: a post which does not validate is refused on arrival and leaves the transaction open for the wallet’s own answer. This goes beyond what OpenID4VP section 14.3.2 asks. Two limits remain with direct_post: a wallet error cannot be authenticated, so whoever sees the QR code can still end the transaction with one; and nothing ties the wallet which answers to the browser which started the flow, so a valid presentation made by another wallet for this very request is accepted. This is the session fixation described in OpenID4VP section 14.2, which the Digital Credentials API avoids.

Validation uses the saved request, even if the configuration changes afterwards. Verified results are available through VerifiablePresentationCredentials.getVerifiedCredentials(): each query identifier maps to a list of credentials.

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. The configuration provides a SdJwtVcVerifier by default, but it accepts no issuer until trusted keys or certificate trust anchors are configured. The same registration mechanism applies to OpenId4VpClient, OpenId4VpDcApiClient and EudiWalletClient. For implementation requirements, see Custom verifiers.

To use SdJwtVcVerifier, explicitly add the following EUDI dependency, which is not included by default:

<dependency>
    <groupId>eu.europa.ec.eudi</groupId>
    <artifactId>eudi-lib-jvm-sdjwt-kt</artifactId>
    <version>0.20.1</version>
</dependency>

Without this dependency, SdJwtVcVerifier throws an OpenId4VpException when validating a credential, with a message identifying the dependency to add. Applications replacing it with their own implementation do not need EUDI.

2.1) Built-in SD-JWT VC verifier

2.1.1) Trusted issuers

Choose how to trust the credential issuer:

// For credentials with iss:
var verifier = new SdJwtVcVerifier().setTrustedIssuers(Map.of(
    "https://issuer.example", new SdJwtVcTrustedIssuer(JWKSet.parse(trustedIssuerJwksJson))));

// For credentials with x5c whose iss, if any, is not configured above; both settings can coexist:
verifier.setTrustStore(new KeystoreProperties()
    .setKeystorePath("classpath:trusted-issuers.p12")
    .setKeyStoreType("PKCS12")
    .setKeystorePassword("changeit"));

config.addCredentialVerifier(verifier);

Use a JKS (default) or PKCS12 truststore with trusted certificate entries. An optional keyStoreAlias restricts trust to one entry. No private key, private-key password or automatic keystore generation is needed. Certificates supplied by the wallet are never automatically trusted. The trust anchors are read at the first certificate validation and kept: they are read again only when the truststore’s resource, password, type or alias change, or when setTrustStore(...) is called again, which is the way to pick up a truststore file replaced on disk.

When the iss value is configured in trustedIssuers, its keys verify the signature; kid selects a key when present. This mode always takes precedence over x5c for that iss: “A Verifier MUST ensure that for any given iss value, an attacker cannot influence the type of verification process used” (SD-JWT VC, section 7.3). Otherwise, the verifier validates the leaf-first certificate chain against the truststore, checks validity and signing usage, then verifies the JWT with the leaf key. Necessary intermediates must be in x5c. VerifiedCredential.issuer becomes the certificate subject (RFC 2253), even when an iss claim is present: “the Issuer of the Verifiable Digital Credential is the subject of the end-entity certificate” (SD-JWT VC, section 2.5). The iss claim is then kept as a claim; no iss claim is invented when absent.

Certificate revocation checks are enabled by default. Supply local CRLs with setCertificateRevocationLists(List<X509CRL>); unavailable or revoked status causes rejection. For a test PKI without revocation data, explicitly use setCertificateRevocationEnabled(false). Credential status checking remains separate (see below).

Both modes require typ=dc+sd-jwt and vct. No issuer is trusted by default. EUDI verifies disclosure hashes and reconstructs nested objects and arrays, checks exp/nbf, and verifies a present key-binding JWT, including its signature, typ=kb+jwt and sd_hash.

2.1.2) Signature algorithms and holder binding

The holder proof must match the nonce and audience from the transaction’s saved request. For URL flows the audience is the full client_id; for the Digital Credentials API it is origin: followed by one of the saved expected_origins. The proof’s iat must fall between transaction creation and the current time, with a 30-second tolerance configurable through clockSkewSeconds. Credential expiration is checked without that tolerance. Issuer and holder signature algorithms default to ES256 and can be configured with issuerAlgorithms and holderAlgorithms; symmetric algorithms are rejected.

A presentation without key binding is reported as such and rejected by the common DCQL validator unless the query explicitly sets require_cryptographic_holder_binding to false. A present but invalid proof is always rejected.

2.1.3) Trusted authorities

Authority evidence may be configured on SdJwtVcTrustedIssuer.setTrustedAuthorities(...) only after establishing the corresponding trust relationship; otherwise it remains empty and a DCQL authority requirement cannot be satisfied. In certificate mode, once the x5c chain is validated against the truststore, the verifier reports the aki evidence itself: the key identifiers of the AuthorityKeyIdentifier extensions of the chain’s certificates, in base64url, as OpenID4VP 1.0, section 6.1.1.1 defines them. A query asking for {"type": "aki", "values": ["<root key identifier>"]} thus accepts the credentials issued under that CA. Other authority types (etsi_tl, openid_federation) are never inferred from a certificate.

2.1.4) Credential status

Credential status is not checked automatically in this initial implementation. A credential containing a status claim is rejected unless SdJwtVcVerifier.setStatusChecker(...) is configured. This checker receives the cryptographically verified credential, including its reconstructed claims, and must throw if status is invalid, unsupported or cannot be checked. If it uses a status list, it must authenticate that list and check its validity and the credential’s status entry. A credential without a status claim does not invoke the checker.

2.2) Remote resources and application responsibilities

pac4j does not automatically download issuer metadata, keys or credential status lists:

An application may obtain these resources separately and provide trusted keys and a status checker, or register a different CredentialVerifier that manages retrieval. Obtaining a key from an issuer’s endpoint does not by itself establish that the application should trust that issuer.

This is a minimal SD-JWT integration, not a complete EUDI trust-list or HAIP validation implementation. No built-in mdoc verifier is provided yet. All format verifiers remain replaceable through addCredentialVerifier.

2.3) Custom verifiers

Implement CredentialVerifier and register it with config.addCredentialVerifier(...):