Skip to content

Commit 526f227

Browse files
Rotate roots per spec (#143)
* update roots * removing some debugging comments * removing duplicate code for getLocalRootMeta by calling it from getLocalMeta * fix based on the reviews. * enable an arbitrary root verify another root (use case: n verify n+1) without the need for store them permanently. * check non root metadata, refactor test, address comments * updated according to the comments * remove persistent metadata is the keys have changed. * removing the unused ErrWrongRootVersion * add DeleteMeta to the LocalStore interface and implemenet in MemoryLocalStore and FileLocalStore subtypes. * delete (instead of setting to an empty raw message) the top-level metadata when their key has changed. * add test fixtures for fast forward attack recovery. * test for fast forward attack recovery * addressed several comments. * addressed more comments. Set the rootVersion in loadAndVerifyLocalRootMeta. Fixed a buggy test. * Fixed a buggy test. * fix comment typos * fix race condition related to the expired check. * fix race condition related to the expired check. * kill unmarshalIgnoreExpired. * add test for root update for client version above 1. * add test for root update for client version greater than 1. * update the VerifyIgnoreExpiredCheck method signature and add test for it. * Avoid mocking IsExpired in the tests. Instead update test fixtured to have concerete timestamps (either expired or long exiring one) * remove commented code * update fixtures and clarify test comments. * updating the comments based on the feedbacks. * update roots * removing some debugging comments * removing duplicate code for getLocalRootMeta by calling it from getLocalMeta * fix based on the reviews. * enable an arbitrary root verify another root (use case: n verify n+1) without the need for store them permanently. * check non root metadata, refactor test, address comments * updated according to the comments * remove persistent metadata is the keys have changed. * removing the unused ErrWrongRootVersion * delete (instead of setting to an empty raw message) the top-level metadata when their key has changed. * add test fixtures for fast forward attack recovery. * test for fast forward attack recovery * addressed several comments. * addressed more comments. Set the rootVersion in loadAndVerifyLocalRootMeta. Fixed a buggy test. * Fixed a buggy test. * fix comment typos * Update client/client_test.go Co-authored-by: Trishank Karthik Kuppusamy <[email protected]> * Update client/client_test.go Co-authored-by: Trishank Karthik Kuppusamy <[email protected]> * fix race condition related to the expired check. * fix race condition related to the expired check. * kill unmarshalIgnoreExpired. * add test for root update for client version above 1. * add test for root update for client version greater than 1. * update the VerifyIgnoreExpiredCheck method signature and add test for it. * Avoid mocking IsExpired in the tests. Instead update test fixtured to have concerete timestamps (either expired or long exiring one) * remove commented code * update fixtures and clarify test comments. * updating the comments based on the feedbacks. * rebase and update test cases to long expiration (10 years from now), by default. * add test cases for (1) when there is no local root, (2) there is a local root but no other top-level metadata * remove the 'previous' of test folders Co-authored-by: Trishank Karthik Kuppusamy <[email protected]>
1 parent f38e930 commit 526f227

File tree

831 files changed

+42512
-137
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

831 files changed

+42512
-137
lines changed

client/client.go

+290-115
Large diffs are not rendered by default.

client/client_test.go

+202-7
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ package client
33
import (
44
"bytes"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"io/ioutil"
10+
"net"
11+
"net/http"
912
"os"
1013
"path/filepath"
1114
"testing"
1215
"time"
1316

17+
"github.com/stretchr/testify/assert"
1418
cjson "github.com/tent/canonical-json-go"
1519
tuf "github.com/theupdateframework/go-tuf"
1620
"github.com/theupdateframework/go-tuf/data"
@@ -298,7 +302,7 @@ func (s *ClientSuite) TestNoChangeUpdate(c *C) {
298302
_, err := client.Update()
299303
c.Assert(err, IsNil)
300304
_, err = client.Update()
301-
c.Assert(IsLatestSnapshot(err), Equals, true)
305+
c.Assert(err, IsNil)
302306
}
303307

304308
func (s *ClientSuite) TestNewTimestamp(c *C) {
@@ -308,7 +312,7 @@ func (s *ClientSuite) TestNewTimestamp(c *C) {
308312
c.Assert(s.repo.Timestamp(), IsNil)
309313
s.syncRemote(c)
310314
_, err := client.Update()
311-
c.Assert(IsLatestSnapshot(err), Equals, true)
315+
c.Assert(err, IsNil)
312316
c.Assert(client.timestampVer > version, Equals, true)
313317
}
314318

@@ -360,6 +364,187 @@ func (s *ClientSuite) TestNewRoot(c *C) {
360364
}
361365
}
362366

367+
// startTUFRepoServer starts a HTTP server to serve a TUF Repo.
368+
func startTUFRepoServer(baseDir string, relPath string) (net.Listener, error) {
369+
serverDir := filepath.Join(baseDir, relPath)
370+
l, err := net.Listen("tcp", "127.0.0.1:0")
371+
go http.Serve(l, http.FileServer(http.Dir(serverDir)))
372+
return l, err
373+
}
374+
375+
// newClientWithMeta creates new client and sets the root metadata for it.
376+
func newClientWithMeta(baseDir string, relPath string, serverAddr string) (*Client, error) {
377+
initialStateDir := filepath.Join(baseDir, relPath)
378+
opts := &HTTPRemoteOptions{
379+
MetadataPath: "metadata",
380+
TargetsPath: "targets",
381+
}
382+
383+
remote, err := HTTPRemoteStore(fmt.Sprintf("http://%s/", serverAddr), opts, nil)
384+
if err != nil {
385+
return nil, err
386+
}
387+
c := NewClient(MemoryLocalStore(), remote)
388+
for _, m := range []string{"root.json", "snapshot.json", "timestamp.json", "targets.json"} {
389+
if _, err := os.Stat(initialStateDir + "/" + m); err == nil {
390+
metadataJSON, err := ioutil.ReadFile(initialStateDir + "/" + m)
391+
if err != nil {
392+
return nil, err
393+
}
394+
c.local.SetMeta(m, metadataJSON)
395+
}
396+
}
397+
return c, nil
398+
}
399+
400+
func initRootTest(c *C, baseDir string) (*Client, func() error) {
401+
l, err := startTUFRepoServer(baseDir, "server")
402+
c.Assert(err, IsNil)
403+
tufClient, err := newClientWithMeta(baseDir, "client/metadata/current", l.Addr().String())
404+
c.Assert(err, IsNil)
405+
return tufClient, l.Close
406+
}
407+
408+
func (s *ClientSuite) TestUpdateRoots(c *C) {
409+
var tests = []struct {
410+
fixturePath string
411+
expectedError error
412+
expectedVersions map[string]int
413+
}{
414+
// Succeeds when there is no root update.
415+
{"testdata/Published1Time", nil, map[string]int{"root": 1, "timestamp": 1, "snapshot": 1, "targets": 1}},
416+
// Succeeds when client only has root.json
417+
{"testdata/Published1Time_client_root_only", nil, map[string]int{"root": 1, "timestamp": 1, "snapshot": 1, "targets": 1}},
418+
// Succeeds updating root from version 1 to version 2.
419+
{"testdata/Published2Times_keyrotated", nil, map[string]int{"root": 2, "timestamp": 1, "snapshot": 1, "targets": 1}},
420+
// Succeeds updating root from version 1 to version 2 when the client's initial root version is expired.
421+
{"testdata/Published2Times_keyrotated_initialrootexpired", nil, map[string]int{"root": 2, "timestamp": 1, "snapshot": 1, "targets": 1}},
422+
// Succeeds updating root from version 1 to version 3 when versions 1 and 2 are expired.
423+
{"testdata/Published3Times_keyrotated_initialrootsexpired", nil, map[string]int{"root": 3, "timestamp": 1, "snapshot": 1, "targets": 1}},
424+
// Succeeds updating root from version 2 to version 3.
425+
{"testdata/Published3Times_keyrotated_initialrootsexpired_clientversionis2", nil, map[string]int{"root": 3, "timestamp": 1, "snapshot": 1, "targets": 1}},
426+
// Fails updating root from version 1 to version 3 when versions 1 and 3 are expired but version 2 is not expired.
427+
{"testdata/Published3Times_keyrotated_latestrootexpired", ErrDecodeFailed{File: "root.json", Err: verify.ErrExpired{}}, map[string]int{"root": 2, "timestamp": 1, "snapshot": 1, "targets": 1}},
428+
// Fails updating root from version 1 to version 2 when old root 1 did not sign off on it (nth root didn't sign off n+1).
429+
{"testdata/Published2Times_keyrotated_invalidOldRootSignature", errors.New("tuf: signature verification failed"), map[string]int{}},
430+
// Fails updating root from version 1 to version 2 when the new root 2 did not sign itself (n+1th root didn't sign off n+1)
431+
{"testdata/Published2Times_keyrotated_invalidNewRootSignature", errors.New("tuf: signature verification failed"), map[string]int{}},
432+
// Fails updating root to 2.root.json when the value of the version field inside it is 1 (rollback attack prevention).
433+
{"testdata/Published1Time_backwardRootVersion", verify.ErrWrongVersion(verify.ErrWrongVersion{Given: 1, Expected: 2}), map[string]int{}},
434+
// Fails updating root to 2.root.json when the value of the version field inside it is 3 (rollforward attack prevention).
435+
{"testdata/Published3Times_keyrotated_forwardRootVersion", verify.ErrWrongVersion(verify.ErrWrongVersion{Given: 3, Expected: 2}), map[string]int{}},
436+
// Fails updating when there is no local trusted root.
437+
{"testdata/Published1Time_client_no_root", errors.New("tuf: no root keys found in local meta store"), map[string]int{}},
438+
439+
// snapshot role key rotation increase the snapshot and timestamp.
440+
{"testdata/Published2Times_snapshot_keyrotated", nil, map[string]int{"root": 2, "timestamp": 2, "snapshot": 2, "targets": 1}},
441+
// targets role key rotation increase the snapshot, timestamp, and targets.
442+
{"testdata/Published2Times_targets_keyrotated", nil, map[string]int{"root": 2, "timestamp": 2, "snapshot": 2, "targets": 2}},
443+
// timestamp role key rotation increase the timestamp.
444+
{"testdata/Published2Times_timestamp_keyrotated", nil, map[string]int{"root": 2, "timestamp": 2, "snapshot": 1, "targets": 1}},
445+
}
446+
447+
for _, test := range tests {
448+
tufClient, closer := initRootTest(c, test.fixturePath)
449+
_, err := tufClient.Update()
450+
if test.expectedError == nil {
451+
c.Assert(err, IsNil)
452+
// Check if the root.json is being saved in non-volatile storage.
453+
tufClient.getLocalMeta()
454+
versionMethods := map[string]int{"root": tufClient.rootVer,
455+
"timestamp": tufClient.timestampVer,
456+
"snapshot": tufClient.snapshotVer,
457+
"targets": tufClient.targetsVer}
458+
for m, v := range test.expectedVersions {
459+
assert.Equal(c, v, versionMethods[m])
460+
}
461+
} else {
462+
// For backward compatibility, the update root returns
463+
// ErrDecodeFailed that wraps the verify.ErrExpired.
464+
if _, ok := test.expectedError.(ErrDecodeFailed); ok {
465+
decodeErr, ok := err.(ErrDecodeFailed)
466+
c.Assert(ok, Equals, true)
467+
c.Assert(decodeErr.File, Equals, "root.json")
468+
_, ok = decodeErr.Err.(verify.ErrExpired)
469+
c.Assert(ok, Equals, true)
470+
} else {
471+
assert.Equal(c, test.expectedError, err)
472+
}
473+
}
474+
closer()
475+
}
476+
}
477+
478+
func (s *ClientSuite) TestFastForwardAttackRecovery(c *C) {
479+
var tests = []struct {
480+
fixturePath string
481+
expectMetaDeleted map[string]bool
482+
}{
483+
// Each of the following test cases each has a two sets of TUF metadata:
484+
// (1) client's initial, and (2) server's current.
485+
// The naming format is PublishedTwiceMultiKeysadd_X_revoke_Y_threshold_Z_ROLE
486+
// The client includes TUF metadata before key rotation for TUF ROLE with X keys.
487+
// The server includes updated TUF metadata after key rotation. The
488+
// rotation involves revoking Y keys from the initial keys.
489+
// For each test, the TUF client's will be initialized to the client files.
490+
// The test checks whether the client is able to update itself properly.
491+
492+
// Fast-forward recovery is not needed if less than threshold keys are revoked.
493+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_2_threshold_4_root",
494+
map[string]bool{"root.json": false, "timestamp.json": false, "snapshot.json": false, "targets.json": false}},
495+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_2_threshold_4_snapshot",
496+
map[string]bool{"root.json": false, "timestamp.json": false, "snapshot.json": false, "targets.json": false}},
497+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_2_threshold_4_targets",
498+
map[string]bool{"root.json": false, "timestamp.json": false, "snapshot.json": false, "targets.json": false}},
499+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_2_threshold_4_timestamp",
500+
map[string]bool{"root.json": false, "timestamp.json": false, "snapshot.json": false, "targets.json": false}},
501+
502+
// Fast-forward recovery not needed if root keys are revoked, even when the threshold number of root keys are revoked.
503+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_4_threshold_4_root",
504+
map[string]bool{"root.json": false, "timestamp.json": false, "snapshot.json": false, "targets.json": false}},
505+
506+
// Delete snapshot and timestamp metadata if a threshold number of snapshot keys are revoked.
507+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_4_threshold_4_snapshot",
508+
map[string]bool{"root.json": false, "timestamp.json": true, "snapshot.json": true, "targets.json": false}},
509+
// Delete targets and snapshot metadata if a threshold number of targets keys are revoked.
510+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_4_threshold_4_targets",
511+
map[string]bool{"root.json": false, "timestamp.json": false, "snapshot.json": true, "targets.json": true}},
512+
// Delete timestamp metadata if a threshold number of timestamp keys are revoked.
513+
{"testdata/PublishedTwiceMultiKeysadd_9_revoke_4_threshold_4_timestamp",
514+
map[string]bool{"root.json": false, "timestamp.json": true, "snapshot.json": false, "targets.json": false}},
515+
}
516+
for _, test := range tests {
517+
tufClient, closer := initRootTest(c, test.fixturePath)
518+
c.Assert(tufClient.updateRoots(), IsNil)
519+
m, err := tufClient.local.GetMeta()
520+
c.Assert(err, IsNil)
521+
for md, deleted := range test.expectMetaDeleted {
522+
if deleted {
523+
if _, ok := m[md]; ok {
524+
c.Fatalf("Metadata %s is not deleted!", md)
525+
}
526+
} else {
527+
if _, ok := m[md]; !ok {
528+
c.Fatalf("Metadata %s deleted!", md)
529+
}
530+
}
531+
}
532+
closer()
533+
}
534+
535+
}
536+
537+
func (s *ClientSuite) TestUpdateRace(c *C) {
538+
// Tests race condition for the client update. You need to run the test with -race flag:
539+
// go test -race
540+
for i := 0; i < 2; i++ {
541+
go func() {
542+
c := NewClient(MemoryLocalStore(), newFakeRemoteStore())
543+
c.Update()
544+
}()
545+
}
546+
}
547+
363548
func (s *ClientSuite) TestNewTargets(c *C) {
364549
client := s.newClient(c)
365550
files, err := client.Update()
@@ -552,25 +737,31 @@ func (s *ClientSuite) TestUpdateLocalRootExpired(c *C) {
552737

553738
// add soon to expire root.json to local storage
554739
s.genKeyExpired(c, "timestamp")
740+
c.Assert(s.repo.Snapshot(), IsNil)
555741
c.Assert(s.repo.Timestamp(), IsNil)
742+
c.Assert(s.repo.Commit(), IsNil)
556743
s.syncLocal(c)
557744

558745
// add far expiring root.json to remote storage
559746
s.genKey(c, "timestamp")
560747
s.addRemoteTarget(c, "bar.txt")
748+
c.Assert(s.repo.Snapshot(), IsNil)
749+
c.Assert(s.repo.Timestamp(), IsNil)
750+
c.Assert(s.repo.Commit(), IsNil)
561751
s.syncRemote(c)
562752

753+
const expectedRootVersion = 3
754+
563755
// check the update downloads the non expired remote root.json and
564756
// restarts itself, thus successfully updating
565757
s.withMetaExpired(func() {
566758
err := client.getLocalMeta()
567759
if _, ok := err.(verify.ErrExpired); !ok {
568760
c.Fatalf("expected err to have type signed.ErrExpired, got %T", err)
569761
}
570-
571-
client := NewClient(s.local, s.remote)
572762
_, err = client.Update()
573763
c.Assert(err, IsNil)
764+
c.Assert(client.rootVer, Equals, expectedRootVersion)
574765
})
575766
}
576767

@@ -587,6 +778,7 @@ func (s *ClientSuite) TestUpdateRemoteExpired(c *C) {
587778

588779
c.Assert(s.repo.SnapshotWithExpires(s.expiredTime), IsNil)
589780
c.Assert(s.repo.Timestamp(), IsNil)
781+
c.Assert(s.repo.Commit(), IsNil)
590782
s.syncRemote(c)
591783
s.withMetaExpired(func() {
592784
_, err := client.Update()
@@ -619,14 +811,18 @@ func (s *ClientSuite) TestUpdateLocalRootExpiredKeyChange(c *C) {
619811

620812
// add soon to expire root.json to local storage
621813
s.genKeyExpired(c, "timestamp")
814+
c.Assert(s.repo.Snapshot(), IsNil)
622815
c.Assert(s.repo.Timestamp(), IsNil)
816+
c.Assert(s.repo.Commit(), IsNil)
623817
s.syncLocal(c)
624818

625819
// replace all keys
626820
newKeyIDs := make(map[string][]string)
627821
for role, ids := range s.keyIDs {
628-
c.Assert(s.repo.RevokeKey(role, ids[0]), IsNil)
629-
newKeyIDs[role] = s.genKey(c, role)
822+
if role != "snapshot" && role != "timestamp" && role != "targets" {
823+
c.Assert(s.repo.RevokeKey(role, ids[0]), IsNil)
824+
newKeyIDs[role] = s.genKey(c, role)
825+
}
630826
}
631827

632828
// update metadata
@@ -704,7 +900,6 @@ func (s *ClientSuite) TestUpdateReplayAttack(c *C) {
704900
c.Assert(s.repo.Timestamp(), IsNil)
705901
s.syncRemote(c)
706902
_, err := client.Update()
707-
c.Assert(IsLatestSnapshot(err), Equals, true)
708903
c.Assert(client.timestampVer > version, Equals, true)
709904

710905
// replace remote timestamp.json with the old one

client/interop_test.go

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

1314
"github.com/theupdateframework/go-tuf/data"
1415
"github.com/theupdateframework/go-tuf/util"
@@ -130,14 +131,12 @@ func newTestCase(c *C, name string, consistentSnapshot bool, options *HTTPRemote
130131
func (t *testCase) run(c *C) {
131132
c.Logf("test case: %s consistent-snapshot: %t", t.name, t.consistentSnapshot)
132133

133-
init := true
134134
for _, stepName := range t.testSteps {
135-
t.runStep(c, stepName, init)
136-
init = false
135+
t.runStep(c, stepName)
137136
}
138137
}
139138

140-
func (t *testCase) runStep(c *C, stepName string, init bool) {
139+
func (t *testCase) runStep(c *C, stepName string) {
141140
c.Logf("step: %s", stepName)
142141

143142
addr, cleanup := startFileServer(c, t.testDir)
@@ -147,17 +146,15 @@ func (t *testCase) runStep(c *C, stepName string, init bool) {
147146
c.Assert(err, IsNil)
148147

149148
client := NewClient(t.local, remote)
150-
151149
// initiate a client with the root keys
152-
if init {
153-
keys := getKeys(c, remote)
154-
c.Assert(client.Init(keys, 1), IsNil)
155-
}
150+
keys := getKeys(c, remote)
151+
c.Assert(client.Init(keys, 1), IsNil)
156152

157153
// check update returns the correct updated targets
158154
files, err := client.Update()
159155
c.Assert(err, IsNil)
160-
c.Assert(files, HasLen, 1)
156+
l, _ := strconv.Atoi(stepName)
157+
c.Assert(files, HasLen, l+1)
161158

162159
targetName := stepName
163160
t.targets[targetName] = []byte(targetName)

client/leveldbstore/leveldbstore.go

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ func (f *fileLocalStore) SetMeta(name string, meta json.RawMessage) error {
4040
return f.db.Put([]byte(name), []byte(meta), nil)
4141
}
4242

43+
func (f *fileLocalStore) DeleteMeta(name string) error {
44+
return f.db.Delete([]byte(name), nil)
45+
}
46+
4347
func (f *fileLocalStore) Close() error {
4448
if err := f.db.Close(); err != nil {
4549
return err

client/leveldbstore/leveldbstore_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,32 @@ func (LocalStoreSuite) TestFileLocalStore(c *C) {
4848
c.Assert(err, IsNil)
4949
assertGet(meta{"root.json": rootJSON, "targets.json": targetsJSON})
5050
}
51+
52+
func (LocalStoreSuite) TestDeleteMeta(c *C) {
53+
tmp := c.MkDir()
54+
path := filepath.Join(tmp, "tuf.db")
55+
store, err := FileLocalStore(path)
56+
c.Assert(err, IsNil)
57+
58+
type meta map[string]json.RawMessage
59+
60+
assertGet := func(expected meta) {
61+
actual, err := store.GetMeta()
62+
c.Assert(err, IsNil)
63+
c.Assert(meta(actual), DeepEquals, expected)
64+
}
65+
66+
// initial GetMeta should return empty meta
67+
assertGet(meta{})
68+
69+
// SetMeta should persist
70+
rootJSON := []byte(`{"_type":"Root"}`)
71+
c.Assert(store.SetMeta("root.json", rootJSON), IsNil)
72+
assertGet(meta{"root.json": rootJSON})
73+
74+
store.DeleteMeta("root.json")
75+
m, _ := store.GetMeta()
76+
if _, ok := m["root.json"]; ok {
77+
c.Fatalf("Metadata is not deleted!")
78+
}
79+
}

client/local_store.go

+5
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ func (m memoryLocalStore) SetMeta(name string, meta json.RawMessage) error {
1818
m[name] = meta
1919
return nil
2020
}
21+
22+
func (m memoryLocalStore) DeleteMeta(name string) error {
23+
delete(m, name)
24+
return nil
25+
}

0 commit comments

Comments
 (0)