sdjwt

package module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 13 Imported by: 0

README

go-sdjwt

SD-JWT and SD-JWT VC for the EUDI Wallet ecosystem in Go: combined-format parsing, disclosure digest verification, issuer-JWS and Key-Binding-JWT verification, status-list reference extraction, and an Issue + PresentKB façade for issuers and test wallets.

  • Framework-free; all cryptography delegated to github.com/gmb-eudi/go-eudi-crypto (ECCG-pinned policy; algorithms are derived from keys/policy, never chosen by tokens). No algorithm literals.
  • Verify: split combined format, verify issuer JWS, enforce typ/iss/vct/ validity/cnf, reconstruct disclosed claims by digest, verify KB-JWT, extract status reference.
  • typ dc+sd-jwt enforced; legacy vc+sd-jwt accepted only behind an explicit compatibility option.
  • Two façades so selective-disclosure logic never forks between issuer and verifier.
  • NewIssuer(kp, keyID, WithChain(chain)) embeds an x5c certificate chain (leaf first) in every issued credential's JWS header (RFC 7515 §4.1.6); the two-argument NewIssuer(kp, keyID) is unchanged and embeds no x5c.
  • Peek([]byte) (*PeekResult, error) is a pre-trust structural read: it returns typ / x5c chain / iss / vct / disclosure count WITHOUT verifying the signature, digests, or validity, so a caller can resolve the issuer key from the x5c chain (against a trust anchor) BEFORE calling Verify with that key (ADR-0004: key resolution/trust stays outside this library). NEVER trust a PeekResult directly — verify with Verify first.

Implemented specs: SD-JWT (RFC 9901, finalized from draft-ietf-oauth-selective-disclosure-jwt), draft-ietf-oauth-sd-jwt-vc (dc+sd-jwt), HAIP 1.0 SD-JWT VC profile, ARF 2.9 §6.6.3.6/6.6.3.8. See SPECREFS.md for pinned versions.

Status: pre-v1. API frozen no earlier than OIDF conformance pass.

Documentation

Overview

Package sdjwt implements SD-JWT (RFC 9901, finalized from draft-ietf-oauth-selective-disclosure-jwt) and SD-JWT VC (draft-ietf-oauth-sd-jwt-vc, dc+sd-jwt): combined-format parsing, disclosure digest verification, issuer-JWS and Key-Binding-JWT verification, status-list reference extraction, and an Issue + PresentKB façade for issuers and test wallets.

All cryptography is delegated to github.com/gmb-eudi/go-eudi-crypto: the package holds no algorithm string literals; hash algorithms, JOSE signatures, and JWK conversions come from the ECCG-pinned policy. Time is injected (Verifier clock; PresentKB now parameter). Errors are typed sentinels carrying protocol identifiers and claim names only, never claim values.

Import go-eudi-crypto with an alias to avoid clashing with the standard library:

eudicrypto "github.com/gmb-eudi/go-eudi-crypto"

Index

Constants

This section is empty.

Variables

