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)
- Ecosystem guarantees next (wallet integrity)
- 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: 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 4: 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 5: Wallet Integrity¶
At this layer, the verifier validates that: - Wallet integrity or attestation evidence is present - The evidence is authentic and verifiable - The wallet provider is present in the list of trusted wallet providers - The wallet is certified and in an uncompromised state - The attestation is fresh and corresponds to the wallet that created the presentation
Layer 6: 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 7: 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
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.
```json
{
"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 | Required | Purpose | Notes |
|---|---|---|---|---|
| (1) | response_type |
Yes | Response type | Must be vp_token for OpenID4VP. |
| (2) | client_id |
Yes | Verifier identifier | Uses x509_hash: scheme per HAIP; hash binds request to verifier certificate. |
| (3) | response_uri |
Yes | Response endpoint | HTTPS endpoint where wallet posts the vp_token. |
| (4) | response_mode |
Yes | Response delivery | direct_post.jwt is mandatory for HAIP high-assurance flows. |
| (5) | nonce |
Yes | Session binding | Cryptographically random value preventing replay attacks. |
| (6) | dcql_query |
Yes | Credential query | Defines which credentials and attributes are requested. |
| (7) | credentials[].id |
Yes | Credential identifier | Used to reference credentials in credential_sets. |
| (8) | credentials[].format |
Yes | Credential format | dc+sd-jwt for SD-JWT VC format. |
| (9) | claims |
Yes | Requested claims | List of attributes to be selectively disclosed. |
| (10–17) | claims[].path |
Yes | Claim paths (SD-JWT) | JSON path segments into the SD-JWT payload. |
| (18) | meta.vct_values |
Yes | Credential type | Identifies German PID VC (urn:eudi:pid:de:1). |
| (19) | credentials[].id |
Yes | Credential identifier | Identifier for mDoc PID request. |
| (20) | credentials[].format |
Yes | Credential format | ISO/IEC 18013-5 mDoc format. |
| (21–28) | claims[].path |
Yes | Claim paths (SD-JWT) | Namespace + attribute name as defined in PID mDoc profile. |
| (29) | meta.doctype_value |
Yes | Document type | Identifies the PID mDoc document type. |
| (30) | credential_sets.options[0] |
Yes | Option 1 | Wallet may respond using mDOc PID. |
| (31) | credential_sets.options[1] |
Yes | Option 2 | Wallet may respond using SD-JWT PID. |
| (32) | client_metadata |
Yes | Verifier metadata | Provides encryption keys and supported formats. |
| (33) | jwks.keys[].kty |
Yes | Key type | Elliptic Curve key. |
| (34) | jwks.keys[].crv |
Yes | Curve | P-256 curve (secp256r1). |
| (35) | jwks.keys[].x |
Yes | Public key X | Base64url-encoded coordinate. |
| (36) | jwks.keys[].y |
Yes | Public key Y | Base64url-encoded coordinate. |
| (37) | jwks.keys[].alg |
Yes | Key algorithm | Used for ECDH encryption of response. |
| (38) | jwks.keys[].kid |
Yes | Key ID | Used by wallet to select encryption key. |
| (39) | vp_formats_supported |
Yes | Format capabilities | Declares supported VP formats and algorithms. |
| (40) | vp_formats_supported.mso_mdoc.alg |
Yes | mDoc algorithms | Algorithms supported for mDoc signatures. |
| (41) | kb-jwt_alg_values |
Yes | Holder binding algs | Algorithms supported for key binding JWT. |
| (42) | sd-jwt_alg_values |
Yes | SD-JWT algs | Algorithms supported for SD-JWT issuer signatures. |
| (43) | encrypted_response_enc_values_supported |
Yes | Encryption | Symmetric encryption algorithms supported. |
| (44) | state |
Yes | CSRF protection | Returned unmodified in the response. |
| (45) | aud |
Yes | Audience | Fixed value for HAIP aligned responses. |
| (46) | exp |
Yes | Expiration | Limits lifetime of request object. |
| (47) | iat |
Yes | Issued-at | Used for freshness validation. |
3.3 Request Construction Checklist¶
Before sending a Presentation Request, verify that your request includes all required elements:
| 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 | |
state |
Present (recommended for CSRF protection) | Security risk: CSRF 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 | |
credential_sets |
Create optionality for responses | Wallet may not be able to respond | |
| 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/jwt
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUiLCJub25jZSI6IjEyMzQ1NiIsInZwX3Rva2VuIjoiLi4uIn0.MEUCIQD...
The payload is a signed JWT. 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 a vp_token¶
Although the exact contents vary by credential format, a HAIP-compliant vp_token contains:
- A JOSE header describing the signing algorithm and key identifiers.
- Standard JWT claims binding the response to the original request.
- An embedded verifiable presentation (SD-JWT or mDoc).
- Proof material demonstrating holder binding.
- A wallet integrity attestation.
vp_token
OpenID4VP defines vp_token as an abstract container. Its concrete structure depends on the response mode, credential format, and applicable ecosystem profile. For a PID-Presentaiton, the ecosystem profile is defined and as such will always be structured in the same way.
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.
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 | TLS 1.2+ used, certificate valid | Reject: Insecure transport |
| 1.2 | Check content type | Content-Type: application/jwt |
Reject: Wrong content type |
| 1.3 | Verify payload size | Payload within reasonable limits (e.g., < 1MB) | Optionally reject: Payload too large |
| 2. JWT Parsing | |||
| 2.1 | Parse JWT structure | Valid JWT format (header.payload.signature) | Reject: Malformed JWT |
| 2.2 | Decode JWT header | Header is valid JSON | Reject: Invalid header |
| 2.3 | Extract algorithm from header | Algorithm identifier present | Reject: Missing algorithm |
| 2.4 | Verify algorithm is supported | Algorithm is in allowed list | Reject: Unsupported algorithm |
| 2.5 | Decode JWT payload | Payload is valid JSON | Reject: Invalid payload |
| 3. Signature Verification | |||
| 3.1 | Extract wallet public key from wallet attestation | Attestation present | Reject: Unknown key |
| 3.2 | Verify JWT signature | Signature valid using wallet's public key | Reject: Invalid signature |
| 4. Session Binding | |||
| 4.1 | Extract nonce |
Nonce present in payload | Reject: Missing nonce |
| 4.2 | Compare with request nonce | Nonces match exactly | Reject: Nonce mismatch |
| 4.3 | Extract aud |
Audience present | Reject: Missing audience |
| 4.4 | Verify audience | Audience matches verifier client_id | Reject: Wrong audience |
| 4.5 | Extract iat |
Issued at timestamp present | Reject: Missing timestamp |
| 4.6 | Extract exp |
Expiration timestamp present | Reject: Missing expiration |
| 4.7 | Verify timestamps | Current time between iat and exp |
Reject: Expired or invalid timestamps |
| 4.8 | Check freshness | iat within acceptable window (e.g., last 5 min) |
Reject: Response too old |
| 5. Holder Binding | |||
| 5.1 | Extract holder binding proof | Proof present in response | Reject: Missing holder binding |
| 5.2 | Verify proof signature | Proof signature valid | Reject: Invalid proof signature |
| 5.3 | Verify proof covers nonce | Nonce included in proof | Reject: Nonce not bound |
| 5.4 | Verify proof covers audience | Audience included in proof | Reject: Audience not bound |
| 5.5 | Verify key association | Key matches PID holder binding key | Reject: Key mismatch |
| 6. Wallet Integrity | |||
| 6.1 | Extract wallet attestation | Attestation present | Reject: Missing attestation |
| 6.2 | Verify attestation signature | Attestation properly signed | Reject: Invalid attestation |
| 6.3 | Check wallet provider trust | Provider in trusted registry | Reject: Untrusted wallet |
| 6.4 | Check integrity status | Wallet uncompromised | Reject: Compromised wallet |
| 7. Credential Validation | |||
| 7.1 | Extract credentials | At least one credential present | Reject: No credentials |
| 7.2 | Identify credential format | Format is dc+sd-jwt or mso_mdoc |
Reject: Unsupported format |
| 7.3 | Verify issuer signature | Credential signature valid | Reject: Invalid credential signature |
| 7.4 | Check issuer trust | Issuer in trusted PID providers | Reject: Untrusted issuer |
| 7.5 | Verify credential validity | Credential not expired | Reject: Expired credential |
| 7.6 | Check revocation status | Credential not revoked | Reject: Revoked credential |
| 7.7 | Verify credential type | Type matches request | Reject: Wrong credential type |
| 8. Selective Disclosure | |||
| 8.1 | Extract disclosed claims | All disclosed attributes listed | Reject: Cannot extract claims |
| 8.2 | Compare with request | Only requested claims disclosed | Warning: Extra claims disclosed |
| 8.3 | Verify mandatory claims | All required claims present | Reject: Missing mandatory claims |
| 8.4 | Check credential sets | Combination matches request | Reject: Invalid combination |
| 9. Business Rules | |||
| 9.1 | Evaluate attribute values | Values meet business requirements | Reject: Business rule violation |
| 9.2 | Check data quality | Data is complete and valid | Reject: Incomplete data |
Fail-Fast Principle
Reject immediately upon any validation failure. 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.