SD-JWT VC mandate (draft-ietf-oauth-sd-jwt-vc + RFC 7800 + RFC 8707)
Headers and claims
Headers and claims:
- Header
typ=vc+sd-jwtper the draft §5 - Header
alg=EdDSA(default; Ed25519) iss=https://as.oid4pay.comsub=<principal_id>aud=<merchant_resource_url>per RFC 8707iat,nbf,expvct=https://schema.oid4pay.com/oid4ac/mandate/v1cnf.jkt=<agent_dpop_key_thumbprint>per RFC 7800 + RFC 9449 §6authorization_details(RAR, RFC 9396) carrying capabilities, line_items, offer_digest, step_up_triggersoid4ac_version="1.0"credentialStatus.statusListIndex,credentialStatus.statusListCredential=https://as.oid4pay.com/oauth/status-list/{list_id}
Key-Binding JWT (KB-JWT, draft §4.3), REQUIRED for every presentation: when the agent presents
the mandate at a merchant, it MUST attach a KB-JWT signed by the same DPoP key naming the mandate
as iat, the merchant's resource URL as aud, a per-presentation nonce, and a fresh iat. Without KB-JWT a leaked mandate is trivially
replayable.
Selective disclosure (_sd)
The mandate's _sd array carries hashed claims the holder may selectively reveal at presentation
time. Disclosable claims in v1:
principal_emailprincipal_countryprincipal_phoneconsent_evidence.ipconsent_evidence.user_agent
The merchant verifier sees only what was disclosed; the Wallet Portal audit log retains the full unredacted record.
Compact form
The presentation is the SD-JWT VC followed by tilde-separated disclosures and the KB-JWT:
<sd-jwt-vc>~<disclosure1>~<disclosure2>~<kb-jwt>KB-JWT
Header: { "typ": "kb+jwt", "alg": "EdDSA" }
Body: {
"iat": 1747260400,
"aud": "https://shop.alpacanica.com",
"nonce": "sha256(merchant_nonce || offer_digest)",
"sd_hash": "<b64url SHA-256 of the SD-JWT VC + disclosures>"
}Verifier rules
- Header
typ=vc+sd-jwtexactly. - Signature against the AS JWKS.
iss=https://as.oid4pay.com.audincludes the merchant's resource URL.expin the future.credentialStatusbit not set in the W3C VC Status List.- KB-JWT signature against the key whose thumbprint matches
cnf.jkt. - KB-JWT
audmatches the merchant origin exactly. - KB-JWT
noncematches the per-presentation merchant-computed value. - KB-JWT
iatwithin 60 s of server clock.
Worked example
import { verifyMandate } from "@oid4pay/oid4ac-merchant";
import { createHash, randomBytes } from "node:crypto";
const merchantNonce = randomBytes(16).toString("base64url");
const expectedNonce = createHash("sha256")
.update(`${merchantNonce}:${offerDigest}`)
.digest("base64url");
const m = await verifyMandate(sdJwtVc, kbJwt, asJwks, {
expectedAudience: "https://shop.alpacanica.com",
expectedNonce,
});
// m.spendCapMinor, m.currency, m.merchantAllowlist available
// m.disclosed = subset of _sd claims the agent revealed