Skip to main content

Implementer guide — implementing PID presentations with OpenID4VP

Audience: Relying Parties implementing an OpenID4VP flow to enable the consumption of a PID credential in the German EUDI Wallet Ecosystem.


Contents

  1. Introduction
  2. Concepts and Validation Layers
  3. Building a PID Presentation Request
  4. Interpreting the PID Presentation Response

1. Introduction

This guide targets implementers of OpenID4VP to support PID presentations in the German EUDI Ecosystem. Following this short introduction, Chapter 2 explains the security concepts and validation layers that make PID presentations trustworthy. Chapter 3 shows the structure of a PID Presentation Request and how to construct one correctly. Chapter 4 explains the response structure and provides a complete validation checklist.

The specification that forms the basis for this documentation can be found in the Blueprint for the German EUDI Wallet Ecosystem.

Throughout this guide, rules or optionality specific to Germany will be called out. Notes have been added with information specific to Sandbox participation.

2. Concepts and Validation Layers

This chapter explains the foundations of PID (Person Identification Data) presentation as defined by the ARF PID Rulebook and realized through the OpenID4VP High Assurance Interoperability Profile. It covers the security concepts that make PID presentations trustworthy, then explains the validation layers that implement these concepts in practice.

2.1 Trust Anchors and Governance

Everything in PID verification begins with the trust framework. Before any cryptographic validation is done, the verifier must know which issuers it is allowed to trust and which certificate hierarchies govern PID issuance. This prevents the acceptance of PID data from unrecognized or malicious sources. In the eIDAS context, this usually means the use of trust lists.

German PID Provider

In Germany, there is only one PID Provider (Bundesdruckerei). For the purpose of Sandbox testing, we provide mock trust lists

2.2 Authenticity of PID Credentials

Once trust anchors are known, the verifier evaluates whether the PID itself was authentically issued. PIDs include signatures created by the issuing authority. Validating these signatures against the keys from the trust framework establishes that the data presented by the wallet corresponds to a genuine PID record and has not been modified since issuance. This ensures that the identifier information originates from the correct governmental entity and is in an unaltered state.

2.3 Holder Binding

Even if the PID was authentically issued, the verifier must ensure that the wallet instance presenting it is the same wallet that the PID was issued into. This is called Holder Binding or Key Binding. A detailed discussion can be found in the SD-JWT specification; it is applicable in general to all credential formats.

During the presentation, the wallet signs the presented credential along with information bound to the particular presentation (see below) in the VP Token. The signature is performed with a private key that is tied to the secure wallet instance and for which the corresponding public key is contained in the Issuer-signed credential. This ensures the presentation comes from a valid wallet on a valid device, not from an exported credential or a cloned environment.

Without holder binding, an intercepted presentation, a malicious verifier, a cloned backup, or a compromised device could be used to impersonate someone. The holder-binding bridges the gap between "this is a valid PID" and "this PID is being presented by the right person". The signature produced by the wallet cannot be replayed or forged without access to the protected key material.

2.4 Session and Transaction Binding

A high-assurance PID presentation must be tied explicitly to the verifier’s session. The verifier generates a cryptographically strong nonce and embeds it in the presentation request. The wallet then includes this nonce in the proof-of-possession signature along with information about the verifier's identity and a timestamp. When the verifier later validates the response, it confirms that the presentation is fresh and could only have been created in response to its own request, mitigating phishing and replay attacks.

2.5 Selective Disclosure and Data Minimization

The ARF mandates that verifiers request only the attributes they genuinely need. HAIP and the underlying credential formats (SD-JWT or mDoc) allow the wallet to disclose only these attributes selectively. The verifier cannot collect anything outside its stated purpose, and over disclosed elements should be ignored.

Validation Philosophy

Validation should follow a fail-fast, layered approach:

  • Foundational checks first (trust, transport, session binding)
  • Cryptographic assurance next (credential validity, holder binding)
  • Privacy and policy enforcement next (selective disclosure)
  • Business logic last

If any layer fails, validation should stop immediately. Continuing after a critical failure increases attack surface and risks incorrect authorization decisions.


Layer 1: Trust and Transport

Before interpreting any protocol data, the verifier must establish a trusted baseline.

At this layer, the verifier conceptually validates that:

  • Trust anchors for PID issuers and wallet providers are loaded from an official, trusted registry
  • Trust lists are authentic, fresh, and correctly parsed
  • Communication occurs over secure transport (HTTPS with valid TLS)
  • The received payload is of the expected type and within reasonable size limits

Layer 2: Session Binding

The verifier must ensure that the response is cryptographically bound to its own request.

