mirror of https://github.com/docker/docs.git
Merge pull request #209 from endophage/key_rotation
Strawman for targets and snapshot key rotation
This commit is contained in:
commit
f70a03fb64
|
@ -106,7 +106,7 @@
|
|||
},
|
||||
{
|
||||
"ImportPath": "github.com/endophage/gotuf",
|
||||
"Rev": "9bcdad0308e34a49f38448b8ad436ad8860825ce"
|
||||
"Rev": "338013a6720b3dfe4e4e9d1368109ea314c936d3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/go-sql-driver/mysql",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
},
|
||||
"storage": {
|
||||
"backend": "mysql",
|
||||
"db_url": "dockercondemo:dockercondemo@tcp(localhost:3306)/dockercondemo"
|
||||
"db_url": "root@tcp(localhost:3306)/notary"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue