// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2015-2022 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

// Package asserts implements snappy assertions and a database
// abstraction for managing and holding them.
package asserts

import (
	"errors"
	"fmt"
	"regexp"
	"time"
)

// NotFoundError is returned when an assertion can not be found.
type NotFoundError struct {
	Type    *AssertionType
	Headers map[string]string
}

func (e *NotFoundError) Error() string {
	pk, err := PrimaryKeyFromHeaders(e.Type, e.Headers)
	if err != nil || len(e.Headers) != len(pk) {
		// TODO: worth conveying more information?
		return fmt.Sprintf("%s assertion not found", e.Type.Name)
	}

	return fmt.Sprintf("%v not found", &Ref{Type: e.Type, PrimaryKey: pk})
}

func (e *NotFoundError) Is(err error) bool {
	_, ok := err.(*NotFoundError)
	return ok
}

// A Backstore stores assertions. It can store and retrieve assertions
// by type under unique primary key headers (whose names are available
// from assertType.PrimaryKey). Plus it supports searching by headers.
// Lookups can be limited to a maximum allowed format.
type Backstore interface {
	// Put stores an assertion.
	// It is responsible for checking that assert is newer than a
	// previously stored revision with the same primary key headers.
	Put(assertType *AssertionType, assert Assertion) error
	// Get returns the assertion with the given unique key for its
	// primary key headers.
	// A suffix of optional primary keys can be left out from key
	// in which case their default values are implied.
	// If the assertion is not present it returns a
	// NotFoundError, usually with omitted Headers.
	Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error)
	// Search returns assertions matching the given headers.
	// It invokes foundCb for each found assertion.
	Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error
	// SequenceMemberAfter returns for a sequence-forming assertType the
	// first assertion in the sequence under the given sequenceKey
	// with sequence number larger than after. If after==-1 it
	// returns the assertion with largest sequence number. If none
	// exists it returns a NotFoundError, usually with omitted
	// Headers. If assertType is not sequence-forming it can
	// panic.
	SequenceMemberAfter(assertType *AssertionType, sequenceKey []string, after, maxFormat int) (SequenceMember, error)
}

type nullBackstore struct{}

func (nbs nullBackstore) Put(t *AssertionType, a Assertion) error {
	return fmt.Errorf("cannot store assertions without setting a proper assertion backstore implementation")
}

func (nbs nullBackstore) Get(t *AssertionType, k []string, maxFormat int) (Assertion, error) {
	return nil, &NotFoundError{Type: t}
}

func (nbs nullBackstore) Search(t *AssertionType, h map[string]string, f func(Assertion), maxFormat int) error {
	return nil
}

func (nbs nullBackstore) SequenceMemberAfter(t *AssertionType, kp []string, after, maxFormat int) (SequenceMember, error) {
	return nil, &NotFoundError{Type: t}
}

// keyNotFoundError is returned when the key with a given ID cannot be found.
type keyNotFoundError struct {
	msg string
}

func (e *keyNotFoundError) Error() string { return e.msg }

func (e *keyNotFoundError) Is(target error) bool {
	_, ok := target.(*keyNotFoundError)
	return ok
}

// IsKeyNotFound returns true when the error indicates that a given key was not
// found.
func IsKeyNotFound(err error) bool {
	return errors.Is(err, &keyNotFoundError{})
}

// A KeypairManager is a manager and backstore for private/public key pairs.
type KeypairManager interface {
	// Put stores the given private/public key pair,
	// making sure it can be later retrieved by its unique key id with Get.
	// Trying to store a key with an already present key id should
	// result in an error.
	Put(privKey PrivateKey) error
	// Get returns the private/public key pair with the given key id. The
	// error can be tested with IsKeyNotFound to check whether the given key
	// was not found, or other error occurred.
	Get(keyID string) (PrivateKey, error)
	// Delete deletes the private/public key pair with the given key id.
	Delete(keyID string) error
}

// DatabaseConfig for an assertion database.
type DatabaseConfig struct {
	// trusted set of assertions (account and account-key supported),
	// used to establish root keys and trusted authorities
	Trusted []Assertion
	// predefined assertions but that do not establish foundational trust
	OtherPredefined []Assertion
	// backstore for assertions, left unset storing assertions will error
	Backstore Backstore
	// manager/backstore for keypairs, defaults to in-memory implementation
	KeypairManager KeypairManager
	// assertion checkers used by Database.Check, left unset DefaultCheckers will be used which is recommended
	Checkers []Checker
}

