vc-presentation-{0,1,2,3}.html)Read this before changing any vc-presentation-* page or sd_jwt_vp.js. The credential it presents comes from the issuance workflow, and the two meet at the localStorage keys and nowhere else.
The SD-JWT VC presentation workflow (vc-presentation-{0,1,2,3}.html, bundles vcpresentation0..3) is the other half: a Verifier asks for some of a credential over OID4VP 1.0 and the wallet answers with an SD-JWT+KB (RFC 9901 section 4.3). It is reached from its own landing-page card and presents whatever credential the issuance workflow left in localStorage — the two workflows meet at those keys and nowhere else. Step 0 chooses the flow (request by value with the redirect_uri client identifier prefix, a signed Request Object by reference for a pre-registered client, or cross-device by QR code) and all three start at the verifier, because a presentation is something a verifier asks for. Step 1 reads the request — fetching and verifying the Request Object when it is passed by reference — and decodes the DCQL query into claim paths shown against what the credential actually holds; nothing is disclosed there. Step 2 is the point of the format: a checkbox per Disclosure, defaulting to exactly what was asked for, and it builds the presentation in front of you — the issuer-signed JWT, the selected Disclosures, and a Key Binding JWT whose nonce is the request’s, whose aud is the verifier’s Client Identifier, and whose sd_hash covers exactly the bytes being sent. Step 3 shows the verifier’s verdict check by check next to the wallet’s own re-check of those bytes, and an Answered the request? line comparing what the DCQL asked for against what actually went — by name, not by count (counting made withholding one claim and disclosing one unasked-for claim cancel out, and plain withholding read as the reassuring “nothing more than was asked for”). Both directions are named there because neither is something a verifier must report: extra claims are accepted silently, and a presentation that answers only part of the query may be accepted too — walt.id’s verifier has no DCQL-fulfilment policy at all, so it says SUCCESSFUL. The verdict pane is asked only of this debugger’s own mock verifier (resultUrl() requires a Response URI ending /oid4vp/response): that result endpoint is the mock’s invention, and deriving it from any Response URI that arrives made step 3 fetch a 404 from walt.id and present a successful presentation as a failure.
Which private key the presentation is signed with is sd_jwt_vc.js’s boundPrivateJwk(), and it reads the paste field for a reason. The credential names the key it is bound to in cnf.jwk; a Key Binding JWT signed with any other key produces a presentation the Verifier refuses with a complaint about a signature, which says nothing about the key. So that function tests everything in storage against the cnf — the holder key and the DPoP key, since under Holder of Key the DPoP key is the holder key and both are present at once — and only then looks at what the reader pasted. Until 2026-09-01 that last step called readHolderPrivateJwk(), which prefers storage, so it handed back the same stored key the loop had just rejected: a reader who pasted the correct key had it ignored and was told the key they pasted was wrong, about a key they had not pasted. The storage-first read is right for readHolderPrivateJwk()’s other callers on the refresh page (they want the holder key from wherever it is), so the field read was split out as pastedHolderPrivateJwk() rather than the shared function being changed. The four outcomes — a pasted key that matches, one that does not, an unreadable paste, and nothing pasted at all — are now four different sentences and are pinned one per check in tests/sd_jwt_vc_engine.js; the last of them was unreachable while any holder key was stored.
Three things that are easy to get wrong here, and are therefore tested: sd_hash is computed over <Issuer-signed JWT>~<Disclosure>*~ including the trailing tilde, so it commits to the exact byte string; the KB-JWT must verify against the credential’s own cnf.jwk and not a key the presenter nominates; and the vp_token is a JSON object keyed by the DCQL credential query id, each value an array of presentations (OID4VP section 8.1), not a bare string.