trust

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 17 Imported by: 0

README

go-eudi-trust

Verifier-side client for the EUDI trust-anchor service: typed API client with strong-ETag polling, a fail-closed in-memory trust-anchor cache, and issuer-chain resolution against typed anchor sets.

  • Client: GET /v1/anchors.json?type=&territory= with If-None-Match revalidation (304 = freshness confirmation), X-Trust-Stale mapping, GET /v1/snapshot telemetry. Auth is an injected HTTP doer — wire DPoP, mTLS, or internal-network clients as your deployment requires.
  • CachingSource: in-memory anchor source fed by an external refresh loop. Per-anchor-type grace windows; expired-beyond-grace fails closed with ErrCacheExpired. Degraded-mode state transitions are observable via a callback (the library never logs).
  • ResolveIssuerKey: resolves an mdoc x5chain / SD-JWT x5c to a verified issuer key against anchors of the required type — issuing territory first, then EU-level — via go-eudi-crypto RFC 5280 path validation (explicit anchors only, no system pool).
  • MatchCertificate: ETSI TS 119 615 verdict passthrough (server-side check, optional extension) with a short-TTL verdict cache.

This library NEVER parses ETSI TS 119 612 trusted-list XML. Trusted-list ingestion, LOTL verification, and TS 119 612/615 processing live in the trust-anchor service; this client consumes its JSON contract only.

Implemented contract: the verifier trust-service API (ETag polling against snapshots, no changes cursor). See SPECREFS.md for pinned references.

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

Documentation

Overview

Package trust is the verifier-side client for the central Trust Service (the trust-anchor service) — the ONLY path to trust anchors in the EUDI verifier: typed API client with ETag polling, a fail-closed in-memory anchor cache, and chain-resolution glue over go-eudi-crypto.

The library is framework-free: it takes an injected HTTP Doer (services wire DPoP or internal-network auth per TRUST_AUTH_MODE), an injected clock, and returns typed errors. It never logs, never touches the system certificate pool, and never parses ETSI TS 119 612 trusted-list XML — trusted-list knowledge stays server-side.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSchema: a trust-service response violates the contract in
	// docs/trust-service-api.md (missing valid_until, fingerprint mismatch,
	// ETag/body divergence, undecodable body). Fail closed.
	ErrSchema = errors.New("trust: trust-service response violates contract schema")
	// ErrStatus: unexpected HTTP status from the trust service.
	ErrStatus = errors.New("trust: unexpected trust-service HTTP status")
	// ErrUnavailable: transport-level failure reaching the trust service.
	ErrUnavailable = errors.New("trust: trust service unreachable")
	// ErrCacheExpired: the anchor cache for the requested type is beyond
	// MaxAge+grace, or was never filled (CLAUDE.md rule 7: fail closed).
	ErrCacheExpired = errors.New("trust: anchor cache expired")
	// ErrUnknownAnchorType: not one of the valid AnchorType taxonomy values
	// (see ValidAnchorType), or not configured on this source. Unknown =
	// reject, never fall through.
	ErrUnknownAnchorType = errors.New("trust: unknown anchor type")
	// ErrChainParse: the presented x5chain/x5c could not be parsed.
	ErrChainParse = errors.New("trust: issuer chain malformed")
	// ErrChainUntrusted: the chain does not terminate at any anchor of the
	// requested type in the resolution territories.
	ErrChainUntrusted = errors.New("trust: issuer chain does not reach a trust anchor")
)

Sentinel errors. Services map these to err:domain:reason problem codes (docs/conventions.md): ErrCacheExpired/ErrUnavailable → err:trust:anchor-unavailable, ErrChainUntrusted → err:credential:issuer-untrusted. This library never carries HTTP semantics or framework dependencies (ADR-0004). Error text never contains certificate bytes or attribute values (CLAUDE.md rule 3).

AnchorTypes lists every accepted taxonomy value.

Functions

func ValidAnchorType

func ValidAnchorType(t AnchorType) bool

ValidAnchorType reports whether t is an accepted taxonomy value.

Types

type Anchor

