package tuf_test // tests for builder that live in an external package, tuf_test, so that we can use // the testutils without causing an import cycle import ( "bytes" "crypto/sha512" "encoding/json" "fmt" "testing" "github.com/docker/notary" "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/docker/notary/tuf/utils" "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], 1, 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, 1, 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, 1, builder.GetLoadedVersion(roleName)) } } // we can load the root require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, 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], 1, false)) // delegations can't be loaded without target for _, delgName := range []string{"targets/a", "targets/a/b"} { err := builder.Load(delgName, meta[delgName], 1, 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, 1, builder.GetLoadedVersion(delgName)) } // load the targets require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) // targets/a/b can't be loaded because targets/a isn't loaded err := builder.Load("targets/a/b", meta["targets/a/b"], 1, 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"], 1, 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"], 1, 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], 1, 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], 1, 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], 1, false)) require.True(t, builder.IsLoaded(roleName)) } _, err := builder.Finish() require.NoError(t, err) err = builder.Load("targets/a", meta["targets/a"], 1, 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], 1, 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], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) } require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, 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], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) } require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, 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], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, 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], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, 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], 1, false)) require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, 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], 1, 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], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, 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], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, 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], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, 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], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) _, _, err = builder.GenerateTimestamp(&data.SignedTimestamp{}) require.IsType(t, data.ErrInvalidMetadata{}, err) require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) } func TestGetConsistentInfo(t *testing.T) { gun := "docker.com/notary" repo, _, err := testutils.EmptyRepo(gun) require.NoError(t, err) // add some hashes for items in the snapshot that don't correspond to real metadata, but that // will cause ConsistentInfo to behave differently realSha512Sum := sha512.Sum512([]byte("stuff")) repo.Snapshot.Signed.Meta["only512"] = data.FileMeta{Hashes: data.Hashes{notary.SHA512: realSha512Sum[:]}} repo.Snapshot.Signed.Meta["targets/random"] = data.FileMeta{Hashes: data.Hashes{"randomsha": []byte("12345")}} repo.Snapshot.Signed.Meta["targets/nohashes"] = data.FileMeta{Length: 1} extraMeta := []string{"only512", "targets/random", "targets/nohashes"} meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) // if neither snapshot nor timestamp are loaded, no matter how much other data is loaded, consistent info // is empty except for timestamp: timestamps have no checksums, and the length is always -1 for _, roleToLoad := range []string{data.CanonicalRootRole, data.CanonicalTargetsRole} { require.NoError(t, builder.Load(roleToLoad, meta[roleToLoad], 1, false)) for _, checkName := range append(data.BaseRoles, extraMeta...) { ci := builder.GetConsistentInfo(checkName) require.Equal(t, checkName, ci.ConsistentName()) switch checkName { case data.CanonicalTimestampRole: // timestamp's size is always the max timestamp size require.True(t, ci.ChecksumKnown()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) default: require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) } } } // once timestamp is loaded, we can get the consistent info for snapshot but nothing else require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) for _, checkName := range append(data.BaseRoles, extraMeta...) { ci := builder.GetConsistentInfo(checkName) switch checkName { case data.CanonicalSnapshotRole: cName := utils.ConsistentName(data.CanonicalSnapshotRole, repo.Timestamp.Signed.Meta[data.CanonicalSnapshotRole].Hashes[notary.SHA256]) require.Equal(t, cName, ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.True(t, ci.Length() > -1) case data.CanonicalTimestampRole: // timestamp's canonical name is always "timestamp" and its size is always the max // timestamp size require.Equal(t, data.CanonicalTimestampRole, ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) default: require.Equal(t, checkName, ci.ConsistentName()) require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) } } // once the snapshot is loaded, we can get real consistent info for all loaded roles require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) for _, checkName := range data.BaseRoles { ci := builder.GetConsistentInfo(checkName) require.True(t, ci.ChecksumKnown(), "%s's checksum is not known", checkName) switch checkName { case data.CanonicalTimestampRole: // timestamp's canonical name is always "timestamp" and its size is always -1 require.Equal(t, data.CanonicalTimestampRole, ci.ConsistentName()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) default: fileInfo := repo.Snapshot.Signed.Meta if checkName == data.CanonicalSnapshotRole { fileInfo = repo.Timestamp.Signed.Meta } cName := utils.ConsistentName(checkName, fileInfo[checkName].Hashes[notary.SHA256]) require.Equal(t, cName, ci.ConsistentName()) require.True(t, ci.Length() > -1) } } // the fake roles have invalid-ish checksums: the ConsistentInfos for those will return // non-consistent names but non -1 sizes for _, checkName := range extraMeta { ci := builder.GetConsistentInfo(checkName) require.Equal(t, checkName, ci.ConsistentName()) // because no sha256 hash require.True(t, ci.ChecksumKnown()) require.True(t, ci.Length() > -1) } // a non-existent role's ConsistentInfo is empty ci := builder.GetConsistentInfo("nonExistent") require.Equal(t, "nonExistent", ci.ConsistentName()) require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) // when we bootstrap a new builder, the root has consistent info because the checksum is provided, // but nothing else does builder = builder.BootstrapNewBuilder() for _, checkName := range append(data.BaseRoles, extraMeta...) { ci := builder.GetConsistentInfo(checkName) switch checkName { case data.CanonicalTimestampRole: // timestamp's size is always the max timestamp size require.Equal(t, checkName, ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) case data.CanonicalRootRole: cName := utils.ConsistentName(data.CanonicalRootRole, repo.Snapshot.Signed.Meta[data.CanonicalRootRole].Hashes[notary.SHA256]) require.Equal(t, cName, ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.True(t, ci.Length() > -1) default: require.Equal(t, checkName, ci.ConsistentName()) require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) } } } // No matter what order timestamp and snapshot is loaded, if the snapshot's checksum doesn't match // what's in the timestamp, the builder will error and refuse to load the latest piece of metadata // whether that is snapshot (because it was loaded after timestamp) or timestamp (because builder // retroactive checks the loaded snapshot's checksum). Timestamp ONLY checks the snapshot checksum. func TestTimestampPreAndPostChecksumming(t *testing.T) { gun := "docker.com/notary" repo, _, err := testutils.EmptyRepo(gun, "targets/other", "targets/other/other") require.NoError(t, err) // add invalid checkums for all the other roles to timestamp too, and show that // cached items aren't checksummed against this fakeChecksum, err := data.NewFileMeta(bytes.NewBuffer([]byte("fake")), notary.SHA256, notary.SHA512) require.NoError(t, err) for _, roleName := range append(data.BaseRoles, "targets/other") { // add a wrong checksum for every role, including timestamp itself repo.Timestamp.Signed.Meta[roleName] = fakeChecksum } // this will overwrite the snapshot checksum with the right one meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // ensure that the fake meta for other roles weren't destroyed by signing the timestamp require.Len(t, repo.Timestamp.Signed.Meta, 5) snapJSON := append(meta[data.CanonicalSnapshotRole], ' ') // --- load timestamp first builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) // timestamp doesn't fail, even though its checksum for root is wrong according to timestamp require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) // loading the snapshot in fails, because of the checksum the timestamp has err = builder.Load(data.CanonicalSnapshotRole, snapJSON, 1, false) require.Error(t, err) require.IsType(t, data.ErrMismatchedChecksum{}, err) require.True(t, builder.IsLoaded(data.CanonicalTimestampRole)) require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) // all the other metadata can be loaded in, even though the checksums are wrong according to timestamp for _, roleName := range []string{data.CanonicalTargetsRole, "targets/other"} { require.NoError(t, builder.Load(roleName, meta[roleName], 1, false)) } // --- load snapshot first builder = tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) for _, roleName := range append(data.BaseRoles, "targets/other") { switch roleName { case data.CanonicalTimestampRole: continue case data.CanonicalSnapshotRole: require.NoError(t, builder.Load(roleName, snapJSON, 1, false)) default: require.NoError(t, builder.Load(roleName, meta[roleName], 1, false)) } } // timestamp fails because the snapshot checksum is wrong err = builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false) require.Error(t, err) checksumErr, ok := err.(data.ErrMismatchedChecksum) require.True(t, ok) require.Contains(t, checksumErr.Error(), "checksum for snapshot did not match") require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) require.True(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } // Creates metadata in the following manner: // - the snapshot has bad checksums for itself and for timestamp, to show that those aren't checked // - snapshot has valid checksums for root, targets, and targets/other // - snapshot doesn't have a checksum for targets/other/other, but targets/other/other is a valid // delegation role in targets/other and there is metadata for targets/other/other that is correctly // signed func setupSnapshotChecksumming(t *testing.T, gun string) map[string][]byte { repo, _, err := testutils.EmptyRepo(gun, "targets/other", "targets/other/other") require.NoError(t, err) // add invalid checkums for all the other roles to timestamp too, and show that // cached items aren't checksummed against this fakeChecksum, err := data.NewFileMeta(bytes.NewBuffer([]byte("fake")), notary.SHA256, notary.SHA512) require.NoError(t, err) // fake the snapshot and timestamp checksums repo.Snapshot.Signed.Meta[data.CanonicalSnapshotRole] = fakeChecksum repo.Snapshot.Signed.Meta[data.CanonicalTimestampRole] = fakeChecksum meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // ensure that the fake metadata for other roles wasn't destroyed by signing require.Len(t, repo.Snapshot.Signed.Meta, 5) // create delegation metadata that should not be in snapshot, but has a valid role and signature _, err = repo.InitTargets("targets/other/other") require.NoError(t, err) s, err := repo.SignTargets("targets/other/other", data.DefaultExpires(data.CanonicalTargetsRole)) require.NoError(t, err) meta["targets/other/other"], err = json.Marshal(s) require.NoError(t, err) return meta } // If the snapshot is loaded first (-ish, because really root has to be loaded first) // it will be used to validate the checksums of all other metadata that gets loaded. // If the checksum doesn't match, or if there is no checksum, then the other metadata // cannot be loaded. func TestSnapshotLoadedFirstChecksumsOthers(t *testing.T) { gun := "docker.com/notary" meta := setupSnapshotChecksumming(t, gun) // --- load root then snapshot builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) // loading timestamp is fine, even though the timestamp metadata has the wrong checksum because // we don't check timestamp checksums require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) // loading the other roles' metadata with a space will fail because of a checksum failure (builder // checks right away if the snapshot is loaded) - in the case of targets/other/other, which should // not be in snapshot at all, loading should fail even without a space because there is no checksum // for it for _, roleNameToLoad := range []string{data.CanonicalTargetsRole, "targets/other"} { err := builder.Load(roleNameToLoad, append(meta[roleNameToLoad], ' '), 0, false) require.Error(t, err) checksumErr, ok := err.(data.ErrMismatchedChecksum) require.True(t, ok) require.Contains(t, checksumErr.Error(), fmt.Sprintf("checksum for %s did not match", roleNameToLoad)) require.False(t, builder.IsLoaded(roleNameToLoad)) // now load it for real (since we need targets loaded before trying to load "targets/other") require.NoError(t, builder.Load(roleNameToLoad, meta[roleNameToLoad], 1, false)) } // loading the non-existent role wil fail err := builder.Load("targets/other/other", meta["targets/other/other"], 1, false) require.Error(t, err) require.IsType(t, data.ErrMissingMeta{}, err) require.False(t, builder.IsLoaded("targets/other/other")) } // If any other metadata is loaded first, when the snapshot is loaded it will retroactively go back // and validate that metadata. If anything fails to validate, or there is metadata for which this // snapshot has no checksums for, the snapshot will fail to validate. func TestSnapshotLoadedAfterChecksumsOthersRetroactively(t *testing.T) { gun := "docker.com/notary" meta := setupSnapshotChecksumming(t, gun) // --- load all the other metadata first, but with an extra space at the end which should // --- validate fine, except for the checksum. for _, roleNameToPermute := range append(data.BaseRoles, "targets/other") { builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) if roleNameToPermute == data.CanonicalSnapshotRole { continue } // load all the roles normally, except for roleToPermute, which has one space added // to the end, thus changing the checksum for _, roleNameToLoad := range append(data.BaseRoles, "targets/other") { switch roleNameToLoad { case data.CanonicalSnapshotRole: continue // we load this later case roleNameToPermute: // having a space added at the end should not affect any validity check except checksum require.NoError(t, builder.Load(roleNameToLoad, append(meta[roleNameToLoad], ' '), 0, false)) default: require.NoError(t, builder.Load(roleNameToLoad, meta[roleNameToLoad], 1, false)) } require.True(t, builder.IsLoaded(roleNameToLoad)) } // now load the snapshot - it should fail with the checksum failure for the permuted role err := builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false) switch roleNameToPermute { case data.CanonicalTimestampRole: require.NoError(t, err) // we don't check the timestamp's checksum default: require.Error(t, err) checksumErr, ok := err.(data.ErrMismatchedChecksum) require.True(t, ok) require.Contains(t, checksumErr.Error(), fmt.Sprintf("checksum for %s did not match", roleNameToPermute)) require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } } // load all the metadata as is without alteration (so they should validate all checksums) // but also load the metadata that is not contained in the snapshot. Then when the snapshot // is loaded it will fail validation, because it doesn't have target/other/other's checksum builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) for _, roleNameToLoad := range append(data.BaseRoles, "targets/other", "targets/other/other") { if roleNameToLoad == data.CanonicalSnapshotRole { continue } require.NoError(t, builder.Load(roleNameToLoad, meta[roleNameToLoad], 1, false)) } err := builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false) require.Error(t, err) require.IsType(t, data.ErrMissingMeta{}, err) }