SD-JWT VC explained: credentials for AI agents
An SD-JWT VC is a signed JSON Web Token that carries a type (vct), an issuer, a subject, an optional holder key (cnf) and claims that can be hidden behind salted hashes. SD-JWT itself is RFC 9901, published in November 2025. The VC profile is still an Internet-Draft: revision -23 is dated 31 October 2026.
An SD-JWT VC is a JWT with a type, an issuer and optional hidden claims
A JSON Web Token is a signed JSON object. RFC 9901 adds one idea to it: instead of putting every claim in the signed payload, the issuer can put a digest of the claim there and hand the claim itself, the "Disclosure", to the holder separately. The holder decides which Disclosures to show a given verifier. The RFC's abstract says it defines "a mechanism for the selective disclosure of individual elements of a JSON data structure" used as the payload of a JSON Web Signature.
On the wire, parts are separated by tildes: <Issuer-signed JWT>~<D.1>~<D.2>~...~<D.N>~<KB-JWT>. The last part, a Key Binding JWT, is optional. When there is no Key Binding JWT, RFC 9901 (Section 4) says the last element must be an empty string. A credential with no Disclosures at all is therefore the issuer-signed JWT followed by one tilde.
The "VC" in SD-JWT VC is a profile on top of that container. It answers questions the container leaves open: what kind of credential is this, who issued it, how the issuer's key is found, and which claims may never be hidden. The profile is what a wallet or a verifier implements when it says it accepts "SD-JWT VC".
The profile is still a draft, and -23 is on the IESG agenda
SD-JWT VC is not an RFC. The datatracker page read on 2 November 2026 shows draft-ietf-oauth-sd-jwt-vc-23, dated 31 October 2026, in IESG Evaluation, with the IESG telechat of 5 November 2026 listed. Its abstract says the draft "describes data formats as well as validation and processing rules to express Verifiable Digital Credentials with JSON payloads with and without selective disclosure based on the SD-JWT format."
A draft can still change. Anything built against -23 today is built against a document that reviewers may yet edit. The claim rules below come from the full text of -23, Section 2.2.2.
| Claim | Requirement in -23 | Can it be selectively disclosed? |
|---|---|---|
vct | REQUIRED, the type of the credential | No |
iss | OPTIONAL, "when it is not conveyed by other means" | No |
nbf | OPTIONAL, the credential must not be accepted before this time | No |
exp | OPTIONAL, the credential is invalid after this time | No |
cnf | OPTIONAL, but REQUIRED if key binding is to be supported | No |
status | OPTIONAL, how to read the credential's status | No |
iat | OPTIONAL, time of issuance | Yes |
sub | OPTIONAL, the subject's identifier | Yes |
The vct value is "a case-sensitive string serving as an identifier for the type" and must be a Collision-Resistant Name. RFC 7519 defines that as a name in a namespace where collisions are highly unlikely, and lists domain names, OIDs and UUIDs as examples.
Selective disclosure is salted hashing, not encryption
A Disclosure for an object property is a three-element JSON array: a salt, the claim name and the claim value (RFC 9901, Section 4.2.1). The array is base64url-encoded. The digest that goes into the signed payload, under the _sd key, is computed "over the US-ASCII bytes of the base64url-encoded value that is the Disclosure" (Section 4.2.3). If _sd_alg is absent, sha-256 applies.
Computing both values for an arbitrary salt and a made-up claim shows how little is involved:
# arr = ["Ma4ZyUq3mQ0xd1B5", "region", "EU"]
Disclosure: WyJNYTRaeVVxM21RMHhkMUI1IiwicmVnaW9uIiwiRVUiXQ
_sd digest: 4GF-TeoeSgpiS_efsSeCrI3Ac8epW3iG224IZEQodocTwo properties follow. A verifier that holds the signed JWT but not the Disclosure sees only the digest. A verifier that receives the Disclosure recomputes the digest and checks that it appears in _sd. Nothing is encrypted: whoever holds the Disclosure string can read it.
The salt matters. RFC 9901 (Section 9.3) says each salt must be cryptographically random, and "the RECOMMENDED minimum length of the randomly generated portion of the salt is 128 bits". Without a random salt, a short, guessable value such as "EU" could be found by hashing candidates.
The verifier must learn the issuer's key from somewhere
A valid signature proves that some key signed the credential. It does not say whose key that is. RFC 9901 (Section 7.1, step 2) tells the verifier to "Validate the Issuer and that the signing key belongs to this Issuer". The SD-JWT VC draft is where a concrete method is defined.
The SD-JWT VC draft (Section 2.5) defines two mechanisms:
- HTTPS issuer metadata. "When the value of the
issclaim of the Issuer-signed JWT is an HTTPS URI", the verifier fetches the keys from JWT VC Issuer Metadata, served under/.well-known/jwt-vc-issuer(Section 4). - X.509 in the header. When the protected header carries
x5c, the verifier takes the key from the end-entity certificate and validates the chain against a trust anchor it permits.
If the key cannot be tied to the issuer by a permitted mechanism, "the SD-JWT VC MUST be rejected". The draft also says other specifications or ecosystems "may define additional key discovery and validation mechanisms", so a deployment can use something else, as long as both sides agree on it.
The security section (7.3) is blunt about the risk: "A Verifier MUST ensure that for any given iss value, an attacker cannot influence the type of verification process used." If an attacker can pick the verification path, they can pick the one that accepts their own key.
For agent credentials this is the central question. A credential saying "this key may act for that owner" is worth exactly as much as the verifier's trust in the issuer key.
Key binding ties a credential to a key the holder controls
A bearer credential works for whoever copies it. Key binding closes that. The issuer puts the holder's public key into cnf, and the holder proves possession when presenting. In RFC 9901 the proof is a Key Binding JWT, signed by the holder's key, with iat, aud, nonce and sd_hash. The sd_hash is "the base64url-encoded hash value over the Issuer-signed JWT and the selected Disclosures", so the proof is tied to exactly what was shown.
The VC draft keeps this optional. cnf is "OPTIONAL unless cryptographic Key Binding is to be supported, in which case it is REQUIRED" (Section 2.2.2.3), and a presentation can appear "without a KB-JWT". It also advises (Section 8.3) that verifiers and holders "are advised not to follow or resolve remote references for key material in the cnf claim".
Two other checks sit outside the signature. The draft says that if status is present, "the status SHOULD be checked", and acceptance is left to verifier policy (Section 2.4). And nbf and exp must be applied; a credential outside its window is not valid.
A checklist for accepting a credential from an agent
- Accept the signing algorithm only if the application deems it secure; never
none(RFC 9901, Section 7.1). - Decide how the issuer key is found before the credential arrives, and fix that choice per issuer. An attacker should not be able to nominate the method.
- Compare
isswith the party you expect. A valid signature from an unexpected issuer is a valid signature from a stranger. - Check
vctagainst the types you accept. - Apply
nbfandexpwith your own clock. - Decide what a missing or unreachable
statusmeans. Fail closed or fail open is a policy, and it should be written down. The fail-closed entry covers that trade-off for gateways. - If the credential is meant to bind an agent, require proof that the presenter holds the
cnfkey. A Key Binding JWT is one way. Signing each request is another; see RFC 9421 for AI agents.
Where Mandare fits, and where it departs from the draft
Mandare's Passport uses SD-JWT VC as the envelope for three documents (the authority's owner attestation, the delegation credential and the mandate), built on the @sd-jwt/core and @sd-jwt/sd-jwt-vc libraries (version range ^0.20.0 in the passport package). Statements below were checked against the public repository at commit 327442e (3 October 2026) and apply to @mandarelabs/cli 0.1.0; see also the Concepts page.
- Delegation credential. delegation.ts issues it with
vcturn:mandare:credential:agent-delegation, the owner'sdid:keyasiss, the agent'sdid:keyassub, and acnfJWK carrying the agent's Ed25519 key. It adds anattestation(a countersignature by an authority over the owner), arevocation_refand akey_provenance. - Mandate. The owner-signed mandate travels as an SD-JWT VC with
vcturn:mandare:credential:mandate. The mandate JSON inside keeps its own detached owner signature, so it verifies without the envelope.
The departures matter more than the match:
- The issuer is a did:key, not an HTTPS URI or a certificate chain. In sd-jwt.ts the verification key is derived from the
issvalue itself, offline. The code comment says the check proves "signed by the key the iss DID names" and nothing more. The caller must still check that the issuer is the party it expects. In the passport chain,verifyPassporttakes a trusted authority DID as an input. In the draft's terms this would be an additional mechanism defined by an ecosystem, not one of the two the draft defines. We make no claim of interoperability with third-party SD-JWT VC wallets or verifiers, and we make no claim that these credentials conform to draft -23. - The type values are URNs. The
vctvalues areurn:mandare:credential:…. Whether that meets the draft's Collision-Resistant Name rule is not something we claim. - No selective disclosure is used. The authority's attestation code says it carries "nothing selectively disclosable", and the delegation and mandate issue calls list their claims without a disclosure frame.
- No Key Binding JWT. The function
verifyPassportreadscnfand checks that it equals the agent'sdid:key. Possession is proved by RFC 9421 request signatures made with that key, not by a presentation-time Key Binding JWT. - No status claim. The credentials carry a
revocation_refstring, and the gateway reads revocation from the local ledger instead of fetching a status list. Enforcement stays offline. - Owner attestation is a mock. The KYC step behind the attestation is a mock provider, so a Mandare passport is a signed identity, and Mandare does not verify who the owner is.
An SD-JWT VC also says nothing about spending. A signed identity answers who is acting; the limit on what that agent may spend is the mandate, and the record of what happened is the ledger.
Questions
What is the difference between SD-JWT and SD-JWT VC?
SD-JWT (RFC 9901) is the container format: a signed JWT plus optional Disclosures and an optional Key Binding JWT. SD-JWT VC is a profile on top of it. It requires a vct claim, defines how to find the issuer's key, and says which claims must stay visible.
Is SD-JWT VC an RFC yet?
Not as of 2 November 2026. The IETF datatracker lists draft-ietf-oauth-sd-jwt-vc-23, dated 31 October 2026, in IESG Evaluation and on the agenda for the IESG telechat of 5 November 2026. RFC 9901 (SD-JWT, the base format) is already published.
Does an SD-JWT VC have to use selective disclosure?
No. The draft's abstract says the format covers JSON payloads 'with and without selective disclosure', and RFC 9901 defines an SD-JWT without Disclosures as the issuer-signed JWT followed by a single tilde. A credential for a machine identity can carry every claim in plain view.
How does a verifier know who issued an SD-JWT VC?
The draft defines two ways: JWT VC Issuer Metadata when iss is an HTTPS URI, or an x5c certificate chain in the header. If the key cannot be tied to the issuer by a permitted mechanism, the draft says the credential must be rejected. Ecosystems may define further mechanisms.
Sources
Run it yourself: 3 commands, no API keys.
github.com/mandarelabs/mandare →