type Anchor struct {
	Cert       *x509.Certificate
	Type       AnchorType
	Country    string    // TL territory code; "" or "EU" = EU-level list
	Status     string    // TS 119 612 service-status URI, verbatim
	ValidUntil time.Time // anchor cert notAfter — REQUIRED upstream (T-06.2)
	TLSequence int64     // sequence of the TL the anchor came from
	UseCases   []string  // GAP-04: EAA use cases this anchor is accredited for (from the trusted list); empty = not use-case-scoped
}

Anchor is one trust anchor as consumed by the verification pipeline (WP-06 README target interface — binding).

type AnchorSet

type AnchorSet struct {
	Anchors   []Anchor
	Snapshot  string    // snapshot id == strong ETag; report provenance
	Stale     bool      // X-Trust-Stale / body stale: upstream degraded
	FetchedAt time.Time // injected clock at fetch; drives cache aging
}

AnchorSet is one 200 result of /v1/anchors.json: the COMPLETE anchor set of one type. A new snapshot (changed ETag) fully replaces the previous set — anchor withdrawal arrives as snapshot replacement; there is no changes cursor (WP-06 Decisions; trust-anchor D9).

type AnchorSource

type AnchorSource interface {
	// AnchorsFor returns the usable anchors of one type for one country
	// ("" = EU-level lists only). ErrCacheExpired when the cache is stale
	// beyond grace or never filled (fail closed, CLAUDE.md rule 7).
	AnchorsFor(t AnchorType, country string) ([]Anchor, error)
}

AnchorSource is what the verification pipeline consumes (WP-06 README target interface — binding): anchors served from memory, fed by trust-cache-worker via Refresh. Implemented by CachingSource.

type AnchorType

type AnchorType string

AnchorType is the verifier anchor-type taxonomy — the values of the additive type= query parameter on /v1/anchors.json (trust-service extension E3, docs/trust-service-api.md §3). ARF §6.6.3.6 scopes which anchor type may authenticate which artefact; unknown type = reject.

const (
	PIDProvider    AnchorType = "pid_provider"     // ARF §6.6.3.2: PID provider anchors
	QEAAProvider   AnchorType = "qeaa_provider"    // qualified EAA providers (qtsp-tl)
	PubEAAProvider AnchorType = "pub_eaa_provider" // public-body EAA providers
	EAAProvider    AnchorType = "eaa_provider"     // non-qualified EAA providers
	WalletProvider AnchorType = "wallet_provider"  // wallet-provider trusted list
	AccessCA       AnchorType = "access_ca"        // wallet Access-CA LoTE
	WRPRCIssuer    AnchorType = "wrprc_issuer"     // registrar / WRPRC issuer keys
	// PIDProviderStatus and its *_status siblings are status-list /
	// Identifiers-list signer anchors (ADR-0010, GAP-01): the status service
	// may be distinct from the issuer. Resolved status-first with issuer
	// fallback by verifier-core statusAnchorTypesFor (WP-09).
	PIDProviderStatus    AnchorType = "pid_provider_status"
	QEAAProviderStatus   AnchorType = "qeaa_provider_status"
	PubEAAProviderStatus AnchorType = "pub_eaa_provider_status"
	EAAProviderStatus    AnchorType = "eaa_provider_status"
)

Anchor-type taxonomy values (docs/trust-service-api.md §3): the additive type= query parameter values accepted by /v1/anchors.json.

func StatusType

func StatusType(t AnchorType) (AnchorType, bool)

StatusType maps an issuer provider anchor type to its status-signer counterpart (ADR-0010): PIDProvider→PIDProviderStatus, etc. ok is false for types with no status pairing (WalletProvider, AccessCA, WRPRCIssuer) and for the *_status types themselves. Keeps the provider↔status pairing authoritative in one place; consumers (verifier-core credtrust, GAP-03) derive status anchor types from issuer types instead of hardcoding the string mapping.

type CacheConfig

type CacheConfig struct {
	// Types to maintain. Required, non-empty, all valid taxonomy values.
	Types []AnchorType
	// Territory restricts upstream fetches ("" = all territories).
	Territory string
	// MaxAge is the freshness window measured from FetchedAt. Required > 0.
	MaxAge time.Duration
	// Grace extends serving per type beyond MaxAge (served stale + flagged).
	// Types without an entry get zero grace: stale = expired immediately.
	Grace map[AnchorType]time.Duration
	// Clock is the injected time source. Defaults to time.Now().UTC.
	Clock func() time.Time
	// OnStateChange is invoked synchronously on every per-type state
	// transition (edge-triggered; T-06.6). Optional. See StateCallback.
	OnStateChange StateCallback
}