At this layer, the verifier validates that:

  • A nonce is present and exactly matches the verifier-issued nonce
  • The audience (aud) identifies the verifier and matches the request
  • Timestamps (iat, exp) are present, valid, and within an acceptable freshness window
  • The wallet follows the verifier's redirect_uri after posting a response

This layer prevents replay attacks, phishing, and cross-session injection.


Layer 3: Holder Binding

Even a valid PID credential is insufficient unless it is presented by its rightful holder.

At this layer, the verifier conceptually validates that:

  • A proof-of-possession mechanism is present
  • The proof is cryptographically valid
  • The proof binds the presentation to the verifier’s nonce, the verifier’s identifier (audience), and the presented credential.
  • The signing key is correctly associated with the PID’s holder binding mechanism

Layer 4: Credential Assurance

The verifier must confirm that the presented PID credential itself is genuine and valid.

At this layer, the verifier validates that:

  • A PID credential is present and extractable from the presentation
  • The credential format is supported (SD-JWT or mDoc)
  • Issuer signatures are valid
  • The issuer is trusted according to the loaded trust anchors
  • The credential is within its validity period and not revoked
  • The credential type matches what was requested

Layer 5: Selective Disclosure and Data Minimization

PID verification is subject to strict data minimization requirements.

At this layer, the verifier validates that:

  • Only attributes explicitly requested are disclosed
  • All mandatory attributes for the transaction are present
  • No additional credentials are included
  • The disclosed credential combination matches one of the allowed credential sets

Layer 6: Business Rules and Policy Enforcement

Only after all assurance layers succeed should the verifier apply business logic.

At this layer, the verifier validates that:

  • Required attributes are present and usable
  • Attribute values satisfy business requirements
  • Data quality and completeness meet application expectations
  • A clear authorization- and or business decision can be derived and audited
Wallet Integrity

Wallet integrity is established during the issuance process through a Wallet Unit Attestation. The issuer ensures that the PID is only issued into valid wallets. During presentation, the verifier relies on that established trust implicitly.


3. Building a PID Presentation Request

This chapter explains the structure of a PID Presentation Request and how to construct one correctly. It covers what a wallet receives when it calls a request_uri and how to understand a HAIP-aligned PID Presentation Request. The examples below are illustrative but structurally realistic for the German PID profile.


3.1 HTTPS Response When Dereferencing request_uri