// RevisionError indicates a revision improperly used for an operation.
type RevisionError struct {
	Used, Current int
}

func (e *RevisionError) Error() string {
	if e.Used < 0 || e.Current < 0 {
		// TODO: message may need tweaking once there's a use.
		return "assertion revision is unknown"
	}
	if e.Used == e.Current {
		return fmt.Sprintf("revision %d is already the current revision", e.Used)
	}
	if e.Used < e.Current {
		return fmt.Sprintf("revision %d is older than current revision %d", e.Used, e.Current)
	}
	return fmt.Sprintf("revision %d is more recent than current revision %d", e.Used, e.Current)
}

// UnsupportedFormatError indicates an assertion with a format iteration not yet supported by the present version of asserts.
type UnsupportedFormatError struct {
	Ref    *Ref
	Format int
	// Update marks there was already a current revision of the assertion and it has been kept.
	Update bool
}

func (e *UnsupportedFormatError) Error() string {
	postfx := ""
	if e.Update {
		postfx = " (current not updated)"
	}
	return fmt.Sprintf("proposed %q assertion has format %d but %d is latest supported%s", e.Ref.Type.Name, e.Format, e.Ref.Type.MaxSupportedFormat(), postfx)
}

// IsUnaccceptedUpdate returns whether the error indicates that an
// assertion revision was already present and has been kept because
// the update was not accepted.
func IsUnaccceptedUpdate(err error) bool {
	switch x := err.(type) {
	case *UnsupportedFormatError:
		return x.Update
	case *RevisionError:
		return x.Used <= x.Current
	}
	return false
}

// A RODatabase exposes read-only access to an assertion database.
type RODatabase interface {
	// IsTrustedAccount returns whether the account is part of the trusted set.
	IsTrustedAccount(accountID string) bool
	// WithStackedBackstore returns a database view that layers the provided
	// backstore on top of the current one while preserving read-only access to
	// existing assertions.
	WithStackedBackstore(backstore Backstore) *Database
	// Find an assertion based on arbitrary headers.
	// Provided headers must contain the primary key for the assertion type.
	// Optional primary key headers can be omitted in which case
	// their default values will be used.
	// It returns a NotFoundError if the assertion cannot be found.
	Find(assertionType *AssertionType, headers map[string]string) (Assertion, error)
	// FindPredefined finds an assertion in the predefined sets
	// (trusted or not) based on arbitrary headers.  Provided
	// headers must contain the primary key for the assertion
	// type.
	// Optional primary key headers can be omitted in which case
	// their default values will be used.
	// It returns a NotFoundError if the assertion cannot
	// be found.
	FindPredefined(assertionType *AssertionType, headers map[string]string) (Assertion, error)
	// FindTrusted finds an assertion in the trusted set based on
	// arbitrary headers.  Provided headers must contain the
	// primary key for the assertion type.
	// Optional primary key headers can be omitted in which case
	// their default values will be used.
	// It returns a NotFoundError if the assertion cannot be
	// found.
	FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error)
	// FindMany finds assertions based on arbitrary headers.
	// It returns a NotFoundError if no assertion can be found.
	FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error)
	// FindManyPredefined finds assertions in the predefined sets
	// (trusted or not) based on arbitrary headers.  It returns a
	// NotFoundError if no assertion can be found.
	FindManyPredefined(assertionType *AssertionType, headers map[string]string) ([]Assertion, error)
	// FindSequence finds an assertion for the given headers and after for
	// a sequence-forming type.
	// The provided headers must contain a sequence key, i.e. a prefix of
	// the primary key for the assertion type except for the sequence
	// number header.
	// The assertion is the first in the sequence under the sequence key
	// with sequence number > after.
	// If after is -1 it returns instead the assertion with the largest
	// sequence number.
	// It will constraint itself to assertions with format <= maxFormat
	// unless maxFormat is -1.
	// It returns a NotFoundError if the assertion cannot be found.
	FindSequence(assertType *AssertionType, sequenceHeaders map[string]string, after, maxFormat int) (SequenceMember, error)
	// Check tests whether the assertion is properly signed and consistent with all the stored knowledge.
	Check(assert Assertion) error
}

