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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
514 changes: 4 additions & 510 deletions README.md

Large diffs are not rendered by default.

389 changes: 281 additions & 108 deletions client/client.go

Large diffs are not rendered by default.

123 changes: 117 additions & 6 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ package client
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"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 @@ -298,7 +302,7 @@ func (s *ClientSuite) TestNoChangeUpdate(c *C) {
_, err := client.Update()
c.Assert(err, IsNil)
_, err = client.Update()
c.Assert(IsLatestSnapshot(err), Equals, true)
c.Assert(err, IsNil)
}

func (s *ClientSuite) TestNewTimestamp(c *C) {
Expand All @@ -308,7 +312,7 @@ func (s *ClientSuite) TestNewTimestamp(c *C) {
c.Assert(s.repo.Timestamp(), IsNil)
s.syncRemote(c)
_, err := client.Update()
c.Assert(IsLatestSnapshot(err), Equals, true)
c.Assert(err, IsNil)
c.Assert(client.timestampVer > version, Equals, true)
}

Expand Down Expand Up @@ -360,6 +364,113 @@ func (s *ClientSuite) TestNewRoot(c *C) {
}
}

// startTUFRepoServer starts a HTTP server to serve a TUF Repo.
func startTUFRepoServer(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
}

// newClientWithMeta creates new client and sets the root metadata for it.
func newClientWithMeta(baseDir string, relPath string, serverAddr string, initWithLocalMetadata bool) (*Client, error) {
initialStateDir := filepath.Join(baseDir, relPath)
opts := &HTTPRemoteOptions{
MetadataPath: "metadata",
TargetsPath: "targets",
}

remote, err := HTTPRemoteStore(fmt.Sprintf("http://%s/", serverAddr), opts, nil)
if err != nil {
return nil, err
}
c := NewClient(MemoryLocalStore(), remote)
for _, m := range []string{"root.json", "snapshot.json", "timestamp.json", "targets.json"} {
metadataJSON, err := ioutil.ReadFile(initialStateDir + "/" + m)
if err != nil {
return nil, err
}
c.local.SetMeta(m, metadataJSON)
}
return c, nil
}

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

func (s *ClientSuite) TestUpdateRoots(c *C) {
var tests = []struct {
fixturePath string
isExpired bool // Value retuned by verify.IsExpired.
expectedError error
expectedVersions map[string]int
}{
// New root version update (no key update) succeeds.
{"testdata/PublishedTwice", false, nil, map[string]int{"root": 2, "timestamp": 1, "snapshot": 1, "targets": 1}},
// New root update (root role key rotation) succeeds.
{"testdata/PublishedTwiceWithRotatedKeys_root", false, nil, map[string]int{"root": 2, "timestamp": 1, "snapshot": 1, "targets": 1}},
// New root update (snapshot role key rotation) succeeds.
{"testdata/PublishedTwiceWithRotatedKeys_snapshot", false, nil, map[string]int{"root": 2, "timestamp": 2, "snapshot": 2, "targets": 1}},
// New root update (targets role key rotation) succeeds.
{"testdata/PublishedTwiceWithRotatedKeys_targets", false, nil, map[string]int{"root": 2, "timestamp": 2, "snapshot": 2, "targets": 2}},
// New root update (timestamp role key rotation) succeeds.
{"testdata/PublishedTwiceWithRotatedKeys_timestamp", false, nil, map[string]int{"root": 2, "timestamp": 2, "snapshot": 1, "targets": 1}},
// New expired root update fails.
{"testdata/PublishedTwiceWithRotatedKeys_root", true, ErrDecodeFailed{File: "root.json", Err: verify.ErrExpired{}}, map[string]int{}},
// New root update with a rollback attack fails.
{"testdata/PublishedTwiceWithStaleVersion_root", false, verify.ErrWrongVersion(verify.ErrWrongVersion{Given: 1, Expected: 2}), map[string]int{}},
// New root update with fast forward attack fails.
{"testdata/PublishedTwiceForwardVersionWithRotatedKeys_root", false, verify.ErrWrongVersion(verify.ErrWrongVersion{Given: 3, Expected: 2}), map[string]int{}},
// New root with invalid new root signature fails (n+1th root didn't sign off n+1).
{"testdata/PublishedTwiceInvalidNewRootSignatureWithRotatedKeys_root", false, errors.New("tuf: signature verification failed"), map[string]int{}},
// New root with invalid old root signature fails (nth root didn't sign off n+1).
{"testdata/PublishedTwiceInvalidOldRootSignatureWithRotatedKeys_root", false, errors.New("tuf: signature verification failed"), map[string]int{}},
}

for _, test := range tests {
e := verify.IsExpired
verify.IsExpired = func(t time.Time) bool { return test.isExpired }
tufClient, closer := initRootTest(c, test.fixturePath /* initWithLocalMetadata = */, true /* ignoreExpired = */, true)
_, err := tufClient.Update()
if test.expectedError == nil {
c.Assert(err, IsNil)
// Check if the root.json is being saved in non-volatile storage.
tufClient.getLocalMeta()
versionMethods := map[string]int{"root": tufClient.rootVer,
"timestamp": tufClient.timestampVer,
"snapshot": tufClient.snapshotVer,
"targets": tufClient.targetsVer}
for m, v := range test.expectedVersions {
assert.Equal(c, v, versionMethods[m])
}
} else {
// For backward compatibility, the update root returns
// ErrDecodeFailed that wraps the verify.ErrExpired.
if _, ok := test.expectedError.(ErrDecodeFailed); ok {
decodeErr, ok := err.(ErrDecodeFailed)
c.Assert(ok, Equals, true)
c.Assert(decodeErr.File, Equals, "root.json")
_, ok = decodeErr.Err.(verify.ErrExpired)
c.Assert(ok, Equals, true)
} else {
assert.Equal(c, test.expectedError, err)
}
}
closer()
verify.IsExpired = e
}
}

func (s *ClientSuite) TestNewTargets(c *C) {
client := s.newClient(c)
files, err := client.Update()
Expand Down Expand Up @@ -560,17 +671,18 @@ func (s *ClientSuite) TestUpdateLocalRootExpired(c *C) {
s.addRemoteTarget(c, "bar.txt")
s.syncRemote(c)

const expectedRootVersion = 2

// check the update downloads the non expired remote root.json and
// restarts itself, thus successfully updating
s.withMetaExpired(func() {
err := client.getLocalMeta()
if _, ok := err.(verify.ErrExpired); !ok {
c.Fatalf("expected err to have type signed.ErrExpired, got %T", err)
}

client := NewClient(s.local, s.remote)
_, err = client.Update()
c.Assert(err, IsNil)
c.Assert(client.rootVer, Equals, expectedRootVersion)
})
}

Expand Down Expand Up @@ -704,7 +816,6 @@ func (s *ClientSuite) TestUpdateReplayAttack(c *C) {
c.Assert(s.repo.Timestamp(), IsNil)
s.syncRemote(c)
_, err := client.Update()
c.Assert(IsLatestSnapshot(err), Equals, true)
c.Assert(client.timestampVer > version, Equals, true)

// replace remote timestamp.json with the old one
Expand Down Expand Up @@ -804,7 +915,7 @@ func (t *testDestination) Delete() error {
func (s *ClientSuite) TestDownloadUnknownTarget(c *C) {
client := s.updatedClient(c)
var dest testDestination
c.Assert(client.Download("nonexistent", &dest), Equals, ErrUnknownTarget{"nonexistent"})
c.Assert(client.Download("nonexistent", &dest), Equals, ErrUnknownTarget{Name: "nonexistent", SnapshotVersion: 1})
c.Assert(dest.deleted, Equals, true)
}

Expand Down
198 changes: 198 additions & 0 deletions client/delegations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package client

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

// getTargetFileMeta searches for a verified TargetFileMeta matching a target
// Requires a local snapshot to be loaded and is locked to the snapshot versions.
// Searches through delegated targets following TUF spec 1.0.19 section 5.6.
func (c *Client) getTargetFileMeta(target string) (data.TargetFileMeta, error) {
snapshot, err := c.loadLocalSnapshot()
if err != nil {
return data.TargetFileMeta{}, err
}

// delegationsIterator covers 5.6.7
// - pre-order depth-first search starting with the top targets
// - filter delegations with paths or path_hash_prefixes matching searched target
// - 5.6.7.1 cycles protection
// - 5.6.7.2 terminations
delegations := newDelegationsIterator(target)
for i := 0; i < c.MaxDelegations; i++ {
d, ok := delegations.next()
if !ok {
return data.TargetFileMeta{}, ErrUnknownTarget{target, snapshot.Version}
}

// covers 5.6.{1,2,3,4,5,6}
targets, err := c.loadDelegatedTargets(snapshot, d.delegatee.Name, d.verifier)
if err != nil {
return data.TargetFileMeta{}, err
}

// stop when the searched TargetFileMeta is found
if m, ok := targets.Targets[target]; ok {
return m, nil
}

if targets.Delegations != nil {
delegationsVerifier, err := verify.NewDelegationsVerifier(targets.Delegations)
if err != nil {
return data.TargetFileMeta{}, err
}
err = delegations.add(targets.Delegations.Roles, d.delegatee.Name, delegationsVerifier)
if err != nil {
return data.TargetFileMeta{}, err
}
}
}

return data.TargetFileMeta{}, ErrMaxDelegations{
Target: target,
MaxDelegations: c.MaxDelegations,
SnapshotVersion: snapshot.Version,
}
}

func (c *Client) loadLocalSnapshot() (*data.Snapshot, error) {
if err := c.getLocalMeta(); err != nil {
return nil, err
}

rawS, ok := c.localMeta["snapshot.json"]
if !ok {
return nil, ErrNoLocalSnapshot
}

snapshot := &data.Snapshot{}
if err := c.db.Unmarshal(rawS, snapshot, "snapshot", c.snapshotVer); err != nil {
return nil, ErrDecodeFailed{"snapshot.json", err}
}
return snapshot, nil
}

// loadDelegatedTargets downloads, decodes, verifies and stores targets
func (c *Client) loadDelegatedTargets(snapshot *data.Snapshot, role string, verifier verify.DelegationsVerifier) (*data.Targets, error) {
var err error
fileName := role + ".json"
fileMeta, ok := snapshot.Meta[fileName]
if !ok {
return nil, ErrRoleNotInSnapshot{role, snapshot.Version}
}

// 5.6.1 download target if not in the local store
// 5.6.2 check against snapshot hash
raw, alreadyStored := c.localMetaFromSnapshot(fileName, fileMeta)
if !alreadyStored {
raw, err = c.downloadMetaFromSnapshot(fileName, fileMeta)
if err != nil {
return nil, err
}
}

targets := &data.Targets{}
// 5.6.3 verify signature with parent public keys
// 5.6.5 verify that the targets is not expired
// role "targets" is a top role verified by root keys loaded in the client db
if role == "targets" {
err = c.db.Unmarshal(raw, targets, role, fileMeta.Version)
} else {
err = verifier.Unmarshal(raw, targets, role, fileMeta.Version)
}
if err != nil {
return nil, ErrDecodeFailed{fileName, err}
}

// 5.6.4 check against snapshot version
if targets.Version != fileMeta.Version {
return nil, ErrTargetsSnapshotVersionMismatch{
Role: fileName,
DownloadedTargetsVersion: fileMeta.Version,
TargetsSnapshotVersion: targets.Version,
SnapshotVersion: snapshot.Version,
}
}
// 5.6.6 persist
if !alreadyStored {
if err := c.local.SetMeta(fileName, raw); err != nil {
return nil, err
}
}
return targets, nil
}

type delegation struct {
delegator string
verifier verify.DelegationsVerifier
delegatee data.DelegatedRole
}

type delegationsIterator struct {
stack []delegation
target string
visitedRoles map[string]struct{}
}

// newDelegationsIterator initialises an iterator with a first step
// on top level targets
func newDelegationsIterator(target string) *delegationsIterator {
i := &delegationsIterator{
target: target,
stack: []delegation{
{
delegatee: data.DelegatedRole{Name: "targets"},
},
},
visitedRoles: make(map[string]struct{}),
}
return i
}

func (d *delegationsIterator) next() (value delegation, ok bool) {
if len(d.stack) == 0 {
return delegation{}, false
}
delegation := d.stack[len(d.stack)-1]
d.stack = d.stack[:len(d.stack)-1]

// 5.6.7.1: If this role has been visited before, then skip this role (so
// that cycles in the delegation graph are avoided).
roleName := delegation.delegatee.Name
if _, ok := d.visitedRoles[roleName]; ok {
return d.next()
}
d.visitedRoles[roleName] = struct{}{}

// 5.6.7.2 trim delegations to visit, only the current role and its delegations
// will be considered
// https://github.com/theupdateframework/specification/issues/168
if delegation.delegatee.Terminating {
// Empty the stack.
d.stack = d.stack[0:0]
}
return delegation, true
}

func (d *delegationsIterator) add(roles []data.DelegatedRole, delegator string, verifier verify.DelegationsVerifier) error {
for i := len(roles) - 1; i >= 0; i-- {
// Push the roles onto the stack in reverse so we get an preorder traversal
// of the delegations graph.
r := roles[i]
matchesPath, err := r.MatchesPath(d.target)
if err != nil {
return err
}
if matchesPath {
delegation := delegation{
delegator: delegator,
delegatee: r,
verifier: verifier,
}
d.stack = append(d.stack, delegation)
}
}

return nil
}
Loading