When the wallet fetches the Presentation Request referenced by the request_uri, the verifier responds with a JSON object containing the actual Presentation Request. In HAIP-aligned deployments, this request is a signed Pushed Authorization Request (PAR).

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
eyJ0eXAiOiJvYXV0aC1hdXRoei1yZXErand0IiwiYWxnIjoiRVMyNTYiLCJ4NWMiOlsiTUlJQ2N6Q0NBaGlnQXdJQkFnSVVPeEQ3SkZrS1lnRlBrOEozWm1Tc0VDMkJIazR3Q2dZSUtvWkl6ajBFQXdJd0tERUxNQWtHQTFVRUJoTUNSRVV4R1RBWEJnTlZCQU1NRUVkbGNtMWhiaUJTWldkcGMzUnlZWEl3SGhjTk1qWXdNVEkzTURreU9ETTFXaGNOTWpjd01USTNNRGt5T0RNMVdqQmZNUXN3Q1FZRFZRUUdFd0pFUlRFWU1CWUdBMVVFQ2d3UFJWVkVTU0JRYkdGNVozSnZkVzVrTVJ3d0dnWURWUVJoREJORVJTNDBPRGN6TlRKRE1FWTJNVGN4UTBRek1SZ3dGZ1lEVlFRRERBOUZWVVJKSUZCc1lYbG5jbTkxYm1Rd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFSdEFiL3d2MXYyTXJHKzNDSE1sQmFnQXgwQ1NXL0IzNUZWQThkTC9DdjRYWG81ZU9xeHl0c1dlQThmbHFLamRhNjBnTTdnWVF4MUpWMEJ3akRQbVIwK280SG9NSUhsTUF3R0ExVWRFd0VCL3dRQ01BQXdIUVlEVlIwT0JCWUVGRXlpMG1VSFRnVmFCTDRHRS9ZeTlRQ1EvMXdHTUI4R0ExVWRJd1FZTUJhQUZLbkNvOW92YmF4VTdzNjVUdWdzeVN3QWc0QXpNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVNCZ05WSFNVRUN6QUpCZ2NvZ1l4ZEJRRUdNQ1VHQTFVZEVRUWVNQnlDR25Cc1lYbG5jbTkxYm1RdVpYVmthUzEzWVd4c1pYUXViM0puTUVvR0ExVWRId1JETUVFd1A2QTlvRHVHT1doMGRIQnpPaTh2YzJGdVpHSnZlQzVsZFdScExYZGhiR3hsZEM1dmNtY3ZZWEJwTDNOMFlYUjFjeTF0WVc1aFoyVnRaVzUwTDJOeWJEQUtCZ2dxaGtqT1BRUURBZ05KQURCR0FpRUFrbmsydkd0MUw4cGU2RXR0elhKMlIxdHVlRm5kQ1RVNDJFYUlSbGo5MlRvQ0lRRGova0ZmcldpN2p5UXRLbVNLb3JIY0JsWGluV09uRGtXLzZwQU5tZ1hHZGc9PSJdfQ.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJjbGllbnRfaWQiOiJ4NTA5X2hhc2g6ZlF1b2JWd0p2MDAwdkRXY010cmlYUHpvMnNQVG01X01wMTBPODdsQ3FjRSIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vcGxheWdyb3VuZC5ldWRpLXdhbGxldC5vcmcvZXVkaXBsby8zYWE0NzA2Yy02ZjM1LTQ3YjgtOGEwNS02YTBhMzM0YzMwMWQvb2lkNHZwIiwicmVzcG9uc2VfbW9kZSI6ImRpcmVjdF9wb3N0Lmp3dCIsIm5vbmNlIjoiODVkNWUyNGEtNzE2My00ZTQ0LWEwZWQtOTRiZTk4Y2FiNjY2IiwiZGNxbF9xdWVyeSI6eyJjcmVkZW50aWFscyI6W3siaWQiOiJwaWQtc2Qtand0IiwiZm9ybWF0IjoiZGMrc2Qtand0IiwiY2xhaW1zIjpbeyJwYXRoIjpbImdpdmVuX25hbWUiXX0seyJwYXRoIjpbImZhbWlseV9uYW1lIl19LHsicGF0aCI6WyJiaXJ0aGRhdGUiXX0seyJwYXRoIjpbImFkZHJlc3MiLCJzdHJlZXRfYWRkcmVzcyJdfSx7InBhdGgiOlsiYWRkcmVzcyIsInBvc3RhbF9jb2RlIl19LHsicGF0aCI6WyJhZGRyZXNzIiwibG9jYWxpdHkiXX0seyJwYXRoIjpbImFkZHJlc3MiLCJjb3VudHJ5Il19LHsicGF0aCI6WyJuYXRpb25hbGl0aWVzIl19XSwibWV0YSI6eyJ2Y3RfdmFsdWVzIjpbInVybjpldWRpOnBpZDpkZToxIl19fSx7ImlkIjoicGlkLW1zby1tZG9jIiwiZm9ybWF0IjoibXNvX21kb2MiLCJjbGFpbXMiOlt7InBhdGgiOlsiZXUuZXVyb3BhLmVjLmV1ZGkucGlkLjEiLCJnaXZlbl9uYW1lIl19LHsicGF0aCI6WyJldS5ldXJvcGEuZWMuZXVkaS5waWQuMSIsImZhbWlseV9uYW1lIl19LHsicGF0aCI6WyJldS5ldXJvcGEuZWMuZXVkaS5waWQuMSIsImJpcnRoX2RhdGUiXX0seyJwYXRoIjpbImV1LmV1cm9wYS5lYy5ldWRpLnBpZC4xIiwicmVzaWRlbnRfc3RyZWV0Il19LHsicGF0aCI6WyJldS5ldXJvcGEuZWMuZXVkaS5waWQuMSIsInJlc2lkZW50X3Bvc3RhbF9jb2RlIl19LHsicGF0aCI6WyJldS5ldXJvcGEuZWMuZXVkaS5waWQuMSIsInJlc2lkZW50X2NpdHkiXX0seyJwYXRoIjpbImV1LmV1cm9wYS5lYy5ldWRpLnBpZC4xIiwicmVzaWRlbnRfY291bnRyeSJdfSx7InBhdGgiOlsiZXUuZXVyb3BhLmVjLmV1ZGkucGlkLjEiLCJuYXRpb25hbGl0eSJdfV0sIm1ldGEiOnsiZG9jdHlwZV92YWx1ZSI6ImV1LmV1cm9wYS5lYy5ldWRpLnBpZC4xIn19XSwiY3JlZGVudGlhbF9zZXRzIjpbeyJvcHRpb25zIjpbWyJwaWQtc2Qtand0Il0sWyJwaWQtbXNvLW1kb2MiXV19XX0sImNsaWVudF9tZXRhZGF0YSI6eyJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IkVDIiwieCI6IlNoVTRGcjNOSDd2OVRPQWM5YVlpdTllaWNka2ZWVDllY1ZDUGFQZ0pyTXMiLCJ5IjoiaVYwVlhBU3lsUjBxV29Ecl9tS1VXd3pvLU01OVd6M1FCenBDbTRvaVhUMCIsImNydiI6IlAtMjU2IiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImE0MjBlZTgzLWVjZmEtNDRmYy1iYjE2LTgwMzIwZDg3Zjc0NSJ9XX0sInZwX2Zvcm1hdHNfc3VwcG9ydGVkIjp7Im1zb19tZG9jIjp7ImFsZyI6WyJFUzI1NiIsIkVkMjU1MTkiXX0sImRjK3NkLWp3dCI6eyJrYi1qd3RfYWxnX3ZhbHVlcyI6WyJFUzI1NiIsIkVkMjU1MTkiXSwic2Qtand0X2FsZ192YWx1ZXMiOlsiRVMyNTYiLCJFZDI1NTE5Il19fSwiZW5jcnlwdGVkX3Jlc3BvbnNlX2VuY192YWx1ZXNfc3VwcG9ydGVkIjpbIkExMjhHQ00iXX0sInN0YXRlIjoiM2FhNDcwNmMtNmYzNS00N2I4LThhMDUtNmEwYTMzNGMzMDFkIiwiYXVkIjoiaHR0cHM6Ly9zZWxmLWlzc3VlZC5tZS92MiIsImV4cCI6MTc2OTUxMzY5NywiaWF0IjoxNzY5NTEwMDk3fQ.2VHx61tIyHCisIXq3v_QoDpOxMBVHs7R5K0qOlIRwc5FgP-O0Q0qVhBgt6Sr-1SmN9u2jinH4Fx7lIav5J4p6g
}

