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

fix(localMeta): Add delegated targets back to localMeta #384

74 changes: 73 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"

"github.com/theupdateframework/go-tuf/data"
"github.com/theupdateframework/go-tuf/internal/roles"
"github.com/theupdateframework/go-tuf/util"
"github.com/theupdateframework/go-tuf/verify"
)
Expand Down Expand Up @@ -399,8 +401,8 @@ func (c *Client) getLocalMeta() error {
}
}

snapshot := &data.Snapshot{}
if snapshotJSON, ok := meta["snapshot.json"]; ok {
snapshot := &data.Snapshot{}
if err := c.db.UnmarshalTrusted(snapshotJSON, snapshot, "snapshot"); err != nil {
loadFailed = true
retErr = err
Expand All @@ -423,13 +425,83 @@ func (c *Client) getLocalMeta() error {
c.loadTargets(targets.Targets)
}
}

if loadFailed {
// If any of the metadata failed to be verified, return the reason for that failure
// and fail fast before delegated targets
return retErr
}

// verifiedDelegatedTargets is a set of verified delegated targets
asraa marked this conversation as resolved.
Show resolved Hide resolved
verifiedDelegatedTargets := make(map[string]bool)
for fileName := range meta {
if !verifiedDelegatedTargets[fileName] && roles.IsDelegatedTargetsManifest(fileName) {
if delegationPath, err := c.getDelegationPathFromRaw(snapshot, meta[fileName]); err != nil {
asraa marked this conversation as resolved.
Show resolved Hide resolved
loadFailed = true
retErr = err
} else {
// Every delegated targets in the path has been verified
// as a side effect of getDelegationPathFromRaw
for _, key := range delegationPath {
fileName := fmt.Sprintf("%s.json", key)
verifiedDelegatedTargets[fileName] = true
}
}
}
}

for fileName := range verifiedDelegatedTargets {
c.localMeta[fileName] = meta[fileName]
}

if loadFailed {
// If any of the metadata failed to be verified, return the reason for that failure
return retErr
}
return nil
}

// getDelegationPathFromRaw verifies a delegated targets against
// a given snapshot and returns an error if it's invalid
//
// Delegation must have targets to get a path, else an empty list
// will be returned: this is because the delegation iterator is leveraged.
//
// Concrete example:
// targets
// └── a.json
//   └── b.json
//      └── c.json
//        └── target_file.txt
//
// If you try to use that function on "a.json" or "b.json", it'll return an empty list
// with no error, as neither of them declare a target file
// On the other hand, if you use that function on "c.json", it'll return & verify
// [c.json, b.json, a.json]. Running that function on every delegated targets
// guarantees that if a delegated targets is in the path of a target file, then it will
// appear at least once in the result
func (c *Client) getDelegationPathFromRaw(snapshot *data.Snapshot, delegatedTargetsJSON json.RawMessage) ([]string, error) {
// unmarshal the delegated targets first without verifying as
// we need at least one targets file name to leverage the
// getTargetFileMetaDelegationPath method
s := &data.Signed{}
if err := json.Unmarshal(delegatedTargetsJSON, s); err != nil {
return nil, err
}
targets := &data.Targets{}
if err := json.Unmarshal(s.Signed, targets); err != nil {
return nil, err
}
for targetPath := range targets.Targets {
_, resp, err := c.getTargetFileMetaDelegationPath(targetPath, snapshot)
// We only need to test one targets file:
// - If it is valid, it means the delegated targets has been validated
// - If it is not, the delegated targets isn't valid
return resp, err
}
return nil, nil
}

// loadAndVerifyLocalRootMeta decodes and verifies root metadata from
// local storage and loads the top-level keys. This method first clears
// the DB for top-level keys and then loads the new keys.
Expand Down
53 changes: 44 additions & 9 deletions client/delegations.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,99 @@ import (

// 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
}

targetFileMeta, _, err := c.getTargetFileMetaDelegationPath(target, snapshot)
if err != nil {
return data.TargetFileMeta{}, err
}
return targetFileMeta, nil
}

