SD-JWT VC Format
SD-JWT VC is the credential format Proof uses, defined in RFC 9901 and SD-JWT VC draft 16.
SD-JWT VC is a Verifiable Credential format that encodes a credential as a Selective Disclosure JWT, defined in RFC 9901 and profiled by draft-ietf-oauth-sd-jwt-vc-16. Proof issues every Verifiable Credential in this format.
A presentation of an SD-JWT VC is a single string with three kinds of components, delimited by tildes (~). In an OID4VP response it is wrapped in the vp_token object described in Verify a Credential:
<Issuer-Signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Key Binding JWT>
| Component | Description |
|---|---|
| Issuer-Signed JWT | The base SD-JWT signed by Proof. Carries hashed claim digests in the _sd array, the Holder's public key in the cnf claim, and the credential type in vct. |
| Disclosures | Base64url-encoded arrays of [salt, claim_name, claim_value]. Only the claims requested by the OID4VP scope value are included. |
| Key Binding JWT | Signed by the Holder's private key in Proof's Cloud Wallet. Carries nonce, aud, and iat. Proves Holder possession, prevents replay, and optionally binds the presentation to specific transaction_data. |
Sample payload
The following shows a complete, decoded vp_token for a https://credentials.notarize.com/ProofCredentialV1 presentation where the Verifier requested given_name, family_name, and age_equal_or_over.18. Each component is shown separately.
Issuer-Signed JWT, header
{
"alg": "ES256",
"typ": "dc+sd-jwt",
"x5c": ["<Proof.com end-entity certificate>", "<Proof Organization Authenticity Issuing CA R1>"]
}
Thex5cheader carries the signing certificate chain, leaf first, without the root. Validate it up to Proof Root CA R1; see Proof Certificate Authority.
Issuer-Signed JWT, payload
{
"iss": "https://api.proof.com",
"vct": "https://credentials.notarize.com/ProofCredentialV1",
"iat": 1739280000,
"_sd_alg": "sha-256",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ",
"y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8"
}
},
"_sd": [
"--NrmJe6y2u_S2dC8RKHHv3rElWsqpY8r6xslJuwaJI",
"JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE",
"UgEtkU6-DwPQXQM-UrsaWMd7MOWMB_3iZjDeyp6gvUY",
"S4sER_VP45OHkmC7-BVsj2NqlA_0jhsJE7P0rTkkUFQ",
"bViw5OlxKJBrlXkjSBkz2MERPl1gUHnKxHmkR6Zf6Mk",
"h4JMnGjFOh9HfOB-r09jqeMxEpGELe3BDLn_fuY34vs",
"qF42T_IHyzGvTIGLH1gPMFGVLW92-Ol9HwFBaFDuGB4",
"UkVeSoup2-mIUXvkbsm2aQr6BdwyW34Tydo6PtLINQ0"
]
}The cnf.jwk is the public key of the user's end-entity certificate, issued by Proof Individual Authenticity Issuing CA R1. See Proof Certificate Authority for the full chain.
What to validate:
iss: must exactly matchhttps://api.proof.com(or the sandbox equivalent). Reject if different.vct: must match thevct_valuesyou specified in your DCQL query.iat: issuance timestamp. Use for audit logging. Optionally reject credentials older than your policy allows.cnf.jwk: the Holder's public key. Use this to verify the Key Binding JWT signature, and nothing else._sd: array of base64url-encoded SHA-256 digests. Each top-level disclosure must hash to one of these values; a nested disclosure hashes to a digest in its parent's_sd. The array also contains random decoy digests, so its length does not reveal how many claims the credential holds.
Disclosures (4 disclosed)
The basic scope discloses given_name, family_name, and age_equal_or_over.18. Because 18 is nested inside age_equal_or_over, it takes two disclosures: one for the age_equal_or_over object and one for its 18 member. The other claims (birthdate, age_equal_or_over.21, age_equal_or_over.65, and is_national.us when present) are not included.
// given_name (base64url decoded)
["2GLC42sKQveCfGfryNRN9w", "given_name", "Darren"]
// family_name (base64url decoded)
["eluV5Og3gSNII8EYnsxA_A", "family_name", "Louie"]
// age_equal_or_over (base64url decoded)
["Qg_O64zqAxe412a108iroA", "age_equal_or_over", {"_sd": ["<digest>", "<digest>", "<digest>"]}]
// age_equal_or_over.18 (base64url decoded)
["6Ij7tM-a5iVPGboS5tmvVA", "18", true]Each disclosure is an array of three elements:
[0]: salt. A random value that prevents rainbow-table attacks on claim digests.[1]: claim name. The disclosed attribute. For a nested claim this is the member name (18), not the full path. The key you use when extracting claims.[2]: claim value. Type depends on the claim: strings for names, booleans for age threshold claims, objects for structured data.
For each disclosure, computeSHA-256(base64url_encoded_disclosure_string)and verify the resulting digest appears in the_sdarray. Hash the raw base64url string as received, not the decoded JSON.
Key Binding JWT
Header:
{
"alg": "ES256",
"typ": "kb+jwt"
}Payload:
{
"nonce": "n-0S6_WzA2Mj_6a8bRs2TU",
"aud": "camdrbpxd",
"iat": 1739280120,
"sd_hash": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
}What to validate:
nonce: must exactly match thenonceyou sent in the authorization request.aud: must match yourclient_id.iat: presentation creation time. Reject if too far in the past (recommended: no more than 5 minutes).sd_hash: SHA-256 hash over the Issuer-Signed JWT concatenated with all disclosures (with~delimiters). Binds the KB-JWT to this specific presentation.- Signature: verify with
cnf.jwkfrom the Issuer-Signed JWT.
If the request bound transaction_data, the Key Binding JWT carries each value's payload as a top-level claim named after its template (wire_instructions_v1, payment_mandate_v1, payment_itemized_v1, session_data). The full catalog is in Transaction Data Templates.
See also: Proof Certificate Authority · Verify a Credential · Verifiable Credential Types
Updated 13 days ago