The wallet receives a single signed request object rather than individual parameters. The signature allows the wallet to verify authenticity, and cache-control headers prevent reuse.

3.2 Example PID Presentation Request (Decoded)

Below is the decoded content of the Presentation Request contained in the signed request object. Line breaks and formatting are added for readability. Each field is annotated with a number (1) that corresponds to the explanation table below.

{
"response_type": "vp_token", // (1)
"client_id": "x509_hash:fQuobVwJv000vDWcMtriXPzo2sPTm5_Mp10O87lCqcE", // (2)
"response_uri": "https://playground.eudi-wallet.org/eudiplo/3aa4706c-6f35-47b8-8a05-6a0a334c301d/oid4vp", // (3)
"response_mode": "direct_post.jwt", // (4)
"nonce": "85d5e24a-7163-4e44-a0ed-94be98cab666", // (5)

"dcql_query": { // (6)
"credentials": [
{
"id": "pid-sd-jwt", // (7)
"format": "dc+sd-jwt", // (8)
"claims": [ // (9)
{"path": ["given_name"]}, // (10)
{"path": ["family_name"]}, // (11)
{"path": ["birthdate"]}, // (12)
{"path": ["address", "street_address"]}, // (13)
{"path": ["address", "postal_code"]}, // (14)
{"path": ["address", "locality"]}, // (15)
{"path": ["address", "country"]}, // (16)
{"path": ["nationalities"]} // (17)
],
"meta": {
"vct_values": ["urn:eudi:pid:de:1"] // (18)
}
},
{
"id": "pid-mso-mdoc", // (19)
"format": "mso_mdoc", // (20)
"claims": [
{"path": ["eu.europa.ec.eudi.pid.1", "given_name"]}, // (21)
{"path": ["eu.europa.ec.eudi.pid.1", "family_name"]}, // (22)
{"path": ["eu.europa.ec.eudi.pid.1", "birth_date"]}, // (23)
{"path": ["eu.europa.ec.eudi.pid.1", "resident_street"]}, // (24)
{"path": ["eu.europa.ec.eudi.pid.1", "resident_postal_code"]}, // (25)
{"path": ["eu.europa.ec.eudi.pid.1", "resident_city"]}, // (26)
{"path": ["eu.europa.ec.eudi.pid.1", "resident_country"]},// (27)
{"path": ["eu.europa.ec.eudi.pid.1", "nationality"]} // (28)
],
"meta": {
"doctype_value": "eu.europa.ec.eudi.pid.1" // (29)
}
}
],
"credential_sets": [
{
"options": [
["pid-sd-jwt"], // (30)
["pid-mso-mdoc"] // (31)
]
}
]
},

"client_metadata": { // (32)
"jwks": {
"keys": [
{
"kty": "EC", // (33)
"crv": "P-256", // (34)
"x": "ShU4Fr3NH7v9TOAc9aYiu9eicdkfVT9ecVCPaPgJrMs", // (35)
"y": "iV0VXASylR0qWoDr_mKUWwzo-M59Wz3QBzpCm4oiXT0", // (36)
"alg": "ECDH-ES", // (37)
"kid": "a420ee83-ecfa-44fc-bb16-80320d87f745" // (38)
}
]
},
"vp_formats_supported": { // (39)
"mso_mdoc": {
"alg": ["ES256", "Ed25519"] // (40)
},
"dc+sd-jwt": {
"kb-jwt_alg_values": ["ES256", "Ed25519"], // (41)
"sd-jwt_alg_values": ["ES256", "Ed25519"] // (42)
}
},
"encrypted_response_enc_values_supported": ["A128GCM"] // (43)
},

"state": "3aa4706c-6f35-47b8-8a05-6a0a334c301d", // (44)
"aud": "https://self-issued.me/v2", // (45)
"exp": 1769513697, // (46)
"iat": 1769510097 // (47)
}

