package client import ( "bytes" "crypto/rand" regJson "encoding/json" "fmt" "io/ioutil" "math" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "sort" "strings" "testing" "time" "github.com/Sirupsen/logrus" ctxu "github.com/docker/distribution/context" "github.com/docker/notary/certs" "github.com/docker/notary/client/changelist" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/passphrase" "github.com/docker/notary/server" "github.com/docker/notary/server/storage" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/store" "github.com/docker/notary/tuf/validation" "github.com/jfrazelle/go/canonical/json" "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) func simpleTestServer(t *testing.T, roles ...string) ( *httptest.Server, *http.ServeMux, map[string]data.PrivateKey) { if len(roles) == 0 { roles = []string{data.CanonicalTimestampRole, data.CanonicalSnapshotRole} } keys := make(map[string]data.PrivateKey) mux := http.NewServeMux() for _, role := range roles { key, err := trustmanager.GenerateECDSAKey(rand.Reader) assert.NoError(t, err) keys[role] = key pubKey := data.PublicKeyFromPrivate(key) jsonBytes, err := json.MarshalCanonical(&pubKey) assert.NoError(t, err) keyJSON := string(jsonBytes) // TUF will request /v2/docker.com/notary/_trust/tuf/.key mux.HandleFunc( fmt.Sprintf("/v2/docker.com/notary/_trust/tuf/%s.key", role), func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, keyJSON) }) } ts := httptest.NewServer(mux) return ts, mux, keys } func fullTestServer(t *testing.T) *httptest.Server { // Set up server ctx := context.WithValue( context.Background(), "metaStore", storage.NewMemStorage()) // Do not pass one of the const KeyAlgorithms here as the value! Passing a // string is in itself good test that we are handling it correctly as we // will be receiving a string from the configuration. ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa") // Eat the logs instead of spewing them out var b bytes.Buffer l := logrus.New() l.Out = &b ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService( "", trustmanager.NewKeyMemoryStore(passphraseRetriever)) return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService)) } // server that returns some particular error code all the time func errorTestServer(t *testing.T, errorCode int) *httptest.Server { handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(errorCode) } server := httptest.NewServer(http.HandlerFunc(handler)) return server } // initializes a repository in a temporary directory func initializeRepo(t *testing.T, rootType, gun, url string, serverManagesSnapshot bool) (*NotaryRepository, string) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory: %s", err) serverManagedRoles := []string{} if serverManagesSnapshot { serverManagedRoles = []string{data.CanonicalSnapshotRole} } repo, rootPubKeyID := createRepoAndKey(t, rootType, tempBaseDir, gun, url) err = repo.Initialize(rootPubKeyID, serverManagedRoles...) assert.NoError(t, err, "error creating repository: %s", err) if err != nil { os.RemoveAll(tempBaseDir) } return repo, rootPubKeyID } // Creates a new repository and adds a root key. Returns the repo and key ID. func createRepoAndKey(t *testing.T, rootType, tempBaseDir, gun, url string) ( *NotaryRepository, string) { repo, err := NewNotaryRepository( tempBaseDir, gun, url, http.DefaultTransport, passphraseRetriever) assert.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", rootType) assert.NoError(t, err, "error generating root key: %s", err) return repo, rootPubKey.ID() } // creates a new notary repository with the same gun and url as the previous // repo, so that it can be used to pull the trust data the original repo pushed func newRepoToTestRepo(t *testing.T, existingRepo *NotaryRepository) *NotaryRepository { tempBaseDir, err := ioutil.TempDir("", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory") repo, err := NewNotaryRepository( tempBaseDir, existingRepo.gun, existingRepo.baseURL, http.DefaultTransport, passphraseRetriever) assert.NoError(t, err, "error creating repository: %s", err) if err != nil { defer os.RemoveAll(tempBaseDir) } return repo } // Initializing a new repo while specifying that the server should manage the root // role will fail. func TestInitRepositoryManagedRolesIncludingRoot(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) repo, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") err = repo.Initialize(rootPubKeyID, data.CanonicalRootRole) assert.Error(t, err) assert.IsType(t, ErrInvalidRemoteRole{}, err) // Just testing the error message here in this one case assert.Equal(t, err.Error(), "notary does not support the server managing the root key") } // Initializing a new repo while specifying that the server should manage some // invalid role will fail. func TestInitRepositoryManagedRolesInvalidRole(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) repo, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") err = repo.Initialize(rootPubKeyID, "randomrole") assert.Error(t, err) assert.IsType(t, ErrInvalidRemoteRole{}, err) } // Initializing a new repo while specifying that the server should manage the // targets role will fail. func TestInitRepositoryManagedRolesIncludingTargets(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) repo, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") err = repo.Initialize(rootPubKeyID, data.CanonicalTargetsRole) assert.Error(t, err) assert.IsType(t, ErrInvalidRemoteRole{}, err) } // Initializing a new repo while specifying that the server should manage the // timestamp key is fine - that's what it already does, so no error. func TestInitRepositoryManagedRolesIncludingTimestamp(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole) assert.NoError(t, err) } // Initializing a new repo fails if unable to get the timestamp key, even if // the snapshot key is available func TestInitRepositoryNeedsRemoteTimestampKey(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t, data.CanonicalSnapshotRole) defer ts.Close() repo, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole) assert.Error(t, err) assert.IsType(t, store.ErrMetaNotFound{}, err) } // Initializing a new repo with remote server signing fails if unable to get // the snapshot key, even if the timestamp key is available func TestInitRepositoryNeedsRemoteSnapshotKey(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t, data.CanonicalTimestampRole) defer ts.Close() repo, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) err = repo.Initialize(rootPubKeyID, data.CanonicalSnapshotRole) assert.Error(t, err) assert.IsType(t, store.ErrMetaNotFound{}, err) } // passing timestamp + snapshot, or just snapshot, is tested in the next two // test cases. // TestInitRepoServerOnlyManagesTimestampKey runs through the process of // initializing a repository and makes sure the repository looks correct on disk. // We test this with both an RSA and ECDSA root key. // This test case covers the default case where the server only manages the // timestamp key. func TestInitRepoServerOnlyManagesTimestampKey(t *testing.T) { testInitRepo(t, data.ECDSAKey, false) if !testing.Short() { testInitRepo(t, data.RSAKey, false) } } // TestInitRepoServerManagesTimestampAndSnapshotKeys runs through the process of // initializing a repository and makes sure the repository looks correct on disk. // We test this with both an RSA and ECDSA root key. // This test case covers the server managing both the timestap and snapshot keys. func TestInitRepoServerManagesTimestampAndSnapshotKeys(t *testing.T) { testInitRepo(t, data.ECDSAKey, true) if !testing.Short() { testInitRepo(t, data.RSAKey, true) } } // This creates a new KeyFileStore in the repo's base directory and makes sure // the repo has the right number of keys func assertRepoHasExpectedKeys(t *testing.T, repo *NotaryRepository, rootKeyID string, expectedSnapshotKey bool) { // The repo should have a keyFileStore and have created keys using it, // so create a new KeyFileStore, and check that the keys do exist and are // valid ks, err := trustmanager.NewKeyFileStore(repo.baseDir, passphraseRetriever) assert.NoError(t, err) roles := make(map[string]bool) for keyID, role := range ks.ListKeys() { if role == data.CanonicalRootRole { assert.Equal(t, rootKeyID, keyID, "Unexpected root key ID") } // just to ensure the content of the key files created are valid _, r, err := ks.GetKey(keyID) assert.NoError(t, err) assert.Equal(t, role, r) roles[role] = true } // there is a root key and a targets key alwaysThere := []string{data.CanonicalRootRole, data.CanonicalTargetsRole} for _, role := range alwaysThere { _, ok := roles[role] assert.True(t, ok, "missing %s key", role) } // there may be a snapshots key, depending on whether the server is managing // the snapshots key _, ok := roles[data.CanonicalSnapshotRole] if expectedSnapshotKey { assert.True(t, ok, "missing snapshot key") } else { assert.False(t, ok, "there should be no snapshot key because the server manages it") } // The server manages the timestamp key - there should not be a timestamp // key _, ok = roles[data.CanonicalTimestampRole] assert.False(t, ok, "there should be no timestamp key because the server manages it") } // This creates a new certificate manager in the repo's base directory and // makes sure the repo has the right certificates func assertRepoHasExpectedCerts(t *testing.T, repo *NotaryRepository) { // The repo should have a certificate manager and have created certs using // it, so create a new manager, and check that the certs do exist and // are valid certManager, err := certs.NewManager(repo.baseDir) assert.NoError(t, err) certificates := certManager.TrustedCertificateStore().GetCertificates() assert.Len(t, certificates, 1, "unexpected number of trusted certificates") certID, err := trustmanager.FingerprintCert(certificates[0]) assert.NoError(t, err, "unable to fingerprint the trusted certificate") assert.NotEqual(t, certID, "") } // Sanity check the TUF metadata files. Verify that it exists for a particular // role, the JSON is well-formed, and the signatures exist. // For the root.json file, also check that the root, snapshot, and // targets key IDs are present. func assertRepoHasExpectedMetadata(t *testing.T, repo *NotaryRepository, role string, expected bool) { filename := filepath.Join(tufDir, filepath.FromSlash(repo.gun), "metadata", role+".json") fullPath := filepath.Join(repo.baseDir, filename) _, err := os.Stat(fullPath) if expected { assert.NoError(t, err, "missing TUF metadata file: %s", filename) } else { assert.Error(t, err, "%s metadata should not exist, but does: %s", role, filename) return } jsonBytes, err := ioutil.ReadFile(fullPath) assert.NoError(t, err, "error reading TUF metadata file %s: %s", filename, err) var decoded data.Signed err = json.Unmarshal(jsonBytes, &decoded) assert.NoError(t, err, "error parsing TUF metadata file %s: %s", filename, err) assert.Len(t, decoded.Signatures, 1, "incorrect number of signatures in TUF metadata file %s", filename) assert.NotEmpty(t, decoded.Signatures[0].KeyID, "empty key ID field in TUF metadata file %s", filename) assert.NotEmpty(t, decoded.Signatures[0].Method, "empty method field in TUF metadata file %s", filename) assert.NotEmpty(t, decoded.Signatures[0].Signature, "empty signature in TUF metadata file %s", filename) // Special case for root.json: also check that the signed // content for keys and roles if role == data.CanonicalRootRole { var decodedRoot data.Root err := json.Unmarshal(decoded.Signed, &decodedRoot) assert.NoError(t, err, "error parsing root.json signed section: %s", err) assert.Equal(t, "Root", decodedRoot.Type, "_type mismatch in root.json") // Expect 1 key for each valid role in the Keys map - one for // each of root, targets, snapshot, timestamp assert.Len(t, decodedRoot.Keys, len(data.ValidRoles), "wrong number of keys in root.json") assert.Len(t, decodedRoot.Roles, len(data.ValidRoles), "wrong number of roles in root.json") for role := range data.ValidRoles { _, ok := decodedRoot.Roles[role] assert.True(t, ok, "Missing role %s in root.json", role) } } } func testInitRepo(t *testing.T, rootType string, serverManagesSnapshot bool) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID := initializeRepo(t, rootType, gun, ts.URL, serverManagesSnapshot) defer os.RemoveAll(repo.baseDir) assertRepoHasExpectedKeys(t, repo, rootKeyID, !serverManagesSnapshot) assertRepoHasExpectedCerts(t, repo) assertRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true) assertRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true) assertRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, !serverManagesSnapshot) } // TestInitRepoAttemptsExceeded tests error handling when passphrase.Retriever // (or rather the user) insists on an incorrect password. func TestInitRepoAttemptsExceeded(t *testing.T) { testInitRepoAttemptsExceeded(t, data.ECDSAKey) if !testing.Short() { testInitRepoAttemptsExceeded(t, data.RSAKey) } } func testInitRepoAttemptsExceeded(t *testing.T, rootType string) { gun := "docker.com/notary" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t) defer ts.Close() retriever := passphrase.ConstantRetriever("password") repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever) assert.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", rootType) assert.NoError(t, err, "error generating root key: %s", err) retriever = passphrase.ConstantRetriever("incorrect password") // repo.CryptoService’s FileKeyStore caches the unlocked private key, so to test // private key unlocking we need a new repo instance. repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever) assert.NoError(t, err, "error creating repo: %s", err) err = repo.Initialize(rootPubKey.ID()) assert.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error()) } // TestInitRepoPasswordInvalid tests error handling when passphrase.Retriever // (or rather the user) fails to provide a correct password. func TestInitRepoPasswordInvalid(t *testing.T) { testInitRepoPasswordInvalid(t, data.ECDSAKey) if !testing.Short() { testInitRepoPasswordInvalid(t, data.RSAKey) } } func giveUpPassphraseRetriever(_, _ string, _ bool, _ int) (string, bool, error) { return "", true, nil } func testInitRepoPasswordInvalid(t *testing.T, rootType string) { gun := "docker.com/notary" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t) defer ts.Close() retriever := passphrase.ConstantRetriever("password") repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever) assert.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", rootType) assert.NoError(t, err, "error generating root key: %s", err) // repo.CryptoService’s FileKeyStore caches the unlocked private key, so to test // private key unlocking we need a new repo instance. repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever) assert.NoError(t, err, "error creating repo: %s", err) err = repo.Initialize(rootPubKey.ID()) assert.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error()) } func addTarget(t *testing.T, repo *NotaryRepository, targetName, targetFile string, roles ...string) *Target { target, err := NewTarget(targetName, targetFile) assert.NoError(t, err, "error creating target") err = repo.AddTarget(target, roles...) assert.NoError(t, err, "error adding target") return target } // calls GetChangelist and gets the actual changes out func getChanges(t *testing.T, repo *NotaryRepository) []changelist.Change { changeList, err := repo.GetChangelist() assert.NoError(t, err) return changeList.List() } // TestAddTargetToTargetRoleByDefault adds a target without specifying a role // to a repo without delegations. Confirms that the changelist is created // correctly, for the targets scope. func TestAddTargetToTargetRoleByDefault(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) testAddOrDeleteTarget(t, repo, changelist.ActionCreate, nil, []string{data.CanonicalTargetsRole}) } // Tests that adding a target to a repo or deleting a target from a repo, // with the given roles, makes a change to the expected scopes func testAddOrDeleteTarget(t *testing.T, repo *NotaryRepository, action string, rolesToChange []string, expectedScopes []string) { assert.Len(t, getChanges(t, repo), 0, "should start with zero changes") if action == changelist.ActionCreate { // Add fixtures/intermediate-ca.crt as a target. There's no particular // reason for using this file except that it happens to be available as // a fixture. addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt", rolesToChange...) } else { err := repo.RemoveTarget("latest", rolesToChange...) assert.NoError(t, err, "error removing target") } changes := getChanges(t, repo) assert.Len(t, changes, len(expectedScopes), "wrong number of changes files found") foundScopes := make(map[string]bool) for _, c := range changes { // there is only one assert.EqualValues(t, action, c.Action()) foundScopes[c.Scope()] = true assert.Equal(t, "target", c.Type()) assert.Equal(t, "latest", c.Path()) if action == changelist.ActionCreate { assert.NotEmpty(t, c.Content()) } else { assert.Empty(t, c.Content()) } } assert.Len(t, foundScopes, len(expectedScopes)) for _, expectedScope := range expectedScopes { _, ok := foundScopes[expectedScope] assert.True(t, ok, "Target was not added/removed from %s", expectedScope) } // add/delete a second time if action == changelist.ActionCreate { addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt", rolesToChange...) } else { err := repo.RemoveTarget("current", rolesToChange...) assert.NoError(t, err, "error removing target") } changes = getChanges(t, repo) assert.Len(t, changes, 2*len(expectedScopes), "wrong number of changelist files found") newFileFound := false foundScopes = make(map[string]bool) for _, c := range changes { if c.Path() != "latest" { assert.EqualValues(t, action, c.Action()) foundScopes[c.Scope()] = true assert.Equal(t, "target", c.Type()) assert.Equal(t, "current", c.Path()) if action == changelist.ActionCreate { assert.NotEmpty(t, c.Content()) } else { assert.Empty(t, c.Content()) } newFileFound = true } } assert.True(t, newFileFound, "second changelist file not found") assert.Len(t, foundScopes, len(expectedScopes)) for _, expectedScope := range expectedScopes { _, ok := foundScopes[expectedScope] assert.True(t, ok, "Target was not added/removed from %s", expectedScope) } } // TestAddTargetToSpecifiedValidRoles adds a target to the specified roles. // Confirms that the changelist is created correctly, one for each of the // the specified roles as scopes. func TestAddTargetToSpecifiedValidRoles(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) roleName := filepath.Join(data.CanonicalTargetsRole, "a") testAddOrDeleteTarget(t, repo, changelist.ActionCreate, []string{ strings.ToUpper(data.CanonicalTargetsRole), strings.ToUpper(roleName), }, []string{data.CanonicalTargetsRole, roleName}) } // TestAddTargetToSpecifiedInvalidRoles expects errors to be returned if // adding a target to an invalid role. If any of the roles are invalid, // no targets are added to any roles. func TestAddTargetToSpecifiedInvalidRoles(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) invalidRoles := []string{ data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTimestampRole, "target/otherrole", "otherrole", } for _, invalidRole := range invalidRoles { target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt") assert.NoError(t, err, "error creating target") err = repo.AddTarget(target, data.CanonicalTargetsRole, invalidRole) assert.Error(t, err, "Expected an ErrInvalidRole error") assert.IsType(t, data.ErrInvalidRole{}, err) changes := getChanges(t, repo) assert.Len(t, changes, 0) } } // General way to assert that errors writing a changefile are propagated up func testErrorWritingChangefiles(t *testing.T, writeChangeFile func(*NotaryRepository) error) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) // first, make the actual changefile unwritable by making the changelist // directory unwritable changelistPath := filepath.Join(repo.tufRepoPath, "changelist") err := os.MkdirAll(changelistPath, 0744) assert.NoError(t, err, "could not create changelist dir") err = os.Chmod(changelistPath, 0600) assert.NoError(t, err, "could not change permission of changelist dir") err = writeChangeFile(repo) assert.Error(t, err, "Expected an error writing the change") assert.IsType(t, &os.PathError{}, err) // then break prevent the changlist directory from being able to be created err = os.Chmod(changelistPath, 0744) assert.NoError(t, err, "could not change permission of temp dir") err = os.RemoveAll(changelistPath) assert.NoError(t, err, "could not remove changelist dir") // creating a changelist file so the directory can't be created err = ioutil.WriteFile(changelistPath, []byte("hi"), 0644) assert.NoError(t, err, "could not write temporary file") err = writeChangeFile(repo) assert.Error(t, err, "Expected an error writing the change") assert.IsType(t, &os.PathError{}, err) } // TestAddTargetErrorWritingChanges expects errors writing a change to file // to be propagated. func TestAddTargetErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *NotaryRepository) error { target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt") assert.NoError(t, err, "error creating target") return repo.AddTarget(target, data.CanonicalTargetsRole) }) } // TestRemoveTargetToTargetRoleByDefault removes a target without specifying a // role from a repo. Confirms that the changelist is created correctly for // the targets scope. func TestRemoveTargetToTargetRoleByDefault(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) testAddOrDeleteTarget(t, repo, changelist.ActionDelete, nil, []string{data.CanonicalTargetsRole}) } // TestRemoveTargetFromSpecifiedValidRoles removes a target from the specified // roles. Confirms that the changelist is created correctly, one for each of // the the specified roles as scopes. func TestRemoveTargetFromSpecifiedValidRoles(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) roleName := filepath.Join(data.CanonicalTargetsRole, "a") testAddOrDeleteTarget(t, repo, changelist.ActionDelete, []string{ strings.ToUpper(data.CanonicalTargetsRole), strings.ToUpper(roleName), }, []string{data.CanonicalTargetsRole, roleName}) } // TestRemoveTargetFromSpecifiedInvalidRoles expects errors to be returned if // removing a target to an invalid role. If any of the roles are invalid, // no targets are removed from any roles. func TestRemoveTargetToSpecifiedInvalidRoles(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) invalidRoles := []string{ data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTimestampRole, "target/otherrole", "otherrole", } for _, invalidRole := range invalidRoles { err := repo.RemoveTarget("latest", data.CanonicalTargetsRole, invalidRole) assert.Error(t, err, "Expected an ErrInvalidRole error") assert.IsType(t, data.ErrInvalidRole{}, err) changes := getChanges(t, repo) assert.Len(t, changes, 0) } } // TestRemoveTargetErrorWritingChanges expects errors writing a change to file // to be propagated. func TestRemoveTargetErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *NotaryRepository) error { return repo.RemoveTarget("latest", data.CanonicalTargetsRole) }) } // TestListTarget fakes serving signed metadata files over the test's // internal HTTP server to ensure that ListTargets returns the correct number // of listed targets. // We test this with both an RSA and ECDSA root key func TestListTarget(t *testing.T) { testListEmptyTargets(t, data.ECDSAKey) testListTarget(t, data.ECDSAKey) testListTargetWithDelegates(t, data.ECDSAKey) if !testing.Short() { testListEmptyTargets(t, data.RSAKey) testListTarget(t, data.RSAKey) testListTargetWithDelegates(t, data.RSAKey) } } func testListEmptyTargets(t *testing.T, rootType string) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) _, err := repo.ListTargets(data.CanonicalTargetsRole) assert.Error(t, err) // no trust data } // reads data from the repository in order to fake data being served via // the ServeMux. func fakeServerData(t *testing.T, repo *NotaryRepository, mux *http.ServeMux, keys map[string]data.PrivateKey) { timestampKey, ok := keys[data.CanonicalTimestampRole] assert.True(t, ok) savedTUFRepo := repo.tufRepo // in case this is overwritten fileStore, err := trustmanager.NewKeyFileStore(repo.baseDir, passphraseRetriever) assert.NoError(t, err) fileStore.AddKey( filepath.Join(filepath.FromSlash(repo.gun), timestampKey.ID()), "nonroot", timestampKey) rootJSONFile := filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "metadata", "root.json") rootFileBytes, err := ioutil.ReadFile(rootJSONFile) signedTargets, err := savedTUFRepo.SignTargets( "targets", data.DefaultExpires("targets")) assert.NoError(t, err) signedLevel1, err := savedTUFRepo.SignTargets( "targets/level1", data.DefaultExpires(data.CanonicalTargetsRole), ) if _, ok := savedTUFRepo.Targets["targets/level1"]; ok { assert.NoError(t, err) } signedLevel2, err := savedTUFRepo.SignTargets( "targets/level2", data.DefaultExpires(data.CanonicalTargetsRole), ) if _, ok := savedTUFRepo.Targets["targets/level2"]; ok { assert.NoError(t, err) } signedSnapshot, err := savedTUFRepo.SignSnapshot( data.DefaultExpires("snapshot")) assert.NoError(t, err) signedTimestamp, err := savedTUFRepo.SignTimestamp( data.DefaultExpires("timestamp")) assert.NoError(t, err) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", func(w http.ResponseWriter, r *http.Request) { assert.NoError(t, err) fmt.Fprint(w, string(rootFileBytes)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", func(w http.ResponseWriter, r *http.Request) { timestampJSON, _ := json.Marshal(signedTimestamp) fmt.Fprint(w, string(timestampJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json", func(w http.ResponseWriter, r *http.Request) { snapshotJSON, _ := json.Marshal(signedSnapshot) fmt.Fprint(w, string(snapshotJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json", func(w http.ResponseWriter, r *http.Request) { targetsJSON, _ := json.Marshal(signedTargets) fmt.Fprint(w, string(targetsJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1.json", func(w http.ResponseWriter, r *http.Request) { level1JSON, err := json.Marshal(signedLevel1) assert.NoError(t, err) fmt.Fprint(w, string(level1JSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2.json", func(w http.ResponseWriter, r *http.Request) { level2JSON, err := json.Marshal(signedLevel2) assert.NoError(t, err) fmt.Fprint(w, string(level2JSON)) }) } // We want to sort by name, so we can guarantee ordering. type targetSorter []*Target func (k targetSorter) Len() int { return len(k) } func (k targetSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } func (k targetSorter) Less(i, j int) bool { return k[i].Name < k[j].Name } func testListTarget(t *testing.T, rootType string) { ts, mux, keys := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) // tests need to manually boostrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() assert.NoError(t, err, "error creating repository: %s", err) latestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo cl, err := changelist.NewFileChangelist( filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist")) assert.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, cl) assert.NoError(t, err, "could not apply changelist") fakeServerData(t, repo, mux, keys) targets, err := repo.ListTargets(data.CanonicalTargetsRole) assert.NoError(t, err) // Should be two targets assert.Len(t, targets, 2, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) // current should be first assert.Equal(t, currentTarget, targets[0], "current target does not match") assert.Equal(t, latestTarget, targets[1], "latest target does not match") // Also test GetTargetByName newLatestTarget, err := repo.GetTargetByName("latest") assert.NoError(t, err) assert.Equal(t, latestTarget, newLatestTarget, "latest target does not match") newCurrentTarget, err := repo.GetTargetByName("current") assert.NoError(t, err) assert.Equal(t, currentTarget, newCurrentTarget, "current target does not match") } func testListTargetWithDelegates(t *testing.T, rootType string) { ts, mux, keys := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) // tests need to manually boostrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() assert.NoError(t, err, "error creating repository: %s", err) latestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // setup delegated targets/level1 role k, err := repo.CryptoService.Create("targets/level1", rootType) assert.NoError(t, err) r, err := data.NewRole("targets/level1", 1, []string{k.ID()}, []string{""}, nil) assert.NoError(t, err) repo.tufRepo.UpdateDelegations(r, []data.PublicKey{k}) delegatedTarget := addTarget(t, repo, "current", "../fixtures/root-ca.crt", "targets/level1") otherTarget := addTarget(t, repo, "other", "../fixtures/root-ca.crt", "targets/level1") // setup delegated targets/level2 role k, err = repo.CryptoService.Create("targets/level2", rootType) assert.NoError(t, err) r, err = data.NewRole("targets/level2", 1, []string{k.ID()}, []string{""}, nil) assert.NoError(t, err) repo.tufRepo.UpdateDelegations(r, []data.PublicKey{k}) // this target should not show up as the one in targets/level1 takes higher priority _ = addTarget(t, repo, "current", "../fixtures/notary-server.crt", "targets/level2") // this target should show up as the name doesn't exist elsewhere level2Target := addTarget(t, repo, "level2", "../fixtures/notary-server.crt", "targets/level2") // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo cl, err := changelist.NewFileChangelist( filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist")) assert.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, cl) assert.NoError(t, err, "could not apply changelist") _, ok := repo.tufRepo.Targets["targets/level1"].Signed.Targets["current"] assert.True(t, ok) _, ok = repo.tufRepo.Targets["targets/level1"].Signed.Targets["other"] assert.True(t, ok) _, ok = repo.tufRepo.Targets["targets/level2"].Signed.Targets["level2"] assert.True(t, ok) fakeServerData(t, repo, mux, keys) // test default listing targets, err := repo.ListTargets() assert.NoError(t, err) // Should be two targets assert.Len(t, targets, 4, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) // current should be first. assert.Equal(t, currentTarget, targets[0], "current target does not match") assert.Equal(t, latestTarget, targets[1], "latest target does not match") assert.Equal(t, level2Target, targets[2], "level2 target does not match") assert.Equal(t, otherTarget, targets[3], "other target does not match") // test listing with priority specified targets, err = repo.ListTargets("targets/level1", data.CanonicalTargetsRole) assert.NoError(t, err) // Should be two targets assert.Len(t, targets, 4, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) // current should be first assert.Equal(t, delegatedTarget, targets[0], "current target does not match") assert.Equal(t, latestTarget, targets[1], "latest target does not match") assert.Equal(t, level2Target, targets[2], "level2 target does not match") assert.Equal(t, otherTarget, targets[3], "other target does not match") // Also test GetTargetByName newLatestTarget, err := repo.GetTargetByName("latest") assert.NoError(t, err) assert.Equal(t, latestTarget, newLatestTarget, "latest target does not match") newCurrentTarget, err := repo.GetTargetByName("current", "targets/level1", "targets") assert.NoError(t, err) assert.Equal(t, delegatedTarget, newCurrentTarget, "current target does not match") newOtherTarget, err := repo.GetTargetByName("other") assert.NoError(t, err) assert.True(t, reflect.DeepEqual(otherTarget, newOtherTarget), "other target does not match") newLevel2Target, err := repo.GetTargetByName("level2") assert.NoError(t, err) assert.True(t, reflect.DeepEqual(level2Target, newLevel2Target), "level2 target does not match") } // TestValidateRootKey verifies that the public data in root.json for the root // key is a valid x509 certificate. func TestValidateRootKey(t *testing.T) { testValidateRootKey(t, data.ECDSAKey) if !testing.Short() { testValidateRootKey(t, data.RSAKey) } } func testValidateRootKey(t *testing.T, rootType string) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) rootJSONFile := filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "metadata", "root.json") jsonBytes, err := ioutil.ReadFile(rootJSONFile) assert.NoError(t, err, "error reading TUF metadata file %s: %s", rootJSONFile, err) var decoded data.Signed err = json.Unmarshal(jsonBytes, &decoded) assert.NoError(t, err, "error parsing TUF metadata file %s: %s", rootJSONFile, err) var decodedRoot data.Root err = json.Unmarshal(decoded.Signed, &decodedRoot) assert.NoError(t, err, "error parsing root.json signed section: %s", err) keyids := []string{} for role, roleData := range decodedRoot.Roles { if role == "root" { keyids = append(keyids, roleData.KeyIDs...) } } assert.NotEmpty(t, keyids) for _, keyid := range keyids { key, ok := decodedRoot.Keys[keyid] assert.True(t, ok, "key id not found in keys") _, err := trustmanager.LoadCertFromPEM(key.Public()) assert.NoError(t, err, "key is not a valid cert") } } // TestGetChangelist ensures that the changelist returned matches the changes // added. // We test this with both an RSA and ECDSA root key func TestGetChangelist(t *testing.T) { testGetChangelist(t, data.ECDSAKey) if !testing.Short() { testGetChangelist(t, data.RSAKey) } } func testGetChangelist(t *testing.T, rootType string) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) assert.Len(t, getChanges(t, repo), 0, "No changes should be in changelist yet") // Create 2 targets addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // Test loading changelist chgs := getChanges(t, repo) assert.Len(t, chgs, 2, "Wrong number of changes returned from changelist") changes := make(map[string]changelist.Change) for _, ch := range chgs { changes[ch.Path()] = ch } currentChange := changes["current"] assert.NotNil(t, currentChange, "Expected changelist to contain a change for path 'current'") assert.EqualValues(t, changelist.ActionCreate, currentChange.Action()) assert.Equal(t, "targets", currentChange.Scope()) assert.Equal(t, "target", currentChange.Type()) assert.Equal(t, "current", currentChange.Path()) latestChange := changes["latest"] assert.NotNil(t, latestChange, "Expected changelist to contain a change for path 'latest'") assert.EqualValues(t, changelist.ActionCreate, latestChange.Action()) assert.Equal(t, "targets", latestChange.Scope()) assert.Equal(t, "target", latestChange.Type()) assert.Equal(t, "latest", latestChange.Path()) } // Create a repo, instantiate a notary server, and publish the bare repo to the // server, signing all the non-timestamp metadata. Root, targets, and snapshots // (if locally signing) should be sent. func TestPublishBareRepo(t *testing.T) { testPublishNoData(t, data.ECDSAKey, true) testPublishNoData(t, data.ECDSAKey, false) if !testing.Short() { testPublishNoData(t, data.RSAKey, true) testPublishNoData(t, data.RSAKey, false) } } func testPublishNoData(t *testing.T, rootType string, serverManagesSnapshot bool) { ts := fullTestServer(t) defer ts.Close() repo1, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, serverManagesSnapshot) defer os.RemoveAll(repo1.baseDir) assert.NoError(t, repo1.Publish()) // use another repo to check metadata repo2 := newRepoToTestRepo(t, repo1) defer os.RemoveAll(repo2.baseDir) targets, err := repo2.ListTargets() assert.NoError(t, err) assert.Empty(t, targets) for role := range data.ValidRoles { // we don't cache timstamp metadata if role != data.CanonicalTimestampRole { assertRepoHasExpectedMetadata(t, repo2, role, true) } } } // Publishing an uninitialized repo will fail, but initializing and republishing // after should succeed func TestPublishUninitializedRepo(t *testing.T) { gun := "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // uninitialized repo should fail to publish tempBaseDir, err := ioutil.TempDir("", "notary-tests") assert.NoError(t, err) defer os.RemoveAll(tempBaseDir) repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) assert.NoError(t, err, "error creating repository: %s", err) err = repo.Publish() assert.Error(t, err) // no metadata created assertRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, false) assertRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false) assertRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, false) // now, initialize and republish in the same directory rootPubKey, err := repo.CryptoService.Create("root", data.ECDSAKey) assert.NoError(t, err, "error generating root key: %s", err) assert.NoError(t, repo.Initialize(rootPubKey.ID())) // now metadata is created assertRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true) assertRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true) assertRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true) assert.NoError(t, repo.Publish()) } // Create a repo, instantiate a notary server, and publish the repo with // some targets to the server, signing all the non-timestamp metadata. // We test this with both an RSA and ECDSA root key func TestPublishClientHasSnapshotKey(t *testing.T) { testPublishWithData(t, data.ECDSAKey, false) if !testing.Short() { testPublishWithData(t, data.RSAKey, false) } } // Create a repo, instantiate a notary server (designating the server as the // snapshot signer) , and publish the repo with some targets to the server, // signing the root and targets metadata only. The server should sign just fine. // We test this with both an RSA and ECDSA root key func TestPublishAfterInitServerHasSnapshotKey(t *testing.T) { testPublishWithData(t, data.ECDSAKey, true) if !testing.Short() { testPublishWithData(t, data.RSAKey, true) } } func testPublishWithData(t *testing.T, rootType string, serverManagesSnapshot bool) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, serverManagesSnapshot) defer os.RemoveAll(repo.baseDir) assertPublishSucceeds(t, repo) } // asserts that publish succeeds by adding to the default only and publishing; // the targets should appear in targets func assertPublishSucceeds(t *testing.T, repo1 *NotaryRepository) { assertPublishToRolesSucceeds(t, repo1, nil, []string{data.CanonicalTargetsRole}) } // asserts that adding to the given roles results in the targets actually being // added only to the expected roles and no others func assertPublishToRolesSucceeds(t *testing.T, repo1 *NotaryRepository, publishToRoles []string, expectedPublishedRoles []string) { // were there unpublished changes before? changesOffset := len(getChanges(t, repo1)) // Create 2 targets - (actually 3, but we delete 1) addTarget(t, repo1, "toDelete", "../fixtures/intermediate-ca.crt", publishToRoles...) latestTarget := addTarget( t, repo1, "latest", "../fixtures/intermediate-ca.crt", publishToRoles...) currentTarget := addTarget( t, repo1, "current", "../fixtures/intermediate-ca.crt", publishToRoles...) repo1.RemoveTarget("toDelete", publishToRoles...) // if no roles are provided, the default role is target numRoles := int(math.Max(1, float64(len(publishToRoles)))) assert.Len(t, getChanges(t, repo1), changesOffset+4*numRoles, "wrong number of changelist files found") // Now test Publish err := repo1.Publish() assert.NoError(t, err) assert.Len(t, getChanges(t, repo1), 0, "wrong number of changelist files found") // use another repo to check metadata repo2 := newRepoToTestRepo(t, repo1) defer os.RemoveAll(repo2.baseDir) // Should be two targets per role for _, role := range expectedPublishedRoles { for _, repo := range []*NotaryRepository{repo1, repo2} { targets, err := repo.ListTargets(role) assert.NoError(t, err) assert.Len(t, targets, 2, "unexpected number of targets returned by ListTargets(%s)", role) sort.Stable(targetSorter(targets)) assert.Equal(t, currentTarget, targets[0], "current target does not match") assert.Equal(t, latestTarget, targets[1], "latest target does not match") // Also test GetTargetByName if role == data.CanonicalTargetsRole { newLatestTarget, err := repo.GetTargetByName("latest") assert.NoError(t, err) assert.Equal(t, latestTarget, newLatestTarget, "latest target does not match") newCurrentTarget, err := repo.GetTargetByName("current") assert.NoError(t, err) assert.Equal(t, currentTarget, newCurrentTarget, "current target does not match") } } } } // After pulling a repo from the server, so there is a snapshots metadata file, // push a different target to the server (the server is still the snapshot // signer). The server should sign just fine. // We test this with both an RSA and ECDSA root key func TestPublishAfterPullServerHasSnapshotKey(t *testing.T) { testPublishAfterPullServerHasSnapshotKey(t, data.ECDSAKey) if !testing.Short() { testPublishAfterPullServerHasSnapshotKey(t, data.RSAKey) } } func testPublishAfterPullServerHasSnapshotKey(t *testing.T, rootType string) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, true) defer os.RemoveAll(repo.baseDir) // no timestamp metadata because that comes from the server assertRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false) // no snapshot metadata because that comes from the server assertRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false) // Publish something published := addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt") assert.NoError(t, repo.Publish()) // still no timestamp or snapshot metadata info assertRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false) assertRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false) // list, so that the snapshot metadata is pulled from server targets, err := repo.ListTargets(data.CanonicalTargetsRole) assert.NoError(t, err) assert.Equal(t, []*Target{published}, targets) // listing downloaded the timestamp and snapshot metadata info assertRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, true) assertRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true) // Publish again should succeed addTarget(t, repo, "v2", "../fixtures/intermediate-ca.crt") err = repo.Publish() assert.NoError(t, err) } // If neither the client nor the server has the snapshot key, signing will fail // with an ErrNoKeys error. // We test this with both an RSA and ECDSA root key func TestPublishNoOneHasSnapshotKey(t *testing.T) { testPublishNoOneHasSnapshotKey(t, data.ECDSAKey) if !testing.Short() { testPublishNoOneHasSnapshotKey(t, data.RSAKey) } } func testPublishNoOneHasSnapshotKey(t *testing.T, rootType string) { ts := fullTestServer(t) defer ts.Close() // create repo and delete the snapshot key and metadata repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) snapshotRole, ok := repo.tufRepo.Root.Signed.Roles[data.CanonicalSnapshotRole] assert.True(t, ok) for _, keyID := range snapshotRole.KeyIDs { repo.CryptoService.RemoveKey(keyID) } // Publish something addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt") err := repo.Publish() assert.Error(t, err) assert.IsType(t, validation.ErrBadHierarchy{}, err) } // If the snapshot metadata is corrupt or the snapshot metadata is unreadable, // we can't publish for the first time (whether the client or server has the // snapshot key), because there is no existing data for us to download. If the // repo has already been published, it doesn't matter if the metadata is corrupt // because we can just redownload if it is. func TestPublishSnapshotCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // do not publish first - publish should fail with corrupt snapshot data even with server signing snapshot repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, true) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, false, false) // do not publish first - publish should fail with corrupt snapshot data with local snapshot signing repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, false, false) // publish first - publish again should succeed despite corrupt snapshot data (server signing snapshot) repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary3", ts.URL, true) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, true, true) // publish first - publish again should succeed despite corrupt snapshot data (local snapshot signing) repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary4", ts.URL, false) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, true, true) } // If the targets metadata is corrupt or the targets metadata is unreadable, // we can't publish for the first time, because there is no existing data for. // us to download. If the repo has already been published, it doesn't matter // if the metadata is corrupt because we can just redownload if it is. func TestPublishTargetsCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // do not publish first - publish should fail with corrupt snapshot data repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, false) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalTargetsRole, repo, false, false) // publish first - publish again should succeed despite corrupt snapshot data repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalTargetsRole, repo, true, true) } // If the root metadata is corrupt or the root metadata is unreadable, // we can't publish for the first time. If there is already a remote root, // we just download that and verify (using our trusted certificate trust // anchors) that it is signed with the same keys, and if so, we just use the // remote root. func TestPublishRootCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // do not publish first - publish should fail with corrupt snapshot data repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, false) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalRootRole, repo, false, false) // publish first - publish should still succeed if root corrupt since the // remote root is signed with the same key. repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) defer os.RemoveAll(repo.baseDir) testPublishBadMetadata(t, data.CanonicalRootRole, repo, true, true) } // When publishing snapshot, root, or target, if the repo hasn't been published // before, if the metadata is corrupt, it can't be published. If it has been // published already, then the corrupt metadata can just be re-downloaded, so // publishing is successful. func testPublishBadMetadata(t *testing.T, roleName string, repo *NotaryRepository, publishFirst, succeeds bool) { if publishFirst { assert.NoError(t, repo.Publish()) } addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt") // readable, but corrupt file repo.fileStore.SetMeta(roleName, []byte("this isn't JSON")) err := repo.Publish() if succeeds { assert.NoError(t, err) } else { assert.Error(t, err) assert.IsType(t, ®Json.SyntaxError{}, err) } // make an unreadable file by creating a directory instead of a file path := fmt.Sprintf("%s.%s", filepath.Join(repo.baseDir, tufDir, filepath.FromSlash(repo.gun), "metadata", roleName), "json") os.RemoveAll(path) assert.NoError(t, os.Mkdir(path, 0755)) defer os.RemoveAll(path) err = repo.Publish() if succeeds { assert.NoError(t, err) } else { assert.Error(t, err) assert.IsType(t, &os.PathError{}, err) } } // If the repo is not initialized, calling repo.Publish() should return ErrRepoNotInitialized func TestNotInitializedOnPublish(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") defer os.RemoveAll(tempBaseDir) assert.NoError(t, err, "failed to create a temporary directory: %s", err) gun := "docker.com/notary" ts := fullTestServer(t) defer ts.Close() repo, _ := createRepoAndKey(t, data.ECDSAKey, tempBaseDir, gun, ts.URL) addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt") err = repo.Publish() assert.Error(t, err) assert.IsType(t, &ErrRepoNotInitialized{}, err) } type cannotCreateKeys struct { signed.CryptoService } func (cs cannotCreateKeys) Create(_, _ string) (data.PublicKey, error) { return nil, fmt.Errorf("Oh no I cannot create keys") } // If there is an error creating the local keys, no call is made to get a // remote key. func TestPublishSnapshotLocalKeysCreatedFirst(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") defer os.RemoveAll(tempBaseDir) assert.NoError(t, err, "failed to create a temporary directory: %s", err) gun := "docker.com/notary" requestMade := false ts := httptest.NewServer(http.HandlerFunc( func(http.ResponseWriter, *http.Request) { requestMade = true })) defer ts.Close() repo, err := NewNotaryRepository( tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) assert.NoError(t, err, "error creating repo: %s", err) cs := cryptoservice.NewCryptoService(gun, trustmanager.NewKeyMemoryStore(passphraseRetriever)) rootPubKey, err := cs.Create(data.CanonicalRootRole, data.ECDSAKey) assert.NoError(t, err, "error generating root key: %s", err) repo.CryptoService = cannotCreateKeys{CryptoService: cs} err = repo.Initialize(rootPubKey.ID(), data.CanonicalSnapshotRole) assert.Error(t, err) assert.Contains(t, err.Error(), "Oh no I cannot create keys") assert.False(t, requestMade) } // Publishing delegations works so long as the delegation parent exists by the // time that delegation addition change is applied. Most of the tests for // applying delegation changes in in helpers_test.go (applyTargets tests), so // this is just a sanity test to make sure Publish calls it correctly and // no fallback happens. func TestPublishDelegations(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo1, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo1.baseDir) delgKey, err := repo1.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") // This should publish fine, even though targets/a/b is dependent upon // targets/a, because these should execute in order for _, delgName := range []string{"targets/a", "targets/a/b", "targets/c"} { assert.NoError(t, repo1.AddDelegation(delgName, 1, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") } assert.Len(t, getChanges(t, repo1), 3, "wrong number of changelist files found") assert.NoError(t, repo1.Publish()) assert.Len(t, getChanges(t, repo1), 0, "wrong number of changelist files found") // this should not publish, because targets/z doesn't exist assert.NoError(t, repo1.AddDelegation("targets/z/y", 1, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") assert.Len(t, getChanges(t, repo1), 1, "wrong number of changelist files found") assert.Error(t, repo1.Publish()) assert.Len(t, getChanges(t, repo1), 1, "wrong number of changelist files found") // use another repo to check metadata repo2 := newRepoToTestRepo(t, repo1) defer os.RemoveAll(repo2.baseDir) // pull _, err = repo2.ListTargets() assert.NoError(t, err, "unable to pull repo") for _, repo := range []*NotaryRepository{repo1, repo2} { // targets should have delegations targets/a and targets/c targets := repo.tufRepo.Targets[data.CanonicalTargetsRole] assert.Len(t, targets.Signed.Delegations.Roles, 2) assert.Len(t, targets.Signed.Delegations.Keys, 1) _, ok := targets.Signed.Delegations.Keys[delgKey.ID()] assert.True(t, ok) foundRoleNames := make(map[string]bool) for _, r := range targets.Signed.Delegations.Roles { foundRoleNames[r.Name] = true } assert.True(t, foundRoleNames["targets/a"]) assert.True(t, foundRoleNames["targets/c"]) // targets/a should have delegation targets/a/b only a := repo.tufRepo.Targets["targets/a"] assert.Len(t, a.Signed.Delegations.Roles, 1) assert.Len(t, a.Signed.Delegations.Keys, 1) _, ok = a.Signed.Delegations.Keys[delgKey.ID()] assert.True(t, ok) assert.Equal(t, "targets/a/b", a.Signed.Delegations.Roles[0].Name) } } // Publishing delegations works so long as the delegation parent exists by the // time that delegation addition change is applied. Most of the tests for // applying delegation changes in in helpers_test.go (applyTargets tests), so // this is just a sanity test to make sure Publish calls it correctly and // no fallback happens. func TestPublishDelegationsX509(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo1, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo1.baseDir) delgKey, err := repo1.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") start := time.Now() privKey, _, err := repo1.CryptoService.GetPrivateKey(delgKey.ID()) assert.NoError(t, err) cert, err := cryptoservice.GenerateCertificate( privKey, "targets/a", start, start.AddDate(1, 0, 0), ) assert.NoError(t, err) delgCert := data.NewECDSAx509PublicKey(trustmanager.CertToPEM(cert)) // This should publish fine, even though targets/a/b is dependent upon // targets/a, because these should execute in order for _, delgName := range []string{"targets/a", "targets/a/b", "targets/c"} { assert.NoError(t, repo1.AddDelegation(delgName, 1, []data.PublicKey{delgCert}, []string{""}), "error creating delegation") } assert.Len(t, getChanges(t, repo1), 3, "wrong number of changelist files found") assert.NoError(t, repo1.Publish()) assert.Len(t, getChanges(t, repo1), 0, "wrong number of changelist files found") // this should not publish, because targets/z doesn't exist assert.NoError(t, repo1.AddDelegation("targets/z/y", 1, []data.PublicKey{delgCert}, []string{""}), "error creating delegation") assert.Len(t, getChanges(t, repo1), 1, "wrong number of changelist files found") assert.Error(t, repo1.Publish()) assert.Len(t, getChanges(t, repo1), 1, "wrong number of changelist files found") // Create a new repo and pull from the server repo2 := newRepoToTestRepo(t, repo1) defer os.RemoveAll(repo2.baseDir) // pull _, err = repo2.ListTargets() assert.NoError(t, err, "unable to pull repo") for _, repo := range []*NotaryRepository{repo1, repo2} { // targets should have delegations targets/a and targets/c targets := repo.tufRepo.Targets[data.CanonicalTargetsRole] assert.Len(t, targets.Signed.Delegations.Roles, 2) assert.Len(t, targets.Signed.Delegations.Keys, 1) _, ok := targets.Signed.Delegations.Keys[delgCert.ID()] assert.True(t, ok) foundRoleNames := make(map[string]bool) for _, r := range targets.Signed.Delegations.Roles { foundRoleNames[r.Name] = true } assert.True(t, foundRoleNames["targets/a"]) assert.True(t, foundRoleNames["targets/c"]) // targets/a should have delegation targets/a/b only a := repo.tufRepo.Targets["targets/a"] assert.Len(t, a.Signed.Delegations.Roles, 1) assert.Len(t, a.Signed.Delegations.Keys, 1) _, ok = a.Signed.Delegations.Keys[delgCert.ID()] assert.True(t, ok) assert.Equal(t, "targets/a/b", a.Signed.Delegations.Roles[0].Name) } } // If a changelist specifies a particular role to push targets to, and there // is no such role, publish will try to publish to its parent. If the parent // doesn't work, it falls back on its parent, and so forth, and eventually // falls back on publishing to "target". This *only* falls back if the role // doesn't exist, not if the user doesn't have a key. (different test) func TestPublishTargetsDelgationScopeFallback(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) assertPublishToRolesSucceeds(t, repo, []string{"targets/a/b", "targets/b/c"}, []string{data.CanonicalTargetsRole}) } // If a changelist specifies a particular role to push targets to, and there // is a role but no key, publish not fall back and just fail. func TestPublishTargetsDelgationScopeNoFallbackIfNoKeys(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) // generate a key that isn't in the cryptoservice, so we can't sign this // one aPrivKey, err := trustmanager.GenerateECDSAKey(rand.Reader) assert.NoError(t, err, "error generating key that is not in our cryptoservice") aPubKey := data.PublicKeyFromPrivate(aPrivKey) // ensure that the role exists assert.NoError(t, repo.AddDelegation("targets/a", 1, []data.PublicKey{aPubKey}, []string{""})) assert.NoError(t, repo.Publish()) // add a target to targets/a/b - no role b, so it falls back on a, which // exists but there is no signing key for addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt", "targets/a/b") assert.Len(t, getChanges(t, repo), 1, "wrong number of changelist files found") // Now Publish should fail assert.Error(t, repo.Publish()) assert.Len(t, getChanges(t, repo), 1, "wrong number of changelist files found") targets, err := repo.ListTargets("targets", "targets/a", "targets/a/b") assert.NoError(t, err) assert.Empty(t, targets) } // If a changelist specifies a particular role to push targets to, and such // a role and the keys are present, publish will write to that role only, and // not its parents. This tests the case where the local machine knows about // all the roles (in fact, the role creations will be applied before the // targets) func TestPublishTargetsDelgationSuccessLocallyHasRoles(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) delgKey, err := repo.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") for _, delgName := range []string{"targets/a", "targets/a/b"} { assert.NoError(t, repo.AddDelegation(delgName, 1, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") } assertPublishToRolesSucceeds(t, repo, []string{"targets/a/b"}, []string{"targets/a/b"}) } // If a changelist specifies a particular role to push targets to, and the role // is present, publish will write to that role only. The targets keys are not // needed. func TestPublishTargetsDelgationNoTargetsKeyNeeded(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) delgKey, err := repo.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") for _, delgName := range []string{"targets/a", "targets/a/b"} { assert.NoError(t, repo.AddDelegation(delgName, 1, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") } assert.NoError(t, repo.Publish()) // remove targets key - it is not even needed targetsKeys := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) assert.Len(t, targetsKeys, 1) assert.NoError(t, repo.CryptoService.RemoveKey(targetsKeys[0])) assertPublishToRolesSucceeds(t, repo, []string{"targets/a/b"}, []string{"targets/a/b"}) } // If a changelist specifies a particular role to push targets to, and is such // a role and the keys are present, publish will write to that role only, and // not its parents. Tests: // - case where the local doesn't know about all the roles, and has to download // them before publish. // - owner of a repo may not have the delegated keys, so can't sign a delegated // role func TestPublishTargetsDelgationSuccessNeedsToDownloadRoles(t *testing.T) { gun := "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // this is the original repo - it owns the root/targets keys and creates // the delegation to which it doesn't have the key (so server snapshot // signing would be required) ownerRepo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, true) defer os.RemoveAll(ownerRepo.baseDir) // this is a user, or otherwise a repo that only has access to the delegation // key so it can publish targets to the delegated role delgRepo := newRepoToTestRepo(t, ownerRepo) defer os.RemoveAll(delgRepo.baseDir) // create a key on the owner repo aKey, err := ownerRepo.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") // create a key on the delegated repo bKey, err := delgRepo.CryptoService.Create("targets/a/b", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") // owner creates delegations, adds the delegated key to them, and publishes them assert.NoError(t, ownerRepo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}), "error creating delegation") assert.NoError(t, ownerRepo.AddDelegation("targets/a/b", 1, []data.PublicKey{bKey}, []string{""}), "error creating delegation") assert.NoError(t, ownerRepo.Publish()) // delegated repo now publishes to delegated roles, but it will need // to download those roles first, since it doesn't know about them assertPublishToRolesSucceeds(t, delgRepo, []string{"targets/a/b"}, []string{"targets/a/b"}) } // Ensure that two clients can publish delegations with two different keys and // the changes will not clobber each other. func TestPublishTargetsDelgationFromTwoRepos(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // this happens to be the client that creates the repo, but can also // write a delegation repo1, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(repo1.baseDir) // this is the second writable repo repo2 := newRepoToTestRepo(t, repo1) defer os.RemoveAll(repo2.baseDir) // create keys for each repo key1, err := repo1.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") // create a key on the delegated repo key2, err := repo2.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") // delegation includes both keys assert.NoError(t, repo1.AddDelegation("targets/a", 1, []data.PublicKey{key1, key2}, []string{""}), "error creating delegation") assert.NoError(t, repo1.Publish()) // both repos add targets and publish addTarget(t, repo1, "first", "../fixtures/root-ca.crt", "targets/a") assert.NoError(t, repo1.Publish()) addTarget(t, repo2, "second", "../fixtures/root-ca.crt", "targets/a") assert.NoError(t, repo2.Publish()) // first repo can publish again addTarget(t, repo1, "third", "../fixtures/root-ca.crt", "targets/a") assert.NoError(t, repo1.Publish()) // both repos should be able to see all targets for _, repo := range []*NotaryRepository{repo1, repo2} { targets, err := repo.ListTargets() assert.NoError(t, err) assert.Len(t, targets, 3) found := make(map[string]bool) for _, t := range targets { found[t.Name] = true } for _, targetName := range []string{"first", "second", "third"} { _, ok := found[targetName] assert.True(t, ok) } } } // A client who could publish before can no longer publish once the owner // removes their delegation key from the delegation role. func TestPublishRemoveDelgationKeyFromDelegationRole(t *testing.T) { gun := "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // this is the original repo - it owns the root/targets keys and creates // the delegation to which it doesn't have the key (so server snapshot // signing would be required) ownerRepo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, true) defer os.RemoveAll(ownerRepo.baseDir) // this is a user, or otherwise a repo that only has access to the delegation // key so it can publish targets to the delegated role delgRepo := newRepoToTestRepo(t, ownerRepo) defer os.RemoveAll(delgRepo.baseDir) // create a key on the delegated repo aKey, err := delgRepo.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") // owner creates delegation, adds the delegated key to it, and publishes it assert.NoError(t, ownerRepo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}), "error creating delegation") assert.NoError(t, ownerRepo.Publish()) // delegated repo can now publish to delegated role addTarget(t, delgRepo, "v1", "../fixtures/root-ca.crt", "targets/a") assert.NoError(t, delgRepo.Publish()) // owner revokes delegation // note there is no removekeyfromdelegation yet, so here's a hack to do so newKey, err := ownerRepo.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err) tdJSON, err := json.Marshal(&changelist.TufDelegation{ NewThreshold: 1, AddKeys: data.KeyList([]data.PublicKey{newKey}), RemoveKeys: []string{aKey.ID()}, }) assert.NoError(t, err) cl, err := changelist.NewFileChangelist(filepath.Join(ownerRepo.tufRepoPath, "changelist")) assert.NoError(t, cl.Add(changelist.NewTufChange( changelist.ActionUpdate, "targets/a", changelist.TypeTargetsDelegation, "", tdJSON, ))) cl.Close() assert.NoError(t, ownerRepo.Publish()) // delegated repo can now no longer publish to delegated role addTarget(t, delgRepo, "v2", "../fixtures/root-ca.crt", "targets/a") assert.Error(t, delgRepo.Publish()) } // A client who could publish before can no longer publish once the owner // deletes the delegation func TestPublishRemoveDelgation(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // this is the original repo - it owns the root/targets keys and creates // the delegation to which it doesn't have the key (so server snapshot // signing would be required) ownerRepo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(ownerRepo.baseDir) // this is a user, or otherwise a repo that only has access to the delegation // key so it can publish targets to the delegated role delgRepo := newRepoToTestRepo(t, ownerRepo) defer os.RemoveAll(delgRepo.baseDir) // create a key on the delegated repo aKey, err := delgRepo.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") // owner creates delegation, adds the delegated key to it, and publishes it assert.NoError(t, ownerRepo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}), "error creating delegation") assert.NoError(t, ownerRepo.Publish()) // delegated repo can now publish to delegated role addTarget(t, delgRepo, "v1", "../fixtures/root-ca.crt", "targets/a") assert.NoError(t, delgRepo.Publish()) // owner removes delegation assert.NoError(t, ownerRepo.RemoveDelegation("targets/a")) assert.NoError(t, ownerRepo.Publish()) // delegated repo can now no longer publish to delegated role addTarget(t, delgRepo, "v2", "../fixtures/root-ca.crt", "targets/a") assert.Error(t, delgRepo.Publish()) } // If the delegation data is corrupt or unreadable, it doesn't matter because // all the delegation information is just re-downloaded. When bootstrapping // the repository from disk, we just don't load the data from disk because // there should not be anything there yet. func TestPublishSucceedsDespiteDelegationCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) delgKey, err := repo.CryptoService.Create("targets/a", data.ECDSAKey) assert.NoError(t, err, "error creating delegation key") assert.NoError(t, repo.AddDelegation("targets/a", 1, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") testPublishBadMetadata(t, "targets/a", repo, false, true) // publish again, now that it has already been published, and again there // is no error. testPublishBadMetadata(t, "targets/a", repo, true, true) } // Rotate invalid roles, or attempt to delegate target signing to the server func TestRotateKeyInvalidRole(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) // the equivalent of: (root, true), (root, false), (timestamp, true), // (timestamp, false), (targets, true) for role := range data.ValidRoles { if role == data.CanonicalSnapshotRole { continue } for _, serverManagesKey := range []bool{true, false} { if role == data.CanonicalTargetsRole && !serverManagesKey { continue } err := repo.RotateKey(role, serverManagesKey) assert.Error(t, err, "Rotating a %s key with server-managing the key as %v should fail", role, serverManagesKey) } } } // Rotates the keys. After the rotation, downloading the latest metadata // and assert that the keys have changed func assertRotationSuccessful(t *testing.T, repo1 *NotaryRepository, keysToRotate map[string]bool, alreadyPublished bool) { // Create 2 new repos: 1 will download repo data before the publish, // and one only downloads after the publish. This reflects a client // that has some previous trust data (but is not the publisher), and a // completely new client being able to read the rotated trust data. repo2 := newRepoToTestRepo(t, repo1) defer os.RemoveAll(repo2.baseDir) repos := []*NotaryRepository{repo1, repo2} if alreadyPublished { repo3 := newRepoToTestRepo(t, repo1) defer os.RemoveAll(repo2.baseDir) // force a pull on repo3 _, err := repo3.GetTargetByName("latest") assert.NoError(t, err) repos = append(repos, repo3) } oldKeyIDs := make(map[string][]string) for role := range keysToRotate { keyIDs := repo1.tufRepo.Root.Signed.Roles[role].KeyIDs oldKeyIDs[role] = keyIDs } // Do rotation for role, serverManaged := range keysToRotate { assert.NoError(t, repo1.RotateKey(role, serverManaged)) } // Publish err := repo1.Publish() assert.NoError(t, err) // Download data from remote and check that keys have changed for _, repo := range repos { _, err := repo.GetTargetByName("latest") // force a pull assert.NoError(t, err) for role, isRemoteKey := range keysToRotate { keyIDs := repo.tufRepo.Root.Signed.Roles[role].KeyIDs assert.Len(t, keyIDs, 1) // the new key is not the same as any of the old keys, and the // old keys have been removed not just from the TUF file, but // from the cryptoservice for _, oldKeyID := range oldKeyIDs[role] { assert.NotEqual(t, oldKeyID, keyIDs[0]) _, _, err := repo.CryptoService.GetPrivateKey(oldKeyID) assert.Error(t, err) } // On the old repo, the new key is present in the cryptoservice, or // not present if remote. On the new repo, no keys are ever in the // cryptoservice key, _, err := repo.CryptoService.GetPrivateKey(keyIDs[0]) if repo != repo1 || isRemoteKey { assert.Error(t, err) assert.Nil(t, key) } else { assert.NoError(t, err) assert.NotNil(t, key) } } // Confirm changelist dir empty (on repo1, it should be empty after // after publishing changes, on repo2, there should never have been // any changelists) changes := getChanges(t, repo) assert.Len(t, changes, 0, "wrong number of changelist files found") } } // Initialize repo to have the server sign snapshots (remote snapshot key) // Without downloading a server-signed snapshot file, rotate keys so that // snapshots are locally signed (local snapshot key) // Assert that we can publish. func TestRotateBeforePublishFromRemoteKeyToLocalKey(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(repo.baseDir) // Adding a target will allow us to confirm the repository is still valid // after rotating the keys. addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") assertRotationSuccessful(t, repo, map[string]bool{ data.CanonicalTargetsRole: false, data.CanonicalSnapshotRole: false}, false) } // Initialize a repo, locally signed snapshots // Publish some content (so that the server has a root.json), and download root.json // Rotate keys // Download the latest metadata and assert that the keys have changed. func TestRotateKeyAfterPublishNoServerManagementChange(t *testing.T) { // rotate a single target key testRotateKeySuccess(t, false, map[string]bool{data.CanonicalTargetsRole: false}) testRotateKeySuccess(t, false, map[string]bool{data.CanonicalSnapshotRole: false}) // rotate two at once before publishing testRotateKeySuccess(t, false, map[string]bool{ data.CanonicalSnapshotRole: false, data.CanonicalTargetsRole: false}) } // Tests rotating keys when there's a change from locally managed keys to // remotely managed keys and vice versa // Before rotating, publish some content (so that the server has a root.json), // and download root.json func TestRotateKeyAfterPublishServerManagementChange(t *testing.T) { // delegate snapshot key management to the server testRotateKeySuccess(t, false, map[string]bool{ data.CanonicalSnapshotRole: true, data.CanonicalTargetsRole: false, }) // reclaim snapshot key management from the server testRotateKeySuccess(t, true, map[string]bool{ data.CanonicalSnapshotRole: false, data.CanonicalTargetsRole: false, }) } func testRotateKeySuccess(t *testing.T, serverManagesSnapshotInit bool, keysToRotate map[string]bool) { ts := fullTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, serverManagesSnapshotInit) defer os.RemoveAll(repo.baseDir) // Adding a target will allow us to confirm the repository is still valid after // rotating the keys. addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") // Publish assert.NoError(t, repo.Publish()) // Get root.json and capture targets + snapshot key IDs repo.GetTargetByName("latest") // force a pull assertRotationSuccessful(t, repo, keysToRotate, true) } // If there is no local cache, notary operations return the remote error code func TestRemoteServerUnavailableNoLocalCache(t *testing.T) { tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) ts := errorTestServer(t, 500) defer ts.Close() repo, err := NewNotaryRepository(tempBaseDir, "docker.com/notary", ts.URL, http.DefaultTransport, passphraseRetriever) assert.NoError(t, err, "error creating repo: %s", err) _, err = repo.ListTargets(data.CanonicalTargetsRole) assert.Error(t, err) assert.IsType(t, store.ErrServerUnavailable{}, err) _, err = repo.GetTargetByName("targetName") assert.Error(t, err) assert.IsType(t, store.ErrServerUnavailable{}, err) err = repo.Publish() assert.Error(t, err) assert.IsType(t, store.ErrServerUnavailable{}, err) } // AddDelegation creates a valid changefile (rejects invalid delegation names, // but does not check the delegation hierarchy). When applied, the change adds // a new delegation role with the correct keys. func TestAddDelegationChangefileValid(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(repo.baseDir) targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) assert.NotEmpty(t, targetKeyIds) targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0]) assert.NotNil(t, targetPubKey) err := repo.AddDelegation("root", 1, []data.PublicKey{targetPubKey}, []string{""}) assert.Error(t, err) assert.IsType(t, data.ErrInvalidRole{}, err) assert.Empty(t, getChanges(t, repo)) // to show that adding does not care about the hierarchy err = repo.AddDelegation("targets/a/b/c", 1, []data.PublicKey{targetPubKey}, []string{""}) assert.NoError(t, err) // ensure that the changefiles is correct changes := getChanges(t, repo) assert.Len(t, changes, 1) assert.Equal(t, changelist.ActionCreate, changes[0].Action()) assert.Equal(t, "targets/a/b/c", changes[0].Scope()) assert.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type()) assert.Equal(t, "", changes[0].Path()) assert.NotEmpty(t, changes[0].Content()) } // The changefile produced by AddDelegation, when applied, actually adds // the delegation to the repo (assuming the delegation hierarchy is correct - // tests for change application validation are in helpers_test.go) func TestAddDelegationChangefileApplicable(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(repo.baseDir) targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) assert.NotEmpty(t, targetKeyIds) targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0]) assert.NotNil(t, targetPubKey) // this hierarchy has to be right to be applied err := repo.AddDelegation("targets/a", 1, []data.PublicKey{targetPubKey}, []string{""}) assert.NoError(t, err) changes := getChanges(t, repo) assert.Len(t, changes, 1) // ensure that it can be applied correctly err = applyTargetsChange(repo.tufRepo, changes[0]) assert.NoError(t, err) targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole] assert.Len(t, targetRole.Signed.Delegations.Roles, 1) assert.Len(t, targetRole.Signed.Delegations.Keys, 1) _, ok := targetRole.Signed.Delegations.Keys[targetPubKey.ID()] assert.True(t, ok) newDelegationRole := targetRole.Signed.Delegations.Roles[0] assert.Len(t, newDelegationRole.KeyIDs, 1) assert.Equal(t, targetPubKey.ID(), newDelegationRole.KeyIDs[0]) assert.Equal(t, "targets/a", newDelegationRole.Name) } // TestAddDelegationErrorWritingChanges expects errors writing a change to file // to be propagated. func TestAddDelegationErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *NotaryRepository) error { targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole) assert.NotEmpty(t, targetKeyIds) targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0]) assert.NotNil(t, targetPubKey) return repo.AddDelegation("targets/a", 1, []data.PublicKey{targetPubKey}, []string{""}) }) } // RemoveDelegation rejects attempts to remove invalidly-named delegations, // but otherwise does not validate the name of the delegation to remove. This // test ensures that the changefile generated by RemoveDelegation is correct. func TestRemoveDelegationChangefileValid(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(repo.baseDir) rootPubKey := repo.CryptoService.GetKey(rootKeyID) assert.NotNil(t, rootPubKey) err := repo.RemoveDelegation("root") assert.Error(t, err) assert.IsType(t, data.ErrInvalidRole{}, err) assert.Empty(t, getChanges(t, repo)) // to demonstrate that so long as the delegation name is valid, the // existence of the delegation doesn't matter assert.NoError(t, repo.RemoveDelegation("targets/a/b/c")) // ensure that the changefile is correct changes := getChanges(t, repo) assert.Len(t, changes, 1) assert.Equal(t, changelist.ActionDelete, changes[0].Action()) assert.Equal(t, "targets/a/b/c", changes[0].Scope()) assert.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type()) assert.Equal(t, "", changes[0].Path()) assert.Empty(t, changes[0].Content()) } // The changefile produced by RemoveDelegation, when applied, actually removes // the delegation from the repo (assuming the repo exists - tests for // change application validation are in helpers_test.go) func TestRemoveDelegationChangefileApplicable(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(repo.baseDir) rootPubKey := repo.CryptoService.GetKey(rootKeyID) assert.NotNil(t, rootPubKey) // add a delegation first so it can be removed assert.NoError(t, repo.AddDelegation("targets/a", 1, []data.PublicKey{rootPubKey}, []string{""})) changes := getChanges(t, repo) assert.Len(t, changes, 1) assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[0])) targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole] assert.Len(t, targetRole.Signed.Delegations.Roles, 1) assert.Len(t, targetRole.Signed.Delegations.Keys, 1) // now remove it assert.NoError(t, repo.RemoveDelegation("targets/a")) changes = getChanges(t, repo) assert.Len(t, changes, 2) assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1])) targetRole = repo.tufRepo.Targets[data.CanonicalTargetsRole] assert.Empty(t, targetRole.Signed.Delegations.Roles) assert.Empty(t, targetRole.Signed.Delegations.Keys) } // TestRemoveDelegationErrorWritingChanges expects errors writing a change to // file to be propagated. func TestRemoveDelegationErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *NotaryRepository) error { return repo.RemoveDelegation("targets/a") }) }