Skip to content

Commit 17ca5d9

Browse files
Add delegation client
1 parent 22a9481 commit 17ca5d9

12 files changed

Lines changed: 718 additions & 23 deletions

File tree

client/client.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
// big it is.
1919
defaultRootDownloadLimit = 512000
2020
defaultTimestampDownloadLimit = 16384
21+
defaultMaxDelegations = 32
2122
)
2223

2324
// LocalStore is local storage for downloaded top-level metadata.
@@ -81,12 +82,17 @@ type Client struct {
8182
// consistentSnapshot indicates whether the remote storage is using
8283
// consistent snapshots (as specified in root.json)
8384
consistentSnapshot bool
85+
86+
// MaxDelegations limits by default the number of delegations visited for any
87+
// target
88+
MaxDelegations int
8489
}
8590

8691
func NewClient(local LocalStore, remote RemoteStore) *Client {
8792
return &Client{
88-
local: local,
89-
remote: remote,
93+
local: local,
94+
remote: remote,
95+
MaxDelegations: defaultMaxDelegations,
9096
}
9197
}
9298

@@ -198,7 +204,7 @@ func (c *Client) update(latestRoot bool) (data.TargetFiles, error) {
198204
if err != nil {
199205
return nil, err
200206
}
201-
rootMeta, targetsMeta, err := c.decodeSnapshot(snapshotJSON)
207+
rootMeta, rootInSnapshot, targetsMeta, err := c.decodeSnapshot(snapshotJSON)
202208
if err != nil {
203209
// ErrRoleThreshold could indicate snapshot keys have been
204210
// revoked, so retry with the latest root.json
@@ -210,7 +216,8 @@ func (c *Client) update(latestRoot bool) (data.TargetFiles, error) {
210216

211217
// If we don't have the root.json, download it, save it in local
212218
// storage and restart the update
213-
if !c.hasMetaFromSnapshot("root.json", rootMeta) {
219+
// Root should no longer be pinned in snapshot meta https://github.com/theupdateframework/tuf/pull/988
220+
if rootInSnapshot && !c.hasMetaFromSnapshot("root.json", rootMeta) {
214221
return c.updateWithLatestRoot(&rootMeta)
215222
}
216223

@@ -539,13 +546,14 @@ func (c *Client) decodeRoot(b json.RawMessage) error {
539546

540547
// decodeSnapshot decodes and verifies snapshot metadata, and returns the new
541548
// root and targets file meta.
542-
func (c *Client) decodeSnapshot(b json.RawMessage) (data.SnapshotFileMeta, data.SnapshotFileMeta, error) {
549+
func (c *Client) decodeSnapshot(b json.RawMessage) (data.SnapshotFileMeta, bool, data.SnapshotFileMeta, error) {
543550
snapshot := &data.Snapshot{}
544551
if err := c.db.Unmarshal(b, snapshot, "snapshot", c.snapshotVer); err != nil {
545-
return data.SnapshotFileMeta{}, data.SnapshotFileMeta{}, ErrDecodeFailed{"snapshot.json", err}
552+
return data.SnapshotFileMeta{}, false, data.SnapshotFileMeta{}, ErrDecodeFailed{"snapshot.json", err}
546553
}
547554
c.snapshotVer = snapshot.Version
548-
return snapshot.Meta["root.json"], snapshot.Meta["targets.json"], nil
555+
rootMeta, rootInSnapshot := snapshot.Meta["root.json"]
556+
return rootMeta, rootInSnapshot, snapshot.Meta["targets.json"], nil
549557
}
550558

551559
// decodeTargets decodes and verifies targets metadata, sets c.targets and
@@ -582,18 +590,24 @@ func (c *Client) decodeTimestamp(b json.RawMessage) (data.TimestampFileMeta, err
582590
return timestamp.Meta["snapshot.json"], nil
583591
}
584592

585-
// hasSnapshotMeta checks whether local metadata has the given meta
593+
// hasMetaFromSnapshot checks whether local metadata has the given meta
586594
func (c *Client) hasMetaFromSnapshot(name string, m data.SnapshotFileMeta) bool {
595+
_, ok := c.localMetaFromSnapshot(name, m)
596+
return ok
597+
}
598+
599+
// localMetaFromSnapshot returns localmetadata if it matches the snapshot
600+
func (c *Client) localMetaFromSnapshot(name string, m data.SnapshotFileMeta) (json.RawMessage, bool) {
587601
b, ok := c.localMeta[name]
588602
if !ok {
589-
return false
603+
return nil, false
590604
}
591605
meta, err := util.GenerateSnapshotFileMeta(bytes.NewReader(b), m.HashAlgorithms()...)
592606
if err != nil {
593-
return false
607+
return nil, false
594608
}
595609
err = util.SnapshotFileMetaEqual(meta, m)
596-
return err == nil
610+
return b, err == nil
597611
}
598612

599613
// hasTargetsMeta checks whether local metadata has the given snapshot meta
@@ -634,7 +648,8 @@ type Destination interface {
634648
// dest will be deleted and an error returned in the following situations:
635649
//
636650
// * The target does not exist in the local targets.json
637-
// * The target does not exist in remote storage
651+
// * Failed to fetch the chain of delegations accessible from local snapshot.json
652+
// * The target does not exist in any targets
638653
// * Metadata cannot be generated for the downloaded data
639654
// * Generated metadata does not match local metadata for the given file
640655
func (c *Client) Download(name string, dest Destination) (err error) {
@@ -652,11 +667,14 @@ func (c *Client) Download(name string, dest Destination) (err error) {
652667
}
653668
}
654669

655-
// return ErrUnknownTarget if the file is not in the local targets.json
656670
normalizedName := util.NormalizeTarget(name)
657671
localMeta, ok := c.targets[normalizedName]
658672
if !ok {
659-
return ErrUnknownTarget{name}
673+
// search in delegations
674+
localMeta, err = c.getTargetFileMeta(normalizedName)
675+
if err != nil {
676+
return err
677+
}
660678
}
661679

662680
// get the data from remote storage

client/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ func (t *testDestination) Delete() error {
804804
func (s *ClientSuite) TestDownloadUnknownTarget(c *C) {
805805
client := s.updatedClient(c)
806806
var dest testDestination
807-
c.Assert(client.Download("nonexistent", &dest), Equals, ErrUnknownTarget{"nonexistent"})
807+
c.Assert(client.Download("nonexistent", &dest), Equals, ErrUnknownTarget{Name: "nonexistent", SnapshotVersion: 1})
808808
c.Assert(dest.deleted, Equals, true)
809809
}
810810

client/delegations.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package client
2+
3+
import (
4+
"github.com/theupdateframework/go-tuf/data"
5+
"github.com/theupdateframework/go-tuf/verify"
6+
)
7+
8+
// getTargetFileMeta searches for a verified TargetFileMeta matching a file name
9+
// Requires a local snapshot to be loaded and is locked to the snapshot versions.
10+
// Searches through delegated targets following TUF spec 1.0.19 section 5.6.
11+
func (c *Client) getTargetFileMeta(file string) (data.TargetFileMeta, error) {
12+
snapshot, err := c.loadLocalSnapshot()
13+
if err != nil {
14+
return data.TargetFileMeta{}, err
15+
}
16+
verifiers := map[string]verify.DelegationsVerifier{"root": c.db}
17+
18+
// delegationsIterator covers 5.6.7
19+
// - pre-order depth-first search starting with the top targets
20+
// - filter delegations with paths or path_hash_prefixes matching searched file
21+
// - 5.6.7.1 cycles protection
22+
// - 5.6.7.2 terminations
23+
delegations := newDelegationsIterator(c.rootTargetDelegation(), "root", file)
24+
for i := 0; i < c.MaxDelegations; i++ {
25+
d, ok := delegations.next()
26+
if !ok {
27+
return data.TargetFileMeta{}, ErrUnknownTarget{file, snapshot.Version}
28+
}
29+
verifier := verifiers[d.parent]
30+
// covers 5.6.{1,2,3,4,5,6}
31+
target, err := c.loadDelegatedTargets(snapshot, d.child.Name, verifier)
32+
if err != nil {
33+
return data.TargetFileMeta{}, err
34+
}
35+
// stop when the searched TargetFileMeta is found
36+
if m, ok := target.Targets[file]; ok {
37+
return m, nil
38+
}
39+
if target.Delegations != nil {
40+
delegations.add(target.Delegations.Roles, d.child.Name)
41+
targetVerifier, err := verify.NewDelegationsVerifier(target.Delegations)
42+
if err != nil {
43+
return data.TargetFileMeta{}, err
44+
}
45+
verifiers[d.child.Name] = targetVerifier
46+
}
47+
}
48+
return data.TargetFileMeta{}, ErrMaxDelegations{
49+
File: file,
50+
MaxDelegations: c.MaxDelegations,
51+
SnapshotVersion: snapshot.Version,
52+
}
53+
}
54+
55+
func (c *Client) loadLocalSnapshot() (*data.Snapshot, error) {
56+
if err := c.getLocalMeta(); err != nil {
57+
return nil, err
58+
}
59+
rawS, ok := c.localMeta["snapshot.json"]
60+
if !ok {
61+
return nil, ErrNoLocalSnapshot
62+
}
63+
snapshot := &data.Snapshot{}
64+
if err := c.db.Unmarshal(rawS, snapshot, "snapshot", c.snapshotVer); err != nil {
65+
return nil, ErrDecodeFailed{"snapshot.json", err}
66+
}
67+
return snapshot, nil
68+
}
69+
70+
// loadDelegatedTargets downloads, decodes, verifies and stores delegated targets
71+
func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, verifier verify.DelegationsVerifier) (*data.Targets, error) {
72+
var err error
73+
fileName := role + ".json"
74+
fileMeta, ok := snapshot.Meta[fileName]
75+
if !ok {
76+
return nil, ErrRoleNotInSnapshot{role, snapshot.Version}
77+
}
78+
// 5.6.1 download target if not in the local store
79+
// 5.6.2 check against snapshot hash
80+
raw, alreadyStored := c.localMetaFromSnapshot(fileName, fileMeta)
81+
if !alreadyStored {
82+
raw, err = c.downloadMetaFromSnapshot(fileName, fileMeta)
83+
if err != nil {
84+
return nil, err
85+
}
86+
}
87+
target := &data.Targets{}
88+
// 5.6.3 verify signature with parent public keys
89+
// 5.6.5 verify that the targets is not expired
90+
if err := verifier.Unmarshal(raw, target, role, fileMeta.Version); err != nil {
91+
return nil, ErrDecodeFailed{fileName, err}
92+
}
93+
// 5.6.4 check against snapshot version
94+
if target.Version != fileMeta.Version {
95+
return nil, ErrTargetsSnapshotVersionMismatch{
96+
Role: fileName,
97+
DownloadedTargetsVersion: fileMeta.Version,
98+
TargetsSnapshotVersion: target.Version,
99+
SnapshotVersion: snapshot.Version,
100+
}
101+
}
102+
// 5.6.6 persist
103+
if !alreadyStored {
104+
if err := c.local.SetMeta(fileName, raw); err != nil {
105+
return nil, err
106+
}
107+
}
108+
return target, nil
109+
}
110+
111+
func (c *Client) rootTargetDelegation() data.DelegatedRole {
112+
role := "targets"
113+
r := c.db.GetRole(role)
114+
if r == nil {
115+
return data.DelegatedRole{}
116+
}
117+
keyIDs := make([]string, 0, len(r.KeyIDs))
118+
for id, _ := range r.KeyIDs {
119+
keyIDs = append(keyIDs, id)
120+
}
121+
return data.DelegatedRole{
122+
Name: role,
123+
KeyIDs: keyIDs,
124+
Threshold: r.Threshold,
125+
Paths: []string{"*"},
126+
}
127+
}
128+
129+
type delegation struct {
130+
parent string
131+
child data.DelegatedRole
132+
}
133+
134+
type delegationID struct {
135+
parent string
136+
child string
137+
}
138+
139+
type delegationsIterator struct {
140+
stack []delegation
141+
file string
142+
visited map[delegationID]struct{}
143+
}
144+
145+
func newDelegationsIterator(role data.DelegatedRole, parent string, file string) *delegationsIterator {
146+
i := &delegationsIterator{
147+
file: file,
148+
stack: make([]delegation, 0, 1),
149+
visited: make(map[delegationID]struct{}),
150+
}
151+
i.add([]data.DelegatedRole{role}, parent)
152+
return i
153+
}
154+
155+
func (d *delegationsIterator) next() (delegation, bool) {
156+
if len(d.stack) == 0 {
157+
return delegation{}, false
158+
}
159+
delegation := d.stack[len(d.stack)-1]
160+
d.stack = d.stack[:len(d.stack)-1]
161+
162+
// 5.6.7.1 cycles protection
163+
id := delegationID{delegation.parent, delegation.child.Name}
164+
if _, ok := d.visited[id]; ok {
165+
return d.next()
166+
}
167+
d.visited[id] = struct{}{}
168+
169+
// 5.6.7.2 trim delegations to visit, only the current role and its delegations
170+
// will be considered
171+
// https://github.com/theupdateframework/specification/issues/168
172+
if delegation.child.Terminating {
173+
d.stack = d.stack[0:0]
174+
}
175+
return delegation, true
176+
}
177+
178+
func (d *delegationsIterator) add(roles []data.DelegatedRole, parent string) {
179+
for i := len(roles) - 1; i >= 0; i-- {
180+
r := roles[i]
181+
if r.MatchesPath(d.file) {
182+
d.stack = append(d.stack, delegation{parent, r})
183+
}
184+
}
185+
}

0 commit comments

Comments
 (0)