Skip to content

OID4VP

  • OpenID for Verifiable Presentations 1.0 (final)
  • OpenID4VC High Assurance Interoperability Profile (HAIP) 1.0 (final)
  • W3C Digital Credentials API request/response processing (backend API and tests; no browser executor)
  • OAuth 2.0 Authorization Framework (request/response parameter model)
  • JOSE (JWT/JWS/JWE processing for request objects, direct_post.jwt, and dc_api.jwt)
  • DID Core / did:web (for decentralized_identifier client_id scheme handling)
  • RFC 5280 / PKIX (X.509 certificate chain validation for x509_san_dns and x509_hash)
Flow ID Name Steps Description
oid4vp-direct-post DCQL + direct_post 4 Verifier creates signed request object, wallet fetches from request_uri, posts vp_token + state to response_uri, verifier validates and evaluates policy
oid4vp-direct-post-jwt DCQL + direct_post.jwt 4 Verifier creates signed request, wallet fetches from request_uri, posts encrypted JWE containing signed oauth-authz-resp+jwt, verifier decrypts and evaluates policy

The table lists the flows available in Looking Glass. profile: "haip" and dc_api.jwt are backend API capabilities covered by Go tests; there is no Looking Glass DC API flow, and the wallet harness currently accepts only direct_post and direct_post.jwt.

  • DCQL direct_post – Create request object, hand off to wallet, evaluate verifier policy
  • DCQL direct_post.jwt – Same flow with encrypted response transport and inner JWT validation

The Looking Glass OID4VP controls preflight the selected VC credential profile against the DCQL preset before wallet submission. The default preset and credential profile both target the mDL mso_mdoc. If the preset requests mso_mdoc, dc+sd-jwt, jwt_vc_json, jwt_vc_json-ld, or ldp_vc, the wallet auto-issue profile must issue the matching format. Otherwise the wallet action is blocked with an explanation, because the wallet cannot build a matching vp_token from the selected profile.

When a credential_jwt is pasted manually, the UI allows submission but warns when the selected auto-issue profile does not match the DCQL format. The wallet and verifier still validate the actual credential and will reject the presentation if the credential evidence does not satisfy the request.

For JOSE/W3C credentials, verifier policy also requires issuance lineage in ProtocolSoup’s credential store; this demo verifier is not a general trust-list verifier for arbitrary externally issued credentials. The mso_mdoc path uses independently configured IACA trust anchors instead.

The default and canonical presentation request targets the ISO/IEC 18013-5 mobile driving licence (mso_mdoc). An authorization request created without an explicit dcql_query or scope receives this query, requesting family_name and document_number from the org.iso.18013.5.1 namespace:

{
"credentials": [{
"id": "mdl",
"format": "mso_mdoc",
"meta": {
"doctype_values": ["org.iso.18013.5.1.mDL"]
},
"claims": [
{ "path": ["org.iso.18013.5.1", "family_name"] },
{ "path": ["org.iso.18013.5.1", "document_number"] }
]
}]
}

SD-JWT VC and the W3C formats remain fully selectable by sending an explicit dcql_query (or scope) that names them. For example, a university_degree SD-JWT VC requesting degree and graduation_year:

{
"credentials": [{
"id": "university_degree",
"format": "dc+sd-jwt",
"claims": [
{ "path": ["degree"] },
{ "path": ["graduation_year"] }
],
"meta": {
"vct_values": ["https://protocolsoup.com/credentials/university_degree"]
}
}]
}
  • Authorization request enforces XOR: exactly one of dcql_query or scope.
  • For response_mode direct_post and direct_post.jwt, response_uri is required and redirect_uri must be absent.
  • For response_mode dc_api and dc_api.jwt (the W3C Digital Credentials API), response_uri and state are not used; the request carries origin (and expected_origins for signed requests), and the wallet response is correlated by request_id.
  • Request object header typ must be oauth-authz-req+jwt.
  • VP token header typ must be vp+jwt (JWT/SD-JWT VP tokens; not used by mso_mdoc).
  • direct_post.jwt inner response header typ must be oauth-authz-resp+jwt (SD-JWT path; the mso_mdoc online profile uses an unsigned encrypted JWT, see below).

