diff --git a/client/delegations.go b/client/delegations.go index 7935360cc..1beca24c1 100644 --- a/client/delegations.go +++ b/client/delegations.go @@ -13,7 +13,10 @@ func (c *Client) getTargetFileMeta(file string) (data.TargetFileMeta, error) { if err != nil { return data.TargetFileMeta{}, err } - verifiers := map[string]verify.DelegationsVerifier{"root": verify.DelegationsVerifier{c.db}} + + verifiers := map[string]verify.DelegationsVerifier{ + "root": {DB: c.db}, + } // delegationsIterator covers 5.6.7 // - pre-order depth-first search starting with the top targets @@ -26,16 +29,19 @@ func (c *Client) getTargetFileMeta(file string) (data.TargetFileMeta, error) { if !ok { return data.TargetFileMeta{}, ErrUnknownTarget{file, snapshot.Version} } - verifier := verifiers[d.parent] + // covers 5.6.{1,2,3,4,5,6} + verifier := verifiers[d.parent] target, err := c.loadDelegatedTargets(snapshot, d.child.Name, verifier) if err != nil { return data.TargetFileMeta{}, err } + // stop when the searched TargetFileMeta is found if m, ok := target.Targets[file]; ok { return m, nil } + if target.Delegations != nil { delegations.add(target.Delegations.Roles, d.child.Name) targetVerifier, err := verify.NewDelegationsVerifier(target.Delegations) @@ -45,6 +51,7 @@ func (c *Client) getTargetFileMeta(file string) (data.TargetFileMeta, error) { verifiers[d.child.Name] = targetVerifier } } + return data.TargetFileMeta{}, ErrMaxDelegations{ File: file, MaxDelegations: c.MaxDelegations, @@ -56,10 +63,12 @@ func (c *Client) loadLocalSnapshot() (*data.Snapshot, error) { if err := c.getLocalMeta(); err != nil { return nil, err } + rawS, ok := c.localMeta["snapshot.json"] if !ok { return nil, ErrNoLocalSnapshot } + snapshot := &data.Snapshot{} if err := c.db.Unmarshal(rawS, snapshot, "snapshot", c.snapshotVer); err != nil { return nil, ErrDecodeFailed{"snapshot.json", err} @@ -75,6 +84,7 @@ func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, veri if !ok { return nil, ErrRoleNotInSnapshot{role, snapshot.Version} } + // 5.6.1 download target if not in the local store // 5.6.2 check against snapshot hash raw, alreadyStored := c.localMetaFromSnapshot(fileName, fileMeta) @@ -84,6 +94,7 @@ func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, veri return nil, err } } + target := &data.Targets{} // 5.6.3 verify signature with parent public keys // 5.6.5 verify that the targets is not expired @@ -114,15 +125,17 @@ func (c *Client) rootTargetDelegation() data.DelegatedRole { if r == nil { return data.DelegatedRole{} } + keyIDs := make([]string, 0, len(r.KeyIDs)) for id, _ := range r.KeyIDs { keyIDs = append(keyIDs, id) } + return data.DelegatedRole{ - Name: role, - KeyIDs: keyIDs, - Threshold: r.Threshold, - PathMatchers: []string{"*"}, + Name: role, + KeyIDs: keyIDs, + Threshold: r.Threshold, + Paths: []string{"*"}, } } @@ -148,7 +161,9 @@ func newDelegationsIterator(role data.DelegatedRole, parent string, file string) stack: make([]delegation, 0, 1), visited: make(map[delegationID]struct{}), } + i.add([]data.DelegatedRole{role}, parent) + return i } diff --git a/client/delegations_test.go b/client/delegations_test.go index a702f0099..7f528942b 100644 --- a/client/delegations_test.go +++ b/client/delegations_test.go @@ -17,8 +17,8 @@ import ( ) var ( - defaultPathMatchers = []string{"tmp", "*"} - notMatchingPathMatchers = []string{"vars", "null"} + defaultPathPatterns = []string{"tmp", "*"} + noMatchPathPatterns = []string{"vars", "null"} ) func TestDelegationsIterator(t *testing.T) { @@ -30,44 +30,68 @@ func TestDelegationsIterator(t *testing.T) { resultOrder []string }{ { - "no termination", - map[string][]data.DelegatedRole{ - "a": []data.DelegatedRole{{Name: "b", PathMatchers: defaultPathMatchers}, {Name: "e", PathMatchers: defaultPathMatchers}}, - "b": []data.DelegatedRole{{Name: "c", PathMatchers: defaultPathMatchers}, {Name: "d", PathMatchers: defaultPathMatchers}}, + testName: "no termination", + roles: map[string][]data.DelegatedRole{ + "a": { + {Name: "b", Paths: defaultPathPatterns}, + {Name: "e", Paths: defaultPathPatterns}, + }, + "b": { + {Name: "c", Paths: defaultPathPatterns}, + {Name: "d", Paths: defaultPathPatterns}, + }, }, - data.DelegatedRole{Name: "a", PathMatchers: defaultPathMatchers}, - "", - []string{"a", "b", "c", "d", "e"}, + rootDelegation: data.DelegatedRole{Name: "a", Paths: defaultPathPatterns}, + file: "", + resultOrder: []string{"a", "b", "c", "d", "e"}, }, { - "terminated in b", - map[string][]data.DelegatedRole{ - "a": []data.DelegatedRole{{Name: "b", PathMatchers: defaultPathMatchers, Terminating: true}, {Name: "e", PathMatchers: defaultPathMatchers}}, - "b": []data.DelegatedRole{{Name: "c", PathMatchers: defaultPathMatchers}, {Name: "d", PathMatchers: defaultPathMatchers}}, + testName: "terminated in b", + roles: map[string][]data.DelegatedRole{ + "a": { + {Name: "b", Paths: defaultPathPatterns, Terminating: true}, + {Name: "e", Paths: defaultPathPatterns}, + }, + "b": { + {Name: "c", Paths: defaultPathPatterns}, + {Name: "d", Paths: defaultPathPatterns}, + }, }, - data.DelegatedRole{Name: "a", PathMatchers: defaultPathMatchers}, - "", - []string{"a", "b", "c", "d"}, + rootDelegation: data.DelegatedRole{Name: "a", Paths: defaultPathPatterns}, + file: "", + resultOrder: []string{"a", "b", "c", "d"}, }, { - "path does not match b", - map[string][]data.DelegatedRole{ - "a": []data.DelegatedRole{{Name: "b", PathMatchers: notMatchingPathMatchers}, {Name: "e", PathMatchers: defaultPathMatchers}}, - "b": []data.DelegatedRole{{Name: "c", PathMatchers: defaultPathMatchers}, {Name: "d", PathMatchers: defaultPathMatchers}}, + testName: "path does not match b", + roles: map[string][]data.DelegatedRole{ + "a": { + {Name: "b", Paths: noMatchPathPatterns}, + {Name: "e", Paths: defaultPathPatterns}, + }, + "b": { + {Name: "c", Paths: defaultPathPatterns}, + {Name: "d", Paths: defaultPathPatterns}, + }, }, - data.DelegatedRole{Name: "a", PathMatchers: defaultPathMatchers}, - "", - []string{"a", "e"}, + rootDelegation: data.DelegatedRole{Name: "a", Paths: defaultPathPatterns}, + file: "", + resultOrder: []string{"a", "e"}, }, { - "cycle avoided", - map[string][]data.DelegatedRole{ - "a": []data.DelegatedRole{{Name: "b", PathMatchers: defaultPathMatchers}, {Name: "e", PathMatchers: defaultPathMatchers}}, - "b": []data.DelegatedRole{{Name: "a", PathMatchers: defaultPathMatchers}, {Name: "d", PathMatchers: defaultPathMatchers}}, + testName: "cycle avoided", + roles: map[string][]data.DelegatedRole{ + "a": { + {Name: "b", Paths: defaultPathPatterns}, + {Name: "e", Paths: defaultPathPatterns}, + }, + "b": { + {Name: "a", Paths: defaultPathPatterns}, + {Name: "d", Paths: defaultPathPatterns}, + }, }, - data.DelegatedRole{Name: "a", PathMatchers: defaultPathMatchers}, - "", - []string{"a", "b", "a", "e", "d"}, + rootDelegation: data.DelegatedRole{Name: "a", Paths: defaultPathPatterns}, + file: "", + resultOrder: []string{"a", "b", "a", "e", "d"}, }, } @@ -87,9 +111,9 @@ func TestDelegationsIterator(t *testing.T) { } d.add(delegations, r.child.Name) } - assert.Equal(t, len(iterationOrder), len(tt.resultOrder)) + assert.Equal(t, len(tt.resultOrder), len(iterationOrder)) for i, role := range iterationOrder { - assert.Equal(t, role, tt.resultOrder[i]) + assert.Equal(t, tt.resultOrder[i], role) } }) } @@ -104,7 +128,7 @@ func TestGetTargetMeta(t *testing.T) { f, err := c.getTargetFileMeta("f.txt") assert.Nil(t, err) - assert.Equal(t, f.Length, int64(15)) + assert.Equal(t, int64(15), f.Length) } func TestMaxDelegations(t *testing.T) { @@ -115,7 +139,7 @@ func TestMaxDelegations(t *testing.T) { assert.Nil(t, err) c.MaxDelegations = 2 _, err = c.getTargetFileMeta("c.txt") - assert.Equal(t, err, ErrMaxDelegations{File: "c.txt", MaxDelegations: 2, SnapshotVersion: 2}) + assert.Equal(t, ErrMaxDelegations{File: "c.txt", MaxDelegations: 2, SnapshotVersion: 2}, err) } func TestMetaNotFound(t *testing.T) { @@ -125,7 +149,7 @@ func TestMetaNotFound(t *testing.T) { _, err := c.Update() assert.Nil(t, err) _, err = c.getTargetFileMeta("unknown.txt") - assert.Equal(t, err, ErrUnknownTarget{Name: "unknown.txt", SnapshotVersion: 2}) + assert.Equal(t, ErrUnknownTarget{Name: "unknown.txt", SnapshotVersion: 2}, err) } type fakeRemote struct { @@ -209,69 +233,69 @@ func TestPersistedMeta(t *testing.T) { fileContent string }{ { - "unknown", - []expectedTargets{ + file: "unknown", + targets: []expectedTargets{ { - "targets.json", - 2, + name: "targets.json", + version: 2, }, }, - ErrUnknownTarget{Name: "unknown", SnapshotVersion: 2}, - "", + downloadError: ErrUnknownTarget{Name: "unknown", SnapshotVersion: 2}, + fileContent: "", }, { - "b.txt", - []expectedTargets{ + file: "b.txt", + targets: []expectedTargets{ { - "targets.json", - 2, + name: "targets.json", + version: 2, }, { - "a.json", - 1, + name: "a.json", + version: 1, }, { - "b.json", - 1, + name: "b.json", + version: 1, }, }, - nil, - "Contents: b.txt", + downloadError: nil, + fileContent: "Contents: b.txt", }, { - "f.txt", - []expectedTargets{ + file: "f.txt", + targets: []expectedTargets{ { - "targets.json", - 2, + name: "targets.json", + version: 2, }, { - "a.json", - 1, + name: "a.json", + version: 1, }, { - "b.json", - 1, + name: "b.json", + version: 1, }, { - "c.json", - 1, + name: "c.json", + version: 1, }, { - "d.json", - 1, + name: "d.json", + version: 1, }, { - "e.json", - 1, + name: "e.json", + version: 1, }, { - "f.json", - 1, + name: "f.json", + version: 1, }, }, - nil, - "Contents: f.txt", + downloadError: nil, + fileContent: "Contents: f.txt", }, } @@ -291,7 +315,7 @@ func TestPersistedMeta(t *testing.T) { } for _, targets := range tt.targets { storedVersion, err := versionOfStoredTargets(targets.name, persisted) - assert.Equal(t, storedVersion, targets.version) + assert.Equal(t, targets.version, storedVersion) assert.Nil(t, err) delete(persisted, targets.name) } diff --git a/data/types.go b/data/types.go index cbf2ca821..da62e67c2 100644 --- a/data/types.go +++ b/data/types.go @@ -230,70 +230,61 @@ type Delegations struct { // delegatedRoleJSON and delegatedRoleCopy help decoding and encoding jsons. // DelegatedRole UnmarshalJSON will fail and return ErrPathsAndPathHashesSet if both fields are set and not empty type DelegatedRole struct { - Name string `json:"name"` - KeyIDs []string `json:"keyids"` - Threshold int `json:"threshold"` - Terminating bool `json:"terminating"` - MatchWithHashPrefixes bool `json:"-"` - PathMatchers []string `json:"-"` + Name string `json:"name"` + KeyIDs []string `json:"keyids"` + Threshold int `json:"threshold"` + Terminating bool `json:"terminating"` + PathHashPrefixes []string `json:"path_hash_prefixes,omitempty"` + Paths []string `json:"paths"` } func (d *DelegatedRole) MatchesPath(file string) bool { - for _, pattern := range d.PathMatchers { - if d.MatchWithHashPrefixes { - pathHash := PathHexDigest(file) - if strings.HasPrefix(pathHash, pattern) { - return true - } - } + for _, pattern := range d.Paths { if matched, _ := filepath.Match(pattern, file); matched { return true } } + + pathHash := PathHexDigest(file) + for _, hashPrefix := range d.PathHashPrefixes { + if strings.HasPrefix(pathHash, hashPrefix) { + return true + } + } + return false } -type delegatedRoleJSON struct { - delegatedRoleCopy - PathHashPrefixes []string `json:"path_hash_prefixes,omitempty"` - Paths []string `json:"paths"` -} +// validateFields enforces the spec 1.0.19 section 4.5: +// 'role MUST specify only one of the "path_hash_prefixes" or "paths"' +// Marshalling and unmarshalling JSON will fail and return +// ErrPathsAndPathHashesSet if both fields are set and not empty. +func (d *DelegatedRole) validateFields() error { + if len(d.PathHashPrefixes) > 0 && len(d.Paths) > 0 { + return ErrPathsAndPathHashesSet + } -// delegatedRoleCopy is used for MarshalJSON and UnmarshalJSON to extract DelegatedRole -// fields from delegatedRoleJSON -type delegatedRoleCopy DelegatedRole + return nil +} func (d *DelegatedRole) MarshalJSON() ([]byte, error) { - delegatedJSON := delegatedRoleJSON{delegatedRoleCopy: delegatedRoleCopy(*d)} - if d.MatchWithHashPrefixes { - delegatedJSON.PathHashPrefixes = d.PathMatchers - } else { - delegatedJSON.Paths = d.PathMatchers + type delegatedRoleAlias DelegatedRole + + if err := d.validateFields(); err != nil { + return nil, err } - return json.Marshal(&delegatedJSON) + + return json.Marshal((*delegatedRoleAlias)(d)) } func (d *DelegatedRole) UnmarshalJSON(b []byte) error { - var djson delegatedRoleJSON - err := json.Unmarshal(b, &djson) - if err != nil { + type delegatedRoleAlias DelegatedRole + + if err := json.Unmarshal(b, (*delegatedRoleAlias)(d)); err != nil { return err } - new := DelegatedRole(djson.delegatedRoleCopy) - *d = new - if len(djson.PathHashPrefixes) != 0 && len(djson.Paths) != 0 { - return ErrPathsAndPathHashesSet - } - if len(djson.PathHashPrefixes) > 0 { - d.MatchWithHashPrefixes = true - d.PathMatchers = djson.PathHashPrefixes - return nil - } - if len(djson.Paths) > 0 { - d.PathMatchers = djson.Paths - return nil - } - return nil + + return d.validateFields() } func NewTargets() *Targets { diff --git a/data/types_test.go b/data/types_test.go index f9086971b..4f28ea67f 100644 --- a/data/types_test.go +++ b/data/types_test.go @@ -97,11 +97,11 @@ func (TypesSuite) TestRoleAddKeyIDs(c *C) { func TestDelegatedRolePathMatch(t *testing.T) { var tts = []struct { - testName string - file string - matchWithHashPrefixes bool - pathMatchers []string - matches bool + testName string + pathPatterns []string + pathHashPrefixes []string + file string + shouldMatch bool }{ { testName: "no path", @@ -109,62 +109,62 @@ func TestDelegatedRolePathMatch(t *testing.T) { }, { testName: "match path *", - pathMatchers: []string{"null", "targets/*.tgz"}, + pathPatterns: []string{"null", "targets/*.tgz"}, file: "targets/foo.tgz", - matches: true, + shouldMatch: true, }, { testName: "does not match path *", - pathMatchers: []string{"null", "targets/*.tgz"}, + pathPatterns: []string{"null", "targets/*.tgz"}, file: "targets/foo.txt", + shouldMatch: false, }, { testName: "match path ?", - pathMatchers: []string{"foo-version-?.tgz"}, + pathPatterns: []string{"foo-version-?.tgz"}, file: "foo-version-a.tgz", - matches: true, + shouldMatch: true, }, { testName: "does not match ?", - pathMatchers: []string{"foo-version-?.tgz"}, + pathPatterns: []string{"foo-version-?.tgz"}, file: "foo-version-alpha.tgz", + shouldMatch: false, }, // picked from https://github.com/theupdateframework/tuf/blob/30ba6e9f9ab25e0370e29ce574dada2d8809afa0/tests/test_updater.py#L1726-L1734 { - testName: "match hash prefix", - pathMatchers: []string{"badd", "8baf"}, - file: "/file3.txt", - matchWithHashPrefixes: true, - matches: true, + testName: "match hash prefix", + pathHashPrefixes: []string{"badd", "8baf"}, + file: "/file3.txt", + shouldMatch: true, }, { - testName: "does not match hash prefix", - pathMatchers: []string{"badd"}, - matchWithHashPrefixes: true, - file: "/file3.txt", + testName: "does not match hash prefix", + pathHashPrefixes: []string{"badd"}, + file: "/file3.txt", + shouldMatch: false, }, { - testName: "hash prefix first char", - pathMatchers: []string{"2"}, - matchWithHashPrefixes: true, - file: "/a/b/c/file_d.txt", - matches: true, + testName: "hash prefix first char", + pathHashPrefixes: []string{"2"}, + file: "/a/b/c/file_d.txt", + shouldMatch: true, }, { - testName: "full hash prefix", - pathMatchers: []string{"34c85d1ee84f61f10d7dc633472a49096ed87f8f764bd597831eac371f40ac39"}, - matchWithHashPrefixes: true, - file: "/e/f/g.txt", - matches: true, + testName: "full hash prefix", + pathHashPrefixes: []string{"34c85d1ee84f61f10d7dc633472a49096ed87f8f764bd597831eac371f40ac39"}, + file: "/e/f/g.txt", + shouldMatch: true, }, } for _, tt := range tts { t.Run(tt.testName, func(t *testing.T) { d := DelegatedRole{ - PathMatchers: tt.pathMatchers, - MatchWithHashPrefixes: tt.matchWithHashPrefixes, + Paths: tt.pathPatterns, + PathHashPrefixes: tt.pathHashPrefixes, } - assert.Equal(t, tt.matches, d.MatchesPath(tt.file)) + assert.NoError(t, d.validateFields()) + assert.Equal(t, tt.shouldMatch, d.MatchesPath(tt.file)) }) } @@ -173,49 +173,46 @@ func TestDelegatedRolePathMatch(t *testing.T) { func TestDelegatedRoleJSON(t *testing.T) { var tts = []struct { testName string - d DelegatedRole + d *DelegatedRole rawCJSON string }{{ - "all fields with hashes", - DelegatedRole{ - Name: "n1", - KeyIDs: []string{"k1"}, - Threshold: 5, - Terminating: true, - MatchWithHashPrefixes: true, - PathMatchers: []string{"8f"}, + testName: "all fields with hashes", + d: &DelegatedRole{ + Name: "n1", + KeyIDs: []string{"k1"}, + Threshold: 5, + Terminating: true, + PathHashPrefixes: []string{"8f"}, }, - `{"keyids":["k1"],"name":"n1","path_hash_prefixes":["8f"],"paths":null,"terminating":true,"threshold":5}`, + rawCJSON: `{"keyids":["k1"],"name":"n1","path_hash_prefixes":["8f"],"paths":null,"terminating":true,"threshold":5}`, }, { - "paths only", - DelegatedRole{ - Name: "n2", - KeyIDs: []string{"k1", "k3"}, - Threshold: 12, - PathMatchers: []string{"*.txt"}, + testName: "paths only", + d: &DelegatedRole{ + Name: "n2", + KeyIDs: []string{"k1", "k3"}, + Threshold: 12, + Paths: []string{"*.txt"}, }, - `{"keyids":["k1","k3"],"name":"n2","paths":["*.txt"],"terminating":false,"threshold":12}`, + rawCJSON: `{"keyids":["k1","k3"],"name":"n2","paths":["*.txt"],"terminating":false,"threshold":12}`, }, { - "default", - DelegatedRole{}, - `{"keyids":null,"name":"","paths":null,"terminating":false,"threshold":0}`, + testName: "default", + d: &DelegatedRole{}, + rawCJSON: `{"keyids":null,"name":"","paths":null,"terminating":false,"threshold":0}`, }, } for _, tt := range tts { t.Run(tt.testName, func(t *testing.T) { - d := &DelegatedRole{} - *d = tt.d - - raw, err := cjson.Marshal(d) - assert.Nil(t, err) - assert.Equal(t, tt.rawCJSON, string(raw)) - - var newD DelegatedRole - assert.Nil(t, json.Unmarshal(raw, &newD)) - assert.Equal(t, d, &newD) + b, err := cjson.Marshal(tt.d) + assert.NoError(t, err) + assert.Equal(t, tt.rawCJSON, string(b)) + + newD := &DelegatedRole{} + err = json.Unmarshal(b, newD) + assert.NoError(t, err) + assert.Equal(t, tt.d, newD) }) } } @@ -223,9 +220,9 @@ func TestDelegatedRoleJSON(t *testing.T) { func TestDelegatedRoleUnmarshalErr(t *testing.T) { targetsWithBothMatchers := []byte(`{"keyids":null,"name":"","paths":["*.txt"],"path_hash_prefixes":["8f"],"terminating":false,"threshold":0}`) var d DelegatedRole - assert.Equal(t, json.Unmarshal(targetsWithBothMatchers, &d), ErrPathsAndPathHashesSet) + assert.Equal(t, ErrPathsAndPathHashesSet, json.Unmarshal(targetsWithBothMatchers, &d)) // test for type errors err := json.Unmarshal([]byte(`{"keyids":"a"}`), &d) - assert.Equal(t, err.Error(), "json: cannot unmarshal string into Go struct field delegatedRoleJSON.keyids of type []string") + assert.Equal(t, "keyids", err.(*json.UnmarshalTypeError).Field) }