From df1c425caea6dceed32c01b1cbd12a66e282e04c Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com> Date: Tue, 2 Aug 2016 16:06:23 -0700 Subject: [PATCH 1/2] notary delete CLI command and integration test Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com> --- cmd/notary/integration_test.go | 167 ++++++++++++++++++++++++++++++--- cmd/notary/main_test.go | 1 + cmd/notary/tuf.go | 47 ++++++++++ 3 files changed, 200 insertions(+), 15 deletions(-) diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index 06a6780ae..1745ee868 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -24,10 +24,12 @@ import ( "github.com/Sirupsen/logrus" ctxu "github.com/docker/distribution/context" "github.com/docker/notary" + "github.com/docker/notary/client" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/passphrase" "github.com/docker/notary/server" "github.com/docker/notary/server/storage" + notaryStorage "github.com/docker/notary/storage" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/utils" @@ -245,6 +247,156 @@ func TestClientTUFInteraction(t *testing.T) { require.False(t, strings.Contains(string(output), target)) } +func TestClientDeleteTUFInteraction(t *testing.T) { + // -- setup -- + setUp(t) + + tempDir := tempDirWithConfig(t, "{}") + defer os.RemoveAll(tempDir) + + server := setupServer() + defer server.Close() + + tempFile, err := ioutil.TempFile("", "targetfile") + require.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + // Setup certificate + certFile, err := ioutil.TempFile("", "pemfile") + require.NoError(t, err) + + privKey, err := utils.GenerateECDSAKey(rand.Reader) + startTime := time.Now() + endTime := startTime.AddDate(10, 0, 0) + cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) + require.NoError(t, err) + + _, err = certFile.Write(utils.CertToPEM(cert)) + require.NoError(t, err) + tempFile.Close() + defer os.Remove(certFile.Name()) + + var ( + output string + target = "helloIamanotarytarget" + ) + // -- tests -- + + // init repo + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") + require.NoError(t, err) + + // add a target + _, err = runCommand(t, tempDir, "add", "gun", target, tempFile.Name()) + require.NoError(t, err) + + // check status - see target + output, err = runCommand(t, tempDir, "status", "gun") + require.NoError(t, err) + require.True(t, strings.Contains(output, target)) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + require.NoError(t, err) + + // list repo - see target + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") + require.NoError(t, err) + require.True(t, strings.Contains(string(output), target)) + + // add a delegation and publish + output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", certFile.Name()) + require.NoError(t, err) + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + require.NoError(t, err) + + // list delegations - see role + output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") + require.NoError(t, err) + require.True(t, strings.Contains(string(output), "targets/delegation")) + + // Delete the repo metadata locally, so no need for server URL + output, err = runCommand(t, tempDir, "delete", "gun") + require.NoError(t, err) + assertLocalMetadataForGun(t, tempDir, "gun", false) + + // list repo - see target still because remote data exists + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") + require.NoError(t, err) + require.True(t, strings.Contains(string(output), target)) + + // list delegations - see role because remote data still exists + output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") + require.NoError(t, err) + require.True(t, strings.Contains(string(output), "targets/delegation")) + + // Trying to delete the repo with the remote flag fails if it's given a badly formed URL + output, err = runCommand(t, tempDir, "-s", "//invalidURLType", "delete", "gun", "--remote") + require.Error(t, err) + // since the connection fails to parse the URL before we can delete anything, local data should exist + assertLocalMetadataForGun(t, tempDir, "gun", true) + + // Trying to delete the repo with the remote flag fails if it's given a well-formed URL that doesn't point to a server + output, err = runCommand(t, tempDir, "-s", "https://invalid-server", "delete", "gun", "--remote") + require.Error(t, err) + require.IsType(t, notaryStorage.ErrOffline{}, err) + // In this case, local notary metadata does not exist since local deletion operates first if we have a valid transport + assertLocalMetadataForGun(t, tempDir, "gun", false) + + // Delete the repo remotely and locally, pointing to the correct server + output, err = runCommand(t, tempDir, "-s", server.URL, "delete", "gun", "--remote") + require.NoError(t, err) + assertLocalMetadataForGun(t, tempDir, "gun", false) + _, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") + require.Error(t, err) + require.IsType(t, client.ErrRepositoryNotExist{}, err) + + // Silent success on extraneous deletes + output, err = runCommand(t, tempDir, "-s", server.URL, "delete", "gun", "--remote") + require.NoError(t, err) + assertLocalMetadataForGun(t, tempDir, "gun", false) + _, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") + require.Error(t, err) + require.IsType(t, client.ErrRepositoryNotExist{}, err) + + // Now check that we can re-publish the same repo + // init repo + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") + require.NoError(t, err) + + // add a target + _, err = runCommand(t, tempDir, "add", "gun", target, tempFile.Name()) + require.NoError(t, err) + + // check status - see target + output, err = runCommand(t, tempDir, "status", "gun") + require.NoError(t, err) + require.True(t, strings.Contains(output, target)) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + require.NoError(t, err) + + // list repo - see target + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") + require.NoError(t, err) + require.True(t, strings.Contains(string(output), target)) +} + +func assertLocalMetadataForGun(t *testing.T, configDir, gun string, shouldExist bool) { + for _, role := range data.BaseRoles { + fileInfo, err := os.Stat(filepath.Join(configDir, "tuf", gun, "metadata", role+".json")) + if shouldExist { + require.NoError(t, err) + require.NotNil(t, fileInfo) + } else { + require.Error(t, err) + require.Nil(t, fileInfo) + } + } +} + // Initializes a repo, adds a target, publishes the target by hash, lists the target, // verifies the target, and then removes the target. func TestClientTUFAddByHashInteraction(t *testing.T) { @@ -1171,21 +1323,6 @@ func TestClientKeyGenerationRotation(t *testing.T) { require.True(t, strings.Contains(string(output), target)) } -// Helper method to get the subdirectory for TUF keys -func getKeySubdir(role, gun string) string { - subdir := notary.PrivDir - switch role { - case data.CanonicalRootRole: - return filepath.Join(subdir, notary.RootKeysSubdir) - case data.CanonicalTargetsRole: - return filepath.Join(subdir, notary.NonRootKeysSubdir, gun) - case data.CanonicalSnapshotRole: - return filepath.Join(subdir, notary.NonRootKeysSubdir, gun) - default: - return filepath.Join(subdir, notary.NonRootKeysSubdir) - } -} - // Tests default root key generation func TestDefaultRootKeyGeneration(t *testing.T) { // -- setup -- diff --git a/cmd/notary/main_test.go b/cmd/notary/main_test.go index 42e768e46..1d44a6be9 100644 --- a/cmd/notary/main_test.go +++ b/cmd/notary/main_test.go @@ -166,6 +166,7 @@ var exampleValidCommands = []string{ "delegation add repo targets/releases path/to/pem/file.pem", "delegation remove repo targets/releases", "witness gun targets/releases", + "delete repo", } // config parsing bugs are propagated in all commands diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 671f73227..72da3f20a 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -89,6 +89,12 @@ var cmdWitnessTemplate = usageTemplate{ Long: "Marks roles to be re-signed the next time they're published. Currently will always bump version and expiry for role. N.B. behaviour may change when thresholding is introduced.", } +var cmdTUFDeleteTemplate = usageTemplate{ + Use: "delete [ GUN ]", + Short: "Deletes all content for a trusted collection", + Long: "Deletes all local content for a trusted collection identified by the Globally Unique Name. Remote data can also be deleted with an additional flag.", +} + type tufCommander struct { // these need to be set configGetter func() (*viper.Viper, error) @@ -107,6 +113,8 @@ type tufCommander struct { deleteIdx []int reset bool archiveChangelist string + + deleteRemote bool } func (t *tufCommander) AddToCommand(cmd *cobra.Command) { @@ -149,6 +157,10 @@ func (t *tufCommander) AddToCommand(cmd *cobra.Command) { cmdWitness := cmdWitnessTemplate.ToCommand(t.tufWitness) cmd.AddCommand(cmdWitness) + + cmdTUFDeleteGUN := cmdTUFDeleteTemplate.ToCommand(t.tufDeleteGUN) + cmdTUFDeleteGUN.Flags().BoolVar(&t.deleteRemote, "remote", false, "Delete remote data for GUN in addition to local cache") + cmd.AddCommand(cmdTUFDeleteGUN) } func (t *tufCommander) tufWitness(cmd *cobra.Command, args []string) error { @@ -301,6 +313,41 @@ func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error { return nil } +func (t *tufCommander) tufDeleteGUN(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + cmd.Usage() + return fmt.Errorf("Must specify a GUN") + } + config, err := t.configGetter() + if err != nil { + return err + } + + gun := args[0] + + trustPin, err := getTrustPinning(config) + + // Only initialize a roundtripper if we get the remote flag + var rt http.RoundTripper + if t.deleteRemote { + rt, err = getTransport(config, gun, admin) + if err != nil { + return err + } + } + + nRepo, err := notaryclient.NewNotaryRepository( + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) + + if err != nil { + return err + } + + cmd.Printf("Deleting trust data for repository %s.\n", gun) + + return nRepo.DeleteTrustData(t.deleteRemote) +} + func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() From 2beb1fdb698d09cc8ed04fd3f4b31cfc06570389 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com> Date: Wed, 3 Aug 2016 11:04:14 -0700 Subject: [PATCH 2/2] Factor out cert generation to helper Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com> --- cmd/notary/integration_test.go | 118 ++++++++++++--------------------- 1 file changed, 41 insertions(+), 77 deletions(-) diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index 1745ee868..1e04dd345 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -11,7 +11,9 @@ import ( "crypto/rand" "crypto/sha256" "crypto/sha512" + "crypto/x509" "encoding/hex" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -265,16 +267,8 @@ func TestClientDeleteTUFInteraction(t *testing.T) { // Setup certificate certFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) - - privKey, err := utils.GenerateECDSAKey(rand.Reader) - startTime := time.Now() - endTime := startTime.AddDate(10, 0, 0) - cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) - require.NoError(t, err) - + cert, _, _ := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = certFile.Write(utils.CertToPEM(cert)) - require.NoError(t, err) - tempFile.Close() defer os.Remove(certFile.Name()) var ( @@ -557,23 +551,12 @@ func TestClientDelegationsInteraction(t *testing.T) { // Setup certificate tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) - - privKey, err := utils.GenerateECDSAKey(rand.Reader) - startTime := time.Now() - endTime := startTime.AddDate(10, 0, 0) - cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) - require.NoError(t, err) - + cert, _, keyID := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) - rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) - parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes) - keyID, err := utils.CanonicalKeyID(parsedPubKey) - require.NoError(t, err) - var output string // -- tests -- @@ -646,23 +629,12 @@ func TestClientDelegationsInteraction(t *testing.T) { tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) - privKey, err = utils.GenerateECDSAKey(rand.Reader) - startTime = time.Now() - endTime = startTime.AddDate(10, 0, 0) - cert, err = cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) - require.NoError(t, err) - - _, err = tempFile2.Write(utils.CertToPEM(cert)) - require.NoError(t, err) + cert2, _, keyID2 := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) + _, err = tempFile2.Write(utils.CertToPEM(cert2)) require.NoError(t, err) tempFile2.Close() defer os.Remove(tempFile2.Name()) - rawPubBytes2, _ := ioutil.ReadFile(tempFile2.Name()) - parsedPubKey2, _ := utils.ParsePEMPublicKey(rawPubBytes2) - keyID2, err := utils.CanonicalKeyID(parsedPubKey2) - require.NoError(t, err) - // add to the delegation by specifying the same role, this time add a scoped path output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile2.Name(), "--paths", "path") require.NoError(t, err) @@ -929,26 +901,15 @@ func TestClientDelegationsPublishing(t *testing.T) { // Setup certificate for delegation role tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) - - privKey, err := utils.GenerateRSAKey(rand.Reader, 2048) - require.NoError(t, err) - privKeyBytesNoRole, err := utils.KeyToPEM(privKey, "") - require.NoError(t, err) - privKeyBytesWithRole, err := utils.KeyToPEM(privKey, "user") - require.NoError(t, err) - startTime := time.Now() - endTime := startTime.AddDate(10, 0, 0) - cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) - require.NoError(t, err) - + cert, privKey, canonicalKeyID := generateCertPrivKeyPair(t, "gun", data.RSAKey) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) - rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) - parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes) - canonicalKeyID, err := utils.CanonicalKeyID(parsedPubKey) + privKeyBytesNoRole, err := utils.KeyToPEM(privKey, "") + require.NoError(t, err) + privKeyBytesWithRole, err := utils.KeyToPEM(privKey, "user") require.NoError(t, err) // Set up targets for publishing @@ -1103,24 +1064,12 @@ func TestClientDelegationsPublishing(t *testing.T) { // Setup another certificate tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) - - privKey, err = utils.GenerateECDSAKey(rand.Reader) - startTime = time.Now() - endTime = startTime.AddDate(10, 0, 0) - cert, err = cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) - require.NoError(t, err) - - _, err = tempFile2.Write(utils.CertToPEM(cert)) - require.NoError(t, err) + cert2, _, keyID2 := generateCertPrivKeyPair(t, "gun", data.RSAKey) + _, err = tempFile2.Write(utils.CertToPEM(cert2)) require.NoError(t, err) tempFile2.Close() defer os.Remove(tempFile2.Name()) - rawPubBytes2, _ := ioutil.ReadFile(tempFile2.Name()) - parsedPubKey2, _ := utils.ParsePEMPublicKey(rawPubBytes2) - keyID2, err := utils.CanonicalKeyID(parsedPubKey2) - require.NoError(t, err) - // add a nested delegation under this releases role output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/releases/nested", tempFile2.Name(), "--paths", "nested/path") require.NoError(t, err) @@ -1525,29 +1474,21 @@ func TestWitness(t *testing.T) { server := setupServer() defer server.Close() - startTime := time.Now() - endTime := startTime.AddDate(10, 0, 0) - // Setup certificates tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) - privKey, err := utils.GenerateECDSAKey(rand.Reader) - cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) - require.NoError(t, err) + + cert, privKey, keyID := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) - rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) - parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes) - keyID, err := utils.CanonicalKeyID(parsedPubKey) - require.NoError(t, err) - tempFile2, err := ioutil.TempFile("", "pemfile") - require.NoError(t, err) - privKey2, err := utils.GenerateECDSAKey(rand.Reader) - cert2, err := cryptoservice.GenerateCertificate(privKey2, "gun", startTime, endTime) + // Setup another certificate + tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) + + cert2, privKey2, _ := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile2.Write(utils.CertToPEM(cert2)) require.NoError(t, err) tempFile2.Close() @@ -1663,3 +1604,26 @@ func TestWitness(t *testing.T) { require.Error(t, err) } } + +func generateCertPrivKeyPair(t *testing.T, gun, keyAlgorithm string) (*x509.Certificate, data.PrivateKey, string) { + // Setup certificate + var privKey data.PrivateKey + var err error + switch keyAlgorithm { + case data.ECDSAKey: + privKey, err = utils.GenerateECDSAKey(rand.Reader) + case data.RSAKey: + privKey, err = utils.GenerateRSAKey(rand.Reader, 4096) + default: + err = fmt.Errorf("invalid key algorithm provided: %s", keyAlgorithm) + } + require.NoError(t, err) + startTime := time.Now() + endTime := startTime.AddDate(10, 0, 0) + cert, err := cryptoservice.GenerateCertificate(privKey, gun, startTime, endTime) + require.NoError(t, err) + parsedPubKey, _ := utils.ParsePEMPublicKey(utils.CertToPEM(cert)) + keyID, err := utils.CanonicalKeyID(parsedPubKey) + require.NoError(t, err) + return cert, privKey, keyID +}