CacheConfig configures a CachingSource. The refresh LOOP lives in the consuming service (trust-cache-worker) — this library runs no goroutines or timers (ADR-0004); it only exposes the Refresh hook.

type CachingSource

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

CachingSource is the in-memory AnchorSource fed by Refresh. Safe for concurrent use. The ONLY path from the trust service to the pipeline (CLAUDE.md rule 6).

func NewCachingSource

func NewCachingSource(client Client, cfg CacheConfig) (*CachingSource, error)

NewCachingSource validates the config and returns an empty (fail-closed) source: every type is StateUnknown until its first successful Refresh.

func (*CachingSource) AnchorsFor

func (s *CachingSource) AnchorsFor(t AnchorType, country string) ([]Anchor, error)

AnchorsFor implements AnchorSource. Fail closed: StateUnknown/StateExpired ⇒ ErrCacheExpired (services map to err:trust:anchor-unavailable); StateStale serves with the flag observable via Status/StateCallback. Per-anchor validity: anchors past ValidUntil are never served (ARF §6.6.3.2: an expired trust anchor must not validate anything).

func (*CachingSource) Now

func (s *CachingSource) Now() time.Time

Now exposes the injected clock. ResolveIssuerKey (T-06.4) uses it for RFC 5280 §6.1 time checks so the whole trust path shares one time source.

func (*CachingSource) Refresh

func (s *CachingSource) Refresh(ctx context.Context) error

Refresh fetches every configured type once with If-None-Match revalidation — the hook the trust-cache-worker loop calls.

docs/trust-service-api.md §4 / trust-anchor D9 (ETag polling, no changes cursor): a 304 confirms the cached snapshot is still current (FetchedAt renewed); a 200 with a new ETag carries the COMPLETE new set which fully REPLACES the cached one — anchor withdrawal arrives exactly this way and the withdrawn anchor is unusable on the next AnchorsFor (WP-06 Decisions). Fetch errors keep the previous set (carry-over) and are returned joined; the entry keeps aging toward fail-closed expiry (CLAUDE.md rule 7). State transitions observed here fire the OnStateChange callback (T-06.6).

func (*CachingSource) States

func (s *CachingSource) States() []SourceStatus

States reports every configured type in config order — one call for a consumer /health endpoint (WP-06 T-06.6: staleness state for /health of consumers). Observed transitions fire the callback.

func (*CachingSource) Status

func (s *CachingSource) Status(t AnchorType) SourceStatus

Status reports the serve-time status of one type (health + provenance). Observing a transition here fires the callback (T-06.6).

type Client

type Client interface {
	// Anchors fetches GET /v1/anchors.json?type=&territory= with
	// If-None-Match revalidation. ok=false on 304 (cache still fresh).
	Anchors(ctx context.Context, t AnchorType, territory string, etag string) (AnchorSet, bool, error)
	// Snapshot fetches GET /v1/snapshot.
	Snapshot(ctx context.Context) (SnapshotMeta, error)
}

Client is the typed trust-service API client (WP-06 README target interface — binding). *HTTPClient implements it; CachingSource consumes it.

type ClientOption

type ClientOption func(*HTTPClient)

ClientOption configures HTTPClient.

func WithClock

func WithClock(clock func() time.Time) ClientOption

WithClock injects the time source stamped into AnchorSet.FetchedAt (docs/conventions.md: inject clocks). Defaults to time.Now().UTC.

type Doer

type Doer interface {
	Do(req *http.Request) (*http.Response, error)
}

Doer is the injected HTTP transport (ADR-0004). Services wire a DPoP-signing client, an mTLS *http.Client, or a plain internal-network client per TRUST_AUTH_MODE — this library never constructs transports or credentials (docs/trust-service-api.md §1).

type HTTPClient

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

HTTPClient implements Client over an injected Doer.

func NewClient