ProtocolSoup verifies ISO/IEC 18013-5 mso_mdoc presentations over the OpenID4VP 1.0 online profile (Appendix B.2), alongside the SD-JWT VC path, which is unchanged.

  • vp_token shape. With DCQL, the vp_token is a JSON object keyed by the DCQL credential query id, whose value is an array of base64url-encoded CBOR DeviceResponse strings: { "<dcql_id>": ["<base64url DeviceResponse>"] }.
  • SessionTranscript / handover. The device signature is bound to the request through SessionTranscript = [null, null, OpenID4VPHandover] (OID4VP 1.0 Appendix B.2.6.1). The verifier reconstructs the OpenID4VPHandover itself from the request’s client_id, nonce, response_uri, and the RFC 7638 JWK SHA-256 thumbprint of its response-encryption key; it never trusts a transmitted handover. The handover identifier is the fixed string "OpenID4VPHandover", and the second element is SHA-256 over the CBOR of [client_id, nonce, jwkThumbprint, response_uri].
  • Encrypted responses (direct_post.jwt). For an mso_mdoc request with response_mode=direct_post.jwt, the verifier provisions an ephemeral EC (P-256) ECDH-ES response-encryption key and advertises its public half in client_metadata.jwks with encrypted_response_enc_values_supported (A128GCM). The wallet encrypts the Authorization Response as a JWE (ECDH-ES + A128GCM) whose payload carries vp_token and state as top-level members. The jwkThumbprint bound into the handover is the thumbprint of the key actually used to encrypt, so a response re-encrypted to a different key is detected as a device-authentication failure (anti-substitution, Appendix B.2.6). Response encryption is mandatory for the online profile.
  • Issuer trust. The presented document-signer certificate must chain to a configured ISO/IEC 18013-5 IACA trust anchor (MDOC_IACA_ROOT_PEM / MDOC_IACA_ROOT_PEM_FILE); the digests, validityInfo, and the detached deviceSignature over the reconstructed DeviceAuthenticationBytes are all verified.

Example DCQL query requesting an mdoc mobile driving licence:

{
"credentials": [{
"id": "mdl",
"format": "mso_mdoc",
"meta": { "doctype_values": ["org.iso.18013.5.1.mDL"] },
"claims": [
{ "path": ["org.iso.18013.5.1", "family_name"] },
{ "path": ["org.iso.18013.5.1", "document_number"] }
]
}]
}

HAIP mode and the W3C Digital Credentials API

Section titled “HAIP mode and the W3C Digital Credentials API”

ProtocolSoup implements the OpenID4VC High Assurance Interoperability Profile (HAIP 1.0, final) as an opt-in mode and adds the browser-mediated W3C Digital Credentials API presentation path. Both layer on top of the general SD-JWT VC and mso_mdoc support, which stays available and unchanged outside HAIP / DC API mode.

HAIP is a constraining profile, not an additive option. When a request sets profile: "haip", the verifier enforces the pinned constraints and rejects out-of-profile choices at request creation:

  • DCQL is required (a scope alias is rejected).
  • The response mode must be encrypted (direct_post.jwt or dc_api.jwt); plain direct_post / dc_api is rejected.
  • The signed-request Client Identifier Prefix must be x509_hash (the HAIP-mandated scheme). verifier_attestation, x509_san_dns, redirect_uri, and pre_registered are rejected in HAIP mode.
  • ES256 / SHA-256 baseline; HAIP sessions advertise both A128GCM and A256GCM in encrypted_response_enc_values_supported, and ECDH-ES response encryption is provisioned for both mso_mdoc and dc+sd-jwt.
  • Each mso_mdoc credential in the dcql_query gains an AKI Trusted Authorities Query (OID4VP 1.0 Section 6.1.1, aki), with the value set to the base64url SubjectKeyIdentifier of the configured IACA root. The wallet uses it to select an issuer the verifier trusts; the verifier validates both that the document-signer chains to a configured IACA root and that its certificate Authority Key Identifier is one of the requested values. An explicit trusted_authorities on a credential is preserved, and non-mdoc credentials are left untouched.

