ts11client

package module
v0.0.0-...-a8d5997 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: BSD-2-Clause Imports: 10 Imported by: 0

README

go-ts11client

CI Go Reference Go Report Card Go Version OpenSSF Scorecard License

A reusable Go client for TS11 credential metadata registries - registry.siros.org and any compatible registry generated by registry-cli. Resolves SD-JWT VC Type Metadata documents by their vct claim value, and mdoc/MDDL metadata documents by their doctype value, replacing per-service vendored/hardcoded credential metadata with a live (but cache-first) lookup.

Why

Issuers and verifiers in the SIROS ecosystem have historically vendored SD-JWT VC Type Metadata and mdoc doctype schemas as local files. A TS11 registry publishes the same metadata centrally, but a client resolving against it still needs: a mapping from vct/doctype to the registry's actual document URL (not derivable from the vct value alone - it's often an issuer-controlled identifier, not a URL on the registry's own domain), support for more than one registry, and a cache so every resolution isn't a live round-trip.

Design

  • Two-level registry structure. Config.Registries is an ordered list of LogicalRegistry entries, each with one or more Mirrors. Mirrors are queried concurrently and race - happy-eyeballs, first hit wins - since they're expected to hold identical content (e.g. a CDN in front of the same registry, or two independently-operated copies of the same catalog). Distinct LogicalRegistry entries are not raced against each other: they may hold different, non-overlapping credentials (e.g. your own org's registry plus a federation partner's), so every ResolveVCT/ResolveDoctype call tries them from the end of the list backwards - a later entry overrides an earlier one for the same key, the same convention used to let a deployment-local registry extend or override a shared upstream one - stopping at the first hit. A slow or unreachable mirror never blocks resolution as long as another mirror of the same logical registry has the answer.
  • Cache-first by default. Each registry's discovery index (.well-known/vctm-registry.json) is fetched once and reused for every subsequent lookup; set Config.RefreshInterval to re-fetch periodically instead of relying on it forever.
  • Local overrides behave as permanently cached. LocalSource wraps a fixed set of statically-configured documents that never expire and are never re-fetched - checked first, and always winning over any registry
    • so an existing local-file configuration keeps working unchanged when registries are introduced alongside it.
  • Callers own their own types. Resolved.Data is the raw document bytes; this package doesn't impose a VCTM/MDDL struct of its own - decode into whatever type you already use.
  • Bulk enumeration is separate from lookup. FetchTS11Schemas fetches every entry from a paginated TS11 /api/v1/schemas.json endpoint in one call, for callers (e.g. a poll-and-store service) that need to sync a whole catalog rather than resolve one key at a time. It's stateless - no cache, no local-override layer - callers that poll on a schedule already have their own storage for that.

Usage

import "github.com/sirosfoundation/go-ts11client"

local, _ := ts11client.NewLocalSource(
    ts11client.LocalEntry{VCT: "urn:eudi:pid:1", Data: pidVCTMBytes},
)

client, err := ts11client.New(ts11client.Config{
    Local: local, // always checked first, always wins
    Registries: []ts11client.LogicalRegistry{
        // Our own registry, served by two mirrors - raced, first wins.
        {Mirrors: []ts11client.RegistryConfig{
            {BaseURL: "https://registry.siros.org"},
            {BaseURL: "https://registry-mirror.siros.org"},
        }},
        // A federation partner's registry - tried only if the above
        // (higher-priority, since it's later in the list) misses.
        {Mirrors: []ts11client.RegistryConfig{
            {BaseURL: "https://registry.example.org"},
        }},
    },
})

res, err := client.ResolveVCT(ctx, "https://issuer.example.com/credentials/ehic")
if err != nil {
    if errors.Is(err, ts11client.ErrNotFound) {
        // no local override or registry has this vct
    }
    return err
}
var meta MyVCTMType
json.Unmarshal(res.Data, &meta)

Development

make test       # Run tests
make lint       # Run linter
make coverage   # Generate coverage report

License

BSD 2-Clause — see LICENSE.

Documentation

Overview

Package ts11client is a reusable client for TS11 credential metadata registries (registry.siros.org and compatible registries generated by registry-cli). It resolves SD-JWT VC Type Metadata documents by their vct claim value, and mdoc/MDDL metadata documents by their doctype value, across one or more registries queried concurrently - the first registry to answer wins, so a slow or unreachable registry never blocks resolution as long as at least one configured registry (or a configured local override) has the answer.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("ts11client: not found")

