Advanced OID4VP profiles and testing
Use this guide when you need a local synthetic wallet response, mDOC trust anchors, or the signed and encrypted request profile.
Build a local wallet response
vp_token must be a real SD-JWT-VC signed by the issuer_id given in step 1, with a Key Binding JWT bound to this session's nonce. The Gateway verifies both against the trust store (src/core/crypto/trust-store.ts) before accepting the presentation. There's no shortcut around that from cURL alone, but src/sandbox/mock-wallet/ builds one for you: it generates a synthetic issuer/holder keypair, seeds the issuer's public key into CSCA_CACHE, issues an SD-JWT-VC with the requested claims selectively disclosed, and signs a Key Binding JWT against the session's real nonce/aud, so the resulting AuthorizationResponsePayload passes the Gateway's real verifier, not a stand-in for it.
import { simulateWalletResponse } from "../src/sandbox/mock-wallet";
const payload = await simulateWalletResponse({
kv: env.CSCA_CACHE, // the same CSCA_CACHE binding the Gateway reads
authorizationRequest, // parsed from qr_code_url's openid4vp:// query params
issuerId: "https://issuer.example.gov", // must match step 1's issuer_id
disclosedClaims: { is_over_18: true },
});
// POST `payload` (as JSON) to /v1/oid4vp/response/:session_idThis only runs inside the Workers runtime because it needs the CSCA_CACHE KV binding. Use it in a Worker-side script or vitest test. See test/sandbox/mock-wallet/index.test.ts for a full round-trip example. It does not run as a plain Node script alongside the cURL calls above.
mDL (mDOC) trust anchors
Steps 1–4 walk a vc+sd-jwt presentation, whose issuer trust is a bare JWK in CSCA_CACHE (seeded synthetically by src/sandbox/mock-wallet above). An mso_mdoc presentation (ISO/IEC 18013-5 mDL) is different: its IssuerAuth.x5chain is authenticated up to a real X.509 CSCA root (src/core/crypto/csca.ts) and revocation-checked via OCSP/CRL (revocation.ts), so seed real ICAO CSCA roots in CSCA_CACHE before a staging or production Gateway verifies an mDOC. The sandbox's synthetic keys are not sufficient.
Obtain CSCA certificates from an ICAO Doc 9303 Part 12 Master List in the ICAO PKD portal or from a national registry such as BSI or A-SIT. These sources require registered, authenticated access, so an operator must prepare the file manually. Then seed the certificates:
# 1. Verify the CMS envelope and extract its eContent (the CscaMasterList payload)
openssl cms -verify -noverify -inform DER -in icaoMasterList.ml -out cscaMasterList.der
# 2. Parse the ASN.1 SET OF Certificate into a PEM bundle
npm run extract:csca-masterlist -- --input cscaMasterList.der --output csca-bundle.pem
# 3. Inspect what would be seeded (validity window, BasicConstraints.cA,
# KeyUsage.keyCertSign). These match the checks verifyCscaChain applies at
# verification time, so CSCA_CACHE will not contain rejected certificates.
npm run import:csca-masterlist -- --input csca-bundle.pem --env staging --dry-run
# 4. Seed the live CSCA_CACHE KV after reviewing the dry-run output
npm run import:csca-masterlist -- --input csca-bundle.pem --env staging --remote--remote is what actually writes to a live KV namespace; omit it and the import writes to wrangler dev's local emulated KV instead, regardless of --env. For production, drop --env staging (production is wrangler.jsonc's top-level, unnamed environment) and keep --remote.
Signed and encrypted OID4VP (JAR profile)
OID4VP_REQUEST_MODE in each environment's wrangler.jsonc vars selects between two Authorization Request profiles:
"inline"(default): the request is unsigned and carried by value in theopenid4vp://URI; the wallet's response is a plaindirect_postback toresponse_uri."jar": the request is a signed Request Object (RFC 9101) fetched by reference fromGET /v1/oid4vp/request/:session_id.qr_code_urlcarries onlyclient_id(did:web:<host>) andrequest_uri, no inlinepresentation_definition. The wallet resolves it, verifies the JWS againstGET /.well-known/jwks.json, and encrypts itsvp_tokenresponse into adirect_post.jwt(ECDH-ES, per the Request Object'sclient_metadata.jwksephemeral key) instead of posting it in the clear.
Both profiles use the same parseAuthorizationResponse function and return the same claims and verification_details shape in step 3. The jar profile only changes request delivery and response transport.
Run staging smoke checks
Two Node scripts drive a real deployment end-to-end (not the Worker test harness) for both credential formats, auto-detecting whichever profile that deployment is actually running from the shape of qr_code_url:
STAGING_URL=https://<staging-worker>.workers.dev STAGING_API_KEY=sk_test_... \
npm run smoke:mdoc-staging # mso_mdoc (ISO 18013-5)
STAGING_URL=https://<staging-worker>.workers.dev STAGING_API_KEY=sk_test_... \
npm run smoke:sdjwt-staging # vc+sd-jwtEach script seeds a synthetic issuer's key into that environment's CSCA_CACHE KV, builds a real presentation with src/sandbox/mock-wallet, and asserts that the session reaches VERIFIED with the expected claim. Run both after a deploy that changes OID4VP_REQUEST_MODE, core/crypto/jws.ts, or core/crypto/jwe.ts.