Skip to content

SD JWT VC

github-actions[bot] edited this page Jul 8, 2026 · 2 revisions

SD-JWT VC (selective disclosure)

SD-JWT VC lets an issuer mark claims as selectively disclosable: the holder later presents only the claims it chooses, and proves possession of the key the credential was bound to (cnf) with a Key Binding JWT over the verifier's aud + nonce.

Issue → present → verify

from openvc.keys import Ed25519SigningKey
from openvc.proof.sd_jwt import SdJwtVcProofSuite

issuer = Ed25519SigningKey.generate(kid="https://issuer.example#key-1")
holder = Ed25519SigningKey.generate(kid="holder-key-1")
suite = SdJwtVcProofSuite()

# Issue: given_name and age are selectively disclosable; cnf binds the holder key.
sd_jwt = suite.issue(
    {"iss": "https://issuer.example", "given_name": "Ada",
     "family_name": "Lovelace", "age": 36},
    signing_key=issuer, disclosable=["given_name", "age"],
    holder_jwk=holder.public_jwk(), vct="https://credentials.example/identity")

# Present: the holder signs a KB-JWT over this verifier's aud + nonce.
presentation = suite.create_presentation(
    sd_jwt, holder_key=holder, audience="https://verifier.example", nonce="n-123")

# Verify: issuer signature, disclosure digests, KB-JWT, aud/nonce, vct.
result = suite.verify(
    presentation, public_key_jwk=issuer.public_jwk(),
    audience="https://verifier.example", nonce="n-123", require_key_binding=True,
    expected_vct="https://credentials.example/identity")
print(result.vct, result.key_bound, result.claims["given_name"])

The pipeline also accepts SD-JWT presentations directly — verify_credential(presentation, ...) detects the format and resolves the issuer key from iss (see Resolving issuer keys).

Type Metadata (vct + vct#integrity)

An issuer can pin the credential type's published Type Metadata document with vct#integrity; the verifier resolves the document, checks the hash, and validates the disclosed claims against the type's claims metadata (paths + mandatory):

import base64
import hashlib
import json

from openvc.keys import Ed25519SigningKey
from openvc.proof.sd_jwt import SdJwtVcProofSuite
from openvc.type_metadata import validate_type_metadata

VCT = "https://credentials.example.com/education_credential/v1"
type_metadata = {
    "vct": VCT,
    "name": "Example Education Credential",
    "claims": [
        {"path": ["name"], "sd": "always", "mandatory": True},
        {"path": ["degree"], "sd": "always"},
    ],
}
metadata_bytes = json.dumps(type_metadata).encode()
integrity = "sha256-" + base64.b64encode(hashlib.sha256(metadata_bytes).digest()).decode()

issuer = Ed25519SigningKey.generate(kid="https://issuer.example#key-1")
holder = Ed25519SigningKey.generate(kid="holder-key-1")
suite = SdJwtVcProofSuite()
issued = suite.issue(
    {"iss": "https://issuer.example", "vct#integrity": integrity,
     "name": "Ada Lovelace", "degree": "Mathematics"},
    signing_key=issuer, vct=VCT, disclosable=["name", "degree"],
    holder_jwk=holder.public_jwk())

verified = suite.verify(issued, public_key_jwk=issuer.public_jwk())
result = validate_type_metadata(
    verified.claims, vct=verified.vct,
    vct_integrity=verified.claims.get("vct#integrity"),
    resolve=lambda url: {VCT: metadata_bytes}[url])   # in-memory; real: fetch by URL
print(result.documents[0]["name"], [c["path"] for c in result.claims])

In production, resolve Type Metadata over the network with the SSRF-guarded default — openvc.resolvers.default_type_metadata_resolver — instead of the in-memory lambda above.

Caveat: keep enforcement pointers non-disclosable

If a credential's credentialStatus (or credentialSchema) must be enforceable, do not list it in disclosable: a holder could simply omit the disclosure and the verifier would never see the pointer. Fail-closed status only works for claims the holder cannot withhold — the same applies to ecdsa-sd-2023's mandatory pointers (see the Security model).

Clone this wiki locally