// A Checker defines a check on an assertion considering aspects such as
// the signing key, and consistency with other
// assertions in the database.
type Checker func(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTimeEarliest, checkTimeLatest time.Time) error

// Database holds assertions and can be used to sign or check
// further assertions.
type Database struct {
	bs         Backstore
	keypairMgr KeypairManager

	trusted    Backstore
	predefined Backstore
	// all backstores to consider for find
	backstores []Backstore
	// backstores of dbs this was built on by stacking
	stackedOn []Backstore

	checkers     []Checker
	earliestTime time.Time
}

// OpenDatabase opens the assertion database based on the configuration.
func OpenDatabase(cfg *DatabaseConfig) (*Database, error) {
	bs := cfg.Backstore
	keypairMgr := cfg.KeypairManager

	if bs == nil {
		bs = nullBackstore{}
	}
	if keypairMgr == nil {
		keypairMgr = NewMemoryKeypairManager()
	}

	trustedBackstore := NewMemoryBackstore()

	for _, a := range cfg.Trusted {
		switch accepted := a.(type) {
		case *AccountKey:
			accKey := accepted
			err := trustedBackstore.Put(AccountKeyType, accKey)
			if err != nil {
				return nil, fmt.Errorf("cannot predefine trusted account key %q for %q: %v", accKey.PublicKeyID(), accKey.AccountID(), err)
			}

		case *Account:
			acct := accepted
			err := trustedBackstore.Put(AccountType, acct)
			if err != nil {
				return nil, fmt.Errorf("cannot predefine trusted account %q: %v", acct.DisplayName(), err)
			}
		default:
			return nil, fmt.Errorf("cannot predefine trusted assertions that are not account-key or account: %s", a.Type().Name)
		}
	}

	otherPredefinedBackstore := NewMemoryBackstore()

	for _, a := range cfg.OtherPredefined {
		err := otherPredefinedBackstore.Put(a.Type(), a)
		if err != nil {
			return nil, fmt.Errorf("cannot predefine assertion %v: %v", a.Ref(), err)
		}
	}

	checkers := cfg.Checkers
	if len(checkers) == 0 {
		checkers = DefaultCheckers
	}
	dbCheckers := make([]Checker, len(checkers))
	copy(dbCheckers, checkers)

	return &Database{
		bs:         bs,
		keypairMgr: keypairMgr,
		trusted:    trustedBackstore,
		predefined: otherPredefinedBackstore,
		// order here is relevant, Find* precedence and
		// findAccountKey depend on it, trusted should win over the
		// general backstore!
		backstores: []Backstore{trustedBackstore, otherPredefinedBackstore, bs},
		checkers:   dbCheckers,
	}, nil
}

// WithStackedBackstore returns a new database that adds to the given backstore
// only but finds in backstore and the base database backstores and
// cross-checks against all of them.
// This is useful to cross-check a set of assertions without adding
// them to the database.
func (db *Database) WithStackedBackstore(backstore Backstore) *Database {
	// original bs goes in front of stacked-on ones
	stackedOn := []Backstore{db.bs}
	stackedOn = append(stackedOn, db.stackedOn...)
	// find order: trusted, predefined, new backstore, stacked-on ones
	backstores := []Backstore{db.trusted, db.predefined}
	backstores = append(backstores, backstore)
	backstores = append(backstores, stackedOn...)
	return &Database{
		bs:         backstore,
		keypairMgr: db.keypairMgr,
		trusted:    db.trusted,
		predefined: db.predefined,
		backstores: backstores,
		stackedOn:  stackedOn,
		checkers:   db.checkers,
	}
}

// ImportKey stores the given private/public key pair.
func (db *Database) ImportKey(privKey PrivateKey) error {
	return db.keypairMgr.Put(privKey)
}

var (
	// for validity checking of base64 hash strings
	base64HashLike = regexp.MustCompile("^[[:alnum:]_-]*$")
)

func (db *Database) safeGetPrivateKey(keyID string) (PrivateKey, error) {
	if keyID == "" {
		return nil, fmt.Errorf("key id is empty")
	}
	if !base64HashLike.MatchString(keyID) {
		return nil, fmt.Errorf("key id contains unexpected chars: %q", keyID)
	}
	return db.keypairMgr.Get(keyID)
}