View Source
var (
	// Combined-format parsing.
	ErrMalformed       = errors.New("sdjwt: malformed SD-JWT")              // err:credential:parse
	ErrTooLarge        = errors.New("sdjwt: presentation exceeds size cap") // err:credential:parse
	ErrDisclosureLimit = errors.New("sdjwt: disclosure count exceeds cap")  // err:credential:parse

	// Disclosure / digest reconstruction.
	ErrDisclosure      = errors.New("sdjwt: invalid disclosure")                    // err:credential:parse
	ErrDigestMismatch  = errors.New("sdjwt: disclosure digest not referenced")      // err:credential:integrity
	ErrDuplicateDigest = errors.New("sdjwt: digest referenced more than once")      // err:credential:integrity
	ErrHashAlg         = errors.New("sdjwt: unsupported _sd_alg")                   // err:credential:parse
	ErrClaimCollision  = errors.New("sdjwt: disclosed claim collides with a claim") // err:credential:integrity

	// Issuer JWT + envelope.
	ErrIssuerSignature = errors.New("sdjwt: issuer signature verification failed")   // err:credential:integrity
	ErrType            = errors.New("sdjwt: unexpected typ header")                  // err:credential:parse
	ErrMissingIssuer   = errors.New("sdjwt: iss claim missing")                      // err:credential:parse
	ErrMissingVCT      = errors.New("sdjwt: vct claim missing")                      // err:credential:parse
	ErrExpired         = errors.New("sdjwt: credential outside validity window")     // err:credential:expired
	ErrNotYetValid     = errors.New("sdjwt: credential not yet valid")               // err:credential:expired
	ErrMissingCNF      = errors.New("sdjwt: holder binding required but cnf absent") // err:credential:binding-failed

	// KB-JWT.
	ErrKBRequired  = errors.New("sdjwt: key binding JWT required but absent")                     // err:credential:binding-failed
	ErrKBType      = errors.New("sdjwt: KB-JWT typ must be kb+jwt")                               // err:credential:binding-failed
	ErrKBSignature = errors.New("sdjwt: KB-JWT signature verification failed")                    // err:credential:binding-failed
	ErrKBAudience  = errors.New("sdjwt: KB-JWT aud mismatch")                                     // err:presentation:nonce-mismatch
	ErrKBNonce     = errors.New("sdjwt: KB-JWT nonce mismatch")                                   // err:presentation:nonce-mismatch
	ErrKBStale     = errors.New("sdjwt: KB-JWT iat outside acceptable window")                    // err:credential:binding-failed
	ErrKBSDHash    = errors.New("sdjwt: KB-JWT sd_hash does not match the presented disclosures") // err:credential:binding-failed

	// Status.
	ErrStatusMalformed = errors.New("sdjwt: malformed status claim") // err:credential:parse

	// Issue / PresentKB façade.
	ErrTemplate  = errors.New("sdjwt: invalid credential template")                    // programming error
	ErrClaimPath = errors.New("sdjwt: requested claim is not selectively disclosable") // programming error
)

Sentinel errors. This library returns typed errors; services map them to err:domain:reason problem codes — the expected mapping is noted inline. No message ever carries a claim value, salt, disclosure content, or JWK coordinate.

Functions

func PresentKB

func PresentKB(ctx context.Context, holder stdcrypto.Signer, sdJWT []byte, disclose []ClaimPath, aud, nonce string, now time.Time) ([]byte, error)

PresentKB builds a holder presentation of an issued SD-JWT: it selects the disclosures required for the disclose paths, appends a Key-Binding JWT (kb+jwt) signed by holder over the exact presented issuer-JWT+disclosures ([SD-JWT §4.3]). The issuer signature is NOT re-verified here (the holder presents its own, already-trusted credential — see the README Decisions section). now is injected (no wall clock). Reused by the future issuer and internal/testwallet.

Types

type ClaimPath

type ClaimPath []any

ClaimPath identifies a selectively-disclosable claim in an SD-JWT for presentation (PresentKB). Elements are object keys (string) or array indices (non-negative int), mirroring the [OID4VP §7] claims path pointer JSON shape. It is defined locally so this format library stays independent of the protocol layer: go-dcql owns a parallel ClaimPath ([]PathElement) and the two are reconciled at the go-oid4vp boundary. go-sdjwt does NOT import go-dcql (layering: avoids a go-sdjwt->go-dcql dependency cycle).

func Path

func Path(elems ...any) ClaimPath

Path builds a ClaimPath from string keys and int indices.

type CredentialTemplate

type CredentialTemplate struct {
	VCT       string
	Issuer    string
	IssuedAt  time.Time
	NotBefore time.Time // zero → omit nbf
	Expiry    time.Time // zero → omit exp
	HolderKey stdcrypto.PublicKey
	Status    *StatusRef
	Claims    map[string]any
	Selective []ClaimPath
	HashName  string
}

