From 96daed25f70efb205980a5bc6b133c33d1912255 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Thu, 23 Jun 2016 14:17:51 -0400 Subject: [PATCH] Support passing in root keys on repo initialization Signed-off-by: Evan Cordell --- client/client.go | 26 ++++++---- client/client_test.go | 51 ++++++++++++++----- cmd/notary/integration_test.go | 91 ++++++++++++++++++++++++++++++++++ cmd/notary/keys_test.go | 2 +- cmd/notary/tuf.go | 48 +++++++++++++++--- 5 files changed, 189 insertions(+), 29 deletions(-) diff --git a/client/client.go b/client/client.go index eababee60..688763115 100644 --- a/client/client.go +++ b/client/client.go @@ -173,10 +173,14 @@ func rootCertKey(gun string, privKey data.PrivateKey) (data.PublicKey, error) { // timestamp key and possibly other serverManagedRoles), but the created repository // result is only stored on local disk, not published to the server. To do that, // use r.Publish() eventually. -func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error { - privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID) - if err != nil { - return err +func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...string) error { + privKeys := []data.PrivateKey{} + for _, keyID := range rootKeyIDs { + privKey, _, err := r.CryptoService.GetPrivateKey(keyID) + if err != nil { + return err + } + privKeys = append(privKeys, privKey) } // currently we only support server managing timestamps and snapshots, and @@ -206,16 +210,20 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st } } - rootKey, err := rootCertKey(r.gun, privKey) - if err != nil { - return err + rootKeys := []data.PublicKey{} + for _, privKey := range privKeys { + rootKey, err := rootCertKey(r.gun, privKey) + if err != nil { + return err + } + rootKeys = append(rootKeys, rootKey) } var ( rootRole = data.NewBaseRole( data.CanonicalRootRole, notary.MinThreshold, - rootKey, + rootKeys..., ) timestampRole data.BaseRole snapshotRole data.BaseRole @@ -271,7 +279,7 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st r.tufRepo = tuf.NewRepo(r.CryptoService) - err = r.tufRepo.InitRoot( + err := r.tufRepo.InitRoot( rootRole, timestampRole, snapshotRole, diff --git a/client/client_test.go b/client/client_test.go index 6daffaafa..810d912ed 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -167,7 +167,7 @@ func initializeRepo(t *testing.T, rootType, gun, url string, repo, rec, rootPubKeyID := createRepoAndKey(t, rootType, tempBaseDir, gun, url) - err = repo.Initialize(rootPubKeyID, serverManagedRoles...) + err = repo.Initialize([]string{rootPubKeyID}, serverManagedRoles...) if err != nil { os.RemoveAll(tempBaseDir) } @@ -241,7 +241,7 @@ func TestInitRepositoryManagedRolesIncludingRoot(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") - err = repo.Initialize(rootPubKeyID, data.CanonicalRootRole) + err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalRootRole) require.Error(t, err) require.IsType(t, ErrInvalidRemoteRole{}, err) // Just testing the error message here in this one case @@ -261,7 +261,7 @@ func TestInitRepositoryManagedRolesInvalidRole(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") - err = repo.Initialize(rootPubKeyID, "randomrole") + err = repo.Initialize([]string{rootPubKeyID}, "randomrole") require.Error(t, err) require.IsType(t, ErrInvalidRemoteRole{}, err) // no key creation happened @@ -278,7 +278,7 @@ func TestInitRepositoryManagedRolesIncludingTargets(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") - err = repo.Initialize(rootPubKeyID, data.CanonicalTargetsRole) + err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalTargetsRole) require.Error(t, err) require.IsType(t, ErrInvalidRemoteRole{}, err) // no key creation happened @@ -298,12 +298,37 @@ func TestInitRepositoryManagedRolesIncludingTimestamp(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) - err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole) + err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalTimestampRole) require.NoError(t, err) + // generates the target role, the snapshot role rec.requireCreated(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole}) } +func TestInitRepositoryMultipleRootKeys(t *testing.T) { + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err, "failed to create a temporary directory") + defer os.RemoveAll(tempBaseDir) + + ts, _, _ := simpleTestServer(t) + defer ts.Close() + + repo, rec, rootPubKeyID := createRepoAndKey( + t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) + rootPubKey2, err := repo.CryptoService.Create("root", repo.gun, data.ECDSAKey) + require.NoError(t, err, "error generating second root key: %s", err) + + err = repo.Initialize([]string{rootPubKeyID, rootPubKey2.ID()}, data.CanonicalTimestampRole) + require.NoError(t, err) + + // generates the target role, the snapshot role + rec.requireCreated(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole}) + + // has two root keys + require.Len(t, repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs, 2) +} + // Initializing a new repo fails if unable to get the timestamp key, even if // the snapshot key is available func TestInitRepositoryNeedsRemoteTimestampKey(t *testing.T) { @@ -317,7 +342,7 @@ func TestInitRepositoryNeedsRemoteTimestampKey(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) - err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole) + err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalTimestampRole) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) @@ -339,7 +364,7 @@ func TestInitRepositoryNeedsRemoteSnapshotKey(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) - err = repo.Initialize(rootPubKeyID, data.CanonicalSnapshotRole) + err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalSnapshotRole) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) @@ -516,9 +541,9 @@ func testInitRepoSigningKeys(t *testing.T, rootType string, serverManagesSnapsho repo, rec := newRepoToTestRepo(t, repo, false) if serverManagesSnapshot { - err = repo.Initialize(rootPubKeyID, data.CanonicalSnapshotRole) + err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalSnapshotRole) } else { - err = repo.Initialize(rootPubKeyID) + err = repo.Initialize([]string{rootPubKeyID}) } require.NoError(t, err, "error initializing repository") @@ -563,7 +588,7 @@ func testInitRepoAttemptsExceeded(t *testing.T, rootType string) { // private key unlocking we need a new repo instance. repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) - err = repo.Initialize(rootPubKey.ID()) + err = repo.Initialize([]string{rootPubKey.ID()}) require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error()) } @@ -600,7 +625,7 @@ func testInitRepoPasswordInvalid(t *testing.T, rootType string) { // private key unlocking we need a new repo instance. repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) - err = repo.Initialize(rootPubKey.ID()) + err = repo.Initialize([]string{rootPubKey.ID()}) require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error()) } @@ -1650,7 +1675,7 @@ func TestPublishUninitializedRepo(t *testing.T) { rootPubKey, err := repo.CryptoService.Create("root", repo.gun, data.ECDSAKey) require.NoError(t, err, "error generating root key: %s", err) - require.NoError(t, repo.Initialize(rootPubKey.ID())) + require.NoError(t, repo.Initialize([]string{rootPubKey.ID()})) // now metadata is created requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true) @@ -2011,7 +2036,7 @@ func TestPublishSnapshotLocalKeysCreatedFirst(t *testing.T) { repo.CryptoService = cannotCreateKeys{CryptoService: cs} - err = repo.Initialize(rootPubKey.ID(), data.CanonicalSnapshotRole) + err = repo.Initialize([]string{rootPubKey.ID()}, data.CanonicalSnapshotRole) require.Error(t, err) require.Contains(t, err.Error(), "Oh no I cannot create keys") require.False(t, requestMade) diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index 68271cc7b..1ffb1097e 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -81,6 +81,97 @@ func setupServer() *httptest.Server { return httptest.NewServer(setupServerHandler(storage.NewMemStorage())) } +// Initializes a repo with existing key +func TestInitWithRootKey(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()) + + // -- tests -- + + // create encrypted root key + privKey, err := trustmanager.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + encryptedPEMPrivKey, err := trustmanager.EncryptPrivateKey(privKey, data.CanonicalRootRole, testPassphrase) + require.NoError(t, err) + encryptedPEMKeyFilename := filepath.Join(tempDir, "encrypted_key.key") + err = ioutil.WriteFile(encryptedPEMKeyFilename, encryptedPEMPrivKey, 0644) + require.NoError(t, err) + + // init repo + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun", "--rootkey", encryptedPEMKeyFilename) + require.NoError(t, err) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + require.NoError(t, err) + + // check that the root key used for init is the one listed as root key + output, err := runCommand(t, tempDir, "key", "list") + require.NoError(t, err) + require.True(t, strings.Contains(output, data.PublicKeyFromPrivate(privKey).ID())) + + // check error if file doesn't exist + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", "bad_file") + require.Error(t, err, "Init with nonexistent key file should error") + + // check error if file is invalid format + badKeyFilename := filepath.Join(tempDir, "bad_key.key") + nonPEMKey := []byte("thisisnotapemkey") + err = ioutil.WriteFile(badKeyFilename, nonPEMKey, 0644) + require.NoError(t, err) + + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", badKeyFilename) + require.Error(t, err, "Init with non-PEM key should error") + + // check error if unencrypted PEM used + unencryptedPrivKey, err := trustmanager.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + unencryptedPEMPrivKey, err := trustmanager.KeyToPEM(unencryptedPrivKey, data.CanonicalRootRole) + require.NoError(t, err) + unencryptedPEMKeyFilename := filepath.Join(tempDir, "unencrypted_key.key") + err = ioutil.WriteFile(unencryptedPEMKeyFilename, unencryptedPEMPrivKey, 0644) + require.NoError(t, err) + + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", unencryptedPEMKeyFilename) + require.Error(t, err, "Init with unencrypted PEM key should error") + + // check error if invalid password used + // instead of using a new retriever, we create a new key with a different pass + badPassPrivKey, err := trustmanager.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + badPassPEMPrivKey, err := trustmanager.EncryptPrivateKey(badPassPrivKey, data.CanonicalRootRole, "bad_pass") + require.NoError(t, err) + badPassPEMKeyFilename := filepath.Join(tempDir, "badpass_key.key") + err = ioutil.WriteFile(badPassPEMKeyFilename, badPassPEMPrivKey, 0644) + require.NoError(t, err) + + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", badPassPEMKeyFilename) + require.Error(t, err, "Init with wrong password should error") + + // check error if wrong role specified + snapshotPrivKey, err := trustmanager.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + snapshotPEMPrivKey, err := trustmanager.KeyToPEM(snapshotPrivKey, data.CanonicalSnapshotRole) + require.NoError(t, err) + snapshotPEMKeyFilename := filepath.Join(tempDir, "snapshot_key.key") + err = ioutil.WriteFile(snapshotPEMKeyFilename, snapshotPEMPrivKey, 0644) + require.NoError(t, err) + + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", snapshotPEMKeyFilename) + require.Error(t, err, "Init with wrong role should error") +} + // Initializes a repo, adds a target, publishes the target, lists the target, // verifies the target, and then removes the target. func TestClientTUFInteraction(t *testing.T) { diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index a232326e3..bc4eae7a0 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -336,7 +336,7 @@ func setUpRepo(t *testing.T, tempBaseDir, gun string, ret notary.PassRetriever) rootPubKey, err := repo.CryptoService.Create("root", "", data.ECDSAKey) require.NoError(t, err, "error generating root key: %s", err) - err = repo.Initialize(rootPubKey.ID()) + err = repo.Initialize([]string{rootPubKey.ID()}) require.NoError(t, err) return ts, repo.CryptoService.ListAllKeys() diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index dc7cedbe1..f55eb72dc 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/hex" "fmt" + "io/ioutil" "net" "net/http" "net/url" @@ -19,6 +20,8 @@ import ( "github.com/docker/go-connections/tlsconfig" "github.com/docker/notary" notaryclient "github.com/docker/notary/client" + "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/utils" @@ -86,9 +89,10 @@ type tufCommander struct { retriever notary.PassRetriever // these are for command line parsing - no need to set - roles []string - sha256 string - sha512 string + roles []string + sha256 string + sha512 string + rootKey string input string output string @@ -96,7 +100,10 @@ type tufCommander struct { } func (t *tufCommander) AddToCommand(cmd *cobra.Command) { - cmd.AddCommand(cmdTUFInitTemplate.ToCommand(t.tufInit)) + cmdTUFInit := cmdTUFInitTemplate.ToCommand(t.tufInit) + cmdTUFInit.Flags().StringVar(&t.rootKey, "rootkey", "", "Root key to initialize the repository with") + cmd.AddCommand(cmdTUFInit) + cmd.AddCommand(cmdTUFStatusTemplate.ToCommand(t.tufStatus)) cmd.AddCommand(cmdTUFPublishTemplate.ToCommand(t.tufPublish)) cmd.AddCommand(cmdTUFLookupTemplate.ToCommand(t.tufLookup)) @@ -268,7 +275,36 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { return err } - rootKeyList := nRepo.CryptoService.ListKeys(data.CanonicalRootRole) + var rootKeyList []string + + if t.rootKey != "" { + keyFile, err := os.Open(t.rootKey) + if err != nil { + return fmt.Errorf("Opening file for import: %v", err) + } + defer keyFile.Close() + + pemBytes, err := ioutil.ReadAll(keyFile) + if err != nil { + return fmt.Errorf("Error reading input file: %v", err) + } + if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil { + return err + } + + privKey, _, err := trustmanager.GetPasswdDecryptBytes(t.retriever, pemBytes, "", data.CanonicalRootRole) + if err != nil { + return err + } + + err = nRepo.CryptoService.AddKey(data.CanonicalRootRole, "", privKey) + if err != nil { + return fmt.Errorf("Error importing key: %v", err) + } + rootKeyList = []string{data.PublicKeyFromPrivate(privKey).ID()} + } else { + rootKeyList = nRepo.CryptoService.ListKeys(data.CanonicalRootRole) + } var rootKeyID string if len(rootKeyList) < 1 { @@ -285,7 +321,7 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { cmd.Printf("Root key found, using: %s\n", rootKeyID) } - if err = nRepo.Initialize(rootKeyID); err != nil { + if err = nRepo.Initialize([]string{rootKeyID}); err != nil { return err } return nil