Builder interface cleanup and bugfixes:

- can never set an invalid repo, so we can remove the failed builder state
- once a builder is "finished", it's swapped out with one that has no state and cannot be modified
- add builder tests for the negative path cases where builder should error
- fix bug with GenerateSnapshot where we didn't check for a targets to be loaded when generating for the first time

This also adds some negative path tests (cases in which the builder errors due to invalid input or things not being loaded)

Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
Ying Li 2016-04-06 11:31:20 -07:00
parent d1ccf0048d
commit c9e91446a1
7 changed files with 570 additions and 157 deletions

View File

@ -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
}

View File

@ -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
}
}

346
tuf/builder_test.go Normal file
View File

@ -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))
}

View File

@ -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
}

View File

@ -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))

View File

@ -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))

View File

@ -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.