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 arrives as a single string with three kinds of components, delimited by tildes (~):

<Issuer-Signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Key Binding JWT>
ComponentDescription
Issuer-Signed JWTThe 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.
DisclosuresBase64url-encoded arrays of [salt, claim_name, claim_value]. Only the claims requested by the OID4VP scope value are included.
Key Binding JWTSigned 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",
  "kid": "d96f1ca3-864f-49c8-92c7-17d341ae1234"
}
📘

The kid references a key in Proof's JWKS.

Issuer-Signed JWT, payload

{
  "iss": "https://api.proof.com",
  "sub": "user_8f3a92b1-47d6-4e2c-b5a0-1c9d3e7f6a84",
  "vct": "https://credentials.notarize.com/ProofCredentialV1",
  "iat": 1739280000,
  "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 match https://api.proof.com (or the sandbox equivalent). Reject if different.
  • vct: must match the vct_values you 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 disclosure you receive must hash to one of these values.

Disclosures (3 of 8 disclosed)

Only the three claims requested via the scope are disclosed. The remaining five claims in the credential are not included in the presentation.

// given_name (base64url decoded)
["2GLC42sKQveCfGfryNRN9w", "given_name", "Darren"]

// family_name (base64url decoded)
["eluV5Og3gSNII8EYnsxA_A", "family_name", "Louie"]

// age_equal_or_over.18 (base64url decoded)
["6Ij7tM-a5iVPGboS5tmvVA", "age_equal_or_over.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. 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, compute SHA-256(base64url_encoded_disclosure_string) and verify the resulting digest appears in the _sd array. 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 the nonce you sent in the authorization request.
  • aud: must match your client_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.jwk from the Issuer-Signed JWT.

If the request bound transaction_data, the Key Binding JWT carries those values as top-level claims. The full catalog is in Transaction Data Templates.


See also: Proof Certificate Authority · Verify a Credential · Verifiable Credential Types