mirror of https://github.com/docker/docs.git
442 lines
16 KiB
Go
442 lines
16 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/docker/notary/passphrase"
|
|
"github.com/docker/notary/tuf/data"
|
|
"github.com/docker/notary/tuf/store"
|
|
"github.com/docker/notary/tuf/testutils"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newBlankRepo(t *testing.T, url string) *NotaryRepository {
|
|
// Temporary directory where test files will be created
|
|
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
|
require.NoError(t, err, "failed to create a temporary directory: %s", err)
|
|
|
|
repo, err := NewNotaryRepository(tempBaseDir, "docker.com/notary", url,
|
|
http.DefaultTransport, passphrase.ConstantRetriever("pass"))
|
|
require.NoError(t, err)
|
|
return repo
|
|
}
|
|
|
|
// If there's no local cache, we go immediately to check the remote server for
|
|
// root, and if it doesn't exist, we return ErrRepositoryNotExist. This happens
|
|
// with or without a force check (update for write).
|
|
func TestUpdateNotExistNoLocalCache(t *testing.T) {
|
|
testUpdateNotExistNoLocalCache(t, false)
|
|
testUpdateNotExistNoLocalCache(t, true)
|
|
}
|
|
|
|
func testUpdateNotExistNoLocalCache(t *testing.T, forWrite bool) {
|
|
ts, _, _ := simpleTestServer(t)
|
|
defer ts.Close()
|
|
|
|
repo := newBlankRepo(t, ts.URL)
|
|
defer os.RemoveAll(repo.baseDir)
|
|
|
|
// there is no metadata at all - this is a fresh repo, and the server isn't
|
|
// aware of the root.
|
|
_, err := repo.Update(forWrite)
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrRepositoryNotExist{}, err)
|
|
}
|
|
|
|
// If there is a local cache, we use the local root as the trust anchor and we
|
|
// then an update. If the server has no root.json, we return an ErrRepositoryNotExist.
|
|
// If we force check (update for write), then it hits the server first, and
|
|
// still returns an ErrRepositoryNotExist.
|
|
func TestUpdateNotExistWithLocalCache(t *testing.T) {
|
|
testUpdateNotExistWithLocalCache(t, false)
|
|
testUpdateNotExistWithLocalCache(t, true)
|
|
}
|
|
|
|
func testUpdateNotExistWithLocalCache(t *testing.T, forWrite bool) {
|
|
ts, _, _ := simpleTestServer(t)
|
|
defer ts.Close()
|
|
|
|
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
|
|
defer os.RemoveAll(repo.baseDir)
|
|
|
|
// the repo has metadata, but the server is unaware of any metadata
|
|
// whatsoever.
|
|
_, err := repo.Update(forWrite)
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrRepositoryNotExist{}, err)
|
|
}
|
|
|
|
// If there is a local cache, we use the local root as the trust anchor and we
|
|
// then an update. If the server has a root.json, but is missing other data,
|
|
// then we propagate the ErrMetaNotFound. Same if we force check
|
|
// (update for write); the root exists, but other metadata doesn't.
|
|
func TestUpdateWithLocalCacheRemoteMissingMetadata(t *testing.T) {
|
|
testUpdateWithLocalCacheRemoteMissingMetadata(t, false)
|
|
testUpdateWithLocalCacheRemoteMissingMetadata(t, true)
|
|
}
|
|
|
|
func testUpdateWithLocalCacheRemoteMissingMetadata(t *testing.T, forWrite bool) {
|
|
ts, mux, _ := simpleTestServer(t)
|
|
defer ts.Close()
|
|
|
|
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
|
|
defer os.RemoveAll(repo.baseDir)
|
|
|
|
rootJSON, err := repo.fileStore.GetMeta(data.CanonicalRootRole, maxSize)
|
|
require.NoError(t, err)
|
|
|
|
// the server should know about the root.json, and nothing else
|
|
mux.HandleFunc(
|
|
fmt.Sprintf("/v2/docker.com/notary/_trust/tuf/root.json"),
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprint(w, string(rootJSON))
|
|
})
|
|
|
|
// the first thing the client tries to get is the timestamp - so that
|
|
// will be the failed metadata update.
|
|
_, err = repo.Update(forWrite)
|
|
require.Error(t, err)
|
|
require.IsType(t, store.ErrMetaNotFound{}, err)
|
|
metaNotFound, ok := err.(store.ErrMetaNotFound)
|
|
require.True(t, ok)
|
|
require.Equal(t, data.CanonicalTimestampRole, metaNotFound.Resource)
|
|
}
|
|
|
|
// create a server that just serves static metadata files from a metaStore
|
|
func readOnlyServer(t *testing.T, cache store.MetadataStore, roles []string) *httptest.Server {
|
|
mux := http.NewServeMux()
|
|
|
|
// serve all the metadata files from the metadata cache
|
|
for _, roleName := range roles {
|
|
localRoleName := roleName
|
|
mux.HandleFunc(
|
|
fmt.Sprintf("/v2/docker.com/notary/_trust/tuf/%s.json", localRoleName),
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
metaBytes, err := cache.GetMeta(localRoleName, maxSize)
|
|
require.NoError(t, err)
|
|
w.Write(metaBytes)
|
|
})
|
|
|
|
}
|
|
|
|
return httptest.NewServer(mux)
|
|
}
|
|
|
|
type unwritableStore struct {
|
|
store.MetadataStore
|
|
roleToNotWrite string
|
|
}
|
|
|
|
func (u *unwritableStore) SetMeta(role string, meta []byte) error {
|
|
if role == u.roleToNotWrite {
|
|
return fmt.Errorf("Non-writable")
|
|
}
|
|
return u.MetadataStore.SetMeta(role, meta)
|
|
}
|
|
|
|
// Update can succeed even if we cannot write any metadata to the repo (assuming
|
|
// no data in the repo)
|
|
func TestUpdateSucceedsEvenIfCannotWriteNewRepo(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping test in short mode")
|
|
}
|
|
s, err := testutils.NewMetadataSwizzler("docker.com/notary")
|
|
require.NoError(t, err)
|
|
|
|
ts := readOnlyServer(t, s.MetadataCache, s.Roles)
|
|
defer ts.Close()
|
|
|
|
for _, role := range s.Roles {
|
|
repo := newBlankRepo(t, ts.URL)
|
|
repo.fileStore = &unwritableStore{MetadataStore: repo.fileStore, roleToNotWrite: role}
|
|
_, err := repo.Update(false)
|
|
|
|
if role == data.CanonicalRootRole {
|
|
require.Error(t, err) // because checkRoot loads root from cache to check hashes
|
|
continue
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
for _, r := range s.Roles {
|
|
expected, err := s.MetadataCache.GetMeta(r, maxSize)
|
|
require.NoError(t, err, "problem getting expected metadata for %s", r)
|
|
actual, err := repo.fileStore.GetMeta(r, maxSize)
|
|
if r == role {
|
|
require.Error(t, err)
|
|
require.IsType(t, store.ErrMetaNotFound{}, err,
|
|
"expected no data because unable to write for %s", role)
|
|
} else {
|
|
require.NoError(t, err, "problem getting repo metadata for %s", r)
|
|
require.True(t, bytes.Equal(expected, actual),
|
|
"%s: expected to update since only %s was unwritable", r, role)
|
|
}
|
|
}
|
|
|
|
os.RemoveAll(repo.baseDir)
|
|
}
|
|
}
|
|
|
|
// Update can succeed even if we cannot write any metadata to the repo (assuming
|
|
// existing data in the repo)
|
|
func TestUpdateSucceedsEvenIfCannotWriteExistingRepo(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping test in short mode")
|
|
}
|
|
s, err := testutils.NewMetadataSwizzler("docker.com/notary")
|
|
require.NoError(t, err)
|
|
|
|
ts := readOnlyServer(t, s.MetadataCache, s.Roles)
|
|
defer ts.Close()
|
|
|
|
// download existing metadata
|
|
repo := newBlankRepo(t, ts.URL)
|
|
defer os.RemoveAll(repo.baseDir)
|
|
_, err = repo.Update(false)
|
|
require.NoError(t, err)
|
|
|
|
origFileStore := repo.fileStore
|
|
for _, role := range s.Roles {
|
|
for _, forWrite := range []bool{true, false} {
|
|
// bump versions of everything on the server, to force everything to update
|
|
for _, r := range s.Roles {
|
|
require.NoError(t, s.OffsetMetadataVersion(r, 1))
|
|
}
|
|
require.NoError(t, s.UpdateSnapshotHashes())
|
|
require.NoError(t, s.UpdateTimestampHash())
|
|
|
|
// update fileStore
|
|
repo.fileStore = &unwritableStore{MetadataStore: origFileStore, roleToNotWrite: role}
|
|
_, err := repo.Update(forWrite)
|
|
|
|
if role == data.CanonicalRootRole {
|
|
require.Error(t, err) // because checkRoot loads root from cache to check hashes
|
|
continue
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
for _, r := range s.Roles {
|
|
expected, err := s.MetadataCache.GetMeta(r, maxSize)
|
|
require.NoError(t, err, "problem getting expected metadata for %s", r)
|
|
actual, err := repo.fileStore.GetMeta(r, maxSize)
|
|
require.NoError(t, err, "problem getting repo metadata for %s", r)
|
|
if role == r {
|
|
require.False(t, bytes.Equal(expected, actual),
|
|
"%s: expected to not update because %s was unwritable", r, role)
|
|
} else {
|
|
require.True(t, bytes.Equal(expected, actual),
|
|
"%s: expected to update since only %s was unwritable", r, role)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type messUpMetadata func(role string) error
|
|
|
|
// If a repo has corrupt metadata (in that the hash doesn't match the snapshot) or
|
|
// missing metadata, an update will replace all of it
|
|
func TestUpdateReplacesCorruptOrMissingMetadata(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping test in short mode")
|
|
}
|
|
s, err := testutils.NewMetadataSwizzler("docker.com/notary")
|
|
require.NoError(t, err)
|
|
|
|
ts := readOnlyServer(t, s.MetadataCache, s.Roles)
|
|
defer ts.Close()
|
|
|
|
repo := newBlankRepo(t, ts.URL)
|
|
defer os.RemoveAll(repo.baseDir)
|
|
|
|
_, err = repo.Update(false) // ensure we have all metadata to start with
|
|
require.NoError(t, err)
|
|
|
|
// we want to swizzle the local cache, not the server, so create a new one
|
|
swizzler := testutils.MetadataSwizzler{
|
|
MetadataCache: repo.fileStore,
|
|
CryptoService: repo.CryptoService,
|
|
Roles: s.Roles,
|
|
}
|
|
|
|
waysToMessUp := map[string]messUpMetadata{
|
|
"invalid JSON": swizzler.SetInvalidJSON,
|
|
"missing metadata": swizzler.RemoveMetadata,
|
|
}
|
|
for _, role := range s.Roles {
|
|
for text, messItUp := range waysToMessUp {
|
|
for _, forWrite := range []bool{true, false} {
|
|
require.NoError(t, messItUp(role), "could not fuzz %s (%s)", role, text)
|
|
_, err := repo.Update(forWrite)
|
|
require.NoError(t, err)
|
|
for _, role := range s.Roles {
|
|
expected, err := s.MetadataCache.GetMeta(role, maxSize)
|
|
require.NoError(t, err, "problem getting expected metadata for %s", role)
|
|
actual, err := repo.fileStore.GetMeta(role, maxSize)
|
|
require.NoError(t, err, "problem getting repo metadata for %s", role)
|
|
require.True(t, bytes.Equal(expected, actual),
|
|
"%s for %s: expected to recover after update", text, role)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a repo has an invalid root (signed by wrong key, expired, invalid version, etc.),
|
|
// the repo will just get the new root from the server, whether or not the update
|
|
// is for writing (forced update)
|
|
func TestUpdateWhenLocalRootRecoverablyCorrupt(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping test in short mode")
|
|
}
|
|
s, err := testutils.NewMetadataSwizzler("docker.com/notary")
|
|
require.NoError(t, err)
|
|
|
|
ts := readOnlyServer(t, s.MetadataCache, s.Roles)
|
|
defer ts.Close()
|
|
|
|
repo := newBlankRepo(t, ts.URL)
|
|
defer os.RemoveAll(repo.baseDir)
|
|
|
|
_, err = repo.Update(false) // ensure we have all metadata to start with
|
|
require.NoError(t, err)
|
|
|
|
// we want to swizzle the local cache, not the server, so create a new one
|
|
swizzler := testutils.MetadataSwizzler{
|
|
MetadataCache: repo.fileStore,
|
|
CryptoService: s.CryptoService,
|
|
Roles: s.Roles,
|
|
}
|
|
|
|
waysToMessUp := map[string]messUpMetadata{
|
|
// TODO: If invalid threshold fails because it's an invalid role, then this
|
|
// should also fail because it's an invalid role (if the metadata type is wrong)
|
|
"wrong metadata type": swizzler.SetInvalidMetadataType,
|
|
"signed with right key but wrong hash": swizzler.InvalidateMetadataSignatures,
|
|
"signed with wrong key": swizzler.SignMetadataWithInvalidKey,
|
|
"expired": swizzler.ExpireMetadata,
|
|
|
|
"negative version": func(r string) error { return swizzler.OffsetMetadataVersion(r, -100) },
|
|
// // TODO: This fails at bootstrapClient, when we do SetRoot - then the only way to recover
|
|
// // from this type of local data corruption/change is to probably wipe out the local cache.
|
|
// // Is that ok? It seems like this is a client corruption where we can just redownload
|
|
// "invalid threshold": func(r string) error { return swizzler.SetThreshold(r, 0) },
|
|
// // TODO: this makes more sense, but the on-disk root itself does not have sufficient sigs.
|
|
// // Is it ok to use it as the trust anchor?
|
|
// "insufficient signatures": func(r string) error { return swizzler.SetThreshold(r, 5) },
|
|
}
|
|
|
|
role := data.CanonicalRootRole
|
|
for text, messItUp := range waysToMessUp {
|
|
for _, forWrite := range []bool{true, false} {
|
|
repo.baseURL = ts.URL
|
|
require.NoError(t, messItUp(role), "could not fuzz %s (%s)", role, text)
|
|
_, err := repo.Update(forWrite)
|
|
require.NoError(t, err, "unable to update after locally fuzzing: %s", text)
|
|
|
|
for _, role := range s.Roles {
|
|
expected, err := s.MetadataCache.GetMeta(role, maxSize)
|
|
require.NoError(t, err, "problem getting expected metadata for %s", role)
|
|
actual, err := repo.fileStore.GetMeta(role, maxSize)
|
|
require.NoError(t, err, "problem getting repo metadata for %s", role)
|
|
require.True(t, bytes.Equal(expected, actual),
|
|
"%s for %s: expected to recover after update", text, role)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a repo has an invalid root (signed by wrong key, expired, invalid version,
|
|
// invalid number of signatures, etc.), the repo will just get the new root from
|
|
// the server, whether or not the update is for writing (forced update), but
|
|
// it will refuse to update if the root key has changed and the new root is
|
|
// not signed by the old and new key
|
|
func TestUpdateFailsIfServerRootKeyChangedWithoutMultiSign(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping test in short mode")
|
|
}
|
|
serverSwizzler, err := testutils.NewMetadataSwizzler("docker.com/notary")
|
|
require.NoError(t, err)
|
|
|
|
ts := readOnlyServer(t, serverSwizzler.MetadataCache, serverSwizzler.Roles)
|
|
defer ts.Close()
|
|
|
|
origMeta := make(map[string][]byte)
|
|
for _, role := range serverSwizzler.Roles {
|
|
origMetadata, err := serverSwizzler.MetadataCache.GetMeta(role, maxSize)
|
|
require.NoError(t, err)
|
|
origMeta[role] = origMetadata
|
|
}
|
|
|
|
repo := newBlankRepo(t, ts.URL)
|
|
defer os.RemoveAll(repo.baseDir)
|
|
|
|
_, err = repo.Update(false) // ensure we have all metadata to start with
|
|
require.NoError(t, err)
|
|
ts.Close()
|
|
|
|
// rotate the server's root.json root key so that they no longer match trust anchors
|
|
require.NoError(t, serverSwizzler.ChangeRootKey())
|
|
// bump versions, update snapshot and timestamp too so it will not fail on a hash
|
|
require.NoError(t, serverSwizzler.OffsetMetadataVersion(data.CanonicalRootRole, 1))
|
|
require.NoError(t, serverSwizzler.OffsetMetadataVersion(data.CanonicalSnapshotRole, 1))
|
|
require.NoError(t, serverSwizzler.OffsetMetadataVersion(data.CanonicalTimestampRole, 1))
|
|
require.NoError(t, serverSwizzler.UpdateSnapshotHashes(data.CanonicalRootRole))
|
|
require.NoError(t, serverSwizzler.UpdateTimestampHash())
|
|
|
|
// we want to swizzle the local cache, not the server, so create a new one
|
|
swizzler := testutils.MetadataSwizzler{
|
|
MetadataCache: repo.fileStore,
|
|
CryptoService: serverSwizzler.CryptoService,
|
|
Roles: serverSwizzler.Roles,
|
|
}
|
|
|
|
waysToMessUp := map[string]messUpMetadata{
|
|
"wrong metadata type": swizzler.SetInvalidMetadataType,
|
|
"signed with right key but wrong hash": swizzler.InvalidateMetadataSignatures,
|
|
"signed with wrong key": swizzler.SignMetadataWithInvalidKey,
|
|
"expired": swizzler.ExpireMetadata,
|
|
|
|
"negative version": func(r string) error { return swizzler.OffsetMetadataVersion(r, -100) },
|
|
"invalid threshold": func(r string) error { return swizzler.SetThreshold(r, 0) },
|
|
"insufficient signatures": func(r string) error { return swizzler.SetThreshold(r, 5) },
|
|
}
|
|
|
|
role := data.CanonicalRootRole
|
|
for text, messItUp := range waysToMessUp {
|
|
for _, forWrite := range []bool{true, false} {
|
|
require.NoError(t, messItUp(role), "could not fuzz %s (%s)", role, text)
|
|
messedUpMeta, err := repo.fileStore.GetMeta(data.CanonicalRootRole, maxSize)
|
|
require.NoError(t, err)
|
|
|
|
_, err = repo.Update(forWrite)
|
|
require.Error(t, err) // the new server has a different root, update fails
|
|
|
|
// we can't test that all the metadata is the same, because we probably would
|
|
// have downloaded a new timestamp and maybe snapshot. But the root should be the
|
|
// same because it has failed to update.
|
|
for role, expected := range origMeta {
|
|
if role != data.CanonicalTimestampRole && role != data.CanonicalSnapshotRole {
|
|
actual, err := repo.fileStore.GetMeta(role, maxSize)
|
|
require.NoError(t, err, "problem getting repo metadata for %s", role)
|
|
|
|
if role == data.CanonicalRootRole {
|
|
expected = messedUpMeta
|
|
}
|
|
require.True(t, bytes.Equal(expected, actual),
|
|
"%s for %s: expected to not have updated", text, role)
|
|
}
|
|
}
|
|
// revert our original root metadata
|
|
require.NoError(t,
|
|
repo.fileStore.SetMeta(data.CanonicalRootRole, origMeta[data.CanonicalRootRole]))
|
|
}
|
|
}
|
|
}
|