blob: 3e9a051ca4be336fe8da80cb56d806bb94c47677 [file] [log] [blame]
package tuf
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/flynn/go-tuf/data"
"github.com/flynn/go-tuf/encrypted"
"github.com/flynn/go-tuf/sign"
"github.com/flynn/go-tuf/util"
"github.com/flynn/go-tuf/verify"
"golang.org/x/crypto/ed25519"
. "gopkg.in/check.v1"
)
// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }
type RepoSuite struct{}
var _ = Suite(&RepoSuite{})
func (RepoSuite) TestNewRepo(c *C) {
testNewRepo(c, NewRepo)
}
func (RepoSuite) TestNewRepoIndent(c *C) {
testNewRepo(c, func(local LocalStore, hashAlgorithms ...string) (*Repo, error) {
return NewRepoIndent(local, "", "\t")
})
}
// AssertNumUniqueKeys verifies that the number of unique root keys for a given role is as expected.
func (*RepoSuite) assertNumUniqueKeys(c *C, root *data.Root, role string, num int) {
c.Assert(root.UniqueKeys()[role], HasLen, num)
}
func testNewRepo(c *C, newRepo func(local LocalStore, hashAlgorithms ...string) (*Repo, error)) {
meta := map[string]json.RawMessage{
"root.json": []byte(`{
"signed": {
"_type": "root",
"version": 1,
"expires": "2015-12-26T03:26:55.821520874Z",
"keys": {},
"roles": {}
},
"signatures": []
}`),
"targets.json": []byte(`{
"signed": {
"_type": "targets",
"version": 1,
"expires": "2015-03-26T03:26:55.82155686Z",
"targets": {}
},
"signatures": []
}`),
"snapshot.json": []byte(`{
"signed": {
"_type": "snapshot",
"version": 1,
"expires": "2015-01-02T03:26:55.821585981Z",
"meta": {}
},
"signatures": []
}`),
"timestamp.json": []byte(`{
"signed": {
"_type": "timestamp",
"version": 1,
"expires": "2014-12-27T03:26:55.821599702Z",
"meta": {}
},
"signatures": []
}`),
}
local := MemoryStore(meta, nil)
r, err := newRepo(local)
c.Assert(err, IsNil)
root, err := r.root()
c.Assert(err, IsNil)
c.Assert(root.Type, Equals, "root")
c.Assert(root.Version, Equals, 1)
c.Assert(root.Keys, NotNil)
c.Assert(root.Keys, HasLen, 0)
targets, err := r.targets()
c.Assert(err, IsNil)
c.Assert(targets.Type, Equals, "targets")
c.Assert(targets.Version, Equals, 1)
c.Assert(targets.Targets, NotNil)
c.Assert(targets.Targets, HasLen, 0)
snapshot, err := r.snapshot()
c.Assert(err, IsNil)
c.Assert(snapshot.Type, Equals, "snapshot")
c.Assert(snapshot.Version, Equals, 1)
c.Assert(snapshot.Meta, NotNil)
c.Assert(snapshot.Meta, HasLen, 0)
timestamp, err := r.timestamp()
c.Assert(err, IsNil)
c.Assert(timestamp.Type, Equals, "timestamp")
c.Assert(timestamp.Version, Equals, 1)
c.Assert(timestamp.Meta, NotNil)
c.Assert(timestamp.Meta, HasLen, 0)
}
func (rs *RepoSuite) TestInit(c *C) {
local := MemoryStore(
make(map[string]json.RawMessage),
map[string][]byte{"foo.txt": []byte("foo")},
)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// Init() sets root.ConsistentSnapshot
for _, v := range []bool{true, false} {
c.Assert(r.Init(v), IsNil)
root, err := r.root()
c.Assert(err, IsNil)
c.Assert(root.ConsistentSnapshot, Equals, v)
}
// Init() fails if targets have been added
c.Assert(r.AddTarget("foo.txt", nil), IsNil)
c.Assert(r.Init(true), Equals, ErrInitNotAllowed)
}
func genKey(c *C, r *Repo, role string) []string {
keyids, err := r.GenKey(role)
c.Assert(err, IsNil)
c.Assert(len(keyids) > 0, Equals, true)
return keyids
}
func (rs *RepoSuite) TestGenKey(c *C) {
local := MemoryStore(make(map[string]json.RawMessage), nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// generate a key for an unknown role
_, err = r.GenKey("foo")
c.Assert(err, Equals, ErrInvalidRole{"foo"})
// generate a root key
ids := genKey(c, r, "root")
// check root metadata is correct
root, err := r.root()
c.Assert(err, IsNil)
c.Assert(root.Roles, NotNil)
c.Assert(root.Roles, HasLen, 1)
rs.assertNumUniqueKeys(c, root, "root", 1)
rootRole, ok := root.Roles["root"]
if !ok {
c.Fatal("missing root role")
}
c.Assert(rootRole.KeyIDs, HasLen, 2)
c.Assert(rootRole.KeyIDs, DeepEquals, ids)
for _, keyID := range ids {
k, ok := root.Keys[keyID]
if !ok {
c.Fatal("missing key")
}
c.Assert(k.IDs(), DeepEquals, ids)
c.Assert(k.Value.Public, HasLen, ed25519.PublicKeySize)
}
// check root key + role are in db
db, err := r.db()
c.Assert(err, IsNil)
for _, keyID := range ids {
rootKey := db.GetKey(keyID)
c.Assert(rootKey, NotNil)
c.Assert(rootKey.IDs(), DeepEquals, ids)
role := db.GetRole("root")
c.Assert(role.KeyIDs, DeepEquals, util.StringSliceToSet(ids))
// check the key was saved correctly
localKeys, err := local.GetSigningKeys("root")
c.Assert(err, IsNil)
c.Assert(localKeys, HasLen, 1)
c.Assert(localKeys[0].IDs(), DeepEquals, ids)
// check RootKeys() is correct
rootKeys, err := r.RootKeys()
c.Assert(err, IsNil)
c.Assert(rootKeys, HasLen, 1)
c.Assert(rootKeys[0].IDs(), DeepEquals, rootKey.IDs())
c.Assert(rootKeys[0].Value.Public, DeepEquals, rootKey.Value.Public)
}
rootKey := db.GetKey(ids[0])
c.Assert(rootKey, NotNil)
// generate two targets keys
genKey(c, r, "targets")
genKey(c, r, "targets")
// check root metadata is correct
root, err = r.root()
c.Assert(err, IsNil)
c.Assert(root.Roles, HasLen, 2)
rs.assertNumUniqueKeys(c, root, "root", 1)
rs.assertNumUniqueKeys(c, root, "targets", 2)
targetsRole, ok := root.Roles["targets"]
if !ok {
c.Fatal("missing targets role")
}
c.Assert(targetsRole.KeyIDs, HasLen, 4)
targetKeyIDs := make(map[string]struct{}, 2)
db, err = r.db()
c.Assert(err, IsNil)
for _, id := range targetsRole.KeyIDs {
targetKeyIDs[id] = struct{}{}
_, ok = root.Keys[id]
if !ok {
c.Fatal("missing key")
}
key := db.GetKey(id)
c.Assert(key, NotNil)
c.Assert(key.ContainsID(id), Equals, true)
}
role := db.GetRole("targets")
c.Assert(role.KeyIDs, DeepEquals, targetKeyIDs)
// check RootKeys() is unchanged
rootKeys, err := r.RootKeys()
c.Assert(err, IsNil)
c.Assert(rootKeys, HasLen, 1)
c.Assert(rootKeys[0].IDs(), DeepEquals, rootKey.IDs())
// check the keys were saved correctly
localKeys, err := local.GetSigningKeys("targets")
c.Assert(err, IsNil)
c.Assert(localKeys, HasLen, 2)
for _, key := range localKeys {
found := false
for _, id := range targetsRole.KeyIDs {
if key.ContainsID(id) {
found = true
break
}
}
if !found {
c.Fatal("missing key")
}
}
// check root.json got staged
meta, err := local.GetMeta()
c.Assert(err, IsNil)
rootJSON, ok := meta["root.json"]
if !ok {
c.Fatal("missing root metadata")
}
s := &data.Signed{}
c.Assert(json.Unmarshal(rootJSON, s), IsNil)
stagedRoot := &data.Root{}
c.Assert(json.Unmarshal(s.Signed, stagedRoot), IsNil)
c.Assert(stagedRoot.Type, Equals, root.Type)
c.Assert(stagedRoot.Version, Equals, root.Version)
c.Assert(stagedRoot.Expires.UnixNano(), Equals, root.Expires.UnixNano())
// make sure both root and stagedRoot have evaluated IDs(), otherwise
// DeepEquals will fail because those values might not have been
// computed yet.
for _, key := range root.Keys {
key.IDs()
}
for _, key := range stagedRoot.Keys {
key.IDs()
}
c.Assert(stagedRoot.Keys, DeepEquals, root.Keys)
c.Assert(stagedRoot.Roles, DeepEquals, root.Roles)
}
func addPrivateKey(c *C, r *Repo, role string, key *sign.PrivateKey) []string {
err := r.AddPrivateKey(role, key)
c.Assert(err, IsNil)
keyids := key.PublicData().IDs()
c.Assert(len(keyids) > 0, Equals, true)
return keyids
}
func addGeneratedPrivateKey(c *C, r *Repo, role string) []string {
key, err := sign.GenerateEd25519Key()
c.Assert(err, IsNil)
return addPrivateKey(c, r, role, key)
}
func (rs *RepoSuite) TestAddPrivateKey(c *C) {
local := MemoryStore(make(map[string]json.RawMessage), nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// generate a key for an unknown role
key, err := sign.GenerateEd25519Key()
c.Assert(err, IsNil)
err = r.AddPrivateKey("foo", key)
c.Assert(err, Equals, ErrInvalidRole{"foo"})
// add a root key
ids := addPrivateKey(c, r, "root", key)
// check root metadata is correct
root, err := r.root()
c.Assert(err, IsNil)
c.Assert(root.Version, Equals, 1)
c.Assert(root.Roles, NotNil)
c.Assert(root.Roles, HasLen, 1)
rs.assertNumUniqueKeys(c, root, "root", 1)
rootRole, ok := root.Roles["root"]
if !ok {
c.Fatal("missing root role")
}
c.Assert(rootRole.KeyIDs, HasLen, 2)
c.Assert(rootRole.KeyIDs, DeepEquals, ids)
for _, keyID := range ids {
k, ok := root.Keys[keyID]
if !ok {
c.Fatalf("missing key %s", keyID)
}
c.Assert(k.IDs(), DeepEquals, ids)
c.Assert(k.Value.Public, HasLen, ed25519.PublicKeySize)
}
// check root key + role are in db
db, err := r.db()
c.Assert(err, IsNil)
for _, keyID := range ids {
rootKey := db.GetKey(keyID)
c.Assert(rootKey, NotNil)
c.Assert(rootKey.IDs(), DeepEquals, ids)
role := db.GetRole("root")
c.Assert(role.KeyIDs, DeepEquals, util.StringSliceToSet(ids))
// check the key was saved correctly
localKeys, err := local.GetSigningKeys("root")
c.Assert(err, IsNil)
c.Assert(localKeys, HasLen, 1)
c.Assert(localKeys[0].IDs(), DeepEquals, ids)
// check RootKeys() is correct
rootKeys, err := r.RootKeys()
c.Assert(err, IsNil)
c.Assert(rootKeys, HasLen, 1)
c.Assert(rootKeys[0].IDs(), DeepEquals, rootKey.IDs())
c.Assert(rootKeys[0].Value.Public, DeepEquals, rootKey.Value.Public)
}
rootKey := db.GetKey(ids[0])
c.Assert(rootKey, NotNil)
// generate two targets keys
addGeneratedPrivateKey(c, r, "targets")
addGeneratedPrivateKey(c, r, "targets")
// check root metadata is correct
root, err = r.root()
c.Assert(err, IsNil)
c.Assert(root.Roles, HasLen, 2)
rs.assertNumUniqueKeys(c, root, "root", 1)
rs.assertNumUniqueKeys(c, root, "targets", 2)
targetsRole, ok := root.Roles["targets"]
if !ok {
c.Fatal("missing targets role")
}
c.Assert(targetsRole.KeyIDs, HasLen, 4)
targetKeyIDs := make(map[string]struct{}, 2)
db, err = r.db()
c.Assert(err, IsNil)
for _, id := range targetsRole.KeyIDs {
targetKeyIDs[id] = struct{}{}
_, ok = root.Keys[id]
if !ok {
c.Fatal("missing key")
}
key := db.GetKey(id)
c.Assert(key, NotNil)
c.Assert(key.ContainsID(id), Equals, true)
}
role := db.GetRole("targets")
c.Assert(role.KeyIDs, DeepEquals, targetKeyIDs)
// check RootKeys() is unchanged
rootKeys, err := r.RootKeys()
c.Assert(err, IsNil)
c.Assert(rootKeys, HasLen, 1)
c.Assert(rootKeys[0].IDs(), DeepEquals, rootKey.IDs())
// check the keys were saved correctly
localKeys, err := local.GetSigningKeys("targets")
c.Assert(err, IsNil)
c.Assert(localKeys, HasLen, 2)
for _, key := range localKeys {
found := false
for _, id := range targetsRole.KeyIDs {
if key.ContainsID(id) {
found = true
break
}
}
if !found {
c.Fatal("missing key")
}
}
// check root.json got staged
meta, err := local.GetMeta()
c.Assert(err, IsNil)
rootJSON, ok := meta["root.json"]
if !ok {
c.Fatal("missing root metadata")
}
s := &data.Signed{}
c.Assert(json.Unmarshal(rootJSON, s), IsNil)
stagedRoot := &data.Root{}
c.Assert(json.Unmarshal(s.Signed, stagedRoot), IsNil)
c.Assert(stagedRoot.Type, Equals, root.Type)
c.Assert(stagedRoot.Version, Equals, root.Version)
c.Assert(stagedRoot.Expires.UnixNano(), Equals, root.Expires.UnixNano())
// make sure both root and stagedRoot have evaluated IDs(), otherwise
// DeepEquals will fail because those values might not have been
// computed yet.
for _, key := range root.Keys {
key.IDs()
}
for _, key := range stagedRoot.Keys {
key.IDs()
}
c.Assert(stagedRoot.Keys, DeepEquals, root.Keys)
c.Assert(stagedRoot.Roles, DeepEquals, root.Roles)
// commit to make sure we don't modify metadata after committing metadata.
addGeneratedPrivateKey(c, r, "snapshot")
addGeneratedPrivateKey(c, r, "timestamp")
c.Assert(r.AddTargets([]string{}, nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
// add the same root key to make sure the metadata is unmodified.
oldRoot, err := r.root()
c.Assert(err, IsNil)
addPrivateKey(c, r, "root", key)
newRoot, err := r.root()
c.Assert(err, IsNil)
c.Assert(oldRoot, DeepEquals, newRoot)
if _, ok := r.versionUpdated["root.json"]; ok {
c.Fatal("root should not be marked dirty")
}
}
func (rs *RepoSuite) TestRevokeKey(c *C) {
local := MemoryStore(make(map[string]json.RawMessage), nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// revoking a key for an unknown role returns ErrInvalidRole
c.Assert(r.RevokeKey("foo", ""), DeepEquals, ErrInvalidRole{"foo"})
// revoking a key which doesn't exist returns ErrKeyNotFound
c.Assert(r.RevokeKey("root", "nonexistent"), DeepEquals, ErrKeyNotFound{"root", "nonexistent"})
// generate keys
genKey(c, r, "root")
target1IDs := genKey(c, r, "targets")
target2IDs := genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
root, err := r.root()
c.Assert(err, IsNil)
c.Assert(root.Roles, NotNil)
c.Assert(root.Roles, HasLen, 4)
c.Assert(root.Keys, NotNil)
rs.assertNumUniqueKeys(c, root, "root", 1)
rs.assertNumUniqueKeys(c, root, "targets", 2)
rs.assertNumUniqueKeys(c, root, "snapshot", 1)
rs.assertNumUniqueKeys(c, root, "timestamp", 1)
// revoke a key
targetsRole, ok := root.Roles["targets"]
if !ok {
c.Fatal("missing targets role")
}
c.Assert(targetsRole.KeyIDs, HasLen, len(target1IDs)+len(target2IDs))
id := targetsRole.KeyIDs[0]
c.Assert(r.RevokeKey("targets", id), IsNil)
// make sure all the other key ids were also revoked
for _, id := range target1IDs {
c.Assert(r.RevokeKey("targets", id), DeepEquals, ErrKeyNotFound{"targets", id})
}
// check root was updated
root, err = r.root()
c.Assert(err, IsNil)
c.Assert(root.Roles, NotNil)
c.Assert(root.Roles, HasLen, 4)
c.Assert(root.Keys, NotNil)
rs.assertNumUniqueKeys(c, root, "root", 1)
rs.assertNumUniqueKeys(c, root, "targets", 1)
rs.assertNumUniqueKeys(c, root, "snapshot", 1)
rs.assertNumUniqueKeys(c, root, "timestamp", 1)
targetsRole, ok = root.Roles["targets"]
if !ok {
c.Fatal("missing targets role")
}
c.Assert(targetsRole.KeyIDs, HasLen, 2)
c.Assert(targetsRole.KeyIDs, DeepEquals, target2IDs)
}
func (rs *RepoSuite) TestSign(c *C) {
meta := map[string]json.RawMessage{"root.json": []byte(`{"signed":{},"signatures":[]}`)}
local := MemoryStore(meta, nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// signing with no keys returns ErrInsufficientKeys
c.Assert(r.Sign("root.json"), Equals, ErrInsufficientKeys{"root.json"})
checkSigIDs := func(keyIDs ...string) {
meta, err := local.GetMeta()
c.Assert(err, IsNil)
rootJSON, ok := meta["root.json"]
if !ok {
c.Fatal("missing root.json")
}
s := &data.Signed{}
c.Assert(json.Unmarshal(rootJSON, s), IsNil)
c.Assert(s.Signatures, HasLen, len(keyIDs))
for i, id := range keyIDs {
c.Assert(s.Signatures[i].KeyID, Equals, id)
}
}
// signing with an available key generates a signature
key, err := sign.GenerateEd25519Key()
c.Assert(err, IsNil)
c.Assert(local.SavePrivateKey("root", key), IsNil)
c.Assert(r.Sign("root.json"), IsNil)
checkSigIDs(key.PublicData().IDs()...)
// signing again does not generate a duplicate signature
c.Assert(r.Sign("root.json"), IsNil)
checkSigIDs(key.PublicData().IDs()...)
// signing with a new available key generates another signature
newKey, err := sign.GenerateEd25519Key()
c.Assert(err, IsNil)
c.Assert(local.SavePrivateKey("root", newKey), IsNil)
c.Assert(r.Sign("root.json"), IsNil)
checkSigIDs(append(key.PublicData().IDs(), newKey.PublicData().IDs()...)...)
}
func (rs *RepoSuite) TestCommit(c *C) {
files := map[string][]byte{"foo.txt": []byte("foo"), "bar.txt": []byte("bar")}
local := MemoryStore(make(map[string]json.RawMessage), files)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// commit without root.json
c.Assert(r.Commit(), DeepEquals, ErrMissingMetadata{"root.json"})
// commit without targets.json
genKey(c, r, "root")
c.Assert(r.Commit(), DeepEquals, ErrMissingMetadata{"targets.json"})
// commit without snapshot.json
genKey(c, r, "targets")
c.Assert(r.AddTarget("foo.txt", nil), IsNil)
c.Assert(r.Commit(), DeepEquals, ErrMissingMetadata{"snapshot.json"})
// commit without timestamp.json
genKey(c, r, "snapshot")
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Commit(), DeepEquals, ErrMissingMetadata{"timestamp.json"})
// commit with timestamp.json but no timestamp key
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), DeepEquals, ErrInsufficientSignatures{"timestamp.json", verify.ErrNoSignatures})
// commit success
genKey(c, r, "timestamp")
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
// commit with an invalid root hash in snapshot.json due to new key creation
genKey(c, r, "targets")
c.Assert(r.Sign("targets.json"), IsNil)
c.Assert(r.Commit(), DeepEquals, errors.New("tuf: invalid root.json in snapshot.json: wrong length, expected 3216 got 3828"))
// commit with an invalid targets hash in snapshot.json
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.AddTarget("bar.txt", nil), IsNil)
c.Assert(r.Commit(), DeepEquals, errors.New("tuf: invalid targets.json in snapshot.json: wrong length, expected 1229 got 1403"))
// commit with an invalid timestamp
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
// TODO: Change this test once Snapshot() supports compression and we
// can guarantee the error will end in "wrong length" by
// compressing a file and thus changing the size of snapshot.json
err = r.Commit()
c.Assert(err, NotNil)
c.Assert(err.Error()[0:44], Equals, "tuf: invalid snapshot.json in timestamp.json")
// commit with a role's threshold greater than number of keys
root, err := r.root()
c.Assert(err, IsNil)
role, ok := root.Roles["timestamp"]
if !ok {
c.Fatal("missing timestamp role")
}
c.Assert(role.KeyIDs, HasLen, 2)
c.Assert(role.Threshold, Equals, 1)
c.Assert(r.RevokeKey("timestamp", role.KeyIDs[0]), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), DeepEquals, ErrNotEnoughKeys{"timestamp", 0, 1})
}
func (rs *RepoSuite) TestCommitVersions(c *C) {
files := map[string][]byte{"foo.txt": []byte("foo")}
local := MemoryStore(make(map[string]json.RawMessage), files)
r, err := NewRepo(local)
c.Assert(err, IsNil)
genKey(c, r, "root")
genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
c.Assert(r.AddTarget("foo.txt", nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
// on initial commit everything should be at version 1.
rootVersion, err := r.RootVersion()
c.Assert(err, IsNil)
c.Assert(rootVersion, Equals, 1)
targetsVersion, err := r.TargetsVersion()
c.Assert(err, IsNil)
c.Assert(targetsVersion, Equals, 1)
snapshotVersion, err := r.SnapshotVersion()
c.Assert(err, IsNil)
c.Assert(snapshotVersion, Equals, 1)
timestampVersion, err := r.SnapshotVersion()
c.Assert(err, IsNil)
c.Assert(timestampVersion, Equals, 1)
// taking a snapshot should only incremept snapshot and timestamp.
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
rootVersion, err = r.RootVersion()
c.Assert(err, IsNil)
c.Assert(rootVersion, Equals, 1)
targetsVersion, err = r.TargetsVersion()
c.Assert(err, IsNil)
c.Assert(targetsVersion, Equals, 1)
snapshotVersion, err = r.SnapshotVersion()
c.Assert(err, IsNil)
c.Assert(snapshotVersion, Equals, 2)
timestampVersion, err = r.SnapshotVersion()
c.Assert(err, IsNil)
c.Assert(timestampVersion, Equals, 2)
// rotating multiple keys should increment the root once.
genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
rootVersion, err = r.RootVersion()
c.Assert(err, IsNil)
c.Assert(rootVersion, Equals, 2)
targetsVersion, err = r.TargetsVersion()
c.Assert(err, IsNil)
c.Assert(targetsVersion, Equals, 1)
snapshotVersion, err = r.SnapshotVersion()
c.Assert(err, IsNil)
c.Assert(snapshotVersion, Equals, 3)
timestampVersion, err = r.SnapshotVersion()
c.Assert(err, IsNil)
c.Assert(timestampVersion, Equals, 3)
}
type tmpDir struct {
path string
c *C
}
func newTmpDir(c *C) *tmpDir {
return &tmpDir{path: c.MkDir(), c: c}
}
func (t *tmpDir) assertExists(path string) {
if _, err := os.Stat(filepath.Join(t.path, path)); os.IsNotExist(err) {
t.c.Fatalf("expected path to exist but it doesn't: %s", path)
}
}
func (t *tmpDir) assertNotExist(path string) {
if _, err := os.Stat(filepath.Join(t.path, path)); !os.IsNotExist(err) {
t.c.Fatalf("expected path to not exist but it does: %s", path)
}
}
func (t *tmpDir) assertHashedFilesExist(path string, hashes data.Hashes) {
t.c.Assert(len(hashes) > 0, Equals, true)
for _, path := range util.HashedPaths(path, hashes) {
t.assertExists(path)
}
}
func (t *tmpDir) assertHashedFilesNotExist(path string, hashes data.Hashes) {
t.c.Assert(len(hashes) > 0, Equals, true)
for _, path := range util.HashedPaths(path, hashes) {
t.assertNotExist(path)
}
}
func (t *tmpDir) assertVersionedFileExist(path string, version int) {
t.assertExists(util.VersionedPath(path, version))
}
func (t *tmpDir) assertVersionedFileNotExist(path string, version int) {
t.assertNotExist(util.VersionedPath(path, version))
}
func (t *tmpDir) assertEmpty(dir string) {
path := filepath.Join(t.path, dir)
f, err := os.Stat(path)
if os.IsNotExist(err) {
t.c.Fatalf("expected dir to exist but it doesn't: %s", dir)
}
t.c.Assert(err, IsNil)
t.c.Assert(f.IsDir(), Equals, true)
entries, err := ioutil.ReadDir(path)
t.c.Assert(err, IsNil)
// check that all (if any) entries are also empty
for _, e := range entries {
t.assertEmpty(filepath.Join(dir, e.Name()))
}
}
func (t *tmpDir) assertFileContent(path, content string) {
actual := t.readFile(path)
t.c.Assert(string(actual), Equals, content)
}
func (t *tmpDir) stagedTargetPath(path string) string {
return filepath.Join(t.path, "staged", "targets", path)
}
func (t *tmpDir) writeStagedTarget(path, data string) {
path = t.stagedTargetPath(path)
t.c.Assert(os.MkdirAll(filepath.Dir(path), 0755), IsNil)
t.c.Assert(ioutil.WriteFile(path, []byte(data), 0644), IsNil)
}
func (t *tmpDir) readFile(path string) []byte {
t.assertExists(path)
data, err := ioutil.ReadFile(filepath.Join(t.path, path))
t.c.Assert(err, IsNil)
return data
}
func (rs *RepoSuite) TestCommitFileSystem(c *C) {
tmp := newTmpDir(c)
local := FileSystemStore(tmp.path, nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// don't use consistent snapshots to make the checks simpler
c.Assert(r.Init(false), IsNil)
// cleaning with nothing staged or committed should fail
c.Assert(r.Clean(), Equals, ErrNewRepository)
// generating keys should stage root.json and create repo dirs
genKey(c, r, "root")
genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
tmp.assertExists("staged/root.json")
tmp.assertEmpty("repository")
tmp.assertEmpty("staged/targets")
// cleaning with nothing committed should fail
c.Assert(r.Clean(), Equals, ErrNewRepository)
// adding a non-existent file fails
c.Assert(r.AddTarget("foo.txt", nil), Equals, ErrFileNotFound{tmp.stagedTargetPath("foo.txt")})
tmp.assertEmpty("repository")
// adding a file stages targets.json
tmp.writeStagedTarget("foo.txt", "foo")
c.Assert(r.AddTarget("foo.txt", nil), IsNil)
tmp.assertExists("staged/targets.json")
tmp.assertEmpty("repository")
t, err := r.targets()
c.Assert(err, IsNil)
c.Assert(t.Targets, HasLen, 1)
if _, ok := t.Targets["foo.txt"]; !ok {
c.Fatal("missing target file: foo.txt")
}
// Snapshot() stages snapshot.json
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
tmp.assertExists("staged/snapshot.json")
tmp.assertEmpty("repository")
// Timestamp() stages timestamp.json
c.Assert(r.Timestamp(), IsNil)
tmp.assertExists("staged/timestamp.json")
tmp.assertEmpty("repository")
// committing moves files from staged -> repository
c.Assert(r.Commit(), IsNil)
tmp.assertExists("repository/root.json")
tmp.assertExists("repository/targets.json")
tmp.assertExists("repository/snapshot.json")
tmp.assertExists("repository/timestamp.json")
tmp.assertFileContent("repository/targets/foo.txt", "foo")
tmp.assertEmpty("staged/targets")
tmp.assertEmpty("staged")
// adding and committing another file moves it into repository/targets
tmp.writeStagedTarget("path/to/bar.txt", "bar")
c.Assert(r.AddTarget("path/to/bar.txt", nil), IsNil)
tmp.assertExists("staged/targets.json")
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
tmp.assertFileContent("repository/targets/foo.txt", "foo")
tmp.assertFileContent("repository/targets/path/to/bar.txt", "bar")
tmp.assertEmpty("staged/targets")
tmp.assertEmpty("staged")
// removing and committing a file removes it from repository/targets
c.Assert(r.RemoveTarget("foo.txt"), IsNil)
tmp.assertExists("staged/targets.json")
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
tmp.assertNotExist("repository/targets/foo.txt")
tmp.assertFileContent("repository/targets/path/to/bar.txt", "bar")
tmp.assertEmpty("staged/targets")
tmp.assertEmpty("staged")
}
func (rs *RepoSuite) TestCommitFileSystemWithNewRepositories(c *C) {
tmp := newTmpDir(c)
newRepo := func() *Repo {
local := FileSystemStore(tmp.path, nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
return r
}
genKey(c, newRepo(), "root")
genKey(c, newRepo(), "targets")
genKey(c, newRepo(), "snapshot")
genKey(c, newRepo(), "timestamp")
tmp.writeStagedTarget("foo.txt", "foo")
c.Assert(newRepo().AddTarget("foo.txt", nil), IsNil)
c.Assert(newRepo().Snapshot(CompressionTypeNone), IsNil)
c.Assert(newRepo().Timestamp(), IsNil)
c.Assert(newRepo().Commit(), IsNil)
}
func (rs *RepoSuite) TestConsistentSnapshot(c *C) {
tmp := newTmpDir(c)
local := FileSystemStore(tmp.path, nil)
r, err := NewRepo(local, "sha512", "sha256")
c.Assert(err, IsNil)
genKey(c, r, "root")
genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
tmp.writeStagedTarget("foo.txt", "foo")
c.Assert(r.AddTarget("foo.txt", nil), IsNil)
tmp.writeStagedTarget("dir/bar.txt", "bar")
c.Assert(r.AddTarget("dir/bar.txt", nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
versions, err := r.fileVersions()
c.Assert(err, IsNil)
c.Assert(versions["root.json"], Equals, 1)
c.Assert(versions["targets.json"], Equals, 1)
c.Assert(versions["snapshot.json"], Equals, 1)
hashes, err := r.fileHashes()
c.Assert(err, IsNil)
// root.json, targets.json and snapshot.json should exist at both versioned and unversioned paths
for _, path := range []string{"root.json", "targets.json", "snapshot.json"} {
repoPath := filepath.Join("repository", path)
tmp.assertHashedFilesNotExist(repoPath, hashes[path])
tmp.assertVersionedFileExist(repoPath, versions[path])
tmp.assertExists(repoPath)
}
// target files should exist at hashed but not unhashed paths
for _, path := range []string{"targets/foo.txt", "targets/dir/bar.txt"} {
repoPath := filepath.Join("repository", path)
tmp.assertHashedFilesExist(repoPath, hashes[path])
tmp.assertNotExist(repoPath)
}
// timestamp.json should exist at an unversioned and unhashed path (it doesn't have a hash)
c.Assert(hashes["repository/timestamp.json"], IsNil)
tmp.assertVersionedFileNotExist("repository/timestamp.json", versions["repository/timestamp.json"])
tmp.assertExists("repository/timestamp.json")
// removing a file should remove the hashed files
c.Assert(r.RemoveTarget("foo.txt"), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
versions, err = r.fileVersions()
c.Assert(err, IsNil)
c.Assert(versions["root.json"], Equals, 1)
c.Assert(versions["targets.json"], Equals, 2)
c.Assert(versions["snapshot.json"], Equals, 2)
// Save the old hashes for foo.txt to make sure we can assert it doesn't exist later.
fooHashes := hashes["targets/foo.txt"]
hashes, err = r.fileHashes()
c.Assert(err, IsNil)
// root.json, targets.json and snapshot.json should exist at both versioned and unversioned paths
for _, path := range []string{"root.json", "targets.json", "snapshot.json"} {
repoPath := filepath.Join("repository", path)
tmp.assertHashedFilesNotExist(repoPath, hashes[path])
tmp.assertVersionedFileExist(repoPath, versions[path])
tmp.assertExists(repoPath)
}
tmp.assertHashedFilesNotExist("repository/targets/foo.txt", fooHashes)
tmp.assertNotExist("repository/targets/foo.txt")
// targets should be returned by new repo
newRepo, err := NewRepo(local, "sha512", "sha256")
c.Assert(err, IsNil)
t, err := newRepo.targets()
c.Assert(err, IsNil)
c.Assert(t.Targets, HasLen, 1)
if _, ok := t.Targets["dir/bar.txt"]; !ok {
c.Fatal("missing targets file: dir/bar.txt")
}
}
func (rs *RepoSuite) TestExpiresAndVersion(c *C) {
files := map[string][]byte{"foo.txt": []byte("foo")}
local := MemoryStore(make(map[string]json.RawMessage), files)
r, err := NewRepo(local)
c.Assert(err, IsNil)
past := time.Now().Add(-1 * time.Second)
_, genKeyErr := r.GenKeyWithExpires("root", past)
for _, err := range []error{
genKeyErr,
r.AddTargetWithExpires("foo.txt", nil, past),
r.RemoveTargetWithExpires("foo.txt", past),
r.SnapshotWithExpires(CompressionTypeNone, past),
r.TimestampWithExpires(past),
} {
c.Assert(err, Equals, ErrInvalidExpires{past})
}
genKey(c, r, "root")
genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
c.Assert(r.AddTargets([]string{}, nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
root, err := r.root()
c.Assert(err, IsNil)
c.Assert(root.Version, Equals, 1)
expires := time.Now().Add(24 * time.Hour)
_, err = r.GenKeyWithExpires("root", expires)
c.Assert(err, IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
root, err = r.root()
c.Assert(err, IsNil)
c.Assert(root.Expires.Unix(), DeepEquals, expires.Round(time.Second).Unix())
c.Assert(root.Version, Equals, 2)
expires = time.Now().Add(12 * time.Hour)
role, ok := root.Roles["root"]
if !ok {
c.Fatal("missing root role")
}
c.Assert(role.KeyIDs, HasLen, 4)
c.Assert(r.RevokeKeyWithExpires("root", role.KeyIDs[0], expires), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
root, err = r.root()
c.Assert(err, IsNil)
c.Assert(root.Expires.Unix(), DeepEquals, expires.Round(time.Second).Unix())
c.Assert(root.Version, Equals, 3)
expires = time.Now().Add(6 * time.Hour)
c.Assert(r.AddTargetWithExpires("foo.txt", nil, expires), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
targets, err := r.targets()
c.Assert(err, IsNil)
c.Assert(targets.Expires.Unix(), Equals, expires.Round(time.Second).Unix())
c.Assert(targets.Version, Equals, 2)
expires = time.Now().Add(2 * time.Hour)
c.Assert(r.RemoveTargetWithExpires("foo.txt", expires), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
targets, err = r.targets()
c.Assert(err, IsNil)
c.Assert(targets.Expires.Unix(), Equals, expires.Round(time.Second).Unix())
c.Assert(targets.Version, Equals, 3)
expires = time.Now().Add(time.Hour)
c.Assert(r.SnapshotWithExpires(CompressionTypeNone, expires), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
snapshot, err := r.snapshot()
c.Assert(err, IsNil)
c.Assert(snapshot.Expires.Unix(), Equals, expires.Round(time.Second).Unix())
c.Assert(snapshot.Version, Equals, 6)
root, err = r.root()
c.Assert(err, IsNil)
c.Assert(snapshot.Meta["root.json"].Version, Equals, root.Version)
c.Assert(snapshot.Meta["targets.json"].Version, Equals, targets.Version)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
snapshot, err = r.snapshot()
c.Assert(err, IsNil)
c.Assert(snapshot.Version, Equals, 7)
expires = time.Now().Add(10 * time.Minute)
c.Assert(r.TimestampWithExpires(expires), IsNil)
c.Assert(r.Commit(), IsNil)
timestamp, err := r.timestamp()
c.Assert(err, IsNil)
c.Assert(timestamp.Expires.Unix(), Equals, expires.Round(time.Second).Unix())
c.Assert(timestamp.Version, Equals, 8)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
timestamp, err = r.timestamp()
c.Assert(err, IsNil)
c.Assert(timestamp.Version, Equals, 9)
c.Assert(timestamp.Meta["snapshot.json"].Version, Equals, snapshot.Version)
}
func (rs *RepoSuite) TestHashAlgorithm(c *C) {
files := map[string][]byte{"foo.txt": []byte("foo")}
local := MemoryStore(make(map[string]json.RawMessage), files)
type hashTest struct {
args []string
expected []string
}
for _, test := range []hashTest{
{args: []string{}, expected: []string{"sha512"}},
{args: []string{"sha256"}},
{args: []string{"sha512", "sha256"}},
} {
// generate metadata with specific hash functions
r, err := NewRepo(local, test.args...)
c.Assert(err, IsNil)
genKey(c, r, "root")
genKey(c, r, "targets")
genKey(c, r, "snapshot")
c.Assert(r.AddTarget("foo.txt", nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
// check metadata has correct hash functions
if test.expected == nil {
test.expected = test.args
}
targets, err := r.targets()
c.Assert(err, IsNil)
snapshot, err := r.snapshot()
c.Assert(err, IsNil)
timestamp, err := r.timestamp()
c.Assert(err, IsNil)
for name, file := range map[string]data.FileMeta{
"foo.txt": targets.Targets["foo.txt"].FileMeta,
"root.json": snapshot.Meta["root.json"].FileMeta,
"targets.json": snapshot.Meta["targets.json"].FileMeta,
"snapshot.json": timestamp.Meta["snapshot.json"].FileMeta,
} {
for _, hashAlgorithm := range test.expected {
if _, ok := file.Hashes[hashAlgorithm]; !ok {
c.Fatalf("expected %s hash to contain hash func %s, got %s", name, hashAlgorithm, file.HashAlgorithms())
}
}
}
}
}
func testPassphraseFunc(p []byte) util.PassphraseFunc {
return func(string, bool) ([]byte, error) { return p, nil }
}
func (rs *RepoSuite) TestKeyPersistence(c *C) {
tmp := newTmpDir(c)
passphrase := []byte("s3cr3t")
store := FileSystemStore(tmp.path, testPassphraseFunc(passphrase))
assertKeys := func(role string, enc bool, expected []*sign.PrivateKey) {
keysJSON := tmp.readFile("keys/" + role + ".json")
pk := &persistedKeys{}
c.Assert(json.Unmarshal(keysJSON, pk), IsNil)
// check the persisted keys are correct
var actual []*sign.PrivateKey
if enc {
c.Assert(pk.Encrypted, Equals, true)
decrypted, err := encrypted.Decrypt(pk.Data, passphrase)
c.Assert(err, IsNil)
c.Assert(json.Unmarshal(decrypted, &actual), IsNil)
} else {
c.Assert(pk.Encrypted, Equals, false)
c.Assert(json.Unmarshal(pk.Data, &actual), IsNil)
}
c.Assert(actual, HasLen, len(expected))
for i, key := range expected {
c.Assert(expected[i], DeepEquals, key)
}
// check GetKeys is correct
signers, err := store.GetSigningKeys(role)
c.Assert(err, IsNil)
c.Assert(signers, HasLen, len(expected))
for i, s := range signers {
c.Assert(s.IDs(), DeepEquals, expected[i].PublicData().IDs())
}
}
// save a key and check it gets encrypted
key, err := sign.GenerateEd25519Key()
c.Assert(err, IsNil)
c.Assert(store.SavePrivateKey("root", key), IsNil)
assertKeys("root", true, []*sign.PrivateKey{key})
// save another key and check it gets added to the existing keys
newKey, err := sign.GenerateEd25519Key()
c.Assert(err, IsNil)
c.Assert(store.SavePrivateKey("root", newKey), IsNil)
assertKeys("root", true, []*sign.PrivateKey{key, newKey})
// check saving a key to an encrypted file without a passphrase fails
insecureStore := FileSystemStore(tmp.path, nil)
key, err = sign.GenerateEd25519Key()
c.Assert(err, IsNil)
c.Assert(insecureStore.SavePrivateKey("root", key), Equals, ErrPassphraseRequired{"root"})
// save a key to an insecure store and check it is not encrypted
key, err = sign.GenerateEd25519Key()
c.Assert(err, IsNil)
c.Assert(insecureStore.SavePrivateKey("targets", key), IsNil)
assertKeys("targets", false, []*sign.PrivateKey{key})
}
func (rs *RepoSuite) TestManageMultipleTargets(c *C) {
tmp := newTmpDir(c)
local := FileSystemStore(tmp.path, nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
// don't use consistent snapshots to make the checks simpler
c.Assert(r.Init(false), IsNil)
genKey(c, r, "root")
genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
assertRepoTargets := func(paths ...string) {
t, err := r.targets()
c.Assert(err, IsNil)
for _, path := range paths {
if _, ok := t.Targets[path]; !ok {
c.Fatalf("missing target file: %s", path)
}
}
}
// adding and committing multiple files moves correct targets from staged -> repository
tmp.writeStagedTarget("foo.txt", "foo")
tmp.writeStagedTarget("bar.txt", "bar")
c.Assert(r.AddTargets([]string{"foo.txt", "bar.txt"}, nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
assertRepoTargets("foo.txt", "bar.txt")
tmp.assertExists("repository/targets/foo.txt")
tmp.assertExists("repository/targets/bar.txt")
// adding all targets moves them all from staged -> repository
count := 10
files := make([]string, count)
for i := 0; i < count; i++ {
files[i] = fmt.Sprintf("file%d.txt", i)
tmp.writeStagedTarget(files[i], "data")
}
c.Assert(r.AddTargets(nil, nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
tmp.assertExists("repository/targets/foo.txt")
tmp.assertExists("repository/targets/bar.txt")
assertRepoTargets(files...)
for _, file := range files {
tmp.assertExists("repository/targets/" + file)
}
tmp.assertEmpty("staged/targets")
tmp.assertEmpty("staged")
// removing all targets removes them from the repository and targets.json
c.Assert(r.RemoveTargets(nil), IsNil)
c.Assert(r.Snapshot(CompressionTypeNone), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
tmp.assertEmpty("repository/targets")
t, err := r.targets()
c.Assert(err, IsNil)
c.Assert(t.Targets, HasLen, 0)
}
func (rs *RepoSuite) TestCustomTargetMetadata(c *C) {
files := map[string][]byte{
"foo.txt": []byte("foo"),
"bar.txt": []byte("bar"),
"baz.txt": []byte("baz"),
}
local := MemoryStore(make(map[string]json.RawMessage), files)
r, err := NewRepo(local)
c.Assert(err, IsNil)
custom := json.RawMessage(`{"foo":"bar"}`)
assertCustomMeta := func(file string, custom *json.RawMessage) {
t, err := r.targets()
c.Assert(err, IsNil)
target, ok := t.Targets[file]
if !ok {
c.Fatalf("missing target file: %s", file)
}
c.Assert(target.Custom, DeepEquals, custom)
}
// check custom metadata gets added to the target
c.Assert(r.AddTarget("foo.txt", custom), IsNil)
assertCustomMeta("foo.txt", &custom)
// check adding bar.txt with no metadata doesn't affect foo.txt
c.Assert(r.AddTarget("bar.txt", nil), IsNil)
assertCustomMeta("bar.txt", nil)
assertCustomMeta("foo.txt", &custom)
// check adding all files with no metadata doesn't reset existing metadata
c.Assert(r.AddTargets(nil, nil), IsNil)
assertCustomMeta("baz.txt", nil)
assertCustomMeta("bar.txt", nil)
assertCustomMeta("foo.txt", &custom)
}