// PublicKey returns the public key part of the key pair that has the given key id.
func (db *Database) PublicKey(keyID string) (PublicKey, error) {
	privKey, err := db.safeGetPrivateKey(keyID)
	if err != nil {
		return nil, err
	}
	return privKey.PublicKey(), nil
}

// Sign assembles an assertion with the provided information and signs it
// with the private key from `headers["authority-id"]` that has the provided key id.
func (db *Database) Sign(assertType *AssertionType, headers map[string]any, body []byte, keyID string) (Assertion, error) {
	privKey, err := db.safeGetPrivateKey(keyID)
	if err != nil {
		return nil, err
	}
	return assembleAndSign(assertType, headers, body, privKey)
}

// findAccountKey finds an AccountKey exactly with account id and key id.
func (db *Database) findAccountKey(authorityID, keyID string) (*AccountKey, error) {
	key := []string{keyID}
	// consider trusted account keys then disk stored account keys
	for _, bs := range db.backstores {
		a, err := bs.Get(AccountKeyType, key, AccountKeyType.MaxSupportedFormat())
		if err == nil {
			hit := a.(*AccountKey)
			if hit.AccountID() != authorityID {
				return nil, fmt.Errorf("found public key %q from %q but expected it from: %s", keyID, hit.AccountID(), authorityID)
			}
			return hit, nil
		}
		if !errors.Is(err, &NotFoundError{}) {
			return nil, err
		}
	}
	return nil, &NotFoundError{Type: AccountKeyType}
}

// IsTrustedAccount returns whether the account is part of the trusted set.
func (db *Database) IsTrustedAccount(accountID string) bool {
	if accountID == "" {
		return false
	}
	_, err := db.trusted.Get(AccountType, []string{accountID}, AccountType.MaxSupportedFormat())
	return err == nil
}

var timeNow = time.Now

// SetEarliestTime affects how key expiration is checked.
// Instead of considering current system time, only assume that current time
// is >= earliest. If earliest is zero reset to considering current system time.
func (db *Database) SetEarliestTime(earliest time.Time) {
	db.earliestTime = earliest
}

// Check tests whether the assertion is properly signed and consistent with all the stored knowledge.
func (db *Database) Check(assert Assertion) error {
	if !assert.SupportedFormat() {
		return &UnsupportedFormatError{Ref: assert.Ref(), Format: assert.Format()}
	}

	typ := assert.Type()
	// assume current time is >= earliestTime and <= latestTime
	earliestTime := db.earliestTime
	var latestTime time.Time
	if earliestTime.IsZero() {
		// use the current system time by setting both to it
		earliestTime = timeNow()
		latestTime = earliestTime
	}

	var accKey *AccountKey
	var err error
	if typ.flags&noAuthority == 0 {
		// TODO: later may need to consider type of assert to find candidate keys
		accKey, err = db.findAccountKey(assert.AuthorityID(), assert.SignKeyID())
		if errors.Is(err, &NotFoundError{}) {
			return fmt.Errorf("no matching public key %q for signature by %q", assert.SignKeyID(), assert.AuthorityID())
		}
		if err != nil {
			return fmt.Errorf("error finding matching public key for signature: %v", err)
		}
	} else {
		if assert.AuthorityID() != "" {
			return fmt.Errorf("internal error: %q assertion cannot have authority-id set", typ.Name)
		}
	}

	for _, checker := range db.checkers {
		err := checker(assert, accKey, db, earliestTime, latestTime)
		if err != nil {
			return err
		}
	}

	return nil
}

