From 414ee1d7d90a2d306cb0143881771ec42ae52faa Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Thu, 23 Jun 2016 14:17:51 -0400 Subject: [PATCH 1/2] Support passing in root keys on repo initialization Signed-off-by: Evan Cordell --- client/client.go | 26 +++++++++++------ client/client_test.go | 46 ++++++++++++++++++++--------- cmd/notary/integration_test.go | 40 +++++++++++++++++++++++++ cmd/notary/keys_test.go | 2 +- cmd/notary/tuf.go | 53 ++++++++++++++++++++++++++++++---- 5 files changed, 139 insertions(+), 28 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..051201b00 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,7 +298,27 @@ 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}) @@ -317,7 +337,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 +359,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 +536,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 +583,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 +620,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 +1670,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 +2031,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..2ec727943 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -81,6 +81,46 @@ func setupServer() *httptest.Server { return httptest.NewServer(setupServerHandler(storage.NewMemStorage())) } +// Initializes a repo with multiple existing keys +func TestInitWithMultipleRootKeys(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 -- + + // generate root key produces a single root key and no other keys + _, err = runCommand(t, tempDir, "key", "generate", data.ECDSAKey) + require.NoError(t, err) + assertNumKeys(t, tempDir, 1, 0, true) + _, err = runCommand(t, tempDir, "key", "generate", data.ECDSAKey) + require.NoError(t, err) + rootIDs, _ := assertNumKeys(t, tempDir, 2, 0, true) + var rootFiles []string + for _, rID := range rootIDs { + rootFiles = append(rootFiles, filepath.Join( + tempDir, "private", "root_keys", rID+".key")) + } + + // init repo + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun", "--rootkeys", strings.Join(rootFiles, ",")) + require.NoError(t, err) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + require.NoError(t, err) +} + // 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..49bcfb2ac 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 + rootKeys []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().StringSliceVar(&t.rootKeys, "rootkeys", nil, "Root keys 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,6 +275,42 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { return err } + if len(t.rootKeys) > 0 { + for _, keyFilename := range t.rootKeys { + keyFile, err := os.Open(keyFilename) + 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.ParsePEMPrivateKey(pemBytes, "") + if err != nil { + privKey, _, err = trustmanager.GetPasswdDecryptBytes(t.retriever, pemBytes, "", "imported root") + if err != nil { + return err + } + } + err = nRepo.CryptoService.AddKey(data.CanonicalRootRole, "", privKey) + if err != nil { + return fmt.Errorf("Error importing key: %v", err) + } + } + // if keys were explicitly passed in, we assume all of them should be added + // to the root role + if err = nRepo.Initialize(nRepo.CryptoService.ListKeys(data.CanonicalRootRole)); err != nil { + return err + } + return nil + } + rootKeyList := nRepo.CryptoService.ListKeys(data.CanonicalRootRole) var rootKeyID string @@ -285,7 +328,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 From 31e428f56a681a6a0cba88e187d452cfea599d12 Mon Sep 17 00:00:00 2001 From: David Wake Date: Fri, 24 Jun 2016 05:13:08 -0700 Subject: [PATCH 2/2] Import only one root key, and add ability to import root cert Addresses https://github.com/docker/notary/issues/731 Signed-off-by: David Wake --- client/client.go | 55 +++++++++++++++------ client/client_test.go | 89 +++++++++++++++++++++------------- cmd/notary/integration_test.go | 40 --------------- cmd/notary/keys_test.go | 2 +- cmd/notary/tuf.go | 83 ++++++++++++++++++++----------- tuf/signed/verify.go | 29 +++++++++++ tuf/signed/verify_test.go | 19 ++++++++ 7 files changed, 199 insertions(+), 118 deletions(-) diff --git a/client/client.go b/client/client.go index 688763115..f5bfc7243 100644 --- a/client/client.go +++ b/client/client.go @@ -168,19 +168,40 @@ func rootCertKey(gun string, privKey data.PrivateKey) (data.PublicKey, error) { return x509PublicKey, nil } -// Initialize creates a new repository by using rootKey as the root Key for the +// Initialize creates a new repository by using rootKeyID to identify the root Key for the // TUF repository. The server must be reachable (and is asked to generate a // 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(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) +func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error { + return r.InitializeWithPubKey(rootKeyID, (data.PublicKey)(nil), serverManagedRoles...) +} + +// KeyPairVerifier is used to verify that a private key and public key form a matching keypair +type KeyPairVerifier func(privKey data.PrivateKey, pubKey data.PublicKey) error + +func defaultKeyPairVerifier() KeyPairVerifier { + return signed.VerifyPublicKeyMatchesPrivateKey +} + +// InitializeWithPubKey creates a new repository by using rootPrivKeyID to identify the root private key for the +// TUF repository. +// If rootPubKey is non-nil, it must form a valid keypair together with the private key identified by rootPrivKeyID, +// otherwise an error is thrown. The repository is then initialized with the given root keypair. +// If rootPubKey is nil, then the root public key is generated automatically. +// The server must be reachable (and is asked to generate a +// 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) InitializeWithPubKey(rootPrivKeyID string, rootPubKey data.PublicKey, serverManagedRoles ...string) error { + return r.InitializeWithPubKeyAndVerifier(rootPrivKeyID, rootPubKey, defaultKeyPairVerifier(), serverManagedRoles...) +} + +// InitializeWithPubKeyAndVerifier allows the keyPairVerifier to be overridden for testing +func (r *NotaryRepository) InitializeWithPubKeyAndVerifier(rootPrivKeyID string, rootPubKey data.PublicKey, keyPairVerifier KeyPairVerifier, serverManagedRoles ...string) error { + rootPrivKey, _, err := r.CryptoService.GetPrivateKey(rootPrivKeyID) + if err != nil { + return err } // currently we only support server managing timestamps and snapshots, and @@ -210,20 +231,24 @@ func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles .. } } - rootKeys := []data.PublicKey{} - for _, privKey := range privKeys { - rootKey, err := rootCertKey(r.gun, privKey) + if rootPubKey == nil { + logrus.Debug("No root public key specified; generating a new one.") + rootPubKey, err = rootCertKey(r.gun, rootPrivKey) if err != nil { return err } - rootKeys = append(rootKeys, rootKey) + } else { + logrus.Debugf("Verifying user-specified root public key: %s", rootPubKey.ID()) + if err := keyPairVerifier(rootPrivKey, rootPubKey); err != nil { + return err + } } var ( rootRole = data.NewBaseRole( data.CanonicalRootRole, notary.MinThreshold, - rootKeys..., + rootPubKey, ) timestampRole data.BaseRole snapshotRole data.BaseRole @@ -279,7 +304,7 @@ func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles .. 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 051201b00..9fe5434e7 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([]string{rootPubKeyID}, serverManagedRoles...) + err = repo.Initialize(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([]string{rootPubKeyID}, data.CanonicalRootRole) + err = repo.Initialize(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([]string{rootPubKeyID}, "randomrole") + err = repo.Initialize(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([]string{rootPubKeyID}, data.CanonicalTargetsRole) + err = repo.Initialize(rootPubKeyID, data.CanonicalTargetsRole) require.Error(t, err) require.IsType(t, ErrInvalidRemoteRole{}, err) // no key creation happened @@ -298,27 +298,7 @@ func TestInitRepositoryManagedRolesIncludingTimestamp(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) - 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) + err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole) require.NoError(t, err) // generates the target role, the snapshot role rec.requireCreated(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole}) @@ -337,7 +317,7 @@ func TestInitRepositoryNeedsRemoteTimestampKey(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) - err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalTimestampRole) + err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) @@ -359,7 +339,7 @@ func TestInitRepositoryNeedsRemoteSnapshotKey(t *testing.T) { repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) - err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalSnapshotRole) + err = repo.Initialize(rootPubKeyID, data.CanonicalSnapshotRole) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) @@ -536,9 +516,9 @@ func testInitRepoSigningKeys(t *testing.T, rootType string, serverManagesSnapsho repo, rec := newRepoToTestRepo(t, repo, false) if serverManagesSnapshot { - err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalSnapshotRole) + err = repo.Initialize(rootPubKeyID, data.CanonicalSnapshotRole) } else { - err = repo.Initialize([]string{rootPubKeyID}) + err = repo.Initialize(rootPubKeyID) } require.NoError(t, err, "error initializing repository") @@ -583,7 +563,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([]string{rootPubKey.ID()}) + err = repo.Initialize(rootPubKey.ID()) require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error()) } @@ -620,7 +600,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([]string{rootPubKey.ID()}) + err = repo.Initialize(rootPubKey.ID()) require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error()) } @@ -1670,7 +1650,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([]string{rootPubKey.ID()})) + require.NoError(t, repo.Initialize(rootPubKey.ID())) // now metadata is created requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true) @@ -2031,12 +2011,55 @@ func TestPublishSnapshotLocalKeysCreatedFirst(t *testing.T) { repo.CryptoService = cannotCreateKeys{CryptoService: cs} - err = repo.Initialize([]string{rootPubKey.ID()}, data.CanonicalSnapshotRole) + err = repo.Initialize(rootPubKey.ID(), data.CanonicalSnapshotRole) require.Error(t, err) require.Contains(t, err.Error(), "Oh no I cannot create keys") require.False(t, requestMade) } +// If there is an error creating the local keys, no call is made to get a +// remote key. +func TestInitializeWithPublicKeyVerifiesKeypairAndFailsIfKeysDontMatch(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) + + repo, rec, privKeyID := createRepoAndKey( + t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") + require.NotNil(t, rec, "Fuck off") + rootPubKey := repo.CryptoService.GetKey(privKeyID) + require.NotNil(t, rootPubKey, "Couldn't get the root public key") + + keyPairVerifierFail := func(privKey data.PrivateKey, pubKey data.PublicKey) error { + return fmt.Errorf("Private key %s does not match public key %s", privKey.ID(), pubKey.ID()) + } + err = repo.InitializeWithPubKeyAndVerifier(privKeyID, rootPubKey, keyPairVerifierFail, data.CanonicalSnapshotRole) + require.Error(t, err) + require.Equal(t, err.Error(), fmt.Sprintf("Private key %s does not match public key %s", privKeyID, rootPubKey.ID())) +} + +func TestInitializeWithPublicKeyVerifiesKeypairAndSucceedsIfKeysMatch(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, privKeyID := createRepoAndKey( + t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) + require.NotNil(t, rec, "Fuck off") + rootPubKey := repo.CryptoService.GetKey(privKeyID) + require.NotNil(t, rootPubKey, "Couldn't get the root public key") + + keyPairVerifierSucceed := func(privKey data.PrivateKey, pubKey data.PublicKey) error { return nil } + err = repo.InitializeWithPubKeyAndVerifier(privKeyID, rootPubKey, keyPairVerifierSucceed, data.CanonicalSnapshotRole) + require.NoError(t, err) + require.Equal(t, rootPubKey.Public(), repo.tufRepo.Root.Signed.Keys[rootPubKey.ID()].Public()) +} + func createKey(t *testing.T, repo *NotaryRepository, role string, x509 bool) data.PublicKey { key, err := repo.CryptoService.Create(role, repo.gun, data.ECDSAKey) require.NoError(t, err, "error creating key") diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index 2ec727943..68271cc7b 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -81,46 +81,6 @@ func setupServer() *httptest.Server { return httptest.NewServer(setupServerHandler(storage.NewMemStorage())) } -// Initializes a repo with multiple existing keys -func TestInitWithMultipleRootKeys(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 -- - - // generate root key produces a single root key and no other keys - _, err = runCommand(t, tempDir, "key", "generate", data.ECDSAKey) - require.NoError(t, err) - assertNumKeys(t, tempDir, 1, 0, true) - _, err = runCommand(t, tempDir, "key", "generate", data.ECDSAKey) - require.NoError(t, err) - rootIDs, _ := assertNumKeys(t, tempDir, 2, 0, true) - var rootFiles []string - for _, rID := range rootIDs { - rootFiles = append(rootFiles, filepath.Join( - tempDir, "private", "root_keys", rID+".key")) - } - - // init repo - _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun", "--rootkeys", strings.Join(rootFiles, ",")) - require.NoError(t, err) - - // publish repo - _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") - require.NoError(t, err) -} - // 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 bc4eae7a0..a232326e3 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([]string{rootPubKey.ID()}) + err = repo.Initialize(rootPubKey.ID()) require.NoError(t, err) return ts, repo.CryptoService.ListAllKeys() diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 49bcfb2ac..fef8cdb96 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -89,10 +89,12 @@ type tufCommander struct { retriever notary.PassRetriever // these are for command line parsing - no need to set - roles []string - sha256 string - sha512 string - rootKeys []string + roles []string + sha256 string + sha512 string + + rootCert string + rootKey string input string output string @@ -101,7 +103,8 @@ type tufCommander struct { func (t *tufCommander) AddToCommand(cmd *cobra.Command) { cmdTUFInit := cmdTUFInitTemplate.ToCommand(t.tufInit) - cmdTUFInit.Flags().StringSliceVar(&t.rootKeys, "rootkeys", nil, "Root keys to initialize the repository with.") + cmdTUFInit.Flags().StringVar(&t.rootCert, "rootcert", "", "Root cert to initialize the repository with. Must correspond to the private key specified in --rootkey") + cmdTUFInit.Flags().StringVar(&t.rootKey, "rootkey", "", "Root key to initialize the repository with.") cmd.AddCommand(cmdTUFInit) cmd.AddCommand(cmdTUFStatusTemplate.ToCommand(t.tufStatus)) @@ -253,6 +256,10 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { return fmt.Errorf("Must specify a GUN") } + if t.rootCert != "" && t.rootKey == "" { + return fmt.Errorf("--rootCert specified without --rootKey being specified") + } + config, err := t.configGetter() if err != nil { return err @@ -275,38 +282,56 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { return err } - if len(t.rootKeys) > 0 { - for _, keyFilename := range t.rootKeys { - keyFile, err := os.Open(keyFilename) - if err != nil { - return fmt.Errorf("Opening file for import: %v", err) - } - defer keyFile.Close() + 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) + 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.ParsePEMPrivateKey(pemBytes, "") + if err != nil { + privKey, _, err = trustmanager.GetPasswdDecryptBytes(t.retriever, pemBytes, "", "imported root") if err != nil { - return fmt.Errorf("Error reading input file: %v", err) - } - if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil { return err } + } + err = nRepo.CryptoService.AddKey(data.CanonicalRootRole, "", privKey) + if err != nil { + return fmt.Errorf("Error importing key: %v", err) + } - privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") + // if a key was explicitly passed in, we assume it should be added + // to the root role + if t.rootCert == "" { + if err = nRepo.Initialize(privKey.ID()); err != nil { + return err + } + } else { + // Load the user-specified root cert and initialize the repo with the cert and private key + // Read public key bytes from PEM file + pubKeyBytes, err := ioutil.ReadFile(t.rootCert) if err != nil { - privKey, _, err = trustmanager.GetPasswdDecryptBytes(t.retriever, pemBytes, "", "imported root") - if err != nil { - return err - } + return fmt.Errorf("unable to read public key from file: %s", t.rootCert) } - err = nRepo.CryptoService.AddKey(data.CanonicalRootRole, "", privKey) + + // Parse PEM bytes into type PublicKey + pubKey, err := trustmanager.ParsePEMPublicKey(pubKeyBytes) if err != nil { - return fmt.Errorf("Error importing key: %v", err) + return fmt.Errorf("unable to parse valid public key certificate from PEM file %s: %v", t.rootCert, err) + } + + if err = nRepo.InitializeWithPubKey(privKey.ID(), pubKey); err != nil { + return err } - } - // if keys were explicitly passed in, we assume all of them should be added - // to the root role - if err = nRepo.Initialize(nRepo.CryptoService.ListKeys(data.CanonicalRootRole)); err != nil { - return err } return nil } @@ -328,7 +353,7 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { cmd.Printf("Root key found, using: %s\n", rootKeyID) } - if err = nRepo.Initialize([]string{rootKeyID}); err != nil { + if err = nRepo.Initialize(rootKeyID); err != nil { return err } return nil diff --git a/tuf/signed/verify.go b/tuf/signed/verify.go index fadcbd034..fa11168fb 100644 --- a/tuf/signed/verify.go +++ b/tuf/signed/verify.go @@ -1,6 +1,7 @@ package signed import ( + "crypto/rand" "errors" "fmt" "strings" @@ -105,3 +106,31 @@ func VerifySignature(msg []byte, sig data.Signature, pk data.PublicKey) error { } return nil } + +// VerifyPublicKeyMatchesPrivateKey checks that the specified private key and public key together form a valid keypair. +func VerifyPublicKeyMatchesPrivateKey(privKey data.PrivateKey, pubKey data.PublicKey) error { + // generate a random message + msgLength := 64 + msg := make([]byte, msgLength) + _, err := rand.Read(msg) + if err != nil { + return fmt.Errorf("failed to generate random test message: %s", err) + } + + // sign the message with the private key + signatureBytes, err := privKey.Sign(rand.Reader, msg, nil) + if err != nil { + return fmt.Errorf("Failed to sign test message:", err) + } + + verifier, ok := Verifiers[privKey.SignatureAlgorithm()] + if !ok { + return fmt.Errorf("signing method is not supported: %s\n", privKey.SignatureAlgorithm()) + } + + if err := verifier.Verify(pubKey, signatureBytes, msg); err != nil { + return fmt.Errorf("Private Key did not match Public Key: %s", err) + } + + return nil +} diff --git a/tuf/signed/verify_test.go b/tuf/signed/verify_test.go index 64d70f593..6ba9f2504 100644 --- a/tuf/signed/verify_test.go +++ b/tuf/signed/verify_test.go @@ -1,11 +1,13 @@ package signed import ( + "crypto/rand" "testing" "time" "github.com/docker/go/canonical/json" "github.com/docker/notary" + "github.com/docker/notary/trustmanager" "github.com/stretchr/testify/require" "github.com/docker/notary/tuf/data" @@ -173,3 +175,20 @@ func TestVerifyExpiry(t *testing.T) { require.Error(t, err) require.IsType(t, ErrExpired{}, err) } + +func TestVerifyPublicKeyMatchesPrivateKeyHappyCase(t *testing.T) { + privKey, err := trustmanager.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + pubKey := data.PublicKeyFromPrivate(privKey) + err = VerifyPublicKeyMatchesPrivateKey(privKey, pubKey) + require.NoError(t, err) +} + +func TestVerifyPublicKeyMatchesPrivateKeyFails(t *testing.T) { + goodPrivKey, err := trustmanager.GenerateECDSAKey(rand.Reader) + badPrivKey, err := trustmanager.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + badPubKey := data.PublicKeyFromPrivate(badPrivKey) + err = VerifyPublicKeyMatchesPrivateKey(goodPrivKey, badPubKey) + require.Error(t, err) +}