ErrNotFound is returned (wrapped) when no configured local override or registry has an entry for the requested vct/doctype.

Functions

func FetchTS11Schemas

func FetchTS11Schemas(ctx context.Context, httpClient *http.Client, endpointURL string) ([]TS11Document, []SkippedDocument, error)

FetchTS11Schemas fetches every page of a TS11 /api/v1/schemas.json endpoint (following pagination to exhaustion) and every schema's declared format documents.

This is a stateless bulk operation, unlike ResolveVCT/ResolveDoctype: it has no cache and no local-override layer, since callers that need a poll-and-store model of their own (rather than per-key lazy resolution) already have their own storage/caching for exactly that - this just does the network fetch/parse/paginate mechanics for them.

A single schema or format document that fails to fetch or parse is skipped, not fatal - matching how a partial catalog is still useful - but is still reported via the returned skipped slice so callers can log or monitor individual failures. Only a failure fetching or parsing the first page fails the whole call.

func FetchTS11SchemasFromFirstPage

func FetchTS11SchemasFromFirstPage(ctx context.Context, httpClient *http.Client, endpointURL string, firstPageBody []byte) ([]TS11Document, []SkippedDocument, error)

FetchTS11SchemasFromFirstPage is FetchTS11Schemas for callers that already fetched endpointURL's first-page body themselves - e.g. as part of their own response-format auto-detection, before knowing this was a TS11 schemas.json response. It reuses firstPageBody instead of fetching endpointURL a second time, and only fetches subsequent pages (if any).

Types

type Client

type Client interface {
	// ResolveVCT resolves an SD-JWT VC Type Metadata document by its vct
	// claim value. Returns an error wrapping ErrNotFound if nothing has it.
	ResolveVCT(ctx context.Context, vct string) (*Resolved, error)
	// ResolveDoctype resolves an mdoc MDDL metadata document by its
	// doctype value. Returns an error wrapping ErrNotFound if nothing has it.
	ResolveDoctype(ctx context.Context, doctype string) (*Resolved, error)
}

Client resolves credential metadata by vct (SD-JWT VC) or doctype (mdoc), from local overrides and/or one or more TS11 registries.

type Config

type Config struct {
	// Registries is an ordered list of logical (independent) registries.
	// A later entry overrides an earlier one for the same key - the same
	// convention a deployment-local registry uses to extend or override a
	// shared upstream one - so resolution tries registries from the end
	// of the list backwards, stopping at the first hit. Racing
	// (happy-eyeballs, first-hit-wins) only ever happens within a single
	// LogicalRegistry's Mirrors, never across distinct entries in this
	// list - see LogicalRegistry's doc comment for why.
	Registries []LogicalRegistry
	// Local, if set, is checked first (synchronously, no network) and
	// always wins over any registry - it exists so an existing
	// vendored-local-file configuration keeps working unchanged when
	// registries are introduced alongside it.
	Local *LocalSource
	// RefreshInterval controls how long a registry's discovery index (and
	// derived doctype index) is trusted before being re-fetched. Zero
	// means fetch once and cache forever (matches "always serve from
	// cache" as the default - there is no time-based invalidation unless
	// this is set).
	RefreshInterval time.Duration
	// HTTPClient is used for all registry requests. Defaults to
	// http.DefaultClient if nil.
	HTTPClient *http.Client
}

Config configures a MultiRegistryClient.

type LocalEntry

type LocalEntry struct {
	VCT     string // set for an SD-JWT VC Type Metadata document
	Doctype string // set for an mdoc MDDL metadata document
	Data    []byte
}

LocalEntry is one statically-configured metadata document - the caller's existing "vendored local file" behavior, expressed as data rather than a file path so callers can load it however they already do (embed, disk, config). Exactly one of VCT/Doctype should be set, matching which format Data holds.

type LocalSource

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

LocalSource is a fixed set of metadata documents that never expire and are never re-fetched - it exists so a deployment's existing local-file configuration keeps working unchanged when a MultiRegistryClient is introduced alongside it: a LocalSource entry behaves as if permanently present in the cache, and is always checked (and always wins) before any registry is consulted.

func NewLocalSource

func NewLocalSource(entries ...LocalEntry) (*LocalSource, error)

NewLocalSource builds a LocalSource from a fixed list of entries.