W3C Digital Credentials API path (dc_api, dc_api.jwt)

Section titled “W3C Digital Credentials API path (dc_api, dc_api.jwt)”

The backend implements the request-object, handover, audience, decryption, and policy-processing side of this path. The repository does not currently invoke the browser’s Digital Credentials API, so this is not exposed as an end-to-end browser demo.

In a conforming browser invocation, the request is delivered browser-mediated rather than by redirect / direct_post. The verifier sets response_mode: "dc_api.jwt" and the Verifier Web origin (optionally expected_origins for signed requests). There is no response_uri and no state; the wallet response is correlated by request_id. The response audience is the Origin prefixed with origin: (the KB-JWT aud for SD-JWT VC; the bare Origin inside the mdoc handover). This path serves both SD-JWT VC and mso_mdoc.

The DC API path uses a distinct SessionTranscript handover, OpenID4VPDCAPIHandover (OID4VP 1.0 Appendix B.2.6.2), built over [origin, nonce, jwkThumbprint] — not the redirect OpenID4VPHandover over [client_id, nonce, jwkThumbprint, response_uri]. The fixed identifier string is "OpenID4VPDCAPIHandover" and the info array has 3 elements (the Origin replaces client_id and response_uri), so the two handovers can never collide. The verifier selects the variant by invocation path and reconstructs it from its own inputs; it never trusts a transmitted handover.

HAIP 1.0 makes wallet/key attestation support mandatory but leaves the wire format an Ecosystem extension point (HAIP §9.3); no single attestation format is pinned by HAIP itself, so the presentation profile requires none. The verifier_attestation signed-request scheme remains available in the general profile, but is rejected when profile: "haip" is selected.

OpenID4VP defines multiple client identification schemes. ProtocolSoup implements the following:

Verifier identity is established by the redirect URI trust context. The client_id equals the response_uri.

Verifier identity is resolved via DID document. The DID document is fetched and validated at runtime – id must match the presented DID, and verification material must be present in authentication, assertionMethod, or verificationMethod.

Verifier presents a signed attestation JWT. The verifier’s attestation issuer publishes OpenID discovery metadata and JWKS. The wallet validates the attestation JWT against the published JWKS. When OID4VP_VERIFIER_ATTESTATION_PRIVATE_KEY_PEM is unset, an ephemeral in-memory key is auto-provisioned at startup.

Verifier identity is bound to a DNS name via X.509 certificate Subject Alternative Name. The request object carries an x5c JOSE header containing the certificate chain. Trust validation requires:

  1. PKIX chain validation (leaf signed by CA, validity period, key usage)
  2. Leaf certificate DNS SAN matches the client_id DNS name
  3. response_uri hostname matches the client_id DNS name
  4. JWT signature verifies against the leaf certificate public key
  5. The client_id_scheme claim is present in the signed request object

When no certificate material is configured via environment variables, the verifier auto-provisions an ephemeral ECDSA P-256 self-signed CA + leaf chain at startup, with the leaf SAN bound to the deployment hostname.

x509_hash (HAIP 1.0 §5; OpenID4VP §5.9.3)

Section titled “x509_hash (HAIP 1.0 §5; OpenID4VP §5.9.3)”

The HAIP-mandated Client Identifier Prefix for signed requests. The client_id is x509_hash:<base64url(SHA-256(leaf-cert-DER))> — verifier identity is bound to the hash of the leaf certificate’s DER encoding rather than a DNS SAN. The request object carries the x5c chain. This is the default scheme for HAIP / DC API requests and reuses the same X.509 signer as x509_san_dns. When a persistence root (data dir) is configured the auto-provisioned signing certificate is persisted and reloaded across restarts, so the x509_hash client_id stays stable for a certification window rather than churning every process start; the wallet trusts the request signature by that leaf hash.

