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())
+}