Skip to content

Commit

Permalink
fix(localMeta): Add delegated targets back to localMeta (#384)
Browse files Browse the repository at this point in the history
* upgrade(localMeta): Verify delegated targets

Adds verification of delegated targets when they go from the local
store to the localMeta structure.
The verification is done by selecting one target file per delegated
targets that verify at least one, then build the delegation path from
the top targets to that specific delegated file. Building the path is
already verifying every targets in the path, so the whole path is safe
to add to the localMeta. Doing so for every delegated targets lets us
verify every delegated targets that is used at least once.

Signed-off-by: Baptiste Foy <baptiste.foy@datadoghq.com>
  • Loading branch information
BaptisteFoy authored Jan 9, 2023
1 parent 9cb61d6 commit 7e86441
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 10 deletions.
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
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 {
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]
}

0 comments on commit 7e86441

Please sign in to comment.