Merge pull request #49 from flynn/improve-client-tests
Improve client tests
diff --git a/client/client.go b/client/client.go
index d271339..0be0c1a 100644
--- a/client/client.go
+++ b/client/client.go
@@ -43,12 +43,12 @@
local LocalStore
remote RemoteStore
- // The following four fields represent the versions of metatdata from
- // local storage
- localRootVer int
- localTargetsVer int
- localSnapshotVer int
- localTimestampVer int
+ // The following four fields represent the versions of metatdata either
+ // from local storage or from recently downloaded metadata
+ rootVer int
+ targetsVer int
+ snapshotVer int
+ timestampVer int
// targets is the list of available targets, either from local storage
// or from recently downloaded targets metadata
@@ -75,6 +75,9 @@
// and threshold, and then saved in local storage. It is expected that rootKeys
// were securely distributed with the software being updated.
func (c *Client) Init(rootKeys []*data.Key, threshold int) error {
+ if len(rootKeys) < threshold {
+ return ErrInsufficientKeys
+ }
rootJSON, err := c.downloadMetaUnsafe("root.json")
if err != nil {
return err
@@ -94,7 +97,7 @@
return err
}
- if err := c.verifyRoot(rootJSON); err != nil {
+ if err := c.decodeRoot(rootJSON); err != nil {
return err
}
@@ -114,20 +117,18 @@
func (c *Client) update(latestRoot bool) (data.Files, error) {
// Always start the update using local metadata
if err := c.getLocalMeta(); err != nil {
- if err == signed.ErrExpired {
- // if the latest root.json has been downloaded and it
- // is still expired, halt the update with an error
- if latestRoot {
- return nil, ErrExpiredMeta{"root.json"}
+ if _, ok := err.(signed.ErrExpired); ok {
+ if !latestRoot {
+ return c.updateWithLatestRoot(nil)
}
- return c.updateWithLatestRoot(nil)
+ // this should not be reached as if the latest root has
+ // been downloaded and it is expired, updateWithLatestRoot
+ // should not have continued the update
+ return nil, err
}
return nil, err
}
- // TODO: If we get an invalid signature downloading timestamp.json
- // or snapshot.json, return c.updateWithLatestRoot
-
// Get timestamp.json, extract snapshot.json file meta and save the
// timestamp.json locally
timestampJSON, err := c.downloadMetaUnsafe("timestamp.json")
@@ -136,6 +137,11 @@
}
snapshotMeta, err := c.decodeTimestamp(timestampJSON)
if err != nil {
+ // ErrRoleThreshold could indicate timestamp keys have been
+ // revoked, so retry with the latest root.json
+ if isDecodeFailedWithErr(err, signed.ErrRoleThreshold) && !latestRoot {
+ return c.updateWithLatestRoot(nil)
+ }
return nil, err
}
if err := c.local.SetMeta("timestamp.json", timestampJSON); err != nil {
@@ -144,7 +150,7 @@
// Return ErrLatestSnapshot if we already have the latest snapshot.json
if c.hasMeta("snapshot.json", snapshotMeta) {
- return nil, ErrLatestSnapshot{c.localSnapshotVer}
+ return nil, ErrLatestSnapshot{c.snapshotVer}
}
// Get snapshot.json, then extract root.json and targets.json file meta.
@@ -158,6 +164,11 @@
}
rootMeta, targetsMeta, err := c.decodeSnapshot(snapshotJSON)
if err != nil {
+ // ErrRoleThreshold could indicate snapshot keys have been
+ // revoked, so retry with the latest root.json
+ if isDecodeFailedWithErr(err, signed.ErrRoleThreshold) && !latestRoot {
+ return c.updateWithLatestRoot(nil)
+ }
return nil, err
}
@@ -203,7 +214,7 @@
if err != nil {
return nil, err
}
- if err := c.verifyRoot(rootJSON); err != nil {
+ if err := c.decodeRoot(rootJSON); err != nil {
return nil, err
}
if err := c.local.SetMeta("root.json", rootJSON); err != nil {
@@ -247,7 +258,6 @@
if err := signed.Verify(s, "root", 0, db); err != nil {
return err
}
- c.localRootVer = root.Version
c.db = db
} else {
return ErrNoRootKeys
@@ -258,7 +268,7 @@
if err := signed.UnmarshalTrusted(snapshotJSON, snapshot, "snapshot", c.db); err != nil {
return err
}
- c.localSnapshotVer = snapshot.Version
+ c.snapshotVer = snapshot.Version
}
if targetsJSON, ok := meta["targets.json"]; ok {
@@ -266,7 +276,7 @@
if err := signed.UnmarshalTrusted(targetsJSON, targets, "targets", c.db); err != nil {
return err
}
- c.localTargetsVer = targets.Version
+ c.targetsVer = targets.Version
c.targets = targets.Targets
}
@@ -275,7 +285,7 @@
if err := signed.UnmarshalTrusted(timestampJSON, timestamp, "timestamp", c.db); err != nil {
return err
}
- c.localTimestampVer = timestamp.Version
+ c.timestampVer = timestamp.Version
}
c.localMeta = meta
@@ -342,29 +352,24 @@
return buf.Bytes(), nil
}
-// verifyRoot verifies root metadata.
-func (c *Client) verifyRoot(b json.RawMessage) (err error) {
- s := &data.Signed{}
- if err = json.Unmarshal(b, s); err != nil {
- return
+// decodeRoot decodes and verifies root metadata.
+func (c *Client) decodeRoot(b json.RawMessage) error {
+ root := &data.Root{}
+ if err := signed.Unmarshal(b, root, "root", c.rootVer, c.db); err != nil {
+ return ErrDecodeFailed{"root.json", err}
}
- err = signed.Verify(s, "root", c.localRootVer, c.db)
- if err == signed.ErrExpired {
- err = ErrExpiredMeta{"root.json"}
- }
- return
+ c.rootVer = root.Version
+ return nil
}
// decodeSnapshot decodes and verifies snapshot metadata, and returns the new
// root and targets file meta.
func (c *Client) decodeSnapshot(b json.RawMessage) (data.FileMeta, data.FileMeta, error) {
snapshot := &data.Snapshot{}
- if err := signed.Unmarshal(b, snapshot, "snapshot", c.localSnapshotVer, c.db); err != nil {
- if err == signed.ErrExpired {
- err = ErrExpiredMeta{"snapshot.json"}
- }
- return data.FileMeta{}, data.FileMeta{}, err
+ if err := signed.Unmarshal(b, snapshot, "snapshot", c.snapshotVer, c.db); err != nil {
+ return data.FileMeta{}, data.FileMeta{}, ErrDecodeFailed{"snapshot.json", err}
}
+ c.snapshotVer = snapshot.Version
return snapshot.Meta["root.json"], snapshot.Meta["targets.json"], nil
}
@@ -372,11 +377,8 @@
// returns updated targets.
func (c *Client) decodeTargets(b json.RawMessage) (data.Files, error) {
targets := &data.Targets{}
- if err := signed.Unmarshal(b, targets, "targets", c.localTargetsVer, c.db); err != nil {
- if err == signed.ErrExpired {
- err = ErrExpiredMeta{"targets.json"}
- }
- return nil, err
+ if err := signed.Unmarshal(b, targets, "targets", c.targetsVer, c.db); err != nil {
+ return nil, ErrDecodeFailed{"targets.json", err}
}
updatedTargets := make(data.Files)
for path, meta := range targets.Targets {
@@ -387,6 +389,7 @@
}
updatedTargets[path] = meta
}
+ c.targetsVer = targets.Version
c.targets = targets.Targets
return updatedTargets, nil
}
@@ -395,12 +398,10 @@
// new snapshot file meta.
func (c *Client) decodeTimestamp(b json.RawMessage) (data.FileMeta, error) {
timestamp := &data.Timestamp{}
- if err := signed.Unmarshal(b, timestamp, "timestamp", c.localTimestampVer, c.db); err != nil {
- if err == signed.ErrExpired {
- err = ErrExpiredMeta{"timestamp.json"}
- }
- return data.FileMeta{}, err
+ if err := signed.Unmarshal(b, timestamp, "timestamp", c.timestampVer, c.db); err != nil {
+ return data.FileMeta{}, ErrDecodeFailed{"timestamp.json", err}
}
+ c.timestampVer = timestamp.Version
return timestamp.Meta["snapshot.json"], nil
}
diff --git a/client/client_test.go b/client/client_test.go
index bea5978..54fe718 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ "encoding/json"
"io"
"os"
"strings"
@@ -179,6 +180,31 @@
}
}
+func assertWrongHash(c *C, err error) {
+ // just test the type of err rather using DeepEquals as it contains
+ // hashes we don't necessarily need to check.
+ e, ok := err.(ErrDownloadFailed)
+ if !ok {
+ c.Fatalf("expected err to have type ErrDownloadFailed, got %T", err)
+ }
+ if _, ok := e.Err.(util.ErrWrongHash); !ok {
+ c.Fatalf("expected err.Err to have type util.ErrWrongHash, got %T", err)
+ }
+}
+
+func (s *ClientSuite) assertErrExpired(c *C, err error, file string) {
+ decodeErr, ok := err.(ErrDecodeFailed)
+ if !ok {
+ c.Fatalf("expected err to have type ErrDecodeFailed, got %T", err)
+ }
+ c.Assert(decodeErr.File, Equals, file)
+ expiredErr, ok := decodeErr.Err.(signed.ErrExpired)
+ if !ok {
+ c.Fatalf("expected err.Err to have type signed.ErrExpired, got %T", err)
+ }
+ c.Assert(expiredErr.Expired.Unix(), Equals, s.expiredTime.Unix())
+}
+
func (s *ClientSuite) TestInitRootTooLarge(c *C) {
client := NewClient(MemoryLocalStore(), s.remote)
s.remote["root.json"] = newFakeFile(make([]byte, maxMetaSize+1))
@@ -190,7 +216,7 @@
s.syncRemote(c)
client := NewClient(MemoryLocalStore(), s.remote)
s.withMetaExpired(func() {
- c.Assert(client.Init(s.rootKeys(c), 1), Equals, ErrExpiredMeta{"root.json"})
+ s.assertErrExpired(c, client.Init(s.rootKeys(c), 1), "root.json")
})
}
@@ -201,7 +227,7 @@
c.Assert(client.Init(s.rootKeys(c), 0), Equals, keys.ErrInvalidThreshold)
// check Init() returns signed.ErrRoleThreshold when not enough keys
- c.Assert(client.Init(s.rootKeys(c), 2), Equals, signed.ErrRoleThreshold)
+ c.Assert(client.Init(s.rootKeys(c), 2), Equals, ErrInsufficientKeys)
// check Update() returns ErrNoRootKeys when uninitialized
_, err := client.Update()
@@ -221,9 +247,15 @@
}
func (s *ClientSuite) TestMissingRemoteMetadata(c *C) {
+ client := s.newClient(c)
+
delete(s.remote, "targets.json")
- _, err := s.newClient(c).Update()
+ _, err := client.Update()
c.Assert(err, Equals, ErrMissingRemoteMetadata{"targets.json"})
+
+ delete(s.remote, "timestamp.json")
+ _, err = client.Update()
+ c.Assert(err, Equals, ErrMissingRemoteMetadata{"timestamp.json"})
}
func (s *ClientSuite) TestNoChangeUpdate(c *C) {
@@ -234,6 +266,17 @@
c.Assert(IsLatestSnapshot(err), Equals, true)
}
+func (s *ClientSuite) TestNewTimestamp(c *C) {
+ client := s.updatedClient(c)
+ version := client.timestampVer
+ c.Assert(version > 0, Equals, true)
+ 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)
+}
+
func (s *ClientSuite) TestNewRoot(c *C) {
client := s.newClient(c)
@@ -252,11 +295,11 @@
// check update gets new root version
c.Assert(client.getLocalMeta(), IsNil)
- version := client.localRootVer
+ version := client.rootVer
c.Assert(version > 0, Equals, true)
_, err := client.Update()
c.Assert(err, IsNil)
- c.Assert(client.localRootVer > version, Equals, true)
+ c.Assert(client.rootVer > version, Equals, true)
// check old keys are not in db
for _, id := range s.keyIDs {
@@ -294,43 +337,149 @@
c.Assert(files, HasLen, 0)
}
+func (s *ClientSuite) TestNewTimestampKey(c *C) {
+ client := s.newClient(c)
+
+ // replace key
+ oldID := s.keyIDs["timestamp"]
+ c.Assert(s.repo.RevokeKey("timestamp", oldID), IsNil)
+ newID := s.genKey(c, "timestamp")
+
+ // generate new snapshot (because root has changed) and timestamp
+ c.Assert(s.repo.Snapshot(tuf.CompressionTypeNone), IsNil)
+ c.Assert(s.repo.Timestamp(), IsNil)
+ s.syncRemote(c)
+
+ // check update gets new root and timestamp
+ c.Assert(client.getLocalMeta(), IsNil)
+ rootVer := client.rootVer
+ timestampVer := client.timestampVer
+ _, err := client.Update()
+ c.Assert(err, IsNil)
+ c.Assert(client.rootVer > rootVer, Equals, true)
+ c.Assert(client.timestampVer > timestampVer, Equals, true)
+
+ // check key has been replaced in db
+ c.Assert(client.db.GetKey(oldID), IsNil)
+ key := client.db.GetKey(newID)
+ c.Assert(key, NotNil)
+ c.Assert(key.ID, Equals, newID)
+ role := client.db.GetRole("timestamp")
+ c.Assert(role, NotNil)
+ c.Assert(role.KeyIDs, DeepEquals, map[string]struct{}{newID: {}})
+}
+
+func (s *ClientSuite) TestNewSnapshotKey(c *C) {
+ client := s.newClient(c)
+
+ // replace key
+ oldID := s.keyIDs["snapshot"]
+ c.Assert(s.repo.RevokeKey("snapshot", oldID), IsNil)
+ newID := s.genKey(c, "snapshot")
+
+ // generate new snapshot and timestamp
+ c.Assert(s.repo.Snapshot(tuf.CompressionTypeNone), IsNil)
+ c.Assert(s.repo.Timestamp(), IsNil)
+ s.syncRemote(c)
+
+ // check update gets new root, snapshot and timestamp
+ c.Assert(client.getLocalMeta(), IsNil)
+ rootVer := client.rootVer
+ snapshotVer := client.snapshotVer
+ timestampVer := client.timestampVer
+ _, err := client.Update()
+ c.Assert(err, IsNil)
+ c.Assert(client.rootVer > rootVer, Equals, true)
+ c.Assert(client.snapshotVer > snapshotVer, Equals, true)
+ c.Assert(client.timestampVer > timestampVer, Equals, true)
+
+ // check key has been replaced in db
+ c.Assert(client.db.GetKey(oldID), IsNil)
+ key := client.db.GetKey(newID)
+ c.Assert(key, NotNil)
+ c.Assert(key.ID, Equals, newID)
+ role := client.db.GetRole("snapshot")
+ c.Assert(role, NotNil)
+ c.Assert(role.KeyIDs, DeepEquals, map[string]struct{}{newID: {}})
+}
+
+func (s *ClientSuite) TestNewTargetsKey(c *C) {
+ client := s.newClient(c)
+
+ // replace key
+ oldID := s.keyIDs["targets"]
+ c.Assert(s.repo.RevokeKey("targets", oldID), IsNil)
+ newID := s.genKey(c, "targets")
+
+ // re-sign targets and generate new snapshot and timestamp
+ c.Assert(s.repo.Sign("targets.json"), IsNil)
+ c.Assert(s.repo.Snapshot(tuf.CompressionTypeNone), IsNil)
+ c.Assert(s.repo.Timestamp(), IsNil)
+ s.syncRemote(c)
+
+ // check update gets new metadata
+ c.Assert(client.getLocalMeta(), IsNil)
+ rootVer := client.rootVer
+ targetsVer := client.targetsVer
+ snapshotVer := client.snapshotVer
+ timestampVer := client.timestampVer
+ _, err := client.Update()
+ c.Assert(err, IsNil)
+ c.Assert(client.rootVer > rootVer, Equals, true)
+ c.Assert(client.targetsVer > targetsVer, Equals, true)
+ c.Assert(client.snapshotVer > snapshotVer, Equals, true)
+ c.Assert(client.timestampVer > timestampVer, Equals, true)
+
+ // check key has been replaced in db
+ c.Assert(client.db.GetKey(oldID), IsNil)
+ key := client.db.GetKey(newID)
+ c.Assert(key, NotNil)
+ c.Assert(key.ID, Equals, newID)
+ role := client.db.GetRole("targets")
+ c.Assert(role, NotNil)
+ c.Assert(role.KeyIDs, DeepEquals, map[string]struct{}{newID: {}})
+}
+
func (s *ClientSuite) TestLocalExpired(c *C) {
client := s.newClient(c)
// locally expired timestamp.json is ok
- version := client.localTimestampVer
+ version := client.timestampVer
c.Assert(s.repo.TimestampWithExpires(s.expiredTime), IsNil)
s.syncLocal(c)
s.withMetaExpired(func() {
c.Assert(client.getLocalMeta(), IsNil)
- c.Assert(client.localTimestampVer > version, Equals, true)
+ c.Assert(client.timestampVer > version, Equals, true)
})
// locally expired snapshot.json is ok
- version = client.localSnapshotVer
+ version = client.snapshotVer
c.Assert(s.repo.SnapshotWithExpires(tuf.CompressionTypeNone, s.expiredTime), IsNil)
s.syncLocal(c)
s.withMetaExpired(func() {
c.Assert(client.getLocalMeta(), IsNil)
- c.Assert(client.localSnapshotVer > version, Equals, true)
+ c.Assert(client.snapshotVer > version, Equals, true)
})
// locally expired targets.json is ok
- version = client.localTargetsVer
+ version = client.targetsVer
c.Assert(s.repo.AddTargetWithExpires("foo.txt", nil, s.expiredTime), IsNil)
s.syncLocal(c)
s.withMetaExpired(func() {
c.Assert(client.getLocalMeta(), IsNil)
- c.Assert(client.localTargetsVer > version, Equals, true)
+ c.Assert(client.targetsVer > version, Equals, true)
})
// locally expired root.json is not ok
- version = client.localRootVer
+ version = client.rootVer
s.genKeyExpired(c, "targets")
s.syncLocal(c)
s.withMetaExpired(func() {
- c.Assert(client.getLocalMeta(), Equals, signed.ErrExpired)
- c.Assert(client.localRootVer, Equals, version)
+ err := client.getLocalMeta()
+ if _, ok := err.(signed.ErrExpired); !ok {
+ c.Fatalf("expected err to have type signed.ErrExpired, got %T", err)
+ }
+ c.Assert(client.rootVer, Equals, version)
})
}
@@ -356,8 +505,11 @@
// check the update downloads the non expired remote root.json and
// restarts itself, thus successfully updating
s.withMetaExpired(func() {
- c.Assert(client.getLocalMeta(), Equals, signed.ErrExpired)
- _, err := client.Update()
+ err := client.getLocalMeta()
+ if _, ok := err.(signed.ErrExpired); !ok {
+ c.Fatalf("expected err to have type signed.ErrExpired, got %T", err)
+ }
+ _, err = client.Update()
c.Assert(err, IsNil)
})
}
@@ -370,7 +522,7 @@
s.syncRemote(c)
s.withMetaExpired(func() {
_, err := client.Update()
- c.Assert(err, DeepEquals, ErrExpiredMeta{"timestamp.json"})
+ s.assertErrExpired(c, err, "timestamp.json")
})
c.Assert(s.repo.SnapshotWithExpires(tuf.CompressionTypeNone, s.expiredTime), IsNil)
@@ -378,7 +530,7 @@
s.syncRemote(c)
s.withMetaExpired(func() {
_, err := client.Update()
- c.Assert(err, DeepEquals, ErrExpiredMeta{"snapshot.json"})
+ s.assertErrExpired(c, err, "snapshot.json")
})
c.Assert(s.repo.AddTargetWithExpires("bar.txt", nil, s.expiredTime), IsNil)
@@ -387,7 +539,7 @@
s.syncRemote(c)
s.withMetaExpired(func() {
_, err := client.Update()
- c.Assert(err, DeepEquals, ErrExpiredMeta{"targets.json"})
+ s.assertErrExpired(c, err, "targets.json")
})
s.genKeyExpired(c, "timestamp")
@@ -397,15 +549,101 @@
s.syncRemote(c)
s.withMetaExpired(func() {
_, err := client.Update()
- c.Assert(err, DeepEquals, ErrExpiredMeta{"root.json"})
+ s.assertErrExpired(c, err, "root.json")
})
}
-// TODO: Implement these tests:
-//
-// * Test new timestamp with same snapshot
-// * Test invalid timestamp / snapshot signature downloads root.json
-// * Test invalid hash returns ErrDownloadFailed
+func (s *ClientSuite) TestUpdateMixAndMatchAttack(c *C) {
+ client := s.updatedClient(c)
+
+ // grab the remote targets.json
+ oldTargets, ok := s.remote["targets.json"]
+ if !ok {
+ c.Fatal("missing remote targets.json")
+ }
+
+ // generate new remote metadata, but replace targets.json with the old one
+ s.addRemoteTarget(c, "bar.txt")
+ newTargets, ok := s.remote["targets.json"]
+ if !ok {
+ c.Fatal("missing remote targets.json")
+ }
+ s.remote["targets.json"] = oldTargets
+
+ // check update returns ErrWrongSize for targets.json
+ _, err := client.Update()
+ c.Assert(err, DeepEquals, ErrWrongSize{"targets.json", oldTargets.size, newTargets.size})
+
+ // do the same but keep the size the same
+ c.Assert(s.repo.RemoveTarget("foo.txt"), IsNil)
+ c.Assert(s.repo.Snapshot(tuf.CompressionTypeNone), IsNil)
+ c.Assert(s.repo.Timestamp(), IsNil)
+ s.syncRemote(c)
+ s.remote["targets.json"] = oldTargets
+
+ // check update returns ErrWrongHash
+ _, err = client.Update()
+ assertWrongHash(c, err)
+}
+
+func (s *ClientSuite) TestUpdateReplayAttack(c *C) {
+ client := s.updatedClient(c)
+
+ // grab the remote timestamp.json
+ oldTimestamp, ok := s.remote["timestamp.json"]
+ if !ok {
+ c.Fatal("missing remote timestamp.json")
+ }
+
+ // generate a new timestamp and sync with the client
+ version := client.timestampVer
+ c.Assert(version > 0, Equals, true)
+ 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
+ s.remote["timestamp.json"] = oldTimestamp
+
+ // check update returns ErrLowVersion
+ _, err = client.Update()
+ c.Assert(err, DeepEquals, ErrDecodeFailed{"timestamp.json", signed.ErrLowVersion{version, client.timestampVer}})
+}
+
+func (s *ClientSuite) TestUpdateTamperedTargets(c *C) {
+ client := s.newClient(c)
+
+ // get local targets.json
+ meta, err := s.store.GetMeta()
+ c.Assert(err, IsNil)
+ targetsJSON, ok := meta["targets.json"]
+ if !ok {
+ c.Fatal("missing targets.json")
+ }
+ targets := &data.Signed{}
+ c.Assert(json.Unmarshal(targetsJSON, targets), IsNil)
+
+ // update remote targets.json to have different content but same size
+ c.Assert(targets.Signatures, HasLen, 1)
+ targets.Signatures[0].Method = "xxxxxxx"
+ tamperedJSON, err := json.Marshal(targets)
+ c.Assert(err, IsNil)
+ s.store.SetMeta("targets.json", tamperedJSON)
+ s.syncRemote(c)
+ _, err = client.Update()
+ assertWrongHash(c, err)
+
+ // update remote targets.json to have the wrong size
+ targets.Signatures[0].Method = "xxx"
+ tamperedJSON, err = json.Marshal(targets)
+ c.Assert(err, IsNil)
+ s.store.SetMeta("targets.json", tamperedJSON)
+ s.syncRemote(c)
+ _, err = client.Update()
+ c.Assert(err, DeepEquals, ErrWrongSize{"targets.json", int64(len(tamperedJSON)), int64(len(targetsJSON))})
+}
type testDestination struct {
bytes.Buffer
@@ -475,16 +713,7 @@
remoteFile := s.remote["targets/foo.txt"]
remoteFile.buf = bytes.NewReader([]byte("corrupt"))
var dest testDestination
- err := client.Download("foo.txt", &dest)
- // just test the type of err rather using DeepEquals (as it contains sha512
- // hashes we don't necessarily care about here).
- e, ok := err.(ErrDownloadFailed)
- if !ok {
- c.Fatalf("expected err to have type ErrDownloadFailed, got %T", err)
- }
- if _, ok := e.Err.(util.ErrWrongHash); !ok {
- c.Fatalf("expected err.Err to have type util.ErrWrongHash, got %T", err)
- }
+ assertWrongHash(c, client.Download("foo.txt", &dest))
c.Assert(dest.deleted, Equals, true)
}
diff --git a/client/errors.go b/client/errors.go
index 39b63b2..314ccc0 100644
--- a/client/errors.go
+++ b/client/errors.go
@@ -5,7 +5,10 @@
"fmt"
)
-var ErrNoRootKeys = errors.New("tuf: no root keys found in local meta store")
+var (
+ ErrNoRootKeys = errors.New("tuf: no root keys found in local meta store")
+ ErrInsufficientKeys = errors.New("tuf: insufficient keys to meet threshold")
+)
type ErrMissingRemoteMetadata struct {
Name string
@@ -24,6 +27,23 @@
return fmt.Sprintf("tuf: failed to download %s: %s", e.File, e.Err)
}
+type ErrDecodeFailed struct {
+ File string
+ Err error
+}
+
+func (e ErrDecodeFailed) Error() string {
+ return fmt.Sprintf("tuf: failed to decode %s: %s", e.File, e.Err)
+}
+
+func isDecodeFailedWithErr(err, expected error) bool {
+ e, ok := err.(ErrDecodeFailed)
+ if !ok {
+ return false
+ }
+ return e.Err == expected
+}
+
type ErrNotFound struct {
File string
}
@@ -76,11 +96,3 @@
func (e ErrMetaTooLarge) Error() string {
return fmt.Sprintf("tuf: %s size %d bytes greater than maximum %d bytes", e.Name, e.Size, maxMetaSize)
}
-
-type ErrExpiredMeta struct {
- Name string
-}
-
-func (e ErrExpiredMeta) Error() string {
- return fmt.Sprintf("tuf: %s has expired", e.Name)
-}
diff --git a/repo.go b/repo.go
index 958fde6..dc42f68 100644
--- a/repo.go
+++ b/repo.go
@@ -255,7 +255,7 @@
}
func (r *Repo) setMeta(name string, meta interface{}) error {
- keys, err := r.local.GetKeys(strings.TrimSuffix(name, ".json"))
+ keys, err := r.getKeys(strings.TrimSuffix(name, ".json"))
if err != nil {
return err
}
@@ -282,7 +282,7 @@
return err
}
- keys, err := r.local.GetKeys(role)
+ keys, err := r.getKeys(role)
if err != nil {
return err
}
@@ -301,6 +301,40 @@
return r.local.SetMeta(name, b)
}
+// getKeys returns signing keys from local storage.
+//
+// Only keys contained in the keys db are returned (i.e. local keys which have
+// been revoked are omitted), except for the root role in which case all local
+// keys are returned (revoked root keys still need to sign new root metadata so
+// clients can verify the new root.json and update their keys db accordingly).
+func (r *Repo) getKeys(name string) ([]*data.Key, error) {
+ localKeys, err := r.local.GetKeys(name)
+ if err != nil {
+ return nil, err
+ }
+ if name == "root" {
+ return localKeys, nil
+ }
+ db, err := r.db()
+ if err != nil {
+ return nil, err
+ }
+ role := db.GetRole(name)
+ if role == nil {
+ return nil, nil
+ }
+ if len(role.KeyIDs) == 0 {
+ return nil, nil
+ }
+ keys := make([]*data.Key, 0, len(role.KeyIDs))
+ for _, key := range localKeys {
+ if _, ok := role.KeyIDs[key.ID()]; ok {
+ keys = append(keys, key)
+ }
+ }
+ return keys, nil
+}
+
func (r *Repo) signedMeta(name string) (*data.Signed, error) {
b, ok := r.meta[name]
if !ok {
diff --git a/signed/errors.go b/signed/errors.go
new file mode 100644
index 0000000..10bdfd6
--- /dev/null
+++ b/signed/errors.go
@@ -0,0 +1,23 @@
+package signed
+
+import (
+ "fmt"
+ "time"
+)
+
+type ErrExpired struct {
+ Expired time.Time
+}
+
+func (e ErrExpired) Error() string {
+ return fmt.Sprintf("expired at %s", e.Expired)
+}
+
+type ErrLowVersion struct {
+ Actual int
+ Current int
+}
+
+func (e ErrLowVersion) Error() string {
+ return fmt.Sprintf("version %d is lower than current version %d", e.Actual, e.Current)
+}
diff --git a/signed/verify.go b/signed/verify.go
index 5d37501..4935b37 100644
--- a/signed/verify.go
+++ b/signed/verify.go
@@ -18,9 +18,7 @@
ErrWrongMethod = errors.New("tuf: invalid signature type")
ErrUnknownRole = errors.New("tuf: unknown role")
ErrRoleThreshold = errors.New("tuf: valid signatures did not meet threshold")
- ErrLowVersion = errors.New("tuf: version is lower than current version")
ErrWrongType = errors.New("tuf: meta file has wrong type")
- ErrExpired = errors.New("tuf: meta file has expired")
)
type signedMeta struct {
@@ -42,10 +40,10 @@
return ErrWrongType
}
if IsExpired(sm.Expires) {
- return ErrExpired
+ return ErrExpired{sm.Expires}
}
if sm.Version < minVersion {
- return ErrLowVersion
+ return ErrLowVersion{sm.Version, minVersion}
}
return nil
diff --git a/signed/verify_test.go b/signed/verify_test.go
index f40c2c4..c8c374d 100644
--- a/signed/verify_test.go
+++ b/signed/verify_test.go
@@ -32,10 +32,7 @@
mut func(*test)
}
- expires := func(d time.Duration) *time.Time {
- t := time.Now().Add(d)
- return &t
- }
+ expiredTime := time.Now().Add(-time.Hour)
minVer := 10
tests := []test{
{
@@ -135,12 +132,12 @@
{
name: "low version",
ver: minVer - 1,
- err: ErrLowVersion,
+ err: ErrLowVersion{minVer - 1, minVer},
},
{
name: "expired",
- exp: expires(-time.Hour),
- err: ErrExpired,
+ exp: &expiredTime,
+ err: ErrExpired{expiredTime},
},
}
for _, t := range tests {
@@ -151,7 +148,8 @@
t.ver = minVer
}
if t.exp == nil {
- t.exp = expires(time.Hour)
+ expires := time.Now().Add(time.Hour)
+ t.exp = &expires
}
if t.typ == "" {
t.typ = t.role
@@ -184,6 +182,18 @@
}
err := Verify(t.s, t.role, minVer, db)
- c.Assert(err, Equals, t.err, Commentf("name = %s", t.name))
+ if e, ok := t.err.(ErrExpired); ok {
+ assertErrExpired(c, err, e)
+ } else {
+ c.Assert(err, DeepEquals, t.err, Commentf("name = %s", t.name))
+ }
}
}
+
+func assertErrExpired(c *C, err error, expected ErrExpired) {
+ actual, ok := err.(ErrExpired)
+ if !ok {
+ c.Fatalf("expected err to have type ErrExpired, got %T", err)
+ }
+ c.Assert(actual.Expired.Unix(), Equals, expected.Expired.Unix())
+}