Skip to content

Commit ed6788e

Browse files
authored
Merge pull request from GHSA-66x3-6cw3-v5gj
* Remove obsolete snapshot error check Signed-off-by: Radoslav Dimitrov <[email protected]> * Add a dedicated error for missing target metadata file Signed-off-by: Radoslav Dimitrov <[email protected]> * Fix protection against metadata rollback attacks The go-tuf client now loads any previously trusted metadata before proceeding with the update process. This is mandatory for the protection against rollback attacks. It also fixes the detailed order of operations necessary to implement such protection. Signed-off-by: Radoslav Dimitrov <[email protected]> * Don't abort the update process if loading trusted metadata fails Signed-off-by: Radoslav Dimitrov <[email protected]> * Update getLocalMeta so it tries loading every verified metadata file If some of the metadata files fail to load, getLocalMeta will proceed with trying to load the rest, but still return an error at the end, if such occurred. Signed-off-by: Radoslav Dimitrov <[email protected]> * Revert the preliminary targets.json download check Signed-off-by: Radoslav Dimitrov <[email protected]> * Use current instead of old when addressing metadata Signed-off-by: Radoslav Dimitrov <[email protected]> * Timestamp metadata do not require hashes and lenght being present Signed-off-by: Radoslav Dimitrov <[email protected]> * fix: reload local meta based on the latest root Clear the in-memory copy of the local metadata. The goal is to reload and take into account only the metadata files that are verified by the latest root. Otherwise, their content should be ignored. Signed-off-by: Radoslav Dimitrov <[email protected]> * fix: update client unit tests for cases where metadata is now invalidated Signed-off-by: Radoslav Dimitrov <[email protected]> * chore: clarify the case where targets rollback verification will be skipped Signed-off-by: Radoslav Dimitrov <[email protected]> * chore: update getLocalMeta() description Signed-off-by: Radoslav Dimitrov <[email protected]> * chore: simplify getLocalMeta() so it wraps the inner error upon failure Signed-off-by: Radoslav Dimitrov <[email protected]> * chore: remove unused ErrLoadLocalFailed error type Signed-off-by: Radoslav Dimitrov <[email protected]> * chore: improve code layout for decodeSnapshot() Signed-off-by: Radoslav Dimitrov <[email protected]>
1 parent 90f34f0 commit ed6788e

File tree

8 files changed

+126
-49
lines changed

8 files changed

+126
-49
lines changed

client/client.go

+97-20
Original file line numberDiff line numberDiff line change
@@ -177,33 +177,38 @@ func (c *Client) Update() (data.TargetFiles, error) {
177177
return nil, err
178178
}
179179

180-
// Get timestamp.json, extract snapshot.json file meta and save the
181-
// timestamp.json locally
180+
// Load trusted metadata files, if any, and verify them against the latest root
181+
c.getLocalMeta()
182+
183+
// 5.4.1 - Download the timestamp metadata
182184
timestampJSON, err := c.downloadMetaUnsafe("timestamp.json", defaultTimestampDownloadLimit)
183185
if err != nil {
184186
return nil, err
185187
}
188+
// 5.4.(2,3 and 4) - Verify timestamp against various attacks
189+
// Returns the extracted snapshot metadata
186190
snapshotMeta, err := c.decodeTimestamp(timestampJSON)
187191
if err != nil {
188192
return nil, err
189193
}
194+
// 5.4.5 - Persist the timestamp metadata
190195
if err := c.local.SetMeta("timestamp.json", timestampJSON); err != nil {
191196
return nil, err
192197
}
193198

