OpenID4VP / mdoc verifier (v6.6)

MdocVerifier validates the mso_mdoc presentations (ISO/IEC 18013-5 mobile documents, such as the EUDI PID or a mobile driving licence): the issuer signature and trust, the attribute digests and the device signature bound to the transaction. It is registered by default, but trusts no issuer until you configure trusted issuers.

See also:

  ▸ OpenID4VP overview

  ▸ Response validation and verifiers

  ▸ SD-JWT VC verifier

  ▸ DCQL queries, user profiles and logging


1) Dependencies

1.1) walt.id

The verifier relies on walt.id 0.11.0, which is optional and published in the walt.id Maven repository, outside Maven Central:

<repositories>
    <repository>
        <id>waltid-releases</id>
        <url>https://maven.waltid.dev/releases</url>
        <snapshots><enabled>false</enabled></snapshots>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>id.walt.mdoc-credentials</groupId>
        <artifactId>waltid-mdoc-credentials-jvm</artifactId>
        <version>0.11.0</version>
    </dependency>
</dependencies>

1.2) Kotlin alignment

Align the Kotlin dependencies in your application’s dependencyManagement, especially when you also use the SD-JWT VC verifier. This combination is tested with Java 17, EUDI SD-JWT 0.20.1 and walt.id 0.11.0:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-bom</artifactId>
            <version>2.2.21</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-coroutines-bom</artifactId>
            <version>1.10.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-serialization-bom</artifactId>
            <version>1.9.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-datetime-jvm</artifactId>
            <version>0.6.1</version>
        </dependency>
    </dependencies>
</dependencyManagement>

2) Quick start

// trustedIssuers: see "Trusted issuers" in Response validation and verifiers
config.addCredentialVerifier(new MdocVerifier().setTrustedIssuers(trustedIssuers));
config.setDcqlQuery(EudiPidQuery.mdoc(GIVEN_NAME, PERSONAL_ADMINISTRATIVE_NUMBER));
config.setProfileIdResolver(...); // required, see "Claims and profile identifier" below

3) What is verified

4) Holder binding

The device signature covers a SessionTranscript rebuilt from the transaction’s saved request, as defined by OpenID4VP 1.0, appendix B.2.6, for both URL/QR code and Digital Credentials API clients:

Element URL / QR code flow (OpenID4VPHandover) Digital Credentials API (OpenID4VPDCAPIHandover)
Verifier identity the full client_id one of the saved expected_origins
nonce the request nonce the request nonce
Encryption key JWK thumbprint of the response encryption key if the response is encrypted, otherwise null same
Response URI the response_uri —

A presentation signed for another request, another verifier or another encryption key is rejected. Unlike SD-JWT VC, an mdoc presentation always carries a device signature: there is no presentation without holder binding.

5) Supported presentations

Supported Not supported (rejected)
DeviceSignature DeviceMAC
ES256 (COSE algorithm -7) with P-256 keys, for issuer and device other algorithms and curves
issuer-signed claims device-signed claims
  transaction data

6) Claims and profile identifier

The claims are grouped by namespace: VerifiedCredential.getClaims() maps each namespace to a map of its issuer-signed elements. Dates are ISO text, byte strings unpadded base64url, and maps and lists keep their nesting (map keys must be strings). For the EUDI PID, the namespace is the doctype:

{
  "eu.europa.ec.eudi.pid.1": {
    "given_name": "Erika",
    "birth_date": "1964-08-12",
    "personal_administrative_number": "..."
  }
}

In the DCQL query, an mdoc claim path is the namespace followed by the element name.

You must configure your own profile identifier resolver: ProfileIdResolver.issuerAndClaim(...) only reads top-level SD-JWT VC claims and refuses, at initialization, a query without SD-JWT VC credential. Request a namespace-qualified claim which identifies the user (a name alone does not) and read it, for example:

config.setProfileIdResolver(credentials -> {
    VerifiedCredential pid = credentials.getVerifiedCredentials().get(EudiPidQuery.PID).get(0);
    var namespace = (Map<?, ?>) pid.getClaims().get(EudiPidProfileDefinition.PID_DOCTYPE);
    return pid.getIssuer() + "|" + namespace.get(EudiPidProfileDefinition.PERSONAL_ADMINISTRATIVE_NUMBER);
});

Check that the PID provider issues that claim, and override check(DcqlQuery) to verify the query at startup.

7) Credential status

Credential status is not checked automatically, and no status list is fetched. When an authenticated MSO contains a status object, configure setStatusChecker((credential, status) -> { ... }):

Without a checker, credentials containing a status are rejected.

8) Scope and limits

This is format verification, not a complete EUDI trust-list, certificate-profile or HAIP conformance implementation. walt.id provides the parsing, the COSE verification and the MSO digest checks; pac4j supplies the transaction binding, the issuer trust and the profile mapping. The digest check is invoked separately for every item, because walt.id 0.11.0’s bulk method returns after the first item.