Skip to content

luikyv/go-sdjwt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-sdjwt

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.

Installation

go get github.com/luikyv/go-sdjwt

Usage

Issuance

The 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>~

Nested and array disclosures

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"},
})

Options

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(...)

Holder: verify the received SD-JWT

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)

Holder: build a presentation

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>~

Verifier: verify a presentation

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"]
}

Disclosure helpers

// 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())
}

About

A Go implementation of Selective Disclosure JWT (SD-JWT)

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors