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 ¶
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).
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 ¶
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 WithKBMaxAge ¶
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).
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 ¶
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 ¶
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 ¶
func (v *Verifier) Verify(ctx context.Context, in VerifyInput) (*VerifiedCredential, error)
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.