Field-by-Field Explanation

#FieldPurposeNotes
(1)response_typeResponse typeMust be vp_token for OpenID4VP.
(2)client_idVerifier identifierUses x509_hash: scheme per HAIP; hash binds request to verifier certificate.
(3)response_uriResponse endpointHTTPS endpoint where wallet posts the vp_token.
(4)response_modeResponse deliverydirect_post.jwt is mandatory for HAIP high-assurance flows.
(5)nonceSession bindingCryptographically random value preventing replay attacks.
(6)dcql_queryCredential queryDefines which credentials and attributes are requested.
(7)credentials[].idCredential identifierUsed to reference credentials in credential_sets.
(8)credentials[].formatCredential formatdc+sd-jwt for SD-JWT VC format.
(9)claimsRequested claimsList of attributes to be selectively disclosed.
(10–17)claims[].pathClaim paths (SD-JWT)JSON path segments into the SD-JWT payload.
(18)meta.vct_valuesCredential typeIdentifies German PID VC (urn:eudi:pid:de:1).
(19)credentials[].idCredential identifierIdentifier for mDoc PID request.
(20)credentials[].formatCredential formatISO/IEC 18013-5 mDoc format.
(21–28)claims[].pathClaim paths (SD-JWT)Namespace + attribute name as defined in PID mDoc profile.
(29)meta.doctype_valueDocument typeIdentifies the PID mDoc document type.
(30)credential_sets.options[0]Option 1Wallet may respond using mDOc PID.
(31)credential_sets.options[1]Option 2Wallet may respond using SD-JWT PID.
(32)client_metadataVerifier metadataProvides encryption keys and supported formats.
(33)jwks.keys[].ktyKey typeElliptic Curve key.
(34)jwks.keys[].crvCurveP-256 curve (secp256r1).
(35)jwks.keys[].xPublic key XBase64url-encoded coordinate.
(36)jwks.keys[].yPublic key YBase64url-encoded coordinate.
(37)jwks.keys[].algKey algorithmUsed for ECDH encryption of response.
(38)jwks.keys[].kidKey IDUsed by wallet to select encryption key.
(39)vp_formats_supportedFormat capabilitiesDeclares supported VP formats and algorithms.
(40)vp_formats_supported.mso_mdoc.algmDoc algorithmsAlgorithms supported for mDoc signatures.
(41)kb-jwt_alg_valuesHolder binding algsAlgorithms supported for key binding JWT.
(42)sd-jwt_alg_valuesSD-JWT algsAlgorithms supported for SD-JWT issuer signatures.
(43)encrypted_response_enc_values_supportedEncryptionSymmetric encryption algorithms supported.
(44)stateCSRF protectionReturned unmodified in the response.
(45)audAudienceFixed value for HAIP aligned responses.
(46)expExpirationLimits lifetime of request object.
(47)iatIssued-atUsed for freshness validation.
Ephemeral Encryption Keys in client_metadata

The jwks contained in client_metadata are used only for encrypting the wallet's response. These keys must be ephemeral. The are generated fresh for each transaction and discarded after use. Never reuse the same encryption keypair across multiple presentation requests. Reusing keys increases the risk of exposure if a key is compromised, and defeats the purpose of per-transaction encryption.

3.3 Request Construction Checklist

Before sending a Presentation Request, verify that your request includes all elements needed for a PID presentation:

