Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 152 additions & 5 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"errors"
"io"
"io/ioutil"
"reflect"
"sort"

"github.com/theupdateframework/go-tuf/data"
"github.com/theupdateframework/go-tuf/util"
Expand All @@ -14,11 +16,11 @@ import (

const (
// This is the upper limit in bytes we will use to limit the download
// size of the root/timestamp roles, since we might not don't know how
// big it is.
// size of the root/timestamp roles, since the size is unknown.
defaultRootDownloadLimit = 512000
defaultTimestampDownloadLimit = 16384
defaultMaxDelegations = 32
defaultMaxRoots = 10000
)

// LocalStore is local storage for downloaded top-level metadata.
Expand Down Expand Up @@ -86,13 +88,19 @@ type Client struct {
// MaxDelegations limits by default the number of delegations visited for any
// target
MaxDelegations int

// ChainedRootUpdater enables https://theupdateframework.github.io/specification/v1.0.19/index.html#update-root
ChainedRootUpdater bool
// UpdaterMaxRoots limits the number of downloaded roots in 1.0.19 root updater
UpdaterMaxRoots int
}

func NewClient(local LocalStore, remote RemoteStore) *Client {
return &Client{
local: local,
remote: remote,
MaxDelegations: defaultMaxDelegations,
local: local,
remote: remote,
MaxDelegations: defaultMaxDelegations,
UpdaterMaxRoots: defaultMaxRoots,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a more descriptive name like MaxRootRotations?

}
}

Expand Down Expand Up @@ -144,6 +152,13 @@ func (c *Client) Init(rootKeys []*data.Key, threshold int) error {
//
// https://github.com/theupdateframework/tuf/blob/v0.9.9/docs/tuf-spec.txt#L714
func (c *Client) Update() (data.TargetFiles, error) {
if c.ChainedRootUpdater {
if err := c.updateRoots(); err != nil {
return data.TargetFiles{}, err
}
return c.update(true)
}

return c.update(false)
}

Expand Down Expand Up @@ -249,6 +264,138 @@ func (c *Client) update(latestRoot bool) (data.TargetFiles, error) {
return updatedTargets, nil
}

func (c *Client) updateRoots() error {
// https://theupdateframework.github.io/specification/v1.0.19/index.html#load-trusted-root

// 5.2 Load the trusted root metadata file. We assume that a good,
// trusted copy of this file was shipped with the package manager
// or software updater using an out-of-band process. Note that
// the expiration of the trusted root metadata file does not
// matter, because we will attempt to update it in the next step.
if err := c.getLocalMeta(); err != nil {
if _, ok := err.(verify.ErrExpired); !ok {
return err
}
}

// Prepare for 5.3.11: If the timestamp and / or snapshot keys have been rotated,
// then delete the trusted timestamp and snapshot metadata files.
getKeyIDs := func(role string) []string {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
getKeyIDs := func(role string) []string {
getKeyIDs := func(rolename string) []string {

keyIDs := make([]string, 0, len(c.db.GetRole("timestamp").KeyIDs))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
keyIDs := make([]string, 0, len(c.db.GetRole("timestamp").KeyIDs))
keyIDs := make([]string, 0, len(c.db.GetRole(role).KeyIDs))

for k := range c.db.GetRole(role).KeyIDs {
keyIDs = append(keyIDs, k)
}
sort.Strings(keyIDs)
return keyIDs
}
startTimestampKeyIDs := getKeyIDs("timestamp")
startSnapshotKeyIDs := getKeyIDs("snapshot")

// 5.3.1 Temorarily turn on the consistent snapshots in order to download
// versioned root metadata files as described next.
consistentSnapshot := c.consistentSnapshot
c.consistentSnapshot = true

// https://theupdateframework.github.io/specification/v1.0.19/index.html#update-root

// 5.3.1 Since it may now be signed using entirely different keys,
// the client MUST somehow be able to establish a trusted line of
// continuity to the latest set of keys (see § 6.1 Key
// management and migration). To do so, the client MUST
// download intermediate root metadata files, until the
// latest available one is reached. Therefore, it MUST
// temporarily turn on consistent snapshots in order to
// download versioned root metadata files as described next.

// This loop returns on error or breaks after downloading the lastest root metadata.
// 5.3.2 Let N denote the version number of the trusted root metadata file.
for i := 0; i < c.UpdaterMaxRoots; i++ {
// 5.3.3 Try downloading version nPlusOne of the root metadata file.
// NOTE: as a side effect, we do update c.rootVer to nPlusOne between iterations.
nPlusOne := c.rootVer + 1
nPlusOneRootPath := util.VersionedPath("root.json", nPlusOne)
nPlusOneRootMetadata, err := c.downloadMetaUnsafe(nPlusOneRootPath, defaultRootDownloadLimit)
//fmt.Println(string(nPlusOneRootMetadata[:]))
if err != nil {
if _, ok := err.(ErrMissingRemoteMetadata); ok {
// stop when the next root can't be downloaded
break
} else {
return err
}
}

// 5.3.4 Check for an arbitrary software attack.
nPlusOneRootMetadataSigned := &data.Root{}
// 5.3.4.1 Check that N signed N+1
// 5.3.5 Check for a rollback attack. Here, we check that nPlusOneRootMetadataSigned.version >= nPlusOne.
if err := c.db.UnmarshalExpired(nPlusOneRootMetadata, nPlusOneRootMetadataSigned, "root", nPlusOne); err != nil {
// 5.3.6 Note that the expiration of the new (intermediate) root
// metadata file does not matter yet, because we will check for
// it in step 5.3.10.
if _, ok := err.(verify.ErrExpired); !ok {
return err
}
}

// 5.3.5 Following up, we check for a fast-forward attack: here, we check for that nPlusOneRootMetadataSigned.version >= nPlusOne.
if nPlusOneRootMetadataSigned.Version != nPlusOne {
return verify.ErrWrongVersion{
Given: nPlusOneRootMetadataSigned.Version,
Expected: nPlusOne,
}
}

// 5.3.7 Set the trusted root metadata file to the new root metadata file.
c.rootVer = nPlusOneRootMetadataSigned.Version
// NOTE: following up on 5.3.1, we want to always have consistent snapshots on for the duration
// of root rotation. AFTER the rotation is over, we will set it to the value of the last root.
consistentSnapshot = nPlusOneRootMetadataSigned.ConsistentSnapshot
// 5.3.8 Persist root metadata. The client MUST write the file to non-volatile storage as FILENAME.EXT (e.g. root.json).
// NOTE: Internally, setMeta stores metadata in LevelDB in a persistent manner.
if err := c.local.SetMeta("root.json", nPlusOneRootMetadata); err != nil {
return err
}

// 5.3.4.2 check that N+1 signed itself.
if err := c.getLocalMeta(); err != nil {
// 5.3.6 Note that the expiration of the new (intermediate) root
// metadata file does not matter yet, because we will check for
// it in step 5.3.10.
if _, ok := err.(verify.ErrExpired); !ok {
return err
}
}

// 5.3.9 Repeat steps 5.3.2 to 5.3.9
}

// 5.3.10 Check for a freeze attack.
// NOTE: this will check for any, including freeze, attack.
if err := c.getLocalMeta(); err != nil {
return err
}

// 5.3.11 If the timestamp and / or snapshot keys have been rotated,
// then delete the trusted timestamp and snapshot metadata files.
// To figure out if the keys are rotated, compare the timestamp/snapshot keys
// of start root and end root.
//endTimestampKeyIDs := someFunc(c.db.GetRole("timestamp").KeyIDs)
endTimestampKeyIDs := getKeyIDs("timestamp")
endSnapshotKeyIDs := getKeyIDs("snapshot")

if !reflect.DeepEqual(startTimestampKeyIDs, endTimestampKeyIDs) ||
!reflect.DeepEqual(startSnapshotKeyIDs, endSnapshotKeyIDs) {
c.local.SetMeta("snapshot", json.RawMessage{})
c.local.SetMeta("timestamp", json.RawMessage{})
return ErrEmptyTimestampOrSnapshot
}

// 5.3.12 Set whether consistent snapshots are used as per the trusted root metadata file.
c.consistentSnapshot = consistentSnapshot
return nil
}

func (c *Client) updateWithLatestRoot(m *data.SnapshotFileMeta) (data.TargetFiles, error) {
var rootJSON json.RawMessage
var err error
Expand Down
79 changes: 79 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
Expand All @@ -11,6 +12,7 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"
cjson "github.com/tent/canonical-json-go"
tuf "github.com/theupdateframework/go-tuf"
"github.com/theupdateframework/go-tuf/data"
Expand Down Expand Up @@ -360,6 +362,83 @@ func (s *ClientSuite) TestNewRoot(c *C) {
}
}

// Test helper
func initTestClient(c *C, baseDir string, initWithLocalMetadata bool, ignoreExpired bool) (*Client, func() error) {
l, err := initTestTUFRepoServer(baseDir, "server")
c.Assert(err, IsNil)
e := verify.IsExpired
if ignoreExpired {
verify.IsExpired = func(t time.Time) bool { return false }
}
tufClient, err := initTestTUFClient(baseDir, "client/metadata/current", l.Addr().String(), initWithLocalMetadata)
verify.IsExpired = e
c.Assert(err, IsNil)
return tufClient, l.Close
}

// Tests updateRoots method.
func (s *ClientSuite) TestUpdateRoot(c *C) {
var tests = []struct {
fixturePath string
rootUpdater bool
isExpired bool // Value retuned by verify.IsExpired.
extpectedError error
expectedRootVersion int // -1 means no check is performed on this.
expectNonRootMetadataDeletion bool
}{
// Good new root update succeeds (the timestamp check disabled).
{"testdata/PublishedTwiceWithRotatedKeys_root", true, false, nil, 2, false},
// Good new root update with a new key for timestamp succeeds (the timestamp check disabled), and timestamp and snapshot metadata deleted.
{"testdata/PublishedTwiceRotateTimestampKeysWithRotatedKeys_root", true, false, ErrEmptyTimestampOrSnapshot, 2, true},
// Good update but with an expired root fails.
{"testdata/PublishedTwiceWithRotatedKeys_root", true, true, verify.ErrExpired{}, -1, false},
// Bad update does not happen with rootUpdater set to false.
{"testdata/PublishedTwiceWithStaleVersion_root", false, false, nil, 1, false},
// Bad root update with a rollback attack fails.
{"testdata/PublishedTwiceWithStaleVersion_root", true, false, verify.ErrLowVersion{Actual: 1, Current: 2}, -1, false},
//Bad root update with fast forward attack fails.
{"testdata/PublishedTwiceForwardVersionWithRotatedKeys_root", true, false, verify.ErrWrongVersion(verify.ErrWrongVersion{Given: 3, Expected: 2}), -1, false},
// Bad root with invalid new root signature fails.
{"testdata/PublishedTwiceInvalidNewRootSignatureWithRotatedKeys_root", true, false, errors.New("tuf: signature verification failed"), -1, false},
// Bad root with invalid old root signature fails.
{"testdata/PublishedTwiceInvalidOldRootSignatureWithRotatedKeys_root", true, false, errors.New("tuf: signature verification failed"), -1, false},
}

for _, test := range tests {
fmt.Println(test.fixturePath)
e := verify.IsExpired
verify.IsExpired = func(t time.Time) bool { return test.isExpired }

tufClient, closer := initTestClient(c, test.fixturePath /* initWithLocalMetadata = */, true /* ignoreExpired = */, true)
tufClient.ChainedRootUpdater = test.rootUpdater
_, err := tufClient.Update()
if test.extpectedError == nil {
c.Assert(err, IsNil)
// Check if the root.json is being saved in non-volatile storage.
tufClient.getLocalMeta()
assert.Equal(c, test.expectedRootVersion, tufClient.RootVersion())
// Check the timestamp and metadata deletion.
if test.expectNonRootMetadataDeletion {
if m, err := tufClient.local.GetMeta(); err == nil {
assert.Equal(c, m["timestamp"], json.RawMessage{})
assert.Equal(c, m["snapshot"], json.RawMessage{})
}
}

} else {
if _, ok := err.(verify.ErrExpired); ok {
_, ok := test.extpectedError.(verify.ErrExpired)
assert.True(c, ok)
} else {
assert.Equal(c, test.extpectedError, err)
}
}
closer()
verify.IsExpired = e
}

}

func (s *ClientSuite) TestNewTargets(c *C) {
client := s.newClient(c)
files, err := client.Update()
Expand Down
Loading