Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 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
121 changes: 116 additions & 5 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"io"
"io/ioutil"
"strings"

"github.com/theupdateframework/go-tuf/data"
"github.com/theupdateframework/go-tuf/util"
Expand All @@ -14,11 +15,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 +87,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,
Copy link
Copy Markdown

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 +151,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 +263,103 @@ 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 {
return err
}

// 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 and breaks after downloading the lastest root metadata:
// (i) real error (timestamp expiration, decode fail, etc)
// (ii) missing root metadata version X (the latest version is being fetched already)
// 5.3.2 Let N denote the version number of the trusted root metadata file.
for nPlusOne := c.rootVer + 1; nPlusOne < c.UpdaterMaxRoots; nPlusOne++ {
// 5.3.3 Try downloading version nPlusOne (N+1) of the root metadata file
nPlusOneRootPath := util.VersionedPath("root.json", nPlusOne)
nPlusOneRootMetadata, err := c.downloadMetaUnsafe(nPlusOneRootPath, defaultRootDownloadLimit)
if err != nil {
// stop when the next root can't be downloaded
// hosseinsia: Instead, check the error type,
// then check for the expiration dates
if _, ok := err.(ErrMissingRemoteMetadata); ok {
break
} else {
return err
}
}

// 5.3.4 Check for an arbitrary software attack.
nPlusOnethRootMetadataSigned := &data.Root{}
// 5.3.4.1 Check that N signed N+1
if err := c.db.Unmarshal(nPlusOneRootMetadata, nPlusOnethRootMetadataSigned, "root", c.rootVer); err != nil {
Copy link
Copy Markdown

@trishankatdatadog trishankatdatadog Jul 28, 2021

Choose a reason for hiding this comment

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

Not clear whether calling this function from c.db has any side effects... just needs documentation

// NOTE: ignore ONLY expiration error; see 5.3.10
if _, ok := err.(verify.ErrExpired); !ok {
if strings.Contains(err.Error(), "signature verification failed") {
return ErrInvalidSignature{}
}
return err
}
}

// 5.3.5 Check for a rollback attack.
if nPlusOnethRootMetadataSigned.Version != nPlusOne {
return ErrWrongRootVersion{
DownloadedVersion: nPlusOnethRootMetadataSigned.Version,
ExpectedVersion: nPlusOne,
}
}

// 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.

// 5.3.8 Persist root metadata.
c.rootVer = nPlusOnethRootMetadataSigned.Version
c.consistentSnapshot = nPlusOnethRootMetadataSigned.ConsistentSnapshot
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 {
if strings.Contains(err.Error(), "signature verification failed") {
return ErrInvalidSignature{}
}
return err
}
// 5.3.9 Repeat steps 5.3.2 to 5.3.9
}
// 5.3.10 Check for a freeze attack.
if err := c.getLocalMeta(); err != nil {
if _, ok := err.(verify.ErrExpired); ok {
return err
}
}
// FIXME: 5.3.11 If the timestamp and / or snapshot keys have been rotated,
// then delete the trusted timestamp and snapshot metadata files.

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

func (c *Client) updateWithLatestRoot(m *data.SnapshotFileMeta) (data.TargetFiles, error) {
var rootJSON json.RawMessage
var err error
Expand Down
63 changes: 63 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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 +361,68 @@ 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 // Expect verify.IsExpired to return this value the test.
extpectedError error
expectedRootVersion int // -1 means no check is performed on this.
}{
// Good new root update succeeds (the timestamp check disabled).
{"testdata/PublishedTwiceWithRotatedKeys_root", true, false, nil, 2},
// Updating with an expired root fails.
{"testdata/PublishedTwiceWithRotatedKeys_root", true, true, verify.ErrExpired{}, -1},
// Root update does not happen with rootUpdater set to false.
{"testdata/PublishedTwiceWithStaleVersion_root", false, false, nil, 1},
// New root update with a worng version number (potentially rollback attack) fails.
{"testdata/PublishedTwiceWithStaleVersion_root", true, false, ErrWrongRootVersion{1, 2}, -1},
// New root with invalid new root signature fails.
{"testdata/PublishedTwiceInvalidNewRootSignatureWithRotatedKeys_root", true, false, ErrInvalidSignature{}, -1},
// New root with invalid old root signature fails.
{"testdata/PublishedTwiceInvalidOldRootSignatureWithRotatedKeys_root", true, false, ErrInvalidSignature{}, -1},
}