// Add persists the assertion after ensuring it is properly signed and consistent with all the stored knowledge.
// It will return an error when trying to add an older revision of the assertion than the one currently stored.
func (db *Database) Add(assert Assertion) error {
	ref := assert.Ref()

	if len(ref.PrimaryKey) == 0 {
		return fmt.Errorf("internal error: assertion type %q has no primary key", ref.Type.Name)
	}

	err := db.Check(assert)
	if err != nil {
		if ufe, ok := err.(*UnsupportedFormatError); ok {
			_, err := ref.Resolve(db.Find)
			if err != nil && !errors.Is(err, &NotFoundError{}) {
				return err
			}
			return &UnsupportedFormatError{Ref: ufe.Ref, Format: ufe.Format, Update: err == nil}
		}
		return err
	}

	for i, keyVal := range ref.PrimaryKey {
		if keyVal == "" {
			return fmt.Errorf("missing or non-string primary key header: %v", ref.Type.PrimaryKey[i])
		}
	}

	// assuming trusted account keys/assertions will be managed
	// through the os snap this seems the safest policy until we
	// know more/better
	_, err = db.trusted.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat())
	if !errors.Is(err, &NotFoundError{}) {
		return fmt.Errorf("cannot add %q assertion with primary key clashing with a trusted assertion: %v", ref.Type.Name, ref.PrimaryKey)
	}

	_, err = db.predefined.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat())
	if !errors.Is(err, &NotFoundError{}) {
		return fmt.Errorf("cannot add %q assertion with primary key clashing with a predefined assertion: %v", ref.Type.Name, ref.PrimaryKey)
	}

	// this is non empty only in the stacked case
	if len(db.stackedOn) != 0 {
		headers, err := HeadersFromPrimaryKey(ref.Type, ref.PrimaryKey)
		if err != nil {
			return fmt.Errorf("internal error: HeadersFromPrimaryKey for %q failed on prechecked data: %s", ref.Type.Name, ref.PrimaryKey)
		}
		cur, err := find(db.stackedOn, ref.Type, headers, -1)
		if err == nil {
			curRev := cur.Revision()
			rev := assert.Revision()
			if curRev >= rev {
				return &RevisionError{Current: curRev, Used: rev}
			}
		} else if !errors.Is(err, &NotFoundError{}) {
			return err
		}
	}

	return db.bs.Put(ref.Type, assert)
}

func searchMatch(assert Assertion, expectedHeaders map[string]string) bool {
	// check non-primary-key headers as well
	for expectedKey, expectedValue := range expectedHeaders {
		if assert.Header(expectedKey) != expectedValue {
			return false
		}
	}
	return true
}

func find(backstores []Backstore, assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) {
	err := checkAssertType(assertionType)
	if err != nil {
		return nil, err
	}
	maxSupp := assertionType.MaxSupportedFormat()
	if maxFormat == -1 {
		maxFormat = maxSupp
	} else {
		if maxFormat > maxSupp {
			return nil, fmt.Errorf("cannot find %q assertions for format %d higher than supported format %d", assertionType.Name, maxFormat, maxSupp)
		}
	}

	keyValues, err := PrimaryKeyFromHeaders(assertionType, headers)
	if err != nil {
		return nil, err
	}

	var assert Assertion
	for _, bs := range backstores {
		a, err := bs.Get(assertionType, keyValues, maxFormat)
		if err == nil {
			assert = a
			break
		}
		if !errors.Is(err, &NotFoundError{}) {
			return nil, err
		}
	}

	if assert == nil || !searchMatch(assert, headers) {
		return nil, &NotFoundError{Type: assertionType, Headers: headers}
	}

	return assert, nil
}

// Find an assertion based on arbitrary headers.
// Provided headers must contain the primary key for the assertion type.
// Optional primary key headers can be omitted in which case
// their default values will be used.
// It returns a NotFoundError if the assertion cannot be found.
func (db *Database) Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) {
	return find(db.backstores, assertionType, headers, -1)
}

// FindMaxFormat finds an assertion like Find but such that its
// format is <= maxFormat by passing maxFormat along to the backend.
// It returns a NotFoundError if such an assertion cannot be found.
func (db *Database) FindMaxFormat(assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) {
	return find(db.backstores, assertionType, headers, maxFormat)
}

// FindPredefined finds an assertion in the predefined sets (trusted
// or not) based on arbitrary headers.  Provided headers must contain
// the primary key for the assertion type.
// Optional primary key headers can be omitted in which case
// their default values will be used.
// It returns a NotFoundError if the assertion cannot be found.
func (db *Database) FindPredefined(assertionType *AssertionType, headers map[string]string) (Assertion, error) {
	return find([]Backstore{db.trusted, db.predefined}, assertionType, headers, -1)
}

// FindTrusted finds an assertion in the trusted set based on arbitrary headers.
// Provided headers must contain the primary key for the assertion type.
// Optional primary key headers can be omitted in which case
// their default values will be used.
// It returns a NotFoundError if the assertion cannot be found.
func (db *Database) FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) {
	return find([]Backstore{db.trusted}, assertionType, headers, -1)
}

func (db *Database) findMany(backstores []Backstore, assertionType *AssertionType, headers map[string]string) ([]Assertion, error) {
	err := checkAssertType(assertionType)
	if err != nil {
		return nil, err
	}
	res := []Assertion{}

	foundCb := func(assert Assertion) {
		res = append(res, assert)
	}

	// TODO: Find variant taking this
	maxFormat := assertionType.MaxSupportedFormat()
	for _, bs := range backstores {
		err = bs.Search(assertionType, headers, foundCb, maxFormat)
		if err != nil {
			return nil, err
		}
	}

	if len(res) == 0 {
		return nil, &NotFoundError{Type: assertionType, Headers: headers}
	}
	return res, nil
}

// FindMany finds assertions based on arbitrary headers.
// It returns a NotFoundError if no assertion can be found.
func (db *Database) FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) {
	return db.findMany(db.backstores, assertionType, headers)
}

// FindManyPrefined finds assertions in the predefined sets (trusted
// or not) based on arbitrary headers.  It returns a NotFoundError if
// no assertion can be found.
func (db *Database) FindManyPredefined(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) {
	return db.findMany([]Backstore{db.trusted, db.predefined}, assertionType, headers)
}

// FindSequence finds an assertion for the given headers and after for
// a sequence-forming type.
// The provided headers must contain a sequence key, i.e. a prefix of
// the primary key for the assertion type except for the sequence
// number header.
// The assertion is the first in the sequence under the sequence key
// with sequence number > after.
// If after is -1 it returns instead the assertion with the largest
// sequence number.
// It will constraint itself to assertions with format <= maxFormat
// unless maxFormat is -1.
// It returns a NotFoundError if the assertion cannot be found.
func (db *Database) FindSequence(assertType *AssertionType, sequenceHeaders map[string]string, after, maxFormat int) (SequenceMember, error) {
	err := checkAssertType(assertType)
	if err != nil {
		return nil, err
	}
	if !assertType.SequenceForming() {
		return nil, fmt.Errorf("cannot use FindSequence with non sequence-forming assertion type %q", assertType.Name)
	}
	maxSupp := assertType.MaxSupportedFormat()
	if maxFormat == -1 {
		maxFormat = maxSupp
	} else {
		if maxFormat > maxSupp {
			return nil, fmt.Errorf("cannot find %q assertions for format %d higher than supported format %d", assertType.Name, maxFormat, maxSupp)
		}
	}

	// form the sequence key using all keys but the last one which
	// is the sequence number
	seqKey, err := keysFromHeaders(assertType.PrimaryKey[:len(assertType.PrimaryKey)-1], sequenceHeaders, nil)
	if err != nil {
		return nil, err
	}

	// find the better result across backstores' results
	better := func(cur, a SequenceMember) SequenceMember {
		if cur == nil {
			return a
		}
		curSeq := cur.Sequence()
		aSeq := a.Sequence()
		if after == -1 {
			if aSeq > curSeq {
				return a
			}
		} else {
			if aSeq < curSeq {
				return a
			}
		}
		return cur
	}

	var assert SequenceMember
	for _, bs := range db.backstores {
		a, err := bs.SequenceMemberAfter(assertType, seqKey, after, maxFormat)
		if err == nil {
			assert = better(assert, a)
			continue
		}
		if !errors.Is(err, &NotFoundError{}) {
			return nil, err
		}
	}

	if assert != nil {
		return assert, nil
	}

	return nil, &NotFoundError{Type: assertType, Headers: sequenceHeaders}
}

// assertion checkers

// CheckSigningKeyIsNotExpired checks that the signing key is not expired.
func CheckSigningKeyIsNotExpired(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTimeEarliest, checkTimeLatest time.Time) error {
	if signingKey == nil {
		// assert isn't signed with an account-key key, CheckSignature
		// will fail anyway unless we teach it more stuff,
		// Also this check isn't so relevant for self-signed asserts
		// (e.g. account-key-request)
		return nil
	}
	if !signingKey.isValidAssumingCurTimeWithin(checkTimeEarliest, checkTimeLatest) {
		mismatchReason := timeMismatchMsg(checkTimeEarliest, checkTimeLatest, signingKey.since, signingKey.until)
		return fmt.Errorf("assertion is signed with expired public key %q from %q: %s", assert.SignKeyID(), assert.AuthorityID(), mismatchReason)
	}
	return nil
}

