-
Notifications
You must be signed in to change notification settings - Fork 512
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: Client rotate remote key #553
Changes from all commits
cc72108
e4bb888
f236451
cc2ee80
413e1cd
e9182f7
5ea5208
4f39c93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,7 +52,18 @@ type ErrInvalidRemoteRole struct { | |
|
||
func (err ErrInvalidRemoteRole) Error() string { | ||
return fmt.Sprintf( | ||
"notary does not support the server managing the %s key", err.Role) | ||
"notary does not permit 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 permit the client managing the %s key", err.Role) | ||
} | ||
|
||
// ErrRepositoryNotExist is returned when an action is taken on a remote | ||
|
@@ -511,11 +522,10 @@ func (r *NotaryRepository) Publish() error { | |
initialPublish = true | ||
} else { | ||
// We could not update, so we cannot publish. | ||
logrus.Error("Could not publish Repository: ", err.Error()) | ||
logrus.Error("Could not publish Repository since we could not update: ", err.Error()) | ||
return err | ||
} | ||
} | ||
|
||
cl, err := r.GetChangelist() | ||
if err != nil { | ||
return err | ||
|
@@ -608,7 +618,7 @@ func (r *NotaryRepository) Publish() error { | |
// to load metadata for all roles. Since server snapshots are supported, | ||
// if the snapshot metadata fails to load, that's ok. | ||
// This can also be unified with some cache reading tools from tuf/client. | ||
// This assumes that bootstrapRepo is only used by Publish() | ||
// This assumes that bootstrapRepo is only used by Publish() or RotateKey() | ||
func (r *NotaryRepository) bootstrapRepo() error { | ||
kdb := keys.NewDB() | ||
tufRepo := tuf.NewRepo(kdb, r.CryptoService) | ||
|
@@ -829,28 +839,68 @@ 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 { | ||
switch { | ||
case role == data.CanonicalRootRole: | ||
return fmt.Errorf( | ||
"notary does not currently support rotating the %s key", role) | ||
} | ||
if serverManagesKey && role == data.CanonicalTargetsRole { | ||
"notary does not currently permit rotating the %s key", role) | ||
|
||
// We currently support locally managing targets keys, remotely managing | ||
// timestamp keys, and locally or remotely managing snapshot keys. | ||
case serverManagesKey && role == data.CanonicalTargetsRole: | ||
return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole} | ||
case !serverManagesKey && role == data.CanonicalTimestampRole: | ||
return ErrInvalidLocalRole{Role: data.CanonicalTimestampRole} | ||
} | ||
|
||
var ( | ||
pubKey data.PublicKey | ||
err error | ||
pubKey data.PublicKey | ||
errFmtString string | ||
err error | ||
) | ||
// Key rotation is an offline operation unless we are rotating a remote key, in which case we | ||
// need to ensure the rotation can actually happen: the user needs to have the root key, and | ||
// we want to publish right away so that the remote key doesn't expire | ||
if serverManagesKey { | ||
pubKey, err = getRemoteKey(r.baseURL, r.gun, role, r.roundTrip) | ||
if err = r.bootstrapRepo(); err != nil { | ||
if _, ok := err.(store.ErrMetaNotFound); ok { | ||
err = ErrRepoNotInitialized{} | ||
} | ||
logrus.Debug("Repository should be valid and initialized before rotating a remote key:", err) | ||
return err | ||
} | ||
|
||
// get root keys - since we bootstrapped the repo, a valid root should always specify | ||
// the root role | ||
rootKeys, err := r.tufRepo.SigningKeysForRole(data.CanonicalRootRole) | ||
if err != nil { | ||
logrus.Debug("The root is invalid, because it does not contain any signing keys") | ||
return err | ||
} | ||
|
||
errFmtString = "unable to rotate remote key: %s" | ||
remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip) | ||
if err == nil { | ||
pubKey, err = remote.RotateKey(role, r.CryptoService, rootKeys...) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the signing functionality currently contained in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm of the opposite opinion :D |
||
} else if _, ok := err.(signed.ErrNoKeys); ok { | ||
err = fmt.Errorf("root signing key unavailable so unable to rotate key") | ||
} | ||
} else { | ||
pubKey, err = r.CryptoService.Create(role, data.ECDSAKey) | ||
errFmtString = "unable to generate key: %s" | ||
} | ||
|
||
if err != nil { | ||
return fmt.Errorf(errFmtString, err) | ||
} | ||
|
||
if err = r.rootFileKeyChange(role, changelist.ActionCreate, pubKey); err != nil { | ||
return err | ||
} | ||
|
||
return r.rootFileKeyChange(role, changelist.ActionCreate, pubKey) | ||
if serverManagesKey { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment to the effect: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
||
return r.Publish() | ||
} | ||
return nil | ||
} | ||
|
||
func (r *NotaryRepository) rootFileKeyChange(role, action string, key data.PublicKey) error { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import ( | |
"github.com/Sirupsen/logrus" | ||
ctxu "github.com/docker/distribution/context" | ||
"github.com/docker/go/canonical/json" | ||
"github.com/gorilla/mux" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for updating this 👍 |
||
"github.com/stretchr/testify/assert" | ||
"golang.org/x/net/context" | ||
|
||
|
@@ -93,13 +94,13 @@ func (p *passRoleRecorder) assertAsked(t *testing.T, expected []string, args ... | |
var passphraseRetriever = passphrase.ConstantRetriever(password) | ||
|
||
func simpleTestServer(t *testing.T, roles ...string) ( | ||
*httptest.Server, *http.ServeMux, map[string]data.PrivateKey) { | ||
*httptest.Server, *mux.Router, map[string]data.PrivateKey) { | ||
|
||
if len(roles) == 0 { | ||
roles = []string{data.CanonicalTimestampRole, data.CanonicalSnapshotRole} | ||
} | ||
keys := make(map[string]data.PrivateKey) | ||
mux := http.NewServeMux() | ||
m := mux.NewRouter() | ||
|
||
for _, role := range roles { | ||
key, err := trustmanager.GenerateECDSAKey(rand.Reader) | ||
|
@@ -112,15 +113,15 @@ func simpleTestServer(t *testing.T, roles ...string) ( | |
keyJSON := string(jsonBytes) | ||
|
||
// TUF will request /v2/docker.com/notary/_trust/tuf/<role>.key | ||
mux.HandleFunc( | ||
fmt.Sprintf("/v2/docker.com/notary/_trust/tuf/%s.key", role), | ||
m.Methods("GET").Path( | ||
fmt.Sprintf("/v2/docker.com/notary/_trust/tuf/%s.key", role)).HandlerFunc( | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, keyJSON) | ||
}) | ||
} | ||
|
||
ts := httptest.NewServer(mux) | ||
return ts, mux, keys | ||
ts := httptest.NewServer(m) | ||
return ts, m, keys | ||
} | ||
|
||
func fullTestServer(t *testing.T) *httptest.Server { | ||
|
@@ -247,7 +248,7 @@ func TestInitRepositoryManagedRolesIncludingRoot(t *testing.T) { | |
assert.IsType(t, ErrInvalidRemoteRole{}, err) | ||
// Just testing the error message here in this one case | ||
assert.Equal(t, err.Error(), | ||
"notary does not support the server managing the root key") | ||
"notary does not permit the server managing the root key") | ||
// no key creation happened | ||
rec.assertCreated(t, nil) | ||
} | ||
|
@@ -1019,7 +1020,7 @@ func testListEmptyTargets(t *testing.T, rootType string) { | |
|
||
// reads data from the repository in order to fake data being served via | ||
// the ServeMux. | ||
func fakeServerData(t *testing.T, repo *NotaryRepository, mux *http.ServeMux, | ||
func fakeServerData(t *testing.T, repo *NotaryRepository, m *mux.Router, | ||
keys map[string]data.PrivateKey) { | ||
|
||
timestampKey, ok := keys[data.CanonicalTimestampRole] | ||
|
@@ -1085,54 +1086,54 @@ func fakeServerData(t *testing.T, repo *NotaryRepository, mux *http.ServeMux, | |
cksmBytes = sha256.Sum256(level2JSON) | ||
level2Checksum := hex.EncodeToString(cksmBytes[:]) | ||
|
||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
assert.NoError(t, err) | ||
fmt.Fprint(w, string(rootFileBytes)) | ||
}) | ||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root."+rootChecksum+".json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/root."+rootChecksum+".json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
assert.NoError(t, err) | ||
fmt.Fprint(w, string(rootFileBytes)) | ||
}) | ||
|
||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(timestampJSON)) | ||
}) | ||
|
||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(snapshotJSON)) | ||
}) | ||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot."+snapshotChecksum+".json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot."+snapshotChecksum+".json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(snapshotJSON)) | ||
}) | ||
|
||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(targetsJSON)) | ||
}) | ||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets."+targetsChecksum+".json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets."+targetsChecksum+".json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(targetsJSON)) | ||
}) | ||
|
||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1.json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1.json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(level1JSON)) | ||
}) | ||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1."+level1Checksum+".json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1."+level1Checksum+".json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(level1JSON)) | ||
}) | ||
|
||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2.json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2.json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(level2JSON)) | ||
}) | ||
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2."+level2Checksum+".json", | ||
m.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2."+level2Checksum+".json", | ||
func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, string(level2JSON)) | ||
}) | ||
|
@@ -1146,7 +1147,7 @@ func (k targetSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } | |
func (k targetSorter) Less(i, j int) bool { return k[i].Name < k[j].Name } | ||
|
||
func testListTarget(t *testing.T, rootType string) { | ||
ts, mux, keys := simpleTestServer(t) | ||
ts, m, keys := simpleTestServer(t) | ||
defer ts.Close() | ||
|
||
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) | ||
|
@@ -1170,7 +1171,7 @@ func testListTarget(t *testing.T, rootType string) { | |
err = applyChangelist(repo.tufRepo, cl) | ||
assert.NoError(t, err, "could not apply changelist") | ||
|
||
fakeServerData(t, repo, mux, keys) | ||
fakeServerData(t, repo, m, keys) | ||
|
||
targets, err := repo.ListTargets(data.CanonicalTargetsRole) | ||
assert.NoError(t, err) | ||
|
@@ -1202,7 +1203,7 @@ func testListTarget(t *testing.T, rootType string) { | |
} | ||
|
||
func testListTargetWithDelegates(t *testing.T, rootType string) { | ||
ts, mux, keys := simpleTestServer(t) | ||
ts, m, keys := simpleTestServer(t) | ||
defer ts.Close() | ||
|
||
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) | ||
|
@@ -1253,7 +1254,7 @@ func testListTargetWithDelegates(t *testing.T, rootType string) { | |
_, ok = repo.tufRepo.Targets["targets/level2"].Signed.Targets["level2"] | ||
assert.True(t, ok) | ||
|
||
fakeServerData(t, repo, mux, keys) | ||
fakeServerData(t, repo, m, keys) | ||
|
||
// test default listing | ||
targets, err := repo.ListTargets() | ||
|
@@ -2392,14 +2393,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) | ||
|
@@ -2567,6 +2570,54 @@ func testRotateKeySuccess(t *testing.T, serverManagesSnapshotInit bool, | |
} | ||
} | ||
|
||
// If remotely rotating key fails for a non-rate-limit reason, fail the rotation | ||
// entirely | ||
func TestRemoteRotationNonRateLimitError(t *testing.T) { | ||
ts, _, _ := simpleTestServer(t) | ||
defer ts.Close() | ||
|
||
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) | ||
defer os.RemoveAll(repo.baseDir) | ||
|
||
// simpleTestServer has no rotate key endpoint, so this should fail | ||
err := repo.RotateKey(data.CanonicalTimestampRole, true) | ||
assert.Error(t, err) | ||
assert.IsType(t, store.ErrMetaNotFound{}, err) | ||
} | ||
|
||
// The rotator is not the owner of the repository, they cannot rotate the remote | ||
// key | ||
func TestRemoteRotationNoRootKey(t *testing.T) { | ||
ts := fullTestServer(t) | ||
defer ts.Close() | ||
|
||
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) | ||
defer os.RemoveAll(repo.baseDir) | ||
assert.NoError(t, repo.Publish()) | ||
|
||
newRepo, _ := newRepoToTestRepo(t, repo, true) | ||
defer os.RemoveAll(newRepo.baseDir) | ||
_, err := newRepo.ListTargets() | ||
assert.NoError(t, err) | ||
|
||
err = newRepo.RotateKey(data.CanonicalSnapshotRole, true) | ||
assert.Error(t, err) | ||
assert.IsType(t, signed.ErrNoKeys{}, err) | ||
} | ||
|
||
// The repo hasn't been initialized, so we can't rotate | ||
func TestRemoteRotationNonexistentRepo(t *testing.T) { | ||
ts, _, _ := simpleTestServer(t) | ||
defer ts.Close() | ||
|
||
repo := newBlankRepo(t, ts.URL) | ||
defer os.RemoveAll(repo.baseDir) | ||
|
||
err := repo.RotateKey(data.CanonicalTimestampRole, true) | ||
assert.Error(t, err) | ||
assert.IsType(t, ErrRepoNotInitialized{}, err) | ||
} | ||
|
||
// If there is no local cache, notary operations return the remote error code | ||
func TestRemoteServerUnavailableNoLocalCache(t *testing.T) { | ||
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method could probably use some
Debug
output.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added one
Debug
, and otherwise just tried to return better errors. Does it look reasonable atm?