diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d59d55ef7f..bfc4599377 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -106,7 +106,7 @@ }, { "ImportPath": "github.com/endophage/gotuf", - "Rev": "9bcdad0308e34a49f38448b8ad436ad8860825ce" + "Rev": "338013a6720b3dfe4e4e9d1368109ea314c936d3" }, { "ImportPath": "github.com/go-sql-driver/mysql", diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/data/keys.go b/Godeps/_workspace/src/github.com/endophage/gotuf/data/keys.go index 3df1ce05c7..eccccc420d 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/data/keys.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/data/keys.go @@ -71,7 +71,7 @@ func (k TUFKey) Public() []byte { return k.Value.Public } -func (k *TUFKey) Private() []byte { +func (k TUFKey) Private() []byte { return k.Value.Private } diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/data/roles.go b/Godeps/_workspace/src/github.com/endophage/gotuf/data/roles.go index d3047d7844..cf4446dc62 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/data/roles.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/data/roles.go @@ -88,6 +88,7 @@ type Role struct { Name string `json:"name"` Paths []string `json:"paths,omitempty"` PathHashPrefixes []string `json:"path_hash_prefixes,omitempty"` + Email string `json:"email,omitempty"` } func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []string) (*Role, error) { diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go b/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go index 4d226acebb..8e480ec79b 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go @@ -71,24 +71,46 @@ func NewTufRepo(keysDB *keys.KeyDB, cryptoService signed.CryptoService) *TufRepo } // AddBaseKeys is used to add keys to the role in root.json -func (tr *TufRepo) AddBaseKeys(role string, keys ...*data.TUFKey) error { +func (tr *TufRepo) AddBaseKeys(role string, keys ...data.PublicKey) error { if tr.Root == nil { return ErrNotLoaded{role: "root"} } + ids := []string{} for _, k := range keys { // Store only the public portion - pubKey := *k - pubKey.Value.Private = nil - tr.Root.Signed.Keys[pubKey.ID()] = &pubKey - tr.keysDB.AddKey(&pubKey) + pubKey := data.NewPrivateKey(k.Algorithm(), k.Public(), nil) + tr.Root.Signed.Keys[pubKey.ID()] = pubKey + tr.keysDB.AddKey(k) tr.Root.Signed.Roles[role].KeyIDs = append(tr.Root.Signed.Roles[role].KeyIDs, pubKey.ID()) + ids = append(ids, pubKey.ID()) } + r, err := data.NewRole( + role, + tr.Root.Signed.Roles[role].Threshold, + ids, + nil, + nil, + ) + if err != nil { + return err + } + tr.keysDB.AddRole(r) tr.Root.Dirty = true return nil } -// RemoveKeys is used to remove keys from the roles in root.json +// ReplaceBaseKeys is used to replace all keys for the given role with the new keys +func (tr *TufRepo) ReplaceBaseKeys(role string, keys ...data.PublicKey) error { + r := tr.keysDB.GetRole(role) + err := tr.RemoveBaseKeys(role, r.KeyIDs...) + if err != nil { + return err + } + return tr.AddBaseKeys(role, keys...) +} + +// RemoveBaseKeys is used to remove keys from the roles in root.json func (tr *TufRepo) RemoveBaseKeys(role string, keyIDs ...string) error { if tr.Root == nil { return ErrNotLoaded{role: "root"} diff --git a/client/changelist/change.go b/client/changelist/change.go index 867c230517..dfdaed5c36 100644 --- a/client/changelist/change.go +++ b/client/changelist/change.go @@ -1,5 +1,9 @@ package changelist +import ( + "github.com/endophage/gotuf/data" +) + // Scopes for TufChanges are simply the TUF roles. // Unfortunately because of targets delegations, we can only // cover the base roles. @@ -10,6 +14,17 @@ const ( ScopeTimestamp = "timestamp" ) +// Types for TufChanges are namespaced by the Role they +// are relevant for. The Root and Targets roles are the +// only ones for which user action can cause a change, as +// all changes in Snapshot and Timestamp are programatically +// generated base on Root and Targets changes. +const ( + TypeRootRole = "role" + TypeTargetsTarget = "target" + TypeTargetsDelegation = "delegation" +) + // TufChange represents a change to a TUF repo type TufChange struct { // Abbreviated because Go doesn't permit a field and method of the same name @@ -20,6 +35,13 @@ type TufChange struct { Data []byte `json:"data"` } +// TufRootData represents a modification of the keys associated +// with a role that appears in the root.json +type TufRootData struct { + Keys []data.TUFKey `json:"keys"` + RoleName string `json:"role"` +} + // NewTufChange initializes a tufChange object func NewTufChange(action string, role, changeType, changePath string, content []byte) *TufChange { return &TufChange{ diff --git a/client/client.go b/client/client.go index ce7a519af1..ca57a89f1a 100644 --- a/client/client.go +++ b/client/client.go @@ -245,6 +245,7 @@ func (r *NotaryRepository) AddTarget(target *Target) error { if err != nil { return err } + defer cl.Close() logrus.Debugf("Adding target \"%s\" with sha256 \"%x\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length) meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes} @@ -258,7 +259,7 @@ func (r *NotaryRepository) AddTarget(target *Target) error { if err != nil { return err } - return cl.Close() + return nil } // RemoveTarget creates a new changelist entry to remove a target from the repository @@ -604,3 +605,55 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) { r.fileStore, ), nil } + +// RotateKeys removes all existing keys associated with role and adds +// the keys specified by keyIDs to the role. These changes are staged +// in a changelist until publish is called. +func (r *NotaryRepository) RotateKeys() error { + for _, role := range []string{"targets", "snapshot"} { + key, err := r.cryptoService.Create(role, data.ECDSAKey) + if err != nil { + return err + } + err = r.rootFileKeyChange(role, changelist.ActionCreate, key) + if err != nil { + return err + } + } + return nil +} + +func (r *NotaryRepository) rootFileKeyChange(role, action string, key data.PublicKey) error { + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + k, ok := key.(*data.TUFKey) + if !ok { + return errors.New("Invalid key type found during rotation.") + } + + meta := changelist.TufRootData{ + RoleName: role, + Keys: []data.TUFKey{*k}, + } + metaJSON, err := json.Marshal(meta) + if err != nil { + return err + } + + c := changelist.NewTufChange( + action, + changelist.ScopeRoot, + changelist.TypeRootRole, + role, + metaJSON, + ) + err = cl.Add(c) + if err != nil { + return err + } + return nil +} diff --git a/client/client_test.go b/client/client_test.go index 082dd0bfd6..d18e8825b6 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -633,3 +633,95 @@ func testPublish(t *testing.T, rootType data.KeyAlgorithm) { assert.NoError(t, err) assert.Equal(t, currentTarget, newCurrentTarget, "current target does not match") } + +func TestRotate(t *testing.T) { + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + + assert.NoError(t, err, "failed to create a temporary directory: %s", err) + + gun := "docker.com/notary" + + // Set up server + ctx := context.WithValue(context.Background(), "metaStore", storage.NewMemStorage()) + + // Do not pass one of the const KeyAlgorithms here as the value! Passing a + // string is in itself good test that we are handling it correctly as we will + // be receiving a string from the configuration. + ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa") + + hand := utils.RootHandlerFactory(nil, ctx, + cryptoservice.NewCryptoService("", trustmanager.NewKeyMemoryStore(passphraseRetriever))) + + r := mux.NewRouter() + r.Methods("POST").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/").Handler(hand(handlers.AtomicUpdateHandler, "push", "pull")) + r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/{tufRole:(root|targets|snapshot)}.json").Handler(hand(handlers.GetHandler, "pull")) + r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/timestamp.json").Handler(hand(handlers.GetTimestampHandler, "pull")) + r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/timestamp.key").Handler(hand(handlers.GetTimestampKeyHandler, "push", "pull")) + //r.Methods("POST").Path("/v2/{imageName:" + server.RepositoryNameRegexp + "}/_trust/tuf/{tufRole:(root|targets|timestamp|snapshot)}.json").Handler(hand(handlers.UpdateHandler, "push", "pull")) + r.Methods("DELETE").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/").Handler(hand(handlers.DeleteHandler, "push", "pull")) + + ts := httptest.NewServer(r) + + repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) + assert.NoError(t, err, "error creating repository: %s", err) + + rootKeyID, err := repo.KeyStoreManager.GenRootKey(data.ECDSAKey.String()) + assert.NoError(t, err, "error generating root key: %s", err) + + rootCryptoService, err := repo.KeyStoreManager.GetRootCryptoService(rootKeyID) + assert.NoError(t, err, "error retreiving root key: %s", err) + + err = repo.Initialize(rootCryptoService) + assert.NoError(t, err, "error creating repository: %s", err) + + // Add fixtures/intermediate-ca.crt as a target. There's no particular reason + // for using this file except that it happens to be available as + // a fixture. + // Adding a target will allow us to confirm the repository is still valid after + // rotating the keys. + latestTarget, err := NewTarget("latest", "../fixtures/intermediate-ca.crt") + assert.NoError(t, err, "error creating target") + err = repo.AddTarget(latestTarget) + assert.NoError(t, err, "error adding target") + + // Publish + err = repo.Publish() + assert.NoError(t, err) + + // Get root.json and capture targets + snapshot key IDs + repo.GetTargetByName("latest") // force a pull + targetsKeyIDs := repo.tufRepo.Root.Signed.Roles["targets"].KeyIDs + snapshotKeyIDs := repo.tufRepo.Root.Signed.Roles["snapshot"].KeyIDs + assert.Len(t, targetsKeyIDs, 1) + assert.Len(t, snapshotKeyIDs, 1) + + // Do rotation + repo.RotateKeys() + + // Publish + err = repo.Publish() + assert.NoError(t, err) + + // Get root.json. Check targets + snapshot keys have changed + // and that they match those found in the changelist. + _, err = repo.GetTargetByName("latest") // force a pull + assert.NoError(t, err) + newTargetsKeyIDs := repo.tufRepo.Root.Signed.Roles["targets"].KeyIDs + newSnapshotKeyIDs := repo.tufRepo.Root.Signed.Roles["snapshot"].KeyIDs + assert.Len(t, newTargetsKeyIDs, 1) + assert.Len(t, newSnapshotKeyIDs, 1) + assert.NotEqual(t, targetsKeyIDs[0], newTargetsKeyIDs[0]) + assert.NotEqual(t, snapshotKeyIDs[0], newSnapshotKeyIDs[0]) + + // Confirm changelist dir empty after publishing changes + // Look for the changelist file + changelistDirPath := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist") + changelistDir, err := os.Open(changelistDirPath) + assert.NoError(t, err, "could not open changelist directory") + fileInfos, err := changelistDir.Readdir(0) + assert.NoError(t, err, "could not read changelist directory") + // Should only be one file in the directory + assert.Len(t, fileInfos, 0, "wrong number of changelist files found") +} diff --git a/client/helpers.go b/client/helpers.go index 476ef08b71..50be86c6ce 100644 --- a/client/helpers.go +++ b/client/helpers.go @@ -7,7 +7,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/notary/client/changelist" - "github.com/endophage/gotuf" + tuf "github.com/endophage/gotuf" "github.com/endophage/gotuf/data" "github.com/endophage/gotuf/keys" "github.com/endophage/gotuf/store" @@ -38,14 +38,16 @@ func applyChangelist(repo *tuf.TufRepo, cl changelist.Changelist) error { } switch c.Scope() { case changelist.ScopeTargets: - err := applyTargetsChange(repo, c) - if err != nil { - return err - } + err = applyTargetsChange(repo, c) + case changelist.ScopeRoot: + err = applyRootChange(repo, c) default: logrus.Debug("scope not supported: ", c.Scope()) } index++ + if err != nil { + return err + } } logrus.Debugf("applied %d change(s)", index) return nil @@ -75,6 +77,40 @@ func applyTargetsChange(repo *tuf.TufRepo, c changelist.Change) error { return nil } +func applyRootChange(repo *tuf.TufRepo, c changelist.Change) error { + var err error + switch c.Type() { + case changelist.TypeRootRole: + err = applyRootRoleChange(repo, c) + default: + logrus.Debug("type of root change not yet supported: ", c.Type()) + } + return err // might be nil +} + +func applyRootRoleChange(repo *tuf.TufRepo, c changelist.Change) error { + switch c.Action() { + case changelist.ActionCreate: + // replaces all keys for a role + d := &changelist.TufRootData{} + err := json.Unmarshal(c.Content(), d) + if err != nil { + return err + } + k := []data.PublicKey{} + for _, key := range d.Keys { + k = append(k, data.NewPublicKey(key.Algorithm(), key.Public())) + } + err = repo.ReplaceBaseKeys(d.RoleName, k...) + if err != nil { + return err + } + default: + logrus.Debug("action not yet supported for root: ", c.Action()) + } + return nil +} + func nearExpiry(r *data.SignedRoot) bool { plus6mo := time.Now().AddDate(0, 6, 0) return r.Signed.Expires.Before(plus6mo) diff --git a/cmd/notary-server/dev-config.json b/cmd/notary-server/dev-config.json index 62c22ccafc..a70ced6b18 100644 --- a/cmd/notary-server/dev-config.json +++ b/cmd/notary-server/dev-config.json @@ -15,6 +15,6 @@ }, "storage": { "backend": "mysql", - "db_url": "dockercondemo:dockercondemo@tcp(localhost:3306)/dockercondemo" + "db_url": "root@tcp(localhost:3306)/notary" } } diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 8b5a09b03e..0002fa4cd8 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + notaryclient "github.com/docker/notary/client" "github.com/docker/notary/keystoremanager" "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/trustmanager" @@ -29,6 +30,7 @@ func init() { cmdKeyExportRoot.Flags().BoolVarP(&keysExportRootChangePassphrase, "change-passphrase", "c", false, "set a new passphrase for the key being exported") cmdKey.AddCommand(cmdKeyImport) cmdKey.AddCommand(cmdKeyImportRoot) + cmdKey.AddCommand(cmdRotateKey) } var cmdKey = &cobra.Command{ @@ -44,6 +46,13 @@ var cmdKeyList = &cobra.Command{ Run: keysList, } +var cmdRotateKey = &cobra.Command{ + Use: "rotate [ GUN ]", + Short: "Rotate all keys for role.", + Long: "Removes all old keys for the given role and generates 1 new key.", + Run: keysRotate, +} + var keyRemoveGUN string var keyRemoveRoot bool var keyRemoveYes bool @@ -375,3 +384,20 @@ func printKey(keyPath, alias string) { gun := filepath.Dir(keyPath) fmt.Printf("%s - %s - %s\n", gun, alias, keyID) } + +func keysRotate(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Usage() + fatalf("must specify a GUN and target") + } + parseConfig() + + gun := args[0] + nRepo, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, nil, retriever) + if err != nil { + fatalf(err.Error()) + } + if err := nRepo.RotateKeys(); err != nil { + fatalf(err.Error()) + } +}