diff --git a/client/backwards_compatibility_test.go b/client/backwards_compatibility_test.go index efe555fba..e36b3cc89 100644 --- a/client/backwards_compatibility_test.go +++ b/client/backwards_compatibility_test.go @@ -207,7 +207,7 @@ func Test0Dot1RepoFormat(t *testing.T) { require.NoError(t, repo.cache.Remove(data.CanonicalTimestampRole)) // rotate the timestamp key, since the server doesn't have that one - err = repo.RotateKey(data.CanonicalTimestampRole, true) + err = repo.RotateKey(data.CanonicalTimestampRole, true, nil) require.NoError(t, err) require.NoError(t, repo.Publish()) @@ -218,7 +218,7 @@ func Test0Dot1RepoFormat(t *testing.T) { // Also check that we can add/remove keys by rotating keys oldTargetsKeys := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) - require.NoError(t, repo.RotateKey(data.CanonicalTargetsRole, false)) + require.NoError(t, repo.RotateKey(data.CanonicalTargetsRole, false, nil)) require.NoError(t, repo.Publish()) newTargetsKeys := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) @@ -228,7 +228,7 @@ func Test0Dot1RepoFormat(t *testing.T) { // rotate the snapshot key to the server and ensure that the server can re-generate the snapshot // and we can download the snapshot - require.NoError(t, repo.RotateKey(data.CanonicalSnapshotRole, true)) + require.NoError(t, repo.RotateKey(data.CanonicalSnapshotRole, true, nil)) require.NoError(t, repo.Publish()) err = repo.Update(false) require.NoError(t, err) @@ -266,7 +266,7 @@ func Test0Dot3RepoFormat(t *testing.T) { require.NoError(t, repo.cache.Remove(data.CanonicalTimestampRole)) // rotate the timestamp key, since the server doesn't have that one - err = repo.RotateKey(data.CanonicalTimestampRole, true) + err = repo.RotateKey(data.CanonicalTimestampRole, true, nil) require.NoError(t, err) require.NoError(t, repo.Publish()) @@ -282,7 +282,7 @@ func Test0Dot3RepoFormat(t *testing.T) { // Also check that we can add/remove keys by rotating keys oldTargetsKeys := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) - require.NoError(t, repo.RotateKey(data.CanonicalTargetsRole, false)) + require.NoError(t, repo.RotateKey(data.CanonicalTargetsRole, false, nil)) require.NoError(t, repo.Publish()) newTargetsKeys := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) @@ -292,7 +292,7 @@ func Test0Dot3RepoFormat(t *testing.T) { // rotate the snapshot key to the server and ensure that the server can re-generate the snapshot // and we can download the snapshot - require.NoError(t, repo.RotateKey(data.CanonicalSnapshotRole, true)) + require.NoError(t, repo.RotateKey(data.CanonicalSnapshotRole, true, nil)) require.NoError(t, repo.Publish()) err = repo.Update(false) require.NoError(t, err) diff --git a/client/client.go b/client/client.go index ccfdfda21..817161f45 100644 --- a/client/client.go +++ b/client/client.go @@ -9,7 +9,7 @@ import ( "net/url" "os" "path/filepath" - "strings" + "regexp" "time" "github.com/Sirupsen/logrus" @@ -27,6 +27,9 @@ import ( const ( tufDir = "tuf" + + // SignWithAllOldVersions is a sentinel constant for LegacyVersions flag + SignWithAllOldVersions = -1 ) func init() { @@ -36,16 +39,17 @@ func init() { // NotaryRepository stores all the information needed to operate on a notary // repository. type NotaryRepository struct { - baseDir string - gun string - baseURL string - tufRepoPath string - cache store.MetadataStore - CryptoService signed.CryptoService - tufRepo *tuf.Repo - invalid *tuf.Repo // known data that was parsable but deemed invalid - roundTrip http.RoundTripper - trustPinning trustpinning.TrustPinConfig + baseDir string + gun string + baseURL string + tufRepoPath string + cache store.MetadataStore + CryptoService signed.CryptoService + tufRepo *tuf.Repo + invalid *tuf.Repo // known data that was parsable but deemed invalid + roundTrip http.RoundTripper + trustPinning trustpinning.TrustPinConfig + LegacyVersions int // number of versions back to fetch roots to sign with } // NewFileCachedNotaryRepository is a wrapper for NewNotaryRepository that initializes @@ -90,13 +94,14 @@ func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper, cryptoService := cryptoservice.NewCryptoService(keyStores...) nRepo := &NotaryRepository{ - gun: gun, - baseDir: baseDir, - baseURL: baseURL, - tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), - CryptoService: cryptoService, - roundTrip: rt, - trustPinning: trustPin, + gun: gun, + baseDir: baseDir, + baseURL: baseURL, + tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), + CryptoService: cryptoService, + roundTrip: rt, + trustPinning: trustPin, + LegacyVersions: 0, // By default, don't sign with legacy roles } nRepo.cache = cache @@ -640,10 +645,16 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error { // we send anything to remote updatedFiles := make(map[string][]byte) + // Fetch old keys to support old clients + legacyKeys, err := r.oldKeysForLegacyClientSupport(r.LegacyVersions, initialPublish) + if err != nil { + return err + } + // check if our root file is nearing expiry or dirty. Resign if it is. If // root is not dirty but we are publishing for the first time, then just // publish the existing root we have. - if err := signRootIfNecessary(updatedFiles, r.tufRepo, initialPublish); err != nil { + if err := signRootIfNecessary(updatedFiles, r.tufRepo, legacyKeys, initialPublish); err != nil { return err } @@ -661,7 +672,7 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error { } if snapshotJSON, err := serializeCanonicalRole( - r.tufRepo, data.CanonicalSnapshotRole); err == nil { + r.tufRepo, data.CanonicalSnapshotRole, nil); err == nil { // Only update the snapshot if we've successfully signed it. updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON } else if signErr, ok := err.(signed.ErrInsufficientSignatures); ok && signErr.FoundKeys == 0 { @@ -683,9 +694,12 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error { return remote.SetMulti(updatedFiles) } -func signRootIfNecessary(updates map[string][]byte, repo *tuf.Repo, initialPublish bool) error { +func signRootIfNecessary(updates map[string][]byte, repo *tuf.Repo, extraSigningKeys data.KeyList, initialPublish bool) error { + if len(extraSigningKeys) > 0 { + repo.Root.Dirty = true + } if nearExpiry(repo.Root.Signed.SignedCommon) || repo.Root.Dirty { - rootJSON, err := serializeCanonicalRole(repo, data.CanonicalRootRole) + rootJSON, err := serializeCanonicalRole(repo, data.CanonicalRootRole, extraSigningKeys) if err != nil { return err } @@ -700,11 +714,84 @@ func signRootIfNecessary(updates map[string][]byte, repo *tuf.Repo, initialPubli return nil } +// Fetch back a `legacyVersions` number of roots files, collect the root public keys +// This includes old `root` roles as well as legacy versioned root roles, e.g. `1.root` +func (r *NotaryRepository) oldKeysForLegacyClientSupport(legacyVersions int, initialPublish bool) (data.KeyList, error) { + if initialPublish { + return nil, nil + } + + var oldestVersion int + prevVersion := r.tufRepo.Root.Signed.Version + + if legacyVersions == SignWithAllOldVersions { + oldestVersion = 1 + } else { + oldestVersion = r.tufRepo.Root.Signed.Version - legacyVersions + } + + if oldestVersion < 1 { + oldestVersion = 1 + } + + if prevVersion <= 1 || oldestVersion == prevVersion { + return nil, nil + } + oldKeys := make(map[string]data.PublicKey) + + c, err := r.bootstrapClient(true) + // require a server connection to fetch old roots + if err != nil { + return nil, err + } + + for v := prevVersion; v >= oldestVersion; v-- { + logrus.Debugf("fetching old keys from version %d", v) + // fetch old root version + versionedRole := fmt.Sprintf("%d.%s", v, data.CanonicalRootRole) + + raw, err := c.remote.GetSized(versionedRole, -1) + if err != nil { + logrus.Debugf("error downloading %s: %s", versionedRole, err) + continue + } + + signedOldRoot := &data.Signed{} + if err := json.Unmarshal(raw, signedOldRoot); err != nil { + return nil, err + } + oldRootVersion, err := data.RootFromSigned(signedOldRoot) + if err != nil { + return nil, err + } + + // extract legacy versioned root keys + oldRootVersionKeys := getOldRootPublicKeys(oldRootVersion) + for _, oldKey := range oldRootVersionKeys { + oldKeys[oldKey.ID()] = oldKey + } + } + oldKeyList := make(data.KeyList, 0, len(oldKeys)) + for _, key := range oldKeys { + oldKeyList = append(oldKeyList, key) + } + return oldKeyList, nil +} + +// get all the saved previous roles keys < the current root version +func getOldRootPublicKeys(root *data.SignedRoot) data.KeyList { + rootRole, err := root.BuildBaseRole(data.CanonicalRootRole) + if err != nil { + return nil + } + return rootRole.ListKeys() +} + func signTargets(updates map[string][]byte, repo *tuf.Repo, initialPublish bool) error { // iterate through all the targets files - if they are dirty, sign and update for roleName, roleObj := range repo.Targets { if roleObj.Dirty || (roleName == data.CanonicalTargetsRole && initialPublish) { - targetsJSON, err := serializeCanonicalRole(repo, roleName) + targetsJSON, err := serializeCanonicalRole(repo, roleName, nil) if err != nil { return err } @@ -753,7 +840,7 @@ func (r *NotaryRepository) bootstrapRepo() error { func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error { logrus.Debugf("Saving changes to Trusted Collection.") - rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole) + rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole, nil) if err != nil { return err } @@ -784,7 +871,7 @@ func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error { return nil } - snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole) + snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole, nil) if err != nil { return err } @@ -815,10 +902,11 @@ func (r *NotaryRepository) Update(forWrite bool) error { } repo, invalid, err := c.Update() if err != nil { - // notFound.Resource may include a checksum so when the role is root, - // it will be root or root.. Therefore best we can - // do it match a "root." prefix - if notFound, ok := err.(store.ErrMetaNotFound); ok && strings.HasPrefix(notFound.Resource, data.CanonicalRootRole+".") { + // notFound.Resource may include a version or checksum so when the role is root, + // it will be root, .root or root.. + notFound, ok := err.(store.ErrMetaNotFound) + isRoot, _ := regexp.MatchString(`\.?`+data.CanonicalRootRole+`\.?`, notFound.Resource) + if ok && isRoot { return r.errRepositoryNotExist() } return err @@ -837,8 +925,11 @@ func (r *NotaryRepository) Update(forWrite bool) error { // is initialized or not. If set to true, we will always attempt to download // and return an error if the remote repository errors. // -// Populates a tuf.RepoBuilder with this root metadata (only use -// TUFClient.Update to load the rest). +// Populates a tuf.RepoBuilder with this root metadata. If the root metadata +// downloaded is a newer version than what is on disk, then intermediate +// versions will be downloaded and verified in order to rotate trusted keys +// properly. Newer root metadata must always be signed with the previous +// threshold and keys. // // Fails if the remote server is reachable and does not know the repo // (i.e. before the first r.Publish()), in which case the error is @@ -918,48 +1009,94 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*TUFClient, e return NewTUFClient(oldBuilder, newBuilder, remote, r.cache), nil } -// RotateKey removes all existing keys associated with the role, and either -// creates and adds one new key or delegates managing the key to the server. +// RotateKey removes all existing keys associated with the role. If no keys are +// specified in keyList, then this creates and adds one new key or delegates +// managing the key to the server. If key(s) are specified by keyList, then they are +// used for signing the role. // These changes are staged in a changelist until publish is called. -func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error { +func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool, keyList []string) error { if err := checkRotationInput(role, serverManagesKey); err != nil { return err } - var ( - pubKey data.PublicKey - err error - errFmtMsg string - ) - switch serverManagesKey { - case true: + + pubKeyList, err := r.pubKeyListForRotation(role, serverManagesKey, keyList) + if err != nil { + return err + } + + cl := changelist.NewMemChangelist() + if err := r.rootFileKeyChange(cl, role, changelist.ActionCreate, pubKeyList); err != nil { + return err + } + return r.publish(cl) +} + +// Given a set of new keys to rotate to and a set of keys to drop, returns the list of current keys to use +func (r *NotaryRepository) pubKeyListForRotation(role string, serverManaged bool, newKeys []string) (pubKeyList data.KeyList, err error) { + var pubKey data.PublicKey + + // If server manages the key being rotated, request a rotation and return the new key + if serverManaged { pubKey, err = rotateRemoteKey(r.baseURL, r.gun, role, r.roundTrip) - errFmtMsg = "unable to rotate remote key: %s" - default: - pubKey, err = r.CryptoService.Create(role, r.gun, data.ECDSAKey) - errFmtMsg = "unable to generate key: %s" + pubKeyList = make(data.KeyList, 0, 1) + pubKeyList = append(pubKeyList, pubKey) + if err != nil { + return nil, fmt.Errorf("unable to rotate remote key: %s", err) + } + return pubKeyList, nil } + // If no new keys are passed in, we generate one + if len(newKeys) == 0 { + pubKeyList = make(data.KeyList, 0, 1) + pubKey, err = r.CryptoService.Create(role, r.gun, data.ECDSAKey) + pubKeyList = append(pubKeyList, pubKey) + } if err != nil { - return fmt.Errorf(errFmtMsg, err) + return nil, fmt.Errorf("unable to generate key: %s", 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 a list of keys to rotate to are provided, we add those + if len(newKeys) > 0 { + pubKeyList = make(data.KeyList, 0, len(newKeys)) + for _, keyID := range newKeys { + pubKey = r.CryptoService.GetKey(keyID) + if pubKey == nil { + return nil, fmt.Errorf("unable to find key: %s") + } + pubKeyList = append(pubKeyList, pubKey) + } + } + + // Convert to certs (for root keys) + if pubKeyList, err = r.pubKeysToCerts(role, pubKeyList); err != nil { + return nil, err + } + + return pubKeyList, nil +} + +func (r *NotaryRepository) pubKeysToCerts(role string, pubKeyList data.KeyList) (data.KeyList, error) { + // only generate certs for root keys + if role != data.CanonicalRootRole { + return pubKeyList, nil + } + + for i, pubKey := range pubKeyList { + privKey, loadedRole, err := r.CryptoService.GetPrivateKey(pubKey.ID()) if err != nil { - return err + return nil, err + } + if loadedRole != role { + return nil, fmt.Errorf("attempted to load root key but given %s key instead", loadedRole) } pubKey, err = rootCertKey(r.gun, privKey) if err != nil { - return err + return nil, err } + pubKeyList[i] = pubKey } - - cl := changelist.NewMemChangelist() - if err := r.rootFileKeyChange(cl, role, changelist.ActionCreate, pubKey); err != nil { - return err - } - return r.publish(cl) + return pubKeyList, nil } func checkRotationInput(role string, serverManaged bool) error { @@ -980,12 +1117,10 @@ func checkRotationInput(role string, serverManaged bool) error { return nil } -func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, action string, key data.PublicKey) error { - kl := make(data.KeyList, 0, 1) - kl = append(kl, key) +func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, action string, keyList []data.PublicKey) error { meta := changelist.TUFRootData{ RoleName: role, - Keys: kl, + Keys: keyList, } metaJSON, err := json.Marshal(meta) if err != nil { diff --git a/client/client_test.go b/client/client_test.go index ca70b4290..9a31bf3c6 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2565,31 +2565,31 @@ func TestRotateKeyInvalidRole(t *testing.T) { require.NoError(t, repo.Update(false)) // rotating a root key to the server fails - require.Error(t, repo.RotateKey(data.CanonicalRootRole, true), + require.Error(t, repo.RotateKey(data.CanonicalRootRole, true, nil), "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), + require.Error(t, repo.RotateKey(data.CanonicalTargetsRole, true, nil), "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), + require.Error(t, repo.RotateKey(data.CanonicalTimestampRole, false, nil), "Rotating a timestamp key locally should fail") // rotating a delegation key fails - require.Error(t, repo.RotateKey("targets/releases", false), + require.Error(t, repo.RotateKey("targets/releases", false, nil), "Rotating a delegation key should fail") // rotating a delegation key to the server also fails - require.Error(t, repo.RotateKey("targets/releases", true), + require.Error(t, repo.RotateKey("targets/releases", true, nil), "Rotating a delegation key should fail") // rotating a not a real role key fails - require.Error(t, repo.RotateKey("nope", false), + require.Error(t, repo.RotateKey("nope", false, nil), "Rotating a non-real role key should fail") // rotating a not a real role key to the server also fails - require.Error(t, repo.RotateKey("nope", true), + require.Error(t, repo.RotateKey("nope", true, nil), "Rotating a non-real role key should fail") } @@ -2604,7 +2604,7 @@ func TestRemoteRotationError(t *testing.T) { // server has died, so this should fail for _, role := range []string{data.CanonicalSnapshotRole, data.CanonicalTimestampRole} { - err := repo.RotateKey(role, true) + err := repo.RotateKey(role, true, nil) require.Error(t, err) require.Contains(t, err.Error(), "unable to rotate remote key") } @@ -2619,7 +2619,7 @@ func TestRemoteRotationEndpointError(t *testing.T) { // simpleTestServer has no rotate key endpoint, so this should fail for _, role := range []string{data.CanonicalSnapshotRole, data.CanonicalTimestampRole} { - err := repo.RotateKey(role, true) + err := repo.RotateKey(role, true, nil) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) } @@ -2640,7 +2640,7 @@ func TestRemoteRotationNoRootKey(t *testing.T) { _, err := newRepo.ListTargets() require.NoError(t, err) - err = newRepo.RotateKey(data.CanonicalSnapshotRole, true) + err = newRepo.RotateKey(data.CanonicalSnapshotRole, true, nil) require.Error(t, err) require.IsType(t, signed.ErrInsufficientSignatures{}, err) } @@ -2653,7 +2653,7 @@ func TestRemoteRotationNonexistentRepo(t *testing.T) { repo := newBlankRepo(t, ts.URL) defer os.RemoveAll(repo.baseDir) - err := repo.RotateKey(data.CanonicalTimestampRole, true) + err := repo.RotateKey(data.CanonicalTimestampRole, true, nil) require.Error(t, err) require.IsType(t, ErrRepoNotInitialized{}, err) } @@ -2681,7 +2681,7 @@ func requireRotationSuccessful(t *testing.T, repo1 *NotaryRepository, keysToRota // Do rotation for role, serverManaged := range keysToRotate { - require.NoError(t, repo1.RotateKey(role, serverManaged)) + require.NoError(t, repo1.RotateKey(role, serverManaged, nil)) } changesPost := getChanges(t, repo1) @@ -2872,14 +2872,91 @@ func TestRotateRootKey(t *testing.T) { // Rotate root certificate and key. logRepoTrustRoot(t, "original", authorRepo) - err = authorRepo.RotateKey(data.CanonicalRootRole, false) + err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) require.NoError(t, err) logRepoTrustRoot(t, "post-rotate", authorRepo) require.NoError(t, authorRepo.Update(false)) newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) + require.NoError(t, err) 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") + + // Publish the target, which does an update and pulls down the latest metadata, and + // should update the trusted root + logRepoTrustRoot(t, "pre-publish", authorRepo) + err = authorRepo.Publish() + require.NoError(t, err) + logRepoTrustRoot(t, "post-publish", authorRepo) + + // 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)) + 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)) +} + +func TestRotateRootMultiple(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 a 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, nil) + require.NoError(t, err) + logRepoTrustRoot(t, "post-rotate", authorRepo) + + // Rotate root certificate and key again. + err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) + require.NoError(t, err) + logRepoTrustRoot(t, "post-rotate-again", authorRepo) + + require.NoError(t, authorRepo.Update(false)) + newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) + 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) @@ -2900,6 +2977,8 @@ func TestRotateRootKey(t *testing.T) { logRepoTrustRoot(t, "post-publish", authorRepo) // Verify the user can use the rotated repo, and see the added target. + err = userRepo.Update(false) + require.NoError(t, err) _, err = userRepo.GetTargetByName("current") require.NoError(t, err) logRepoTrustRoot(t, "client", userRepo) @@ -2921,6 +3000,173 @@ func TestRotateRootKey(t *testing.T) { require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo)) } +func TestRotateRootKeyProvided(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) + + // Key loaded from file (just generating it here) + rootPublicKey, err := authorRepo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) + require.NoError(t, err) + rootPrivateKey, _, err := authorRepo.CryptoService.GetPrivateKey(rootPublicKey.ID()) + require.NoError(t, err) + + // Fail to rotate to bad key + err = authorRepo.RotateKey(data.CanonicalRootRole, false, []string{"notakey"}) + require.Error(t, err) + + // Rotate root certificate and key. + logRepoTrustRoot(t, "original", authorRepo) + err = authorRepo.RotateKey(data.CanonicalRootRole, false, []string{rootPrivateKey.ID()}) + 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)) + require.NoError(t, err) + // 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) + require.Equal(t, rootPrivateKey.ID(), 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") + + // Publish the target, which does an update and pulls down the latest metadata, and + // should update the trusted root + logRepoTrustRoot(t, "pre-publish", authorRepo) + err = authorRepo.Publish() + require.NoError(t, err) + logRepoTrustRoot(t, "post-publish", authorRepo) + + // 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)) + 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)) +} + +func TestRotateRootKeyLegacySupport(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 a 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, nil) + require.NoError(t, err) + logRepoTrustRoot(t, "post-rotate", authorRepo) + + // Rotate root certificate and key again, this time with legacy support + authorRepo.LegacyVersions = SignWithAllOldVersions + err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) + require.NoError(t, err) + logRepoTrustRoot(t, "post-rotate-again", authorRepo) + + require.NoError(t, authorRepo.Update(false)) + newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) + require.NoError(t, err) + 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") + + // Publish the target, which does an update and pulls down the latest metadata, and + // should update the trusted root + logRepoTrustRoot(t, "pre-publish", authorRepo) + err = authorRepo.Publish() + require.NoError(t, err) + logRepoTrustRoot(t, "post-publish", authorRepo) + + // Verify the user can use the rotated repo, and see the added target. + err = userRepo.Update(false) + require.NoError(t, err) + _, err = userRepo.GetTargetByName("current") + require.NoError(t, err) + logRepoTrustRoot(t, "client", userRepo) + + // Verify that the user's rotated root is signed with all available old keys + require.NoError(t, err) + require.Equal(t, 3, len(userRepo.tufRepo.Root.Signatures)) + + // 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)) + 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("", "notary-test-") diff --git a/client/client_update_test.go b/client/client_update_test.go index 0fbff4696..e318cde42 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -66,7 +66,13 @@ func readOnlyServer(t *testing.T, cache store.MetadataStore, notFoundStatus int, m := mux.NewRouter() handler := func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - metaBytes, err := cache.GetSized(vars["role"], store.NoSizeLimit) + var role string + if vars["version"] != "" { + role = fmt.Sprintf("%s.%s", vars["version"], vars["role"]) + } else { + role = vars["role"] + } + metaBytes, err := cache.GetSized(role, store.NoSizeLimit) if _, ok := err.(store.ErrMetaNotFound); ok { w.WriteHeader(notFoundStatus) } else { @@ -74,6 +80,7 @@ func readOnlyServer(t *testing.T, cache store.MetadataStore, notFoundStatus int, w.Write(metaBytes) } } + m.HandleFunc(fmt.Sprintf("/v2/%s/_trust/tuf/{version:[0-9]+}.{role:.*}.json", gun), handler) m.HandleFunc(fmt.Sprintf("/v2/%s/_trust/tuf/{role:.*}.{checksum:.*}.json", gun), handler) m.HandleFunc(fmt.Sprintf("/v2/%s/_trust/tuf/{role:.*}.json", gun), handler) return httptest.NewServer(m) @@ -158,6 +165,13 @@ func TestUpdateSucceedsEvenIfCannotWriteExistingRepo(t *testing.T) { require.NoError(t, err) for r, expected := range serverMeta { + if r != data.CanonicalRootRole && strings.Contains(r, "root") { + // don't fetch versioned root roles here + continue + } + if strings.ContainsAny(r, "123456789") { + continue + } actual, err := repo.cache.GetSized(r, store.NoSizeLimit) require.NoError(t, err, "problem getting repo metadata for %s", r) if role == r { @@ -1310,7 +1324,7 @@ func checkErrors(t *testing.T, err error, shouldErr bool, expectedErrs []interfa var expectedTypes []string for _, expectErr := range expectedErrs { expectedType := reflect.TypeOf(expectErr) - isExpectedType = isExpectedType || errType == expectedType + isExpectedType = isExpectedType || reflect.DeepEqual(errType, expectedType) expectedTypes = append(expectedTypes, expectedType.String()) } require.True(t, isExpectedType, "expected one of %v when %s: got %s", @@ -1478,6 +1492,13 @@ func signSerializeAndUpdateRoot(t *testing.T, signedRoot data.SignedRoot, require.NoError(t, serverSwizzler.UpdateTimestampHash()) } +func requireRootSignatures(t *testing.T, serverSwizzler *testutils.MetadataSwizzler, num int) { + updatedRootBytes, _ := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole, -1) + updatedRoot := &data.SignedRoot{} + require.NoError(t, json.Unmarshal(updatedRootBytes, updatedRoot)) + require.EqualValues(t, len(updatedRoot.Signatures), num) +} + // A valid root rotation only cares about the immediately previous old root keys, // whether or not there are old root roles, and cares that the role is satisfied // (for instance if the old role has 2 keys, either of which can sign, then it @@ -1577,6 +1598,200 @@ func TestValidateRootRotationWithOldRole(t *testing.T) { require.NoError(t, repo.Update(false)) } +// A valid root role is signed by the current root role keys and the previous root role keys +func TestRootRoleInvariant(t *testing.T) { + // start with a repo + _, serverSwizzler := newServerSwizzler(t) + ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") + defer ts.Close() + + repo := newBlankRepo(t, ts.URL) + defer os.RemoveAll(repo.baseDir) + + // --- setup so that the root starts with a role with 1 keys, and threshold of 1 + rootBytes, err := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole, store.NoSizeLimit) + require.NoError(t, err) + signedRoot := data.SignedRoot{} + require.NoError(t, json.Unmarshal(rootBytes, &signedRoot)) + + // save the old role to prove that it is not needed for client updates + oldVersion := fmt.Sprintf("%v.%v", data.CanonicalRootRole, signedRoot.Signed.Version) + signedRoot.Signed.Roles[oldVersion] = &data.RootRole{ + Threshold: 1, + KeyIDs: signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs, + } + + threeKeys := make([]data.PublicKey, 3) + keyIDs := make([]string, len(threeKeys)) + for i := 0; i < len(threeKeys); i++ { + threeKeys[i], err = testutils.CreateKey( + serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) + require.NoError(t, err) + keyIDs[i] = threeKeys[i].ID() + } + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0]} + signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 + // sign with the first key only + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0]}) + + // Load this root for the first time with 1 key + require.NoError(t, repo.Update(false)) + + // --- First root rotation: replace the first key with a different key + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[1]} + + // --- If the current role is satisfied but the previous one is not, root rotation + // --- will fail. Signing with just the second key will not satisfy the first role. + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[1]}) + require.Error(t, repo.Update(false)) + requireRootSignatures(t, serverSwizzler, 1) + + // --- If both the current and previous roles are satisfied, then the root rotation + // --- will succeed (signing with the first and second keys will satisfy both) + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[:2]) + require.NoError(t, repo.Update(false)) + requireRootSignatures(t, serverSwizzler, 2) + + // --- Second root rotation: replace the second key with a third + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[2]] = threeKeys[2] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[2]} + + // --- If the current role is satisfied but the previous one is not, root rotation + // --- will fail. Signing with just the second key will not satisfy the first role. + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[2]}) + require.Error(t, repo.Update(false)) + requireRootSignatures(t, serverSwizzler, 1) + + // --- If both the current and previous roles are satisfied, then the root rotation + // --- will succeed (signing with the second and third keys will satisfy both) + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[1:]) + require.NoError(t, repo.Update(false)) + requireRootSignatures(t, serverSwizzler, 2) + + // -- If signed with all previous roles, update will succeed + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys) + require.NoError(t, repo.Update(false)) + requireRootSignatures(t, serverSwizzler, 3) +} + +// All intermediate roots must be signed by the previous root role +func TestBadIntermediateTransitions(t *testing.T) { + // start with a repo + _, serverSwizzler := newServerSwizzler(t) + ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") + defer ts.Close() + + repo := newBlankRepo(t, ts.URL) + defer os.RemoveAll(repo.baseDir) + + // --- setup so that the root starts with a role with 1 keys, and threshold of 1 + rootBytes, err := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole, store.NoSizeLimit) + require.NoError(t, err) + signedRoot := data.SignedRoot{} + require.NoError(t, json.Unmarshal(rootBytes, &signedRoot)) + + // generate keys for testing + threeKeys := make([]data.PublicKey, 3) + keyIDs := make([]string, len(threeKeys)) + for i := 0; i < len(threeKeys); i++ { + threeKeys[i], err = testutils.CreateKey( + serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) + require.NoError(t, err) + keyIDs[i] = threeKeys[i].ID() + } + + // increment the root version and sign with the first key only + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0]} + signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0]}) + + require.NoError(t, repo.Update(false)) + + // increment the root version and sign with the second key only + signedRoot.Signed.Version++ + delete(signedRoot.Signed.Keys, keyIDs[0]) + signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[1]} + signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[1]}) + + // increment the root version and sign with all three keys + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] + signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] + signedRoot.Signed.Keys[keyIDs[2]] = threeKeys[2] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0], keyIDs[1], keyIDs[2]} + signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[1]}) + requireRootSignatures(t, serverSwizzler, 1) + + // Update fails because version 1 -> 2 is invalid. + require.Error(t, repo.Update(false)) +} + +// All intermediate roots must be signed by the previous root role +func TestExpiredIntermediateTransitions(t *testing.T) { + // start with a repo + _, serverSwizzler := newServerSwizzler(t) + ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") + defer ts.Close() + + repo := newBlankRepo(t, ts.URL) + defer os.RemoveAll(repo.baseDir) + + // --- setup so that the root starts with a role with 1 keys, and threshold of 1 + rootBytes, err := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole, store.NoSizeLimit) + require.NoError(t, err) + signedRoot := data.SignedRoot{} + require.NoError(t, json.Unmarshal(rootBytes, &signedRoot)) + + // generate keys for testing + threeKeys := make([]data.PublicKey, 3) + keyIDs := make([]string, len(threeKeys)) + for i := 0; i < len(threeKeys); i++ { + threeKeys[i], err = testutils.CreateKey( + serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) + require.NoError(t, err) + keyIDs[i] = threeKeys[i].ID() + } + + // increment the root version and sign with the first key only + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0]} + signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0]}) + + require.NoError(t, repo.Update(false)) + + // increment the root version and sign with the first and second keys, but set metadata to be expired. + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0], keyIDs[1]} + signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 + signedRoot.Signed.Expires = time.Now().AddDate(0, -1, 0) + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0], threeKeys[1]}) + + // increment the root version and sign with all three keys + signedRoot.Signed.Version++ + signedRoot.Signed.Keys[keyIDs[2]] = threeKeys[2] + signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0], keyIDs[1], keyIDs[2]} + signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 + signedRoot.Signed.Expires = time.Now().AddDate(0, 1, 0) + signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[:3]) + requireRootSignatures(t, serverSwizzler, 3) + + // Update succeeds despite version 2 being expired. + require.NoError(t, repo.Update(false)) +} + // TestDownloadTargetsLarge: Check that we can download very large targets metadata files, // which may be caused by adding a large number of targets. // This test is slow, so it will not run in short mode. diff --git a/client/helpers.go b/client/helpers.go index b3fd95591..a9972a38a 100644 --- a/client/helpers.go +++ b/client/helpers.go @@ -256,11 +256,11 @@ func rotateRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKe } // signs and serializes the metadata for a canonical role in a TUF repo to JSON -func serializeCanonicalRole(tufRepo *tuf.Repo, role string) (out []byte, err error) { +func serializeCanonicalRole(tufRepo *tuf.Repo, role string, extraSigningKeys data.KeyList) (out []byte, err error) { var s *data.Signed switch { case role == data.CanonicalRootRole: - s, err = tufRepo.SignRoot(data.DefaultExpires(role)) + s, err = tufRepo.SignRoot(data.DefaultExpires(role), extraSigningKeys) case role == data.CanonicalSnapshotRole: s, err = tufRepo.SignSnapshot(data.DefaultExpires(role)) case tufRepo.Targets[role] != nil: diff --git a/client/tufclient.go b/client/tufclient.go index a6abdac7c..726a3adc8 100644 --- a/client/tufclient.go +++ b/client/tufclient.go @@ -2,10 +2,12 @@ package client import ( "encoding/json" + "fmt" "github.com/Sirupsen/logrus" "github.com/docker/notary" store "github.com/docker/notary/storage" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" @@ -47,8 +49,8 @@ func (c *TUFClient) Update() (*tuf.Repo, *tuf.Repo, error) { c.newBuilder = c.newBuilder.BootstrapNewBuilder() - if err := c.downloadRoot(); err != nil { - logrus.Debug("Client Update (Root):", err) + if err := c.updateRoot(); err != nil { + logrus.Debug("Client Update (Root): ", err) return nil, nil, err } // If we error again, we now have the latest root and just want to fail @@ -78,25 +80,91 @@ func (c *TUFClient) update() error { return nil } -// downloadRoot is responsible for downloading the root.json -func (c *TUFClient) downloadRoot() error { - role := data.CanonicalRootRole - consistentInfo := c.newBuilder.GetConsistentInfo(role) +// updateRoot checks if there is a newer version of the root available, and if so +// downloads all intermediate root files to allow proper key rotation. +func (c *TUFClient) updateRoot() error { + // Get current root version + currentRootConsistentInfo := c.oldBuilder.GetConsistentInfo(data.CanonicalRootRole) + currentVersion := c.oldBuilder.GetLoadedVersion(currentRootConsistentInfo.RoleName) - // We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle - // since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch - if !consistentInfo.ChecksumKnown() { - logrus.Debugf("Loading root with no expected checksum") + // Get new root version + raw, err := c.downloadRoot() - // get the cached root, if it exists, just for version checking - cachedRoot, _ := c.cache.GetSized(role, -1) - // prefer to download a new root - _, remoteErr := c.tryLoadRemote(consistentInfo, cachedRoot) - return remoteErr + switch err.(type) { + case *trustpinning.ErrRootRotationFail: + // Rotation errors are okay since we haven't yet downloaded + // all intermediate root files + break + case nil: + // No error updating root - we were at most 1 version behind + return nil + default: + // Return any non-rotation error. + return err } - _, err := c.tryLoadCacheThenRemote(consistentInfo) - return err + // Load current version into newBuilder + currentRaw, err := c.cache.GetSized(data.CanonicalRootRole, -1) + if err != nil { + logrus.Debugf("error loading %d.%s: %s", currentVersion, data.CanonicalRootRole, err) + return err + } + if err := c.newBuilder.LoadRootForUpdate(currentRaw, currentVersion, false); err != nil { + logrus.Debugf("%d.%s is invalid: %s", currentVersion, data.CanonicalRootRole, err) + return err + } + + // Extract newest version number + signedRoot := &data.Signed{} + if err := json.Unmarshal(raw, signedRoot); err != nil { + return err + } + newestRoot, err := data.RootFromSigned(signedRoot) + if err != nil { + return err + } + newestVersion := newestRoot.Signed.SignedCommon.Version + + // Update from current + 1 (current already loaded) to newest - 1 (newest loaded below) + if err := c.updateRootVersions(currentVersion+1, newestVersion-1); err != nil { + return err + } + + // Already downloaded newest, verify it against newest - 1 + if err := c.newBuilder.LoadRootForUpdate(raw, newestVersion, true); err != nil { + logrus.Debugf("downloaded %d.%s is invalid: %s", newestVersion, data.CanonicalRootRole, err) + return err + } + logrus.Debugf("successfully verified downloaded %d.%s", newestVersion, data.CanonicalRootRole) + + // Write newest to cache + if err := c.cache.Set(data.CanonicalRootRole, raw); err != nil { + logrus.Debugf("unable to write %s to cache: %d.%s", newestVersion, data.CanonicalRootRole, err) + } + logrus.Debugf("finished updating root files") + return nil +} + +// updateRootVersions updates the root from it's current version to a target, rotating keys +// as they are found +func (c *TUFClient) updateRootVersions(fromVersion, toVersion int) error { + for v := fromVersion; v <= toVersion; v++ { + logrus.Debugf("updating root from version %d to version %d, currently fetching %d", fromVersion, toVersion, v) + + versionedRole := fmt.Sprintf("%d.%s", v, data.CanonicalRootRole) + + raw, err := c.remote.GetSized(versionedRole, -1) + if err != nil { + logrus.Debugf("error downloading %s: %s", versionedRole, err) + return err + } + if err := c.newBuilder.LoadRootForUpdate(raw, v, false); err != nil { + logrus.Debugf("downloaded %s is invalid: %s", versionedRole, err) + return err + } + logrus.Debugf("successfully verified downloaded %s", versionedRole) + } + return nil } // downloadTimestamp is responsible for downloading the timestamp.json @@ -198,6 +266,24 @@ func (c TUFClient) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInf return tgs.GetValidDelegations(role), nil } +// downloadRoot is responsible for downloading the root.json +func (c *TUFClient) downloadRoot() ([]byte, error) { + role := data.CanonicalRootRole + consistentInfo := c.newBuilder.GetConsistentInfo(role) + + // We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle + // since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch + if !consistentInfo.ChecksumKnown() { + logrus.Debugf("Loading root with no expected checksum") + + // get the cached root, if it exists, just for version checking + cachedRoot, _ := c.cache.GetSized(role, -1) + // prefer to download a new root + return c.tryLoadRemote(consistentInfo, cachedRoot) + } + return c.tryLoadCacheThenRemote(consistentInfo) +} + func (c *TUFClient) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([]byte, error) { cachedTS, err := c.cache.GetSized(consistentInfo.RoleName, consistentInfo.Length()) if err != nil { diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index c66f6f492..ff008bc7d 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -1289,6 +1289,181 @@ func TestClientKeyGenerationRotation(t *testing.T) { } } +// Tests key rotation +func TestKeyRotation(t *testing.T) { + // -- setup -- + setUp(t) + + tempDir := tempDirWithConfig(t, "{}") + defer os.RemoveAll(tempDir) + + tempfiles := make([]string, 2) + for i := 0; i < 2; i++ { + tempFile, err := ioutil.TempFile("", "targetfile") + require.NoError(t, err) + tempFile.Close() + tempfiles[i] = tempFile.Name() + defer os.Remove(tempFile.Name()) + } + + server := setupServer() + defer server.Close() + + var target = "sdgkadga" + + // -- tests -- + + // starts out with no keys + assertNumKeys(t, tempDir, 0, 0, true) + + // generate root key produces a single root key and no other keys + _, err := runCommand(t, tempDir, "key", "generate", data.ECDSAKey) + require.NoError(t, err) + assertNumKeys(t, tempDir, 1, 0, true) + + // initialize a repo, should have signing keys and no new root key + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") + require.NoError(t, err) + assertNumKeys(t, tempDir, 1, 2, true) + + // publish using the original keys + assertSuccessfullyPublish(t, tempDir, server.URL, "gun", target, tempfiles[0]) + + // invalid keys + badKeyFile, err := ioutil.TempFile("", "badKey") + require.NoError(t, err) + defer os.Remove(badKeyFile.Name()) + _, err = badKeyFile.Write([]byte{0, 0, 0, 0}) + require.NoError(t, err) + badKeyFile.Close() + + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole, "--key", "123") + require.Error(t, err) + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole, "--key", badKeyFile.Name()) + require.Error(t, err) + + // create encrypted root keys + rootPrivKey1, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + encryptedPEMPrivKey1, err := utils.EncryptPrivateKey(rootPrivKey1, data.CanonicalRootRole, "", testPassphrase) + require.NoError(t, err) + encryptedPEMKeyFilename1 := filepath.Join(tempDir, "encrypted_key.key") + err = ioutil.WriteFile(encryptedPEMKeyFilename1, encryptedPEMPrivKey1, 0644) + require.NoError(t, err) + + rootPrivKey2, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + encryptedPEMPrivKey2, err := utils.EncryptPrivateKey(rootPrivKey2, data.CanonicalRootRole, "", testPassphrase) + require.NoError(t, err) + encryptedPEMKeyFilename2 := filepath.Join(tempDir, "encrypted_key2.key") + err = ioutil.WriteFile(encryptedPEMKeyFilename2, encryptedPEMPrivKey2, 0644) + require.NoError(t, err) + + // rotate the root key + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole, "--key", encryptedPEMKeyFilename1, "--key", encryptedPEMKeyFilename2) + require.NoError(t, err) + // 3 root keys - 1 prev, 1 new + assertNumKeys(t, tempDir, 3, 2, true) + + // rotate the root key again + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole) + require.NoError(t, err) + // 3 root keys, 2 prev, 1 new + assertNumKeys(t, tempDir, 3, 2, true) + + // publish using the new keys + output := assertSuccessfullyPublish( + t, tempDir, server.URL, "gun", target+"2", tempfiles[1]) + // assert that the previous target is still there + require.True(t, strings.Contains(string(output), target)) +} + +// Tests rotating non-root keys +func TestKeyRotationNonRoot(t *testing.T) { + // -- setup -- + setUp(t) + + tempDir := tempDirWithConfig(t, "{}") + defer os.RemoveAll(tempDir) + + tempfiles := make([]string, 2) + for i := 0; i < 2; i++ { + tempFile, err := ioutil.TempFile("", "targetfile") + require.NoError(t, err) + tempFile.Close() + tempfiles[i] = tempFile.Name() + defer os.Remove(tempFile.Name()) + } + + server := setupServer() + defer server.Close() + + var target = "sdgkadgad" + + // -- tests -- + + // starts out with no keys + assertNumKeys(t, tempDir, 0, 0, true) + + // generate root key produces a single root key and no other keys + _, err := runCommand(t, tempDir, "key", "generate", data.ECDSAKey) + require.NoError(t, err) + assertNumKeys(t, tempDir, 1, 0, true) + + // initialize a repo, should have signing keys and no new root key + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") + require.NoError(t, err) + assertNumKeys(t, tempDir, 1, 2, true) + + // publish using the original keys + assertSuccessfullyPublish(t, tempDir, server.URL, "gun", target, tempfiles[0]) + + // create new target keys + tempFile, err := ioutil.TempFile("", "pemfile") + require.NoError(t, err) + defer os.Remove(tempFile.Name()) + + privKey, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + + pemBytes, err := utils.EncryptPrivateKey(privKey, data.CanonicalTargetsRole, "", testPassphrase) + require.NoError(t, err) + + nBytes, err := tempFile.Write(pemBytes) + require.NoError(t, err) + tempFile.Close() + require.Equal(t, len(pemBytes), nBytes) + + tempFile2, err := ioutil.TempFile("", "pemfile2") + require.NoError(t, err) + defer os.Remove(tempFile2.Name()) + + privKey2, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + + pemBytes2, err := utils.KeyToPEM(privKey2, data.CanonicalTargetsRole, "") + require.NoError(t, err) + + nBytes2, err := tempFile2.Write(pemBytes2) + require.NoError(t, err) + tempFile2.Close() + require.Equal(t, len(pemBytes2), nBytes2) + + // rotate the targets key + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalTargetsRole, "--key", tempFile.Name(), "--key", tempFile2.Name()) + require.NoError(t, err) + + // publish using the new keys + output := assertSuccessfullyPublish( + t, tempDir, server.URL, "gun", target+"2", tempfiles[1]) + // assert that the previous target is still there + require.True(t, strings.Contains(string(output), target)) + + // rotate to nonexistant key + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalTargetsRole, "--key", "nope.pem") + require.Error(t, err) +} + // Tests default root key generation func TestDefaultRootKeyGeneration(t *testing.T) { // -- setup -- @@ -1724,7 +1899,7 @@ func TestClientTUFInitWithAutoPublish(t *testing.T) { // list repo - expect error _, err = runCommand(t, tempDir, "-s", server.URL, "list", gunNoPublish) require.NotNil(t, err) - require.Equal(t, err, nstorage.ErrMetaNotFound{Resource: data.CanonicalRootRole}) + require.IsType(t, client.ErrRepositoryNotExist{}, err) } func TestClientTUFAddWithAutoPublish(t *testing.T) { diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index ba38b7e87..611a1ce8d 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -76,8 +76,9 @@ type keyCommander struct { // these are for command line parsing - no need to set rotateKeyRole string rotateKeyServerManaged bool - - input io.Reader + rotateKeyFiles []string + legacyVersions int + input io.Reader keysImportRole string keysImportGUN string @@ -97,6 +98,14 @@ func (k *keyCommander) GetCommand() *cobra.Command { false, "Signing and key management will be handled by the remote server "+ "(no key will be generated or stored locally). "+ "Required for timestamp role, optional for snapshot role") + cmdRotateKey.Flags().IntVarP(&k.legacyVersions, "legacy", "l", 0, "Number of old version's root roles to sign with to support old clients") + cmdRotateKey.Flags().StringSliceVarP( + &k.rotateKeyFiles, + "key", + "k", + nil, + "New key(s) to rotate to. If not specified, one will be generated.", + ) cmd.AddCommand(cmdRotateKey) cmdKeysImport := cmdKeyImportTemplate.ToCommand(k.importKeys) @@ -226,12 +235,23 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error { return err } + var keyList []string + + for _, keyFile := range k.rotateKeyFiles { + privKey, err := readKey(rotateKeyRole, keyFile, k.getRetriever()) + if err != nil { + return err + } + err = nRepo.CryptoService.AddKey(rotateKeyRole, gun, privKey) + if err != nil { + return fmt.Errorf("Error importing key: %v", err) + } + keyList = append(keyList, privKey.ID()) + } + if rotateKeyRole == data.CanonicalRootRole { cmd.Print("Warning: you are about to rotate your root key.\n\n" + - "You must use your old key to sign this root rotation. We recommend that\n" + - "you sign all your future root changes with this key as well, so that\n" + - "clients can have a smoother update process. Please do not delete\n" + - "this key after rotating.\n\n" + + "You must use your old key to sign this root rotation.\n" + "Are you sure you want to proceed? (yes/no) ") if !askConfirm(k.input) { @@ -239,8 +259,8 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error { return nil } } - - if err := nRepo.RotateKey(rotateKeyRole, k.rotateKeyServerManaged); err != nil { + nRepo.LegacyVersions = k.legacyVersions + if err := nRepo.RotateKey(rotateKeyRole, k.rotateKeyServerManaged, keyList); err != nil { return err } cmd.Printf("Successfully rotated %s key for repository %s\n", rotateKeyRole, gun) diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 2433a5c81..d242607b4 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -26,6 +26,7 @@ import ( "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" + tufutils "github.com/docker/notary/tuf/utils" "github.com/docker/notary/utils" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -417,7 +418,7 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { var rootKeyList []string if t.rootKey != "" { - privKey, err := readRootKey(t.rootKey, t.retriever) + privKey, err := readKey(data.CanonicalRootRole, t.rootKey, t.retriever) if err != nil { return err } @@ -452,9 +453,10 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { return maybeAutoPublish(cmd, t.autoPublish, gun, config, t.retriever) } -// Attempt to read an encrypted root key from a file, and return it as a data.PrivateKey -func readRootKey(rootKeyFile string, retriever notary.PassRetriever) (data.PrivateKey, error) { - keyFile, err := os.Open(rootKeyFile) +// Attempt to read a role key from a file, and return it as a data.PrivateKey +// If key is for the Root role, it must be encrypted +func readKey(role, keyFilename string, retriever notary.PassRetriever) (data.PrivateKey, error) { + keyFile, err := os.Open(keyFilename) if err != nil { return nil, fmt.Errorf("Opening file to import as a root key: %v", err) } @@ -464,11 +466,19 @@ func readRootKey(rootKeyFile string, retriever notary.PassRetriever) (data.Priva if err != nil { return nil, fmt.Errorf("Error reading input root key file: %v", err) } + isEncrypted := true if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil { - return nil, err + if role == data.CanonicalRootRole { + return nil, err + } + isEncrypted = false + } + var privKey data.PrivateKey + if isEncrypted { + privKey, _, err = trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", data.CanonicalRootRole) + } else { + privKey, err = tufutils.ParsePEMPrivateKey(pemBytes, "") } - - privKey, _, err := trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", data.CanonicalRootRole) if err != nil { return nil, err } diff --git a/server/handlers/default.go b/server/handlers/default.go index 9902adc90..edbdf2022 100644 --- a/server/handlers/default.go +++ b/server/handlers/default.go @@ -127,6 +127,7 @@ func GetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) err func getHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { gun := vars["imageName"] checksum := vars["checksum"] + version := vars["version"] tufRole := vars["tufRole"] s := ctx.Value(notary.CtxKeyMetaStore) @@ -138,7 +139,7 @@ func getHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, var return errors.ErrNoStorage.WithDetail(nil) } - lastModified, output, err := getRole(ctx, store, gun, tufRole, checksum) + lastModified, output, err := getRole(ctx, store, gun, tufRole, checksum, version) if err != nil { logger.Infof("404 GET %s role", tufRole) return err diff --git a/server/handlers/default_test.go b/server/handlers/default_test.go index 467e34aa7..068fc636a 100644 --- a/server/handlers/default_test.go +++ b/server/handlers/default_test.go @@ -220,7 +220,7 @@ func TestGetHandlerRoot(t *testing.T) { ctx := context.Background() ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, metaStore) - root, err := repo.SignRoot(data.DefaultExpires("root")) + root, err := repo.SignRoot(data.DefaultExpires("root"), nil) require.NoError(t, err) rootJSON, err := json.Marshal(root) require.NoError(t, err) @@ -239,6 +239,14 @@ func TestGetHandlerRoot(t *testing.T) { err = getHandler(ctx, rw, req, vars) require.NoError(t, err) + + vars["version"] = "1" + err = getHandler(ctx, rw, req, vars) + require.NoError(t, err) + + vars["version"] = "badversion" + err = getHandler(ctx, rw, req, vars) + require.Error(t, err) } func TestGetHandlerTimestamp(t *testing.T) { diff --git a/server/handlers/roles.go b/server/handlers/roles.go index 99ebf8426..b1ee54c6d 100644 --- a/server/handlers/roles.go +++ b/server/handlers/roles.go @@ -1,6 +1,7 @@ package handlers import ( + "strconv" "time" "golang.org/x/net/context" @@ -17,13 +18,21 @@ import ( "github.com/docker/notary/tuf/signed" ) -func getRole(ctx context.Context, store storage.MetaStore, gun, role, checksum string) (*time.Time, []byte, error) { +func getRole(ctx context.Context, store storage.MetaStore, gun, role, checksum, version string) (*time.Time, []byte, error) { var ( lastModified *time.Time out []byte err error ) - if checksum == "" { + if checksum != "" { + lastModified, out, err = store.GetChecksum(gun, role, checksum) + } else if version != "" { + v, vErr := strconv.Atoi(version) + if vErr != nil { + return nil, nil, errors.ErrMetadataNotFound.WithDetail(vErr) + } + lastModified, out, err = store.GetVersion(gun, role, v) + } else { // the timestamp and snapshot might be server signed so are // handled specially switch role { @@ -31,8 +40,7 @@ func getRole(ctx context.Context, store storage.MetaStore, gun, role, checksum s return getMaybeServerSigned(ctx, store, gun, role) } lastModified, out, err = store.GetCurrent(gun, role) - } else { - lastModified, out, err = store.GetChecksum(gun, role, checksum) + } if err != nil { diff --git a/server/handlers/validation.go b/server/handlers/validation.go index 4affe3ae6..063252e8f 100644 --- a/server/handlers/validation.go +++ b/server/handlers/validation.go @@ -43,8 +43,13 @@ func validateUpdate(cs signed.CryptoService, gun string, updates []storage.MetaU } if rootUpdate, ok := roles[data.CanonicalRootRole]; ok { + currentRootVersion := builder.GetLoadedVersion(data.CanonicalRootRole) + if rootUpdate.Version != currentRootVersion && rootUpdate.Version != currentRootVersion+1 { + msg := fmt.Sprintf("Root modifications must increment the version. Current %d, new %d", currentRootVersion, rootUpdate.Version) + return nil, validation.ErrBadRoot{Msg: msg} + } builder = builder.BootstrapNewBuilder() - if err := builder.Load(data.CanonicalRootRole, rootUpdate.Data, 1, false); err != nil { + if err := builder.Load(data.CanonicalRootRole, rootUpdate.Data, currentRootVersion, false); err != nil { return nil, validation.ErrBadRoot{Msg: err.Error()} } diff --git a/server/handlers/validation_test.go b/server/handlers/validation_test.go index afd156f53..c22e103ad 100644 --- a/server/handlers/validation_test.go +++ b/server/handlers/validation_test.go @@ -431,7 +431,7 @@ func TestValidateRootRotationWithOldSigs(t *testing.T) { // the next root does NOT need to be signed by both keys, because we only care // about signing with both keys if the root keys have changed (signRoot again to bump the version) - r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) // delete all signatures except the one with the new key for _, sig := range repo.Root.Signatures { @@ -460,7 +460,7 @@ func TestValidateRootRotationWithOldSigs(t *testing.T) { newRootID2 := newRootKey2.ID() require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootKey2)) - r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) // delete all signatures except the ones with the first and second new keys sigs := make([]data.Signature, 0, 2) @@ -527,7 +527,7 @@ func TestValidateRootRotationMultipleKeysThreshold1(t *testing.T) { rotatedRootID := rotatedRootKey.ID() require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rotatedRootKey)) - r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) require.Len(t, r.Signatures, 3) // delete all signatures except the additional key (which didn't sign the @@ -587,7 +587,7 @@ func TestRootRotationNotSignedWithOldKeysForOldRole(t *testing.T) { repo.Root.Signed.Roles[data.CanonicalRootRole].Threshold = 1 require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, finalRootKey)) - r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) origSigs := r.Signatures @@ -631,6 +631,48 @@ func TestRootRotationNotSignedWithOldKeysForOldRole(t *testing.T) { require.NoError(t, err) } +// A root rotation must increment the root version by 1 +func TestRootRotationVersionIncrement(t *testing.T) { + gun := "docker.com/notary" + repo, crypto, err := testutils.EmptyRepo(gun) + require.NoError(t, err) + serverCrypto := testutils.CopyKeys(t, crypto, data.CanonicalTimestampRole) + store := storage.NewMemStorage() + + r, tg, sn, ts, err := testutils.Sign(repo) + require.NoError(t, err) + root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) + require.NoError(t, err) + + // set the original root in the store + updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} + require.NoError(t, store.UpdateMany(gun, updates)) + + // rotate the root key, sign with both keys, and update - update should succeed + newRootKey, err := testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) + require.NoError(t, err) + + require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootKey)) + r, _, sn, _, err = testutils.Sign(repo) + require.NoError(t, err) + root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) + require.NoError(t, err) + snapshot.Version = repo.Snapshot.Signed.Version + + // Wrong root version + root.Version = repo.Root.Signed.Version + 1 + + _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) + require.Error(t, err) + require.Contains(t, err.Error(), "Root modifications must increment the version") + + // correct root version + root.Version = root.Version - 1 + updates, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) + require.NoError(t, err) + require.NoError(t, store.UpdateMany(gun, updates)) +} + // An update is not valid without the root metadata. func TestValidateNoRoot(t *testing.T) { gun := "docker.com/notary" diff --git a/server/server.go b/server/server.go index 969ae1e3a..3e1fd289e 100644 --- a/server/server.go +++ b/server/server.go @@ -163,6 +163,14 @@ func RootHandler(ctx context.Context, ac auth.AccessController, trust signed.Cry ServerHandler: handlers.GetHandler, PermissionsRequired: []string{"pull"}, })) + r.Methods("GET").Path("/v2/{imageName:[^*]+}/_trust/tuf/{version:[1-9]*[0-9]+}.{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.json").Handler(createHandler(_serverEndpoint{ + OperationName: "GetRoleByVersion", + ErrorIfGUNInvalid: notFoundError, + IncludeCacheHeaders: true, + CacheControlConfig: consistent, + ServerHandler: handlers.GetHandler, + PermissionsRequired: []string{"pull"}, + })) r.Methods("GET").Path("/v2/{imageName:[^*]+}/_trust/tuf/{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.json").Handler(createHandler(_serverEndpoint{ OperationName: "GetRole", ErrorIfGUNInvalid: notFoundError, diff --git a/server/server_test.go b/server/server_test.go index 7dfc1fc73..9680c4036 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -252,6 +252,72 @@ func TestGetRoleByHash(t *testing.T) { verifyGetResponse(t, res, j) } +// This just checks the URL routing is working correctly and cache headers are set correctly. +// More detailed tests for this path including negative +// tests are located in /server/handlers/ +func TestGetRoleByVersion(t *testing.T) { + store := storage.NewMemStorage() + + ts := data.SignedTimestamp{ + Signatures: make([]data.Signature, 0), + Signed: data.Timestamp{ + SignedCommon: data.SignedCommon{ + Type: data.TUFTypes[data.CanonicalTimestampRole], + Version: 1, + Expires: data.DefaultExpires(data.CanonicalTimestampRole), + }, + }, + } + j, err := json.Marshal(&ts) + require.NoError(t, err) + store.UpdateCurrent("gun", storage.MetaUpdate{ + Role: data.CanonicalTimestampRole, + Version: 1, + Data: j, + }) + + // create and add a newer timestamp. We're going to try and request + // the older version we created above. + ts = data.SignedTimestamp{ + Signatures: make([]data.Signature, 0), + Signed: data.Timestamp{ + SignedCommon: data.SignedCommon{ + Type: data.TUFTypes[data.CanonicalTimestampRole], + Version: 2, + Expires: data.DefaultExpires(data.CanonicalTimestampRole), + }, + }, + } + newTS, err := json.Marshal(&ts) + require.NoError(t, err) + store.UpdateCurrent("gun", storage.MetaUpdate{ + Role: data.CanonicalTimestampRole, + Version: 1, + Data: newTS, + }) + + ctx := context.WithValue( + context.Background(), notary.CtxKeyMetaStore, store) + + ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) + + ccc := utils.NewCacheControlConfig(10, false) + handler := RootHandler(ctx, nil, signed.NewEd25519(), ccc, ccc, nil) + serv := httptest.NewServer(handler) + defer serv.Close() + + res, err := http.Get(fmt.Sprintf( + "%s/v2/gun/_trust/tuf/%d.%s.json", + serv.URL, + 1, + data.CanonicalTimestampRole, + )) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + // if content is equal, checksums are guaranteed to be equal + verifyGetResponse(t, res, j) +} + // This just checks the URL routing is working correctly and cache headers are set correctly. // More detailed tests for this path including negative // tests are located in /server/handlers/ diff --git a/server/snapshot/snapshot_test.go b/server/snapshot/snapshot_test.go index 2eac5b5f3..ae55b74db 100644 --- a/server/snapshot/snapshot_test.go +++ b/server/snapshot/snapshot_test.go @@ -96,7 +96,7 @@ func TestGetSnapshotKeyExistingMetadata(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) - sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) @@ -137,7 +137,7 @@ func TestGetSnapshotNoPreviousSnapshot(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) - sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) @@ -198,7 +198,7 @@ func TestGetSnapshotOldSnapshotExpired(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) - sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) @@ -272,7 +272,7 @@ func TestCreateSnapshotNoKeyInCrypto(t *testing.T) { repo, _, err := testutils.EmptyRepo("gun") require.NoError(t, err) - sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) diff --git a/server/storage/interface.go b/server/storage/interface.go index 17f02b8af..171d4eec3 100644 --- a/server/storage/interface.go +++ b/server/storage/interface.go @@ -36,6 +36,11 @@ type MetaStore interface { // not found, it returns storage.ErrNotFound GetChecksum(gun, tufRole, checksum string) (created *time.Time, data []byte, err error) + // GetVersion returns the given TUF role file and creation date for the + // GUN with the provided version. If the given (gun, role, version) are + // not found, it returns storage.ErrNotFound + GetVersion(gun, tufRole string, version int) (created *time.Time, data []byte, err error) + // Delete removes all metadata for a given GUN. It does not return an // error if no metadata exists for the given GUN. Delete(gun string) error diff --git a/server/storage/memory.go b/server/storage/memory.go index fb7182462..51157c426 100644 --- a/server/storage/memory.go +++ b/server/storage/memory.go @@ -169,6 +169,21 @@ func (st *MemStorage) GetChecksum(gun, role, checksum string) (*time.Time, []byt return &(space.createupdate), space.data, nil } +// GetVersion gets a specific TUF record by its version +func (st *MemStorage) GetVersion(gun, role string, version int) (*time.Time, []byte, error) { + st.lock.Lock() + defer st.lock.Unlock() + + id := entryKey(gun, role) + for _, ver := range st.tufMeta[id] { + if ver.version == version { + return &(ver.createupdate), ver.data, nil + } + } + + return nil, nil, ErrNotFound{} +} + // Delete deletes all the metadata for a given GUN func (st *MemStorage) Delete(gun string) error { st.lock.Lock() diff --git a/server/storage/memory_test.go b/server/storage/memory_test.go index 7417d9abe..4e23b4cdc 100644 --- a/server/storage/memory_test.go +++ b/server/storage/memory_test.go @@ -96,3 +96,8 @@ func TestMemoryGetChanges(t *testing.T) { testGetChanges(t, s) } + +func TestGetVersion(t *testing.T) { + s := NewMemStorage() + testGetVersion(t, s) +} diff --git a/server/storage/rethink_realdb_test.go b/server/storage/rethink_realdb_test.go index 7de38ffb2..84165501d 100644 --- a/server/storage/rethink_realdb_test.go +++ b/server/storage/rethink_realdb_test.go @@ -130,6 +130,13 @@ func TestRethinkUpdateCurrentVersionCheckOldVersionNotExist(t *testing.T) { testUpdateCurrentVersionCheck(t, dbStore, false) } +func TestRethinkGetVersion(t *testing.T) { + dbStore, cleanup := rethinkDBSetup(t) + defer cleanup() + + testGetVersion(t, dbStore) +} + // UpdateMany succeeds if the updates do not conflict with each other or with what's // already in the DB func TestRethinkUpdateManyNoConflicts(t *testing.T) { diff --git a/server/storage/rethinkdb.go b/server/storage/rethinkdb.go index 6fece1695..6adef2d80 100644 --- a/server/storage/rethinkdb.go +++ b/server/storage/rethinkdb.go @@ -232,6 +232,24 @@ func (rdb RethinkDB) GetChecksum(gun, role, checksum string) (created *time.Time return &file.CreatedAt, file.Data, err } +// GetVersion gets a specific TUF record by its version +func (rdb RethinkDB) GetVersion(gun, role string, version int) (*time.Time, []byte, error) { + var file RDBTUFFile + res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).Get([]interface{}{gun, role, version}).Run(rdb.sess) + if err != nil { + return nil, nil, err + } + defer res.Close() + if res.IsNil() { + return nil, nil, ErrNotFound{} + } + err = res.One(&file) + if err == gorethink.ErrEmptyResult { + return nil, nil, ErrNotFound{} + } + return &file.CreatedAt, file.Data, err +} + // Delete removes all metadata for a given GUN. It does not return an // error if no metadata exists for the given GUN. func (rdb RethinkDB) Delete(gun string) error { diff --git a/server/storage/rethinkdb_models.go b/server/storage/rethinkdb_models.go index 1aac19144..56295c590 100644 --- a/server/storage/rethinkdb_models.go +++ b/server/storage/rethinkdb_models.go @@ -6,9 +6,10 @@ import ( // These consts are the index names we've defined for RethinkDB const ( - rdbSHA256Idx = "sha256" - rdbGunRoleIdx = "gun_role" - rdbGunRoleSHA256Idx = "gun_role_sha256" + rdbSHA256Idx = "sha256" + rdbGunRoleIdx = "gun_role" + rdbGunRoleSHA256Idx = "gun_role_sha256" + rdbGunRoleVersionIdx = "gun_role_version" ) var ( diff --git a/server/storage/sqldb.go b/server/storage/sqldb.go index a5dbd9b0b..2cffd7a73 100644 --- a/server/storage/sqldb.go +++ b/server/storage/sqldb.go @@ -202,6 +202,22 @@ func (db *SQLStorage) GetChecksum(gun, tufRole, checksum string) (*time.Time, [] return &(row.CreatedAt), row.Data, nil } +// GetVersion gets a specific TUF record by its version +func (db *SQLStorage) GetVersion(gun, tufRole string, version int) (*time.Time, []byte, error) { + var row TUFFile + q := db.Select("created_at, data").Where( + &TUFFile{ + Gun: gun, + Role: tufRole, + Version: version, + }, + ).First(&row) + if err := isReadErr(q, row); err != nil { + return nil, nil, err + } + return &(row.CreatedAt), row.Data, nil +} + func isReadErr(q *gorm.DB, row TUFFile) error { if q.RecordNotFound() { return ErrNotFound{} diff --git a/server/storage/sqldb_test.go b/server/storage/sqldb_test.go index 8ce22415a..4c7ed10e0 100644 --- a/server/storage/sqldb_test.go +++ b/server/storage/sqldb_test.go @@ -247,3 +247,10 @@ func TestSQLGetChanges(t *testing.T) { testGetChanges(t, s) } + +func TestSQLDBGetVersion(t *testing.T) { + dbStore, cleanup := sqldbSetup(t) + defer cleanup() + + testGetVersion(t, dbStore) +} diff --git a/server/storage/storage_test.go b/server/storage/storage_test.go index 2050b358a..e1ee4bf5f 100644 --- a/server/storage/storage_test.go +++ b/server/storage/storage_test.go @@ -126,6 +126,26 @@ func testUpdateCurrentVersionCheck(t *testing.T, s MetaStore, oldVersionExists b return expected } +// GetVersion should successfully retrieve a version of an existing TUF file, +// but will return an error if the requested version does not exist. +func testGetVersion(t *testing.T, s MetaStore) { + _, _, err := s.GetVersion("gun", "role", 2) + require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound") + + s.UpdateCurrent("gun", MetaUpdate{"role", 2, []byte("version2")}) + _, d, err := s.GetVersion("gun", "role", 2) + require.Nil(t, err, "Expected error to be nil") + require.Equal(t, []byte("version2"), d, "Data was incorrect") + + // Getting newer version fails + _, _, err = s.GetVersion("gun", "role", 3) + require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound") + + // Getting another gun/role fails + _, _, err = s.GetVersion("badgun", "badrole", 2) + require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound") +} + // UpdateMany succeeds if the updates do not conflict with each other or with what's // already in the DB func testUpdateManyNoConflicts(t *testing.T, s MetaStore) []StoredTUFMeta { diff --git a/server/timestamp/timestamp_test.go b/server/timestamp/timestamp_test.go index 8a0513e4a..e6f130421 100644 --- a/server/timestamp/timestamp_test.go +++ b/server/timestamp/timestamp_test.go @@ -271,7 +271,7 @@ func TestGetTimestampKeyExistingMetadata(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) - sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) diff --git a/storage/memorystore.go b/storage/memorystore.go index 8a2ade54d..c175252d8 100644 --- a/storage/memorystore.go +++ b/storage/memorystore.go @@ -2,8 +2,11 @@ package storage import ( "crypto/sha256" + "encoding/json" + "fmt" "github.com/docker/notary" + "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/utils" ) @@ -75,6 +78,15 @@ func (m MemoryStore) Get(name string) ([]byte, error) { func (m *MemoryStore) Set(name string, meta []byte) error { m.data[name] = meta + parsedMeta := &data.SignedMeta{} + err := json.Unmarshal(meta, parsedMeta) + if err == nil { + // no parse error means this is metadata and not a key, so store by version + version := parsedMeta.Signed.Version + versionedName := fmt.Sprintf("%d.%s", version, name) + m.data[versionedName] = meta + } + checksum := sha256.Sum256(meta) path := utils.ConsistentName(name, checksum[:]) m.consistent[path] = meta diff --git a/tuf/builder.go b/tuf/builder.go index 1eaf0498c..d4f0d8e9c 100644 --- a/tuf/builder.go +++ b/tuf/builder.go @@ -57,6 +57,7 @@ func (c ConsistentInfo) Length() int64 { // RepoBuilder is an interface for an object which builds a tuf.Repo type RepoBuilder interface { Load(roleName string, content []byte, minVersion int, allowExpired bool) error + LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) Finish() (*Repo, *Repo, error) @@ -75,6 +76,9 @@ type finishedBuilder struct{} func (f finishedBuilder) Load(roleName string, content []byte, minVersion int, allowExpired bool) error { return ErrBuildDone } +func (f finishedBuilder) LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error { + return ErrBuildDone +} func (f finishedBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) { return nil, 0, ErrBuildDone } @@ -242,11 +246,27 @@ func (rb *repoBuilder) GetConsistentInfo(roleName string) ConsistentInfo { } func (rb *repoBuilder) Load(roleName string, content []byte, minVersion int, allowExpired bool) error { + return rb.loadOptions(roleName, content, minVersion, allowExpired, false, false) +} + +// LoadRootForUpdate adds additional flags for updating the root.json file +func (rb *repoBuilder) LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error { + if err := rb.loadOptions(data.CanonicalRootRole, content, minVersion, !isFinal, !isFinal, true); err != nil { + return err + } + if !isFinal { + rb.prevRoot = rb.repo.Root + } + return nil +} + +// loadOptions adds additional flags that should only be used for updating the root.json +func (rb *repoBuilder) loadOptions(roleName string, content []byte, minVersion int, allowExpired, skipChecksum, allowLoaded bool) error { if !data.ValidRole(roleName) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s is an invalid role", roleName)} } - if rb.IsLoaded(roleName) { + if !allowLoaded && rb.IsLoaded(roleName) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s has already been loaded", roleName)} } @@ -265,7 +285,7 @@ func (rb *repoBuilder) Load(roleName string, content []byte, minVersion int, all switch roleName { case data.CanonicalRootRole: - return rb.loadRoot(content, minVersion, allowExpired) + return rb.loadRoot(content, minVersion, allowExpired, skipChecksum) case data.CanonicalSnapshotRole: return rb.loadSnapshot(content, minVersion, allowExpired) case data.CanonicalTimestampRole: @@ -408,10 +428,10 @@ func (rb *repoBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, in } // loadRoot loads a root if one has not been loaded -func (rb *repoBuilder) loadRoot(content []byte, minVersion int, allowExpired bool) error { +func (rb *repoBuilder) loadRoot(content []byte, minVersion int, allowExpired, skipChecksum bool) error { roleName := data.CanonicalRootRole - signedObj, err := rb.bytesToSigned(content, data.CanonicalRootRole) + signedObj, err := rb.bytesToSigned(content, data.CanonicalRootRole, skipChecksum) if err != nil { return err } @@ -562,7 +582,7 @@ func (rb *repoBuilder) loadDelegation(roleName string, content []byte, minVersio } // bytesToSigned checks checksum - signedObj, err := rb.bytesToSigned(content, roleName) + signedObj, err := rb.bytesToSigned(content, roleName, false) if err != nil { return err } @@ -655,9 +675,11 @@ func (rb *repoBuilder) validateChecksumFor(content []byte, roleName string) erro // Checksums the given bytes, and if they validate, convert to a data.Signed object. // If a checksums are nil (as opposed to empty), adds the bytes to the list of roles that // haven't been checksummed (unless it's a timestamp, which has no checksum reference). -func (rb *repoBuilder) bytesToSigned(content []byte, roleName string) (*data.Signed, error) { - if err := rb.validateChecksumFor(content, roleName); err != nil { - return nil, err +func (rb *repoBuilder) bytesToSigned(content []byte, roleName string, skipChecksum bool) (*data.Signed, error) { + if !skipChecksum { + if err := rb.validateChecksumFor(content, roleName); err != nil { + return nil, err + } } // unmarshal to signed @@ -671,7 +693,7 @@ func (rb *repoBuilder) bytesToSigned(content []byte, roleName string) (*data.Sig func (rb *repoBuilder) bytesToSignedAndValidateSigs(role data.BaseRole, content []byte) (*data.Signed, error) { - signedObj, err := rb.bytesToSigned(content, role.Name) + signedObj, err := rb.bytesToSigned(content, role.Name, false) if err != nil { return nil, err } diff --git a/tuf/builder_test.go b/tuf/builder_test.go index a993d59bb..9f33280c4 100644 --- a/tuf/builder_test.go +++ b/tuf/builder_test.go @@ -135,9 +135,7 @@ func TestMarkingIsValid(t *testing.T) { require.NoError(t, builder.Load("targets/a/b", meta["targets/a/b"], 1, false)) valid, _, err := builder.Finish() - // TODO: Once ValidateRoot is changes to set IsValid as per PR #800 we should uncomment the below test to make - // sure that IsValid is being set on loadRoot - //require.True(t, valid.Root.Signatures[0].IsValid) + require.True(t, valid.Root.Signatures[0].IsValid) require.True(t, valid.Timestamp.Signatures[0].IsValid) require.True(t, valid.Snapshot.Signatures[0].IsValid) require.True(t, valid.Targets[data.CanonicalTargetsRole].Signatures[0].IsValid) @@ -261,6 +259,10 @@ func TestBuilderStopsAcceptingOrProducingDataOnceDone(t *testing.T) { require.Error(t, err) require.Equal(t, tuf.ErrBuildDone, err) + err = builder.LoadRootForUpdate(meta["root"], 1, true) + require.Error(t, err) + require.Equal(t, tuf.ErrBuildDone, err) + // a new bootstrapped builder can also not have any more input output bootstrapped := builder.BootstrapNewBuilder() diff --git a/tuf/signed/interface.go b/tuf/signed/interface.go index 862b23b8f..9f64a47e6 100644 --- a/tuf/signed/interface.go +++ b/tuf/signed/interface.go @@ -1,8 +1,6 @@ package signed -import ( - "github.com/docker/notary/tuf/data" -) +import "github.com/docker/notary/tuf/data" // KeyService provides management of keys locally. It will never // accept or provide private keys. Communication between the KeyService diff --git a/tuf/testutils/repo.go b/tuf/testutils/repo.go index 3e9ea0e7c..0790689ff 100644 --- a/tuf/testutils/repo.go +++ b/tuf/testutils/repo.go @@ -184,7 +184,7 @@ func SignAndSerialize(tufRepo *tuf.Repo) (map[string][]byte, error) { // Sign signs all top level roles in a repo in the appropriate order func Sign(repo *tuf.Repo) (root, targets, snapshot, timestamp *data.Signed, err error) { - root, err = repo.SignRoot(data.DefaultExpires("root")) + root, err = repo.SignRoot(data.DefaultExpires("root"), nil) if _, ok := err.(data.ErrInvalidRole); err != nil && !ok { return nil, nil, nil, nil, err } diff --git a/tuf/testutils/swizzler.go b/tuf/testutils/swizzler.go index 39fd210a7..336a2e956 100644 --- a/tuf/testutils/swizzler.go +++ b/tuf/testutils/swizzler.go @@ -2,6 +2,7 @@ package testutils import ( "bytes" + "fmt" "path" "time" @@ -293,6 +294,28 @@ func (m *MetadataSwizzler) OffsetMetadataVersion(role string, offset int) error return err } + if role == data.CanonicalRootRole { + // store old versions of roots accessible by version + version, ok := unmarshalled["version"].(float64) + if !ok { + version = float64(0) // just ignore the error and set it to 0 + } + + versionedRole := fmt.Sprintf("%d.%s", int(version), data.CanonicalRootRole) + pubKeys, err := getPubKeys(m.CryptoService, signedThing, role) + if err != nil { + return err + } + versionedMetaBytes, err := serializeMetadata(m.CryptoService, signedThing, role, pubKeys...) + if err != nil { + return err + } + err = m.MetadataCache.Set(versionedRole, versionedMetaBytes) + if err != nil { + return err + } + } + oldVersion, ok := unmarshalled["version"].(float64) if !ok { oldVersion = float64(0) // just ignore the error and set it to 0 diff --git a/tuf/tuf.go b/tuf/tuf.go index 135220022..fa8d18921 100644 --- a/tuf/tuf.go +++ b/tuf/tuf.go @@ -6,8 +6,6 @@ import ( "encoding/json" "fmt" "path" - "sort" - "strconv" "strings" "time" @@ -180,6 +178,7 @@ func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error { tr.cryptoService.RemoveKey(k) } } + tr.Root.Dirty = true return nil } @@ -876,23 +875,13 @@ func (tr *Repo) UpdateTimestamp(s *data.Signed) error { return nil } -type versionedRootRole struct { - data.BaseRole - version int -} - -type versionedRootRoles []versionedRootRole - -func (v versionedRootRoles) Len() int { return len(v) } -func (v versionedRootRoles) Swap(i, j int) { v[i], v[j] = v[j], v[i] } -func (v versionedRootRoles) Less(i, j int) bool { return v[i].version < v[j].version } - // 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). 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) { +// Extra signing keys can be added to support older clients +func (tr *Repo) SignRoot(expires time.Time, extraSigningKeys data.KeyList) (*data.Signed, error) { logrus.Debug("signing root...") // duplicate root and attempt to modify it rather than the existing root @@ -910,40 +899,12 @@ func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) { return nil, err } - oldRootRoles := tr.getOldRootRoles() - - var latestSavedRole data.BaseRole - rolesToSignWith := make([]data.BaseRole, 0, len(oldRootRoles)) - - if len(oldRootRoles) > 0 { - sort.Sort(oldRootRoles) - for _, vRole := range oldRootRoles { - rolesToSignWith = append(rolesToSignWith, vRole.BaseRole) - } - latest := rolesToSignWith[len(rolesToSignWith)-1] - latestSavedRole = data.BaseRole{ - Name: data.CanonicalRootRole, - Threshold: latest.Threshold, - Keys: latest.Keys, - } - } + var rolesToSignWith []data.BaseRole - // If the root role (root keys or root threshold) has changed, save the - // previous role under the role name "root.", such that the "n" is the - // latest root.json version for which previous root role was valid. - // Also, guard against re-saving the previous role if the latest - // saved role is the same (which should not happen). - // n = root.json version of the originalRootRole (previous role) - // n+1 = root.json version of the currRoot (current role) - // n-m = root.json version of latestSavedRole (not necessarily n-1, because the - // last root rotation could have happened several root.json versions ago - if !tr.originalRootRole.Equals(currRoot) && !tr.originalRootRole.Equals(latestSavedRole) { + // If the root role (root keys or root threshold) has changed, sign with the + // previous root role keys + if !tr.originalRootRole.Equals(currRoot) { rolesToSignWith = append(rolesToSignWith, tr.originalRootRole) - latestSavedRole = tr.originalRootRole - - versionName := oldRootVersionName(tempRoot.Signed.Version) - tempRoot.Signed.Roles[versionName] = &data.RootRole{ - KeyIDs: latestSavedRole.ListKeyIDs(), Threshold: latestSavedRole.Threshold} } tempRoot.Signed.Expires = expires @@ -954,7 +915,8 @@ func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) { if err != nil { return nil, err } - signed, err = tr.sign(signed, rolesToSignWith, tr.getOptionalRootKeys(rolesToSignWith)) + + signed, err = tr.sign(signed, rolesToSignWith, extraSigningKeys) if err != nil { return nil, err } @@ -965,62 +927,6 @@ func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) { return signed, nil } -// get all the saved previous roles < the current root version -func (tr *Repo) getOldRootRoles() versionedRootRoles { - oldRootRoles := make(versionedRootRoles, 0, len(tr.Root.Signed.Roles)) - - // now go through the old roles - for roleName := range tr.Root.Signed.Roles { - // ensure that the rolename matches our format and that the version is - // not too high - if data.ValidRole(roleName) { - continue - } - nameTokens := strings.Split(roleName, ".") - if len(nameTokens) != 2 || nameTokens[0] != data.CanonicalRootRole { - continue - } - version, err := strconv.Atoi(nameTokens[1]) - if err != nil || version >= tr.Root.Signed.Version { - continue - } - - // ignore invalid roles, which shouldn't happen - oldRole, err := tr.Root.BuildBaseRole(roleName) - if err != nil { - continue - } - - oldRootRoles = append(oldRootRoles, versionedRootRole{BaseRole: oldRole, version: version}) - } - - return oldRootRoles -} - -// gets any extra optional root keys from the existing root.json signatures -// (because older repositories that have already done root rotation may not -// necessarily have older root roles) -func (tr *Repo) getOptionalRootKeys(signingRoles []data.BaseRole) []data.PublicKey { - oldKeysMap := make(map[string]data.PublicKey) - for _, oldSig := range tr.Root.Signatures { - if k, ok := tr.Root.Signed.Keys[oldSig.KeyID]; ok { - oldKeysMap[k.ID()] = k - } - } - for _, role := range signingRoles { - for keyID := range role.Keys { - delete(oldKeysMap, keyID) - } - } - - oldKeys := make([]data.PublicKey, 0, len(oldKeysMap)) - for _, key := range oldKeysMap { - oldKeys = append(oldKeys, key) - } - - return oldKeys -} - func oldRootVersionName(version int) string { return fmt.Sprintf("%s.%v", data.CanonicalRootRole, version) } diff --git a/tuf/tuf_test.go b/tuf/tuf_test.go index 9132680a2..dd11f2e64 100644 --- a/tuf/tuf_test.go +++ b/tuf/tuf_test.go @@ -82,7 +82,7 @@ func TestInitSnapshotNoTargets(t *testing.T) { func writeRepo(t *testing.T, dir string, repo *Repo) { err := os.MkdirAll(dir, 0755) require.NoError(t, err) - signedRoot, err := repo.SignRoot(data.DefaultExpires("root")) + signedRoot, err := repo.SignRoot(data.DefaultExpires("root"), nil) require.NoError(t, err) rootJSON, _ := json.Marshal(signedRoot) ioutil.WriteFile(dir+"/root.json", rootJSON, 0755) @@ -1024,17 +1024,10 @@ func TestReplaceBaseKeysInRoot(t *testing.T) { origNumRoles := len(repo.Root.Signed.Roles) // sign the root and assert the number of roles after - _, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + _, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) - - switch role { - case data.CanonicalRootRole: - // root role changed, so the old role should have been saved - require.Len(t, repo.Root.Signed.Roles, origNumRoles+1) - default: - // number of roles should not have changed - require.Len(t, repo.Root.Signed.Roles, origNumRoles) - } + // number of roles should not have changed + require.Len(t, repo.Root.Signed.Roles, origNumRoles) } } @@ -1328,7 +1321,7 @@ func TestSignRootOldKeyCertExists(t *testing.T) { repo := initRepoWithRoot(t, cs, oldRootCertKey) // Create a first signature, using the old key. - signedRoot, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + signedRoot, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, oldRootCertKey) err = verifyRootSignatureAgainstKey(t, signedRoot, oldRootCertKey) @@ -1350,7 +1343,7 @@ func TestSignRootOldKeyCertExists(t *testing.T) { require.Equal(t, newRootCertKey.ID(), updatedRootKeyIDs[0]) // Create a second signature - signedRoot, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + signedRoot, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, oldRootCertKey, newRootCertKey) @@ -1381,7 +1374,7 @@ func TestSignRootOldKeyCertMissing(t *testing.T) { repo := initRepoWithRoot(t, cs, oldRootCertKey) // Create a first signature, using the old key. - signedRoot, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + signedRoot, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, oldRootCertKey) err = verifyRootSignatureAgainstKey(t, signedRoot, oldRootCertKey) @@ -1409,7 +1402,7 @@ func TestSignRootOldKeyCertMissing(t *testing.T) { repo2.originalRootRole = updatedRootRole // Create a second signature - signedRoot, err = repo2.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + signedRoot, err = repo2.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, newRootCertKey) // Without oldRootCertKey @@ -1420,18 +1413,17 @@ func TestSignRootOldKeyCertMissing(t *testing.T) { require.Error(t, err) } -// SignRoot signs with all old roles with valid keys, and also optionally any old -// signatures we have keys for even if they aren't in an old root. It ignores any -// root role whose version is higher than the current version. If signing fails, -// it reverts back. -func TestSignRootOldRootRolesAndOldSigs(t *testing.T) { +// SignRoot signs with the current root and the previous, to allow root key +// rotation. After signing with the previous keys, they can be discarded from +// the root role. +func TestRootKeyRotation(t *testing.T) { gun := "docker/test-sign-root" referenceTime := time.Now() cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore( passphrase.ConstantRetriever("password"))) - rootCertKeys := make([]data.PublicKey, 9) + rootCertKeys := make([]data.PublicKey, 7) rootPrivKeys := make([]data.PrivateKey, cap(rootCertKeys)) for i := 0; i < cap(rootCertKeys); i++ { rootPublicKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) @@ -1445,140 +1437,55 @@ func TestSignRootOldRootRolesAndOldSigs(t *testing.T) { rootPrivKeys[i] = rootPrivateKey } - repo := initRepoWithRoot(t, cs, rootCertKeys[6]) - // sign with key 0, which represents the key for the a version of the root we - // no longer have a record of + // Initialize and sign with one key + repo := initRepoWithRoot(t, cs, rootCertKeys[0]) signedObj, err := repo.Root.ToSigned() require.NoError(t, err) signedObj, err = repo.sign(signedObj, nil, []data.PublicKey{rootCertKeys[0]}) require.NoError(t, err) - // should be signed with key 0 verifySignatureList(t, signedObj, rootCertKeys[0]) repo.Root.Signatures = signedObj.Signatures - // 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 { - repo.Root.Signed.Keys[key.ID()] = key - } - } - // invalid root role because key not included in the key map - valid root version name - repo.Root.Signed.Roles["root.1"] = &data.RootRole{KeyIDs: []string{rootCertKeys[1].ID()}, Threshold: 1} - // invalid root versions names, but valid roles - repo.Root.Signed.Roles["2.root"] = &data.RootRole{KeyIDs: []string{rootCertKeys[2].ID()}, Threshold: 1} - repo.Root.Signed.Roles["root3"] = &data.RootRole{KeyIDs: []string{rootCertKeys[3].ID()}, Threshold: 1} - repo.Root.Signed.Roles["root.4a"] = &data.RootRole{KeyIDs: []string{rootCertKeys[4].ID()}, Threshold: 1} - // valid old root role and version - repo.Root.Signed.Roles["root.5"] = &data.RootRole{KeyIDs: []string{rootCertKeys[5].ID()}, Threshold: 1} - // greater or equal to the current root version, so invalid name, but valid root role - repo.Root.Signed.Roles["root.6"] = &data.RootRole{KeyIDs: []string{rootCertKeys[7].ID()}, Threshold: 1} - - lenRootRoles := len(repo.Root.Signed.Roles) - - // rotate the current key to key 8 - require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[8])) - - requiredKeys := []data.PrivateKey{ - rootPrivKeys[5], // we need an old valid root role - this was specified in root5 - rootPrivKeys[6], // we need the previous valid key prior to root rotation - rootPrivKeys[8], // we need the new root key we've rotated to - } - - for _, privKey := range requiredKeys { - // if we can't sign with a previous root, we fail - require.NoError(t, cs.RemoveKey(privKey.ID())) - _, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) - require.Error(t, err) - require.IsType(t, signed.ErrInsufficientSignatures{}, err) - require.Contains(t, err.Error(), "signing keys not available") - - // add back for next test - 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 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 - for _, index := range []int{1, 2, 3, 4, 7} { - require.NoError(t, cs.RemoveKey(rootPrivKeys[index].ID())) - } - - // SignRoot will sign with all the old keys based on old root roles as well - // as any old signatures - signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + // Add new root key, should sign with previous and new + require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[1])) + signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) expectedSigningKeys := []data.PublicKey{ - rootCertKeys[0], // old signature key, not in any role - rootCertKeys[5], // root.5 key which is valid - rootCertKeys[6], // previous key before rotation, - rootCertKeys[8], // newly rotated key + rootCertKeys[0], + rootCertKeys[1], } verifySignatureList(t, signedObj, expectedSigningKeys...) - // verify that we saved the previous root (which overwrote an invalid saved root), - // since it wasn't in the list of old valid roots, and we didn't save the newest - // role - require.NotNil(t, repo.Root.Signed.Roles["root.6"]) - require.Equal(t, data.RootRole{KeyIDs: []string{rootCertKeys[6].ID()}, Threshold: 1}, - *repo.Root.Signed.Roles["root.6"]) - require.Nil(t, repo.Root.Signed.Roles["root.7"]) - - // bumped version, 1 new roles, but one overwrote the previous root.6, so actually no - // additional roles - require.Equal(t, 7, repo.Root.Signed.Version) - require.Len(t, repo.Root.Signed.Roles, lenRootRoles) - require.True(t, oldExpiry.Before(repo.Root.Signed.Expires)) - lenRootRoles = len(repo.Root.Signed.Roles) - // remove the optional key - require.NoError(t, cs.RemoveKey(rootPrivKeys[0].ID())) - - // 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.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) + // Add new root key, should sign with previous and new, not with old + require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[2])) + signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) - require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalTargetsRole, newTargetsKey)) + expectedSigningKeys = []data.PublicKey{ + rootCertKeys[1], + rootCertKeys[2], + } + verifySignatureList(t, signedObj, expectedSigningKeys...) - // 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)) + // Rotate to two new keys, should be signed with previous and current (3 total) + require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[3], rootCertKeys[4])) + signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) 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.True(t, oldExpiry.Before(repo.Root.Signed.Expires)) // expiry updated + expectedSigningKeys = []data.PublicKey{ + rootCertKeys[2], + rootCertKeys[3], + rootCertKeys[4], + } + verifySignatureList(t, signedObj, expectedSigningKeys...) - // 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) + // Rotate to two new keys, should be signed with previous set and current set (4 total) + require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[5], rootCertKeys[6])) + signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) - require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalTargetsRole, newTargetsKey)) - - 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) - require.Contains(t, err.Error(), "signing keys not available") - - // 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) + expectedSigningKeys = []data.PublicKey{ + rootCertKeys[3], + rootCertKeys[4], + rootCertKeys[5], + rootCertKeys[6], + } + verifySignatureList(t, signedObj, expectedSigningKeys...) }