Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Server rotate key (for reference) #534

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions client/backwards_compatibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"
"testing"

"github.com/docker/notary/client/changelist"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/tuf/data"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -77,11 +76,7 @@ func Test0Dot1RepoFormat(t *testing.T) {
assert.NoError(t, err, "error creating repo: %s", err)

// rotate the timestamp key, since the server doesn't have that one
timestampPubKey, err := getRemoteKey(ts.URL, gun, data.CanonicalTimestampRole, http.DefaultTransport)
assert.NoError(t, err)
assert.NoError(
t, repo.rootFileKeyChange(data.CanonicalTimestampRole, changelist.ActionCreate, timestampPubKey))

assert.NoError(t, repo.RotateKey(data.CanonicalTimestampRole, true))
assert.NoError(t, repo.Publish())

targets, err := repo.ListTargets()
Expand Down
16 changes: 15 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ func (err ErrInvalidRemoteRole) Error() string {
"notary does not support the server managing the %s key", err.Role)
}

// ErrInvalidLocalRole is returned when the client wants to manage
// an unsupported key type
type ErrInvalidLocalRole struct {
Role string
}

func (err ErrInvalidLocalRole) Error() string {
return fmt.Sprintf(
"notary does not support the client managing the %s key", err.Role)
}

// ErrRepositoryNotExist is returned when an action is taken on a remote
// repository that doesn't exist
type ErrRepositoryNotExist struct {
Expand Down Expand Up @@ -990,13 +1001,16 @@ func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, erro
// creates and adds one new key or delegates managing the key to the server.
// These changes are staged in a changelist until publish is called.
func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error {
if role == data.CanonicalRootRole || role == data.CanonicalTimestampRole {
if role == data.CanonicalRootRole {
return fmt.Errorf(
"notary does not currently support rotating the %s key", role)
}
if serverManagesKey && role == data.CanonicalTargetsRole {
return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole}
}
if !serverManagesKey && role == data.CanonicalTimestampRole {
return ErrInvalidLocalRole{Role: data.CanonicalTargetsRole}
}

var (
pubKey data.PublicKey
Expand Down
10 changes: 6 additions & 4 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2355,14 +2355,16 @@ func TestRotateKeyInvalidRole(t *testing.T) {
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)

// the equivalent of: (root, true), (root, false), (timestamp, true),
// (timestamp, false), (targets, true)
// the equivalent of: (root, true), (root, false), (timestamp, false), (targets, true)
for _, role := range data.BaseRoles {
if role == data.CanonicalSnapshotRole {
if role == data.CanonicalSnapshotRole { // remote or local can manage snapshot
continue
}
for _, serverManagesKey := range []bool{true, false} {
if role == data.CanonicalTargetsRole && !serverManagesKey {
if role == data.CanonicalTargetsRole && !serverManagesKey { // only local can manage targets
continue
}
if role == data.CanonicalTimestampRole && serverManagesKey { // only remote can manage timestamp
continue
}
err := repo.RotateKey(role, serverManagesKey)
Expand Down
6 changes: 6 additions & 0 deletions server/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,11 @@ var (
Description: "No key algorihtm has been configured for the server and it has been asked to perform an operation that requires generation.",
HTTPStatusCode: http.StatusInternalServerError,
})
ErrCannotRotateKey = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "CANNOT_ROTATE_KEY",
Message: "Key has already been rotated recently.",
Description: "The key has been rotated too recently, and cannot be rotated again at this time.",
HTTPStatusCode: 429, // 429 is Too Many Requests - this will be added as a status constant in Go 1.6
})
ErrUnknown = errcode.ErrorCodeUnknown
)
150 changes: 110 additions & 40 deletions server/handlers/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"time"

"github.com/Sirupsen/logrus"
ctxu "github.com/docker/distribution/context"
"github.com/gorilla/mux"
"golang.org/x/net/context"

"github.com/docker/notary/server/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"
"github.com/docker/notary/tuf/validation"
)

// DefaultKeyExpiry is the default time before a rotated key expires, if none is
// provided. This means that after this period, a key rotation can happen again,
// unless the key is signed into a root.json.
const DefaultKeyExpiry = 60 * 24 * time.Minute // 1 day

// MainHandler is the default handler for the server
func MainHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
// For now it only supports `GET`
Expand Down Expand Up @@ -147,68 +153,132 @@ func DeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request)
// it if it doesn't yet exist
func GetKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
defer r.Body.Close()
vars := mux.Vars(r)
return getKeyHandler(ctx, w, r, vars)
return getKeyHandler(ctx, w, mux.Vars(r))
}

func getKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
func getKeyHandler(ctx context.Context, w io.Writer, vars map[string]string) error {
keyInfo, err := parseKeyParams(ctx, vars)
if err != nil {
return err
}
pubKey, err := GetOrCreateKey(*keyInfo)
if err != nil {
return err
}
out, err := json.Marshal(pubKey)
if err != nil {
return errors.ErrUnknown.WithDetail(err)
}
w.Write(out)
return nil
}

type serverKeyInfo struct {
gun string
role string
store storage.MetaStore
crypto signed.CryptoService
keyAlgo string
rotateOncePer time.Duration
}

func parseKeyParams(ctx context.Context, vars map[string]string) (*serverKeyInfo, error) {
gun, ok := vars["imageName"]
if !ok || gun == "" {
return errors.ErrUnknown.WithDetail("no gun")
return nil, errors.ErrUnknown.WithDetail("no gun")
}
role, ok := vars["tufRole"]
if !ok || role == "" {
return errors.ErrUnknown.WithDetail("no role")
return nil, errors.ErrUnknown.WithDetail("no role")
}
if role != data.CanonicalTimestampRole && role != data.CanonicalSnapshotRole {
return nil, errors.ErrInvalidRole.WithDetail(role)
}

logger := ctxu.GetLoggerWithField(ctx, gun, "gun")

s := ctx.Value("metaStore")
store, ok := s.(storage.MetaStore)
if !ok || store == nil {
logger.Error("500 GET storage not configured")
return errors.ErrNoStorage.WithDetail(nil)
return nil, errors.ErrNoStorage.WithDetail(nil)
}

c := ctx.Value("cryptoService")
crypto, ok := c.(signed.CryptoService)
if !ok || crypto == nil {
logger.Error("500 GET crypto service not configured")
return errors.ErrNoCryptoService.WithDetail(nil)
return nil, errors.ErrNoCryptoService.WithDetail(nil)
}

algo := ctx.Value("keyAlgorithm")
keyAlgo, ok := algo.(string)
if !ok || keyAlgo == "" {
logger.Error("500 GET key algorithm not configured")
return errors.ErrNoKeyAlgorithm.WithDetail(nil)
}
keyAlgorithm := keyAlgo

var (
key data.PublicKey
err error
)
switch role {
case data.CanonicalTimestampRole:
key, err = timestamp.GetOrCreateTimestampKey(gun, store, crypto, keyAlgorithm)
case data.CanonicalSnapshotRole:
key, err = snapshot.GetOrCreateSnapshotKey(gun, store, crypto, keyAlgorithm)
default:
logger.Errorf("400 GET %s key: %v", role, err)
return errors.ErrInvalidRole.WithDetail(role)
if !ok || keyAlgo != data.ECDSAKey && keyAlgo != data.RSAKey && keyAlgo != data.ED25519Key {
return nil, errors.ErrNoKeyAlgorithm.WithDetail(nil)
}
if err != nil {
logger.Errorf("500 GET %s key: %v", role, err)
return errors.ErrUnknown.WithDetail(err)

rotateOncePer := DefaultKeyExpiry
rotationLimit := ctx.Value("rotationRateLimitInMinutes")
rotationLimitStr, ok := rotationLimit.(string)
if ok {
inMinutes, err := strconv.Atoi(rotationLimitStr)
if err == nil && inMinutes > 0 {
rotateOncePer = time.Duration(inMinutes) * time.Minute
}
}

out, err := json.Marshal(key)
return &serverKeyInfo{
gun: gun,
role: role,
store: store,
crypto: crypto,
keyAlgo: keyAlgo,
rotateOncePer: rotateOncePer,
}, nil
}

func createNewKey(s serverKeyInfo) (data.PublicKey, error) {
key, err := s.crypto.Create(s.role, s.keyAlgo)
if err != nil {
logger.Errorf("500 GET %s key", role)
return errors.ErrUnknown.WithDetail(err)
return nil, errors.ErrUnknown.WithDetail(err)
}
logger.Debugf("200 GET %s key", role)
w.Write(out)
return nil
logrus.Debug("Creating new timestamp key for ", s.gun, ". With algo: ", s.keyAlgo)
err = s.store.AddKey(s.gun, s.role, key, time.Now().Add(s.rotateOncePer))
if err != nil {
return nil, errors.ErrUnknown.WithDetail(err)
}
return key, nil
}

// GetOrCreateKey returns either the current or pending timestamp key for the given
// gun and role, whichever key was the most recently created. If no key is found,
// it creates a new one.
func GetOrCreateKey(s serverKeyInfo) (data.PublicKey, error) {
key, err := s.store.GetLatestKey(s.gun, s.role)
if _, ok := err.(storage.ErrNoKey); ok {
return createNewKey(s)
}

if err == nil {
return key.PublicKey, nil
}

return nil, errors.ErrUnknown.WithDetail(err)
}

// RotateKey attempts to rotate a key for a role. If it has been rotated too
// recently without being signed into the root.json as an actively used signing
// key, rotation fails. Activating the key (by signing it into the root.json)
// resets the rotation rate limit.
func RotateKey(s serverKeyInfo) (data.PublicKey, error) {
// GetLatestKeys excludes expired keys, so we know that this key is unexpired
key, err := s.store.GetLatestKey(s.gun, s.role)

if err == nil && key.Pending { // there is already a good pending key
return nil, errors.ErrCannotRotateKey.WithDetail(key.ID())
}

if _, ok := err.(storage.ErrNoKey); !ok && err != nil {
return nil, errors.ErrUnknown.WithDetail(err)
}

return createNewKey(s)
}

// NotFoundHandler is used as a generic catch all handler to return the ErrMetadataNotFound
Expand Down
Loading