Repository: theupdateframework/go-tuf
Pull request #215 (2022-02-08 20:35:59)

Add pull request template with release note stub
Signed-off-by: Asra Ali <asraa@google.com>

Adds a pull request template that includes a release note field and issue fixes

coveralls: ## Pull Request Test Coverage Report for [Build 1814566025](https://coveralls.io/builds/46350524)

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **70.068%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/46350524/badge)](https://coveralls.io/builds/46350524) |
| :-- | --: |
| Change from base [Build 1758586329](https://coveralls.io/builds/46016949): |  0.0% |
| Covered Lines: | 2156 |
| Relevant Lines: | 3077 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..eda42a0
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,17 @@
+Please fill in the fields below to submit a pull request.  The more information that is provided, the better.
+
+Fixes #<Issue>
+Release Notes: <!-- What comments/remarks should we include in the release notes for this change? -->
+
+**Types of changes**:
+<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
+- [  ] Bug fix (non-breaking change which fixes an issue)
+- [  ] New feature (non-breaking change which adds functionality)
+- [  ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+
+**Description of the changes being introduced by the pull request**:
+
+**Please verify and check that the pull request fulfills the following requirements**:
+
+- [ ] Tests have been added for the bug fix or new feature
+- [ ] Docs have been added for the bug fix or new feature
diff --git a/client/delegations.go b/client/delegations.go
index cecab3a..de3e664 100644
--- a/client/delegations.go
+++ b/client/delegations.go
@@ -20,7 +20,11 @@ func (c *Client) getTargetFileMeta(target string) (data.TargetFileMeta, error) {
 	// - filter delegations with paths or path_hash_prefixes matching searched target
 	// - 5.6.7.1 cycles protection
 	// - 5.6.7.2 terminations
-	delegations := targets.NewDelegationsIterator(target)
+	delegations, err := targets.NewDelegationsIterator(target, c.db)
+	if err != nil {
+		return data.TargetFileMeta{}, err
+	}
+
 	for i := 0; i < c.MaxDelegations; i++ {
 		d, ok := delegations.Next()
 		if !ok {
@@ -28,7 +32,7 @@ func (c *Client) getTargetFileMeta(target string) (data.TargetFileMeta, error) {
 		}
 
 		// covers 5.6.{1,2,3,4,5,6}
-		targets, err := c.loadDelegatedTargets(snapshot, d.Delegatee.Name, d.Verifier)
+		targets, err := c.loadDelegatedTargets(snapshot, d.Delegatee.Name, d.DB)
 		if err != nil {
 			return data.TargetFileMeta{}, err
 		}
@@ -39,11 +43,11 @@ func (c *Client) getTargetFileMeta(target string) (data.TargetFileMeta, error) {
 		}
 
 		if targets.Delegations != nil {
-			delegationsVerifier, err := verify.NewDelegationsVerifier(targets.Delegations)
+			delegationsDB, err := verify.NewDBFromDelegations(targets.Delegations)
 			if err != nil {
 				return data.TargetFileMeta{}, err
 			}
-			err = delegations.Add(targets.Delegations.Roles, d.Delegatee.Name, delegationsVerifier)
+			err = delegations.Add(targets.Delegations.Roles, d.Delegatee.Name, delegationsDB)
 			if err != nil {
 				return data.TargetFileMeta{}, err
 			}
@@ -75,7 +79,7 @@ func (c *Client) loadLocalSnapshot() (*data.Snapshot, error) {
 }
 
 // loadDelegatedTargets downloads, decodes, verifies and stores targets
-func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, verifier verify.DelegationsVerifier) (*data.Targets, error) {
+func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, db *verify.DB) (*data.Targets, error) {
 	var err error
 	fileName := role + ".json"
 	fileMeta, ok := snapshot.Meta[fileName]
@@ -98,11 +102,7 @@ func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, veri
 	// 5.6.3 verify signature with parent public keys
 	// 5.6.5 verify that the targets is not expired
 	// role "targets" is a top role verified by root keys loaded in the client db
-	if role == "targets" {
-		err = c.db.Unmarshal(raw, targets, role, fileMeta.Version)
-	} else {
-		err = verifier.Unmarshal(raw, targets, role, fileMeta.Version)
-	}
+	err = db.Unmarshal(raw, targets, role, fileMeta.Version)
 	if err != nil {
 		return nil, ErrDecodeFailed{fileName, err}
 	}
diff --git a/go.mod b/go.mod
index cd7e250..80d640b 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/dustin/go-humanize v1.0.0
 	github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e
 	github.com/onsi/gomega v1.18.1 // indirect
-	github.com/secure-systems-lab/go-securesystemslib v0.3.0
-	github.com/stretchr/testify v1.7.0
+	github.com/secure-systems-lab/go-securesystemslib v0.3.1
+	github.com/stretchr/testify v1.7.1
 	github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
 	golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
 	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
diff --git a/go.sum b/go.sum
index c787eb2..0dbe845 100644
--- a/go.sum
+++ b/go.sum
@@ -52,12 +52,13 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
 github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/secure-systems-lab/go-securesystemslib v0.3.0 h1:PH0mUKuUSXVEVDbrKMgGPcrqrnKA8gJii614+EKKi7g=
-github.com/secure-systems-lab/go-securesystemslib v0.3.0/go.mod h1:o8hhjkbNl2gOamKUA/eNW3xUrntHT9L4W89W1nfj43U=
+github.com/secure-systems-lab/go-securesystemslib v0.3.1 h1:LJuyMziazadwmQRRu1M7GMUo5S1oH1+YxU9FjuSFU8k=
+github.com/secure-systems-lab/go-securesystemslib v0.3.1/go.mod h1:o8hhjkbNl2gOamKUA/eNW3xUrntHT9L4W89W1nfj43U=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/local_store.go b/local_store.go
index 139f436..86eceef 100644
--- a/local_store.go
+++ b/local_store.go
@@ -3,8 +3,10 @@ package tuf
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -12,20 +14,50 @@ import (
 
 	"github.com/theupdateframework/go-tuf/data"
 	"github.com/theupdateframework/go-tuf/encrypted"
+	"github.com/theupdateframework/go-tuf/internal/roles"
+	"github.com/theupdateframework/go-tuf/internal/sets"
 	"github.com/theupdateframework/go-tuf/pkg/keys"
 	"github.com/theupdateframework/go-tuf/util"
 )
 
-func signers(privateKeys []*data.PrivateKey) []keys.Signer {
-	res := make([]keys.Signer, 0, len(privateKeys))
-	for _, k := range privateKeys {
-		signer, err := keys.GetSigner(k)
-		if err != nil {
-			continue
-		}
-		res = append(res, signer)
-	}
-	return res
+type LocalStore interface {
+	// GetMeta returns a map from metadata file names (e.g. root.json) to their raw JSON payload or an error.
+	GetMeta() (map[string]json.RawMessage, error)
+
+	// SetMeta is used to update a metadata file name with a JSON payload.
+	SetMeta(name string, meta json.RawMessage) error
+
+	// WalkStagedTargets calls targetsFn for each staged target file in paths.
+	// If paths is empty, all staged target files will be walked.
+	WalkStagedTargets(paths []string, targetsFn TargetsWalkFunc) error
+
+	// FileIsStaged determines if a metadata file is currently staged, to avoid incrementing
+	// version numbers repeatedly while staged.
+	FileIsStaged(filename string) bool
+
+	// Commit is used to publish staged files to the repository
+	//
+	// This will also reset the staged meta to signal incrementing version numbers.
+	// TUF 1.0 requires that the root metadata version numbers in the repository does not
+	// gaps. To avoid this, we will only increment the number once until we commit.
+	Commit(bool, map[string]int, map[string]data.Hashes) error
+
+	// GetSigners return a list of signers for a role.
+	GetSigners(role string) ([]keys.Signer, error)
+
+	// SaveSigner adds a signer to a role.
+	SaveSigner(role string, signer keys.Signer) error
+
+	// SignersForRole return a list of signing keys for a role.
+	SignersForKeyIDs(keyIDs []string) []keys.Signer
+
+	// Clean is used to remove all staged manifests.
+	Clean() error
+}
+
+type PassphraseChanger interface {
+	// ChangePassphrase changes the passphrase for a role keys file.
+	ChangePassphrase(string) error
 }
 
 func MemoryStore(meta map[string]json.RawMessage, files map[string][]byte) LocalStore {
@@ -33,10 +65,11 @@ func MemoryStore(meta map[string]json.RawMessage, files map[string][]byte) Local
 		meta = make(map[string]json.RawMessage)
 	}
 	return &memoryStore{
-		meta:       meta,
-		stagedMeta: make(map[string]json.RawMessage),
-		files:      files,
-		signers:    make(map[string][]keys.Signer),
+		meta:           meta,
+		stagedMeta:     make(map[string]json.RawMessage),
+		files:          files,
+		signerForKeyID: make(map[string]keys.Signer),
+		keyIDsForRole:  make(map[string][]string),
 	}
 }
 
@@ -44,7 +77,9 @@ type memoryStore struct {
 	meta       map[string]json.RawMessage
 	stagedMeta map[string]json.RawMessage
 	files      map[string][]byte
-	signers    map[string][]keys.Signer
+
+	signerForKeyID map[string]keys.Signer
+	keyIDsForRole  map[string][]string
 }
 
 func (m *memoryStore) GetMeta() (map[string]json.RawMessage, error) {
@@ -105,14 +140,53 @@ func (m *memoryStore) Commit(consistentSnapshot bool, versions map[string]int, h
 }
 
 func (m *memoryStore) GetSigners(role string) ([]keys.Signer, error) {
-	return m.signers[role], nil
+	keyIDs, ok := m.keyIDsForRole[role]
+	if ok {
+		return m.SignersForKeyIDs(keyIDs), nil
+	}
+
+	return nil, nil
 }
 
 func (m *memoryStore) SaveSigner(role string, signer keys.Signer) error {
-	m.signers[role] = append(m.signers[role], signer)
+	keyIDs := signer.PublicData().IDs()
+
+	for _, keyID := range keyIDs {
+		m.signerForKeyID[keyID] = signer
+	}
+
+	mergedKeyIDs := sets.DeduplicateStrings(append(m.keyIDsForRole[role], keyIDs...))
+	m.keyIDsForRole[role] = mergedKeyIDs
 	return nil
 }
 
+func (m *memoryStore) SignersForKeyIDs(keyIDs []string) []keys.Signer {
+	signers := []keys.Signer{}
+	keyIDsSeen := map[string]struct{}{}
+
+	for _, keyID := range keyIDs {
+		signer, ok := m.signerForKeyID[keyID]
+		if !ok {
+			continue
+		}
+		addSigner := false
+
+		for _, skid := range signer.PublicData().IDs() {
+			if _, seen := keyIDsSeen[skid]; !seen {
+				addSigner = true
+			}
+
+			keyIDsSeen[skid] = struct{}{}
+		}
+
+		if addSigner {
+			signers = append(signers, signer)
+		}
+	}
+
+	return signers
+}
+
 func (m *memoryStore) Clean() error {
 	return nil
 }
@@ -126,7 +200,8 @@ func FileSystemStore(dir string, p util.PassphraseFunc) LocalStore {
 	return &fileSystemStore{
 		dir:            dir,
 		passphraseFunc: p,
-		signers:        make(map[string][]keys.Signer),
+		signerForKeyID: make(map[string]keys.Signer),
+		keyIDsForRole:  make(map[string][]string),
 	}
 }
 
@@ -134,8 +209,8 @@ type fileSystemStore struct {
 	dir            string
 	passphraseFunc util.PassphraseFunc
 
-	// signers is a cache of persisted keys to avoid decrypting multiple times
-	signers map[string][]keys.Signer
+	signerForKeyID map[string]keys.Signer
+	keyIDsForRole  map[string][]string
 }
 
 func (f *fileSystemStore) repoDir() string {
@@ -146,25 +221,66 @@ func (f *fileSystemStore) stagedDir() string {
 	return filepath.Join(f.dir, "staged")
 }
 
+func isMetaFile(e os.DirEntry) (bool, error) {
+	name := e.Name()
+	if e.IsDir() || !(filepath.Ext(name) == ".json" && roles.IsTopLevelManifest(name)) {
+		return false, nil
+	}
+
+	info, err := e.Info()
+	if err != nil {
+		return false, err
+	}
+
+	return info.Mode().IsRegular(), nil
+}
+
 func (f *fileSystemStore) GetMeta() (map[string]json.RawMessage, error) {
-	meta := make(map[string]json.RawMessage)
-	var err error
-	notExists := func(path string) bool {
-		_, err := os.Stat(path)
-		return os.IsNotExist(err)
-	}
-	for _, name := range topLevelMetadata {
-		path := filepath.Join(f.stagedDir(), name)
-		if notExists(path) {
-			path = filepath.Join(f.repoDir(), name)
-			if notExists(path) {
-				continue
-			}
+	// Build a map of metadata names (e.g. root.json) to their full paths
+	// (whether in the committed repo dir, or in the staged repo dir).
+	metaPaths := map[string]string{}
+
+	rd := f.repoDir()
+	committed, err := os.ReadDir(f.repoDir())
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return nil, fmt.Errorf("could not list repo dir: %w", err)
+	}
+
+	for _, e := range committed {
+		imf, err := isMetaFile(e)
+		if err != nil {
+			return nil, err
 		}
-		meta[name], err = ioutil.ReadFile(path)
+		if imf {
+			name := e.Name()
+			metaPaths[name] = filepath.Join(rd, name)
+		}
+	}
+
+	sd := f.stagedDir()
+	staged, err := os.ReadDir(sd)
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return nil, fmt.Errorf("could not list staged dir: %w", err)
+	}
+
+	for _, e := range staged {
+		imf, err := isMetaFile(e)
+		if err != nil {
+			return nil, err
+		}
+		if imf {
+			name := e.Name()
+			metaPaths[name] = filepath.Join(sd, name)
+		}
+	}
+
+	meta := make(map[string]json.RawMessage)
+	for name, path := range metaPaths {
+		f, err := ioutil.ReadFile(path)
 		if err != nil {
 			return nil, err
 		}
+		meta[name] = f
 	}
 	return meta, nil
 }
@@ -333,18 +449,63 @@ func (f *fileSystemStore) Commit(consistentSnapshot bool, versions map[string]in
 }
 
 func (f *fileSystemStore) GetSigners(role string) ([]keys.Signer, error) {
-	if keys, ok := f.signers[role]; ok {
-		return keys, nil
+	keyIDs, ok := f.keyIDsForRole[role]
+	if ok {
+		return f.SignersForKeyIDs(keyIDs), nil
 	}
-	keys, _, err := f.loadPrivateKeys(role)
+
+	privKeys, _, err := f.loadPrivateKeys(role)
 	if err != nil {
 		if os.IsNotExist(err) {
 			return nil, nil
 		}
 		return nil, err
 	}
-	f.signers[role] = signers(keys)
-	return f.signers[role], nil
+
+	signers := []keys.Signer{}
+	for _, key := range privKeys {
+		signer, err := keys.GetSigner(key)
+		if err != nil {
+			return nil, err
+		}
+
+		// Cache the signers.
+		for _, keyID := range signer.PublicData().IDs() {
+			f.keyIDsForRole[role] = append(f.keyIDsForRole[role], keyID)
+			f.signerForKeyID[keyID] = signer
+		}
+		signers = append(signers, signer)
+	}
+
+	return signers, nil
+}
+
+func (f *fileSystemStore) SignersForKeyIDs(keyIDs []string) []keys.Signer {
+	signers := []keys.Signer{}
+	keyIDsSeen := map[string]struct{}{}
+
+	for _, keyID := range keyIDs {
+		signer, ok := f.signerForKeyID[keyID]
+		if !ok {
+			continue
+		}
+
+		addSigner := false
+
+		for _, skid := range signer.PublicData().IDs() {
+			if _, seen := keyIDsSeen[skid]; !seen {
+				addSigner = true
+			}
+
+			keyIDsSeen[skid] = struct{}{}
+		}
+
+		if addSigner {
+			signers = append(signers, signer)
+		}
+	}
+
+	return signers
 }
 
 // ChangePassphrase changes the passphrase for a role keys file. Implements
@@ -391,7 +552,7 @@ func (f *fileSystemStore) SaveSigner(role string, signer keys.Signer) error {
 	}
 
 	// add the key to the existing keys (if any)
-	keys, pass, err := f.loadPrivateKeys(role)
+	privKeys, pass, err := f.loadPrivateKeys(role)
 	if err != nil && !os.IsNotExist(err) {
 		return err
 	}
@@ -399,7 +560,7 @@ func (f *fileSystemStore) SaveSigner(role string, signer keys.Signer) error {
 	if err != nil {
 		return err
 	}
-	keys = append(keys, key)
+	privKeys = append(privKeys, key)
 
 	// if loadPrivateKeys didn't return a passphrase (because no keys yet exist)
 	// and passphraseFunc is set, get the passphrase so the keys file can
@@ -414,13 +575,13 @@ func (f *fileSystemStore) SaveSigner(role string, signer keys.Signer) error {
 
 	pk := &persistedKeys{}
 	if pass != nil {
-		pk.Data, err = encrypted.Marshal(keys, pass)
+		pk.Data, err = encrypted.Marshal(privKeys, pass)
 		if err != nil {
 			return err
 		}
 		pk.Encrypted = true
 	} else {
-		pk.Data, err = json.MarshalIndent(keys, "", "\t")
+		pk.Data, err = json.MarshalIndent(privKeys, "", "\t")
 		if err != nil {
 			return err
 		}
@@ -432,7 +593,27 @@ func (f *fileSystemStore) SaveSigner(role string, signer keys.Signer) error {
 	if err := util.AtomicallyWriteFile(f.keysPath(role), append(data, '\n'), 0600); err != nil {
 		return err
 	}
-	f.signers[role] = append(f.signers[role], signer)
+
+	// Merge privKeys into f.keyIDsForRole and register signers with
+	// f.signerForKeyID.
+	keyIDsForRole := f.keyIDsForRole[role]
+	for _, key := range privKeys {
+		signer, err := keys.GetSigner(key)
+		if err != nil {
+			return err
+		}
+
+		keyIDs := signer.PublicData().IDs()
+
+		for _, keyID := range keyIDs {
+			f.signerForKeyID[keyID] = signer
+		}
+
+		keyIDsForRole = append(keyIDsForRole, keyIDs...)
+	}
+
+	f.keyIDsForRole[role] = sets.DeduplicateStrings(keyIDsForRole)
+
 	return nil
 }
 
diff --git a/pkg/targets/delegation.go b/pkg/targets/delegation.go
index 8e09c05..ccd52ba 100644
--- a/pkg/targets/delegation.go
+++ b/pkg/targets/delegation.go
@@ -1,14 +1,17 @@
 package targets
 
 import (
+	"errors"
+
 	"github.com/theupdateframework/go-tuf/data"
+	"github.com/theupdateframework/go-tuf/internal/sets"
 	"github.com/theupdateframework/go-tuf/verify"
 )
 
 type Delegation struct {
 	Delegator string
-	Verifier  verify.DelegationsVerifier
 	Delegatee data.DelegatedRole
+	DB        *verify.DB
 }
 
 type delegationsIterator struct {
@@ -17,19 +20,31 @@ type delegationsIterator struct {
 	visitedRoles map[string]struct{}
 }
 
+var ErrTopLevelTargetsRoleMissing = errors.New("tuf: top level targets role missing from top level keys DB")
+
 // NewDelegationsIterator initialises an iterator with a first step
-// on top level targets
-func NewDelegationsIterator(target string) *delegationsIterator {
+// on top level targets.
+func NewDelegationsIterator(target string, topLevelKeysDB *verify.DB) (*delegationsIterator, error) {
+	targetsRole := topLevelKeysDB.GetRole("targets")
+	if targetsRole == nil {
+		return nil, ErrTopLevelTargetsRoleMissing
+	}
+
 	i := &delegationsIterator{
 		target: target,
 		stack: []Delegation{
 			{
-				Delegatee: data.DelegatedRole{Name: "targets"},
+				Delegatee: data.DelegatedRole{
+					Name:      "targets",
+					KeyIDs:    sets.StringSetToSlice(targetsRole.KeyIDs),
+					Threshold: targetsRole.Threshold,
+				},
+				DB: topLevelKeysDB,
 			},
 		},
 		visitedRoles: make(map[string]struct{}),
 	}
-	return i
+	return i, nil
 }
 
 func (d *delegationsIterator) Next() (value Delegation, ok bool) {
@@ -57,7 +72,7 @@ func (d *delegationsIterator) Next() (value Delegation, ok bool) {
 	return delegation, true
 }
 
-func (d *delegationsIterator) Add(roles []data.DelegatedRole, delegator string, verifier verify.DelegationsVerifier) error {
+func (d *delegationsIterator) Add(roles []data.DelegatedRole, delegator string, db *verify.DB) error {
 	for i := len(roles) - 1; i >= 0; i-- {
 		// Push the roles onto the stack in reverse so we get an preorder traversal
 		// of the delegations graph.
@@ -70,7 +85,7 @@ func (d *delegationsIterator) Add(roles []data.DelegatedRole, delegator string,
 			delegation := Delegation{
 				Delegator: delegator,
 				Delegatee: r,
-				Verifier:  verifier,
+				DB:        db,
 			}
 			d.stack = append(d.stack, delegation)
 		}
diff --git a/repo.go b/repo.go
index 482cdf0..c436195 100644
--- a/repo.go
+++ b/repo.go
@@ -34,44 +34,6 @@ var topLevelMetadata = []string{
 // names and generate target file metadata with additional custom metadata.
 type TargetsWalkFunc func(path string, target io.Reader) error
 
-type LocalStore interface {
-	// GetMeta returns a map from metadata file names (e.g. root.json) to their raw JSON payload or an error.
-	GetMeta() (map[string]json.RawMessage, error)
-
-	// SetMeta is used to update a metadata file name with a JSON payload.
-	SetMeta(string, json.RawMessage) error
-
-	// WalkStagedTargets calls targetsFn for each staged target file in paths.
-	//
-	// If paths is empty, all staged target files will be walked.
-	WalkStagedTargets(paths []string, targetsFn TargetsWalkFunc) error
-
-	// FileIsStaged determines if a metadata file is currently staged, to avoid incrementing
-	// version numbers repeatedly while staged.
-	FileIsStaged(filename string) bool
-
-	// Commit is used to publish staged files to the repository
-	//
-	// This will also reset the staged meta to signal incrementing version numbers.
-	// TUF 1.0 requires that the root metadata version numbers in the repository does not
-	// gaps. To avoid this, we will only increment the number once until we commit.
-	Commit(bool, map[string]int, map[string]data.Hashes) error
-
-	// GetSigners return a list of signers for a role.
-	GetSigners(string) ([]keys.Signer, error)
-
-	// SaveSigner adds a signer to a role.
-	SaveSigner(string, keys.Signer) error
-
-	// Clean is used to remove all staged metadata files.
-	Clean() error
-}
-
-type PassphraseChanger interface {
-	// ChangePassphrase changes the passphrase for a role keys file.
-	ChangePassphrase(string) error
-}
-
 type Repo struct {
 	local          LocalStore
 	hashAlgorithms []string
diff --git a/verify/db.go b/verify/db.go
index f809490..a14a514 100644
--- a/verify/db.go
+++ b/verify/db.go
@@ -28,37 +28,28 @@ func NewDB() *DB {
 	}
 }
 
-type DelegationsVerifier struct {
-	DB *DB
-}
-
-func (d *DelegationsVerifier) Unmarshal(b []byte, v interface{}, role string, minVersion int) error {
-	return d.DB.Unmarshal(b, v, role, minVersion)
-}
-
-// NewDelegationsVerifier returns a DelegationsVerifier that verifies delegations
-// of a given Targets. It reuses the DB struct to leverage verified keys, roles
-// unmarshals.
-func NewDelegationsVerifier(d *data.Delegations) (DelegationsVerifier, error) {
+// NewDBFromDelegations returns a DB that verifies delegations
+// of a given Targets.
+func NewDBFromDelegations(d *data.Delegations) (*DB, error) {
 	db := &DB{
 		roles:     make(map[string]*Role, len(d.Roles)),
 		verifiers: make(map[string]keys.Verifier, len(d.Keys)),
 	}
 	for _, r := range d.Roles {
 		if _, ok := roles.TopLevelRoles[r.Name]; ok {
-			return DelegationsVerifier{}, ErrInvalidDelegatedRole
+			return nil, ErrInvalidDelegatedRole
 		}
 		role := &data.Role{Threshold: r.Threshold, KeyIDs: r.KeyIDs}
 		if err := db.addRole(r.Name, role); err != nil {
-			return DelegationsVerifier{}, err
+			return nil, err
 		}
 	}
 	for id, k := range d.Keys {
 		if err := db.AddKey(id, k); err != nil {
-			return DelegationsVerifier{}, err
+			return nil, err
 		}
 	}
-	return DelegationsVerifier{db}, nil
+	return db, nil
 }
 
 func (db *DB) AddKey(id string, k *data.PublicKey) error {


Test patch:
diff --git a/client/delegations_test.go b/client/delegations_test.go
index 801858d..fe1d72b 100644
--- a/client/delegations_test.go
+++ b/client/delegations_test.go
@@ -1,6 +1,7 @@
 package client
 
 import (
+	"crypto/sha256"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -26,7 +27,13 @@ func TestGetTargetMeta(t *testing.T) {
 
 	f, err := c.getTargetFileMeta("f.txt")
 	assert.Nil(t, err)
-	assert.Equal(t, int64(15), f.Length)
+	hash := sha256.Sum256([]byte("Contents: f.txt"))
+	assert.Equal(t, data.HexBytes(hash[:]), f.Hashes["sha256"])
+
+	f, err = c.getTargetFileMeta("targets.txt")
+	assert.Nil(t, err)
+	hash = sha256.Sum256([]byte("Contents: targets.txt"))
+	assert.Equal(t, data.HexBytes(hash[:]), f.Hashes["sha256"])
 }
 
 func TestMaxDelegations(t *testing.T) {
diff --git a/client/testdata/go-tuf-transition-M3/generate.go b/client/testdata/go-tuf-transition-M3/generate.go
index 1d35e1b..20cb8dc 100644
--- a/client/testdata/go-tuf-transition-M3/generate.go
+++ b/client/testdata/go-tuf-transition-M3/generate.go
@@ -22,7 +22,7 @@ type persistedKeys struct {
 	Data      []*data.PrivateKey `json:"data"`
 }
 
-func assertNotNil(err error) {
+func assertNoError(err error) {
 	if err != nil {
 		panic(fmt.Sprintf("assertion failed: %s", err))
 	}
@@ -30,31 +30,31 @@ func assertNotNil(err error) {
 
 func copyRepo(src string, dst string) {
 	cmd := exec.Command("cp", "-r", src, dst)
-	assertNotNil(cmd.Run())
+	assertNoError(cmd.Run())
 }
 
 func newRepo(dir string) *tuf.Repo {
 	repo, err := tuf.NewRepoIndent(tuf.FileSystemStore(dir, nil), "", "\t")
-	assertNotNil(err)
+	assertNoError(err)
 
 	return repo
 }
 
 func commit(dir string, repo *tuf.Repo) {
-	assertNotNil(repo.SnapshotWithExpires(expirationDate))
-	assertNotNil(repo.TimestampWithExpires(expirationDate))
-	assertNotNil(repo.Commit())
+	assertNoError(repo.SnapshotWithExpires(expirationDate))
+	assertNoError(repo.TimestampWithExpires(expirationDate))
+	assertNoError(repo.Commit())
 
 	// Remove the keys directory to make sure we don't accidentally use a key.
-	assertNotNil(os.RemoveAll(filepath.Join(dir, "keys")))
+	assertNoError(os.RemoveAll(filepath.Join(dir, "keys")))
 }
 
 func addKeys(repo *tuf.Repo, roleKeys map[string][]*data.PrivateKey) {
 	for role, keyList := range roleKeys {
 		for _, key := range keyList {
 			signer, err := keys.GetSigner(key)
-			assertNotNil(err)
-			assertNotNil(repo.AddPrivateKeyWithExpires(role, signer, expirationDate))
+			assertNoError(err)
+			assertNoError(repo.AddPrivateKeyWithExpires(role, signer, expirationDate))
 		}
 	}
 }
@@ -63,27 +63,27 @@ func addTargets(repo *tuf.Repo, dir string, files map[string][]byte) {
 	paths := []string{}
 	for file, data := range files {
 		path := filepath.Join(dir, "staged", "targets", file)
-		assertNotNil(os.MkdirAll(filepath.Dir(path), 0755))
-		assertNotNil(ioutil.WriteFile(path, data, 0644))
+		assertNoError(os.MkdirAll(filepath.Dir(path), 0755))
+		assertNoError(ioutil.WriteFile(path, data, 0644))
 		paths = append(paths, file)
 	}
-	assertNotNil(repo.AddTargetsWithExpires(paths, nil, expirationDate))
+	assertNoError(repo.AddTargetsWithExpires(paths, nil, expirationDate))
 }
 
 func revokeKeys(repo *tuf.Repo, role string, keyList []*data.PrivateKey) {
 	for _, key := range keyList {
 		signer, err := keys.GetSigner(key)
-		assertNotNil(err)
-		assertNotNil(repo.RevokeKeyWithExpires(role, signer.PublicData().IDs()[0], expirationDate))
+		assertNoError(err)
+		assertNoError(repo.RevokeKeyWithExpires(role, signer.PublicData().IDs()[0], expirationDate))
 	}
 }
 
 func generateRepos(dir string, consistentSnapshot bool) {
 	f, err := os.Open("../keys.json")
-	assertNotNil(err)
+	assertNoError(err)
 
 	var roleKeys map[string][][]*data.PrivateKey
-	assertNotNil(json.NewDecoder(f).Decode(&roleKeys))
+	assertNoError(json.NewDecoder(f).Decode(&roleKeys))
 
 	// Collect all the initial keys we'll use when creating repositories.
 	// We'll modify this to reflect rotated keys.
@@ -140,7 +140,7 @@ func generateRepos(dir string, consistentSnapshot bool) {
 
 func main() {
 	cwd, err := os.Getwd()
-	assertNotNil(err)
+	assertNoError(err)
 
 	for _, consistentSnapshot := range []bool{false, true} {
 		name := fmt.Sprintf("consistent-snapshot-%t", consistentSnapshot)
diff --git a/client/testdata/go-tuf-transition-M4/generate.go b/client/testdata/go-tuf-transition-M4/generate.go
index 7e6d5f5..552474c 100644
--- a/client/testdata/go-tuf-transition-M4/generate.go
+++ b/client/testdata/go-tuf-transition-M4/generate.go
@@ -22,7 +22,7 @@ type persistedKeys struct {
 	Data      []*data.PrivateKey `json:"data"`
 }
 
-func assertNotNil(err error) {
+func assertNoError(err error) {
 	if err != nil {
 		panic(fmt.Sprintf("assertion failed: %s", err))
 	}
@@ -30,29 +30,29 @@ func assertNotNil(err error) {
 
 func copyRepo(src string, dst string) {
 	cmd := exec.Command("cp", "-r", src, dst)
-	assertNotNil(cmd.Run())
+	assertNoError(cmd.Run())
 }
 
 func newRepo(dir string) *tuf.Repo {
 	repo, err := tuf.NewRepoIndent(tuf.FileSystemStore(dir, nil), "", "\t")
-	assertNotNil(err)
+	assertNoError(err)
 
 	return repo
 }
 
 func commit(dir string, repo *tuf.Repo) {
-	assertNotNil(repo.SnapshotWithExpires(expirationDate))
-	assertNotNil(repo.TimestampWithExpires(expirationDate))
-	assertNotNil(repo.Commit())
+	assertNoError(repo.SnapshotWithExpires(expirationDate))
+	assertNoError(repo.TimestampWithExpires(expirationDate))
+	assertNoError(repo.Commit())
 
 	// Remove the keys directory to make sure we don't accidentally use a key.
-	assertNotNil(os.RemoveAll(filepath.Join(dir, "keys")))
+	assertNoError(os.RemoveAll(filepath.Join(dir, "keys")))
 }
 
 func addKeys(repo *tuf.Repo, roleKeys map[string][]*data.PrivateKey) {
 	for role, keys := range roleKeys {
 		for _, key := range keys {
-			assertNotNil(repo.AddPrivateKeyWithExpires(role, key, expirationDate))
+			assertNoError(repo.AddPrivateKeyWithExpires(role, key, expirationDate))
 		}
 	}
 }
@@ -61,27 +61,27 @@ func addTargets(repo *tuf.Repo, dir string, files map[string][]byte) {
 	paths := []string{}
 	for file, data := range files {
 		path := filepath.Join(dir, "staged", "targets", file)
-		assertNotNil(os.MkdirAll(filepath.Dir(path), 0755))
-		assertNotNil(ioutil.WriteFile(path, data, 0644))
+		assertNoError(os.MkdirAll(filepath.Dir(path), 0755))
+		assertNoError(ioutil.WriteFile(path, data, 0644))
 		paths = append(paths, file)
 	}
-	assertNotNil(repo.AddTargetsWithExpires(paths, nil, expirationDate))
+	assertNoError(repo.AddTargetsWithExpires(paths, nil, expirationDate))
 }
 
 func revokeKeys(repo *tuf.Repo, role string, keyList []*data.PrivateKey) {
 	for _, key := range keyList {
 		signer, err := keys.GetSigner(key)
-		assertNotNil(err)
-		assertNotNil(repo.RevokeKeyWithExpires(role, signer.PublicData().IDs()[0], expirationDate))
+		assertNoError(err)
+		assertNoError(repo.RevokeKeyWithExpires(role, signer.PublicData().IDs()[0], expirationDate))
 	}
 }
 
 func generateRepos(dir string, consistentSnapshot bool) {
 	f, err := os.Open("../keys.json")
-	assertNotNil(err)
+	assertNoError(err)
 
 	var roleKeys map[string][][]*data.PrivateKey
-	assertNotNil(json.NewDecoder(f).Decode(&roleKeys))
+	assertNoError(json.NewDecoder(f).Decode(&roleKeys))
 
 	// Collect all the initial keys we'll use when creating repositories.
 	// We'll modify this to reflect rotated keys.
@@ -138,7 +138,7 @@ func generateRepos(dir string, consistentSnapshot bool) {
 
 func main() {
 	cwd, err := os.Getwd()
-	assertNotNil(err)
+	assertNoError(err)
 
 	for _, consistentSnapshot := range []bool{false, true} {
 		name := fmt.Sprintf("consistent-snapshot-%t", consistentSnapshot)
diff --git a/client/testdata/go-tuf/generator/generator.go b/client/testdata/go-tuf/generator/generator.go
index 93c2bb8..eca808f 100644
--- a/client/testdata/go-tuf/generator/generator.go
+++ b/client/testdata/go-tuf/generator/generator.go
@@ -22,7 +22,7 @@ type persistedKeys struct {
 	Data      []*data.PrivateKey `json:"data"`
 }
 
-func assertNotNil(err error) {
+func assertNoError(err error) {
 	if err != nil {
 		panic(fmt.Sprintf("assertion failed: %s", err))
 	}
@@ -30,31 +30,31 @@ func assertNotNil(err error) {
 
 func copyRepo(src string, dst string) {
 	cmd := exec.Command("cp", "-r", src, dst)
-	assertNotNil(cmd.Run())
+	assertNoError(cmd.Run())
 }
 
 func newRepo(dir string) *tuf.Repo {
 	repo, err := tuf.NewRepoIndent(tuf.FileSystemStore(dir, nil), "", "\t")
-	assertNotNil(err)
+	assertNoError(err)
 
 	return repo
 }
 
 func commit(dir string, repo *tuf.Repo) {
-	assertNotNil(repo.SnapshotWithExpires(expirationDate))
-	assertNotNil(repo.TimestampWithExpires(expirationDate))
-	assertNotNil(repo.Commit())
+	assertNoError(repo.SnapshotWithExpires(expirationDate))
+	assertNoError(repo.TimestampWithExpires(expirationDate))
+	assertNoError(repo.Commit())
 
 	// Remove the keys directory to make sure we don't accidentally use a key.
-	assertNotNil(os.RemoveAll(filepath.Join(dir, "keys")))
+	assertNoError(os.RemoveAll(filepath.Join(dir, "keys")))
 }
 
 func addKeys(repo *tuf.Repo, roleKeys map[string][]*data.PrivateKey) {
 	for role, keyList := range roleKeys {
 		for _, key := range keyList {
 			signer, err := keys.GetSigner(key)
-			assertNotNil(err)
-			assertNotNil(repo.AddPrivateKeyWithExpires(role, signer, expirationDate))
+			assertNoError(err)
+			assertNoError(repo.AddPrivateKeyWithExpires(role, signer, expirationDate))
 		}
 	}
 }
@@ -63,18 +63,18 @@ func addTargets(repo *tuf.Repo, dir string, files map[string][]byte) {
 	paths := []string{}
 	for file, data := range files {
 		path := filepath.Join(dir, "staged", "targets", file)
-		assertNotNil(os.MkdirAll(filepath.Dir(path), 0755))
-		assertNotNil(ioutil.WriteFile(path, data, 0644))
+		assertNoError(os.MkdirAll(filepath.Dir(path), 0755))
+		assertNoError(ioutil.WriteFile(path, data, 0644))
 		paths = append(paths, file)
 	}
-	assertNotNil(repo.AddTargetsWithExpires(paths, nil, expirationDate))
+	assertNoError(repo.AddTargetsWithExpires(paths, nil, expirationDate))
 }
 
 func revokeKeys(repo *tuf.Repo, role string, keyList []*data.PrivateKey) {
 	for _, key := range keyList {
 		signer, err := keys.GetSigner(key)
-		assertNotNil(err)
-		assertNotNil(repo.RevokeKeyWithExpires(role, signer.PublicData().IDs()[0], expirationDate))
+		assertNoError(err)
+		assertNoError(repo.RevokeKeyWithExpires(role, signer.PublicData().IDs()[0], expirationDate))
 	}
 }
 
@@ -134,10 +134,10 @@ func generateRepos(dir string, roleKeys map[string][][]*data.PrivateKey, consist
 
 func Generate(dir string, keysPath string, consistentSnapshot bool) {
 	f, err := os.Open(keysPath)
-	assertNotNil(err)
+	assertNoError(err)
 
 	var roleKeys map[string][][]*data.PrivateKey
-	assertNotNil(json.NewDecoder(f).Decode(&roleKeys))
+	assertNoError(json.NewDecoder(f).Decode(&roleKeys))
 
 	log.Printf("generating %s", dir)
 
diff --git a/client/testdata/tools/gen-keys.go b/client/testdata/tools/gen-keys.go
index 896fc01..f592d15 100644
--- a/client/testdata/tools/gen-keys.go
+++ b/client/testdata/tools/gen-keys.go
@@ -30,7 +30,7 @@ func main() {
 
 		for i := 0; i < 2; i++ {
 			signer, err := keys.GenerateEd25519Key()
-			assertNotNil(err)
+			assertNoError(err)
 			keys = append(keys, []*data.PrivateKey{signer})
 		}
 
@@ -38,12 +38,12 @@ func main() {
 	}
 
 	s, err := json.MarshalIndent(&roles, "", "    ")
-	assertNotNil(err)
+	assertNoError(err)
 
 	ioutil.WriteFile("keys.json", []byte(s), 0644)
 }
 
-func assertNotNil(err error) {
+func assertNoError(err error) {
 	if err != nil {
 		panic(fmt.Sprintf("assertion failed: %s", err))
 	}
diff --git a/local_store_test.go b/local_store_test.go
new file mode 100644
index 0000000..8badcfc
--- /dev/null
+++ b/local_store_test.go
@@ -0,0 +1,70 @@
+package tuf
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/theupdateframework/go-tuf/pkg/keys"
+)
+
+func TestLocalStoreSigners(t *testing.T) {
+	tmpdir, err := os.MkdirTemp("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		os.RemoveAll(tmpdir)
+	}()
+
+	stores := map[string]LocalStore{
+		"MemoryStore":     MemoryStore(nil, nil),
+		"FileSystemStore": FileSystemStore(tmpdir, nil),
+	}
+
+	for name, store := range stores {
+		t.Run(name, func(t *testing.T) {
+			signers, err := store.GetSigners("abc")
+			assert.NoError(t, err)
+			assert.Equal(t, len(signers), 0)
+
+			// Add two signers to role "a".
+			aSigner1, err := keys.GenerateEd25519Key()
+			assert.NoError(t, err)
+			err = store.SaveSigner("a", aSigner1)
+			assert.NoError(t, err)
+
+			aSigner2, err := keys.GenerateEd25519Key()
+			assert.NoError(t, err)
+			err = store.SaveSigner("a", aSigner2)
+			assert.NoError(t, err)
+
+			// Add one signer to role "b".
+			bSigner, err := keys.GenerateEd25519Key()
+			assert.NoError(t, err)
+			err = store.SaveSigner("b", bSigner)
+			assert.NoError(t, err)
+
+			// Add to b again to test deduplication.
+			err = store.SaveSigner("b", bSigner)
+			assert.NoError(t, err)
+
+			signers, err = store.GetSigners("a")
+			assert.NoError(t, err)
+			assert.ElementsMatch(t, []keys.Signer{aSigner1, aSigner2}, signers)
+
+			signers, err = store.GetSigners("b")
+			assert.NoError(t, err)
+			assert.ElementsMatch(t, []keys.Signer{bSigner}, signers)
+
+			a1KeyIDs := aSigner1.PublicData().IDs()
+			a2KeyIDs := aSigner2.PublicData().IDs()
+			bKeyIDs := bSigner.PublicData().IDs()
+
+			assert.Equal(t, []keys.Signer{aSigner1}, store.SignersForKeyIDs(a1KeyIDs))
+			assert.Equal(t, []keys.Signer{aSigner2}, store.SignersForKeyIDs(a2KeyIDs))
+			assert.ElementsMatch(t, []keys.Signer{aSigner1, aSigner2}, store.SignersForKeyIDs(append(a1KeyIDs, a2KeyIDs...)))
+			assert.Equal(t, []keys.Signer{bSigner}, store.SignersForKeyIDs(bKeyIDs))
+		})
+	}
+}
diff --git a/pkg/targets/delegation_test.go b/pkg/targets/delegation_test.go
index bb460b1..2e0c42b 100644
--- a/pkg/targets/delegation_test.go
+++ b/pkg/targets/delegation_test.go
@@ -14,6 +14,20 @@ var (
 )
 
 func TestDelegationsIterator(t *testing.T) {
+	topTargetsPubKey := &data.PublicKey{
+		Type:       data.KeyTypeEd25519,
+		Scheme:     data.KeySchemeEd25519,
+		Algorithms: data.HashAlgorithms,
+		Value:      []byte(`{"public":"aaaaec567e5901ba3976c34f7cd5169704292439bf71e6aa19c64b96706f95ef"}`),
+	}
+	delTargetsPubKey := &data.PublicKey{
+		Type:       data.KeyTypeEd25519,
+		Scheme:     data.KeySchemeEd25519,
+		Algorithms: data.HashAlgorithms,
+		Value:      []byte(`{"public":"bbbbec567e5901ba3976c34f7cd5169704292439bf71e6aa19c64b96706f95ef"}`),
+	}
+
+	defaultKeyIDs := delTargetsPubKey.IDs()
 	var iteratorTests = []struct {
 		testName    string
 		roles       map[string][]data.DelegatedRole
@@ -21,27 +35,35 @@ func TestDelegationsIterator(t *testing.T) {
 		resultOrder []string
 		err         error
 	}{
+		{
+			testName: "no delegation",
+			roles: map[string][]data.DelegatedRole{
+				"targets": {},
+			},
+			file:        "test.txt",
+			resultOrder: []string{"targets"},
+		},
 		{
 			testName: "no termination",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "b", Paths: defaultPathPatterns},
-					{Name: "e", Paths: defaultPathPatterns},
+					{Name: "b", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "e", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"b": {
-					{Name: "c", Paths: defaultPathPatterns},
+					{Name: "c", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"c": {
-					{Name: "d", Paths: defaultPathPatterns},
+					{Name: "d", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"e": {
-					{Name: "f", Paths: defaultPathPatterns},
-					{Name: "g", Paths: defaultPathPatterns},
+					{Name: "f", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "g", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"g": {
-					{Name: "h", Paths: defaultPathPatterns},
-					{Name: "i", Paths: defaultPathPatterns},
-					{Name: "j", Paths: defaultPathPatterns},
+					{Name: "h", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "i", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "j", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "",
@@ -51,12 +73,12 @@ func TestDelegationsIterator(t *testing.T) {
 			testName: "terminated in b",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "b", Paths: defaultPathPatterns, Terminating: true},
-					{Name: "e", Paths: defaultPathPatterns},
+					{Name: "b", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs, Terminating: true},
+					{Name: "e", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"b": {
-					{Name: "c", Paths: defaultPathPatterns},
-					{Name: "d", Paths: defaultPathPatterns},
+					{Name: "c", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "d", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "",
@@ -66,12 +88,12 @@ func TestDelegationsIterator(t *testing.T) {
 			testName: "path does not match b",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "b", Paths: noMatchPathPatterns},
-					{Name: "e", Paths: defaultPathPatterns},
+					{Name: "b", Paths: noMatchPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "e", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"b": {
-					{Name: "c", Paths: defaultPathPatterns},
-					{Name: "d", Paths: defaultPathPatterns},
+					{Name: "c", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "d", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "",
@@ -81,12 +103,13 @@ func TestDelegationsIterator(t *testing.T) {
 			testName: "path does not match b - path prefixes",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "b", PathHashPrefixes: []string{"33472a4909"}},
-					{Name: "c", PathHashPrefixes: []string{"34c85d1ee84f61f10d7dc633"}},
+					{Name: "b", PathHashPrefixes: []string{"33472a4909"}, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "c", PathHashPrefixes: []string{"34c85d1ee84f61f10d7dc633"}, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"c": {
-					{Name: "d", PathHashPrefixes: []string{"8baf"}},
-					{Name: "e", PathHashPrefixes: []string{"34c85d1ee84f61f10d7dc633472a49096ed87f8f764bd597831eac371f40ac39"}},
+
+					{Name: "d", PathHashPrefixes: []string{"8baf"}, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "e", PathHashPrefixes: []string{"34c85d1ee84f61f10d7dc633472a49096ed87f8f764bd597831eac371f40ac39"}, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "/e/f/g.txt",
@@ -96,7 +119,7 @@ func TestDelegationsIterator(t *testing.T) {
 			testName: "err paths and pathHashPrefixes are set",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "b", Paths: defaultPathPatterns, PathHashPrefixes: defaultPathPatterns},
+					{Name: "b", Paths: defaultPathPatterns, PathHashPrefixes: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"b": {},
 			},
@@ -108,48 +131,54 @@ func TestDelegationsIterator(t *testing.T) {
 			testName: "cycle avoided 1",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "b", Paths: defaultPathPatterns},
-					{Name: "e", Paths: defaultPathPatterns},
+					{Name: "a", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+				},
+				"a": {
+					{Name: "b", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "e", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"b": {
-					{Name: "targets", Paths: defaultPathPatterns},
-					{Name: "d", Paths: defaultPathPatterns},
+					{Name: "a", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "d", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "",
-			resultOrder: []string{"targets", "b", "d", "e"},
+			resultOrder: []string{"targets", "a", "b", "d", "e"},
 		},
 		{
 			testName: "cycle avoided 2",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "targets", Paths: defaultPathPatterns},
-					{Name: "b", Paths: defaultPathPatterns},
+					{Name: "a", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+				},
+				"a": {
+					{Name: "a", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "b", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"b": {
-					{Name: "targets", Paths: defaultPathPatterns},
-					{Name: "b", Paths: defaultPathPatterns},
-					{Name: "c", Paths: defaultPathPatterns},
+					{Name: "a", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "b", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "c", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"c": {
-					{Name: "c", Paths: defaultPathPatterns},
+					{Name: "c", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "",
-			resultOrder: []string{"targets", "b", "c"},
+			resultOrder: []string{"targets", "a", "b", "c"},
 		},
 		{
 			testName: "diamond delegation",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "b", Paths: defaultPathPatterns},
-					{Name: "c", Paths: defaultPathPatterns},
+					{Name: "b", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
+					{Name: "c", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"b": {
-					{Name: "d", Paths: defaultPathPatterns},
+					{Name: "d", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"c": {
-					{Name: "d", Paths: defaultPathPatterns},
+					{Name: "d", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "",
@@ -159,10 +188,10 @@ func TestDelegationsIterator(t *testing.T) {
 			testName: "simple cycle",
 			roles: map[string][]data.DelegatedRole{
 				"targets": {
-					{Name: "a", Paths: defaultPathPatterns},
+					{Name: "a", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 				"a": {
-					{Name: "a", Paths: defaultPathPatterns},
+					{Name: "a", Paths: defaultPathPatterns, Threshold: 1, KeyIDs: defaultKeyIDs},
 				},
 			},
 			file:        "",
@@ -172,22 +201,52 @@ func TestDelegationsIterator(t *testing.T) {
 
 	for _, tt := range iteratorTests {
 		t.Run(tt.testName, func(t *testing.T) {
-			d := NewDelegationsIterator(tt.file)
+			topLevelDB := verify.NewDB()
+			topLevelDB.AddKey(topTargetsPubKey.IDs()[0], topTargetsPubKey)
+			topLevelDB.AddRole("targets", &data.Role{
+				KeyIDs:    topTargetsPubKey.IDs(),
+				Threshold: 1,
+			})
+
+			d, err := NewDelegationsIterator(tt.file, topLevelDB)
+			assert.NoError(t, err)
+
 			var iterationOrder []string
 			for {
 				r, ok := d.Next()
 				if !ok {
 					break
 				}
+
+				// A delegation should have associated keys. Testing the exact keys
+				// isn't useful in this module since the keys are supplied by the
+				// caller in the arguments to Add().
+				assert.Greater(t, len(r.Delegatee.KeyIDs), 0)
+
 				iterationOrder = append(iterationOrder, r.Delegatee.Name)
 				delegations, ok := tt.roles[r.Delegatee.Name]
 				if !ok {
 					continue
 				}
-				err := d.Add(delegations, r.Delegatee.Name, verify.DelegationsVerifier{})
+
+				db, err := verify.NewDBFromDelegations(&data.Delegations{
+					Roles: delegations,
+				})
+				assert.NoError(t, err)
+
+				err = d.Add(delegations, r.Delegatee.Name, db)
 				assert.Equal(t, tt.err, err)
 			}
 			assert.Equal(t, tt.resultOrder, iterationOrder)
 		})
 	}
 }
+
+func TestNewDelegationsIteratorError(t *testing.T) {
+	// Empty DB. It is supposed to have at least the top-level targets role and
+	// keys.
+	tldb := verify.NewDB()
+
+	_, err := NewDelegationsIterator("targets", tldb)
+	assert.ErrorIs(t, err, ErrTopLevelTargetsRoleMissing)
+}
diff --git a/verify/db_test.go b/verify/db_test.go
index cb32898..e4bab0a 100644
--- a/verify/db_test.go
+++ b/verify/db_test.go
@@ -7,8 +7,8 @@ import (
 	"github.com/theupdateframework/go-tuf/data"
 )
 
-func TestDelegationsVerifier(t *testing.T) {
-	var verifierTests = []struct {
+func TestDelegationsDB(t *testing.T) {
+	var dbTests = []struct {
 		testName     string
 		delegations  *data.Delegations
 		initErr      error
@@ -42,14 +42,14 @@ func TestDelegationsVerifier(t *testing.T) {
 		},
 	}
 
-	for _, tt := range verifierTests {
+	for _, tt := range dbTests {
 		t.Run(tt.testName, func(t *testing.T) {
-			verifier, err := NewDelegationsVerifier(tt.delegations)
-			assert.NotNil(t, verifier)
+			db, err := NewDBFromDelegations(tt.delegations)
 			assert.Equal(t, tt.initErr, err)
 			if err == nil {
+				assert.NotNil(t, db)
 				var targets data.Targets
-				err = verifier.Unmarshal([]byte(`{"a":"b"}`), targets, "tree", 0)
+				err = db.Unmarshal([]byte(`{"a":"b"}`), targets, "tree", 0)
 				assert.Equal(t, tt.unmarshalErr, err)
 			}
 		})