for _, test := range tests {
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 local root.json is updated.
assert.Equal(c, test.expectedRootVersion, tufClient.RootVersion())
} 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
126 changes: 126 additions & 0 deletions client/clienttest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package client

import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"path/filepath"
"strings"

"github.com/theupdateframework/go-tuf/data"
"github.com/theupdateframework/go-tuf/verify"
)

//Initializes a local HTTP server and serves TUF Repo.
func initTestTUFRepoServer(baseDir string, relPath string) (net.Listener, error) {
serverDir := filepath.Join(baseDir, relPath)
l, err := net.Listen("tcp", "127.0.0.1:0")
go http.Serve(l, http.FileServer(http.Dir(serverDir)))
return l, err
}

// Initializes the client object with local files without fetching
// the latest version from the server.
func (c *Client) initWithLocal(rootKeys []*data.Key, threshold int, localrootpath string) error {
if len(rootKeys) < threshold {
return ErrInsufficientKeys
}
rootJSON, err := ioutil.ReadFile(localrootpath) //c.downloadMetaUnsafe("root.json", defaultRootDownloadLimit)
if err != nil {
return err
}
// create a new key database, and add all the public `rootKeys` to it.
c.db = verify.NewDB()
rootKeyIDs := make([]string, 0, len(rootKeys))
for _, key := range rootKeys {
for _, id := range key.IDs() {
rootKeyIDs = append(rootKeyIDs, id)
if err := c.db.AddKey(id, key); err != nil {
return err
}
}
}

// add a mock "root" role that trusts the passed in key ids. These keys
// will be used to verify the `root.json` we just fetched.
role := &data.Role{Threshold: threshold, KeyIDs: rootKeyIDs}
if err := c.db.AddRole("root", role); err != nil {
return err
}

// verify that the new root is valid.
if err := c.decodeRoot(rootJSON); err != nil {
return err
}

return c.local.SetMeta("root.json", rootJSON)
}

//Initializes a TUF Client based on metadata in a given path.
func initTestTUFClient(baseDir string, relPath string, serverAddr string, initWithLocalMetadata bool) (*Client, error) {
initialStateDir := filepath.Join(baseDir, relPath)
opts := &HTTPRemoteOptions{
MetadataPath: "metadata",
TargetsPath: "targets",
}
rawFile, err := ioutil.ReadFile(initialStateDir + "/" + "root.json")
if err != nil {
return nil, err
}
s := &data.Signed{}
root := &data.Root{}
if err := json.Unmarshal(rawFile, s); err != nil {
return nil, err
}
if err := json.Unmarshal(s.Signed, root); err != nil {
return nil, err
}
var keys []*data.Key
for _, sig := range s.Signatures {
k, ok := root.Keys[sig.KeyID]
if ok {
keys = append(keys, k)
}
}

remote, err := HTTPRemoteStore(fmt.Sprintf("http://%s/", serverAddr), opts, nil)
if err != nil {
return nil, err
}
c := NewClient(MemoryLocalStore(), remote)

if initWithLocalMetadata {
if err := c.initWithLocal(keys, 1, initialStateDir+"/"+"root.json"); err != nil {
return nil, err
}
} else {
if err := c.Init(keys, 1); err != nil {
return nil, err
}
}
files, err := ioutil.ReadDir(initialStateDir)
if err != nil {
return nil, err
}

// load local files
for _, f := range files {
if f.IsDir() {
continue
}
name := f.Name()
// ignoring consistent snapshot when loading initial state
if len(strings.Split(name, ".")) == 1 && strings.HasSuffix(name, ".json") {
rawFile, err := ioutil.ReadFile(initialStateDir + "/" + name)
if err != nil {
return nil, err
}
if err := c.local.SetMeta(name, rawFile); err != nil {
return nil, err
}
}
}
return c, nil
}
Loading