func (*LocalSource) ResolveDoctype

func (l *LocalSource) ResolveDoctype(ctx context.Context, doctype string) (*Resolved, error)

ResolveDoctype returns the locally-configured document for doctype, if any.

func (*LocalSource) ResolveVCT

func (l *LocalSource) ResolveVCT(ctx context.Context, vct string) (*Resolved, error)

ResolveVCT returns the locally-configured document for vct, if any.

type LogicalRegistry

type LogicalRegistry struct {
	// Mirrors is the set of endpoints serving this logical registry's
	// content. At least one is required.
	Mirrors []RegistryConfig
}

LogicalRegistry is one independent TS11 registry, optionally served by more than one mirror endpoint holding equivalent content. Mirrors are queried concurrently and race - happy-eyeballs, first hit wins - since they're expected to hold the same data. Distinct LogicalRegistry entries are NOT raced against each other: they may hold different, non- overlapping credentials (e.g. one org's own registry plus a federation partner's), so racing them would be meaningless when only one of them actually has a given key. See Config.Registries for how they combine.

type MultiRegistryClient

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

MultiRegistryClient implements Client over a LocalSource (checked first, always wins) plus zero or more logical TS11 registries.

func New

func New(cfg Config) (*MultiRegistryClient, error)

New builds a MultiRegistryClient from cfg. At least one of cfg.Local or cfg.Registries must be set.

func (*MultiRegistryClient) ResolveDoctype

func (c *MultiRegistryClient) ResolveDoctype(ctx context.Context, doctype string) (*Resolved, error)

ResolveDoctype implements Client.

func (*MultiRegistryClient) ResolveVCT

func (c *MultiRegistryClient) ResolveVCT(ctx context.Context, vct string) (*Resolved, error)

ResolveVCT implements Client.

type RegistryConfig

type RegistryConfig struct {
	// BaseURL is the registry's origin, e.g. "https://registry.siros.org"
	// (no trailing slash required either way).
	BaseURL string
	// Timeout bounds each HTTP request to this registry. Defaults to
	// defaultTimeout if zero.
	Timeout time.Duration
}

RegistryConfig identifies one registry to query.

type Resolved

type Resolved struct {
	Entry  schemameta.Entry
	Format schemameta.Format
	Data   []byte
	Source string // "local", or the base URL of the registry that answered
}

Resolved is the result of a successful resolution: the raw bytes of the resolved document, plus which format it is and where it came from. Callers decode Data into their own VCTM/MDDL type - this package does not impose one, matching how the rest of this document's format is already owned by each consumer.

type SkipReason

type SkipReason int

SkipReason categorizes why FetchTS11Schemas skipped an item, so callers can log/monitor each distinctly instead of treating every skip as an identical generic failure.

const (
	// SkipReasonNoSchemaURIs means a schema declared no schemaURIs at
	// all, so no document fetch was even attempted.
	SkipReasonNoSchemaURIs SkipReason = iota
	// SkipReasonFetchFailed means a specific format document's fetch
	// (or read) failed.
	SkipReasonFetchFailed
)

type SkippedDocument

type SkippedDocument struct {
	Reason           SkipReason
	SchemaID         string
	FormatIdentifier string
	URI              string
	Err              error
}

SkippedDocument records one schema/format document that couldn't be fetched or was otherwise skipped, so callers can still log or monitor individual failures even though FetchTS11Schemas itself treats them as non-fatal.

type TS11Document

type TS11Document struct {
	Schema           schemameta.SchemaMeta
	FormatIdentifier string
	URI              string
	Data             []byte
}

TS11Document is one resolved (schema, format) document from a paginated TS11 /api/v1/schemas.json endpoint - a schema commonly declares more than one format (e.g. both "dc+sd-jwt" and "mso_mdoc") for the same logical credential, so one SchemaMeta can produce several TS11Documents.

Directories

Path Synopsis
Package schemameta holds the wire types for a TS11 registry's published discovery index and per-credential documents, matching the JSON shapes generated by registry-cli (https://github.com/sirosfoundation/registry-cli) and served statically by registry.siros.org and compatible registries.
Package schemameta holds the wire types for a TS11 registry's published discovery index and per-credential documents, matching the JSON shapes generated by registry-cli (https://github.com/sirosfoundation/registry-cli) and served statically by registry.siros.org and compatible registries.

Jump to

Keyboard shortcuts

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