From 314eed46a8b135443ac3a9efe956cf2a6221ee9f Mon Sep 17 00:00:00 2001 From: Ethan Lowman <53835328+ethan-lowman-dd@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:37:08 -0500 Subject: [PATCH] [Delegations prereq 6] Use a verify.DB for delegation in client (#196) * [Delegations prereq] Use a verify.DB for delegation in client Splitting up https://github.com/theupdateframework/go-tuf/pull/175 * stash * Add tests to make sure the top level targets 'delegation' edge has associated keys. Make NewDelegationsIterator return an error if the passed DB is missing the top level targets role * Pass delegation directly to loadDelegatedTargets --- client/delegations.go | 22 ++--- client/delegations_test.go | 9 ++- pkg/targets/delegation.go | 29 +++++-- pkg/targets/delegation_test.go | 143 +++++++++++++++++++++++---------- verify/db.go | 23 ++---- verify/db_test.go | 12 +-- 6 files changed, 156 insertions(+), 82 deletions(-) diff --git a/client/delegations.go b/client/delegations.go index cecab3ad..f1ec67a9 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) 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,9 @@ 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, delegation targets.Delegation) (*data.Targets, error) { + role := delegation.Delegatee.Name + var err error fileName := role + ".json" fileMeta, ok := snapshot.Meta[fileName] @@ -98,11 +104,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 = delegation.DB.Unmarshal(raw, targets, role, fileMeta.Version) if err != nil { return nil, ErrDecodeFailed{fileName, err} } diff --git a/client/delegations_test.go b/client/delegations_test.go index 801858d6..fe1d72b1 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/pkg/targets/delegation.go b/pkg/targets/delegation.go index 8e09c05c..ccd52bae 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/pkg/targets/delegation_test.go b/pkg/targets/delegation_test.go index bb460b18..2e0c42b6 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.go b/verify/db.go index f8094904..a14a5149 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 { diff --git a/verify/db_test.go b/verify/db_test.go index cb328981..e4bab0a3 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) } })