CheckFieldValidationFailure Action
issPresent and matches your verifier identifierRequest will be rejected
audPresent and correctly targets the walletRequest will be rejected
iatPresent and current timestampRequest will be rejected if expired
expPresent and set to reasonable future time (5-10 min)Request will be rejected if expired
client_idPresent and uses correct scheme (e.g., x509_hash:). x509_hash:<Base64-url encoded hash of access certificate in .der format>Request will be rejected
response_typeSet to "vp_token"Request will be rejected
response_modeSet to "direct_post.jwt" for HAIPRequest will be rejected
noncePresent, cryptographically random, unique per requestSecurity risk: replay attacks possible
client_metadata.jwksContains valid encryption keyResponse cannot be encrypted
verifier_infoContains valid registration certificateWallet will reject untrusted verifier
response_uriValid HTTPS URL, accessible by walletResponse cannot be delivered
dcql_query.credentialsAt least one credential specifiedNo data will be returned
dcql_query.credentials[].formatValid format (dc+sd-jwt or mso_mdoc)Request will be rejected
dcql_query.credentials[].claimsAt least one claim per credentialCredential will be empty
Request object signatureRequest object is properly signed (JAR)Wallet will reject unsigned request
Nonce Security

The nonce must be cryptographically random and unique per request. Reusing nonces enables replay attacks. Generate using a cryptographically secure random number generator.

Expiration Time

Set exp to 5-10 minutes after iat. Too short may cause timeouts; too long increases security risk if the request is intercepted.

3.3.1 Calculating your client_id

The client_id always uses the prefix x509_hash:. The value after the colon is derived from your access certificate and calculated as follows.

Step 1: Obtain your verifier certificate in DER format

If you have your certificate in PEM format, convert it to DER:

openssl x509 -in access_certificate.pem -outform DER -out access_certificate.der

Step 2: Calculate the SHA-256 hash

Hash the DER-encoded certificate:

openssl dgst -sha256 -binary access_certificate.der | base64 -

Or using standard utilities:

sha256sum access_certificate.der # Shows hex hash

Step 3: Base64url encode the hash

If you used openssl dgst above, it outputs base64 (standard). Convert standard base64 to base64url by replacing:

  • + with -
  • / with _
  • Remove padding (=)

Alternatively, in one command:

openssl dgst -sha256 -binary access_certificate.der | base64 | tr '+/' '-_' | tr -d '='

The resulting client_id is formatted as x509_hash:<base64url-encoded-hash> and must match in both the deeplink and the authorization request.

A deeplink is a URL that the verifier sends to the wallet to initiate a PID presentation flow. The wallet opens this deeplink, which contains all the information needed to fetch and process the presentation request.

Deeplink structure:

openid4vp://?client_id=<url-encoded-client-id>&request_uri=<url-encoded-request-uri>&request_uri_method=get

Example from the EUDI Playground:

openid4vp://?client_id=x509_hash%3AtMqdcPmXTb5yZPBdQ7RTJcXOBViD9wUAO_SzvOaBXv0&request_uri=https%3A%2F%2Feudiplo.eudi-wallet.org%2Fpresentations%2Ff1020d4e-9578-4bf1-a233-1431d4c93af3%2Foid4vp%2Frequest&request_uri_method=get

Key components:

  • Scheme: openid4vp:// identifies this as an OpenID4VP presentation request
  • client_id: Must match the client_id in your authorization request (see 3.3.1). It is URL-encoded here (: becomes %3A)
  • request_uri: HTTPS endpoint where the wallet fetches your signed presentation request. This URI should contain a unique session identifier that allows you to correlate the response to the request
  • request_uri_method: Specifies the HTTP method to use when fetching the request (get or post)

3.3.3 Handling the redirect after response

After the wallet successfully posts its presentation response to your response_uri, you must return a redirect to the wallet. This is a key part of session finalization and is validated in Chapter 4 (step 3.10).

Response requirements:

  • HTTP Status Code: Return 200 OK
  • Content-Type: application/json
  • Response body: Include a redirect_uri field pointing to a page on your service where the wallet should navigate next

Example response:

{
"redirect_uri": "https://your-verifier.example/presentation-complete?session_id=abc123"
}

Important: The wallet MUST follow the provided redirect_uri. As a verifier, you must validate that the wallet actually navigates to this URI (e.g., by observing a subsequent HTTP request from the wallet's user agent). This confirms that the wallet accepted the transaction completion and prevents attacks where a malicious application intercepts the response without the user's interaction. For details, see Chapter 4, step 3.10.

4. Interpreting the PID Presentation Response

This chapter explains the structure of the PID Presentation Response and provides a complete validation checklist. It covers what the verifier receives at the response_uri, how to interpret the vp_token structure, and how to validate it using the validation layers described in Chapter 2.

4.1 Receiving the Presentation Response

When the wallet has satisfied the Presentation Request and the user has consented, it sends the response to the verifier’s response_uri. In a HAIP-aligned flow using direct_post.jwt, this happens as a HTTPS POST.

A typical HTTP interaction looks like this:

POST /response HTTP/1.1
Host: response.example
Content-Type: application/x-www-form-urlencoded

response=eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTEyOEdDTSIsImtpZCI6ImFjIn0...

The response parameter contains an encrypted JWT (JWE). The verifier should treat this object as opaque until basic transport-level checks (TLS, content type, size limits) have passed.

4.2 High-Level Structure of the Response

The presentation response structure with direct_post.jwt consists of:

Outer Layer (Encrypted Response):

  • An encrypted JWT (JWE) containing the entire response
  • Encrypted using the verifier's public key (from the request's client_metadata.jwks)
  • After decryption, contains session binding claims (nonce, aud, iat, exp, state) and the vp_token

