-
Notifications
You must be signed in to change notification settings - Fork 115
Support delegations in client #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
trishankatdatadog
merged 23 commits into
theupdateframework:master
from
raphaelgavache:raphael/support_delegations_client
Aug 4, 2021
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
17ca5d9
Add delegation client
raphaelgavache b01e941
Add TUF3 php test
raphaelgavache 205252d
Use ioutil for go 1.15
raphaelgavache b1d9372
Add more delegations tests
raphaelgavache 26d3dcd
Cleanups
raphaelgavache 8eac49a
Check new paths
raphaelgavache 64a1e27
Add suggestion
raphaelgavache e15f592
Add struct tags
ethan-lowman-dd c0b597c
Add tags and remove duplicate types
ethan-lowman-dd 6b76252
Formatting
ethan-lowman-dd 14376fb
Fix order of asserts (should be want, got)
ethan-lowman-dd 0c822ec
Clean up DelegatedRole validation
ethan-lowman-dd 22b1e63
Merge pull request #3 from ethan-lowman-dd/ethan.lowman/delegations-i…
raphaelgavache a8f8cb0
Fix root to top targets delegation
raphaelgavache ea6bcf8
July 14 changes (#4)
ethan-lowman-dd 9631aeb
Update after reviews
raphaelgavache 80371ea
Revert back to "nodes seen" interpretation of delegation traversal sp…
ethan-lowman-dd 0334084
Update client/delegations.go
raphaelgavache f5a27f3
Update following reviews of 16th of july (#7)
raphaelgavache 9c997b3
Add tests
raphaelgavache e9f9750
Update after 19th july review (#9)
raphaelgavache e7cf8b7
Fix comment
raphaelgavache deff639
Update comment
raphaelgavache File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ const ( | |
| // big it is. | ||
| defaultRootDownloadLimit = 512000 | ||
| defaultTimestampDownloadLimit = 16384 | ||
| defaultMaxDelegations = 32 | ||
| ) | ||
|
|
||
| // LocalStore is local storage for downloaded top-level metadata. | ||
|
|
@@ -81,12 +82,17 @@ type Client struct { | |
| // consistentSnapshot indicates whether the remote storage is using | ||
| // consistent snapshots (as specified in root.json) | ||
| consistentSnapshot bool | ||
|
|
||
| // MaxDelegations limits by default the number of delegations visited for any | ||
| // target | ||
| MaxDelegations int | ||
| } | ||
|
|
||
| func NewClient(local LocalStore, remote RemoteStore) *Client { | ||
| return &Client{ | ||
| local: local, | ||
| remote: remote, | ||
| local: local, | ||
| remote: remote, | ||
| MaxDelegations: defaultMaxDelegations, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -198,7 +204,7 @@ func (c *Client) update(latestRoot bool) (data.TargetFiles, error) { | |
| if err != nil { | ||
| return nil, err | ||
| } | ||
| rootMeta, targetsMeta, err := c.decodeSnapshot(snapshotJSON) | ||
| rootMeta, rootInSnapshot, targetsMeta, err := c.decodeSnapshot(snapshotJSON) | ||
| if err != nil { | ||
| // ErrRoleThreshold could indicate snapshot keys have been | ||
| // revoked, so retry with the latest root.json | ||
|
|
@@ -210,7 +216,8 @@ func (c *Client) update(latestRoot bool) (data.TargetFiles, error) { | |
|
|
||
| // If we don't have the root.json, download it, save it in local | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be the responsibility of the user to download the very first root.json from a secure channel? |
||
| // storage and restart the update | ||
| if !c.hasMetaFromSnapshot("root.json", rootMeta) { | ||
| // Root should no longer be pinned in snapshot meta https://github.com/theupdateframework/tuf/pull/988 | ||
| if rootInSnapshot && !c.hasMetaFromSnapshot("root.json", rootMeta) { | ||
| return c.updateWithLatestRoot(&rootMeta) | ||
| } | ||
|
|
||
|
|
@@ -539,13 +546,14 @@ func (c *Client) decodeRoot(b json.RawMessage) error { | |
|
|
||
| // decodeSnapshot decodes and verifies snapshot metadata, and returns the new | ||
| // root and targets file meta. | ||
| func (c *Client) decodeSnapshot(b json.RawMessage) (data.SnapshotFileMeta, data.SnapshotFileMeta, error) { | ||
| func (c *Client) decodeSnapshot(b json.RawMessage) (data.SnapshotFileMeta, bool, data.SnapshotFileMeta, error) { | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| snapshot := &data.Snapshot{} | ||
| if err := c.db.Unmarshal(b, snapshot, "snapshot", c.snapshotVer); err != nil { | ||
| return data.SnapshotFileMeta{}, data.SnapshotFileMeta{}, ErrDecodeFailed{"snapshot.json", err} | ||
| return data.SnapshotFileMeta{}, false, data.SnapshotFileMeta{}, ErrDecodeFailed{"snapshot.json", err} | ||
| } | ||
| c.snapshotVer = snapshot.Version | ||
| return snapshot.Meta["root.json"], snapshot.Meta["targets.json"], nil | ||
| rootMeta, rootInSnapshot := snapshot.Meta["root.json"] | ||
| return rootMeta, rootInSnapshot, snapshot.Meta["targets.json"], nil | ||
| } | ||
|
|
||
| // decodeTargets decodes and verifies targets metadata, sets c.targets and | ||
|
|
@@ -582,18 +590,24 @@ func (c *Client) decodeTimestamp(b json.RawMessage) (data.TimestampFileMeta, err | |
| return timestamp.Meta["snapshot.json"], nil | ||
| } | ||
|
|
||
| // hasSnapshotMeta checks whether local metadata has the given meta | ||
| // hasMetaFromSnapshot checks whether local metadata has the given meta | ||
| func (c *Client) hasMetaFromSnapshot(name string, m data.SnapshotFileMeta) bool { | ||
| _, ok := c.localMetaFromSnapshot(name, m) | ||
| return ok | ||
| } | ||
|
|
||
| // localMetaFromSnapshot returns localmetadata if it matches the snapshot | ||
| func (c *Client) localMetaFromSnapshot(name string, m data.SnapshotFileMeta) (json.RawMessage, bool) { | ||
| b, ok := c.localMeta[name] | ||
| if !ok { | ||
| return false | ||
| return nil, false | ||
| } | ||
| meta, err := util.GenerateSnapshotFileMeta(bytes.NewReader(b), m.HashAlgorithms()...) | ||
| if err != nil { | ||
| return false | ||
| return nil, false | ||
| } | ||
| err = util.SnapshotFileMetaEqual(meta, m) | ||
| return err == nil | ||
| return b, err == nil | ||
| } | ||
|
|
||
| // hasTargetsMeta checks whether local metadata has the given snapshot meta | ||
|
|
@@ -634,7 +648,8 @@ type Destination interface { | |
| // dest will be deleted and an error returned in the following situations: | ||
| // | ||
| // * The target does not exist in the local targets.json | ||
| // * The target does not exist in remote storage | ||
| // * Failed to fetch the chain of delegations accessible from local snapshot.json | ||
| // * The target does not exist in any targets | ||
| // * Metadata cannot be generated for the downloaded data | ||
| // * Generated metadata does not match local metadata for the given file | ||
| func (c *Client) Download(name string, dest Destination) (err error) { | ||
|
|
@@ -652,11 +667,14 @@ func (c *Client) Download(name string, dest Destination) (err error) { | |
| } | ||
| } | ||
|
|
||
| // return ErrUnknownTarget if the file is not in the local targets.json | ||
| normalizedName := util.NormalizeTarget(name) | ||
| localMeta, ok := c.targets[normalizedName] | ||
| if !ok { | ||
| return ErrUnknownTarget{name} | ||
| // search in delegations | ||
| localMeta, err = c.getTargetFileMeta(normalizedName) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| // get the data from remote storage | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| package client | ||
|
|
||
| import ( | ||
| "github.com/theupdateframework/go-tuf/data" | ||
| "github.com/theupdateframework/go-tuf/verify" | ||
| ) | ||
|
|
||
| // getTargetFileMeta searches for a verified TargetFileMeta matching a file name | ||
| // Requires a local snapshot to be loaded and is locked to the snapshot versions. | ||
| // Searches through delegated targets following TUF spec 1.0.19 section 5.6. | ||
| func (c *Client) getTargetFileMeta(file string) (data.TargetFileMeta, error) { | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| snapshot, err := c.loadLocalSnapshot() | ||
| if err != nil { | ||
| return data.TargetFileMeta{}, err | ||
| } | ||
| verifiers := map[string]verify.DelegationsVerifier{"root": c.db} | ||
|
|
||
| // delegationsIterator covers 5.6.7 | ||
| // - pre-order depth-first search starting with the top targets | ||
| // - filter delegations with paths or path_hash_prefixes matching searched file | ||
| // - 5.6.7.1 cycles protection | ||
| // - 5.6.7.2 terminations | ||
| delegations := newDelegationsIterator(c.rootTargetDelegation(), "root", file) | ||
| for i := 0; i < c.MaxDelegations; i++ { | ||
| d, ok := delegations.next() | ||
| if !ok { | ||
| return data.TargetFileMeta{}, ErrUnknownTarget{file, snapshot.Version} | ||
| } | ||
| verifier := verifiers[d.parent] | ||
| // covers 5.6.{1,2,3,4,5,6} | ||
| 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) | ||
| if err != nil { | ||
| return data.TargetFileMeta{}, err | ||
| } | ||
| verifiers[d.child.Name] = targetVerifier | ||
| } | ||
| } | ||
| return data.TargetFileMeta{}, ErrMaxDelegations{ | ||
| File: file, | ||
| MaxDelegations: c.MaxDelegations, | ||
| SnapshotVersion: snapshot.Version, | ||
| } | ||
| } | ||
|
|
||
| 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} | ||
| } | ||
| return snapshot, nil | ||
| } | ||
|
|
||
| // loadDelegatedTargets downloads, decodes, verifies and stores delegated targets | ||
| func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, verifier verify.DelegationsVerifier) (*data.Targets, error) { | ||
| var err error | ||
| fileName := role + ".json" | ||
| fileMeta, ok := snapshot.Meta[fileName] | ||
| 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) | ||
| if !alreadyStored { | ||
| raw, err = c.downloadMetaFromSnapshot(fileName, fileMeta) | ||
| if err != nil { | ||
| 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 | ||
| if err := verifier.Unmarshal(raw, target, role, fileMeta.Version); err != nil { | ||
| return nil, ErrDecodeFailed{fileName, err} | ||
| } | ||
| // 5.6.4 check against snapshot version | ||
| if target.Version != fileMeta.Version { | ||
| return nil, ErrTargetsSnapshotVersionMismatch{ | ||
| Role: fileName, | ||
| DownloadedTargetsVersion: fileMeta.Version, | ||
| TargetsSnapshotVersion: target.Version, | ||
| SnapshotVersion: snapshot.Version, | ||
| } | ||
| } | ||
| // 5.6.6 persist | ||
| if !alreadyStored { | ||
| if err := c.local.SetMeta(fileName, raw); err != nil { | ||
raphaelgavache marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return nil, err | ||
| } | ||
| } | ||
| return target, nil | ||
| } | ||
|
|
||
| func (c *Client) rootTargetDelegation() data.DelegatedRole { | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| role := "targets" | ||
| r := c.db.GetRole(role) | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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, | ||
| Paths: []string{"*"}, | ||
| } | ||
| } | ||
|
|
||
| type delegation struct { | ||
| parent string | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| child data.DelegatedRole | ||
| } | ||
|
|
||
| type delegationID struct { | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| parent string | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| child string | ||
| } | ||
|
|
||
| type delegationsIterator struct { | ||
| stack []delegation | ||
| file string | ||
| visited map[delegationID]struct{} | ||
| } | ||
|
|
||
| func newDelegationsIterator(role data.DelegatedRole, parent string, file string) *delegationsIterator { | ||
| i := &delegationsIterator{ | ||
| file: file, | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| stack: make([]delegation, 0, 1), | ||
| visited: make(map[delegationID]struct{}), | ||
| } | ||
| i.add([]data.DelegatedRole{role}, parent) | ||
| return i | ||
| } | ||
|
|
||
| func (d *delegationsIterator) next() (delegation, bool) { | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if len(d.stack) == 0 { | ||
| return delegation{}, false | ||
| } | ||
| delegation := d.stack[len(d.stack)-1] | ||
| d.stack = d.stack[:len(d.stack)-1] | ||
|
|
||
| // 5.6.7.1 cycles protection | ||
| id := delegationID{delegation.parent, delegation.child.Name} | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if _, ok := d.visited[id]; ok { | ||
| return d.next() | ||
raphaelgavache marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| d.visited[id] = struct{}{} | ||
|
|
||
| // 5.6.7.2 trim delegations to visit, only the current role and its delegations | ||
| // will be considered | ||
| // https://github.com/theupdateframework/specification/issues/168 | ||
| if delegation.child.Terminating { | ||
| d.stack = d.stack[0:0] | ||
raphaelgavache marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| return delegation, true | ||
| } | ||
|
|
||
| func (d *delegationsIterator) add(roles []data.DelegatedRole, parent string) { | ||
raphaelgavache marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| for i := len(roles) - 1; i >= 0; i-- { | ||
| r := roles[i] | ||
| if r.MatchesPath(d.file) { | ||
| d.stack = append(d.stack, delegation{parent, r}) | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.