Skip to content

Commit

Permalink
client: add support for rotating root keys
Browse files Browse the repository at this point in the history
Signed-off-by: Evan Cordell <[email protected]>
  • Loading branch information
ecordell committed Sep 7, 2016
1 parent 4036e81 commit bea7cac
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 30 deletions.
9 changes: 6 additions & 3 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ func (r *NotaryRepository) Update(forWrite bool) error {
// notFound.Resource may include a checksum so when the role is root,
// it will be root or root.<checksum>. Therefore best we can
// do it match a "root." prefix
if notFound, ok := err.(store.ErrMetaNotFound); ok && strings.HasPrefix(notFound.Resource, data.CanonicalRootRole+".") {
if notFound, ok := err.(store.ErrMetaNotFound); ok && strings.Contains(notFound.Resource, data.CanonicalRootRole) {
return r.errRepositoryNotExist()
}
return err
Expand All @@ -823,8 +823,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
Expand Down
15 changes: 13 additions & 2 deletions client/client_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,21 @@ 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 {
require.NoError(t, err)
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)
Expand Down Expand Up @@ -158,6 +165,10 @@ 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
}
actual, err := repo.fileStore.GetSized(r, store.NoSizeLimit)
require.NoError(t, err, "problem getting repo metadata for %s", r)
if role == r {
Expand Down Expand Up @@ -1263,7 +1274,7 @@ func testUpdateRemoteCorruptValidChecksum(t *testing.T, opts updateOpts, expt sw
var expectedTypes []string
for _, expectErr := range expt.expectErrs {
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",
Expand Down
106 changes: 91 additions & 15 deletions client/tufclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client

import (
"encoding/json"
"fmt"

"github.com/Sirupsen/logrus"
"github.com/docker/notary"
Expand Down Expand Up @@ -47,7 +48,7 @@ func (c *TUFClient) Update() (*tuf.Repo, *tuf.Repo, error) {

c.newBuilder = c.newBuilder.BootstrapNewBuilder()

if err := c.downloadRoot(); err != nil {
if err := c.updateRoot(); err != nil {
logrus.Debug("Client Update (Root):", err)
return nil, nil, err
}
Expand Down Expand Up @@ -78,25 +79,79 @@ func (c *TUFClient) update() error {
return nil
}

// downloadRoot is responsible for downloading the root.json
func (c *TUFClient) downloadRoot() error {
// 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 {
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 Current Root Version
currentRootConsistentInfo := c.oldBuilder.GetConsistentInfo(role)
currentRoot, _ := c.cache.GetSized(role, -1)
c.oldBuilder.Load(currentRootConsistentInfo.RoleName, currentRoot, 1, true)
currentVersion := c.oldBuilder.GetLoadedVersion(currentRootConsistentInfo.RoleName)

// 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
// Get New Root Version
raw, err := c.remote.GetSized(data.CanonicalRootRole, -1)
if err != nil {
logrus.Debugf("error downloading root: %s", err)
return err
}
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.Version
if newestVersion-currentVersion < 2 {
// We're only one version behind, just download the new root
if err := c.downloadRoot(); err != nil {
return err
}
} else {
if err := c.updateRootVersions(currentRoot, currentVersion, newestVersion); err != nil {
return err
}

_, err := c.tryLoadCacheThenRemote(consistentInfo)
return err
}
return nil
}

// updateRootVersions updates the root from it's current version to a target, rotating keys
// as they are found
func (c *TUFClient) updateRootVersions(currentRoot []byte, fromVersion, toVersion int) error {
if fromVersion == toVersion {
return nil
}
nextVersion := fromVersion + 1

logrus.Debugf("updating root from version %d to version %d, up to %d", fromVersion, nextVersion, toVersion)

var versionedRole string
if nextVersion == toVersion {
logrus.Debugf("updating root to %d by requesting latest", toVersion)
versionedRole = data.CanonicalRootRole
} else {
versionedRole = fmt.Sprintf("%d.%s", nextVersion, 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.LoadUnchecked(data.CanonicalRootRole, raw, nextVersion); err != nil {
logrus.Debugf("downloaded %s is invalid: %s", versionedRole, err)
return err
}
logrus.Debugf("successfully verified downloaded %s", versionedRole)
if err := c.cache.Set(data.CanonicalRootRole, raw); err != nil {
logrus.Debugf("Unable to write %s to cache: %s", versionedRole, err)
}
return c.updateRootVersions(raw, nextVersion, toVersion)
}

// downloadTimestamp is responsible for downloading the timestamp.json
Expand Down Expand Up @@ -198,6 +253,27 @@ 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() 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
_, remoteErr := c.tryLoadRemote(consistentInfo, cachedRoot)
return remoteErr
}

_, err := c.tryLoadCacheThenRemote(consistentInfo)
return err
}

func (c *TUFClient) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([]byte, error) {
cachedTS, err := c.cache.GetSized(consistentInfo.RoleName, consistentInfo.Length())
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/notary/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1688,7 +1688,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) {
Expand Down
33 changes: 24 additions & 9 deletions tuf/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
LoadUnchecked(roleName string, content []byte, minVersion int) error
GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error)
GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error)
Finish() (*Repo, *Repo, error)
Expand All @@ -74,6 +75,9 @@ type finishedBuilder struct{}
func (f finishedBuilder) Load(roleName string, content []byte, minVersion int, allowExpired bool) error {
return ErrBuildDone
}
func (f finishedBuilder) LoadUnchecked(roleName string, content []byte, minVersion int) error {
return ErrBuildDone
}
func (f finishedBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) {
return nil, 0, ErrBuildDone
}
Expand Down Expand Up @@ -226,11 +230,20 @@ func (rb *repoBuilder) GetConsistentInfo(roleName string) ConsistentInfo {
}

func (rb *repoBuilder) Load(roleName string, content []byte, minVersion int, allowExpired bool) error {
return rb.load(roleName, content, minVersion, allowExpired, false, false)
}

// LoadUnchecked loads a role without checking expiry or checksum. Should only be used for root updating.
func (rb *repoBuilder) LoadUnchecked(roleName string, content []byte, minVersion int) error {
return rb.load(roleName, content, minVersion, true, true, true)
}

func (rb *repoBuilder) load(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)}
}

Expand All @@ -249,7 +262,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:
Expand Down Expand Up @@ -392,10 +405,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
}
Expand Down Expand Up @@ -545,7 +558,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
}
Expand Down Expand Up @@ -637,9 +650,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
Expand All @@ -653,7 +668,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
}
Expand Down
23 changes: 23 additions & 0 deletions tuf/testutils/swizzler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package testutils

import (
"bytes"
"fmt"
"path"
"time"

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit bea7cac

Please sign in to comment.