Skip to content

OID4VP

ProtocolSoup’s OID4VP Verifier and Wallet implementations are OpenID Certified™ through the OpenID Foundation certification programme (HAIP, SD-JWT VC, and mdoc). Certification applies to the named versions and profiles, not every ProtocolSoup feature.

  • 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 an ECDH-ES response JWE whose payload is the JSON Authorization Response (vp_token + state), 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. The hosted wallet harness accepts direct_post and direct_post.jwt, and exposes a public /authorize authorization endpoint for HAIP direct_post.jwt wallet handoff.

  • DCQL direct_post – Create request object, hand off to wallet, evaluate verifier policy
  • DCQL direct_post.jwt – Same flow with encrypted ECDH-ES response transport (OID4VP 1.0 Section 8.3)

The Looking Glass OID4VP controls preflight the selected credential format against the DCQL query before wallet submission. Format (mso_mdoc or dc+sd-jwt) and HAIP (general or HAIP) are independent selectors. The default is the mDL mso_mdoc. HAIP on OID4VP issues the key-attested configuration of that format (MobileDrivingLicenceMsoMdocHAIP or UniversityDegreeCredentialSDJWTHAIP) and applies HAIP presentation: x509_hash plus encrypted direct_post.jwt (HAIP 1.0 Section 5). Wallet and key attestation (HAIP 1.0 Sections 4.4.1 and 4.5.1) are the issuance half of that selector, not a second dropdown. Custom DCQL, including W3C jwt_vc_json / jwt_vc_json-ld / ldp_vc, can still be pasted under Advanced; the wallet action is blocked unless the selected or pasted credential matches the requested format. On the unencrypted direct_post flow, Looking Glass shows that HAIP coerces the response to direct_post.jwt before Execute. Wallet x5c trust failures against WALLET_VERIFIER_X509_TRUST_ANCHOR_PEM keep the protocol error; the hosted wallet already trusts the showcase verifier CA.

When a credential_jwt is pasted manually, the UI allows submission but warns when the selected auto-issue format 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. Independently issued certificate-backed SD-JWT VCs can still be accepted when they chain to OID4VP_SD_JWT_TRUST_ANCHOR_PEM. The mso_mdoc path uses independently configured IACA trust anchors. HAIP dc+sd-jwt credentials include an OAuth Token Status List; the verifier fetches it and validates the token with the same issuer key already accepted for the credential.

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_value": "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 selectable by sending an explicit dcql_query that names them. Scope aliases are not currently resolved to DCQL and are therefore not conformance evidence. For example:

{
"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.
  • The repository does not currently provide a browser-mediated W3C Digital Credentials API flow. Private request_id APIs are not classified as DC API conformance evidence.
  • 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).
  • Encrypted direct_post.jwt responses are JWEs (ECDH-ES) whose payload is the JSON Authorization Response (vp_token and state as top-level members). The HTTP form contains only response (OID4VP 1.0 Section 8.3.1).

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 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; HAIP also lists A256GCM). The wallet encrypts the Authorization Response as a JWE (ECDH-ES + AES-GCM) whose payload carries vp_token and state as top-level members. For mso_mdoc, 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).
  • 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. Annex B DS profile checks apply to IACA→DS chains; when the configured trust anchor itself signs IssuerAuth (self-signed trust-anchor-as-document-signer), those DS-only checks are skipped after path validation.

Example DCQL query requesting an mdoc mobile driving licence:

{
"credentials": [{
"id": "mdl",
"format": "mso_mdoc",
"meta": { "doctype_value": "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 verifier-side HAIP constraints as an opt-in mode. The hosted web wallet implements the OID4VP wallet HAIP profile for response_mode=direct_post.jwt with client_id_prefix=x509_hash and request_method=request_uri_signed for both sd_jwt_vc and iso_mdl. Browser-mediated W3C Digital Credentials API wallet invocation (dc_api.jwt) remains deferred: it requires a native wallet provider (for example Android Credential Manager), not a hosted web wallet.

  • Public authorization endpoint: https://wallet.protocolsoup.com/authorize (client_id, request_uri, request_uri_method).
  • Request-object retrieval supports GET and RFC-compatible POST (with wallet_nonce).
  • External x509_hash verifiers are accepted only after the request object’s x5c chain validates to WALLET_VERIFIER_X509_TRUST_ANCHOR_PEM (or system roots), the leaf hash matches the x509_hash: client_id, and the holder approves the external verifier in the consent UI.
  • Presentations return DCQL-keyed vp_token objects. SD-JWT disclosures and mdoc IssuerSigned elements are derived from DCQL claim paths (OID4VP §6.4.1: absent/empty claims returns only mandatory material — no SD-JWT disclosures and no selectively disclosable mdoc elements). Only Credential Query ids the presented credential actually satisfies are included as vp_token keys (optional credential_sets entries that do not match are omitted). Encrypted responses use ECDH-ES JWE (response=<compact JWE>).
  • After a successful HAIP response, the wallet navigates a verifier-provided redirect_uri when present and safe (https only, no userinfo, no loopback, private, or internal host). The SPA applies the same check before window.location.assign.
  • dc_api.jwt remains deferred pending a native wallet provider.

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 each 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 Authority Key Identifier is one of the requested values. When IssuerAuth is signed by a self-signed trust anchor that omits AuthorityKeyIdentifier, the leaf SubjectKeyIdentifier is used for that match. 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 contains request-object, handover, audience, decryption, and policy building blocks for this path. The repository does not invoke the browser’s Digital Credentials API, and the hosted wallet explicitly defers dc_api.jwt pending a native provider. These private APIs are implementation scaffolding, not a conforming DC API wallet flow.

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 Identifier Prefix is inferred from client_id; the removed draft client_id_scheme parameter is not emitted.

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 without the root trust anchor. This is the default scheme for HAIP 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 across process restarts instead of changing every boot; wallets that pin trust by leaf hash can keep a stable verifier identity.

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.

  • openid_federation is not currently implemented. Unknown Client Identifier Prefixes follow the Final fallback policy and are treated as pre-registered.

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, aud=https://self-issued.me/v2) 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 compact signed JWT from request_uri with media type application/oauth-authz-req+jwt. GET is the default; request_uri_method=post (OpenID4VP 1.0 Section 5.10) has the wallet POST a wallet_nonce and receive a request object bound to that nonce. The wallet validates signature, typ, and expiry.
  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 (ECDH-ES) whose JSON payload contains vp_token and state
  4. Verifier Validates + Policy Decision — Verifier requires exact state correlation for redirect response modes. When direct_post.jwt omits an outer state, the JWE kid selects the request’s ephemeral decryption key and the decrypted state must then match that request exactly. It 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, and for SD-JWT VC, the Key Binding JWT iat is within a 5-minute freshness window)
  • credential evidence (issuer signature, subject binding, disclosure integrity, and trust policy checks)
  • DCQL claim-path completeness: all required claim paths present in disclosed claims, or, when a Credential Query declares claim_sets, at least one alternative combination of claim ids fully disclosed
  • DCQL credential_sets (when present): each required Credential Set Query’s options has at least one fully-matched combination of Credential Query ids; required: false entries do not block the decision
  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 exact state
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 JSON { "vp_token": ..., "state": ... }
  • 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, including claim_sets alternatives and top-level credential_sets combinations when present
  • 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: JWE decrypts with the request’s ephemeral ECDH-ES key and the payload state matches the authorization request
  • 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