Inner Layer (vp_token):

  • For SD-JWT: A signed credential in the format <Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~<KB-JWT>
  • For mDoc: CBOR-encoded credential with deviceAuth structure
  • The vp_token itself is NOT encrypted separately - it's simply included in the encrypted response payload
Response Encryption

The entire response is encrypted as a single JWE. The vp_token inside is a signed (not encrypted) credential format. This prevents "double encryption" which would be unnecessary.

4.3 Response Validation Checklist

The checklist below provides a complete validation workflow for the PID presentation response. It follows the validation layers described in Chapter 2, proceeding in order from transport checks through business rules. Each layer builds on the previous one, following a fail-fast principle: reject immediately upon any validation failure to save processing time and reduce attack surface.

Important: With direct_post.jwt response mode, the entire response is encrypted as a JWE. The vp_token inside is a signed credential (SD-JWT or mDoc), NOT separately encrypted. Decryption must happen before session binding can be validated.

For detailed explanations of why each check matters, refer back to the corresponding validation layer in Chapter 2.

StepCheckImplementationFailure Action
1. Transport Layer
1.1Verify HTTPS connectionConfirm TLS 1.2+ is used and server certificate is validReject: Insecure transport
1.2Check content typeVerify Content-Type: application/x-www-form-urlencoded header is presentReject: Wrong content type
1.3Verify payload sizeConfirm payload is within reasonable limits (e.g., < 1MB) to prevent DoSOptionally reject: Payload too large
2. Decrypt Response
2.1Extract response parameterParse the POST body and extract the response parameter valueReject: Missing response parameter
2.2Parse JWE structureVerify the response has valid JWE format (five base64url-encoded parts)Reject: Malformed JWE
2.3Decode JWE headerParse JWE header as valid JSON and extract alg, enc, and kid fieldsReject: Invalid JWE header
2.4Verify encryption algorithmConfirm alg is in allowed list (e.g., ECDH-ES) and enc is supported (e.g., A128GCM)Reject: Unsupported encryption
2.5Decrypt responseUse your private key (matching the public key sent in request client_metadata.jwks) to decrypt the JWEReject: Decryption failed
2.6Parse decrypted payloadParse the decrypted plaintext as valid JSONReject: Invalid payload structure
3. Session Binding
3.1Extract nonce from payloadLocate nonce claim in decrypted response payloadReject: Missing nonce
3.2Compare nonce with requestVerify nonce matches exactly (byte-for-byte) the nonce from your original requestReject: Nonce mismatch
3.3Extract aud from payloadLocate aud claim in decrypted response payloadReject: Missing audience
3.4Verify audience matches verifierConfirm aud matches your verifier's client_id from the requestReject: Wrong audience
3.5Extract iat timestampLocate iat (issued-at) claim in decrypted response payloadReject: Missing timestamp
3.6Extract exp timestampLocate exp (expiration) claim in decrypted response payloadReject: Missing expiration
3.7Verify timestamp validityConfirm current time is between iat and exp (accounting for clock skew, e.g., ±30s)Reject: Expired or not yet valid
3.8Check response freshnessVerify iat is within acceptable window from request time (e.g., last 5 minutes)Reject: Response too old
3.9Extract state (if used)If state was included in your request, locate it in decrypted response payload and verify it matchesReject: State mismatch
3.10In a same-device flow: Wallet follows the redirect_urivalidate that the wallet follows the redirect link so the presentation stays tied to the session.Reject: Wallet denied redirect
4. Holder Binding
4.1Extract vp_tokenLocate the vp_token field in decrypted response payloadReject: Missing vp_token
4.2Verify credential format matches requestConfirm vp_token format (SD-JWT with ~ separators or mDoc CBOR) matches the format you requested in dcql_queryReject: Wrong credential format
4.3SD-JWT: Extract credentialExtract the issuer-signed JWT (first part before ~)Reject: Cannot extract credential
4.4SD-JWT: Parse credential structureDecode credential JWT header and payloadReject: Malformed credential
4.5SD-JWT: Extract holder's public keyExtract cnf.jwk claim from credential payload per RFC 7800Reject: Missing cnf claim
4.6SD-JWT: Extract KB-JWTExtract the KB-JWT (last element after final ~)Reject: Missing KB-JWT
4.7SD-JWT: Parse KB-JWT structureDecode KB-JWT header and payloadReject: Malformed KB-JWT
4.8SD-JWT: Verify KB-JWT audienceConfirm aud in KB-JWT matches your verifier identifierReject: Wrong KB-JWT audience
4.9SD-JWT: Verify KB-JWT nonceConfirm nonce in KB-JWT matches the nonce from your requestReject: KB-JWT nonce mismatch
4.10SD-JWT: Verify KB-JWT timestampConfirm iat in KB-JWT is recent and consistent with response iat from step 3.5Reject: Invalid KB-JWT timestamp
4.11SD-JWT: Verify KB-JWT signatureVerify the KB-JWT signature using the public key extracted in step 4.5Reject: Invalid holder binding signature
4.12SD-JWT: Verify hash bindingConfirm KB-JWT's sd_hash matches SHA-256 hash of the presentation (issuer-signed JWT + ~ + disclosures)Reject: Hash mismatch
4.13mDoc: Validate holder bindingValidate deviceAuth and holder binding in accordance with ISO 18013-5 specificationReject: Invalid mDoc holder binding
5. Credential Validation
5.1SD-JWT: Identify credential typeVerify vct claim matches requested PID type (e.g., urn:eudi:pid:de:1)Reject: Wrong credential type
5.2SD-JWT: Extract issuer identifierExtract iss claim from credential payloadReject: Missing issuer
5.3SD-JWT: Verify issuer is trustedLook up issuer in your loaded trust list (PID providers) and verify it is authorized to issue PIDsReject: Untrusted issuer
5.4SD-JWT: Extract issuer's public keyRetrieve issuer's public key from trust list or from credential's x5c chain in credential headerReject: Cannot find issuer key
5.5SD-JWT: Verify issuer signatureVerify the credential signature using issuer's public keyReject: Invalid credential signature
5.6SD-JWT: Verify credential validityCheck iat, exp claims and confirm credential is currently valid (accounting for clock skew)Reject: Expired credential
5.7SD-JWT: Check revocation statusQuery revocation mechanism (status list, OCSP, or CRL) to confirm credential is not revokedReject: Revoked credential
5.8SD-JWT: Verify certificate chainIf issuer uses X.509 certificates (x5c in header), verify the full chain up to trust anchor and check certificate validityReject: Invalid certificate chain
5.9mDoc: Validate credentialValidate mDoc credential structure, issuer signature, trust, and validity in accordance with ISO 18013-5 specificationReject: Invalid mDoc credential
6. Selective Disclosure
6.1SD-JWT: Extract disclosuresParse all disclosed attributes from the SD-JWT (elements between ~ separators, excluding issuer-signed JWT and KB-JWT)Reject: Cannot parse disclosures
6.2SD-JWT: Verify disclosure integrityVerify each disclosure's hash matches the corresponding _sd hash in the issuer-signed JWTReject: Disclosure tampering detected
6.3mDoc: Validate selective disclosureValidate disclosed namespaces and elements in accordance with ISO 18013-5 specificationReject: Invalid mDoc disclosures
6.4Compare with requested claimsCheck that all disclosed claims were explicitly requested in your dcql_queryWarning: Extra claims disclosed
6.5Verify mandatory claims presentConfirm all claims you marked as mandatory in your request are present in the responseReject: Missing mandatory claims
6.6Verify credential set compliance (if used)If credential_sets was used in your request, confirm the combination of credentials received matches one of the optionsReject: Invalid credential combination
6.7Check for over-disclosureVerify no additional credentials beyond what was requested are included in the responseWarning: Unexpected credentials
7. Business Rules
7.1Validate attribute formatsVerify each disclosed claim value conforms to expected data types and formats (dates, strings, structured data)Reject: Invalid attribute format
7.2Evaluate business requirementsApply your application-specific logic (e.g., age >= 18, nationality checks, address validation)Reject: Business rule violation
7.3Check data completenessVerify all attributes needed for your use case are present and non-emptyReject: Incomplete data
7.4Record authorization decisionLog the validation outcome, disclosed attributes (respecting privacy), and authorization decision for audit purposesN/A
Fail-Fast Principle

Reject immediately upon any validation failure that is more severe than a warning. Do not continue validation after a critical failure (e.g., signature verification). This prevents information leakage and reduces processing costs.


4.4 Failure Handling and Diagnostics

Verifier implementations should provide clear diagnostics internally. Common rejection points include:

  • Signature verification failure
  • Nonce or audience mismatch
  • Unsupported credential format
  • Missing or extra disclosed attributes
  • Invalid or untrusted wallet attestation

While error details should not be exposed to the wallet or user, precise internal logging is essential for debugging interoperability issues.