Merge pull request #209 from endophage/key_rotation

Strawman for targets and snapshot key rotation
This commit is contained in:
Diogo Mónica 2015-10-11 12:01:15 -07:00
commit f70a03fb64
10 changed files with 267 additions and 15 deletions

2
Godeps/Godeps.json generated
View File

@ -106,7 +106,7 @@
}, },
{ {
"ImportPath": "github.com/endophage/gotuf", "ImportPath": "github.com/endophage/gotuf",
"Rev": "9bcdad0308e34a49f38448b8ad436ad8860825ce" "Rev": "338013a6720b3dfe4e4e9d1368109ea314c936d3"
}, },
{ {
"ImportPath": "github.com/go-sql-driver/mysql", "ImportPath": "github.com/go-sql-driver/mysql",

View File

@ -71,7 +71,7 @@ func (k TUFKey) Public() []byte {
return k.Value.Public return k.Value.Public
} }
func (k *TUFKey) Private() []byte { func (k TUFKey) Private() []byte {
return k.Value.Private return k.Value.Private
} }

View File

@ -88,6 +88,7 @@ type Role struct {
Name string `json:"name"` Name string `json:"name"`
Paths []string `json:"paths,omitempty"` Paths []string `json:"paths,omitempty"`
PathHashPrefixes []string `json:"path_hash_prefixes,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) { func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []string) (*Role, error) {

View File

@ -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 // 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 { if tr.Root == nil {
return ErrNotLoaded{role: "root"} return ErrNotLoaded{role: "root"}
} }
ids := []string{}
for _, k := range keys { for _, k := range keys {
// Store only the public portion // Store only the public portion
pubKey := *k pubKey := data.NewPrivateKey(k.Algorithm(), k.Public(), nil)
pubKey.Value.Private = nil tr.Root.Signed.Keys[pubKey.ID()] = pubKey
tr.Root.Signed.Keys[pubKey.ID()] = &pubKey tr.keysDB.AddKey(k)
tr.keysDB.AddKey(&pubKey)
tr.Root.Signed.Roles[role].KeyIDs = append(tr.Root.Signed.Roles[role].KeyIDs, pubKey.ID()) 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 tr.Root.Dirty = true
return nil 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 { func (tr *TufRepo) RemoveBaseKeys(role string, keyIDs ...string) error {
if tr.Root == nil { if tr.Root == nil {
return ErrNotLoaded{role: "root"} return ErrNotLoaded{role: "root"}

View File

@ -1,5 +1,9 @@
package changelist package changelist
import (
"github.com/endophage/gotuf/data"
)
// Scopes for TufChanges are simply the TUF roles. // Scopes for TufChanges are simply the TUF roles.
// Unfortunately because of targets delegations, we can only // Unfortunately because of targets delegations, we can only
// cover the base roles. // cover the base roles.
@ -10,6 +14,17 @@ const (
ScopeTimestamp = "timestamp" 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 // TufChange represents a change to a TUF repo
type TufChange struct { type TufChange struct {
// Abbreviated because Go doesn't permit a field and method of the same name // 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"` 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 // NewTufChange initializes a tufChange object
func NewTufChange(action string, role, changeType, changePath string, content []byte) *TufChange { func NewTufChange(action string, role, changeType, changePath string, content []byte) *TufChange {
return &TufChange{ return &TufChange{

View File

@ -245,6 +245,7 @@ func (r *NotaryRepository) AddTarget(target *Target) error {
if err != nil { if err != nil {
return err 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) 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} meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes}
@ -258,7 +259,7 @@ func (r *NotaryRepository) AddTarget(target *Target) error {
if err != nil { if err != nil {
return err return err
} }
return cl.Close() return nil
} }
// RemoveTarget creates a new changelist entry to remove a target from the repository // 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, r.fileStore,
), nil ), 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
}

View File

@ -633,3 +633,95 @@ func testPublish(t *testing.T, rootType data.KeyAlgorithm) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, currentTarget, newCurrentTarget, "current target does not match") 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")
}

View File

@ -7,7 +7,7 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/notary/client/changelist" "github.com/docker/notary/client/changelist"
"github.com/endophage/gotuf" tuf "github.com/endophage/gotuf"
"github.com/endophage/gotuf/data" "github.com/endophage/gotuf/data"
"github.com/endophage/gotuf/keys" "github.com/endophage/gotuf/keys"
"github.com/endophage/gotuf/store" "github.com/endophage/gotuf/store"
@ -38,14 +38,16 @@ func applyChangelist(repo *tuf.TufRepo, cl changelist.Changelist) error {
} }
switch c.Scope() { switch c.Scope() {
case changelist.ScopeTargets: case changelist.ScopeTargets:
err := applyTargetsChange(repo, c) err = applyTargetsChange(repo, c)
if err != nil { case changelist.ScopeRoot:
return err err = applyRootChange(repo, c)
}
default: default:
logrus.Debug("scope not supported: ", c.Scope()) logrus.Debug("scope not supported: ", c.Scope())
} }
index++ index++
if err != nil {
return err
}
} }
logrus.Debugf("applied %d change(s)", index) logrus.Debugf("applied %d change(s)", index)
return nil return nil
@ -75,6 +77,40 @@ func applyTargetsChange(repo *tuf.TufRepo, c changelist.Change) error {
return nil 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 { func nearExpiry(r *data.SignedRoot) bool {
plus6mo := time.Now().AddDate(0, 6, 0) plus6mo := time.Now().AddDate(0, 6, 0)
return r.Signed.Expires.Before(plus6mo) return r.Signed.Expires.Before(plus6mo)

View File

@ -15,6 +15,6 @@
}, },
"storage": { "storage": {
"backend": "mysql", "backend": "mysql",
"db_url": "dockercondemo:dockercondemo@tcp(localhost:3306)/dockercondemo" "db_url": "root@tcp(localhost:3306)/notary"
} }
} }

View File

@ -8,6 +8,7 @@ import (
"sort" "sort"
"strings" "strings"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/keystoremanager" "github.com/docker/notary/keystoremanager"
"github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/pkg/passphrase"
"github.com/docker/notary/trustmanager" "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") cmdKeyExportRoot.Flags().BoolVarP(&keysExportRootChangePassphrase, "change-passphrase", "c", false, "set a new passphrase for the key being exported")
cmdKey.AddCommand(cmdKeyImport) cmdKey.AddCommand(cmdKeyImport)
cmdKey.AddCommand(cmdKeyImportRoot) cmdKey.AddCommand(cmdKeyImportRoot)
cmdKey.AddCommand(cmdRotateKey)
} }
var cmdKey = &cobra.Command{ var cmdKey = &cobra.Command{
@ -44,6 +46,13 @@ var cmdKeyList = &cobra.Command{
Run: keysList, 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 keyRemoveGUN string
var keyRemoveRoot bool var keyRemoveRoot bool
var keyRemoveYes bool var keyRemoveYes bool
@ -375,3 +384,20 @@ func printKey(keyPath, alias string) {
gun := filepath.Dir(keyPath) gun := filepath.Dir(keyPath)
fmt.Printf("%s - %s - %s\n", gun, alias, keyID) 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())
}
}