trust

package module
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 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 trust-service API
	// contract (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 (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: ErrCacheExpired/ErrUnavailable → err:trust:anchor-unavailable, ErrChainUntrusted → err:credential:issuer-untrusted. This library never carries HTTP semantics or framework dependencies. Error text never contains certificate bytes or attribute values.

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

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.

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).
	AnchorsFor(t AnchorType, country string) ([]Anchor, error)
}

AnchorSource is what the verification pipeline consumes: 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, trust-service API contract). [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: the status service
	// may be distinct from the issuer. Resolved status-first with issuer
	// fallback by eudi-verifier-core statusAnchorTypesFor.
	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 (trust-service API contract): 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: 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 (eudi-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). 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; 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.

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

Trust-service API contract (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. Fetch errors keep the previous set (carry-over) and are returned joined; the entry keeps aging toward fail-closed expiry. State transitions observed here fire the OnStateChange callback.

func (*CachingSource) States

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

States reports every configured type in config order — one call for a consumer /health endpoint (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.

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. *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 (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. 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 (per the trust-service API contract).

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. Trust-service API contract: 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). v1 verification does NOT depend on it: chains are validated locally by ResolveIssuerKey; this passthrough exists for report enrichment where a server-side [ETSI 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 (trust-service API contract: /v1/snapshot feeds trust-cache-worker telemetry).

type MatchVerdict

type MatchVerdict struct {
	Verdict   string // [ETSI 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 (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   // 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 (territory resolution order recorded verbatim). It carries subjects, fingerprints and territory codes only — never key material or attribute values.

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.

[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. EKU enforcement is the format profiles' concern, so no EKUs are passed here.

Territory resolution order: 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 (per the trust-service API contract).

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 Trust-service API contract: 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 — consumers turn transitions into /health state and metrics (eudi-verifier-core, 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 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 the trust-service API contract and the service's routes/response/response.go + trust/anchor.go.
Package wire holds the JSON DTOs of the trust-anchor service API, mirroring the trust-service API contract 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