194-
// Get snapshot.json, then extract file metas.
195-
// root.json meta should not be stored in the snapshot, if it is,
196-
// the root will be checked, re-downloaded
199+
// 5.5.1 - Download snapshot metadata
200+
// 5.5.2 and 5.5.4 - Check against timestamp role's snapshot hash and version
197201
snapshotJSON, err := c.downloadMetaFromTimestamp("snapshot.json", snapshotMeta)
198202
if err != nil {
199203
return nil, err
200204
}
205+
// 5.5.(3,5 and 6) - Verify snapshot against various attacks
206+
// Returns the extracted metadata files
201207
snapshotMetas, err := c.decodeSnapshot(snapshotJSON)
202208
if err != nil {
203209
return nil, err
204210
}
205-
206-
// Save the snapshot.json
211+
// 5.5.7 - Persist snapshot metadata
207212
if err := c.local.SetMeta("snapshot.json", snapshotJSON); err != nil {
208213
return nil, err
209214
}
@@ -213,14 +218,18 @@ func (c *Client) Update() (data.TargetFiles, error) {
213218
var updatedTargets data.TargetFiles
214219
targetsMeta := snapshotMetas["targets.json"]
215220
if !c.hasMetaFromSnapshot("targets.json", targetsMeta) {
221+
// 5.6.1 - Download the top-level targets metadata file
222+
// 5.6.2 and 5.6.4 - Check against snapshot role's targets hash and version
216223
targetsJSON, err := c.downloadMetaFromSnapshot("targets.json", targetsMeta)
217224
if err != nil {
218225
return nil, err
219226
}
227+
// 5.6.(3 and 5) - Verify signatures and check against freeze attack
220228
updatedTargets, err = c.decodeTargets(targetsJSON)
221229
if err != nil {
222230
return nil, err
223231
}
232+
// 5.6.6 - Persist targets metadata
224233
if err := c.local.SetMeta("targets.json", targetsJSON); err != nil {
225234
return nil, err
226235
}
@@ -393,44 +402,69 @@ func (c *Client) UpdateRoots() error {
393402
// getLocalMeta decodes and verifies metadata from local storage.
394403
// The verification of local files is purely for consistency, if an attacker
395404
// has compromised the local storage, there is no guarantee it can be trusted.
405+
// Before trying to load the metadata files, it clears the in-memory copy of the local metadata.
406+
// This is to insure that all of the loaded metadata files at the end are indeed verified by the latest root.
407+
// If some of the metadata files fail to load it will proceed with trying to load the rest,
408+
// but still return an error at the end, if such occurred. Otherwise returns nil.
396409
func (c *Client) getLocalMeta() error {
410+
var retErr error
411+
loadFailed := false
412+
// Clear the in-memory copy of the local metadata. The goal is to reload and take into account
413+
// only the metadata files that are verified by the latest root. Otherwise, their content should
414+
// be ignored.
415+
c.localMeta = make(map[string]json.RawMessage)
416+
417+
// Load the latest root meta
397418
if err := c.loadAndVerifyLocalRootMeta( /*ignoreExpiredCheck=*/ false); err != nil {
398419
return err
399420
}
400421

422+
// Load into memory the existing meta, if any, from the local storage
401423
meta, err := c.local.GetMeta()
402424
if err != nil {
403425
return nil
404426
}
405427

428+
// Verify the top-level metadata (timestamp, snapshot and targets) against the latest root and load it, if okay
406429
if timestampJSON, ok := meta["timestamp.json"]; ok {
407430
timestamp := &data.Timestamp{}
408431
if err := c.db.UnmarshalTrusted(timestampJSON, timestamp, "timestamp"); err != nil {
409-
return err
432+
loadFailed = true
433+
retErr = err
434+
} else {
435+
c.localMeta["timestamp.json"] = meta["timestamp.json"]
436+
c.timestampVer = timestamp.Version
410437
}
411-
c.timestampVer = timestamp.Version
412438
}
413439

414440
if snapshotJSON, ok := meta["snapshot.json"]; ok {
415441
snapshot := &data.Snapshot{}
416442
if err := c.db.UnmarshalTrusted(snapshotJSON, snapshot, "snapshot"); err != nil {
417-
return err
443+
loadFailed = true
444+
retErr = err
445+
} else {
446+
c.localMeta["snapshot.json"] = meta["snapshot.json"]
447+
c.snapshotVer = snapshot.Version
418448
}
419-
c.snapshotVer = snapshot.Version
420449
}
421450

422451
if targetsJSON, ok := meta["targets.json"]; ok {
423452
targets := &data.Targets{}
424453
if err := c.db.UnmarshalTrusted(targetsJSON, targets, "targets"); err != nil {
425-
return err
454+
loadFailed = true
455+
retErr = err
456+
} else {
457+
c.localMeta["targets.json"] = meta["targets.json"]
458+
c.targetsVer = targets.Version
459+
// FIXME(TUF-0.9) temporarily support files with leading path separators.
460+
// c.targets = targets.Targets
461+
c.loadTargets(targets.Targets)
426462
}
427-
c.targetsVer = targets.Version
428-
// FIXME(TUF-0.9) temporarily support files with leading path separators.
429-
// c.targets = targets.Targets
430-
c.loadTargets(targets.Targets)
431463
}
432-
433-
c.localMeta = meta
464+
if loadFailed {
465+
// If any of the metadata failed to be verified, return the reason for that failure
466+
return retErr
467+
}
434468
return nil
435469
}
436470

@@ -660,6 +694,7 @@ func (c *Client) downloadMetaFromSnapshot(name string, m data.SnapshotFileMeta)
660694
if err != nil {
661695
return nil, err
662696
}
697+
// 5.6.2 and 5.6.4 - Check against snapshot role's targets hash and version
663698
if err := util.SnapshotFileMetaEqual(meta, m); err != nil {
664699
return nil, ErrDownloadFailed{name, err}
665700
}
@@ -676,6 +711,7 @@ func (c *Client) downloadMetaFromTimestamp(name string, m data.TimestampFileMeta
676711
if err != nil {
677712
return nil, err
678713
}
714+
// 5.5.2 and 5.5.4 - Check against timestamp role's snapshot hash and version
679715
if err := util.TimestampFileMetaEqual(meta, m); err != nil {
680716
return nil, ErrDownloadFailed{name, err}
681717
}
@@ -697,20 +733,53 @@ func (c *Client) decodeRoot(b json.RawMessage) error {
697733
// root and targets file meta.
698734
func (c *Client) decodeSnapshot(b json.RawMessage) (data.SnapshotFiles, error) {
699735
snapshot := &data.Snapshot{}
736+
// 5.5.(3 and 6) - Verify it's signed correctly and it's not expired
700737
if err := c.db.Unmarshal(b, snapshot, "snapshot", c.snapshotVer); err != nil {
701738
return data.SnapshotFiles{}, ErrDecodeFailed{"snapshot.json", err}
702739
}
703-
c.snapshotVer = snapshot.Version
740+
// 5.5.5 - Check for top-level targets rollback attack
741+
// Verify explicitly that current targets meta version is less than or equal to the new one
742+
if snapshot.Meta["targets.json"].Version < c.targetsVer {
743+
return data.SnapshotFiles{}, verify.ErrLowVersion{Actual: snapshot.Meta["targets.json"].Version, Current: c.targetsVer}
744+
}
745+
746+
// 5.5.5 - Get the local/trusted snapshot metadata, if any, and check all target metafiles against rollback attack
747+
// In case the local snapshot metadata was not verified by the keys in the latest root during getLocalMeta(),
748+
// snapshot.json won't be present in c.localMeta and thus this check will not be processed.
749+
if snapshotJSON, ok := c.localMeta["snapshot.json"]; ok {
750+
currentSnapshot := &data.Snapshot{}
751+
if err := c.db.UnmarshalTrusted(snapshotJSON, currentSnapshot, "snapshot"); err != nil {
752+
return data.SnapshotFiles{}, err
753+
}
754+
// 5.5.5 - Check for rollback attacks in both top-level and delegated targets roles (note that the Meta object includes both)
755+
for path, local := range currentSnapshot.Meta {
756+
if newMeta, ok := snapshot.Meta[path]; ok {
757+
// 5.5.5 - Check for rollback attack
758+
if newMeta.Version < local.Version {
759+
return data.SnapshotFiles{}, verify.ErrLowVersion{Actual: newMeta.Version, Current: local.Version}
760+
}
761+
} else {
762+
// 5.5.5 - Abort the update if a target file has been removed from the new snapshot file
763+
return data.SnapshotFiles{}, verify.ErrMissingTargetFile
764+
}
765+
}
766+
}
767+
// At this point we can trust the new snapshot, the top-level targets, and any delegated targets versions it refers to
768+
// so we can update the client's trusted versions and proceed with persisting the new snapshot metadata
769+
// c.snapshotVer was already set when we verified the timestamp metadata
770+
c.targetsVer = snapshot.Meta["targets.json"].Version
704771
return snapshot.Meta, nil
705772
}
706773

707774
// decodeTargets decodes and verifies targets metadata, sets c.targets and
708775
// returns updated targets.
709776
func (c *Client) decodeTargets(b json.RawMessage) (data.TargetFiles, error) {
710777
targets := &data.Targets{}
778+
// 5.6.(3 and 5) - Verify signatures and check against freeze attack
711779
if err := c.db.Unmarshal(b, targets, "targets", c.targetsVer); err != nil {
712780
return nil, ErrDecodeFailed{"targets.json", err}
713781
}
782+
// Generate a list with the updated targets
714783
updatedTargets := make(data.TargetFiles)
715784
for path, meta := range targets.Targets {
716785
if local, ok := c.targets[path]; ok {
@@ -720,7 +789,7 @@ func (c *Client) decodeTargets(b json.RawMessage) (data.TargetFiles, error) {
720789
}
721790
updatedTargets[path] = meta
722791
}
723-
c.targetsVer = targets.Version
792+
// c.targetsVer was already updated when we verified the snapshot metadata
724793
// FIXME(TUF-0.9) temporarily support files with leading path separators.
725794
// c.targets = targets.Targets
726795
c.loadTargets(targets.Targets)
@@ -734,7 +803,15 @@ func (c *Client) decodeTimestamp(b json.RawMessage) (data.TimestampFileMeta, err
734803
if err := c.db.Unmarshal(b, timestamp, "timestamp", c.timestampVer); err != nil {
735804
return data.TimestampFileMeta{}, ErrDecodeFailed{"timestamp.json", err}
736805
}
806+
// 5.4.3.2 - Check for snapshot rollback attack
807+
// Verify that the current snapshot meta version is less than or equal to the new one
808+
if timestamp.Meta["snapshot.json"].Version < c.snapshotVer {
809+
return data.TimestampFileMeta{}, verify.ErrLowVersion{Actual: timestamp.Meta["snapshot.json"].Version, Current: c.snapshotVer}
810+
}
811+
// At this point we can trust the new timestamp and the snaphost version it refers to
812+
// so we can update the client's trusted versions and proceed with persisting the new timestamp
737813
c.timestampVer = timestamp.Version
814+
c.snapshotVer = timestamp.Meta["snapshot.json"].Version
738815
return timestamp.Meta["snapshot.json"], nil
739816
}
740817

client/errors.go

-13
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,6 @@ func (e ErrWrongSize) Error() string {
7070
return fmt.Sprintf("tuf: unexpected file size: %s (expected %d bytes, got %d bytes)", e.File, e.Expected, e.Actual)
7171
}
7272

73-
type ErrLatestSnapshot struct {
74-
Version int64
75-
}
76-
77-
func (e ErrLatestSnapshot) Error() string {
78-
return fmt.Sprintf("tuf: the local snapshot version (%d) is the latest", e.Version)
79-
}
80-
81-
func IsLatestSnapshot(err error) bool {
82-
_, ok := err.(ErrLatestSnapshot)
83-
return ok
84-
}
85-
8673
type ErrUnknownTarget struct {
8774
Name string
8875
SnapshotVersion int64

client/interop_test.go

+11-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"net/http"
1010
"os"
1111
"path/filepath"
12-
"strconv"
1312

1413
"github.com/theupdateframework/go-tuf/util"
1514
. "gopkg.in/check.v1"
@@ -159,9 +158,17 @@ func (t *testCase) runStep(c *C, stepName string) {
159158
// check update returns the correct updated targets
160159
files, err := client.Update()
161160
c.Assert(err, IsNil)
162-
l, _ := strconv.Atoi(stepName)
163-
c.Assert(files, HasLen, l+1)
164-
161+
if stepName != "2" {
162+
// The rest of the test cases add one target file at a time for each cycle, so this is why we expect that
163+
// the number of updated targets returned by Update() should equals to 1
164+
c.Assert(files, HasLen, 1)
165+
} else {
166+
// The following test case (#2) verifies that when a targets key has been rotated in the latest 3.root.json,
167+
// the local targets.json meta is indeed ignored since it's signed with a key that has been now changed.
168+
// The reason we check for 3 here is that the updated targets corresponds to all target files listed in the
169+
// targets.json for test case #2
170+
c.Assert(files, HasLen, 3)
171+
}
165172
targetName := stepName
166173
t.targets[targetName] = []byte(targetName)
167174

cmd/tuf-client/get.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (t *tmpFile) Delete() error {
3131
}
3232

3333
func cmdGet(args *docopt.Args, client *tuf.Client) error {
34-
if _, err := client.Update(); err != nil && !tuf.IsLatestSnapshot(err) {
34+
if _, err := client.Update(); err != nil {
3535
return err
3636
}
3737
target := util.NormalizeTarget(args.String["<target>"])

cmd/tuf-client/list.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ List available target files.
2222
}
2323

2424
func cmdList(args *docopt.Args, client *tuf.Client) error {
25-
if _, err := client.Update(); err != nil && !tuf.IsLatestSnapshot(err) {
25+
if _, err := client.Update(); err != nil {
2626
return err
2727
}
2828
targets, err := client.Targets()

util/util.go

+13-8
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,13 @@ func SnapshotFileMetaEqual(actual data.SnapshotFileMeta, expected data.SnapshotF
118118
if expected.Length != 0 && actual.Length != expected.Length {
119119
return ErrWrongLength{expected.Length, actual.Length}
120120
}
121-
121+
// 5.6.2 - Check against snapshot role's targets hash
122122
if len(expected.Hashes) != 0 {
123123
if err := hashEqual(actual.Hashes, expected.Hashes); err != nil {
124124
return err
125125
}
126126
}
127-
127+
// 5.6.4 - Check against snapshot role's snapshot version
128128
if err := versionEqual(actual.Version, expected.Version); err != nil {
129129
return err
130130
}
@@ -137,13 +137,18 @@ func TargetFileMetaEqual(actual data.TargetFileMeta, expected data.TargetFileMet
137137
}
138138

139139
func TimestampFileMetaEqual(actual data.TimestampFileMeta, expected data.TimestampFileMeta) error {
140-
// As opposed to snapshots, the length and hashes are still required in
141-
// TUF-1.0. See:
142-
// https://github.com/theupdateframework/specification/issues/38
143-
if err := FileMetaEqual(actual.FileMeta, expected.FileMeta); err != nil {
144-
return err
140+
// TUF no longer considers the length and hashes to be a required
141+
// member of Timestamp.
142+
if expected.Length != 0 && actual.Length != expected.Length {
143+
return ErrWrongLength{expected.Length, actual.Length}
145144
}
146-
145+
// 5.5.2 - Check against timestamp role's snapshot hash
146+
if len(expected.Hashes) != 0 {
147+
if err := hashEqual(actual.Hashes, expected.Hashes); err != nil {
148+
return err
149+
}
150+
}
151+
// 5.5.4 - Check against timestamp role's snapshot version
147152
if err := versionEqual(actual.Version, expected.Version); err != nil {
148153
return err
149154
}

verify/errors.go

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var (
1818
ErrInvalidDelegatedRole = errors.New("tuf: invalid delegated role")
1919
ErrInvalidKeyID = errors.New("tuf: invalid key id")
2020
ErrInvalidThreshold = errors.New("tuf: invalid role threshold")
21+
ErrMissingTargetFile = errors.New("tuf: missing previously listed targets metadata file")
2122
)
2223

2324
type ErrWrongID struct{}

verify/verify.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (db *DB) VerifyIgnoreExpiredCheck(s *data.Signed, role string, minVersion i
4747
}
4848

4949
func (db *DB) Verify(s *data.Signed, role string, minVersion int64) error {
50-
50+
// Verify signatures and versions
5151
err := db.VerifyIgnoreExpiredCheck(s, role, minVersion)
5252

5353
if err != nil {
@@ -58,7 +58,7 @@ func (db *DB) Verify(s *data.Signed, role string, minVersion int64) error {
5858
if err := json.Unmarshal(s.Signed, sm); err != nil {
5959
return err
6060
}
61-
61+
// Verify expiration
6262
if IsExpired(sm.Expires) {
6363
return ErrExpired{sm.Expires}
6464
}

0 commit comments

Comments
 (0)