CredentialTemplate drives Issuer.Issue. Claims is the full claim object; Selective lists the claim paths (Path(...)) to make selectively disclosable — every other claim is issued in the clear. HolderKey, when set, is embedded as cnf.jwk. HashName selects _sd_alg ("" → ECCG baseline, sha-256), resolved through go-eudi-crypto (no literal here).

type Issuer

type Issuer struct {
	// contains filtered or unexported fields
}

Issuer signs SD-JWT / SD-JWT VC credentials. It is the counterpart of Verifier and is reused by the future OpenID4VCI issuer and by internal/testwallet. Construct with NewIssuer.

func NewIssuer

func NewIssuer(kp eudicrypto.KeyProvider, keyID string, opts ...IssuerOption) *Issuer

NewIssuer returns an Issuer that signs with the key keyID from kp. Options are additive; the two-argument form NewIssuer(kp, keyID) is unchanged.

func (*Issuer) Issue

func (i *Issuer) Issue(ctx context.Context, tmpl CredentialTemplate) ([]byte, error)

Issue produces a combined-format SD-JWT (no KB-JWT), signing the issuer JWT with the Issuer's key. Claims listed in tmpl.Selective are blinded into _sd digests (objects) / "..." wrappers (arrays); everything else is in the clear. Disclosures are emitted sorted (order-independent for verification; avoids leaking insertion order). [SD-JWT §4]; [SD-JWT VC §3].

type IssuerOption added in v0.0.2

type IssuerOption func(*Issuer)

IssuerOption configures an Issuer at construction time. Options are additive; an Issuer built with no options behaves exactly as before this mechanism existed.

func WithChain added in v0.0.2

func WithChain(chain []*x509.Certificate) IssuerOption

WithChain attaches an x5c certificate chain (leaf first) that Issue embeds in every issued credential's JWS protected header ([RFC 7515 §4.1.6]), so a verifier can resolve the issuer key from the chain (against a trust anchor) before verifying. Optional — an Issuer with no chain configured issues exactly as today (no x5c member).

type Option

type Option func(*Verifier)

Option configures a Verifier.

func WithClock

func WithClock(f func() time.Time) Option

WithClock injects the time source (validity windows, KB-JWT iat).

func WithKBMaxAge

func WithKBMaxAge(d time.Duration) Option

WithKBMaxAge sets how old a KB-JWT iat may be before it is rejected as a replay.

func WithLegacyVCTyp

func WithLegacyVCTyp() Option

WithLegacyVCTyp additionally accepts the legacy vc+sd-jwt typ header (compatibility flag). Off by default: dc+sd-jwt only.

func WithPolicy

func WithPolicy(p eudicrypto.Policy) Option

WithPolicy overrides the ECCG policy singleton (tests / alternative deployments).

func WithSkew

func WithSkew(d time.Duration) Option

WithSkew sets the allowed clock skew for exp/nbf/iat comparisons.

type PeekResult added in v0.0.2

type PeekResult struct {
	Typ             string              // protected header "typ" ([RFC 7515 §4.1]), unverified
	X5C             []*x509.Certificate // protected header x5c ([RFC 7515 §4.1.6]), leaf first; nil if absent; NOT validated against any anchor
	Iss             string              // payload "iss" ([SD-JWT VC §3.2]), read WITHOUT signature verification
	VCT             string              // payload "vct" ([SD-JWT VC §3.2]), read WITHOUT signature verification
	DisclosureCount int                 // number of ~-separated disclosure segments
}

PeekResult holds structural fields read from a combined-format SD-JWT WITHOUT verifying anything — no signature check, no digest check, no expiry check. It is what Peek returns. It exists so a caller can resolve the issuer's public key (e.g. from the x5c chain, against a trust anchor) BEFORE calling Verify, which requires the resolved key as input (this library never does key resolution itself). NEVER use a PeekResult's fields to make a trust or authorization decision directly — the header/payload fields below are UNVERIFIED; verify with Verify first.