func NewClient(baseURL string, doer Doer, opts ...ClientOption) (*HTTPClient, error)

NewClient builds a trust-service client for a base URL such as "https://trust-anchor.internal".

func (*HTTPClient) Anchors

func (c *HTTPClient) Anchors(ctx context.Context, t AnchorType, territory, etag string) (AnchorSet, bool, error)

Anchors implements Client. docs/trust-service-api.md §4: GET /v1/anchors.json with type= (+ optional territory=), If-None-Match revalidation (304 = freshness confirmation), X-Trust-Stale mapped onto AnchorSet.Stale. Contract strictness: schema violations (including missing valid_until and ETag/body snapshot divergence) fail closed with ErrSchema.

func (*HTTPClient) MatchCertificate

func (c *HTTPClient) MatchCertificate(ctx context.Context, certDER []byte) (MatchVerdict, error)

MatchCertificate calls POST /v1/match (trust-anchor extension E4 — the recorded fixtures under testdata/trust/ are the contract until the endpoint lands upstream; WP-06 Decisions). v1 verification does NOT depend on it: chains are validated locally by ResolveIssuerKey; this passthrough exists for report enrichment where a server-side TS 119 615 §4.3/§4.4 verdict is wanted.

func (*HTTPClient) Snapshot

func (c *HTTPClient) Snapshot(ctx context.Context) (SnapshotMeta, error)

Snapshot implements Client (docs/trust-service-api.md §4: /v1/snapshot feeds trust-cache-worker telemetry).

type MatchVerdict

type MatchVerdict struct {
	Verdict   string // TS 119 615 §4.4: PASSED | FAILED | WARNING
	Snapshot  string
	CheckedAt time.Time
	Raw       json.RawMessage
}

MatchVerdict is an ETSI TS 119 615 certificate-vs-trusted-list verdict from the trust service. Raw carries the full response body UNMODIFIED — it is surfaced verbatim into the verification report (T-06.5 acceptance: verdict fields surface unmodified).

type Matcher

type Matcher interface {
	MatchCertificate(ctx context.Context, certDER []byte) (MatchVerdict, error)
}

Matcher is the MatchCertificate capability. *HTTPClient implements it; *VerdictCache decorates any Matcher.

type ResolvedIssuer

type ResolvedIssuer struct {
	Subject           string     // leaf certificate subject (RFC 2253 string)
	AnchorSubject     string     // matched anchor certificate subject
	AnchorFingerprint string     // SHA-256 hex of the matched anchor cert
	AnchorType        AnchorType // the type the caller demanded
	AnchorCountry     string     // anchor's territory verbatim ("EU"/"" = EU-level)
	TerritoriesTried  []string   // resolution order, verbatim (e.g. ["LV", ""])
	UseCases          []string   // GAP-04: matched anchor's accredited EAA use cases (Anchor.UseCases passthrough)
}

ResolvedIssuer identifies the trust anchor an issuer chain resolved to. It is recorded verbatim in the verification-report provenance (WP-06 Decisions: territory resolution order recorded verbatim). It carries subjects, fingerprints and territory codes only — never key material or attribute values (CLAUDE.md rule 3).

func ResolveIssuerKey

func ResolveIssuerKey(src AnchorSource, chain [][]byte, t AnchorType) (stdcrypto.PublicKey, ResolvedIssuer, error)

ResolveIssuerKey resolves an mdoc x5chain / SD-JWT x5c (raw DER certificates, leaf first) to a verified issuer public key against anchors of the required type (WP-06 README target interface — binding).

ARF §6.6.3.2: issuer chains terminate at the provider trust anchors from the trusted-list infrastructure. ARF §6.6.3.6: the anchor TYPE scopes what it may authenticate — the caller names the type and a chain to any other type fails (ErrChainUntrusted). Path validation is RFC 5280 §6.1 via go-eudi-crypto.VerifyChain: explicit anchors only, required At time, never the system pool (CLAUDE.md rules 4/6/7). EKU enforcement is the format profiles' concern (WP-02/WP-03), so no EKUs are passed here.

