Skip to content
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

Support delegations in client #137

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 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 @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
}

Expand Down Expand Up @@ -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) {
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
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
185 changes: 185 additions & 0 deletions client/delegations.go
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) {
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 {
return nil, err
}
}
return target, nil
}

func (c *Client) rootTargetDelegation() data.DelegatedRole {
role := "targets"
r := c.db.GetRole(role)
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
child data.DelegatedRole
}

type delegationID struct {
parent string
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,
stack: make([]delegation, 0, 1),
visited: make(map[delegationID]struct{}),
}
i.add([]data.DelegatedRole{role}, parent)
return i
}

func (d *delegationsIterator) next() (delegation, 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 cycles protection
id := delegationID{delegation.parent, delegation.child.Name}
if _, ok := d.visited[id]; ok {
return d.next()
}
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]
}
return delegation, true
}

func (d *delegationsIterator) add(roles []data.DelegatedRole, parent string) {
for i := len(roles) - 1; i >= 0; i-- {
r := roles[i]
if r.MatchesPath(d.file) {
d.stack = append(d.stack, delegation{parent, r})
}
}
}
Loading