(Named PeekResult rather than Peek because Go forbids a type and the Peek function sharing one identifier in this package.)

func Peek added in v0.0.2

func Peek(presentation []byte) (*PeekResult, error)

Peek reads the structural fields of a combined-format SD-JWT presentation WITHOUT verifying anything (no signature, digest, or validity check). It is a pre-trust helper: resolve the issuer key from Peek.X5C against a trust anchor, THEN call Verify with the resolved key — Peek's own output must never be trusted directly. Reuses the combined-format splitter and the go-eudi-crypto header peek; reads only iss/vct from the payload (never the full claim set, keeping the "unverified" boundary obvious and avoiding exposing claim values pre-verification). Fail closed (ErrMalformed) on any decode failure; must not panic on adversarial input (fuzzed: FuzzPeek).

type StatusRef

type StatusRef struct {
	URI   string
	Index int
}

StatusRef is the status_list reference of a credential ([Token Status List §5]). Fetch/verify/lookup live in go-statuslist; this is extraction only.

type VerifiedCredential

type VerifiedCredential struct {
	VCT          string
	Claims       map[string]any
	CNF          stdcrypto.PublicKey
	Status       *StatusRef
	NotBefore    time.Time
	Expiry       time.Time
	SDHash       string // base64url digest over the presented issuer-JWT+disclosures (audit)
	DecoyDigests int
}

VerifiedCredential is the result of a successful Verify. Claims holds the FULL reconstructed claim set: disclosed claims, always-present/cleartext claims, AND registered JWT/VC members (iss, vct, iat, exp, nbf, cnf, status) — SD-JWT control members (_sd/_sd_alg) are the only thing stripped. This is not attributes-only: iss/iat have no dedicated VerifiedCredential field, so filtering Claims would lose them (see the README Decisions section). Registered members that DO have a dedicated field here (cnf, status) are ALSO surfaced via that field. DecoyDigests counts decoy digests seen (SD-JWT privacy feature; the README Decisions require surfacing the count for the verification report). CNF is the holder binding public key (README target says jwk.Key; exposed here as crypto.PublicKey for safety — see the README corrections).

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier verifies SD-JWT / SD-JWT VC presentations against an ECCG policy and an injected clock. Construct with NewVerifier; safe for concurrent use (immutable after construction).

func NewVerifier

func NewVerifier(opts ...Option) *Verifier

NewVerifier returns a Verifier with a wall clock, the ECCG policy, default skew and KB max-age, and dc+sd-jwt-only typ enforcement.

func (*Verifier) Verify

Verify verifies an SD-JWT / SD-JWT VC presentation ([SD-JWT §7]; [SD-JWT VC §3]). Pipeline: split combined format → verify issuer JWS (go-eudi-crypto, alg derived from IssuerKey) → enforce typ → require iss/vct → check validity window → extract cnf holder key → reconstruct disclosed claims by digest → compute sd_hash → verify KB-JWT ([SD-JWT §4.3]) → extract the status_list reference ([Token Status List §5]). Fail closed: any failed check returns a distinct typed error and no VerifiedCredential.

type VerifyInput

type VerifyInput struct {
	Presentation  []byte // <issuer-jwt>~<disclosure>~...~<kb-jwt>
	IssuerKey     stdcrypto.PublicKey
	ExpectedAud   string // verifier client_id; required when a KB-JWT is verified
	ExpectedNonce string // request nonce;      required when a KB-JWT is verified
	RequireKB     bool   // per ARF: callers set true by default
}

VerifyInput is the full input to Verifier.Verify. IssuerKey is resolved by the caller through the trust layer (from x5c/iss) — this library never dereferences x5u/jku or the system pool.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL