mirror of https://github.com/docker/docs.git
Change root cert rotation to be root key rotation instead
Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
parent
708507adde
commit
cea46f7c3e
|
@ -38,10 +38,8 @@ type TufChange struct {
|
|||
// TufRootData represents a modification of the keys associated
|
||||
// with a role that appears in the root.json
|
||||
type TufRootData struct {
|
||||
ReplaceKeys data.KeyList `json:"keys"`
|
||||
AddKeys data.KeyList `json:"add_keys, omitempty"`
|
||||
RemoveKeys []string `json:"remove_keys,omitempty"`
|
||||
RoleName string `json:"role"`
|
||||
Keys data.KeyList `json:"keys"`
|
||||
RoleName string `json:"role"`
|
||||
}
|
||||
|
||||
// NewTufChange initializes a tufChange object
|
||||
|
|
127
client/client.go
127
client/client.go
|
@ -885,25 +885,19 @@ 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 {
|
||||
// We currently support remotely managing timestamp and snapshot keys
|
||||
canBeRemoteKey := role == data.CanonicalTimestampRole || role == data.CanonicalSnapshotRole
|
||||
// And locally managing root, targets, and snapshot keys
|
||||
canBeLocalKey := (role == data.CanonicalSnapshotRole || role == data.CanonicalTargetsRole ||
|
||||
role == data.CanonicalRootRole)
|
||||
|
||||
switch {
|
||||
// We currently support locally or remotely managing snapshot keys...
|
||||
case role == data.CanonicalSnapshotRole:
|
||||
break
|
||||
|
||||
// locally managing targets keys only
|
||||
case role == data.CanonicalTargetsRole && !serverManagesKey:
|
||||
break
|
||||
case role == data.CanonicalTargetsRole && serverManagesKey:
|
||||
return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole}
|
||||
|
||||
// and remotely managing timestamp keys only
|
||||
case role == data.CanonicalTimestampRole && serverManagesKey:
|
||||
break
|
||||
case role == data.CanonicalTimestampRole && !serverManagesKey:
|
||||
return ErrInvalidLocalRole{Role: data.CanonicalTimestampRole}
|
||||
|
||||
default:
|
||||
case !data.ValidRole(role) || data.IsDelegation(role):
|
||||
return fmt.Errorf("notary does not currently permit rotating the %s key", role)
|
||||
case serverManagesKey && !canBeRemoteKey:
|
||||
return ErrInvalidRemoteRole{Role: role}
|
||||
case !serverManagesKey && !canBeLocalKey:
|
||||
return ErrInvalidLocalRole{Role: role}
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -924,6 +918,18 @@ func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error {
|
|||
return fmt.Errorf(errFmtMsg, err)
|
||||
}
|
||||
|
||||
// if this is a root role, generate a root cert for the public key
|
||||
if role == data.CanonicalRootRole {
|
||||
privKey, _, err := r.CryptoService.GetPrivateKey(pubKey.ID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, pubKey, err = rootCertKey(r.gun, privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cl := changelist.NewMemChangelist()
|
||||
if err := r.rootFileKeyChange(cl, role, changelist.ActionCreate, pubKey); err != nil {
|
||||
return err
|
||||
|
@ -935,8 +941,8 @@ func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, act
|
|||
kl := make(data.KeyList, 0, 1)
|
||||
kl = append(kl, key)
|
||||
meta := changelist.TufRootData{
|
||||
RoleName: role,
|
||||
ReplaceKeys: kl,
|
||||
RoleName: role,
|
||||
Keys: kl,
|
||||
}
|
||||
metaJSON, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
|
@ -976,86 +982,3 @@ func (r *NotaryRepository) DeleteTrustData() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRootCerts returns current trusted root certificates.
|
||||
// (In particular, not old certificates which we have already
|
||||
// rotated from but are still using for signing to allow rollover,
|
||||
// even if they are still trusted locally.)
|
||||
func (r *NotaryRepository) ListRootCerts() ([]*x509.Certificate, error) {
|
||||
if err := r.Update(false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyIDs := r.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs
|
||||
certs := make([]*x509.Certificate, 0, len(keyIDs))
|
||||
for _, keyID := range keyIDs {
|
||||
// r.tufRepo contains all data from root.json, which may include irrelevant
|
||||
// certificates. This is handled by certs.ValidateRoot, which adds
|
||||
// only the real trusted certificates into the store. So, this lookup by ID is not
|
||||
// just a dumb way to not parse the certificate, the handling of ErrNoCertificatesFound
|
||||
// ensures we only get the intersection of (certificates in current root.json) with
|
||||
// (locally trusted certificates). The rotation code in certs.ValidateRoot ensures
|
||||
// that we locally store a copy of all relevant certificates.
|
||||
cert, err := r.CertStore.GetCertificateByCertID(keyID)
|
||||
if err == nil {
|
||||
certs = append(certs, cert)
|
||||
} else if _, ok := err.(*trustmanager.ErrNoCertificatesFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// RotateRootCert generates a new certificate to replace oldCert and adds
|
||||
// a changelist entry to update the root role with this certificate replacement
|
||||
// when r.Publish() is called.
|
||||
func (r *NotaryRepository) RotateRootCert(oldCert *x509.Certificate) error {
|
||||
oldCertX509PublicKey := trustmanager.CertToKey(oldCert)
|
||||
if oldCertX509PublicKey == nil {
|
||||
return fmt.Errorf("invalid format for root key: %s", oldCert.PublicKeyAlgorithm)
|
||||
}
|
||||
rootKeyID, err := utils.CanonicalKeyID(oldCertX509PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, newCertX509PublicKey, err := rootCertKey(r.gun, privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
// Not changelist.ActionCreate/ReplaceKeys, which would drop other certificates.
|
||||
meta := changelist.TufRootData{
|
||||
RoleName: data.CanonicalRootRole,
|
||||
AddKeys: []data.PublicKey{newCertX509PublicKey},
|
||||
RemoveKeys: []string{oldCertX509PublicKey.ID()},
|
||||
}
|
||||
metaJSON, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := changelist.NewTufChange(
|
||||
changelist.ActionUpdate,
|
||||
changelist.ScopeRoot,
|
||||
changelist.TypeRootRole,
|
||||
data.CanonicalRootRole,
|
||||
metaJSON,
|
||||
)
|
||||
return cl.Add(c)
|
||||
|
||||
// Note that we don't need to worry about updating r.CertStore (and synchronizing
|
||||
// this with the r.Publish() call; after we push the new root.json to the server,
|
||||
// calls to r.Update() will cause this client to rotate certificates in r.CertStore
|
||||
// just as other clients do.
|
||||
}
|
||||
|
|
|
@ -2510,36 +2510,38 @@ func TestPublishSucceedsDespiteDelegationCorrupt(t *testing.T) {
|
|||
|
||||
// Rotate invalid roles, or attempt to delegate target signing to the server
|
||||
func TestRotateKeyInvalidRole(t *testing.T) {
|
||||
ts, _, _ := simpleTestServer(t)
|
||||
ts := fullTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
|
||||
defer os.RemoveAll(repo.baseDir)
|
||||
|
||||
// the equivalent of: remotely rotating the root key
|
||||
// (RotateKey("root", true)), locally rotating the root key (RotateKey("root", false)),
|
||||
// locally rotating the timestamp key (RotateKey("timestamp", false)),
|
||||
// and remotely rotating the targets key (RotateKey(targets, true)), all of which should
|
||||
// fail
|
||||
for _, role := range data.BaseRoles {
|
||||
if role == data.CanonicalSnapshotRole {
|
||||
continue
|
||||
}
|
||||
for _, serverManagesKey := range []bool{true, false} {
|
||||
// we support local rotation of the targets key and remote rotation of the
|
||||
// timestamp key
|
||||
if role == data.CanonicalTargetsRole && !serverManagesKey {
|
||||
continue
|
||||
}
|
||||
if role == data.CanonicalTimestampRole && serverManagesKey {
|
||||
continue
|
||||
}
|
||||
err := repo.RotateKey(role, serverManagesKey)
|
||||
require.Error(t, err,
|
||||
"Rotating a %s key with server-managing the key as %v should fail",
|
||||
role, serverManagesKey)
|
||||
}
|
||||
}
|
||||
// create a delegation
|
||||
pubKey, err := repo.CryptoService.Create("targets/releases", "docker.com/notary", data.ECDSAKey)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, repo.AddDelegation("targets/releases", []data.PublicKey{pubKey}, []string{""}))
|
||||
require.NoError(t, repo.Publish())
|
||||
require.NoError(t, repo.Update(false))
|
||||
|
||||
// rotating a root key to the server fails
|
||||
require.Error(t, repo.RotateKey(data.CanonicalRootRole, true),
|
||||
"Rotating a root key with server-managing the key should fail")
|
||||
|
||||
// rotating a targets key to the server fails
|
||||
require.Error(t, repo.RotateKey(data.CanonicalTargetsRole, true),
|
||||
"Rotating a targets key with server-managing the key should fail")
|
||||
|
||||
// rotating a timestamp key locally fails
|
||||
require.Error(t, repo.RotateKey(data.CanonicalTimestampRole, false),
|
||||
"Rotating a timestamp key locally should fail")
|
||||
|
||||
// rotating a delegation key fails
|
||||
require.Error(t, repo.RotateKey("targets/releases", false),
|
||||
"Rotating a delegation key should fail")
|
||||
|
||||
// rotating a not a real role key fails
|
||||
require.Error(t, repo.RotateKey("nope", false),
|
||||
"Rotating a non-real role key should fail")
|
||||
}
|
||||
|
||||
// If remotely rotating key fails, the failure is propagated
|
||||
|
@ -2560,19 +2562,19 @@ func TestRemoteRotationError(t *testing.T) {
|
|||
// Rotates the keys. After the rotation, downloading the latest metadata
|
||||
// and require that the keys have changed
|
||||
func requireRotationSuccessful(t *testing.T, repo1 *NotaryRepository, keysToRotate map[string]bool) {
|
||||
// Create 2 new repos: 1 will download repo data before the publish,
|
||||
// and one only downloads after the publish. This reflects a client
|
||||
// that has some previous trust data (but is not the publisher), and a
|
||||
// completely new client being able to read the rotated trust data.
|
||||
// Create a new repo that is used to download the data after the rotation
|
||||
repo2, _ := newRepoToTestRepo(t, repo1, true)
|
||||
defer os.RemoveAll(repo2.baseDir)
|
||||
|
||||
repos := []*NotaryRepository{repo1, repo2}
|
||||
|
||||
oldKeyIDs := make(map[string][]string)
|
||||
for role := range keysToRotate {
|
||||
keyIDs := repo1.tufRepo.Root.Signed.Roles[role].KeyIDs
|
||||
oldKeyIDs[role] = keyIDs
|
||||
oldRoles := make(map[string]data.BaseRole)
|
||||
for roleName := range keysToRotate {
|
||||
baseRole, err := repo1.tufRepo.GetBaseRole(roleName)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, baseRole.Keys, 1)
|
||||
|
||||
oldRoles[roleName] = baseRole
|
||||
}
|
||||
|
||||
// Confirm no changelists get published
|
||||
|
@ -2591,29 +2593,48 @@ func requireRotationSuccessful(t *testing.T, repo1 *NotaryRepository, keysToRota
|
|||
err := repo.Update(true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for role, isRemoteKey := range keysToRotate {
|
||||
keyIDs := repo.tufRepo.Root.Signed.Roles[role].KeyIDs
|
||||
require.Len(t, keyIDs, 1)
|
||||
for roleName, isRemoteKey := range keysToRotate {
|
||||
baseRole, err := repo1.tufRepo.GetBaseRole(roleName)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, baseRole.Keys, 1)
|
||||
|
||||
// the new key is not the same as any of the old keys, and the
|
||||
// old keys have been removed not just from the TUF file, but
|
||||
// from the cryptoservice
|
||||
for _, oldKeyID := range oldKeyIDs[role] {
|
||||
require.NotEqual(t, oldKeyID, keyIDs[0])
|
||||
_, _, err := repo.CryptoService.GetPrivateKey(oldKeyID)
|
||||
require.Error(t, err)
|
||||
// in the new key is not the same as any of the old keys
|
||||
for oldKeyID, oldPubKey := range oldRoles[roleName].Keys {
|
||||
_, ok := baseRole.Keys[oldKeyID]
|
||||
require.False(t, ok)
|
||||
|
||||
// in the old repo, the old keys have been removed not just from
|
||||
// the TUF file, but from the cryptoservice (unless it's a root
|
||||
// key, in which case it should NOT be removed)
|
||||
if repo == repo1 {
|
||||
canonicalID, err := utils.CanonicalKeyID(oldPubKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = repo.CryptoService.GetPrivateKey(canonicalID)
|
||||
switch roleName {
|
||||
case data.CanonicalRootRole:
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On the old repo, the new key is present in the cryptoservice, or
|
||||
// not present if remote. On the new repo, no keys are ever in the
|
||||
// cryptoservice
|
||||
key, _, err := repo.CryptoService.GetPrivateKey(keyIDs[0])
|
||||
if repo != repo1 || isRemoteKey {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, key)
|
||||
} else {
|
||||
// not present if remote.
|
||||
if repo == repo1 {
|
||||
pubKey := baseRole.ListKeys()[0]
|
||||
canonicalID, err := utils.CanonicalKeyID(pubKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
|
||||
key, _, err := repo.CryptoService.GetPrivateKey(canonicalID)
|
||||
if isRemoteKey {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, key)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2635,6 +2656,7 @@ func TestRotateBeforePublishFromRemoteKeyToLocalKey(t *testing.T) {
|
|||
// non-key-rotation changes)
|
||||
addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt")
|
||||
requireRotationSuccessful(t, repo, map[string]bool{
|
||||
data.CanonicalRootRole: false,
|
||||
data.CanonicalTargetsRole: false,
|
||||
data.CanonicalSnapshotRole: false})
|
||||
|
||||
|
@ -2648,11 +2670,12 @@ func TestRotateBeforePublishFromRemoteKeyToLocalKey(t *testing.T) {
|
|||
// Rotate keys
|
||||
// Download the latest metadata and require that the keys have changed.
|
||||
func TestRotateKeyAfterPublishNoServerManagementChange(t *testing.T) {
|
||||
// rotate a single target key
|
||||
testRotateKeySuccess(t, false, map[string]bool{data.CanonicalRootRole: false})
|
||||
testRotateKeySuccess(t, false, map[string]bool{data.CanonicalTargetsRole: false})
|
||||
testRotateKeySuccess(t, false, map[string]bool{data.CanonicalSnapshotRole: false})
|
||||
// rotate two at once before publishing
|
||||
// rotate multiple keys at once before publishing
|
||||
testRotateKeySuccess(t, false, map[string]bool{
|
||||
data.CanonicalRootRole: false,
|
||||
data.CanonicalSnapshotRole: false,
|
||||
data.CanonicalTargetsRole: false})
|
||||
}
|
||||
|
@ -2666,11 +2689,13 @@ func TestRotateKeyAfterPublishServerManagementChange(t *testing.T) {
|
|||
testRotateKeySuccess(t, false, map[string]bool{
|
||||
data.CanonicalSnapshotRole: true,
|
||||
data.CanonicalTargetsRole: false,
|
||||
data.CanonicalRootRole: false,
|
||||
})
|
||||
// reclaim snapshot key management from the server
|
||||
testRotateKeySuccess(t, true, map[string]bool{
|
||||
data.CanonicalSnapshotRole: false,
|
||||
data.CanonicalTargetsRole: false,
|
||||
data.CanonicalRootRole: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2704,6 +2729,130 @@ func testRotateKeySuccess(t *testing.T, serverManagesSnapshotInit bool,
|
|||
}
|
||||
}
|
||||
|
||||
func logRepoTrustRoot(t *testing.T, prefix string, repo *NotaryRepository) {
|
||||
logrus.Debugf("==== %s", prefix)
|
||||
root := repo.tufRepo.Root
|
||||
logrus.Debugf("Root signatures:")
|
||||
for _, s := range root.Signatures {
|
||||
logrus.Debugf("\t%s", s.KeyID)
|
||||
}
|
||||
logrus.Debugf("Valid root keys:")
|
||||
for _, k := range root.Signed.Roles[data.CanonicalRootRole].KeyIDs {
|
||||
logrus.Debugf("\t%s", k)
|
||||
}
|
||||
logrus.Debugf("All trusted certs:")
|
||||
certs, err := repo.CertStore.GetCertificatesByCN(repo.gun)
|
||||
require.NoError(t, err)
|
||||
for _, cert := range certs {
|
||||
id, err := trustmanager.FingerprintCert(cert)
|
||||
require.NoError(t, err)
|
||||
logrus.Debugf("\t%s", id)
|
||||
}
|
||||
}
|
||||
|
||||
// ID of the (only) certificate trusted by the root role metadata
|
||||
func rootRoleCertID(t *testing.T, repo *NotaryRepository) string {
|
||||
rootKeys := repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs
|
||||
require.Len(t, rootKeys, 1)
|
||||
return rootKeys[0]
|
||||
}
|
||||
|
||||
func verifyOnlyTrustedCertificate(t *testing.T, repo *NotaryRepository, certID string) {
|
||||
certs, err := repo.CertStore.GetCertificatesByCN(repo.gun)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, certs, 1)
|
||||
id, err := trustmanager.FingerprintCert(certs[0])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, certID, id)
|
||||
}
|
||||
|
||||
func TestRotateRootKey(t *testing.T) {
|
||||
ts := fullTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Set up author's view of the repo and publish first version.
|
||||
authorRepo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
|
||||
defer os.RemoveAll(authorRepo.baseDir)
|
||||
err := authorRepo.Publish()
|
||||
require.NoError(t, err)
|
||||
oldRootCertID := rootRoleCertID(t, authorRepo)
|
||||
oldRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole)
|
||||
require.NoError(t, err)
|
||||
oldCanonicalKeyID, err := utils.CanonicalKeyID(oldRootRole.Keys[oldRootCertID])
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initialize an user, using the original root cert and key.
|
||||
userRepo, _ := newRepoToTestRepo(t, authorRepo, true)
|
||||
defer os.RemoveAll(userRepo.baseDir)
|
||||
err = userRepo.Update(false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Rotate root certificate and key.
|
||||
logRepoTrustRoot(t, "original", authorRepo)
|
||||
err = authorRepo.RotateKey(data.CanonicalRootRole, false)
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "post-rotate", authorRepo)
|
||||
|
||||
require.NoError(t, authorRepo.Update(false))
|
||||
newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole)
|
||||
require.False(t, newRootRole.Equals(oldRootRole))
|
||||
// not only is the root cert different, but the private key is too
|
||||
newRootCertID := rootRoleCertID(t, authorRepo)
|
||||
require.NotEqual(t, oldRootCertID, newRootCertID)
|
||||
newCanonicalKeyID, err := utils.CanonicalKeyID(newRootRole.Keys[newRootCertID])
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, oldCanonicalKeyID, newCanonicalKeyID)
|
||||
|
||||
// Set up a target to verify the repo is actually usable.
|
||||
_, err = userRepo.GetTargetByName("current")
|
||||
require.Error(t, err)
|
||||
addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt")
|
||||
|
||||
// NotaryRepository.Update's handling of certificate rotation is weird:
|
||||
//
|
||||
// On every run, NotaryRepository.bootstrapClient rotates the trusted certificates
|
||||
// based on CACHED root data.
|
||||
// Then the client calls Repo.Update, which fetches a new timestmap,
|
||||
// notices an updated root.json, validates it and stores it into the cache.
|
||||
//
|
||||
// So, the locally trusted certificates are rotated only on the SECOND call of
|
||||
// NotaryRepository.Update after the rotation is pushed to the server.
|
||||
//
|
||||
// This would be nice to fix eventually (breaking down the NotaryRepository.Update
|
||||
// / Repo.Update separation which causes this), but for now, just ensure that the
|
||||
// second update does result in updated certificates.
|
||||
|
||||
// Publish the target, which does an update and pulls down the latest metadata, and
|
||||
// should update the cert store now
|
||||
logRepoTrustRoot(t, "pre-publish", authorRepo)
|
||||
err = authorRepo.Publish()
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "post-publish", authorRepo)
|
||||
verifyOnlyTrustedCertificate(t, authorRepo, newRootCertID)
|
||||
|
||||
// Verify the user can use the rotated repo, and see the added target.
|
||||
_, err = userRepo.GetTargetByName("current")
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "client", userRepo)
|
||||
|
||||
// Verify that clients initialized post-rotation can use the repo, and use
|
||||
// the new certificate immediately.
|
||||
freshUserRepo, _ := newRepoToTestRepo(t, authorRepo, true)
|
||||
defer os.RemoveAll(freshUserRepo.baseDir)
|
||||
_, err = freshUserRepo.GetTargetByName("current")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo))
|
||||
verifyOnlyTrustedCertificate(t, freshUserRepo, newRootCertID)
|
||||
logRepoTrustRoot(t, "fresh client", freshUserRepo)
|
||||
|
||||
// Verify that the user initialized with the original certificate eventually
|
||||
// rotates to the new certificate.
|
||||
err = userRepo.Update(false)
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "user refresh 1", userRepo)
|
||||
require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo))
|
||||
}
|
||||
|
||||
// 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-")
|
||||
|
@ -3339,131 +3488,3 @@ func TestListRoles(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, rolesWithSigs, len(data.BaseRoles)+2)
|
||||
}
|
||||
|
||||
func logRepoTrustRoot(t *testing.T, prefix string, repo *NotaryRepository) {
|
||||
logrus.Debugf("==== %s", prefix)
|
||||
root := repo.tufRepo.Root
|
||||
logrus.Debugf("Root signatures:")
|
||||
for _, s := range root.Signatures {
|
||||
logrus.Debugf("\t%s", s.KeyID)
|
||||
}
|
||||
logrus.Debugf("Valid root keys:")
|
||||
for _, k := range root.Signed.Roles[data.CanonicalRootRole].KeyIDs {
|
||||
logrus.Debugf("\t%s", k)
|
||||
}
|
||||
logrus.Debugf("All trusted certs:")
|
||||
certs, err := repo.CertStore.GetCertificatesByCN(repo.gun)
|
||||
require.NoError(t, err)
|
||||
for _, cert := range certs {
|
||||
id, err := trustmanager.FingerprintCert(cert)
|
||||
require.NoError(t, err)
|
||||
logrus.Debugf("\t%s", id)
|
||||
}
|
||||
}
|
||||
|
||||
// ID of the (only) certificate trusted by the root role metadata
|
||||
func rootRoleCertID(t *testing.T, repo *NotaryRepository) string {
|
||||
rootKeys := repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs
|
||||
require.Len(t, rootKeys, 1)
|
||||
return rootKeys[0]
|
||||
}
|
||||
|
||||
func verifyOnlyTrustedCertificate(t *testing.T, repo *NotaryRepository, certID string) {
|
||||
certs, err := repo.CertStore.GetCertificatesByCN(repo.gun)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, certs, 1)
|
||||
id, err := trustmanager.FingerprintCert(certs[0])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, certID, id)
|
||||
}
|
||||
|
||||
func TestRotateRootCert(t *testing.T) {
|
||||
ts := fullTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Set up author's view of the repo and publish first version.
|
||||
authorRepo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
|
||||
defer os.RemoveAll(authorRepo.baseDir)
|
||||
err := authorRepo.Publish()
|
||||
require.NoError(t, err)
|
||||
oldRootCertID := rootRoleCertID(t, authorRepo)
|
||||
|
||||
// Initialize an user, using the original certificate.
|
||||
userRepo, _ := newRepoToTestRepo(t, authorRepo, true)
|
||||
defer os.RemoveAll(userRepo.baseDir)
|
||||
err = userRepo.Update(false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Rotate root certificate.
|
||||
certs, err := authorRepo.ListRootCerts()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, certs, 1)
|
||||
logRepoTrustRoot(t, "original", authorRepo)
|
||||
err = authorRepo.RotateRootCert(certs[0])
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "post-rotate", authorRepo)
|
||||
|
||||
// Set up a target to verify the repo is actually usable.
|
||||
publishedTarget := addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt")
|
||||
|
||||
// Publish the rotation
|
||||
logRepoTrustRoot(t, "pre-publish", authorRepo)
|
||||
err = authorRepo.Publish()
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "post-publish", authorRepo)
|
||||
newRootCertID := rootRoleCertID(t, authorRepo)
|
||||
require.NotEqual(t, oldRootCertID, newRootCertID)
|
||||
|
||||
// Verify the user can use the rotated repo, and see the added target.
|
||||
receivedTarget, err := userRepo.GetTargetByName("current")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo))
|
||||
require.True(t, reflect.DeepEqual(*publishedTarget, receivedTarget.Target), "target does not match")
|
||||
logRepoTrustRoot(t, "client", userRepo)
|
||||
|
||||
// Verify that clients initialized post-rotation can use the repo, and use
|
||||
// the new certificate immediately.
|
||||
freshUserRepo, _ := newRepoToTestRepo(t, authorRepo, true)
|
||||
defer os.RemoveAll(freshUserRepo.baseDir)
|
||||
receivedTarget, err = freshUserRepo.GetTargetByName("current")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo))
|
||||
verifyOnlyTrustedCertificate(t, freshUserRepo, newRootCertID)
|
||||
require.True(t, reflect.DeepEqual(*publishedTarget, receivedTarget.Target), "target does not match")
|
||||
logRepoTrustRoot(t, "fresh client", freshUserRepo)
|
||||
|
||||
// NotaryRepository.Update's handling of certificate rotation is weird:
|
||||
//
|
||||
// On every run, NotaryRepository.bootstrapClient rotates the trusted certificates
|
||||
// based on CACHED root data.
|
||||
// Then the client calls Repo.Update, which fetches a new timestmap,
|
||||
// notices an updated root.json, validates it and stores it into the cache.
|
||||
//
|
||||
// So, the locally trusted certificates are rotated only on the SECOND call of
|
||||
// NotaryRepository.Update after the rotation is pushed to the server.
|
||||
//
|
||||
// This would be nice to fix eventually (breaking down the NotaryRepository.Update
|
||||
// / Repo.Update separation which causes this), but for now, just ensure that the
|
||||
// second update does result in updated certificates.
|
||||
|
||||
// Verify that the author eventually rotates to the new certificate.
|
||||
err = authorRepo.Update(false)
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "author refresh 1", authorRepo)
|
||||
require.Equal(t, newRootCertID, rootRoleCertID(t, authorRepo))
|
||||
// can't verify anything here, because the trusted certificates aren't
|
||||
// rotated from the updated root
|
||||
|
||||
err = authorRepo.Update(false)
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "author refresh 2", authorRepo)
|
||||
require.Equal(t, newRootCertID, rootRoleCertID(t, authorRepo))
|
||||
verifyOnlyTrustedCertificate(t, authorRepo, newRootCertID)
|
||||
|
||||
// Verify that the user initialized with the original certificate eventually
|
||||
// rotates to the new certificate.
|
||||
err = userRepo.Update(false)
|
||||
require.NoError(t, err)
|
||||
logRepoTrustRoot(t, "user refresh 1", userRepo)
|
||||
require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo))
|
||||
}
|
||||
|
|
|
@ -180,21 +180,7 @@ func applyRootRoleChange(repo *tuf.Repo, c changelist.Change) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = repo.ReplaceBaseKeys(d.RoleName, d.ReplaceKeys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case changelist.ActionUpdate:
|
||||
d := &changelist.TufRootData{}
|
||||
err := json.Unmarshal(c.Content(), d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = repo.AddBaseKeys(d.RoleName, d.AddKeys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = repo.RemoveBaseKeys(d.RoleName, d.RemoveKeys...)
|
||||
err = repo.ReplaceBaseKeys(d.RoleName, d.Keys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ var cmdKeyListTemplate = usageTemplate{
|
|||
var cmdRotateKeyTemplate = usageTemplate{
|
||||
Use: "rotate [ GUN ] [ key role ]",
|
||||
Short: "Rotate a signing (non-root) key of the given type for the given Globally Unique Name and role.",
|
||||
Long: "Generates a new key for the given Globally Unique Name and role (one of \"snapshot\", \"targets\", or \"timestamp\"). If rotating to a server-managed key, a new key is requested from the server rather than generated. If the generation or key request is successful, the key rotation is immediately published. No other changes, even if they are staged, will be published.",
|
||||
Long: `Generates a new key for the given Globally Unique Name and role (one of "snapshot", "targets", "root", or "timestamp"). If rotating to a server-managed key, a new key is requested from the server rather than generated. If the generation or key request is successful, the key rotation is immediately published. No other changes, even if they are staged, will be published.`,
|
||||
}
|
||||
|
||||
var cmdKeyGenerateRootKeyTemplate = usageTemplate{
|
||||
|
|
|
@ -239,11 +239,10 @@ func TestRemoveMultikeysRemoveOnlyChosenKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Non-roles, root, and delegation keys can't be rotated with the command line
|
||||
// Non-roles and delegation keys can't be rotated with the command line
|
||||
func TestRotateKeyInvalidRoles(t *testing.T) {
|
||||
setUp(t)
|
||||
invalids := []string{
|
||||
data.CanonicalRootRole,
|
||||
"notevenARole",
|
||||
"targets/a",
|
||||
}
|
||||
|
|
|
@ -599,8 +599,11 @@ func TestSwizzlerMutateRoot(t *testing.T) {
|
|||
origSigned, newSigned := &data.SignedRoot{}, &data.SignedRoot{}
|
||||
require.NoError(t, json.Unmarshal(metaBytes, origSigned))
|
||||
require.NoError(t, json.Unmarshal(newMeta, newSigned))
|
||||
require.Len(t, origSigned.Signed.Roles, 5)
|
||||
require.Len(t, newSigned.Signed.Roles, 6)
|
||||
// it may not exactly equal 4 or 5 because if the metadata was
|
||||
// produced by calling SignedRoot, it could have saved a previous
|
||||
// root role
|
||||
require.True(t, len(origSigned.Signed.Roles) >= 4)
|
||||
require.True(t, len(newSigned.Signed.Roles) >= 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ func (tr *Repo) AddBaseKeys(role string, keys ...data.PublicKey) error {
|
|||
}
|
||||
tr.Root.Dirty = true
|
||||
|
||||
// also, whichever role was added to out needs to be re-signed
|
||||
// also, whichever role was switched out needs to be re-signed
|
||||
// root has already been marked dirty.
|
||||
switch role {
|
||||
case data.CanonicalSnapshotRole:
|
||||
|
@ -838,7 +838,8 @@ func (v versionedRootRoles) Less(i, j int) bool { return v[i].version < v[j].ver
|
|||
// SignRoot signs the root, using all keys from the "root" role (i.e. currently trusted)
|
||||
// as well as available keys used to sign the previous version, if the public part is
|
||||
// carried in tr.Root.Keys and the private key is available (i.e. probably previously
|
||||
// trusted keys, to allow rollover).
|
||||
// trusted keys, to allow rollover). If there are any errors, attempt to put root
|
||||
// back to the way it was (so version won't be incremented, for instance).
|
||||
func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) {
|
||||
logrus.Debug("signing root...")
|
||||
currRoot, err := tr.GetBaseRole(data.CanonicalRootRole)
|
||||
|
|
|
@ -1301,6 +1301,7 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) {
|
|||
|
||||
// bump root version and also add the above keys and extra roles to root
|
||||
repo.Root.Signed.Version = 6
|
||||
oldExpiry := repo.Root.Signed.Expires
|
||||
// add every key to the root's key list except 1
|
||||
for i, key := range rootCertKeys {
|
||||
if i != 1 {
|
||||
|
@ -1341,8 +1342,9 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) {
|
|||
require.NoError(t, cs.AddKey(data.CanonicalRootRole, gun, privKey))
|
||||
}
|
||||
// we haven't saved any unsaved roles because there was an error signing,
|
||||
// nor have we bumped the version
|
||||
// nor have we bumped the version or altered the expiry
|
||||
require.Equal(t, 6, repo.Root.Signed.Version)
|
||||
require.Equal(t, oldExpiry, repo.Root.Signed.Expires)
|
||||
require.Len(t, repo.Root.Signed.Roles, lenRootRoles)
|
||||
|
||||
// remove all the keys we don't need and demonstrate we can still sign
|
||||
|
@ -1373,6 +1375,7 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) {
|
|||
// bumped version, 2 new roles, but one overwrote the previous root.7, so one additional role
|
||||
require.Equal(t, 7, repo.Root.Signed.Version)
|
||||
require.Len(t, repo.Root.Signed.Roles, lenRootRoles+1)
|
||||
require.True(t, oldExpiry.Before(repo.Root.Signed.Expires))
|
||||
lenRootRoles = len(repo.Root.Signed.Roles)
|
||||
|
||||
// remove the optional key
|
||||
|
@ -1380,13 +1383,15 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) {
|
|||
|
||||
// SignRoot will still succeed even if the key that wasn't in a root isn't
|
||||
// available
|
||||
oldExpiry = repo.Root.Signed.Expires
|
||||
signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole))
|
||||
require.NoError(t, err)
|
||||
verifySignatureList(t, signedObj, expectedSigningKeys[1:]...)
|
||||
|
||||
// no additional roles were added
|
||||
require.Len(t, repo.Root.Signed.Roles, lenRootRoles)
|
||||
require.Equal(t, 8, repo.Root.Signed.Version) // bumped version
|
||||
require.Equal(t, 8, repo.Root.Signed.Version) // bumped version
|
||||
require.True(t, oldExpiry.Before(repo.Root.Signed.Expires)) // expiry updated
|
||||
|
||||
// now rotate a non-root key
|
||||
newTargetsKey, err := cs.Create(data.CanonicalTargetsRole, gun, data.ECDSAKey)
|
||||
|
@ -1394,11 +1399,13 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) {
|
|||
require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalTargetsRole, newTargetsKey))
|
||||
|
||||
// we still sign with all old roles no additional roles were added
|
||||
oldExpiry = repo.Root.Signed.Expires
|
||||
signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole))
|
||||
require.NoError(t, err)
|
||||
verifySignatureList(t, signedObj, expectedSigningKeys[1:]...)
|
||||
require.Len(t, repo.Root.Signed.Roles, lenRootRoles)
|
||||
require.Equal(t, 9, repo.Root.Signed.Version) // bumped version
|
||||
require.Equal(t, 9, repo.Root.Signed.Version) // bumped version
|
||||
require.True(t, oldExpiry.Before(repo.Root.Signed.Expires)) // expiry updated
|
||||
|
||||
// rotating a targets key again, if we are missing the previous root's keys, signing will fail
|
||||
newTargetsKey, err = cs.Create(data.CanonicalTargetsRole, gun, data.ECDSAKey)
|
||||
|
@ -1407,6 +1414,7 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) {
|
|||
|
||||
require.NoError(t, cs.RemoveKey(rootPrivKeys[6].ID()))
|
||||
|
||||
oldExpiry = repo.Root.Signed.Expires
|
||||
_, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole))
|
||||
require.Error(t, err)
|
||||
require.IsType(t, signed.ErrInsufficientSignatures{}, err)
|
||||
|
@ -1415,4 +1423,5 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) {
|
|||
// no additional roles were saved, version has not changed
|
||||
require.Len(t, repo.Root.Signed.Roles, lenRootRoles)
|
||||
require.Equal(t, 9, repo.Root.Signed.Version) // version has not changed
|
||||
require.Equal(t, oldExpiry, repo.Root.Signed.Expires)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue