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¶
- Introduction
- Concepts and Validation Layers
- Building a PID Presentation Request
- 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 (HAIP). 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
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 typically a pushed signed request object (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 (JAR) 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¶
| # | Field | Purpose | Notes |
|---|---|---|---|
| (1) | response_type |
Response type | Must be vp_token for OpenID4VP. |
| (2) | client_id |
Verifier identifier | Uses x509_hash: scheme per HAIP; hash binds request to verifier certificate. |
| (3) | response_uri |
Response endpoint | HTTPS endpoint where wallet posts the vp_token. |
| (4) | response_mode |
Response delivery | direct_post.jwt is mandatory for HAIP high-assurance flows. |
| (5) | nonce |
Session binding | Cryptographically random value preventing replay attacks. |
| (6) | dcql_query |
Credential query | Defines which credentials and attributes are requested. |
| (7) | credentials[].id |
Credential identifier | Used to reference credentials in credential_sets. |
| (8) | credentials[].format |
Credential format | dc+sd-jwt for SD-JWT VC format. |
| (9) | claims |
Requested claims | List of attributes to be selectively disclosed. |
| (10–17) | claims[].path |
Claim paths (SD-JWT) | JSON path segments into the SD-JWT payload. |
| (18) | meta.vct_values |
Credential type | Identifies German PID VC (urn:eudi:pid:de:1). |
| (19) | credentials[].id |
Credential identifier | Identifier for mDoc PID request. |
| (20) | credentials[].format |
Credential format | ISO/IEC 18013-5 mDoc format. |
| (21–28) | claims[].path |
Claim paths (SD-JWT) | Namespace + attribute name as defined in PID mDoc profile. |
| (29) | meta.doctype_value |
Document type | Identifies the PID mDoc document type. |
| (30) | credential_sets.options[0] |
Option 1 | Wallet may respond using mDOc PID. |
| (31) | credential_sets.options[1] |
Option 2 | Wallet may respond using SD-JWT PID. |
| (32) | client_metadata |
Verifier metadata | Provides encryption keys and supported formats. |
| (33) | jwks.keys[].kty |
Key type | Elliptic Curve key. |
| (34) | jwks.keys[].crv |
Curve | P-256 curve (secp256r1). |
| (35) | jwks.keys[].x |
Public key X | Base64url-encoded coordinate. |
| (36) | jwks.keys[].y |
Public key Y | Base64url-encoded coordinate. |
| (37) | jwks.keys[].alg |
Key algorithm | Used for ECDH encryption of response. |
| (38) | jwks.keys[].kid |
Key ID | Used by wallet to select encryption key. |
| (39) | vp_formats_supported |
Format capabilities | Declares supported VP formats and algorithms. |
| (40) | vp_formats_supported.mso_mdoc.alg |
mDoc algorithms | Algorithms supported for mDoc signatures. |
| (41) | kb-jwt_alg_values |
Holder binding algs | Algorithms supported for key binding JWT. |
| (42) | sd-jwt_alg_values |
SD-JWT algs | Algorithms supported for SD-JWT issuer signatures. |
| (43) | encrypted_response_enc_values_supported |
Encryption | Symmetric encryption algorithms supported. |
| (44) | state |
CSRF protection | Returned unmodified in the response. |
| (45) | aud |
Audience | Fixed value for HAIP aligned responses. |
| (46) | exp |
Expiration | Limits lifetime of request object. |
| (47) | iat |
Issued-at | Used for freshness validation. |
3.3 Request Construction Checklist¶
Before sending a Presentation Request, verify that your request includes all elements needed for a PID presentation:
| Check | Field | Validation | Failure Action |
|---|---|---|---|
iss |
Present and matches your verifier identifier | Request will be rejected | |
aud |
Present and correctly targets the wallet | Request will be rejected | |
iat |
Present and current timestamp | Request will be rejected if expired | |
exp |
Present and set to reasonable future time (5-10 min) | Request will be rejected if expired | |
client_id |
Present and uses correct scheme (e.g., x509_hash:) |
Request will be rejected | |
response_type |
Set to "vp_token" |
Request will be rejected | |
response_mode |
Set to "direct_post.jwt" for HAIP |
Request will be rejected | |
nonce |
Present, cryptographically random, unique per request | Security risk: replay attacks possible | |
client_metadata.jwks |
Contains valid encryption key | Response cannot be encrypted | |
verifier_info |
Contains valid registration certificate | Wallet will reject untrusted verifier | |
response_uri |
Valid HTTPS URL, accessible by wallet | Response cannot be delivered | |
dcql_query.credentials |
At least one credential specified | No data will be returned | |
dcql_query.credentials[].format |
Valid format (dc+sd-jwt or mso_mdoc) |
Request will be rejected | |
dcql_query.credentials[].claims |
At least one claim per credential | Credential will be empty | |
| Request object signature | Request 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.
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.
| Step | Check | Implementation | Failure Action |
|---|---|---|---|
| 1. Transport Layer | |||
| 1.1 | Verify HTTPS connection | Confirm TLS 1.2+ is used and server certificate is valid | Reject: Insecure transport |
| 1.2 | Check content type | Verify Content-Type: application/x-www-form-urlencoded header is present |
Reject: Wrong content type |
| 1.3 | Verify payload size | Confirm payload is within reasonable limits (e.g., < 1MB) to prevent DoS | Optionally reject: Payload too large |
| 2. Decrypt Response | |||
| 2.1 | Extract response parameter | Parse the POST body and extract the response parameter value |
Reject: Missing response parameter |
| 2.2 | Parse JWE structure | Verify the response has valid JWE format (five base64url-encoded parts) | Reject: Malformed JWE |
| 2.3 | Decode JWE header | Parse JWE header as valid JSON and extract alg, enc, and kid fields |
Reject: Invalid JWE header |
| 2.4 | Verify encryption algorithm | Confirm alg is in allowed list (e.g., ECDH-ES) and enc is supported (e.g., A128GCM) |
Reject: Unsupported encryption |
| 2.5 | Decrypt response | Use your private key (matching the public key sent in request client_metadata.jwks) to decrypt the JWE |
Reject: Decryption failed |
| 2.6 | Parse decrypted payload | Parse the decrypted plaintext as valid JSON | Reject: Invalid payload structure |
| 3. Session Binding | |||
| 3.1 | Extract nonce from payload |
Locate nonce claim in decrypted response payload |
Reject: Missing nonce |
| 3.2 | Compare nonce with request | Verify nonce matches exactly (byte-for-byte) the nonce from your original request | Reject: Nonce mismatch |
| 3.3 | Extract aud from payload |
Locate aud claim in decrypted response payload |
Reject: Missing audience |
| 3.4 | Verify audience matches verifier | Confirm aud matches your verifier's client_id from the request |
Reject: Wrong audience |
| 3.5 | Extract iat timestamp |
Locate iat (issued-at) claim in decrypted response payload |
Reject: Missing timestamp |
| 3.6 | Extract exp timestamp |
Locate exp (expiration) claim in decrypted response payload |
Reject: Missing expiration |
| 3.7 | Verify timestamp validity | Confirm current time is between iat and exp (accounting for clock skew, e.g., ±30s) |
Reject: Expired or not yet valid |
| 3.8 | Check response freshness | Verify iat is within acceptable window from request time (e.g., last 5 minutes) |
Reject: Response too old |
| 3.9 | Extract state (if used) |
If state was included in your request, locate it in decrypted response payload and verify it matches |
Reject: State mismatch |
| 4. Holder Binding | |||
| 4.1 | Extract vp_token |
Locate the vp_token field in decrypted response payload |
Reject: Missing vp_token |
| 4.2 | Verify credential format matches request | Confirm vp_token format (SD-JWT with ~ separators or mDoc CBOR) matches the format you requested in dcql_query |
Reject: Wrong credential format |
| 4.3 | SD-JWT: Extract credential | Extract the issuer-signed JWT (first part before ~) |
Reject: Cannot extract credential |
| 4.4 | SD-JWT: Parse credential structure | Decode credential JWT header and payload | Reject: Malformed credential |
| 4.5 | SD-JWT: Extract holder's public key | Extract cnf.jwk claim from credential payload per RFC 7800 |
Reject: Missing cnf claim |
| 4.6 | SD-JWT: Extract KB-JWT | Extract the KB-JWT (last element after final ~) |
Reject: Missing KB-JWT |
| 4.7 | SD-JWT: Parse KB-JWT structure | Decode KB-JWT header and payload | Reject: Malformed KB-JWT |
| 4.8 | SD-JWT: Verify KB-JWT audience | Confirm aud in KB-JWT matches your verifier identifier |
Reject: Wrong KB-JWT audience |
| 4.9 | SD-JWT: Verify KB-JWT nonce | Confirm nonce in KB-JWT matches the nonce from your request |
Reject: KB-JWT nonce mismatch |
| 4.10 | SD-JWT: Verify KB-JWT timestamp | Confirm iat in KB-JWT is recent and consistent with response iat from step 3.5 |
Reject: Invalid KB-JWT timestamp |
| 4.11 | SD-JWT: Verify KB-JWT signature | Verify the KB-JWT signature using the public key extracted in step 4.5 | Reject: Invalid holder binding signature |
| 4.12 | SD-JWT: Verify hash binding | Confirm KB-JWT's sd_hash matches SHA-256 hash of the presentation (issuer-signed JWT + ~ + disclosures) |
Reject: Hash mismatch |
| 4.13 | mDoc: Validate holder binding | Validate deviceAuth and holder binding in accordance with ISO 18013-5 specification | Reject: Invalid mDoc holder binding |
| 5. Credential Validation | |||
| 5.1 | SD-JWT: Identify credential type | Verify vct claim matches requested PID type (e.g., urn:eudi:pid:de:1) |
Reject: Wrong credential type |
| 5.2 | SD-JWT: Extract issuer identifier | Extract iss claim from credential payload |
Reject: Missing issuer |
| 5.3 | SD-JWT: Verify issuer is trusted | Look up issuer in your loaded trust list (PID providers) and verify it is authorized to issue PIDs | Reject: Untrusted issuer |
| 5.4 | SD-JWT: Extract issuer's public key | Retrieve issuer's public key from trust list or from credential's x5c chain in credential header |
Reject: Cannot find issuer key |
| 5.5 | SD-JWT: Verify issuer signature | Verify the credential signature using issuer's public key | Reject: Invalid credential signature |
| 5.6 | SD-JWT: Verify credential validity | Check iat, exp claims and confirm credential is currently valid (accounting for clock skew) |
Reject: Expired credential |
| 5.7 | SD-JWT: Check revocation status | Query revocation mechanism (status list, OCSP, or CRL) to confirm credential is not revoked | Reject: Revoked credential |
| 5.8 | SD-JWT: Verify certificate chain | If issuer uses X.509 certificates (x5c in header), verify the full chain up to trust anchor and check certificate validity |
Reject: Invalid certificate chain |
| 5.9 | mDoc: Validate credential | Validate mDoc credential structure, issuer signature, trust, and validity in accordance with ISO 18013-5 specification | Reject: Invalid mDoc credential |
| 6. Selective Disclosure | |||
| 6.1 | SD-JWT: Extract disclosures | Parse all disclosed attributes from the SD-JWT (elements between ~ separators, excluding issuer-signed JWT and KB-JWT) |
Reject: Cannot parse disclosures |
| 6.2 | SD-JWT: Verify disclosure integrity | Verify each disclosure's hash matches the corresponding _sd hash in the issuer-signed JWT |
Reject: Disclosure tampering detected |
| 6.3 | mDoc: Validate selective disclosure | Validate disclosed namespaces and elements in accordance with ISO 18013-5 specification | Reject: Invalid mDoc disclosures |
| 6.4 | Compare with requested claims | Check that all disclosed claims were explicitly requested in your dcql_query |
Warning: Extra claims disclosed |
| 6.5 | Verify mandatory claims present | Confirm all claims you marked as mandatory in your request are present in the response | Reject: Missing mandatory claims |
| 6.6 | Verify credential set compliance (if used) | If credential_sets was used in your request, confirm the combination of credentials received matches one of the options |
Reject: Invalid credential combination |
| 6.7 | Check for over-disclosure | Verify no additional credentials beyond what was requested are included in the response | Warning: Unexpected credentials |
| 7. Business Rules | |||
| 7.1 | Validate attribute formats | Verify each disclosed claim value conforms to expected data types and formats (dates, strings, structured data) | Reject: Invalid attribute format |
| 7.2 | Evaluate business requirements | Apply your application-specific logic (e.g., age >= 18, nationality checks, address validation) | Reject: Business rule violation |
| 7.3 | Check data completeness | Verify all attributes needed for your use case are present and non-empty | Reject: Incomplete data |
| 7.4 | Record authorization decision | Log the validation outcome, disclosed attributes (respecting privacy), and authorization decision for audit purposes | N/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.