diff --git a/client/client.go b/client/client.go index 12f2af5933..fab3ca2667 100644 --- a/client/client.go +++ b/client/client.go @@ -642,7 +642,6 @@ func (r *NotaryRepository) publish(cl changelist.Changelist) error { // a not yet published repo or a possibly obsolete local copy) into // r.tufRepo. This attempts to load metadata for all roles. Since server // snapshots are supported, if the snapshot metadata fails to load, that's ok. -// This can also be unified with some cache reading tools from tuf/client. // This assumes that bootstrapRepo is only used by Publish() or RotateKey() func (r *NotaryRepository) bootstrapRepo() error { b := tuf.NewRepoBuilder(r.gun, r.CryptoService, r.trustPinning) @@ -653,6 +652,9 @@ func (r *NotaryRepository) bootstrapRepo() error { jsonBytes, err := r.fileStore.GetMeta(role, -1) if err != nil { if _, ok := err.(store.ErrMetaNotFound); ok && + // server snapshots are supported, and server timestamp management + // is required, so if either of these fail to load that's ok - especially + // if the repo is new role == data.CanonicalSnapshotRole || role == data.CanonicalTimestampRole { continue } @@ -767,7 +769,7 @@ func (r *NotaryRepository) Update(forWrite bool) error { // Returns a tufclient.Client for the remote server, which may not be actually // operational (if the URL is invalid but a root.json is cached). func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Client, error) { - version := 0 + minVersion := 0 oldBuilder := tuf.NewRepoBuilder(r.gun, r.CryptoService, r.trustPinning) var newBuilder tuf.RepoBuilder @@ -776,16 +778,16 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl // us to download a new root and perform a rotation. if rootJSON, err := r.fileStore.GetMeta(data.CanonicalRootRole, -1); err == nil { // if we can't load the cached root, fail hard because that is how we pin trust - if err := oldBuilder.Load(data.CanonicalRootRole, rootJSON, version, true); err != nil { + if err := oldBuilder.Load(data.CanonicalRootRole, rootJSON, minVersion, true); err != nil { return nil, err } // use the old builder to bootstrap the new builder - we're just going to // verify the same data again, but with this time we want to validate the expiry - version = oldBuilder.GetLoadedVersion(data.CanonicalRootRole) + minVersion = oldBuilder.GetLoadedVersion(data.CanonicalRootRole) newBuilder = oldBuilder.BootstrapNewBuilder() // ignore error - if there's an error, the root won't be loaded - newBuilder.Load(data.CanonicalRootRole, rootJSON, version, false) + newBuilder.Load(data.CanonicalRootRole, rootJSON, minVersion, false) } if newBuilder == nil { @@ -810,7 +812,7 @@ func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Cl if !newBuilder.IsLoaded(data.CanonicalRootRole) { // we always want to use the downloaded root if we couldn't load from cache - if err := newBuilder.Load(data.CanonicalRootRole, tmpJSON, version, false); err != nil { + if err := newBuilder.Load(data.CanonicalRootRole, tmpJSON, minVersion, false); err != nil { return nil, err } diff --git a/tuf/builder.go b/tuf/builder.go index b08b2ecb2b..3230ddbb2f 100644 --- a/tuf/builder.go +++ b/tuf/builder.go @@ -14,19 +14,8 @@ import ( // ErrBuildDone is returned when any functions are called on RepoBuilder, and it // is already finished building -type ErrBuildDone struct{} - -func (e ErrBuildDone) Error() string { - return "the builder is done building and cannot accept any more input or produce any more output" -} - -// ErrBuildFailed is returned when any functions are called on RepoBuilder, and it -// is already failed building and will not accept any other data -type ErrBuildFailed struct{} - -func (e ErrBuildFailed) Error() string { - return "the builder has failed building and cannot accept any more input or produce any more output" -} +var ErrBuildDone = fmt.Errorf( + "the builder has finished building and cannot accept any more input or produce any more output") // ErrInvalidBuilderInput is returned when RepoBuilder.Load is called // with the wrong type of metadata for thes tate that it's in @@ -79,20 +68,56 @@ type RepoBuilder interface { GetConsistentInfo(roleName string) ConsistentInfo } +// finishedBuilder refuses any more input or output +type finishedBuilder struct{} + +func (f finishedBuilder) Load(roleName string, content []byte, minVersion int, allowExpired bool) error { + return ErrBuildDone +} +func (f finishedBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) { + return nil, 0, ErrBuildDone +} +func (f finishedBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) { + return nil, 0, ErrBuildDone +} +func (f finishedBuilder) Finish() (*Repo, error) { return nil, ErrBuildDone } +func (f finishedBuilder) BootstrapNewBuilder() RepoBuilder { return f } +func (f finishedBuilder) IsLoaded(roleName string) bool { return false } +func (f finishedBuilder) GetLoadedVersion(roleName string) int { return 0 } +func (f finishedBuilder) GetConsistentInfo(roleName string) ConsistentInfo { + return ConsistentInfo{RoleName: roleName} +} + // NewRepoBuilder is the only way to get a pre-built RepoBuilder func NewRepoBuilder(gun string, cs signed.CryptoService, trustpin trustpinning.TrustPinConfig) RepoBuilder { - return &repoBuilder{ + return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ repo: NewRepo(cs), gun: gun, trustpin: trustpin, loadedNotChecksummed: make(map[string][]byte), + }} +} + +// repoBuilderWrapper embeds a repoBuilder, but once Finish is called, swaps +// the embed out with a finishedBuilder +type repoBuilderWrapper struct { + RepoBuilder +} + +func (rbw *repoBuilderWrapper) Finish() (*Repo, error) { + switch rbw.RepoBuilder.(type) { + case finishedBuilder: + return rbw.RepoBuilder.Finish() + default: + old := rbw.RepoBuilder + rbw.RepoBuilder = finishedBuilder{} + return old.Finish() } } +// repoBuilder actually builds a tuf.Repo type repoBuilder struct { - finished bool - failed bool - repo *Repo + repo *Repo // needed for root trust pininng verification gun string @@ -103,38 +128,88 @@ type repoBuilder struct { // data with checksums come in loadedNotChecksummed map[string][]byte - // needed for bootstrapping a builder to validate a new root - prevRoot *data.SignedRoot - rootChecksum *data.FileMeta + // bootstrapped values to validate a new root + prevRoot *data.SignedRoot + bootstrappedRootChecksum *data.FileMeta + + // for bootstrapping the next builder + nextRootChecksum *data.FileMeta } func (rb *repoBuilder) Finish() (*Repo, error) { - if rb.finished { - return nil, ErrBuildDone{} - } - - rb.finished = true return rb.repo, nil } func (rb *repoBuilder) BootstrapNewBuilder() RepoBuilder { - var rootChecksum *data.FileMeta - - if rb.repo.Snapshot != nil { - meta := rb.repo.Snapshot.Signed.Meta[data.CanonicalRootRole] - rootChecksum = &meta - } - - return &repoBuilder{ + return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ repo: NewRepo(rb.repo.cryptoService), gun: rb.gun, loadedNotChecksummed: make(map[string][]byte), + trustpin: rb.trustpin, - prevRoot: rb.repo.Root, - rootChecksum: rootChecksum, + prevRoot: rb.repo.Root, + bootstrappedRootChecksum: rb.nextRootChecksum, + }} +} + +// IsLoaded returns whether a particular role has already been loaded +func (rb *repoBuilder) IsLoaded(roleName string) bool { + switch roleName { + case data.CanonicalRootRole: + return rb.repo.Root != nil + case data.CanonicalSnapshotRole: + return rb.repo.Snapshot != nil + case data.CanonicalTimestampRole: + return rb.repo.Timestamp != nil + default: + return rb.repo.Targets[roleName] != nil } } +// GetLoadedVersion returns the metadata version, if it is loaded, or 0 otherwise +func (rb *repoBuilder) GetLoadedVersion(roleName string) int { + switch { + case roleName == data.CanonicalRootRole && rb.repo.Root != nil: + return rb.repo.Root.Signed.Version + case roleName == data.CanonicalSnapshotRole && rb.repo.Snapshot != nil: + return rb.repo.Snapshot.Signed.Version + case roleName == data.CanonicalTimestampRole && rb.repo.Timestamp != nil: + return rb.repo.Timestamp.Signed.Version + default: + if tgts, ok := rb.repo.Targets[roleName]; ok { + return tgts.Signed.Version + } + } + + return 0 +} + +// GetConsistentInfo returns the consistent name and size of a role, if it is known, +// otherwise just the rolename and a -1 for size (both of which are inside a +// ConsistentInfo object) +func (rb *repoBuilder) GetConsistentInfo(roleName string) ConsistentInfo { + info := ConsistentInfo{RoleName: roleName} // starts out with unknown filemeta + switch roleName { + case data.CanonicalTimestampRole: + // we do not want to get a consistent timestamp, but we do want to + // limit its size + info.fileMeta.Length = notary.MaxTimestampSize + case data.CanonicalSnapshotRole: + if rb.repo.Timestamp != nil { + info.fileMeta = rb.repo.Timestamp.Signed.Meta[roleName] + } + case data.CanonicalRootRole: + if rb.bootstrappedRootChecksum != nil { + info.fileMeta = *rb.bootstrappedRootChecksum + } + default: + if rb.repo.Snapshot != nil { + info.fileMeta = rb.repo.Snapshot.Signed.Meta[roleName] + } + } + return info +} + func (rb *repoBuilder) Load(roleName string, content []byte, minVersion int, allowExpired bool) error { if !data.ValidRole(roleName) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s is an invalid role", roleName)} @@ -180,80 +255,42 @@ func (rb *repoBuilder) checkPrereqsLoaded(prereqRoles []string) error { return nil } -// IsLoaded returns whether a particular role has already been loaded -func (rb *repoBuilder) IsLoaded(roleName string) bool { - switch roleName { - case data.CanonicalRootRole: - return rb.repo.Root != nil - case data.CanonicalSnapshotRole: - return rb.repo.Snapshot != nil - case data.CanonicalTimestampRole: - return rb.repo.Timestamp != nil - default: - return rb.repo.Targets[roleName] != nil - } -} - -// GetLoadedVersion returns the metadata version, if it is loaded, or -1 otherwise -func (rb *repoBuilder) GetLoadedVersion(roleName string) int { - switch { - case roleName == data.CanonicalRootRole && rb.repo.Root != nil: - return rb.repo.Root.Signed.Version - case roleName == data.CanonicalSnapshotRole && rb.repo.Snapshot != nil: - return rb.repo.Snapshot.Signed.Version - case roleName == data.CanonicalTimestampRole && rb.repo.Timestamp != nil: - return rb.repo.Timestamp.Signed.Version - default: - if tgts, ok := rb.repo.Targets[roleName]; ok { - return tgts.Signed.Version - } - } - - return -1 -} - -// GetConsistentInfo returns the consistent name and size of a role, if it is known, -// otherwise just the rolename and a -1 for size -func (rb *repoBuilder) GetConsistentInfo(roleName string) ConsistentInfo { - info := ConsistentInfo{RoleName: roleName} // starts out with unknown filemeta - switch roleName { - case data.CanonicalTimestampRole: - // we do not want to get a consistent timestamp, but we do want to - // limit its size - info.fileMeta.Length = notary.MaxTimestampSize - case data.CanonicalSnapshotRole: - if rb.repo.Timestamp != nil { - info.fileMeta = rb.repo.Timestamp.Signed.Meta[roleName] - } - case data.CanonicalRootRole: - if rb.rootChecksum != nil { - info.fileMeta = *rb.rootChecksum - } - default: - if rb.repo.Snapshot != nil { - info.fileMeta = rb.repo.Snapshot.Signed.Meta[roleName] - } - } - return info -} - +// GenerateSnapshot generates a new snapshot given a previous (optional) snapshot +// We can't just load the previous snapshot, because it may have been signed by a different +// snapshot key (maybe from a previous root version). Note that we need the root role and +// targets role to be loaded, because we need to generate metadata for both (and we need +// the root to be loaded so we can get the snapshot role to sign with) func (rb *repoBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) { - if rb.IsLoaded(data.CanonicalSnapshotRole) { + switch { + case rb.repo.cryptoService == nil: + return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate snapshot without a cryptoservice"} + case rb.IsLoaded(data.CanonicalSnapshotRole): return nil, 0, ErrInvalidBuilderInput{msg: "snapshot has already been loaded"} + case rb.IsLoaded(data.CanonicalTimestampRole): + return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate snapshot if timestamp has already been loaded"} } - if rb.IsLoaded(data.CanonicalTimestampRole) { - return nil, 0, ErrInvalidBuilderInput{msg: "Cannot generate snapshot if timestamp has already been loaded"} - } + if err := rb.checkPrereqsLoaded([]string{data.CanonicalRootRole}); err != nil { return nil, 0, err } - if prev == nil { + // If there is no previous snapshot, we need to generate one, and so the targets must + // have already been loaded. Otherwise, so long as the previous snapshot structure is + // valid (it has a targets meta), we're good. + switch prev { + case nil: + if err := rb.checkPrereqsLoaded([]string{data.CanonicalTargetsRole}); err != nil { + return nil, 0, err + } + if err := rb.repo.InitSnapshot(); err != nil { rb.repo.Snapshot = nil return nil, 0, err } - } else { + default: + if err := data.IsValidSnapshotStructure(prev.Signed); err != nil { + return nil, 0, err + } rb.repo.Snapshot = prev } @@ -263,53 +300,56 @@ func (rb *repoBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, return nil, 0, err } - // verify that we have enough signatures to pass the threshold - snapRole, err := rb.repo.GetBaseRole(data.CanonicalSnapshotRole) - if err != nil { // this should never happen, since it's already been validated - rb.repo.Snapshot = nil - return nil, 0, err - } - - if len(sgnd.Signatures) < snapRole.Threshold { - rb.repo.Snapshot = nil - return nil, 0, signed.ErrRoleThreshold{} - } - sgndJSON, err := json.Marshal(sgnd) if err != nil { rb.repo.Snapshot = nil return nil, 0, err } - // since the snapshot was generated using the root and targets data that - // that have been loaded, remove all of them from rb.loadedNotChecksummed + // loadedNotChecksummed should currently contain the root awaiting checksumming, + // since it has to have been loaded. Since the snapshot was generated using + // the root and targets data (there may not be any) that that have been loaded, + // remove all of them from rb.loadedNotChecksummed for tgtName := range rb.repo.Targets { delete(rb.loadedNotChecksummed, tgtName) } delete(rb.loadedNotChecksummed, data.CanonicalRootRole) - // cache the snapshot bytes so we can validate hte checksum in case a timestamp - // is loaded later (which should not happen, because that's almost certain - // to be an automatic failure) + // The timestamp can't have been loaded yet, so we want to cache the snapshot + // bytes so we can validate the checksum when a timestamp gets generated or + // loaded later. rb.loadedNotChecksummed[data.CanonicalSnapshotRole] = sgndJSON return sgndJSON, rb.repo.Snapshot.Signed.Version, nil } +// GenerateTimestamp generates a new timestamp given a previous (optional) timestamp +// We can't just load the previous timestamp, because it may have been signed by a different +// timestamp key (maybe from a previous root version) func (rb *repoBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) { - if rb.IsLoaded(data.CanonicalTimestampRole) { + switch { + case rb.repo.cryptoService == nil: + return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate timestamp without a cryptoservice"} + case rb.IsLoaded(data.CanonicalTimestampRole): return nil, 0, ErrInvalidBuilderInput{msg: "timestamp has already been loaded"} } + + // SignTimetamp always serializes the loaded snapshot and signs in the data, so we must always + // have the snapshot loaded first if err := rb.checkPrereqsLoaded([]string{data.CanonicalRootRole, data.CanonicalSnapshotRole}); err != nil { return nil, 0, err } - if prev == nil { + switch prev { + case nil: if err := rb.repo.InitTimestamp(); err != nil { rb.repo.Timestamp = nil return nil, 0, err } - } else { + default: + if err := data.IsValidTimestampStructure(prev.Signed); err != nil { + return nil, 0, err + } rb.repo.Timestamp = prev } @@ -319,26 +359,18 @@ func (rb *repoBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, in return nil, 0, err } - // verify that we have enough signatures to pass the threshold - tsRole, err := rb.repo.GetBaseRole(data.CanonicalTimestampRole) - if err != nil { // this should never happen, since it's already been validated - rb.repo.Timestamp = nil - return nil, 0, err - } - - if len(sgnd.Signatures) < tsRole.Threshold { - rb.repo.Timestamp = nil - return nil, 0, signed.ErrRoleThreshold{} - } - sgndJSON, err := json.Marshal(sgnd) if err != nil { rb.repo.Timestamp = nil return nil, 0, err } - // since the timestamp was generated using the snapshot that has been loaded, - // remove it from rb.loadedNotChecksummed + // The snapshot should have been loaded (and not checksumemd, since a timestamp + // cannot have been loaded), so it is awaiting checksumming. Since this + // timestamp was generated using the snapshot awaiting checksumming, we can + // remove it from rb.loadedNotChecksummed. There should be no other items + // awaiting checksumming now since loading/generating a snapshot should have + // cleared out everything else in `loadNotChecksummed`. delete(rb.loadedNotChecksummed, data.CanonicalSnapshotRole) return sgndJSON, rb.repo.Timestamp.Signed.Version, nil @@ -409,8 +441,12 @@ func (rb *repoBuilder) loadTimestamp(content []byte, minVersion int, allowExpire } } + if err := rb.validateCachedTimestampChecksums(signedTimestamp); err != nil { + return err + } + rb.repo.Timestamp = signedTimestamp - return rb.validateCachedTimestampChecksums(signedTimestamp) + return nil } func (rb *repoBuilder) loadSnapshot(content []byte, minVersion int, allowExpired bool) error { @@ -441,8 +477,19 @@ func (rb *repoBuilder) loadSnapshot(content []byte, minVersion int, allowExpired } } + // at this point, the only thing left to validate is existing checksums - we can use + // this snapshot to bootstrap the next builder if needed - and we don't need to do + // the 2-value assignment since we've already validated the signedSnapshot, which MUST + // have root metadata + rootMeta := signedSnapshot.Signed.Meta[data.CanonicalRootRole] + rb.nextRootChecksum = &rootMeta + + if err := rb.validateCachedSnapshotChecksums(signedSnapshot); err != nil { + return err + } + rb.repo.Snapshot = signedSnapshot - return rb.validateCachedSnapshotChecksums(signedSnapshot) + return nil } func (rb *repoBuilder) loadTargets(content []byte, minVersion int, allowExpired bool) error { @@ -508,24 +555,26 @@ func (rb *repoBuilder) loadDelegation(roleName string, content []byte, minVersio } func (rb *repoBuilder) validateCachedTimestampChecksums(ts *data.SignedTimestamp) error { - var err error sn, ok := rb.loadedNotChecksummed[data.CanonicalSnapshotRole] if ok { - delete(rb.loadedNotChecksummed, data.CanonicalSnapshotRole) - err = data.CheckHashes(sn, data.CanonicalSnapshotRole, ts.Signed.Meta[data.CanonicalSnapshotRole].Hashes) - if err != nil { - rb.failed = true + // by this point, the SignedTimestamp has been validated so it must have a snapshot hash + snMeta := ts.Signed.Meta[data.CanonicalSnapshotRole].Hashes + if err := data.CheckHashes(sn, data.CanonicalSnapshotRole, snMeta); err != nil { + return err } + delete(rb.loadedNotChecksummed, data.CanonicalSnapshotRole) } - return err + return nil } func (rb *repoBuilder) validateCachedSnapshotChecksums(sn *data.SignedSnapshot) error { var goodRoles []string for roleName, loadedBytes := range rb.loadedNotChecksummed { - if roleName != data.CanonicalSnapshotRole { + switch roleName { + case data.CanonicalSnapshotRole, data.CanonicalTimestampRole: + break + default: if err := data.CheckHashes(loadedBytes, roleName, sn.Signed.Meta[roleName].Hashes); err != nil { - rb.failed = true return err } goodRoles = append(goodRoles, roleName) @@ -539,8 +588,8 @@ func (rb *repoBuilder) validateCachedSnapshotChecksums(sn *data.SignedSnapshot) func (rb *repoBuilder) validateChecksumFor(content []byte, roleName string) error { // validate the bootstrap checksum for root, if provided - if roleName == data.CanonicalRootRole && rb.rootChecksum != nil { - if err := data.CheckHashes(content, roleName, rb.rootChecksum.Hashes); err != nil { + if roleName == data.CanonicalRootRole && rb.bootstrappedRootChecksum != nil { + if err := data.CheckHashes(content, roleName, rb.bootstrappedRootChecksum.Hashes); err != nil { return err } } diff --git a/tuf/builder_test.go b/tuf/builder_test.go new file mode 100644 index 0000000000..8186630fb4 --- /dev/null +++ b/tuf/builder_test.go @@ -0,0 +1,346 @@ +package tuf_test + +// package tuf_test to avoid an import cycle since we are using testutils.EmptyRepo + +import ( + "testing" + + "github.com/docker/notary/trustpinning" + "github.com/docker/notary/tuf" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/testutils" + "github.com/stretchr/testify/require" +) + +var _cachedMeta map[string][]byte + +// we just want sample metadata for a role - so we can build cached metadata +// and use it once. +func getSampleMeta(t *testing.T) (map[string][]byte, string) { + gun := "docker.com/notary" + delgNames := []string{"targets/a", "targets/a/b", "targets/a/b/force_parent_metadata"} + if _cachedMeta == nil { + meta, _, err := testutils.NewRepoMetadata(gun, delgNames...) + require.NoError(t, err) + + _cachedMeta = meta + } + return _cachedMeta, gun +} + +// We load only if the rolename is a valid rolename - even if the metadata we provided is valid +func TestBuilderLoadsValidRolesOnly(t *testing.T) { + meta, gun := getSampleMeta(t) + builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) + err := builder.Load("NotRoot", meta[data.CanonicalRootRole], 0, false) + require.Error(t, err) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "is an invalid role") +} + +func TestBuilderOnlyAcceptsRootFirstWhenLoading(t *testing.T) { + meta, gun := getSampleMeta(t) + builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) + + for roleName, content := range meta { + if roleName != data.CanonicalRootRole { + err := builder.Load(roleName, content, 0, true) + require.Error(t, err) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "root must be loaded first") + require.False(t, builder.IsLoaded(roleName)) + require.Equal(t, 0, builder.GetLoadedVersion(roleName)) + } + } + + // we can load the root + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + require.True(t, builder.IsLoaded(data.CanonicalRootRole)) +} + +func TestBuilderOnlyAcceptsDelegationsAfterParent(t *testing.T) { + meta, gun := getSampleMeta(t) + builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) + + // load the root + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + + // delegations can't be loaded without target + for _, delgName := range []string{"targets/a", "targets/a/b"} { + err := builder.Load(delgName, meta[delgName], 0, false) + require.Error(t, err) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "targets must be loaded first") + require.False(t, builder.IsLoaded(delgName)) + require.Equal(t, 0, builder.GetLoadedVersion(delgName)) + } + + // load the targets + require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 0, false)) + + // targets/a/b can't be loaded because targets/a isn't loaded + err := builder.Load("targets/a/b", meta["targets/a/b"], 0, false) + require.Error(t, err) + require.IsType(t, data.ErrInvalidRole{}, err) + + // targets/a can be loaded now though because targets is loaded + require.NoError(t, builder.Load("targets/a", meta["targets/a"], 0, false)) + + // and now targets/a/b can be loaded because targets/a is loaded + require.NoError(t, builder.Load("targets/a/b", meta["targets/a/b"], 0, false)) +} + +func TestBuilderAcceptRoleOnce(t *testing.T) { + meta, gun := getSampleMeta(t) + builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) + + for _, roleName := range append(data.BaseRoles, "targets/a", "targets/a/b") { + // first time loading is ok + require.NoError(t, builder.Load(roleName, meta[roleName], 0, false)) + require.True(t, builder.IsLoaded(roleName)) + require.Equal(t, 1, builder.GetLoadedVersion(roleName)) + + // second time loading is not + err := builder.Load(roleName, meta[roleName], 0, false) + require.Error(t, err) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "has already been loaded") + + // still loaded + require.True(t, builder.IsLoaded(roleName)) + } +} + +func TestBuilderStopsAcceptingOrProducingDataOnceDone(t *testing.T) { + meta, gun := getSampleMeta(t) + builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) + + for _, roleName := range data.BaseRoles { + require.NoError(t, builder.Load(roleName, meta[roleName], 0, false)) + require.True(t, builder.IsLoaded(roleName)) + } + + _, err := builder.Finish() + require.NoError(t, err) + + err = builder.Load("targets/a", meta["targets/a"], 0, false) + 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() + + err = bootstrapped.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false) + require.Error(t, err) + require.Equal(t, tuf.ErrBuildDone, err) + + for _, b := range []tuf.RepoBuilder{builder, bootstrapped} { + _, err = b.Finish() + require.Error(t, err) + require.Equal(t, tuf.ErrBuildDone, err) + + _, _, err = b.GenerateSnapshot(nil) + require.Error(t, err) + require.Equal(t, tuf.ErrBuildDone, err) + + _, _, err = b.GenerateTimestamp(nil) + require.Error(t, err) + require.Equal(t, tuf.ErrBuildDone, err) + + for roleName := range meta { + // a finished builder thinks nothing is loaded + require.False(t, b.IsLoaded(roleName)) + // checksums are all empty, versions are all zero + require.Equal(t, 0, b.GetLoadedVersion(roleName)) + require.Equal(t, tuf.ConsistentInfo{RoleName: roleName}, b.GetConsistentInfo(roleName)) + } + + } +} + +// Test the cases in which GenerateSnapshot fails +func TestGenerateSnapshotInvalidOperations(t *testing.T) { + gun := "docker.com/notary" + repo, cs, err := testutils.EmptyRepo(gun) + require.NoError(t, err) + + // make snapshot have 2 keys and a threshold of 2 + snapKeys := make([]data.PublicKey, 2) + for i := 0; i < 2; i++ { + snapKeys[i], err = cs.Create(data.CanonicalSnapshotRole, gun, data.ECDSAKey) + require.NoError(t, err) + } + + require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalSnapshotRole, snapKeys...)) + repo.Root.Signed.Roles[data.CanonicalSnapshotRole].Threshold = 2 + + meta, err := testutils.SignAndSerialize(repo) + require.NoError(t, err) + + for _, prevSnapshot := range []*data.SignedSnapshot{nil, repo.Snapshot} { + // copy keys, since we expect one of these generation attempts to succeed and we do + // some key deletion tests later + newCS := testutils.CopyKeys(t, cs, data.CanonicalSnapshotRole) + + // --- we can't generate a snapshot if the root isn't loaded + builder := tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) + _, _, err := builder.GenerateSnapshot(prevSnapshot) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "root must be loaded first") + require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) + + // --- we can't generate a snapshot if the targets isn't loaded and we have no previous snapshot, + // --- but if we have a previous snapshot with a valid targets, we're good even if no snapshot + // --- is loaded + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + _, _, err = builder.GenerateSnapshot(prevSnapshot) + if prevSnapshot == nil { + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "targets must be loaded first") + require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) + } else { + require.NoError(t, err) + } + + // --- we can't generate a snapshot if we've loaded the timestamp already + builder = tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + if prevSnapshot == nil { + require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 0, false)) + } + require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 0, false)) + + _, _, err = builder.GenerateSnapshot(prevSnapshot) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "cannot generate snapshot if timestamp has already been loaded") + require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) + + // --- we cannot generate a snapshot if we've already loaded a snapshot + builder = tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + if prevSnapshot == nil { + require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 0, false)) + } + require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 0, false)) + + _, _, err = builder.GenerateSnapshot(prevSnapshot) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "snapshot has already been loaded") + + // --- we cannot generate a snapshot if we can't satisfy the role threshold + for i := 0; i < len(snapKeys); i++ { + require.NoError(t, newCS.RemoveKey(snapKeys[i].ID())) + builder = tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + if prevSnapshot == nil { + require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 0, false)) + } + + _, _, err = builder.GenerateSnapshot(prevSnapshot) + require.IsType(t, signed.ErrInsufficientSignatures{}, err) + require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) + } + + // --- we cannot generate a snapshot if we don't have a cryptoservice + builder = tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + if prevSnapshot == nil { + require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 0, false)) + } + + _, _, err = builder.GenerateSnapshot(prevSnapshot) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "cannot generate snapshot without a cryptoservice") + require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) + } + + // --- we can't generate a snapshot if we're given an invalid previous snapshot (for instance, an empty one), + // --- even if we have a targets loaded + builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 0, false)) + + _, _, err = builder.GenerateSnapshot(&data.SignedSnapshot{}) + require.IsType(t, data.ErrInvalidMetadata{}, err) + require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) +} + +// Test the cases in which GenerateTimestamp fails +func TestGenerateTimestampInvalidOperations(t *testing.T) { + gun := "docker.com/notary" + repo, cs, err := testutils.EmptyRepo(gun) + require.NoError(t, err) + + // make timsetamp have 2 keys and a threshold of 2 + tsKeys := make([]data.PublicKey, 2) + for i := 0; i < 2; i++ { + tsKeys[i], err = cs.Create(data.CanonicalTimestampRole, gun, data.ECDSAKey) + require.NoError(t, err) + } + + require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalTimestampRole, tsKeys...)) + repo.Root.Signed.Roles[data.CanonicalTimestampRole].Threshold = 2 + + meta, err := testutils.SignAndSerialize(repo) + require.NoError(t, err) + + for _, prevTimestamp := range []*data.SignedTimestamp{nil, repo.Timestamp} { + // --- we can't generate a timestamp if the root isn't loaded + builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) + _, _, err := builder.GenerateTimestamp(prevTimestamp) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "root must be loaded first") + require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) + + // --- we can't generate a timestamp if the snapshot isn't loaded, no matter if we have a previous + // --- timestamp or not + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + _, _, err = builder.GenerateTimestamp(prevTimestamp) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "snapshot must be loaded first") + require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) + + // --- we can't generate a timestamp if we've loaded the timestamp already + builder = tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 0, false)) + require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 0, false)) + + _, _, err = builder.GenerateTimestamp(prevTimestamp) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "timestamp has already been loaded") + + // --- we cannot generate a timestamp if we can't satisfy the role threshold + for i := 0; i < len(tsKeys); i++ { + require.NoError(t, cs.RemoveKey(tsKeys[i].ID())) + builder = tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 0, false)) + + _, _, err = builder.GenerateTimestamp(prevTimestamp) + require.IsType(t, signed.ErrInsufficientSignatures{}, err) + require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) + } + + // --- we cannot generate a timestamp if we don't have a cryptoservice + builder = tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 0, false)) + + _, _, err = builder.GenerateTimestamp(prevTimestamp) + require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) + require.Contains(t, err.Error(), "cannot generate timestamp without a cryptoservice") + require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) + } + + // --- we can't generate a timsetamp if we're given an invalid previous timestamp (for instance, an empty one), + // --- even if we have a snapshot loaded + builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) + require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 0, false)) + + _, _, err = builder.GenerateTimestamp(&data.SignedTimestamp{}) + require.IsType(t, data.ErrInvalidMetadata{}, err) + require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) +} diff --git a/tuf/client/client.go b/tuf/client/client.go index 337915a8f1..2012e56315 100644 --- a/tuf/client/client.go +++ b/tuf/client/client.go @@ -181,7 +181,8 @@ func (c Client) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInfo) return nil, err } - // we know it unmarshals fine + // we know it unmarshals because if `tryLoadCacheThenRemote` didn't fail, then + // the raw has already been loaded into the builder json.Unmarshal(raw, tgs) return tgs.GetValidDelegations(role), nil } diff --git a/tuf/data/snapshot.go b/tuf/data/snapshot.go index f77b7a2549..682eed2903 100644 --- a/tuf/data/snapshot.go +++ b/tuf/data/snapshot.go @@ -22,10 +22,10 @@ type Snapshot struct { Meta Files `json:"meta"` } -// isValidSnapshotStructure returns an error, or nil, depending on whether the content of the +// IsValidSnapshotStructure returns an error, or nil, depending on whether the content of the // struct is valid for snapshot metadata. This does not check signatures or expiry, just that // the metadata content is valid. -func isValidSnapshotStructure(s Snapshot) error { +func IsValidSnapshotStructure(s Snapshot) error { expectedType := TUFTypes[CanonicalSnapshotRole] if s.Type != expectedType { return ErrInvalidMetadata{ @@ -157,7 +157,7 @@ func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) { if err := defaultSerializer.Unmarshal(*s.Signed, &sp); err != nil { return nil, err } - if err := isValidSnapshotStructure(sp); err != nil { + if err := IsValidSnapshotStructure(sp); err != nil { return nil, err } sigs := make([]Signature, len(s.Signatures)) diff --git a/tuf/data/timestamp.go b/tuf/data/timestamp.go index b9632a1ad1..cb7b0daef4 100644 --- a/tuf/data/timestamp.go +++ b/tuf/data/timestamp.go @@ -21,10 +21,10 @@ type Timestamp struct { Meta Files `json:"meta"` } -// isValidTimestampStructure returns an error, or nil, depending on whether the content of the struct +// IsValidTimestampStructure returns an error, or nil, depending on whether the content of the struct // is valid for timestamp metadata. This does not check signatures or expiry, just that // the metadata content is valid. -func isValidTimestampStructure(t Timestamp) error { +func IsValidTimestampStructure(t Timestamp) error { expectedType := TUFTypes[CanonicalTimestampRole] if t.Type != expectedType { return ErrInvalidMetadata{ @@ -124,7 +124,7 @@ func TimestampFromSigned(s *Signed) (*SignedTimestamp, error) { if err := defaultSerializer.Unmarshal(*s.Signed, &ts); err != nil { return nil, err } - if err := isValidTimestampStructure(ts); err != nil { + if err := IsValidTimestampStructure(ts); err != nil { return nil, err } sigs := make([]Signature, len(s.Signatures)) diff --git a/tuf/testutils/repo.go b/tuf/testutils/repo.go index 44c6ef3ed5..c5595787d7 100644 --- a/tuf/testutils/repo.go +++ b/tuf/testutils/repo.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "sort" + "testing" "time" "github.com/docker/go/canonical/json" @@ -13,6 +14,7 @@ import ( "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/utils" fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/require" tuf "github.com/docker/notary/tuf" "github.com/docker/notary/tuf/signed" @@ -52,6 +54,19 @@ func CreateKey(cs signed.CryptoService, gun, role, keyAlgorithm string) (data.Pu return key, nil } +// CopyKeys copies keys of a particular role to a new cryptoservice, and returns that cryptoservice +func CopyKeys(t *testing.T, from signed.CryptoService, roles ...string) signed.CryptoService { + memKeyStore := trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("pass")) + for _, role := range roles { + for _, keyID := range from.ListKeys(role) { + key, _, err := from.GetPrivateKey(keyID) + require.NoError(t, err) + memKeyStore.AddKey(trustmanager.KeyInfo{Role: role}, key) + } + } + return cryptoservice.NewCryptoService(memKeyStore) +} + // EmptyRepo creates an in memory crypto service // and initializes a repo with no targets. Delegations are only created // if delegation roles are passed in.