diff --git a/client/client_update_test.go b/client/client_update_test.go index 82972d3d4c..f4196a5cc6 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -811,6 +811,36 @@ var waysToMessUpServer = []swizzleExpectations{ }}, } +var _waysToMessUpServerRoot []swizzleExpectations + +// We also want to remove a every role from root once, or remove the role's keys. +// This function generates once and caches the result for later re-use. +func waysToMessUpServerRoot() []swizzleExpectations { + if _waysToMessUpServerRoot == nil { + _waysToMessUpServerRoot = waysToMessUpServer + for _, roleName := range data.BaseRoles { + _waysToMessUpServerRoot = append(_waysToMessUpServerRoot, + swizzleExpectations{ + desc: fmt.Sprintf("no %s keys", roleName), + expectErrs: []interface{}{ + &certs.ErrValidationFail{}, signed.ErrRoleThreshold{}}, + swizzle: func(s *testutils.MetadataSwizzler, role string) error { + return s.MutateRoot(func(r *data.Root) { + r.Roles[roleName].KeyIDs = []string{} + }) + }}, + swizzleExpectations{ + desc: fmt.Sprintf("no %s role", roleName), + expectErrs: []interface{}{data.ErrInvalidRole{}}, + swizzle: func(s *testutils.MetadataSwizzler, role string) error { + return s.MutateRoot(func(r *data.Root) { delete(r.Roles, roleName) }) + }}, + ) + } + } + return _waysToMessUpServerRoot +} + // If there's no local cache, we go immediately to check the remote server for // root, and if it invalid (corrupted), we cannot update. This happens // with and without a force check (update for write). @@ -818,7 +848,8 @@ func TestUpdateRootRemoteCorruptedNoLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } - for _, testData := range waysToMessUpServer { + + for _, testData := range waysToMessUpServerRoot() { if testData.desc == "insufficient signatures" { // Currently if we download the root during the bootstrap phase, // we don't check for enough signatures to meet the threshold. We @@ -842,7 +873,11 @@ func TestUpdateRootRemoteCorruptedNoLocalCache(t *testing.T) { // because the fact that the timestamp hasn't changed should mean that we don't // have to re-download the root. func TestUpdateRootRemoteCorruptedCanUseLocalCache(t *testing.T) { - for _, testData := range waysToMessUpServer { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + for _, testData := range waysToMessUpServerRoot() { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, forWrite: false, @@ -862,7 +897,7 @@ func TestUpdateRootRemoteCorruptedCannotUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } - for _, testData := range waysToMessUpServer { + for _, testData := range waysToMessUpServerRoot() { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, @@ -878,6 +913,69 @@ func TestUpdateRootRemoteCorruptedCannotUseLocalCache(t *testing.T) { } } +func waysToMessUpServerNonRootPerRole(t *testing.T) map[string][]swizzleExpectations { + perRoleSwizzling := make(map[string][]swizzleExpectations) + for _, missing := range []string{data.CanonicalRootRole, data.CanonicalTargetsRole} { + perRoleSwizzling[data.CanonicalSnapshotRole] = append( + perRoleSwizzling[data.CanonicalSnapshotRole], + swizzleExpectations{ + desc: fmt.Sprintf("snapshot missing root meta checksum"), + expectErrs: []interface{}{client.ErrMissingMeta{}}, + swizzle: func(s *testutils.MetadataSwizzler, role string) error { + return s.MutateSnapshot(func(sn *data.Snapshot) { + delete(sn.Meta, missing) + }) + }, + }) + } + perRoleSwizzling[data.CanonicalTargetsRole] = []swizzleExpectations{{ + desc: fmt.Sprintf("target missing delegations data"), + expectErrs: []interface{}{client.ErrChecksumMismatch{}}, + swizzle: func(s *testutils.MetadataSwizzler, role string) error { + return s.MutateTargets(func(tg *data.Targets) { + tg.Delegations.Roles = tg.Delegations.Roles[1:] + }) + }, + }} + perRoleSwizzling[data.CanonicalTimestampRole] = []swizzleExpectations{{ + desc: fmt.Sprintf("timestamp missing snapshot meta checksum"), + expectErrs: []interface{}{client.ErrMissingMeta{}}, + swizzle: func(s *testutils.MetadataSwizzler, role string) error { + return s.MutateTimestamp(func(ts *data.Timestamp) { + delete(ts.Meta, data.CanonicalSnapshotRole) + }) + }, + }} + perRoleSwizzling["targets/a"] = []swizzleExpectations{{ + desc: fmt.Sprintf("delegation has invalid role"), + expectErrs: []interface{}{client.ErrMissingMeta{}}, + swizzle: func(s *testutils.MetadataSwizzler, role string) error { + return s.MutateTargets(func(tg *data.Targets) { + var keyIDs []string + for k := range tg.Delegations.Keys { + keyIDs = append(keyIDs, k) + } + // add the keys from root too + rootMeta, err := s.MetadataCache.GetMeta(data.CanonicalRootRole, -1) + require.NoError(t, err) + + signedRoot := &data.SignedRoot{} + require.NoError(t, json.Unmarshal(rootMeta, signedRoot)) + + for k := range signedRoot.Signed.Keys { + keyIDs = append(keyIDs, k) + } + + // add an invalid role (root) to delegation + tg.Delegations.Roles = append(tg.Delegations.Roles, + &data.Role{RootRole: data.RootRole{KeyIDs: keyIDs, Threshold: 1}, + Name: data.CanonicalRootRole}) + }) + }, + }} + return perRoleSwizzling +} + // If there's no local cache, we just download from the server and if anything // is corrupt, we cannot update. func TestUpdateNonRootRemoteCorruptedNoLocalCache(t *testing.T) { @@ -894,6 +992,32 @@ func TestUpdateNonRootRemoteCorruptedNoLocalCache(t *testing.T) { }, testData, true) } } + for role, expectations := range waysToMessUpServerNonRootPerRole(t) { + for _, testData := range expectations { + switch role { + case data.CanonicalSnapshotRole: + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + role: role, + }, testData, true) + case data.CanonicalTargetsRole: + // if there are no delegation target roles, we're fine, we just don't + // download them + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + role: role, + }, testData, false) + case data.CanonicalTimestampRole: + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + role: role, + }, testData, true) + case "targets/a": + // TODO: Bug where invalid (non-delegation) roles in delegations + // are accepted + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + role: role, + }, testData, false) + } + } + } } // Having a local cache, if the server has the same data (timestamp has not changed), @@ -916,6 +1040,37 @@ func TestUpdateNonRootRemoteCorruptedCanUseLocalCache(t *testing.T) { }, testData, false) } } + for role, expectations := range waysToMessUpServerNonRootPerRole(t) { + for _, testData := range expectations { + + switch role { + case data.CanonicalSnapshotRole: + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + localCache: true, + role: role, + }, testData, false) + case data.CanonicalTargetsRole: + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + localCache: true, + role: role, + }, testData, false) + case data.CanonicalTimestampRole: + // TODO: Bug: this should succeed, because it's an invalid timestamp metadata + // and hence should fall back on the the local cached timestamp. It's not + // succeeding now because the failure occurs on downloading the snapshot, + // not during the validation of the timestamp. + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + localCache: true, + role: role, + }, testData, true) + case "targets/a": + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + localCache: true, + role: role, + }, testData, false) + } + } + } } // Having a local cache, if the server has new same data should fail in all cases @@ -944,6 +1099,41 @@ func TestUpdateNonRootRemoteCorruptedCannotUseLocalCache(t *testing.T) { }, testData, shouldFail) } } + + for role, expectations := range waysToMessUpServerNonRootPerRole(t) { + for _, testData := range expectations { + switch role { + case data.CanonicalSnapshotRole: + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + serverHasNewData: true, + localCache: true, + role: role, + }, testData, true) + case data.CanonicalTargetsRole: + // if there are no delegation target roles, we're fine, we just don't + // download them + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + serverHasNewData: true, + localCache: true, + role: role, + }, testData, false) + case data.CanonicalTimestampRole: + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + serverHasNewData: true, + localCache: true, + role: role, + }, testData, true) + case "targets/a": + // TODO: Bug where invalid (non-delegation) roles in delegations + // are accepted + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + serverHasNewData: true, + localCache: true, + role: role, + }, testData, false) + } + } + } } func testUpdateRemoteCorruptValidChecksum(t *testing.T, opts updateOpts, expt swizzleExpectations, shouldErr bool) { @@ -977,10 +1167,16 @@ func testUpdateRemoteCorruptValidChecksum(t *testing.T, opts updateOpts, expt sw isSignatureSwizzle := expt.desc == "invalid signatures" || expt.desc == "meta signed by wrong key" // just try to do these - if they fail (probably because they've been swizzled), that's fine if opts.role != data.CanonicalSnapshotRole || !isSignatureSwizzle { - serverSwizzler.UpdateSnapshotHashes() + // if we are purposely editing out some snapshot metadata, don't re-generate + if !strings.HasPrefix(expt.desc, "snapshot missing") { + serverSwizzler.UpdateSnapshotHashes() + } } if opts.role != data.CanonicalTimestampRole || !isSignatureSwizzle { - serverSwizzler.UpdateTimestampHash() + // if we are purposely editing out some timestamp metadata, don't re-generate + if !strings.HasPrefix(expt.desc, "timestamp missing") { + serverSwizzler.UpdateTimestampHash() + } } } _, err := repo.Update(opts.forWrite) diff --git a/tuf/client/client.go b/tuf/client/client.go index ca6590ce40..6259ecf83f 100644 --- a/tuf/client/client.go +++ b/tuf/client/client.go @@ -128,8 +128,10 @@ func (c *Client) downloadRoot() error { var size int64 = -1 var expectedSha256 []byte if c.local.Snapshot != nil { - size = c.local.Snapshot.Signed.Meta[role].Length - expectedSha256 = c.local.Snapshot.Signed.Meta[role].Hashes["sha256"] + if prevRootMeta, ok := c.local.Snapshot.Signed.Meta[role]; ok { + size = prevRootMeta.Length + expectedSha256 = prevRootMeta.Hashes["sha256"] + } } // if we're bootstrapping we may not have a cached root, an diff --git a/tuf/testutils/swizzler.go b/tuf/testutils/swizzler.go index 6150bda751..b790b809a4 100644 --- a/tuf/testutils/swizzler.go +++ b/tuf/testutils/swizzler.go @@ -9,6 +9,7 @@ import ( "github.com/docker/notary/cryptoservice" "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/store" @@ -41,7 +42,11 @@ func getPubKeys(cs signed.CryptoService, s *data.Signed, role string) ([]data.Pu if err := json.Unmarshal(s.Signed, root); err != nil { return nil, err } - for _, pubKeyID := range root.Roles[data.CanonicalRootRole].KeyIDs { + rootRole, ok := root.Roles[data.CanonicalRootRole] + if !ok || rootRole == nil { + return nil, tuf.ErrNotLoaded{} + } + for _, pubKeyID := range rootRole.KeyIDs { pubKeys = append(pubKeys, root.Keys[pubKeyID]) } } else { @@ -532,3 +537,137 @@ func (m *MetadataSwizzler) UpdateTimestampHash() error { } return m.MetadataCache.SetMeta(data.CanonicalTimestampRole, metaBytes) } + +// MutateRoot takes a function that mutates the root metadata - once done, it +// serializes the root again +func (m *MetadataSwizzler) MutateRoot(mutate func(*data.Root)) error { + signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalRootRole) + if err != nil { + return err + } + + var root data.Root + if err := json.Unmarshal(signedThing.Signed, &root); err != nil { + return err + } + + // get the original keys, in case the mutation messes with the signing keys + oldPubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalRootRole) + if err != nil { + return err + } + + mutate(&root) + + signedThing, err = data.SignedRoot{Signed: root, Signatures: signedThing.Signatures}.ToSigned() + if err != nil { + return err + } + + pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalRootRole) + if err != nil || len(pubKeys) == 0 { // we have to sign it somehow - might as well use the old keys + pubKeys = oldPubKeys + } + + metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalRootRole, pubKeys...) + if err != nil { + return err + } + return m.MetadataCache.SetMeta(data.CanonicalRootRole, metaBytes) +} + +// MutateTimestamp takes a function that mutates the timestamp metadata - once done, it +// serializes the timestamp again +func (m *MetadataSwizzler) MutateTimestamp(mutate func(*data.Timestamp)) error { + signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalTimestampRole) + if err != nil { + return err + } + + var timestamp data.Timestamp + if err := json.Unmarshal(signedThing.Signed, ×tamp); err != nil { + return err + } + + mutate(×tamp) + + signedThing, err = data.SignedTimestamp{Signed: timestamp, Signatures: signedThing.Signatures}.ToSigned() + if err != nil { + return err + } + + pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalTimestampRole) + if err != nil { + return err + } + + metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalTimestampRole, pubKeys...) + if err != nil { + return err + } + return m.MetadataCache.SetMeta(data.CanonicalTimestampRole, metaBytes) +} + +// MutateSnapshot takes a function that mutates the snapshot metadata - once done, it +// serializes the snapshot again +func (m *MetadataSwizzler) MutateSnapshot(mutate func(*data.Snapshot)) error { + signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalSnapshotRole) + if err != nil { + return err + } + + var snapshot data.Snapshot + if err := json.Unmarshal(signedThing.Signed, &snapshot); err != nil { + return err + } + + mutate(&snapshot) + + signedThing, err = data.SignedSnapshot{Signed: snapshot, Signatures: signedThing.Signatures}.ToSigned() + if err != nil { + return err + } + + pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalSnapshotRole) + if err != nil { + return err + } + + metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalSnapshotRole, pubKeys...) + if err != nil { + return err + } + return m.MetadataCache.SetMeta(data.CanonicalSnapshotRole, metaBytes) +} + +// MutateTargets takes a function that mutates the targets metadata - once done, it +// serializes the targets again +func (m *MetadataSwizzler) MutateTargets(mutate func(*data.Targets)) error { + signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalTargetsRole) + if err != nil { + return err + } + + var targets data.Targets + if err := json.Unmarshal(signedThing.Signed, &targets); err != nil { + return err + } + + mutate(&targets) + + signedThing, err = data.SignedTargets{Signed: targets, Signatures: signedThing.Signatures}.ToSigned() + if err != nil { + return err + } + + pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalTargetsRole) + if err != nil { + return err + } + + metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalTargetsRole, pubKeys...) + if err != nil { + return err + } + return m.MetadataCache.SetMeta(data.CanonicalTargetsRole, metaBytes) +} diff --git a/tuf/testutils/swizzler_test.go b/tuf/testutils/swizzler_test.go index 0e3e22e410..95b94e2765 100644 --- a/tuf/testutils/swizzler_test.go +++ b/tuf/testutils/swizzler_test.go @@ -15,6 +15,7 @@ import ( "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/store" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -577,3 +578,95 @@ func TestMissingSigningKey(t *testing.T) { require.IsType(t, ErrNoKeyForRole{}, f.UpdateSnapshotHashes()) require.IsType(t, ErrNoKeyForRole{}, f.UpdateTimestampHash()) } + +// This mutates the root +func TestSwizzlerMutateRoot(t *testing.T) { + f, origMeta := createNewSwizzler(t) + + assert.NoError(t, f.MutateRoot(func(r *data.Root) { r.Roles["hello"] = nil })) + + for role, metaBytes := range origMeta { + newMeta, err := f.MetadataCache.GetMeta(role, -1) + require.NoError(t, err) + + if role != data.CanonicalRootRole { + require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) + } else { + require.False(t, bytes.Equal(metaBytes, newMeta)) + origSigned, newSigned := &data.SignedRoot{}, &data.SignedRoot{} + require.NoError(t, json.Unmarshal(metaBytes, origSigned)) + require.NoError(t, json.Unmarshal(newMeta, newSigned)) + require.Len(t, origSigned.Signed.Roles, 4) + require.Len(t, newSigned.Signed.Roles, 5) + } + } +} + +// This mutates the timestamp +func TestSwizzlerMutateTimestamp(t *testing.T) { + f, origMeta := createNewSwizzler(t) + + assert.NoError(t, f.MutateTimestamp(func(t *data.Timestamp) { t.Meta["hello"] = data.FileMeta{} })) + + for role, metaBytes := range origMeta { + newMeta, err := f.MetadataCache.GetMeta(role, -1) + require.NoError(t, err) + + if role != data.CanonicalTimestampRole { + require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) + } else { + require.False(t, bytes.Equal(metaBytes, newMeta)) + origSigned, newSigned := &data.SignedTimestamp{}, &data.SignedTimestamp{} + require.NoError(t, json.Unmarshal(metaBytes, origSigned)) + require.NoError(t, json.Unmarshal(newMeta, newSigned)) + require.Len(t, origSigned.Signed.Meta, 1) + require.Len(t, newSigned.Signed.Meta, 2) + } + } +} + +// This mutates the snapshot +func TestSwizzlerMutateSnapshot(t *testing.T) { + f, origMeta := createNewSwizzler(t) + + assert.NoError(t, f.MutateSnapshot(func(s *data.Snapshot) { s.Meta["hello"] = data.FileMeta{} })) + + for role, metaBytes := range origMeta { + newMeta, err := f.MetadataCache.GetMeta(role, -1) + require.NoError(t, err) + + if role != data.CanonicalSnapshotRole { + require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) + } else { + require.False(t, bytes.Equal(metaBytes, newMeta)) + origSigned, newSigned := &data.SignedSnapshot{}, &data.SignedSnapshot{} + require.NoError(t, json.Unmarshal(metaBytes, origSigned)) + require.NoError(t, json.Unmarshal(newMeta, newSigned)) + require.Len(t, origSigned.Signed.Meta, 4) + require.Len(t, newSigned.Signed.Meta, 5) + } + } +} + +// This mutates the targets +func TestSwizzlerMutateTargets(t *testing.T) { + f, origMeta := createNewSwizzler(t) + + assert.NoError(t, f.MutateTargets(func(t *data.Targets) { t.Targets["hello"] = data.FileMeta{} })) + + for role, metaBytes := range origMeta { + newMeta, err := f.MetadataCache.GetMeta(role, -1) + require.NoError(t, err) + + if role != data.CanonicalTargetsRole { + require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) + } else { + require.False(t, bytes.Equal(metaBytes, newMeta)) + origSigned, newSigned := &data.SignedTargets{}, &data.SignedTargets{} + require.NoError(t, json.Unmarshal(metaBytes, origSigned)) + require.NoError(t, json.Unmarshal(newMeta, newSigned)) + require.Len(t, origSigned.Signed.Targets, 0) + require.Len(t, newSigned.Signed.Targets, 1) + } + } +}