From 4f8d28ad7f2f4d3b36db3be90dff02a2bc59642d Mon Sep 17 00:00:00 2001 From: Ying Li Date: Thu, 7 Jan 2016 16:57:44 -0800 Subject: [PATCH] Add tests for updating replacing corrupted local cache Signed-off-by: Ying Li --- client/client_update_test.go | 246 +++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/client/client_update_test.go b/client/client_update_test.go index e7a2e6e55a..213a170b5f 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -1,12 +1,23 @@ package client import ( + "os" + "path" + "testing" + "time" + "fmt" "io/ioutil" "net/http" "os" "testing" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/store" + json "github.com/jfrazelle/go/canonical/json" + "github.com/stretchr/testify/require" + "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/store" "github.com/stretchr/testify/require" @@ -95,3 +106,238 @@ func testUpdateWithLocalCacheRemoteMissingMetadata(t *testing.T, forWrite bool) require.True(t, ok) require.Equal(t, data.CanonicalTimestampRole, metaNotFound.Resource) } + +type messUpMetadata func(t *testing.T, cs signed.CryptoService, ms store.MetadataStore, role string) + +// corrupts metadata into something that is no longer valid JSON +func invalidJSONMetadata(t *testing.T, _ signed.CryptoService, ms store.MetadataStore, role string) { + require.NoError(t, ms.SetMeta(role, []byte("nope"))) +} + +// corrupts the metadata into something that is valid JSON, but not unmarshalable at all +func allUnmarshallableMetadata(t *testing.T, _ signed.CryptoService, ms store.MetadataStore, role string) { + metaBytes, err := json.MarshalCanonical(data.Signed{}) + require.NoError(t, err) + require.NoError(t, ms.SetMeta(role, metaBytes)) +} + +// messes up the metadata in such a way that the hash is no longer valid +func invalidateMetadataHash(t *testing.T, _ signed.CryptoService, ms store.MetadataStore, role string) { + b, err := ms.GetMeta(role, maxSize) + require.NoError(t, err) + + var unmarshalled map[string]interface{} + require.NoError(t, json.Unmarshal(b, &unmarshalled)) + + signed, ok := unmarshalled["signed"].(map[string]interface{}) + require.True(t, ok) + signed["boogeyman"] = "exists" + + metaBytes, err := json.MarshalCanonical(unmarshalled) + require.NoError(t, err) + + require.NoError(t, ms.SetMeta(role, metaBytes)) +} + +// deletes the metadata +func deleteMetadata(t *testing.T, _ signed.CryptoService, ms store.MetadataStore, role string) { + require.NoError(t, ms.DeleteMeta(role)) +} + +func serializeMetadata(t *testing.T, s *data.Signed, cs signed.CryptoService, role string) []byte { + // delete the existing signatures + s.Signatures = []data.Signature{} + + pubKeys := cs.ListKeys(role) + require.Len(t, pubKeys, 1, "no keys for %s", role) + pubKey := cs.GetKey(pubKeys[0]) + require.NotNil(t, pubKey, "unable to get %s key %s", role, pubKeys[0]) + + require.NoError(t, signed.Sign(cs, s, pubKey)) + + metaBytes, err := json.MarshalCanonical(s) + require.NoError(t, err) + + return metaBytes +} + +// signs the metadata with the wrong key +func invalidateMetadataSig(t *testing.T, _ signed.CryptoService, ms store.MetadataStore, role string) { + b, err := ms.GetMeta(role, maxSize) + require.NoError(t, err) + + signedThing := data.Signed{} + require.NoError(t, json.Unmarshal(b, &signedThing), "error unmarshalling data for %s", role) + + // create an invalid key, but not in the existing CryptoService + cs := signed.NewEd25519() + _, err = cs.Create("root", data.ED25519Key) + require.NoError(t, err) + + metaBytes := serializeMetadata(t, &signedThing, cs, "root") + require.NoError(t, ms.SetMeta(role, metaBytes)) +} + +func signedMetaFromStore(t *testing.T, ms store.MetadataStore, role string) data.SignedMeta { + b, err := ms.GetMeta(role, maxSize) + require.NoError(t, err) + + signedMeta := data.SignedMeta{} + require.NoError(t, json.Unmarshal(b, &signedMeta), "error unmarshalling data for %s", role) + + return signedMeta +} + +func signedMetaToSigned(t *testing.T, signedMeta data.SignedMeta) data.Signed { + s, err := json.MarshalCanonical(signedMeta.Signed) + require.NoError(t, err) + signed := json.RawMessage{} + require.NoError(t, signed.UnmarshalJSON(s)) + + return data.Signed{Signed: signed} +} + +// corrupt the metadata in such a way that it is JSON parsable, and correctly signed, but will not +// unmarshal correctly because it has the wrong type +func corruptSignedMetadata(t *testing.T, cs signed.CryptoService, ms store.MetadataStore, role string) { + if role != data.CanonicalTimestampRole || len(cs.ListKeys(role)) > 0 { + signedMeta := signedMetaFromStore(t, ms, role) + signedMeta.Signed.Type = "nonexistent" + signedThing := signedMetaToSigned(t, signedMeta) + metaBytes := serializeMetadata(t, &signedThing, cs, role) + require.NoError(t, ms.SetMeta(role, metaBytes)) + } +} + +// decrements the metadata version, which would make it invalid - don't do anything if we don't +// have the timestamp key +func decrementMetadataVersion(t *testing.T, cs signed.CryptoService, ms store.MetadataStore, role string) { + if role != data.CanonicalTimestampRole || len(cs.ListKeys(role)) > 0 { + signedMeta := signedMetaFromStore(t, ms, role) + signedMeta.Signed.Version-- + signedThing := signedMetaToSigned(t, signedMeta) + metaBytes := serializeMetadata(t, &signedThing, cs, role) + require.NoError(t, ms.SetMeta(role, metaBytes)) + } +} + +// expire the metadata, which would make it invalid - don't do anything if we don't have the +// timestamp key +func expireMetadata(t *testing.T, cs signed.CryptoService, ms store.MetadataStore, role string) { + if role != data.CanonicalTimestampRole || len(cs.ListKeys(role)) > 0 { + signedMeta := signedMetaFromStore(t, ms, role) + signedMeta.Signed.Expires = time.Now().AddDate(-1, -1, -1) + signedThing := signedMetaToSigned(t, signedMeta) + metaBytes := serializeMetadata(t, &signedThing, cs, role) + require.NoError(t, ms.SetMeta(role, metaBytes)) + } +} + +// increments a threshold for a metadata role - invalidates the metadata for which the threshold +// is increased, since there is only 1 signature for each +func incrementThreshold(t *testing.T, cs signed.CryptoService, ms store.MetadataStore, role string) { + roleSpecifyingThreshold := data.CanonicalRootRole + if data.IsDelegation(role) { + roleSpecifyingThreshold = path.Dir(role) + } + + b, err := ms.GetMeta(roleSpecifyingThreshold, maxSize) + require.NoError(t, err) + + signedThing := &data.Signed{} + require.NoError(t, json.Unmarshal(b, signedThing), "error unmarshalling data for %s", + roleSpecifyingThreshold) + + if roleSpecifyingThreshold == data.CanonicalRootRole { + signedRoot, err := data.RootFromSigned(signedThing) + require.NoError(t, err) + signedRoot.Signed.Roles[role].Threshold++ + signedThing, err = signedRoot.ToSigned() + require.NoError(t, err) + } else { + signedTargets, err := data.TargetsFromSigned(signedThing) + require.NoError(t, err) + for _, roleObject := range signedTargets.Signed.Delegations.Roles { + if roleObject.Name == role { + roleObject.Threshold++ + break + } + } + signedThing, err = signedTargets.ToSigned() + require.NoError(t, err) + } + + metaBytes := serializeMetadata(t, signedThing, cs, roleSpecifyingThreshold) + require.NoError(t, ms.SetMeta(roleSpecifyingThreshold, metaBytes)) +} + +// If a repo has corrupt metadata, an update will replace all the metadata +func TestUpdateReplacesCorruptOrMissingMetadata(t *testing.T) { + // create repo with 2 level delegations + ts := fullTestServer(t) + defer ts.Close() + + repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) + defer os.RemoveAll(repo.baseDir) + + delegatedRoles := []string{"targets/a", "targets/a/b"} + for _, delgName := range delegatedRoles { + delgKey, err := repo.CryptoService.Create(delgName, data.ECDSAKey) + require.NoError(t, err, "error creating delegation key") + + require.NoError(t, + repo.AddDelegation(delgName, 1, []data.PublicKey{delgKey}, []string{""}), + "error creating delegation") + } + // add a target so the second level delegation is created + addTarget(t, repo, "first", "../fixtures/root-ca.crt", "targets/a/b") + require.NoError(t, repo.Publish()) + _, err := repo.Update() // ensure we have all metadata to start with + require.NoError(t, err) + + // corrupt any number of roles - an update should fix all of them + roles := []string{ + data.CanonicalTimestampRole, + data.CanonicalSnapshotRole, + "targets/a", + "targets/a/b", + data.CanonicalTargetsRole, + data.CanonicalRootRole, + } + + // store original metadata + origMeta := make(map[string][]byte) + for _, role := range roles { + b, err := repo.fileStore.GetMeta(role, maxSize) + require.NoError(t, err) + require.NotNil(t, b) + origMeta[role] = b + } + + // mess up metadata in different ways, update the repo, and assert that the metadata is fixed. + waysToMessUp := map[string]messUpMetadata{ + "corrupted/invalid JSON": invalidJSONMetadata, + "metadata has invalid hash": invalidateMetadataHash, + "missing metadata": deleteMetadata, + "metadata signed by wrong key": invalidateMetadataSig, + "expired metadata": expireMetadata, + "insufficient signatures": incrementThreshold, + // decremented version just tests that updates do not need to increment + // by 1, only increment at all + "version much lower": decrementMetadataVersion, + } + for i := range roles { + for text, messItUp := range waysToMessUp { + for _, role := range roles[:i+1] { + messItUp(t, repo.CryptoService, repo.fileStore, role) + } + _, err := repo.Update() + require.NoError(t, err) + for role, origBytes := range origMeta { + b, err := repo.fileStore.GetMeta(role, maxSize) + require.NoError(t, err, "problem getting metadata for %s", role) + require.Equal(t, origBytes, b, "%s for %s expected to recover after update", text, role) + } + } + } +}