package catalog import ( "context" "errors" "fmt" ) var ErrNoValidCatalog = errors.New("no valid catalog available") // Fetcher obtains a signed Catalog document from a remote or test source. type Fetcher interface { Fetch(context.Context) ([]byte, error) } // FetchFunc adapts a function to Fetcher. type FetchFunc func(context.Context) ([]byte, error) func (function FetchFunc) Fetch(ctx context.Context) ([]byte, error) { return function(ctx) } // Cache stores the last verified signed document. type Cache interface { Load() ([]byte, error) Store([]byte) error } // DocumentValidator rejects signed documents that this client cannot consume. // Validators run before a remote document can replace the last valid cache. type DocumentValidator interface { Validate(VerifiedDocument) error } // DocumentValidatorFunc adapts a function to DocumentValidator. type DocumentValidatorFunc func(VerifiedDocument) error func (function DocumentValidatorFunc) Validate(document VerifiedDocument) error { return function(document) } // LoadSource describes where a verified result came from. type LoadSource string const ( SourceRemote LoadSource = "remote" SourceCache LoadSource = "cache" ) // LoadResult returns verified bytes and a non-fatal refresh/cache warning. type LoadResult struct { Document VerifiedDocument Source LoadSource Warning error } // LoadError preserves both the refresh and cache failure. type LoadError struct { Refresh error Cache error } func (err *LoadError) Error() string { return fmt.Sprintf("%s: refresh=%v; cache=%v", ErrNoValidCatalog, err.Refresh, err.Cache) } func (err *LoadError) Unwrap() error { return ErrNoValidCatalog } // Loader verifies remote data before storing it and re-verifies cache fallback. type Loader struct { verifier Verifier fetcher Fetcher cache Cache validators []DocumentValidator } func NewLoader( verifier Verifier, fetcher Fetcher, cache Cache, validators ...DocumentValidator, ) *Loader { return &Loader{ verifier: verifier, fetcher: fetcher, cache: cache, validators: append([]DocumentValidator(nil), validators...), } } // Load prefers a verified remote document and falls back to verified cache. func (loader *Loader) Load(ctx context.Context) (LoadResult, error) { remoteBytes, refreshErr := loader.fetcher.Fetch(ctx) if refreshErr == nil { verified, verifyErr := loader.verifier.Verify(remoteBytes) if verifyErr == nil { verifyErr = loader.validate(verified) if verifyErr == nil { storeErr := loader.cache.Store(verified.Bytes) return LoadResult{ Document: verified, Source: SourceRemote, Warning: storeErr, }, nil } } refreshErr = verifyErr } cachedBytes, cacheErr := loader.cache.Load() if cacheErr == nil { var verified VerifiedDocument verified, cacheErr = loader.verifier.Verify(cachedBytes) if cacheErr == nil { cacheErr = loader.validate(verified) if cacheErr == nil { return LoadResult{ Document: verified, Source: SourceCache, Warning: refreshErr, }, nil } } } return LoadResult{}, &LoadError{ Refresh: refreshErr, Cache: cacheErr, } } func (loader *Loader) validate(document VerifiedDocument) error { for _, validator := range loader.validators { if validator == nil { continue } if err := validator.Validate(document); err != nil { return err } } return nil }