From 83f96997d37d3dda9977f6a5ae7b674f93993b61 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 31 Jul 2015 10:27:36 -0700 Subject: [PATCH 1/6] changelist management of key rotation Signed-off-by: David Lawrence (github: endophage) --- client/changelist/change.go | 16 ++++++ client/helpers.go | 66 ++++++++++++++++++++-- cmd/notary-server/dev_w_signer.config.json | 21 +++++++ 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 cmd/notary-server/dev_w_signer.config.json diff --git a/client/changelist/change.go b/client/changelist/change.go index 867c230517..35c76aa520 100644 --- a/client/changelist/change.go +++ b/client/changelist/change.go @@ -10,6 +10,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 +31,11 @@ type TufChange struct { Data []byte `json:"data"` } +type TufRootData struct { + Keys []*data.TUFKey + RoleName string +} + // NewTufChange initializes a tufChange object func NewTufChange(action string, role, changeType, changePath string, content []byte) *TufChange { return &TufChange{ diff --git a/client/helpers.go b/client/helpers.go index 476ef08b71..069ae87052 100644 --- a/client/helpers.go +++ b/client/helpers.go @@ -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,62 @@ 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 *tufRepo, c changelist.Change) error { + switch c.Action() { + case changelist.ActionCreate: + // replaces all keys for a role + d := &changelist.TufRootData{} + err := json.Unmarshal(c.Data, d) + if err != nil { + return err + } + err = repo.ReplaceBaseKeys(d.RoleName, d.Keys...) + if err != nil { + return err + } + case changelist.ActionUpdate: + // adds a key to a role + d := &changelist.TufRootData{} + err := json.Unmarshal(c.Data, d) + if err != nil { + return err + } + err = repo.AddBaseKeys(d.RoleName, d.Keys...) + if err != nil { + return err + } + case changelist.ActionDelete: + // removes a key from a role + d := &changelist.TufRootData{} + err := json.Unmarshal(c.Data, d) + if err != nil { + return err + } + ids := make([]string, 0, len(d.Keys)) + for _, k := range d.Keys { + append(ids, k.ID()) + } + err = repo.RemoveBaseKeys(d.RoleName, ids...) + 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_w_signer.config.json b/cmd/notary-server/dev_w_signer.config.json new file mode 100644 index 0000000000..521c047334 --- /dev/null +++ b/cmd/notary-server/dev_w_signer.config.json @@ -0,0 +1,21 @@ +{ + "server": { + "addr": ":4443", + "tls_key_file": "./fixtures/notary-server.key", + "tls_cert_file": "./fixtures/notary-server.crt" + }, + "trust_service": { + "type": "remote", + "hostname": "localhost", + "port": "7899", + "tls_ca_file": "./fixtures/root-ca.crt", + "key_algorithm": "ecdsa" + }, + "logging": { + "level": "debug" + }, + "storage": { + "backend": "mysql", + "db_url": "root:@tcp(localhost:3306)/dockercondemo" + } +} From 959d0267ac26e5712b268548a19820a58c2ad9d7 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Tue, 4 Aug 2015 14:14:15 -0700 Subject: [PATCH 2/6] command skeletons in place, changelist actions implemented Signed-off-by: David Lawrence (github: endophage) --- client/changelist/change.go | 8 ++- client/client.go | 58 ++++++++++++++++++- client/helpers.go | 12 ++-- cmd/notary/main.go | 1 + cmd/notary/meta.go | 110 ++++++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 cmd/notary/meta.go diff --git a/client/changelist/change.go b/client/changelist/change.go index 35c76aa520..643f17d137 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. @@ -32,8 +36,8 @@ type TufChange struct { } type TufRootData struct { - Keys []*data.TUFKey - RoleName string + Keys []data.PublicKey `json:"keys"` + RoleName string `json:"role"` } // NewTufChange initializes a tufChange object diff --git a/client/client.go b/client/client.go index ce7a519af1..1e7257efcd 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,58 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) { r.fileStore, ), nil } + +// AddKeys adds the specified keyIDs to the role. These changes are +// staged in a changelist until publish is called. +func (r *NotaryRepository) AddKeys(role string, keyIDs ...string) error { + return r.rootKeyChange(role, changelist.ActionUpdate, keyIDs...) +} + +// RemoveKeys removes the specified keyIDs from the role. These changes +// are staged in a changelist until publish is called. +func (r *NotaryRepository) RemoveKeys(role string, keyIDs ...string) error { + return r.rootKeyChange(role, changelist.ActionDelete, keyIDs...) +} + +// ReplaceKey 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) ReplaceKeys(role string, keyIDs ...string) error { + return r.rootKeyChange(role, changelist.ActionCreate, keyIDs...) +} + +func (r *NotaryRepository) rootKeyChange(role, action string, keyIDs ...string) error { + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + keys := make([]data.PublicKey, 0, len(keyIDs)) + for _, kID := range keyIDs { + logrus.Debug(kID) + // get PUBLIC key and append it to keys + } + + meta := changelist.TufRootData{ + RoleName: role, + Keys: keys, + } + 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/helpers.go b/client/helpers.go index 069ae87052..7b4407d618 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" @@ -88,12 +88,12 @@ func applyRootChange(repo *tuf.TufRepo, c changelist.Change) error { return err // might be nil } -func applyRootRoleChange(repo *tufRepo, c changelist.Change) error { +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.Data, d) + err := json.Unmarshal(c.Content(), d) if err != nil { return err } @@ -104,7 +104,7 @@ func applyRootRoleChange(repo *tufRepo, c changelist.Change) error { case changelist.ActionUpdate: // adds a key to a role d := &changelist.TufRootData{} - err := json.Unmarshal(c.Data, d) + err := json.Unmarshal(c.Content(), d) if err != nil { return err } @@ -115,13 +115,13 @@ func applyRootRoleChange(repo *tufRepo, c changelist.Change) error { case changelist.ActionDelete: // removes a key from a role d := &changelist.TufRootData{} - err := json.Unmarshal(c.Data, d) + err := json.Unmarshal(c.Content(), d) if err != nil { return err } ids := make([]string, 0, len(d.Keys)) for _, k := range d.Keys { - append(ids, k.ID()) + ids = append(ids, k.ID()) } err = repo.RemoveBaseKeys(d.RoleName, ids...) if err != nil { diff --git a/cmd/notary/main.go b/cmd/notary/main.go index 2f59fc3c36..993d0a2768 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -96,6 +96,7 @@ func main() { notaryCmd.AddCommand(cmdKey) notaryCmd.AddCommand(cmdCert) + notaryCmd.AddCommand(cmdMeta) notaryCmd.AddCommand(cmdTufInit) cmdTufInit.Flags().StringVarP(&remoteTrustServer, "server", "s", serverURL, "Remote trust server location") notaryCmd.AddCommand(cmdTufList) diff --git a/cmd/notary/meta.go b/cmd/notary/meta.go new file mode 100644 index 0000000000..bb872b5761 --- /dev/null +++ b/cmd/notary/meta.go @@ -0,0 +1,110 @@ +package main + +import ( + "github.com/Sirupsen/logrus" + notaryclient "github.com/docker/notary/client" + "github.com/spf13/cobra" +) + +func init() { + cmdMeta.AddCommand(cmdRoleDisplay) + cmdMeta.AddCommand(cmdReplaceKey) + cmdMeta.AddCommand(cmdAddKey) + cmdMeta.AddCommand(cmdRemoveKey) +} + +var cmdMeta = &cobra.Command{ + Use: "meta", + Short: "Operates on repository metadata.", + Long: "Operations to manage key usage and delegations within a repository.", +} + +var cmdRoleDisplay = &cobra.Command{ + Use: "display [ GUN ] ", + Short: "Shows metadata about a role", + Long: "Display all metadata about a role including the associated keys, the role name, and the owner name if applicable.", + Run: metaRoleDisplay, +} + +var cmdReplaceKey = &cobra.Command{ + Use: "replace [ GUN ] ", + Short: "Replace all keys for role.", + Long: "Replaces all keys for the given role.", + Run: metaReplaceKey, +} + +var cmdAddKey = &cobra.Command{ + Use: "add [ GUN ] ", + Short: "Add key to role.", + Long: "Adds a key to the given role.", + Run: metaReplaceKey, +} + +var cmdRemoveKey = &cobra.Command{ + Use: "remove [ GUN ] ", + Short: "Remove a key role.", + Long: "Removes a key from the given role.", + Run: metaReplaceKey, +} + +func metaRoleDisplay(cmd *cobra.Command, args []string) { + if len(args) < 2 { + cmd.Usage() + fatalf("must specify a GUN and role") + } + + gun := args[0] + parseConfig() + + logrus.Debug("Displaying info") + _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) + if err != nil { + fatalf(err.Error()) + } +} + +func metaReplaceKey(cmd *cobra.Command, args []string) { + if len(args) < 2 { + cmd.Usage() + fatalf("must specify a GUN and role") + } + + gun := args[0] + parseConfig() + + _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) + if err != nil { + fatalf(err.Error()) + } +} + +func metaAddKey(cmd *cobra.Command, args []string) { + if len(args) < 2 { + cmd.Usage() + fatalf("must specify a GUN and role") + } + + gun := args[0] + parseConfig() + + _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) + if err != nil { + fatalf(err.Error()) + } + +} + +func metaRemoveKey(cmd *cobra.Command, args []string) { + if len(args) < 2 { + cmd.Usage() + fatalf("must specify a GUN and role") + } + + gun := args[0] + parseConfig() + + _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) + if err != nil { + fatalf(err.Error()) + } +} From 009400650e664dff0dd2b2b3d8ffd81f0b0d9cca Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 28 Aug 2015 16:32:17 -0700 Subject: [PATCH 3/6] minor tweaks to key rotation Signed-off-by: David Lawrence (github: endophage) --- client/client.go | 24 +++++++++++++++--------- cmd/notary/meta.go | 14 +++++++++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/client/client.go b/client/client.go index 1e7257efcd..55ff41ceb1 100644 --- a/client/client.go +++ b/client/client.go @@ -621,23 +621,29 @@ func (r *NotaryRepository) RemoveKeys(role string, keyIDs ...string) error { // ReplaceKey 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) ReplaceKeys(role string, keyIDs ...string) error { - return r.rootKeyChange(role, changelist.ActionCreate, keyIDs...) +func (r *NotaryRepository) ReplaceKeys(role string) error { + if role == "root" { + // need to create an entirely new root key and certificate + var key data.PublicKey = nil + return r.rootKeyChange(role, changelist.ActionCreate, key) + } else if role == "targets" || role == "snapshot" { + // This is currently hardcoding the targets and snapshots keys to ECDSA + // Targets and snapshot keys are always generated locally. + key, err := r.cryptoService.Create(role, data.ECDSAKey) + if err != nil { + return err + } + return r.rootKeyChange(role, changelist.ActionCreate, key) + } } -func (r *NotaryRepository) rootKeyChange(role, action string, keyIDs ...string) error { +func (r *NotaryRepository) rootKeyChange(role, action string, keys ...data.PublicKey) error { cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } defer cl.Close() - keys := make([]data.PublicKey, 0, len(keyIDs)) - for _, kID := range keyIDs { - logrus.Debug(kID) - // get PUBLIC key and append it to keys - } - meta := changelist.TufRootData{ RoleName: role, Keys: keys, diff --git a/cmd/notary/meta.go b/cmd/notary/meta.go index bb872b5761..935ed6e494 100644 --- a/cmd/notary/meta.go +++ b/cmd/notary/meta.go @@ -37,14 +37,14 @@ var cmdAddKey = &cobra.Command{ Use: "add [ GUN ] ", Short: "Add key to role.", Long: "Adds a key to the given role.", - Run: metaReplaceKey, + Run: metaAddKey, } var cmdRemoveKey = &cobra.Command{ Use: "remove [ GUN ] ", Short: "Remove a key role.", Long: "Removes a key from the given role.", - Run: metaReplaceKey, + Run: metaRemoveKey, } func metaRoleDisplay(cmd *cobra.Command, args []string) { @@ -63,6 +63,7 @@ func metaRoleDisplay(cmd *cobra.Command, args []string) { } } +// metaReplaceKey rotates the key for the given GUN and role. func metaReplaceKey(cmd *cobra.Command, args []string) { if len(args) < 2 { cmd.Usage() @@ -70,12 +71,19 @@ func metaReplaceKey(cmd *cobra.Command, args []string) { } gun := args[0] + role := args[1] parseConfig() - _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) + r, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) if err != nil { fatalf(err.Error()) } + // args[2:] should be a list of paths to public key files. + // they will be imported into the TUF repo for the given + // role, replaceing any existing keys. + if err := r.ReplaceKeys(role, args[2:]...); err != nil { + fatalf(err.Error()) + } } func metaAddKey(cmd *cobra.Command, args []string) { From ac54370fb09831bea67b05b25db641325027613e Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 9 Oct 2015 19:40:36 -0700 Subject: [PATCH 4/6] cleanup after discussing with Diogo Signed-off-by: David Lawrence (github: endophage) --- client/client.go | 16 +-- cmd/notary-server/dev_w_signer.config.json | 21 ---- cmd/notary/keys.go | 12 +++ cmd/notary/meta.go | 118 --------------------- 4 files changed, 14 insertions(+), 153 deletions(-) delete mode 100644 cmd/notary-server/dev_w_signer.config.json delete mode 100644 cmd/notary/meta.go diff --git a/client/client.go b/client/client.go index 55ff41ceb1..046cecceaf 100644 --- a/client/client.go +++ b/client/client.go @@ -606,18 +606,6 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) { ), nil } -// AddKeys adds the specified keyIDs to the role. These changes are -// staged in a changelist until publish is called. -func (r *NotaryRepository) AddKeys(role string, keyIDs ...string) error { - return r.rootKeyChange(role, changelist.ActionUpdate, keyIDs...) -} - -// RemoveKeys removes the specified keyIDs from the role. These changes -// are staged in a changelist until publish is called. -func (r *NotaryRepository) RemoveKeys(role string, keyIDs ...string) error { - return r.rootKeyChange(role, changelist.ActionDelete, keyIDs...) -} - // ReplaceKey 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. @@ -633,11 +621,11 @@ func (r *NotaryRepository) ReplaceKeys(role string) error { if err != nil { return err } - return r.rootKeyChange(role, changelist.ActionCreate, key) + return r.rootFileKeyChange(role, changelist.ActionCreate, key) } } -func (r *NotaryRepository) rootKeyChange(role, action string, keys ...data.PublicKey) error { +func (r *NotaryRepository) rootFileKeyChange(role, action string, keys ...data.PublicKey) error { cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err diff --git a/cmd/notary-server/dev_w_signer.config.json b/cmd/notary-server/dev_w_signer.config.json deleted file mode 100644 index 521c047334..0000000000 --- a/cmd/notary-server/dev_w_signer.config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "server": { - "addr": ":4443", - "tls_key_file": "./fixtures/notary-server.key", - "tls_cert_file": "./fixtures/notary-server.crt" - }, - "trust_service": { - "type": "remote", - "hostname": "localhost", - "port": "7899", - "tls_ca_file": "./fixtures/root-ca.crt", - "key_algorithm": "ecdsa" - }, - "logging": { - "level": "debug" - }, - "storage": { - "backend": "mysql", - "db_url": "root:@tcp(localhost:3306)/dockercondemo" - } -} diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 8b5a09b03e..458ff227ed 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -29,6 +29,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) + cmdMeta.AddCommand(cmdRotateKey) } var cmdKey = &cobra.Command{ @@ -44,6 +45,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 +383,7 @@ 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) { + +} diff --git a/cmd/notary/meta.go b/cmd/notary/meta.go deleted file mode 100644 index 935ed6e494..0000000000 --- a/cmd/notary/meta.go +++ /dev/null @@ -1,118 +0,0 @@ -package main - -import ( - "github.com/Sirupsen/logrus" - notaryclient "github.com/docker/notary/client" - "github.com/spf13/cobra" -) - -func init() { - cmdMeta.AddCommand(cmdRoleDisplay) - cmdMeta.AddCommand(cmdReplaceKey) - cmdMeta.AddCommand(cmdAddKey) - cmdMeta.AddCommand(cmdRemoveKey) -} - -var cmdMeta = &cobra.Command{ - Use: "meta", - Short: "Operates on repository metadata.", - Long: "Operations to manage key usage and delegations within a repository.", -} - -var cmdRoleDisplay = &cobra.Command{ - Use: "display [ GUN ] ", - Short: "Shows metadata about a role", - Long: "Display all metadata about a role including the associated keys, the role name, and the owner name if applicable.", - Run: metaRoleDisplay, -} - -var cmdReplaceKey = &cobra.Command{ - Use: "replace [ GUN ] ", - Short: "Replace all keys for role.", - Long: "Replaces all keys for the given role.", - Run: metaReplaceKey, -} - -var cmdAddKey = &cobra.Command{ - Use: "add [ GUN ] ", - Short: "Add key to role.", - Long: "Adds a key to the given role.", - Run: metaAddKey, -} - -var cmdRemoveKey = &cobra.Command{ - Use: "remove [ GUN ] ", - Short: "Remove a key role.", - Long: "Removes a key from the given role.", - Run: metaRemoveKey, -} - -func metaRoleDisplay(cmd *cobra.Command, args []string) { - if len(args) < 2 { - cmd.Usage() - fatalf("must specify a GUN and role") - } - - gun := args[0] - parseConfig() - - logrus.Debug("Displaying info") - _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) - if err != nil { - fatalf(err.Error()) - } -} - -// metaReplaceKey rotates the key for the given GUN and role. -func metaReplaceKey(cmd *cobra.Command, args []string) { - if len(args) < 2 { - cmd.Usage() - fatalf("must specify a GUN and role") - } - - gun := args[0] - role := args[1] - parseConfig() - - r, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) - if err != nil { - fatalf(err.Error()) - } - // args[2:] should be a list of paths to public key files. - // they will be imported into the TUF repo for the given - // role, replaceing any existing keys. - if err := r.ReplaceKeys(role, args[2:]...); err != nil { - fatalf(err.Error()) - } -} - -func metaAddKey(cmd *cobra.Command, args []string) { - if len(args) < 2 { - cmd.Usage() - fatalf("must specify a GUN and role") - } - - gun := args[0] - parseConfig() - - _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) - if err != nil { - fatalf(err.Error()) - } - -} - -func metaRemoveKey(cmd *cobra.Command, args []string) { - if len(args) < 2 { - cmd.Usage() - fatalf("must specify a GUN and role") - } - - gun := args[0] - parseConfig() - - _, err := notaryclient.NewNotaryRepository(trustDir, gun, remoteTrustServer, getTransport(), retriever) - if err != nil { - fatalf(err.Error()) - } -} From 98cde51f18b24e6a5e559cd7ea9062e98a8dbd2a Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 9 Oct 2015 20:35:06 -0700 Subject: [PATCH 5/6] working basic key rotation for targets and snapshot key. Command is 'notary key rotate [GUN]' Signed-off-by: David Lawrence (github: endophage) --- Godeps/Godeps.json | 2 +- .../github.com/endophage/gotuf/data/keys.go | 2 +- .../github.com/endophage/gotuf/data/roles.go | 1 + .../src/github.com/endophage/gotuf/tuf.go | 34 +++++++++++++++---- client/changelist/change.go | 4 +-- client/client.go | 25 ++++++++------ client/helpers.go | 30 +++------------- cmd/notary-server/dev-config.json | 2 +- cmd/notary/keys.go | 18 ++++++++-- cmd/notary/main.go | 1 - 10 files changed, 68 insertions(+), 51 deletions(-) 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 643f17d137..0626b7ccf3 100644 --- a/client/changelist/change.go +++ b/client/changelist/change.go @@ -36,8 +36,8 @@ type TufChange struct { } type TufRootData struct { - Keys []data.PublicKey `json:"keys"` - RoleName string `json:"role"` + Keys []data.TUFKey `json:"keys"` + RoleName string `json:"role"` } // NewTufChange initializes a tufChange object diff --git a/client/client.go b/client/client.go index 046cecceaf..7732b09820 100644 --- a/client/client.go +++ b/client/client.go @@ -609,32 +609,35 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) { // ReplaceKey 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) ReplaceKeys(role string) error { - if role == "root" { - // need to create an entirely new root key and certificate - var key data.PublicKey = nil - return r.rootKeyChange(role, changelist.ActionCreate, key) - } else if role == "targets" || role == "snapshot" { - // This is currently hardcoding the targets and snapshots keys to ECDSA - // Targets and snapshot keys are always generated locally. +func (r *NotaryRepository) RotateKeys() error { + for _, role := range []string{"targets", "snapshot"} { key, err := r.cryptoService.Create(role, data.ECDSAKey) if err != nil { return err } - return r.rootFileKeyChange(role, changelist.ActionCreate, key) + err = r.rootFileKeyChange(role, changelist.ActionCreate, key) + if err != nil { + return err + } } + return nil } -func (r *NotaryRepository) rootFileKeyChange(role, action string, keys ...data.PublicKey) error { +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: keys, + Keys: []data.TUFKey{*k}, } metaJSON, err := json.Marshal(meta) if err != nil { diff --git a/client/helpers.go b/client/helpers.go index 7b4407d618..50be86c6ce 100644 --- a/client/helpers.go +++ b/client/helpers.go @@ -97,33 +97,11 @@ func applyRootRoleChange(repo *tuf.TufRepo, c changelist.Change) error { if err != nil { return err } - err = repo.ReplaceBaseKeys(d.RoleName, d.Keys...) - if err != nil { - return err + k := []data.PublicKey{} + for _, key := range d.Keys { + k = append(k, data.NewPublicKey(key.Algorithm(), key.Public())) } - case changelist.ActionUpdate: - // adds a key to a role - d := &changelist.TufRootData{} - err := json.Unmarshal(c.Content(), d) - if err != nil { - return err - } - err = repo.AddBaseKeys(d.RoleName, d.Keys...) - if err != nil { - return err - } - case changelist.ActionDelete: - // removes a key from a role - d := &changelist.TufRootData{} - err := json.Unmarshal(c.Content(), d) - if err != nil { - return err - } - ids := make([]string, 0, len(d.Keys)) - for _, k := range d.Keys { - ids = append(ids, k.ID()) - } - err = repo.RemoveBaseKeys(d.RoleName, ids...) + err = repo.ReplaceBaseKeys(d.RoleName, k...) if err != nil { return err } 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 458ff227ed..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,7 +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) - cmdMeta.AddCommand(cmdRotateKey) + cmdKey.AddCommand(cmdRotateKey) } var cmdKey = &cobra.Command{ @@ -46,7 +47,7 @@ var cmdKeyList = &cobra.Command{ } var cmdRotateKey = &cobra.Command{ - Use: "rotate [ GUN ] ", + 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, @@ -385,5 +386,18 @@ func printKey(keyPath, alias string) { } 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()) + } } diff --git a/cmd/notary/main.go b/cmd/notary/main.go index 993d0a2768..2f59fc3c36 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -96,7 +96,6 @@ func main() { notaryCmd.AddCommand(cmdKey) notaryCmd.AddCommand(cmdCert) - notaryCmd.AddCommand(cmdMeta) notaryCmd.AddCommand(cmdTufInit) cmdTufInit.Flags().StringVarP(&remoteTrustServer, "server", "s", serverURL, "Remote trust server location") notaryCmd.AddCommand(cmdTufList) From e587b0427afa557e21545a74d4aa5378a6763f83 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 9 Oct 2015 22:16:56 -0700 Subject: [PATCH 6/6] test for key rotation Signed-off-by: David Lawrence (github: endophage) --- client/changelist/change.go | 2 + client/client.go | 2 +- client/client_test.go | 92 +++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/client/changelist/change.go b/client/changelist/change.go index 0626b7ccf3..dfdaed5c36 100644 --- a/client/changelist/change.go +++ b/client/changelist/change.go @@ -35,6 +35,8 @@ 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"` diff --git a/client/client.go b/client/client.go index 7732b09820..ca57a89f1a 100644 --- a/client/client.go +++ b/client/client.go @@ -606,7 +606,7 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) { ), nil } -// ReplaceKey removes all existing keys associated with role and adds +// 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 { 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") +}