The wallet harness performs PKIX validation against its independently configured trust store (WALLET_VERIFIER_X509_TRUST_ANCHOR_PEM); an x5c chain cannot appoint its own trust root.

  • pre_registered, openid_federation are defined in the spec but not currently implemented.

Verifier Evaluation Flow (OID4VP §5, §6, §8)

Section titled “Verifier Evaluation Flow (OID4VP §5, §6, §8)”
  1. Create Authorization Request — Verifier builds a signed request object (typ=oauth-authz-req+jwt) containing client_id, response_type=vp_token, response_mode, response_uri, dcql_query (or scope), nonce, state, and exp. Publishes it at a request_uri.
  2. Wallet Fetches Request Object — Wallet fetches the signed request from request_uri via GET or POST. Validates JWT signature, typ header, and expiry before processing.
  3. Wallet Submits Response — Wallet evaluates DCQL query, selects matching credentials, creates the vp_token, and submits it:
    • direct_post: form-post vp_token + state
    • direct_post.jwt: form-post encrypted response JWE containing signed inner JWT (typ=oauth-authz-resp+jwt) with vp_token, state, and audience bound to response_uri
    • dc_api.jwt (W3C Digital Credentials API): post encrypted response JWE + request_id (no state), with the response audience bound to origin:<verifier-origin>
  4. Verifier Validates + Policy Decision — Verifier resolves the session by state (redirect path) or request_id (DC API path), then validates:
    • nonce binding (VP token nonce matches request nonce)
    • audience binding (VP token audience includes client_id, or origin:<origin> on the DC API path)
    • token expiry
    • holder binding (iss/sub matches wallet identity, cnf.jkt matches key thumbprint)
  • credential evidence (issuer signature, subject binding, disclosure integrity, and trust policy checks)
  • DCQL claim-path completeness (all required claim paths present in disclosed claims)
  1. Result — Policy decision is stored and returned as allowed/denied with code, message, reasons, and reason_codes.
Endpoint Role Purpose
Authorization Request Endpoint Verifier creates and publishes a request object / request URI
Request URI Retrieval Endpoint Wallet fetches request object by request_uri
Verifier Attestation Discovery OpenID discovery metadata for the verifier attestation issuer
Verifier Attestation AS Metadata OAuth 2.0 Authorization Server metadata for the attestation issuer
Verifier Attestation JWKS Public signing keys used to validate verifier_attestation request objects
Response Endpoint Wallet submits direct_post / direct_post.jwt (correlated by state) or dc_api.jwt (correlated by request_id) response payloads
Verification Result Endpoint (optional) Verifier exposes policy evaluation outcome to client tooling
  • direct_post response parameters: state=<opaque-value>&vp_token=<compact-jws>
  • direct_post.jwt response parameter: response=<compact-jwe> where the decrypted plaintext is an inner JWT with typ=oauth-authz-resp+jwt
  • VP token essentials: typ=vp+jwt, nonce bound to request nonce, audience bound to verifier client_id
  • Authorization request: signed request object with typ=oauth-authz-req+jwt, client_id, response_mode, response_uri, nonce, state, exp
  • DCQL contract: exactly one of dcql_query or scope; required claim paths derived from DCQL are enforced
  • Client identification: chosen client_id scheme follows OpenID4VP scheme-specific trust validation
  • direct_post response: state maps to active request and vp_token is present
  • direct_post.jwt response: decrypts successfully and inner JWT signature/type/audience/subject/expiry checks pass
  • VP token checks: typ=vp+jwt, nonce match, audience match, expiry valid, holder binding verified
  • Credential evidence checks: presented credential subject/issuer/signature/disclosures satisfy verifier trust policy and required claim paths
  • Policy output: verifier returns explicit decision with status, code, and reason details