diff --git a/server/handlers/default.go b/server/handlers/default.go index 471214dfcb..7b56bc774a 100644 --- a/server/handlers/default.go +++ b/server/handlers/default.go @@ -7,15 +7,17 @@ import ( "net/http" "strings" - "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/signed" + ctxu "github.com/docker/distribution/context" "github.com/gorilla/mux" "golang.org/x/net/context" - ctxu "github.com/docker/distribution/context" + "github.com/Sirupsen/logrus" "github.com/docker/notary/errors" + "github.com/docker/notary/server/snapshot" "github.com/docker/notary/server/storage" "github.com/docker/notary/server/timestamp" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" ) // MainHandler is the default handler for the server @@ -35,13 +37,23 @@ func MainHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) er // backend is atomically updated with all the new records. func AtomicUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { defer r.Body.Close() + vars := mux.Vars(r) + return atomicUpdateHandler(ctx, w, r, vars) +} + +func atomicUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + gun := vars["imageName"] s := ctx.Value("metaStore") store, ok := s.(storage.MetaStore) if !ok { return errors.ErrNoStorage.WithDetail(nil) } - vars := mux.Vars(r) - gun := vars["imageName"] + cryptoServiceVal := ctx.Value("cryptoService") + cryptoService, ok := cryptoServiceVal.(signed.CryptoService) + if !ok { + return errors.ErrNoCryptoService.WithDetail(nil) + } + reader, err := r.MultipartReader() if err != nil { return errors.ErrMalformedUpload.WithDetail(nil) @@ -73,7 +85,8 @@ func AtomicUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Req Data: inBuf.Bytes(), }) } - if err = validateUpdate(gun, updates, store); err != nil { + updates, err = validateUpdate(cryptoService, gun, updates, store) + if err != nil { return errors.ErrMalformedUpload.WithDetail(err) } err = store.UpdateMany(gun, updates) @@ -85,24 +98,33 @@ func AtomicUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Req // GetHandler returns the json for a specified role and GUN. func GetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + defer r.Body.Close() + vars := mux.Vars(r) + return getHandler(ctx, w, r, vars) +} + +func getHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + gun := vars["imageName"] + tufRole := vars["tufRole"] s := ctx.Value("metaStore") store, ok := s.(storage.MetaStore) if !ok { return errors.ErrNoStorage.WithDetail(nil) } - vars := mux.Vars(r) - gun := vars["imageName"] - tufRole := vars["tufRole"] logger := ctxu.GetLoggerWithFields(ctx, map[string]interface{}{"gun": gun, "tufRole": tufRole}) - if data.CanonicalRole(tufRole) == data.CanonicalTimestampRole { + switch data.CanonicalRole(tufRole) { + case data.CanonicalTimestampRole: return getTimestamp(ctx, w, logger, store, gun) + case data.CanonicalSnapshotRole: + return getSnapshot(ctx, w, logger, store, gun) } out, err := store.GetCurrent(gun, tufRole) if err != nil { if _, ok := err.(*storage.ErrNotFound); ok { + logrus.Error(gun + ":" + tufRole) return errors.ErrMetadataNotFound.WithDetail(nil) } logger.Error("500 GET") @@ -161,6 +183,31 @@ func getTimestamp(ctx context.Context, w http.ResponseWriter, logger ctxu.Logger return nil } +// getTimestampHandler returns a timestamp.json given a GUN +func getSnapshot(ctx context.Context, w http.ResponseWriter, logger ctxu.Logger, store storage.MetaStore, gun string) error { + cryptoServiceVal := ctx.Value("cryptoService") + cryptoService, ok := cryptoServiceVal.(signed.CryptoService) + if !ok { + return errors.ErrNoCryptoService.WithDetail(nil) + } + + out, err := snapshot.GetOrCreateSnapshot(gun, store, cryptoService) + if err != nil { + switch err.(type) { + case *storage.ErrNoKey, *storage.ErrNotFound: + logger.Error("404 GET snapshot") + return errors.ErrMetadataNotFound.WithDetail(nil) + default: + logger.Error("500 GET snapshot") + return errors.ErrUnknown.WithDetail(err) + } + } + + logger.Debug("200 GET snapshot") + w.Write(out) + return nil +} + // GetTimestampKeyHandler returns a timestamp public key, creating a new key-pair // it if it doesn't yet exist func GetTimestampKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { diff --git a/server/handlers/default_test.go b/server/handlers/default_test.go index 91c104af74..b4d1652cea 100644 --- a/server/handlers/default_test.go +++ b/server/handlers/default_test.go @@ -1,15 +1,22 @@ package handlers import ( + "bytes" + "encoding/json" + "io/ioutil" "net/http" "net/http/httptest" "testing" "golang.org/x/net/context" + "github.com/docker/notary/server/storage" + "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/testutils" "github.com/docker/notary/utils" + "github.com/stretchr/testify/assert" ) func TestMainHandlerGet(t *testing.T) { @@ -38,3 +45,145 @@ func TestMainHandlerNotGet(t *testing.T) { t.Fatalf("Expected 404, received %d", res.StatusCode) } } + +func TestGetHandlerRoot(t *testing.T) { + store := storage.NewMemStorage() + _, repo, _ := testutils.EmptyRepo() + + ctx := context.Background() + ctx = context.WithValue(ctx, "metaStore", store) + + root, err := repo.SignRoot(data.DefaultExpires("root")) + rootJSON, err := json.Marshal(root) + assert.NoError(t, err) + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "root", Version: 1, Data: rootJSON}) + + req := &http.Request{ + Body: ioutil.NopCloser(bytes.NewBuffer(nil)), + } + + vars := map[string]string{ + "imageName": "gun", + "tufRole": "root", + } + + rw := httptest.NewRecorder() + + err = getHandler(ctx, rw, req, vars) + assert.NoError(t, err) +} + +func TestGetHandlerTimestamp(t *testing.T) { + store := storage.NewMemStorage() + _, repo, crypto := testutils.EmptyRepo() + + ctx := context.Background() + ctx = context.WithValue(ctx, "metaStore", store) + ctx = context.WithValue(ctx, "cryptoService", crypto) + + sn, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) + snJSON, err := json.Marshal(sn) + assert.NoError(t, err) + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "snapshot", Version: 1, Data: snJSON}) + + ts, err := repo.SignTimestamp(data.DefaultExpires("timestamp")) + tsJSON, err := json.Marshal(ts) + assert.NoError(t, err) + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "timestamp", Version: 1, Data: tsJSON}) + + req := &http.Request{ + Body: ioutil.NopCloser(bytes.NewBuffer(nil)), + } + + vars := map[string]string{ + "imageName": "gun", + "tufRole": "timestamp", + } + + rw := httptest.NewRecorder() + + err = getHandler(ctx, rw, req, vars) + assert.NoError(t, err) +} + +func TestGetHandlerSnapshot(t *testing.T) { + store := storage.NewMemStorage() + _, repo, crypto := testutils.EmptyRepo() + + ctx := context.Background() + ctx = context.WithValue(ctx, "metaStore", store) + ctx = context.WithValue(ctx, "cryptoService", crypto) + + sn, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) + snJSON, err := json.Marshal(sn) + assert.NoError(t, err) + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "snapshot", Version: 1, Data: snJSON}) + + req := &http.Request{ + Body: ioutil.NopCloser(bytes.NewBuffer(nil)), + } + + vars := map[string]string{ + "imageName": "gun", + "tufRole": "snapshot", + } + + rw := httptest.NewRecorder() + + err = getHandler(ctx, rw, req, vars) + assert.NoError(t, err) +} + +func TestGetHandler404(t *testing.T) { + store := storage.NewMemStorage() + + ctx := context.Background() + ctx = context.WithValue(ctx, "metaStore", store) + + req := &http.Request{ + Body: ioutil.NopCloser(bytes.NewBuffer(nil)), + } + + vars := map[string]string{ + "imageName": "gun", + "tufRole": "root", + } + + rw := httptest.NewRecorder() + + err := getHandler(ctx, rw, req, vars) + assert.Error(t, err) +} + +func TestGetHandlerNilData(t *testing.T) { + store := storage.NewMemStorage() + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "root", Version: 1, Data: nil}) + + ctx := context.Background() + ctx = context.WithValue(ctx, "metaStore", store) + + req := &http.Request{ + Body: ioutil.NopCloser(bytes.NewBuffer(nil)), + } + + vars := map[string]string{ + "imageName": "gun", + "tufRole": "root", + } + + rw := httptest.NewRecorder() + + err := getHandler(ctx, rw, req, vars) + assert.Error(t, err) +} + +func TestGetHandlerNoStorage(t *testing.T) { + ctx := context.Background() + + req := &http.Request{ + Body: ioutil.NopCloser(bytes.NewBuffer(nil)), + } + + err := GetHandler(ctx, nil, req) + assert.Error(t, err) +} diff --git a/server/handlers/errors.go b/server/handlers/errors.go new file mode 100644 index 0000000000..45b0ce4f63 --- /dev/null +++ b/server/handlers/errors.go @@ -0,0 +1,56 @@ +package handlers + +import ( + "fmt" +) + +// VALIDATION ERRORS: + +// ErrValidation represents a general validation error +type ErrValidation struct { + msg string +} + +func (err ErrValidation) Error() string { + return fmt.Sprintf("An error occurred during validation: %s", err.msg) +} + +// ErrBadHierarchy represents a missing snapshot at this current time. +// When delegations are implemented it will also represent a missing +// delegation parent +type ErrBadHierarchy struct { + msg string +} + +func (err ErrBadHierarchy) Error() string { + return fmt.Sprintf("Hierarchy of updates in incorrect: %s", err.msg) +} + +// ErrBadRoot represents a failure validating the root +type ErrBadRoot struct { + msg string +} + +func (err ErrBadRoot) Error() string { + return fmt.Sprintf("The root being updated is invalid: %s", err.msg) +} + +// ErrBadTargets represents a failure to validate a targets (incl delegations) +type ErrBadTargets struct { + msg string +} + +func (err ErrBadTargets) Error() string { + return fmt.Sprintf("The targets being updated is invalid: %s", err.msg) +} + +// ErrBadSnapshot represents a failure to validate the snapshot +type ErrBadSnapshot struct { + msg string +} + +func (err ErrBadSnapshot) Error() string { + return fmt.Sprintf("The snapshot being updated is invalid: %s", err.msg) +} + +// END VALIDATION ERRORS diff --git a/server/handlers/validation.go b/server/handlers/validation.go index 32d4f9879e..91eedf7328 100644 --- a/server/handlers/validation.go +++ b/server/handlers/validation.go @@ -7,68 +7,25 @@ import ( "fmt" "strings" + "github.com/Sirupsen/logrus" + + "github.com/docker/notary/server/storage" "github.com/docker/notary/tuf" "github.com/docker/notary/tuf/data" - - "github.com/Sirupsen/logrus" - "github.com/docker/notary/server/storage" "github.com/docker/notary/tuf/keys" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/utils" ) -// ErrValidation represents a general validation error -type ErrValidation struct { - msg string -} - -func (err ErrValidation) Error() string { - return fmt.Sprintf("An error occurred during validation: %s", err.msg) -} - -// ErrBadHierarchy represents a missing snapshot at this current time. -// When delegations are implemented it will also represent a missing -// delegation parent -type ErrBadHierarchy struct { - msg string -} - -func (err ErrBadHierarchy) Error() string { - return fmt.Sprintf("Hierarchy of updates in incorrect: %s", err.msg) -} - -// ErrBadRoot represents a failure validating the root -type ErrBadRoot struct { - msg string -} - -func (err ErrBadRoot) Error() string { - return fmt.Sprintf("The root being updated is invalid: %s", err.msg) -} - -// ErrBadTargets represents a failure to validate a targets (incl delegations) -type ErrBadTargets struct { - msg string -} - -func (err ErrBadTargets) Error() string { - return fmt.Sprintf("The targets being updated is invalid: %s", err.msg) -} - -// ErrBadSnapshot represents a failure to validate the snapshot -type ErrBadSnapshot struct { - msg string -} - -func (err ErrBadSnapshot) Error() string { - return fmt.Sprintf("The snapshot being updated is invalid: %s", err.msg) -} - // validateUpload checks that the updates being pushed // are semantically correct and the signatures are correct -func validateUpdate(gun string, updates []storage.MetaUpdate, store storage.MetaStore) error { +// A list of possibly modified updates are returned if all +// validation was successful. This allows the snapshot to be +// created and added if snapshotting has been delegated to the +// server +func validateUpdate(cs signed.CryptoService, gun string, updates []storage.MetaUpdate, store storage.MetaStore) ([]storage.MetaUpdate, error) { kdb := keys.NewDB() - repo := tuf.NewRepo(kdb, nil) + repo := tuf.NewRepo(kdb, cs) rootRole := data.RoleName(data.CanonicalRootRole) targetsRole := data.RoleName(data.CanonicalTargetsRole) snapshotRole := data.RoleName(data.CanonicalSnapshotRole) @@ -78,11 +35,6 @@ func validateUpdate(gun string, updates []storage.MetaUpdate, store storage.Meta for _, v := range updates { roles[v.Role] = v } - if err := hierarchyOK(roles); err != nil { - logrus.Error("ErrBadHierarchy: ", err.Error()) - return ErrBadHierarchy{msg: err.Error()} - } - logrus.Debug("Successfully validated hierarchy") var root *data.SignedRoot oldRootJSON, err := store.GetCurrent(gun, rootRole) @@ -90,33 +42,33 @@ func validateUpdate(gun string, updates []storage.MetaUpdate, store storage.Meta // problem with storage. No expectation we can // write if we can't read so bail. logrus.Error("error reading previous root: ", err.Error()) - return err + return nil, err } if rootUpdate, ok := roles[rootRole]; ok { // if root is present, validate its integrity, possibly // against a previous root if root, err = validateRoot(gun, oldRootJSON, rootUpdate.Data, store); err != nil { logrus.Error("ErrBadRoot: ", err.Error()) - return ErrBadRoot{msg: err.Error()} + return nil, ErrBadRoot{msg: err.Error()} } // setting root will update keys db if err = repo.SetRoot(root); err != nil { logrus.Error("ErrValidation: ", err.Error()) - return ErrValidation{msg: err.Error()} + return nil, ErrValidation{msg: err.Error()} } logrus.Debug("Successfully validated root") } else { if oldRootJSON == nil { - return ErrValidation{msg: "no pre-existing root and no root provided in update."} + return nil, ErrValidation{msg: "no pre-existing root and no root provided in update."} } parsedOldRoot := &data.SignedRoot{} if err := json.Unmarshal(oldRootJSON, parsedOldRoot); err != nil { - return ErrValidation{msg: "pre-existing root is corrupted and no root provided in update."} + return nil, ErrValidation{msg: "pre-existing root is corrupted and no root provided in update."} } if err = repo.SetRoot(parsedOldRoot); err != nil { logrus.Error("ErrValidation: ", err.Error()) - return ErrValidation{msg: err.Error()} + return nil, ErrValidation{msg: err.Error()} } } @@ -125,32 +77,139 @@ func validateUpdate(gun string, updates []storage.MetaUpdate, store storage.Meta if _, ok := roles[targetsRole]; ok { if t, err = validateTargets(targetsRole, roles, kdb); err != nil { logrus.Error("ErrBadTargets: ", err.Error()) - return ErrBadTargets{msg: err.Error()} + return nil, ErrBadTargets{msg: err.Error()} } repo.SetTargets(targetsRole, t) } logrus.Debug("Successfully validated targets") - var oldSnap *data.SignedSnapshot - oldSnapJSON, err := store.GetCurrent(gun, snapshotRole) - if _, ok := err.(*storage.ErrNotFound); err != nil && !ok { - // problem with storage. No expectation we can - // write if we can't read so bail. - logrus.Error("error reading previous snapshot: ", err.Error()) - return err - } else if err == nil { - oldSnap = &data.SignedSnapshot{} - if err := json.Unmarshal(oldSnapJSON, oldSnap); err != nil { - oldSnap = nil + if _, ok := roles[snapshotRole]; ok { + var oldSnap *data.SignedSnapshot + oldSnapJSON, err := store.GetCurrent(gun, snapshotRole) + if _, ok := err.(*storage.ErrNotFound); err != nil && !ok { + // problem with storage. No expectation we can + // write if we can't read so bail. + logrus.Error("error reading previous snapshot: ", err.Error()) + return nil, err + } else if err == nil { + oldSnap = &data.SignedSnapshot{} + if err := json.Unmarshal(oldSnapJSON, oldSnap); err != nil { + oldSnap = nil + } } + + if err := validateSnapshot(snapshotRole, oldSnap, roles[snapshotRole], roles, kdb); err != nil { + logrus.Error("ErrBadSnapshot: ", err.Error()) + return nil, ErrBadSnapshot{msg: err.Error()} + } + logrus.Debug("Successfully validated snapshot") + } else { + // Check: + // - we have a snapshot key + // - it matches a snapshot key signed into the root.json + // Then: + // - generate a new snapshot + // - add it to the updates + err := prepRepo(gun, repo, store) + if err != nil { + return nil, err + } + update, err := generateSnapshot(gun, kdb, repo, store) + if err != nil { + return nil, err + } + updates = append(updates, *update) + } + return updates, nil +} + +func prepRepo(gun string, repo *tuf.Repo, store storage.MetaStore) error { + if repo.Root == nil { + rootJSON, err := store.GetCurrent(gun, data.CanonicalRootRole) + if err != nil { + return fmt.Errorf("could not load repo for snapshot generation: %v", err) + } + root := &data.SignedRoot{} + err = json.Unmarshal(rootJSON, root) + if err != nil { + return fmt.Errorf("could not load repo for snapshot generation: %v", err) + } + repo.SetRoot(root) + } + if repo.Targets[data.CanonicalTargetsRole] == nil { + targetsJSON, err := store.GetCurrent(gun, data.CanonicalTargetsRole) + if err != nil { + return fmt.Errorf("could not load repo for snapshot generation: %v", err) + } + targets := &data.SignedTargets{} + err = json.Unmarshal(targetsJSON, targets) + if err != nil { + return fmt.Errorf("could not load repo for snapshot generation: %v", err) + } + repo.SetTargets(data.CanonicalTargetsRole, targets) + } + return nil +} + +func generateSnapshot(gun string, kdb *keys.KeyDB, repo *tuf.Repo, store storage.MetaStore) (*storage.MetaUpdate, error) { + role := kdb.GetRole(data.RoleName(data.CanonicalSnapshotRole)) + if role == nil { + return nil, ErrBadRoot{msg: "root did not include snapshot role"} } - if err := validateSnapshot(snapshotRole, oldSnap, roles[snapshotRole], roles, kdb); err != nil { - logrus.Error("ErrBadSnapshot: ", err.Error()) - return ErrBadSnapshot{msg: err.Error()} + algo, keyBytes, err := store.GetKey(gun, data.CanonicalSnapshotRole) + if role == nil { + return nil, ErrBadRoot{msg: "root did not include snapshot key"} } - logrus.Debug("Successfully validated snapshot") - return nil + foundK := data.NewPublicKey(algo, keyBytes) + + validKey := false + for _, id := range role.KeyIDs { + if id == foundK.ID() { + validKey = true + break + } + } + if !validKey { + return nil, ErrBadHierarchy{msg: "no snapshot was included in update and server does not hold current snapshot key for repository"} + } + + currentJSON, err := store.GetCurrent(gun, data.CanonicalSnapshotRole) + if err != nil { + if _, ok := err.(*storage.ErrNotFound); !ok { + return nil, fmt.Errorf("could not retrieve previous snapshot: %v", err) + } + } + var sn *data.SignedSnapshot + if currentJSON != nil { + sn = new(data.SignedSnapshot) + err := json.Unmarshal(currentJSON, sn) + if err != nil { + return nil, fmt.Errorf("could not retrieve previous snapshot: %v", err) + } + err = repo.SetSnapshot(sn) + if err != nil { + return nil, fmt.Errorf("could not load previous snapshot: %v", err) + } + } else { + err := repo.InitSnapshot() + if err != nil { + return nil, fmt.Errorf("could not initialize snapshot: %v", err) + } + } + sgnd, err := repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) + if err != nil { + return nil, fmt.Errorf("could not sign snapshot: %v", err) + } + sgndJSON, err := json.Marshal(sgnd) + if err != nil { + return nil, fmt.Errorf("could not save snapshot: %v", err) + } + return &storage.MetaUpdate{ + Role: data.CanonicalSnapshotRole, + Version: repo.Snapshot.Signed.Version, + Data: sgndJSON, + }, nil } func validateSnapshot(role string, oldSnap *data.SignedSnapshot, snapUpdate storage.MetaUpdate, roles map[string]storage.MetaUpdate, kdb *keys.KeyDB) error { @@ -234,26 +293,6 @@ func validateTargets(role string, roles map[string]storage.MetaUpdate, kdb *keys return t, nil } -// check the snapshot is present. If it is, the hierarchy -// of the update is OK. This seems like a simplistic check -// but is completely sufficient for all possible use cases: -// 1. the user is updating only the snapshot. -// 2. the user is updating a targets (incl. delegations) or -// root metadata. This requires they also provide a new -// snapshot. -// N.B. users should never be updating timestamps. The server -// always handles timestamping. If the user does send a -// timestamp, the server will replace it on next -// GET timestamp.jsonshould it detect the current -// snapshot has a different hash to the one in the timestamp. -func hierarchyOK(roles map[string]storage.MetaUpdate) error { - snapshotRole := data.RoleName(data.CanonicalSnapshotRole) - if _, ok := roles[snapshotRole]; !ok { - return errors.New("snapshot missing from update") - } - return nil -} - func validateRoot(gun string, oldRoot, newRoot []byte, store storage.MetaStore) ( *data.SignedRoot, error) { diff --git a/server/handlers/validation_test.go b/server/handlers/validation_test.go index 970ba19324..31d5aafc0b 100644 --- a/server/handlers/validation_test.go +++ b/server/handlers/validation_test.go @@ -2,10 +2,12 @@ package handlers import ( "crypto/rand" + "encoding/json" "fmt" "testing" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/keys" "github.com/docker/notary/tuf/signed" @@ -65,7 +67,7 @@ func getUpdates(r, tg, sn, ts *data.Signed) ( } func TestValidateEmptyNew(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -76,12 +78,12 @@ func TestValidateEmptyNew(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.NoError(t, err) } func TestValidateNoNewRoot(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -93,12 +95,12 @@ func TestValidateNoNewRoot(t *testing.T) { updates := []storage.MetaUpdate{targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.NoError(t, err) } func TestValidateNoNewTargets(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -110,12 +112,12 @@ func TestValidateNoNewTargets(t *testing.T) { updates := []storage.MetaUpdate{root, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.NoError(t, err) } func TestValidateOnlySnapshot(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -129,12 +131,12 @@ func TestValidateOnlySnapshot(t *testing.T) { updates := []storage.MetaUpdate{snapshot} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.NoError(t, err) } func TestValidateOldRoot(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -146,7 +148,7 @@ func TestValidateOldRoot(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.NoError(t, err) } @@ -191,12 +193,12 @@ func TestValidateRootRotation(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(crypto, "testGUN", updates, store) assert.NoError(t, err) } func TestValidateNoRoot(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -207,13 +209,13 @@ func TestValidateNoRoot(t *testing.T) { updates := []storage.MetaUpdate{targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrValidation{}, err) } func TestValidateSnapshotMissing(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -224,16 +226,226 @@ func TestValidateSnapshotMissing(t *testing.T) { updates := []storage.MetaUpdate{root, targets} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadHierarchy{}, err) } +func TestValidateSnapshotGenerateNoPrev(t *testing.T) { + kdb, repo, cs := testutils.EmptyRepo() + store := storage.NewMemStorage() + snapRole := kdb.GetRole(data.CanonicalSnapshotRole) + + for _, id := range snapRole.KeyIDs { + k := kdb.GetKey(id) + assert.NotNil(t, k) + err := store.SetKey("testGUN", data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) + assert.NoError(t, err) + } + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, targets, _, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + updates := []storage.MetaUpdate{root, targets} + + copyTimestampKey(t, kdb, store, "testGUN") + _, err = validateUpdate(cs, "testGUN", updates, store) + assert.NoError(t, err) +} + +func TestValidateSnapshotGenerateWithPrev(t *testing.T) { + kdb, repo, cs := testutils.EmptyRepo() + store := storage.NewMemStorage() + snapRole := kdb.GetRole(data.CanonicalSnapshotRole) + + for _, id := range snapRole.KeyIDs { + k := kdb.GetKey(id) + assert.NotNil(t, k) + err := store.SetKey("testGUN", data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) + assert.NoError(t, err) + } + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + updates := []storage.MetaUpdate{root, targets} + + // set the current snapshot in the store manually so we find it when generating + // the next version + store.UpdateCurrent("testGUN", snapshot) + + prev, err := data.SnapshotFromSigned(sn) + assert.NoError(t, err) + + copyTimestampKey(t, kdb, store, "testGUN") + updates, err = validateUpdate(cs, "testGUN", updates, store) + assert.NoError(t, err) + + for _, u := range updates { + if u.Role == data.CanonicalSnapshotRole { + curr := &data.SignedSnapshot{} + err = json.Unmarshal(u.Data, curr) + assert.Equal(t, prev.Signed.Version+1, curr.Signed.Version) + assert.Equal(t, u.Version, curr.Signed.Version) + } + } +} + +func TestValidateSnapshotGeneratePrevCorrupt(t *testing.T) { + kdb, repo, cs := testutils.EmptyRepo() + store := storage.NewMemStorage() + snapRole := kdb.GetRole(data.CanonicalSnapshotRole) + + for _, id := range snapRole.KeyIDs { + k := kdb.GetKey(id) + assert.NotNil(t, k) + err := store.SetKey("testGUN", data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) + assert.NoError(t, err) + } + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + updates := []storage.MetaUpdate{root, targets} + + // corrupt the JSON structure of prev snapshot + snapshot.Data = snapshot.Data[1:] + // set the current snapshot in the store manually so we find it when generating + // the next version + store.UpdateCurrent("testGUN", snapshot) + + copyTimestampKey(t, kdb, store, "testGUN") + updates, err = validateUpdate(cs, "testGUN", updates, store) + assert.Error(t, err) +} + +func TestValidateSnapshotGenerateNoTargets(t *testing.T) { + kdb, repo, cs := testutils.EmptyRepo() + store := storage.NewMemStorage() + snapRole := kdb.GetRole(data.CanonicalSnapshotRole) + + for _, id := range snapRole.KeyIDs { + k := kdb.GetKey(id) + assert.NotNil(t, k) + err := store.SetKey("testGUN", data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) + assert.NoError(t, err) + } + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, _, _, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + updates := []storage.MetaUpdate{root} + + copyTimestampKey(t, kdb, store, "testGUN") + updates, err = validateUpdate(cs, "testGUN", updates, store) + assert.Error(t, err) +} + +func TestValidateSnapshotGenerateLoadRootTargets(t *testing.T) { + kdb, repo, cs := testutils.EmptyRepo() + store := storage.NewMemStorage() + snapRole := kdb.GetRole(data.CanonicalSnapshotRole) + + for _, id := range snapRole.KeyIDs { + k := kdb.GetKey(id) + assert.NotNil(t, k) + err := store.SetKey("testGUN", data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) + assert.NoError(t, err) + } + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, targets, _, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + updates := []storage.MetaUpdate{} + + store.UpdateCurrent("testGUN", root) + store.UpdateCurrent("testGUN", targets) + + copyTimestampKey(t, kdb, store, "testGUN") + updates, err = validateUpdate(cs, "testGUN", updates, store) + assert.NoError(t, err) +} + +func TestPrepRepoLoadRootTargets(t *testing.T) { + _, repo, _ := testutils.EmptyRepo() + store := storage.NewMemStorage() + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, targets, _, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + store.UpdateCurrent("testGUN", root) + store.UpdateCurrent("testGUN", targets) + + toPrep := tuf.NewRepo(keys.NewDB(), nil) + assert.Nil(t, toPrep.Root) + assert.Nil(t, toPrep.Targets[data.CanonicalTargetsRole]) + err = prepRepo("testGUN", toPrep, store) + assert.NoError(t, err) + assert.NotNil(t, toPrep.Root) + assert.NotNil(t, toPrep.Targets[data.CanonicalTargetsRole]) +} + +func TestPrepRepoLoadRootCorrupt(t *testing.T) { + _, repo, _ := testutils.EmptyRepo() + store := storage.NewMemStorage() + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, targets, _, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + root.Data = root.Data[:1] + store.UpdateCurrent("testGUN", root) + store.UpdateCurrent("testGUN", targets) + + toPrep := tuf.NewRepo(keys.NewDB(), nil) + err = prepRepo("testGUN", toPrep, store) + assert.Error(t, err) +} + +func TestPrepRepoLoadTargetsCorrupt(t *testing.T) { + _, repo, _ := testutils.EmptyRepo() + store := storage.NewMemStorage() + + r, tg, sn, ts, err := testutils.Sign(repo) + assert.NoError(t, err) + root, targets, _, _, err := getUpdates(r, tg, sn, ts) + assert.NoError(t, err) + + targets.Data = targets.Data[:1] + store.UpdateCurrent("testGUN", root) + store.UpdateCurrent("testGUN", targets) + + toPrep := tuf.NewRepo(keys.NewDB(), nil) + err = prepRepo("testGUN", toPrep, store) + assert.Error(t, err) +} + +func TestPrepRepoLoadRootMissing(t *testing.T) { + store := storage.NewMemStorage() + + toPrep := tuf.NewRepo(nil, nil) + err := prepRepo("testGUN", toPrep, store) + assert.Error(t, err) +} + // If there is no timestamp key in the store, validation fails. This could // happen if pushing an existing repository from one server to another that // does not have the repo. func TestValidateRootNoTimestampKey(t *testing.T) { - _, oldRepo, _ := testutils.EmptyRepo() + _, oldRepo, cs := testutils.EmptyRepo() r, tg, sn, ts, err := testutils.Sign(oldRepo) assert.NoError(t, err) @@ -249,7 +461,7 @@ func TestValidateRootNoTimestampKey(t *testing.T) { assert.IsType(t, &storage.ErrNoKey{}, err) // do not copy the targets key to the storage, and try to update the root - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) @@ -264,7 +476,7 @@ func TestValidateRootNoTimestampKey(t *testing.T) { // repository from one server to another that had already initialized the same // repo. func TestValidateRootInvalidTimestampKey(t *testing.T) { - _, oldRepo, _ := testutils.EmptyRepo() + _, oldRepo, cs := testutils.EmptyRepo() r, tg, sn, ts, err := testutils.Sign(oldRepo) assert.NoError(t, err) @@ -279,14 +491,14 @@ func TestValidateRootInvalidTimestampKey(t *testing.T) { err = store.SetKey("testGUN", data.CanonicalRootRole, key.Algorithm(), key.Public()) assert.NoError(t, err) - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) } // If the timestamp role has a threshold > 1, validation fails. func TestValidateRootInvalidTimestampThreshold(t *testing.T) { - kdb, oldRepo, _ := testutils.EmptyRepo() + kdb, oldRepo, cs := testutils.EmptyRepo() tsRole, ok := oldRepo.Root.Signed.Roles[data.CanonicalTimestampRole] assert.True(t, ok) tsRole.Threshold = 2 @@ -300,7 +512,7 @@ func TestValidateRootInvalidTimestampThreshold(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.Contains(t, err.Error(), "timestamp role has invalid threshold") } @@ -308,7 +520,7 @@ func TestValidateRootInvalidTimestampThreshold(t *testing.T) { // If any role has a threshold < 1, validation fails func TestValidateRootInvalidZeroThreshold(t *testing.T) { for role := range data.ValidRoles { - kdb, oldRepo, _ := testutils.EmptyRepo() + kdb, oldRepo, cs := testutils.EmptyRepo() tsRole, ok := oldRepo.Root.Signed.Roles[role] assert.True(t, ok) tsRole.Threshold = 0 @@ -322,7 +534,7 @@ func TestValidateRootInvalidZeroThreshold(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.Contains(t, err.Error(), "role has invalid threshold") } @@ -332,7 +544,7 @@ func TestValidateRootInvalidZeroThreshold(t *testing.T) { // These tests remove a role from the Root file and // check for a ErrBadRoot func TestValidateRootRoleMissing(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, "root") @@ -345,13 +557,13 @@ func TestValidateRootRoleMissing(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) } func TestValidateTargetsRoleMissing(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, "targets") @@ -364,13 +576,13 @@ func TestValidateTargetsRoleMissing(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) } func TestValidateSnapshotRoleMissing(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, "snapshot") @@ -383,7 +595,7 @@ func TestValidateSnapshotRoleMissing(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) } @@ -392,7 +604,7 @@ func TestValidateSnapshotRoleMissing(t *testing.T) { // ### Signature missing negative tests ### func TestValidateRootSigMissing(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, "snapshot") @@ -408,13 +620,13 @@ func TestValidateRootSigMissing(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) } func TestValidateTargetsSigMissing(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -428,13 +640,13 @@ func TestValidateTargetsSigMissing(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadTargets{}, err) } func TestValidateSnapshotSigMissing(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -448,7 +660,7 @@ func TestValidateSnapshotSigMissing(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadSnapshot{}, err) } @@ -457,7 +669,7 @@ func TestValidateSnapshotSigMissing(t *testing.T) { // ### Corrupted metadata negative tests ### func TestValidateRootCorrupt(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -471,13 +683,13 @@ func TestValidateRootCorrupt(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) } func TestValidateTargetsCorrupt(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -491,13 +703,13 @@ func TestValidateTargetsCorrupt(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadTargets{}, err) } func TestValidateSnapshotCorrupt(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -511,7 +723,7 @@ func TestValidateSnapshotCorrupt(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadSnapshot{}, err) } @@ -520,7 +732,7 @@ func TestValidateSnapshotCorrupt(t *testing.T) { // ### Snapshot size mismatch negative tests ### func TestValidateRootModifiedSize(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -538,13 +750,13 @@ func TestValidateRootModifiedSize(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadRoot{}, err) } func TestValidateTargetsModifiedSize(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -559,7 +771,7 @@ func TestValidateTargetsModifiedSize(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadSnapshot{}, err) } @@ -568,7 +780,7 @@ func TestValidateTargetsModifiedSize(t *testing.T) { // ### Snapshot hash mismatch negative tests ### func TestValidateRootModifiedHash(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -587,13 +799,13 @@ func TestValidateRootModifiedHash(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadSnapshot{}, err) } func TestValidateTargetsModifiedHash(t *testing.T) { - kdb, repo, _ := testutils.EmptyRepo() + kdb, repo, cs := testutils.EmptyRepo() store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) @@ -612,7 +824,7 @@ func TestValidateTargetsModifiedHash(t *testing.T) { updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} copyTimestampKey(t, kdb, store, "testGUN") - err = validateUpdate("testGUN", updates, store) + _, err = validateUpdate(cs, "testGUN", updates, store) assert.Error(t, err) assert.IsType(t, ErrBadSnapshot{}, err) } diff --git a/server/snapshot/snapshot.go b/server/snapshot/snapshot.go new file mode 100644 index 0000000000..eb909cda74 --- /dev/null +++ b/server/snapshot/snapshot.go @@ -0,0 +1,161 @@ +package snapshot + +import ( + "bytes" + "encoding/json" + "github.com/Sirupsen/logrus" + + "github.com/docker/notary/server/storage" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" +) + +// GetOrCreateSnapshotKey either creates a new snapshot key, or returns +// the existing one. Only the PublicKey is returned. The private part +// is held by the CryptoService. +func GetOrCreateSnapshotKey(gun string, store storage.KeyStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { + keyAlgorithm, public, err := store.GetKey(gun, data.CanonicalSnapshotRole) + if err == nil { + return data.NewPublicKey(keyAlgorithm, public), nil + } + + if _, ok := err.(*storage.ErrNoKey); ok { + key, err := crypto.Create("snapshot", createAlgorithm) + if err != nil { + return nil, err + } + logrus.Debug("Creating new snapshot key for ", gun, ". With algo: ", key.Algorithm()) + err = store.SetKey(gun, data.CanonicalSnapshotRole, key.Algorithm(), key.Public()) + if err == nil { + return key, nil + } + + if _, ok := err.(*storage.ErrKeyExists); ok { + keyAlgorithm, public, err = store.GetKey(gun, data.CanonicalSnapshotRole) + if err != nil { + return nil, err + } + return data.NewPublicKey(keyAlgorithm, public), nil + } + return nil, err + } + return nil, err +} + +// GetOrCreateSnapshot either returns the exisiting latest snapshot, or uses +// whatever the most recent snapshot is to generate a new one. +func GetOrCreateSnapshot(gun string, store storage.MetaStore, cryptoService signed.CryptoService) ([]byte, error) { + + d, err := store.GetCurrent(gun, "snapshot") + if err != nil { + return nil, err + } + + sn := &data.SignedSnapshot{} + if d != nil { + err := json.Unmarshal(d, sn) + if err != nil { + logrus.Error("Failed to unmarshal existing snapshot") + return nil, err + } + if !snapshotExpired(sn) && !contentExpired(gun, sn, store) { + return d, nil + } + } + + sgnd, version, err := createSnapshot(gun, sn, store, cryptoService) + if err != nil { + logrus.Error("Failed to create a new snapshot") + return nil, err + } + out, err := json.Marshal(sgnd) + if err != nil { + logrus.Error("Failed to marshal new snapshot") + return nil, err + } + err = store.UpdateCurrent(gun, storage.MetaUpdate{Role: "snapshot", Version: version, Data: out}) + if err != nil { + return nil, err + } + return out, nil +} + +// snapshotExpired simply checks if the snapshot is past its expiry time +func snapshotExpired(sn *data.SignedSnapshot) bool { + return signed.IsExpired(sn.Signed.Expires) +} + +// contentExpired checks to see if any of the roles already in the snapshot +// have been updated. It will update any roles that have changed as it goes +// so that we don't have to run through all this again a second time. +func contentExpired(gun string, sn *data.SignedSnapshot, store storage.MetaStore) bool { + expired := false + updatedMeta := make(data.Files) + for role, meta := range sn.Signed.Meta { + curr, err := store.GetCurrent(gun, role) + if err != nil { + return false + } + roleExp, newHash := roleExpired(curr, meta) + if roleExp { + updatedMeta[role] = data.FileMeta{ + Length: int64(len(curr)), + Hashes: data.Hashes{ + "sha256": newHash, + }, + } + } + expired = expired || roleExp + } + if expired { + sn.Signed.Meta = updatedMeta + } + return expired +} + +// roleExpired checks if the content for a specific role differs from +// the snapshot +func roleExpired(roleData []byte, meta data.FileMeta) (bool, []byte) { + currMeta, err := data.NewFileMeta(bytes.NewReader(roleData), "sha256") + if err != nil { + // if we can't generate FileMeta from the current roleData, we should + // continue to serve the old role if it isn't time expired + // because we won't be able to generate a new one. + return false, nil + } + hash := currMeta.Hashes["sha256"] + return !bytes.Equal(hash, meta.Hashes["sha256"]), hash +} + +// createSnapshot uses an existing snapshot to create a new one. +// Important things to be aware of: +// - It requires that a snapshot already exists. We create snapshots +// on upload so there should always be an existing snapshot if this +// gets called. +// - It doesn't update what roles are present in the snapshot, as those +// were validated during upload. We also updated the hashes of the +// already present roles as part of our checks on whether we could +// serve the previous version of the snapshot +func createSnapshot(gun string, sn *data.SignedSnapshot, store storage.MetaStore, cryptoService signed.CryptoService) (*data.Signed, int, error) { + algorithm, public, err := store.GetKey(gun, data.CanonicalSnapshotRole) + if err != nil { + // owner of gun must have generated a snapshot key otherwise + // we won't proceed with generating everything. + return nil, 0, err + } + key := data.NewPublicKey(algorithm, public) + + // update version and expiry + sn.Signed.Version = sn.Signed.Version + 1 + sn.Signed.Expires = data.DefaultExpires(data.CanonicalSnapshotRole) + + out, err := sn.ToSigned() + if err != nil { + return nil, 0, err + } + err = signed.Sign(cryptoService, out, key) + if err != nil { + return nil, 0, err + } + return out, sn.Signed.Version, nil +} diff --git a/server/snapshot/snapshot_test.go b/server/snapshot/snapshot_test.go new file mode 100644 index 0000000000..f243126da4 --- /dev/null +++ b/server/snapshot/snapshot_test.go @@ -0,0 +1,213 @@ +package snapshot + +import ( + "bytes" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/docker/notary/server/storage" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" +) + +func TestSnapshotExpired(t *testing.T) { + sn := &data.SignedSnapshot{ + Signatures: nil, + Signed: data.Snapshot{ + Expires: time.Now().AddDate(-1, 0, 0), + }, + } + assert.True(t, snapshotExpired(sn), "Snapshot should have expired") +} + +func TestSnapshotNotExpired(t *testing.T) { + sn := &data.SignedSnapshot{ + Signatures: nil, + Signed: data.Snapshot{ + Expires: time.Now().AddDate(1, 0, 0), + }, + } + assert.False(t, snapshotExpired(sn), "Snapshot should NOT have expired") +} + +func TestGetSnapshotKeyCreate(t *testing.T) { + store := storage.NewMemStorage() + crypto := signed.NewEd25519() + k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + assert.Nil(t, err, "Expected nil error") + assert.NotNil(t, k, "Key should not be nil") + + k2, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + + assert.Nil(t, err, "Expected nil error") + + // trying to get the same key again should return the same value + assert.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") + assert.NotNil(t, k2, "Key should not be nil") +} + +func TestGetSnapshotKeyExisting(t *testing.T) { + store := storage.NewMemStorage() + crypto := signed.NewEd25519() + key, err := crypto.Create(data.CanonicalSnapshotRole, data.ED25519Key) + assert.NoError(t, err) + + store.SetKey("gun", data.CanonicalSnapshotRole, data.ED25519Key, key.Public()) + + k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + assert.Nil(t, err, "Expected nil error") + assert.NotNil(t, k, "Key should not be nil") + assert.Equal(t, key, k, "Did not receive same key when attempting to recreate.") + assert.NotNil(t, k, "Key should not be nil") + + k2, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + + assert.Nil(t, err, "Expected nil error") + + // trying to get the same key again should return the same value + assert.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") + assert.NotNil(t, k2, "Key should not be nil") +} + +type keyStore struct { + getCalled bool + k data.PublicKey +} + +func (ks *keyStore) GetKey(gun, role string) (string, []byte, error) { + defer func() { ks.getCalled = true }() + if ks.getCalled { + return ks.k.Algorithm(), ks.k.Public(), nil + } + return "", nil, &storage.ErrNoKey{} +} + +func (ks keyStore) SetKey(gun, role, algorithm string, public []byte) error { + return &storage.ErrKeyExists{} +} + +// Tests the race condition where the server is being asked to generate a new key +// by 2 parallel requests and the second insert to be executed by the DB fails +// due to duplicate key (gun + role). It should then return the key added by the +// first insert. +func TestGetSnapshotKeyExistsOnSet(t *testing.T) { + crypto := signed.NewEd25519() + key, err := crypto.Create(data.CanonicalSnapshotRole, data.ED25519Key) + assert.NoError(t, err) + store := &keyStore{k: key} + + k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + assert.Nil(t, err, "Expected nil error") + assert.NotNil(t, k, "Key should not be nil") + assert.Equal(t, key, k, "Did not receive same key when attempting to recreate.") + assert.NotNil(t, k, "Key should not be nil") + + k2, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + + assert.Nil(t, err, "Expected nil error") + + // trying to get the same key again should return the same value + assert.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") + assert.NotNil(t, k2, "Key should not be nil") +} + +func TestRoleExpired(t *testing.T) { + meta := data.FileMeta{ + Hashes: data.Hashes{ + "sha256": []byte{1}, + }, + } + newData := []byte{2} + res, _ := roleExpired(newData, meta) + assert.True(t, res) +} + +func TestRoleNotExpired(t *testing.T) { + newData := []byte{2} + currMeta, err := data.NewFileMeta(bytes.NewReader(newData), "sha256") + assert.NoError(t, err) + + meta := data.FileMeta{ + Hashes: data.Hashes{ + "sha256": currMeta.Hashes["sha256"], + }, + } + + res, _ := roleExpired(newData, meta) + assert.False(t, res) +} + +func TestGetSnapshotNotExists(t *testing.T) { + store := storage.NewMemStorage() + crypto := signed.NewEd25519() + + _, err := GetOrCreateSnapshot("gun", store, crypto) + assert.Error(t, err) +} + +func TestGetSnapshotCurrValid(t *testing.T) { + store := storage.NewMemStorage() + crypto := signed.NewEd25519() + + _, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + + newData := []byte{2} + currMeta, err := data.NewFileMeta(bytes.NewReader(newData), "sha256") + assert.NoError(t, err) + + snapshot := &data.SignedSnapshot{ + Signed: data.Snapshot{ + Expires: data.DefaultExpires(data.CanonicalSnapshotRole), + Meta: data.Files{ + data.CanonicalRootRole: currMeta, + }, + }, + } + snapJSON, _ := json.Marshal(snapshot) + + // test when db is missing the role data + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "snapshot", Version: 0, Data: snapJSON}) + _, err = GetOrCreateSnapshot("gun", store, crypto) + assert.NoError(t, err) + + // test when db has the role data + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "root", Version: 0, Data: newData}) + _, err = GetOrCreateSnapshot("gun", store, crypto) + assert.NoError(t, err) + + // test when db role data is expired + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "root", Version: 1, Data: []byte{3}}) + _, err = GetOrCreateSnapshot("gun", store, crypto) + assert.NoError(t, err) +} + +func TestGetSnapshotCurrExpired(t *testing.T) { + store := storage.NewMemStorage() + crypto := signed.NewEd25519() + + _, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + + snapshot := &data.SignedSnapshot{} + snapJSON, _ := json.Marshal(snapshot) + + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "snapshot", Version: 0, Data: snapJSON}) + _, err = GetOrCreateSnapshot("gun", store, crypto) + assert.NoError(t, err) +} + +func TestGetSnapshotCurrCorrupt(t *testing.T) { + store := storage.NewMemStorage() + crypto := signed.NewEd25519() + + _, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + + snapshot := &data.SignedSnapshot{} + snapJSON, _ := json.Marshal(snapshot) + + store.UpdateCurrent("gun", storage.MetaUpdate{Role: "snapshot", Version: 0, Data: snapJSON[1:]}) + _, err = GetOrCreateSnapshot("gun", store, crypto) + assert.Error(t, err) +} diff --git a/server/storage/interface.go b/server/storage/interface.go index 634f53ba25..4651215928 100644 --- a/server/storage/interface.go +++ b/server/storage/interface.go @@ -1,5 +1,16 @@ package storage +// KeyStore provides a minimal interface for managing key persistence +type KeyStore interface { + // GetKey returns the algorithm and public key for the given GUN and role. + // If the GUN+role don't exist, returns an error. + GetKey(gun, role string) (algorithm string, public []byte, err error) + + // SetKey sets the algorithm and public key for the given GUN and role if + // it doesn't already exist. Otherwise an error is returned. + SetKey(gun, role, algorithm string, public []byte) error +} + // MetaStore holds the methods that are used for a Metadata Store type MetaStore interface { // UpdateCurrent adds new metadata version for the given GUN if and only @@ -22,11 +33,5 @@ type MetaStore interface { // error if no metadata exists for the given GUN. Delete(gun string) error - // GetKey returns the algorithm and public key for the given GUN and role. - // If the GUN+role don't exist, returns an error. - GetKey(gun, role string) (algorithm string, public []byte, err error) - - // SetKey sets the algorithm and public key for the given GUN and role if - // it doesn't already exist. Otherwise an error is returned. - SetKey(gun, role, algorithm string, public []byte) error + KeyStore } diff --git a/server/timestamp/timestamp.go b/server/timestamp/timestamp.go index f190a24d9a..7621cbf87a 100644 --- a/server/timestamp/timestamp.go +++ b/server/timestamp/timestamp.go @@ -16,14 +16,14 @@ import ( // found. It attempts to handle the race condition that may occur if 2 servers try to // create the key at the same time by simply querying the store a second time if it // receives a conflict when writing. -func GetOrCreateTimestampKey(gun string, store storage.MetaStore, crypto signed.CryptoService, fallBackAlgorithm string) (data.PublicKey, error) { +func GetOrCreateTimestampKey(gun string, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { keyAlgorithm, public, err := store.GetKey(gun, data.CanonicalTimestampRole) if err == nil { return data.NewPublicKey(keyAlgorithm, public), nil } if _, ok := err.(*storage.ErrNoKey); ok { - key, err := crypto.Create("timestamp", fallBackAlgorithm) + key, err := crypto.Create("timestamp", createAlgorithm) if err != nil { return nil, err } diff --git a/server/timestamp/timestamp_test.go b/server/timestamp/timestamp_test.go index 9ffe335720..acdf7fe97c 100644 --- a/server/timestamp/timestamp_test.go +++ b/server/timestamp/timestamp_test.go @@ -20,7 +20,10 @@ func TestTimestampExpired(t *testing.T) { }, } assert.True(t, timestampExpired(ts), "Timestamp should have expired") - ts = &data.SignedTimestamp{ +} + +func TestTimestampNotExpired(t *testing.T) { + ts := &data.SignedTimestamp{ Signatures: nil, Signed: data.Timestamp{ Expires: time.Now().AddDate(1, 0, 0), @@ -84,7 +87,7 @@ func TestGetTimestampNewSnapshot(t *testing.T) { store.UpdateCurrent("gun", storage.MetaUpdate{Role: "snapshot", Version: 1, Data: snapJSON}) ts2, err := GetOrCreateTimestamp("gun", store, crypto) - assert.Nil(t, err, "GetTimestamp errored") + assert.NoError(t, err, "GetTimestamp errored") assert.NotEqual(t, ts1, ts2, "Timestamp was not regenerated when snapshot changed") }