func timeMismatchMsg(earliest, latest, keySince, keyUntil time.Time) string {
	var msg string

	validFrom := earliest.Format(time.RFC3339)
	if !latest.IsZero() && !latest.Equal(earliest) {
		validTo := latest.Format(time.RFC3339)
		msg = fmt.Sprintf("current time range is [%s, %s]", validFrom, validTo)
	} else {
		msg = fmt.Sprintf("current time is %s", validFrom)
	}

	keyFrom := keySince.Format(time.RFC3339)
	if !keyUntil.IsZero() {
		keyTo := keyUntil.Format(time.RFC3339)
		return msg + fmt.Sprintf(" but key is valid during [%s, %s)", keyFrom, keyTo)
	}

	return msg + fmt.Sprintf(" but key is valid from %s", keyFrom)
}

// CheckSignature checks that the signature is valid.
func CheckSignature(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTimeEarliest, checkTimeLatest time.Time) (err error) {
	var pubKey PublicKey
	if signingKey != nil {
		pubKey = signingKey.publicKey()
		if assert.AuthorityID() != signingKey.AccountID() {
			return fmt.Errorf("assertion authority %q does not match public key from %q", assert.AuthorityID(), signingKey.AccountID())
		}
		if !signingKey.canSign(assert) {
			return fmt.Errorf("assertion does not match signing constraints for public key %q from %q", assert.SignKeyID(), assert.AuthorityID())
		}
	} else {
		custom, ok := assert.(customSigner)
		if !ok {
			return fmt.Errorf("cannot check no-authority assertion type %q", assert.Type().Name)
		}

		pubKey, err = custom.signKey(roDB)
		if err != nil {
			return fmt.Errorf("cannot check no-authority assertion type %q: %w", assert.Type().Name, err)
		}
	}

	content, encSig := assert.Signature()
	signature, err := decodeSignature(encSig)
	if err != nil {
		return err
	}
	err = pubKey.verify(content, signature)
	if err != nil {
		return fmt.Errorf("failed signature verification: %v", err)
	}
	return nil
}

type timestamped interface {
	Timestamp() time.Time
}

// CheckTimestampVsSigningKeyValidity verifies that the timestamp of
// the assertion is within the signing key validity.
func CheckTimestampVsSigningKeyValidity(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTimeEarliest, checkTimeLatest time.Time) error {
	if signingKey == nil {
		// assert isn't signed with an account-key key, CheckSignature
		// will fail anyway unless we teach it more stuff.
		// Also this check isn't so relevant for self-signed asserts
		// (e.g. account-key-request)
		return nil
	}
	if tstamped, ok := assert.(timestamped); ok {
		checkTime := tstamped.Timestamp()
		if !signingKey.isValidAt(checkTime) {
			until := ""
			if !signingKey.Until().IsZero() {
				until = fmt.Sprintf(" until %q", signingKey.Until())
			}
			return fmt.Errorf("%s assertion timestamp %q outside of signing key validity (key valid since %q%s)",
				assert.Type().Name, checkTime, signingKey.Since(), until)
		}
	}
	return nil
}

// A consistencyChecker performs further checks based on the full
// assertion database knowledge and its own signing key.
type consistencyChecker interface {
	checkConsistency(roDB RODatabase, signingKey *AccountKey) error
}

// CheckCrossConsistency verifies that the assertion is consistent with the other statements in the database.
func CheckCrossConsistency(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTimeEarliest, checkTimeLatest time.Time) error {
	// see if the assertion requires further checks
	if checker, ok := assert.(consistencyChecker); ok {
		return checker.checkConsistency(roDB, signingKey)
	}
	return nil
}

// DefaultCheckers lists the default and recommended assertion
// checkers used by Database if none are specified in the
// DatabaseConfig.Checkers.
var DefaultCheckers = []Checker{
	CheckSigningKeyIsNotExpired,
	CheckSignature,
	CheckTimestampVsSigningKeyValidity,
	CheckCrossConsistency,
}
