From 5d1b4bc6ec28b343bbbe42df67fd7be7315d7194 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Thu, 21 Apr 2016 10:56:12 -0700 Subject: [PATCH] Add ConsistentInfo and snapshot/timestamp retroactive checksum tests for the builder. Signed-off-by: Ying Li --- tuf/builder.go | 5 +- tuf/builder_test.go | 331 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 334 insertions(+), 2 deletions(-) diff --git a/tuf/builder.go b/tuf/builder.go index 3230ddbb2f..111223a276 100644 --- a/tuf/builder.go +++ b/tuf/builder.go @@ -199,8 +199,11 @@ func (rb *repoBuilder) GetConsistentInfo(roleName string) ConsistentInfo { info.fileMeta = rb.repo.Timestamp.Signed.Meta[roleName] } case data.CanonicalRootRole: - if rb.bootstrappedRootChecksum != nil { + switch { + case rb.bootstrappedRootChecksum != nil: info.fileMeta = *rb.bootstrappedRootChecksum + case rb.repo.Snapshot != nil: + info.fileMeta = rb.repo.Snapshot.Signed.Meta[roleName] } default: if rb.repo.Snapshot != nil { diff --git a/tuf/builder_test.go b/tuf/builder_test.go index 8186630fb4..01bfe623fb 100644 --- a/tuf/builder_test.go +++ b/tuf/builder_test.go @@ -1,15 +1,22 @@ package tuf_test -// package tuf_test to avoid an import cycle since we are using testutils.EmptyRepo +// 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" ) @@ -344,3 +351,325 @@ func TestGenerateTimestampInvalidOperations(t *testing.T) { 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(nil, 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], 0, 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], 0, 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], 0, 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(nil, gun, nil, trustpinning.TrustPinConfig{}) + require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, 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], 0, false)) + // loading the snapshot in fails, because of the checksum the timestamp has + err = builder.Load(data.CanonicalSnapshotRole, snapJSON, 0, 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], 0, false)) + } + + // --- load snapshot first + builder = tuf.NewRepoBuilder(nil, 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, 0, false)) + default: + require.NoError(t, builder.Load(roleName, meta[roleName], 0, false)) + } + } + // timestamp fails because the snapshot checksum is wrong + err = builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 0, 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(nil, 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)) + + // 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], 0, 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], 0, false)) + } + // loading the non-existent role wil fail + err := builder.Load("targets/other/other", meta["targets/other/other"], 0, 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(nil, 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], 0, 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], 0, 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(nil, 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], 0, false)) + } + err := builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 0, false) + require.Error(t, err) + require.IsType(t, data.ErrMissingMeta{}, err) +}