// getTargetFileMetaDelegationPath searches for a verified TargetFileMeta matching a target
// Requires snapshot to be passed and is locked to that specific snapshot versions.
// Searches through delegated targets following TUF spec 1.0.19 section 5.6.
func (c *Client) getTargetFileMetaDelegationPath(target string, snapshot *data.Snapshot) (data.TargetFileMeta, []string, error) {
// 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, err := targets.NewDelegationsIterator(target, c.db)
if err != nil {
return data.TargetFileMeta{}, err
return data.TargetFileMeta{}, nil, err
}

targetFileMeta := data.TargetFileMeta{}
delegationRole := ""

for i := 0; i < c.MaxDelegations; i++ {
d, ok := delegations.Next()
if !ok {
return data.TargetFileMeta{}, ErrUnknownTarget{target, snapshot.Version}
return data.TargetFileMeta{}, nil, ErrUnknownTarget{target, snapshot.Version}
}

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

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

if targets.Delegations != nil {
delegationsDB, err := verify.NewDBFromDelegations(targets.Delegations)
if err != nil {
return data.TargetFileMeta{}, err
return data.TargetFileMeta{}, nil, err
}
err = delegations.Add(targets.Delegations.Roles, d.Delegatee.Name, delegationsDB)
if err != nil {
return data.TargetFileMeta{}, err
return data.TargetFileMeta{}, nil, err
}
}
}

return data.TargetFileMeta{}, ErrMaxDelegations{
if len(delegationRole) > 0 {
return targetFileMeta, buildPath(delegations.Parent, delegationRole, ""), nil
}

return data.TargetFileMeta{}, nil, ErrMaxDelegations{
Target: target,
MaxDelegations: c.MaxDelegations,
SnapshotVersion: snapshot.Version,
}
}

func buildPath(parent func(string) string, start string, end string) []string {
if start == end {
return nil
}

path := []string{start}
current := start
for {
current = parent(current)
if current == end {
break
}
path = append(path, current)
}
return path
}

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
Expand Down
10 changes: 10 additions & 0 deletions client/delegations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,17 +226,27 @@ func TestPersistedMeta(t *testing.T) {
p, err := c.local.GetMeta()
assert.Nil(t, err)
persisted := copyStore(p)
persistedLocal := copyStore(c.localMeta)
// trim non targets metas
for _, notTargets := range []string{"root.json", "snapshot.json", "timestamp.json"} {
delete(persisted, notTargets)
delete(persistedLocal, notTargets)
}
for _, targets := range tt.targets {
// Test local store
storedVersion, err := versionOfStoredTargets(targets.name, persisted)
assert.Equal(t, targets.version, storedVersion)
assert.Nil(t, err)
delete(persisted, targets.name)

// Test localMeta
storedVersion, err = versionOfStoredTargets(targets.name, persistedLocal)
assert.Equal(t, targets.version, storedVersion)
assert.Nil(t, err)
delete(persistedLocal, targets.name)
}
assert.Empty(t, persisted)
assert.Empty(t, persistedLocal)
})
}
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/targets/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type delegationsIterator struct {
stack []Delegation
target string
visitedRoles map[string]struct{}
parents map[string]string
}

var ErrTopLevelTargetsRoleMissing = errors.New("tuf: top level targets role missing from top level keys DB")
Expand All @@ -43,6 +44,7 @@ func NewDelegationsIterator(target string, topLevelKeysDB *verify.DB) (*delegati
},
},
visitedRoles: make(map[string]struct{}),
parents: make(map[string]string),
}
return i, nil
}
Expand Down Expand Up @@ -88,8 +90,13 @@ func (d *delegationsIterator) Add(roles []data.DelegatedRole, delegator string,
DB: db,
}
d.stack = append(d.stack, delegation)
d.parents[r.Name] = delegator
}
}

return nil
}

func (d *delegationsIterator) Parent(role string) string {
return d.parents[role]
}