Territory resolution order (WP-06 Decisions): the issuing territory from the certificate country attribute (leaf subject C, else leaf issuer C) first, then EU-level ("") — recorded verbatim in TerritoriesTried. A source error (e.g. ErrCacheExpired) aborts immediately: a degraded cache must not silently fall through to the next territory.

type SnapshotMeta

type SnapshotMeta struct {
	ID               string
	GeneratedAt      time.Time
	LOTLSequence     uint64
	LOTLIssueTime    time.Time
	LOTLNextUpdate   *time.Time
	Territories      []TerritoryStatus
	PendingAnchors   int
	PendingBootstrap bool // staged OJ bootstrap awaiting approval → ops alert
}

SnapshotMeta is the /v1/snapshot summary: LOTL sequence, per-territory staleness, pending bootstrap presence — trust-cache-worker telemetry (docs/trust-service-api.md §4).

type SourceState

type SourceState int

SourceState is the degraded-mode state of one anchor type in a CachingSource. The ladder implements the fail-closed staleness policy of docs/trust-service-api.md §4: X-Trust-Stale or cache older than MaxAge ⇒ degraded; beyond the per-type grace ⇒ fail closed.

const (
	// StateUnknown means no successful fetch has happened yet — serving
	// fails closed.
	StateUnknown SourceState = iota
	// StateFresh means the cache is within MaxAge and not flagged stale
	// upstream.
	StateFresh
	// StateStale means the cache is past MaxAge but within grace, or
	// upstream flagged X-Trust-Stale — served WITH the flag
	// (Status/StateCallback).
	StateStale
	// StateExpired means the cache is past the deadline — AnchorsFor
	// returns ErrCacheExpired.
	StateExpired
)

func (SourceState) String

func (s SourceState) String() string

String implements fmt.Stringer (health endpoints, tests).

type SourceStatus

type SourceStatus struct {
	Type          AnchorType
	State         SourceState
	Snapshot      string    // snapshot id of the cached set ("" while Unknown)
	FetchedAt     time.Time // zero while Unknown
	UpstreamStale bool      // X-Trust-Stale / body stale on the last 200
}

SourceStatus is a serve-time status snapshot for one anchor type — consumers surface it in /health and in verification-report provenance.

type StateCallback

type StateCallback func(StateChange)

StateCallback receives state transitions synchronously, in the goroutine of the Refresh/AnchorsFor/Status/States call that observed the change, WITHOUT the cache lock held (re-entrant calls into the source are safe). The library never logs (ADR-0004) — consumers turn transitions into /health state and metrics (WP-09 verifier-core, WP-12 trust-cache-worker). Callbacks must be fast and non-blocking.

type StateChange

type StateChange struct {
	Type     AnchorType
	From, To SourceState
	Snapshot string    // snapshot id of the cached set ("" while Unknown)
	At       time.Time // injected-clock time of the observation
}

StateChange is one observed per-type state transition of a CachingSource.

type TerritoryStatus

type TerritoryStatus struct {
	Code        string
	TLSequence  int64
	NextUpdate  *time.Time
	Stale       bool
	CarriedOver bool
	AnchorCount int
}

TerritoryStatus summarizes one territory of the snapshot.

type VerdictCache

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

VerdictCache caches verdicts by certificate SHA-256 for a short TTL (T-06.5). Verdicts are snapshot-scoped and cheap to refetch — consuming services should keep the TTL low (order of minutes). Errors are never cached. Safe for concurrent use.

func NewVerdictCache

func NewVerdictCache(next Matcher, ttl time.Duration, clock func() time.Time) (*VerdictCache, error)

NewVerdictCache decorates next with a TTL cache. clock nil ⇒ time.Now().UTC.

func (*VerdictCache) MatchCertificate

func (v *VerdictCache) MatchCertificate(ctx context.Context, certDER []byte) (MatchVerdict, error)

MatchCertificate implements Matcher with cache-aside semantics.

Directories

Path Synopsis
internal
wire
Package wire holds the JSON DTOs of the trust-anchor service API, mirroring docs/trust-service-api.md and the service's routes/response/response.go + trust/anchor.go.
Package wire holds the JSON DTOs of the trust-anchor service API, mirroring docs/trust-service-api.md and the service's routes/response/response.go + trust/anchor.go.

Jump to

Keyboard shortcuts

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