A Go implementation of SD-JWT (RFC 9814), the selective disclosure standard for JWTs.
SD-JWT lets an issuer create a signed credential where individual claims can be independently disclosed. The holder later presents only the subset of claims the verifier needs, without the issuer's involvement and without breaking the cryptographic integrity of the credential.
go get github.com/luikyv/go-sdjwtThe issuer marks sensitive claim values with SD(). All other claims are always visible.
import (
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/luikyv/go-sdjwt/sdjwt"
)
issuerSigner, _ := jose.NewSigner(
jose.SigningKey{Algorithm: jose.PS256, Key: issuerKey},
(&jose.SignerOptions{}).WithType("sd-jwt"),
)
serialized, err := sdjwt.Signed(issuerSigner).Claims(
jwt.Claims{
Issuer: "https://issuer.example.com",
Subject: "user-42",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
map[string]any{
"name": sdjwt.SD("Alice"), // selectively disclosable
"email": sdjwt.SD("alice@example.com"), // selectively disclosable
"role": "admin", // always visible
},
).Serialize()Serialize() returns a tilde-separated string:
<issuer-JWT>~<disclosure-1>~<disclosure-2>~
sdjwt.Signed(issuerSigner).Claims(map[string]any{
// Nested: the address object is always visible, but street is selectively disclosable.
"address": map[string]any{
"street": sdjwt.SD("123 Main St"),
"city": "Anytown",
},
// Fully selectively disclosable object (name itself is also hidden).
"employer": sdjwt.SD(map[string]any{
"name": sdjwt.SD("Acme Corp"),
"department": "Engineering",
}),
// Array with mixed plain and selectively disclosable elements.
"nationalities": []any{"US", sdjwt.SD("FR"), "DE"},
})sdjwt.Signed(issuerSigner).
Hash(sha512.New()). // default: sha-256
Decoys(3). // add 3 random decoy digests to obscure claim count
SaltFunc(myRandFunc). // override the default random salt generator
Claims(...)After receiving the serialized SD-JWT from the issuer, the holder verifies it and resolves all disclosed claims.
sdJWT, err := sdjwt.ParseSigned(serialized, []jose.SignatureAlgorithm{jose.PS256})
if err != nil {
// ...
}
var claims map[string]any
if err := sdJWT.Claims(issuerKey.Public(), &claims); err != nil {
// signature verification failed or claims are invalid
}
// claims["name"] == "Alice", claims["email"] == "alice@example.com", ...Claims accepts multiple destination structs, which is useful for extracting typed sub-sections:
var base jwt.Claims
var extra struct {
Name string `json:"name"`
Email string `json:"email"`
}
sdJWT.Claims(issuerKey.Public(), &base, &extra)The holder selects which disclosures to reveal and attaches a key-binding JWT to bind the presentation to a specific verifier request.
The sd_hash claim in the key-binding JWT must cover the exact set of disclosures being presented (per spec §4.3.1), so disclosures must be selected before computing it.
// 1. Select the disclosures to present.
selected := sdJWT.DisclosuresByNames("name", "email")
// 2. Compute sd_hash over the presentation that will be sent.
// Hash() covers: <issuerJWT>~<d1>~...~<dN>~
sdHash, err := sdJWT.Hash(selected)
// 3. Build the key-binding JWT.
holderSigner, _ := jose.NewSigner(
jose.SigningKey{Algorithm: jose.RS256, Key: holderKey},
(&jose.SignerOptions{}).WithType("kb+jwt"),
)
kbJWT, _ := jwt.Signed(holderSigner).Claims(map[string]any{
"nonce": nonce, // from verifier.
"aud": "https://verifier.example.com", // verifier's identifier.
"iat": time.Now().Unix(),
"sd_hash": sdHash,
}).Serialize()
// 4. Build the presentation.
presentation, err := sdJWT.Serialize(selected, kbJWT)
// presentation: <issuerJWT>~<d1>~<d2>~<kbJWT>To present without key binding (no kbJWT), pass an empty string:
presentation, err := sdJWT.Serialize(selected, "")
// presentation: <issuerJWT>~<d1>~<d2>~parsed, err := sdjwt.ParseSigned(
presentation,
[]jose.SignatureAlgorithm{jose.PS256, jose.RS256},
)
if err != nil { ... }
// Verify the issuer signature and resolve the presented claims.
// Use a typed struct to extract the holder's public key from cnf.
var claims map[string]any
var cnf struct {
Cnf struct {
JWK jose.JSONWebKey `json:"jwk"`
} `json:"cnf"`
}
if err := parsed.Claims(issuerKey.Public(), &claims, &cnf); err != nil { ... }
// Verify the key-binding JWT with the holder's key.
if kb := parsed.KeyBindingJWT(); kb != nil {
var kbClaims map[string]any
if err := kb.Claims(cnf.Cnf.JWK.Public(), &kbClaims); err != nil { ... }
// validate kbClaims["nonce"], kbClaims["aud"], kbClaims["iat"], kbClaims["sd_hash"]
}// Find a single disclosure by claim name.
d, ok := sdJWT.DisclosureByName("name")
// Find multiple disclosures by name (in order).
ds := sdJWT.DisclosuresByNames("name", "email", "phone")
// Iterate all disclosures (useful for array elements, which have no claim name).
for _, d := range sdJWT.Disclosures() {
fmt.Println(d.ClaimName(), d.Value())
}