Skip to content

Commit

Permalink
Support delegations in client (#137)
Browse files Browse the repository at this point in the history
* Add delegation client

* Add TUF3 php test

* Use ioutil for go 1.15

* Add more delegations tests

* Cleanups

* Check new paths

* Add suggestion

* Add struct tags

* Add tags and remove duplicate types

* Formatting

* Fix order of asserts (should be want, got)

* Clean up DelegatedRole validation

* Fix root to top targets delegation

* July 14 changes (#4)

* Clarify validation of signing role & metadata type

* Add/update comments

* Validation happens on both decode & encode

* Bubble up an error if MatchesPath is called on an invalid DelegatedRole

* Match spec for delegation traversal

* Comment in the iterator

* Add diamond test case

* Revert "Match spec for delegation traversal"

This reverts commit 15fee6b.

* Rename IsTopLevelRole back to ValidRole to avoid breaking change

* Update after reviews

* Add back lower case check

* Initialize with size and comment

* Simplify iterator initialization

* Revert back to "nodes seen" interpretation of delegation traversal spec (#6)

(instead of true cycle detection with "edges seen").

This reverts commit cfbb024.

* Update client/delegations.go

Co-authored-by: Ethan Lowman <53835328+ethan-lowman-dd@users.noreply.github.com>

* Update following reviews of 16th of july (#7)

* Update name

* Rename file to target

* Nits

* Move verifier in iterator and rename fields

* Add tests

* Update after 19th july review (#9)

* Update delegations to err on top level role

* Add simple cycle test

* Remove duplicate check

* Fix comment

* Update comment

Co-authored-by: Ethan Lowman <ethan.lowman@datadoghq.com>
Co-authored-by: Ethan Lowman <53835328+ethan-lowman-dd@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 4, 2021
1 parent 90e2627 commit e95956b
Show file tree
Hide file tree
Showing 76 changed files with 2,873 additions and 41 deletions.
56 changes: 39 additions & 17 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
// big it is.
defaultRootDownloadLimit = 512000
defaultTimestampDownloadLimit = 16384
defaultMaxDelegations = 32
)

// LocalStore is local storage for downloaded top-level metadata.
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -189,16 +195,18 @@ func (c *Client) update(latestRoot bool) (data.TargetFiles, error) {
return nil, ErrLatestSnapshot{c.snapshotVer}
}

// Get snapshot.json, then extract root.json and targets.json file meta.
// Get snapshot.json, then extract file metas.
//
// The snapshot.json is only saved locally after checking root.json and
// The snapshot.json is only saved locally after checking
// targets.json so that it will be re-downloaded on subsequent updates
// if this update fails.
// root.json meta should not be stored in the snapshot, if it is,
// the root will be checked, re-downloaded
snapshotJSON, err := c.downloadMetaFromTimestamp("snapshot.json", snapshotMeta)
if err != nil {
return nil, err
}
rootMeta, targetsMeta, err := c.decodeSnapshot(snapshotJSON)
snapshotMetas, err := c.decodeSnapshot(snapshotJSON)
if err != nil {
// ErrRoleThreshold could indicate snapshot keys have been
// revoked, so retry with the latest root.json
Expand All @@ -210,13 +218,17 @@ func (c *Client) update(latestRoot bool) (data.TargetFiles, error) {

// If we don't have the root.json, download it, save it in local
// storage and restart the update
if !c.hasMetaFromSnapshot("root.json", rootMeta) {
return c.updateWithLatestRoot(&rootMeta)
// Root should no longer be pinned in snapshot meta https://github.com/theupdateframework/tuf/pull/988
if rootMeta, ok := snapshotMetas["root.json"]; ok {
if !c.hasMetaFromSnapshot("root.json", rootMeta) {
return c.updateWithLatestRoot(&rootMeta)
}
}

// If we don't have the targets.json, download it, determine updated
// targets and save targets.json in local storage
var updatedTargets data.TargetFiles
targetsMeta := snapshotMetas["targets.json"]
if !c.hasMetaFromSnapshot("targets.json", targetsMeta) {
targetsJSON, err := c.downloadMetaFromSnapshot("targets.json", targetsMeta)
if err != nil {
Expand Down Expand Up @@ -539,13 +551,13 @@ 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.SnapshotFiles, error) {
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.SnapshotFiles{}, ErrDecodeFailed{"snapshot.json", err}
}
c.snapshotVer = snapshot.Version
return snapshot.Meta["root.json"], snapshot.Meta["targets.json"], nil
return snapshot.Meta, nil
}

// decodeTargets decodes and verifies targets metadata, sets c.targets and
Expand Down Expand Up @@ -582,18 +594,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
Expand Down Expand Up @@ -634,7 +652,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) {
Expand All @@ -652,11 +671,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
Expand Down
2 changes: 1 addition & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ func (t *testDestination) Delete() error {
func (s *ClientSuite) TestDownloadUnknownTarget(c *C) {
client := s.updatedClient(c)
var dest testDestination
c.Assert(client.Download("nonexistent", &dest), Equals, ErrUnknownTarget{"nonexistent"})
c.Assert(client.Download("nonexistent", &dest), Equals, ErrUnknownTarget{Name: "nonexistent", SnapshotVersion: 1})
c.Assert(dest.deleted, Equals, true)
}

Expand Down
190 changes: 190 additions & 0 deletions client/delegations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package client

import (
"github.com/theupdateframework/go-tuf/data"
"github.com/theupdateframework/go-tuf/verify"
)

// getTargetFileMeta searches for a verified TargetFileMeta matching a target
// 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(target string) (data.TargetFileMeta, error) {
snapshot, err := c.loadLocalSnapshot()
if err != nil {
return data.TargetFileMeta{}, err
}

// 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 target
// - 5.6.7.1 cycles protection
// - 5.6.7.2 terminations
delegations := newDelegationsIterator(target)
for i := 0; i < c.MaxDelegations; i++ {
d, ok := delegations.next()
if !ok {
return data.TargetFileMeta{}, ErrUnknownTarget{target, snapshot.Version}
}

// covers 5.6.{1,2,3,4,5,6}
targets, err := c.loadDelegatedTargets(snapshot, d.delegatee.Name, d.verifier)
if err != nil {
return data.TargetFileMeta{}, err
}

// stop when the searched TargetFileMeta is found
if m, ok := targets.Targets[target]; ok {
return m, nil
}

if targets.Delegations != nil {
delegationsVerifier, err := verify.NewDelegationsVerifier(targets.Delegations)
if err != nil {
return data.TargetFileMeta{}, err
}
err = delegations.add(targets.Delegations.Roles, d.delegatee.Name, delegationsVerifier)
if err != nil {
return data.TargetFileMeta{}, err
}
}
}

return data.TargetFileMeta{}, ErrMaxDelegations{
Target: target,
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 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
// 5.6.4 check against snapshot version
raw, alreadyStored := c.localMetaFromSnapshot(fileName, fileMeta)
if !alreadyStored {
raw, err = c.downloadMetaFromSnapshot(fileName, fileMeta)
if err != nil {
return nil, err
}
}

targets := &data.Targets{}
// 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)
}
if err != nil {
return nil, ErrDecodeFailed{fileName, err}
}

// 5.6.6 persist
if !alreadyStored {
if err := c.local.SetMeta(fileName, raw); err != nil {
return nil, err
}
}
return targets, nil
}

type delegation struct {
delegator string
verifier verify.DelegationsVerifier
delegatee data.DelegatedRole
}

type delegationsIterator struct {
stack []delegation
target string
visitedRoles map[string]struct{}
}

// newDelegationsIterator initialises an iterator with a first step
// on top level targets
func newDelegationsIterator(target string) *delegationsIterator {
i := &delegationsIterator{
target: target,
stack: []delegation{
{
delegatee: data.DelegatedRole{Name: "targets"},
},
},
visitedRoles: make(map[string]struct{}),
}
return i
}

func (d *delegationsIterator) next() (value delegation, ok bool) {
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: If this role has been visited before, then skip this role (so
// that cycles in the delegation graph are avoided).
roleName := delegation.delegatee.Name
if _, ok := d.visitedRoles[roleName]; ok {
return d.next()
}
d.visitedRoles[roleName] = 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.delegatee.Terminating {
// Empty the stack.
d.stack = d.stack[0:0]
}
return delegation, true
}

func (d *delegationsIterator) add(roles []data.DelegatedRole, delegator string, verifier verify.DelegationsVerifier) 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.
r := roles[i]
matchesPath, err := r.MatchesPath(d.target)
if err != nil {
return err
}
if matchesPath {
delegation := delegation{
delegator: delegator,
delegatee: r,
verifier: verifier,
}
d.stack = append(d.stack, delegation)
}
}

return nil
}
Loading

0 comments on commit e95956b

Please sign in to comment.