diff --git a/client/client.go b/client/client.go index f4d3f9432..bbfed208e 100644 --- a/client/client.go +++ b/client/client.go @@ -929,7 +929,7 @@ func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error { ) switch serverManagesKey { case true: - pubKey, err = getRemoteKey(r.baseURL, r.gun, role, r.roundTrip) + pubKey, err = rotateRemoteKey(r.baseURL, r.gun, role, r.roundTrip) errFmtMsg = "unable to rotate remote key: %s" default: pubKey, err = r.CryptoService.Create(role, r.gun, data.ECDSAKey) diff --git a/client/client_test.go b/client/client_test.go index faedb3ebf..75fbb5bc9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2580,9 +2580,17 @@ func TestRotateKeyInvalidRole(t *testing.T) { require.Error(t, repo.RotateKey("targets/releases", false), "Rotating a delegation key should fail") + // rotating a delegation key to the server also fails + require.Error(t, repo.RotateKey("targets/releases", true), + "Rotating a delegation key should fail") + // rotating a not a real role key fails require.Error(t, repo.RotateKey("nope", false), "Rotating a non-real role key should fail") + + // rotating a not a real role key to the server also fails + require.Error(t, repo.RotateKey("nope", true), + "Rotating a non-real role key should fail") } // If remotely rotating key fails, the failure is propagated @@ -2595,9 +2603,59 @@ func TestRemoteRotationError(t *testing.T) { ts.Close() // server has died, so this should fail + for _, role := range []string{data.CanonicalSnapshotRole, data.CanonicalTimestampRole} { + err := repo.RotateKey(role, true) + require.Error(t, err) + require.Contains(t, err.Error(), "unable to rotate remote key") + } +} + +// If remotely rotating key fails for any reason, fail the rotation entirely +func TestRemoteRotationEndpointError(t *testing.T) { + ts, _, _ := simpleTestServer(t) + defer ts.Close() + repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) + defer os.RemoveAll(repo.baseDir) + + // simpleTestServer has no rotate key endpoint, so this should fail + for _, role := range []string{data.CanonicalSnapshotRole, data.CanonicalTimestampRole} { + err := repo.RotateKey(role, true) + require.Error(t, err) + require.IsType(t, store.ErrMetaNotFound{}, err) + } +} + +// The rotator is not the owner of the repository, they cannot rotate the remote +// key +func TestRemoteRotationNoRootKey(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) + require.NoError(t, repo.Publish()) + + newRepo, _ := newRepoToTestRepo(t, repo, true) + defer os.RemoveAll(newRepo.baseDir) + _, err := newRepo.ListTargets() + require.NoError(t, err) + + err = newRepo.RotateKey(data.CanonicalSnapshotRole, true) + require.Error(t, err) + require.IsType(t, signed.ErrInsufficientSignatures{}, err) +} + +// The repo hasn't been initialized, so we can't rotate +func TestRemoteRotationNonexistentRepo(t *testing.T) { + ts, _, _ := simpleTestServer(t) + defer ts.Close() + + repo := newBlankRepo(t, ts.URL) + defer os.RemoveAll(repo.baseDir) + err := repo.RotateKey(data.CanonicalTimestampRole, true) require.Error(t, err) - require.Contains(t, err.Error(), "unable to rotate remote key") + require.IsType(t, ErrRepoNotInitialized{}, err) } // Rotates the keys. After the rotation, downloading the latest metadata @@ -2732,6 +2790,14 @@ func TestRotateKeyAfterPublishServerManagementChange(t *testing.T) { data.CanonicalTargetsRole: false, data.CanonicalRootRole: false, }) + // check that the snapshot remote rotation creates new keys + testRotateKeySuccess(t, true, map[string]bool{ + data.CanonicalSnapshotRole: true, + }) + // check that the timestamp remote rotation creates new keys + testRotateKeySuccess(t, false, map[string]bool{ + data.CanonicalTimestampRole: true, + }) // reclaim snapshot key management from the server testRotateKeySuccess(t, true, map[string]bool{ data.CanonicalSnapshotRole: false, @@ -2761,13 +2827,6 @@ func testRotateKeySuccess(t *testing.T, serverManagesSnapshotInit bool, // Get root.json and capture targets + snapshot key IDs _, err := repo.GetTargetByName("latest") require.NoError(t, err) - - var keysToExpectCreated []string - for role, serverManaged := range keysToRotate { - if !serverManaged { - keysToExpectCreated = append(keysToExpectCreated, role) - } - } } func logRepoTrustRoot(t *testing.T, prefix string, repo *NotaryRepository) { diff --git a/client/client_update_test.go b/client/client_update_test.go index ad1a8d0c9..06acd12dc 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -1058,8 +1058,8 @@ func TestUpdateNonRootRemoteCorruptedNoLocalCache(t *testing.T) { // Having a local cache, if the server has the same data (timestamp has not changed), // should succeed in all cases if whether forWrite (force check) is true or not. // If the timestamp is fine, it hasn't changed and we don't have to download -// anything. If it's broken, we used the cached timestamp and again download -// nothing. +// anything. If it's broken, we used the cached timestamp only if the error on +// downloading the new one was not validation related func TestUpdateNonRootRemoteCorruptedCanUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") @@ -1070,10 +1070,18 @@ func TestUpdateNonRootRemoteCorruptedCanUseLocalCache(t *testing.T) { continue } for _, testData := range waysToMessUpServer { - testUpdateRemoteCorruptValidChecksum(t, updateOpts{ - localCache: true, - role: role, - }, testData, false) + // remote timestamp swizzling will fail the update + if role == data.CanonicalTimestampRole { + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + localCache: true, + role: role, + }, testData, testData.desc != "insufficient signatures") + } else { + testUpdateRemoteCorruptValidChecksum(t, updateOpts{ + localCache: true, + role: role, + }, testData, false) + } } } for role, expectations := range waysToMessUpServerNonRootPerRole(t) { @@ -1094,7 +1102,7 @@ func TestUpdateNonRootRemoteCorruptedCanUseLocalCache(t *testing.T) { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, role: role, - }, testData, false) + }, testData, true) case "targets/a": testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, @@ -1137,19 +1145,6 @@ func TestUpdateNonRootRemoteCorruptedCannotUseLocalCache(t *testing.T) { switch role { case data.CanonicalRootRole: break - case data.CanonicalTimestampRole: - for _, testData := range waysToMessUpServer { - // in general the cached timsestamp will always succeed, but if the threshold has been - // increased, it fails because when we download the new timestamp, it validates as per our - // previous root. But the root hash doesn't match. So we download a new root and - // try the update again. In this case, both the old and new timestamps won't have enough - // signatures. - testUpdateRemoteCorruptValidChecksum(t, updateOpts{ - serverHasNewData: true, - localCache: true, - role: role, - }, testData, testData.desc == "insufficient signatures") - } default: for _, testData := range waysToMessUpServer { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ @@ -1198,13 +1193,13 @@ func TestUpdateNonRootRemoteCorruptedCannotUseLocalCache(t *testing.T) { role: role, }, testData, false) case data.CanonicalTimestampRole: - // If the timestamp is invalid, we just default to the previous - // cached version of the timestamp, so the update succeeds + // we only default to the previous cached version of the timestamp if + // there is a network/storage error, so swizzling will fail the update testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: role, - }, testData, false) + }, testData, true) case "targets/a": testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, diff --git a/client/helpers.go b/client/helpers.go index 76e2b12df..11691f63f 100644 --- a/client/helpers.go +++ b/client/helpers.go @@ -236,6 +236,25 @@ func getRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, return pubKey, nil } +// Rotates a private key in a remote store and returns the public key component +func rotateRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, error) { + remote, err := getRemoteStore(url, gun, rt) + if err != nil { + return nil, err + } + rawPubKey, err := remote.RotateKey(role) + if err != nil { + return nil, err + } + + pubKey, err := data.UnmarshalPublicKey(rawPubKey) + if err != nil { + return nil, err + } + + return pubKey, nil +} + // signs and serializes the metadata for a canonical role in a TUF repo to JSON func serializeCanonicalRole(tufRepo *tuf.Repo, role string) (out []byte, err error) { var s *data.Signed diff --git a/client/helpers_test.go b/client/helpers_test.go index 5c6552098..49afd7456 100644 --- a/client/helpers_test.go +++ b/client/helpers_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "encoding/json" + "net/http" "testing" "time" @@ -1019,3 +1020,15 @@ func TestAllNotNearExpiry(t *testing.T) { require.NotContains(t, a.String(), "snapshot is nearing expiry, you should re-sign the role metadata", "Snapshot should not show near expiry") require.NotContains(t, a.String(), "timestamp", "there should be no logrus warnings pertaining to timestamp") } + +func TestRotateRemoteKeyOffline(t *testing.T) { + // without a valid roundtripper, rotation should fail since we cannot initialize a HTTPStore + key, err := rotateRemoteKey("invalidURL", "gun", data.CanonicalSnapshotRole, nil) + require.Error(t, err) + require.Nil(t, key) + + // if the underlying remote store is faulty and cannot rotate keys, we should get back the error + key, err = rotateRemoteKey("https://notary-server", "gun", data.CanonicalSnapshotRole, http.DefaultTransport) + require.Error(t, err) + require.Nil(t, key) +} diff --git a/client/tufclient.go b/client/tufclient.go index 1f7464dc5..409b019ae 100644 --- a/client/tufclient.go +++ b/client/tufclient.go @@ -107,27 +107,34 @@ func (c *TUFClient) downloadTimestamp() error { role := data.CanonicalTimestampRole consistentInfo := c.newBuilder.GetConsistentInfo(role) - // get the cached timestamp, if it exists + // always get the remote timestamp, since it supersedes the local one cachedTS, cachedErr := c.cache.GetSized(role, notary.MaxTimestampSize) - // always get the remote timestamp, since it supercedes the local one _, remoteErr := c.tryLoadRemote(consistentInfo, cachedTS) - switch { - case remoteErr == nil: + // check that there was no remote error, or if there was a network problem + // If there was a validation error, we should error out so we can download a new root or fail the update + switch remoteErr.(type) { + case nil: return nil - case cachedErr == nil: - logrus.Debug(remoteErr.Error()) - logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely") - - err := c.newBuilder.Load(role, cachedTS, 1, false) - if err == nil { - logrus.Debug("successfully verified cached timestamp") - } - return err + case store.ErrMetaNotFound, store.ErrServerUnavailable: + break default: + return remoteErr + } + + // since it was a network error: get the cached timestamp, if it exists + if cachedErr != nil { logrus.Debug("no cached or remote timestamp available") return remoteErr } + + logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely") + err := c.newBuilder.Load(role, cachedTS, 1, false) + if err == nil { + logrus.Debug("successfully verified cached timestamp") + } + return err + } // downloadSnapshot is responsible for downloading the snapshot.json @@ -220,7 +227,6 @@ func (c *TUFClient) tryLoadRemote(consistentInfo tuf.ConsistentInfo, old []byte) // will be 1 c.oldBuilder.Load(consistentInfo.RoleName, old, 1, true) minVersion := c.oldBuilder.GetLoadedVersion(consistentInfo.RoleName) - if err := c.newBuilder.Load(consistentInfo.RoleName, raw, minVersion, false); err != nil { logrus.Debugf("downloaded %s is invalid: %s", consistentName, err) return raw, err diff --git a/cmd/notary-signer/config.go b/cmd/notary-signer/config.go index 1c1345587..4db61c3ee 100644 --- a/cmd/notary-signer/config.go +++ b/cmd/notary-signer/config.go @@ -26,6 +26,7 @@ import ( "github.com/docker/notary/storage/rethinkdb" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" tufutils "github.com/docker/notary/tuf/utils" "github.com/docker/notary/utils" "github.com/spf13/viper" @@ -105,11 +106,11 @@ func setUpCryptoservices(configuration *viper.Viper, allowedBackends []string, d return nil, fmt.Errorf("%s is not an allowed backend, must be one of: %s", backend, allowedBackends) } - var keyStore trustmanager.KeyStore + var keyService signed.CryptoService switch backend { case notary.MemoryBackend: - keyStore = trustmanager.NewKeyMemoryStore( - passphrase.ConstantRetriever("memory-db-ignore")) + keyService = cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore( + passphrase.ConstantRetriever("memory-db-ignore"))) case notary.RethinkDBBackend: var sess *gorethink.Session storeConfig, err := utils.ParseRethinkDBStorage(configuration) @@ -135,10 +136,11 @@ func setUpCryptoservices(configuration *viper.Viper, allowedBackends []string, d } s := keydbstore.NewRethinkDBKeyStore(storeConfig.DBName, storeConfig.Username, storeConfig.Password, passphraseRetriever, defaultAlias, sess) health.RegisterPeriodicFunc("DB operational", time.Minute, s.CheckHealth) + if doBootstrap { - keyStore = s + keyService = s } else { - keyStore = keydbstore.NewCachedKeyStore(s) + keyService = keydbstore.NewCachedKeyService(s) } case notary.MySQLBackend, notary.SQLiteBackend: storeConfig, err := utils.ParseSQLStorage(configuration) @@ -157,21 +159,20 @@ func setUpCryptoservices(configuration *viper.Viper, allowedBackends []string, d health.RegisterPeriodicFunc( "DB operational", time.Minute, dbStore.HealthCheck) - keyStore = keydbstore.NewCachedKeyStore(dbStore) + keyService = keydbstore.NewCachedKeyService(dbStore) } if doBootstrap { - err := bootstrap(keyStore) + err := bootstrap(keyService) if err != nil { logrus.Fatal(err.Error()) } os.Exit(0) } - cryptoService := cryptoservice.NewCryptoService(keyStore) cryptoServices := make(signer.CryptoServiceIndex) - cryptoServices[data.ED25519Key] = cryptoService - cryptoServices[data.ECDSAKey] = cryptoService + cryptoServices[data.ED25519Key] = keyService + cryptoServices[data.ECDSAKey] = keyService return cryptoServices, nil } @@ -190,22 +191,25 @@ func getDefaultAlias(configuration *viper.Viper) (string, error) { } // set up the GRPC server -func setupGRPCServer(grpcAddr string, tlsConfig *tls.Config, - cryptoServices signer.CryptoServiceIndex) (*grpc.Server, net.Listener, error) { +func setupGRPCServer(signerConfig signer.Config) (*grpc.Server, net.Listener, error) { //RPC server setup - kms := &api.KeyManagementServer{CryptoServices: cryptoServices, - HealthChecker: health.CheckStatus} - ss := &api.SignerServer{CryptoServices: cryptoServices, - HealthChecker: health.CheckStatus} + kms := &api.KeyManagementServer{ + CryptoServices: signerConfig.CryptoServices, + HealthChecker: health.CheckStatus, + } + ss := &api.SignerServer{ + CryptoServices: signerConfig.CryptoServices, + HealthChecker: health.CheckStatus, + } - lis, err := net.Listen("tcp", grpcAddr) + lis, err := net.Listen("tcp", signerConfig.GRPCAddr) if err != nil { return nil, nil, fmt.Errorf("grpc server failed to listen on %s: %v", - grpcAddr, err) + signerConfig.GRPCAddr, err) } - creds := credentials.NewTLS(tlsConfig) + creds := credentials.NewTLS(signerConfig.TLSConfig) opts := []grpc.ServerOption{grpc.Creds(creds)} grpcServer := grpc.NewServer(opts...) diff --git a/cmd/notary-signer/main.go b/cmd/notary-signer/main.go index 721723413..d15f124ae 100644 --- a/cmd/notary-signer/main.go +++ b/cmd/notary-signer/main.go @@ -57,7 +57,7 @@ func main() { logrus.Fatal(err.Error()) } - grpcServer, lis, err := setupGRPCServer(signerConfig.GRPCAddr, signerConfig.TLSConfig, signerConfig.CryptoServices) + grpcServer, lis, err := setupGRPCServer(signerConfig) if err != nil { logrus.Fatal(err.Error()) } diff --git a/cmd/notary-signer/main_test.go b/cmd/notary-signer/main_test.go index 29de7ff71..baf039fcc 100644 --- a/cmd/notary-signer/main_test.go +++ b/cmd/notary-signer/main_test.go @@ -226,15 +226,18 @@ func TestSetupCryptoServicesInvalidStore(t *testing.T) { } func TestSetupGRPCServerInvalidAddress(t *testing.T) { - _, _, err := setupGRPCServer("nope", nil, make(signer.CryptoServiceIndex)) + _, _, err := setupGRPCServer(signer.Config{GRPCAddr: "nope", CryptoServices: make(signer.CryptoServiceIndex)}) require.Error(t, err) require.Contains(t, err.Error(), "grpc server failed to listen on nope") } func TestSetupGRPCServerSuccess(t *testing.T) { tlsConf := tls.Config{InsecureSkipVerify: true} - grpcServer, lis, err := setupGRPCServer(":7899", &tlsConf, - make(signer.CryptoServiceIndex)) + grpcServer, lis, err := setupGRPCServer(signer.Config{ + GRPCAddr: ":7899", + TLSConfig: &tlsConf, + CryptoServices: make(signer.CryptoServiceIndex), + }) defer lis.Close() require.NoError(t, err) require.Equal(t, "[::]:7899", lis.Addr().String()) diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index 736815fbe..43ff39d51 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -1268,8 +1268,16 @@ func TestClientKeyGenerationRotation(t *testing.T) { // publish using the new keys output := assertSuccessfullyPublish( t, tempDir, server.URL, "gun", target+"2", tempfiles[1]) - // assert that the previous target is sitll there - require.Contains(t, output, target) + // assert that the previous target is still there + require.True(t, strings.Contains(string(output), target)) + + // rotate the snapshot and timestamp keys on the server, multiple times + for i := 0; i < 10; i++ { + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalSnapshotRole, "-r") + require.NoError(t, err) + _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalTimestampRole, "-r") + require.NoError(t, err) + } } // Tests default root key generation diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index a80e69b7e..eb54f071e 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -202,7 +202,7 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error { gun := args[0] rotateKeyRole := args[1] - rt, err := getTransport(config, gun, readWrite) + rt, err := getTransport(config, gun, admin) if err != nil { return err } diff --git a/cryptoservice/crypto_service.go b/cryptoservice/crypto_service.go index 92782309e..3f5002913 100644 --- a/cryptoservice/crypto_service.go +++ b/cryptoservice/crypto_service.go @@ -8,15 +8,12 @@ import ( "encoding/pem" "errors" "github.com/Sirupsen/logrus" + "github.com/docker/notary" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/utils" ) -const ( - rsaKeySize = 2048 // Used for snapshots and targets keys -) - var ( // ErrNoValidPrivateKey is returned if a key being imported doesn't // look like a private key @@ -45,7 +42,7 @@ func (cs *CryptoService) Create(role, gun, algorithm string) (data.PublicKey, er switch algorithm { case data.RSAKey: - privKey, err = utils.GenerateRSAKey(rand.Reader, rsaKeySize) + privKey, err = utils.GenerateRSAKey(rand.Reader, notary.MinRSABitSize) if err != nil { return nil, fmt.Errorf("failed to generate RSA key: %v", err) } diff --git a/cryptoservice/crypto_service_test.go b/cryptoservice/crypto_service_test.go index 4c6e684ac..72c00131b 100644 --- a/cryptoservice/crypto_service_test.go +++ b/cryptoservice/crypto_service_test.go @@ -407,7 +407,7 @@ func TestCryptoServiceWithEmptyGUN(t *testing.T) { func TestCryptoSignerInterfaceBehavior(t *testing.T) { cs := NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) interfaces.EmptyCryptoServiceInterfaceBehaviorTests(t, cs) - interfaces.CreateGetKeyCryptoServiceInterfaceBehaviorTests(t, cs, data.ECDSAKey, true) + interfaces.CreateGetKeyCryptoServiceInterfaceBehaviorTests(t, cs, data.ECDSAKey) cs = NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) interfaces.CreateListKeyCryptoServiceInterfaceBehaviorTests(t, cs, data.ECDSAKey) diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index a3e1de39b..8c5c6fd14 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -87,7 +87,7 @@ subsection. ### Rotate keys -In case of potential compromise, notary provides a CLI command for rotating keys. Currently, you can use the `notary key rotate` command to rotate the targets or snapshot keys. +In case of potential compromise, notary provides a CLI command for rotating keys. Currently, you can use the `notary key rotate` command to rotate the root, targets, snapshot, or timestamp keys. While the snapshot key is managed by the notary client by default, use the `notary key rotate snapshot -r` command to rotate the snapshot key to the server, such that the @@ -98,7 +98,8 @@ snapshot key to push their updates to the collection. Note that new collections created by a Docker 1.11 Engine client will have the server manage the snapshot key by default. To reclaim control of the snapshot key on the client, use the `notary key rotate` command without the `-r` flag. -The targets key must be locally managed - to rotate the targets key, for instance in case of compromise, use the `notary key rotate targets` command without the `-r` flag. +The root and targets key must be locally managed - to rotate either the root or targets key, for instance in case of compromise, use the `notary key rotate` command without the `-r` flag. +The timestamp key must be remotely managed - to rotate the timestamp key use the `notary key rotate timestamp -r` command. ### Use a Yubikey diff --git a/migrations/server/mysql/0004_drop_timestamp_key.up.sql b/migrations/server/mysql/0004_drop_timestamp_key.up.sql new file mode 100644 index 000000000..78688617f --- /dev/null +++ b/migrations/server/mysql/0004_drop_timestamp_key.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `timestamp_keys`; \ No newline at end of file diff --git a/migrations/signer/mysql/0002_gun_role_on_keys.up.sql b/migrations/signer/mysql/0002_gun_role_on_keys.up.sql new file mode 100644 index 000000000..d0563e747 --- /dev/null +++ b/migrations/signer/mysql/0002_gun_role_on_keys.up.sql @@ -0,0 +1 @@ +ALTER TABLE `private_keys` ADD COLUMN `gun` VARCHAR(255) NOT NULL, ADD COLUMN `role` VARCHAR(255) NOT NULL, ADD COLUMN `last_used` DATETIME NULL DEFAULT NULL; \ No newline at end of file diff --git a/proto/signer.pb.go b/proto/signer.pb.go index 6be844599..ca2834f8b 100644 --- a/proto/signer.pb.go +++ b/proto/signer.pb.go @@ -1,17 +1,19 @@ // Code generated by protoc-gen-go. -// source: signer.proto +// source: proto/signer.proto // DO NOT EDIT! /* Package proto is a generated protocol buffer package. It is generated from these files: - signer.proto + proto/signer.proto It has these top-level messages: + CreateKeyRequest KeyInfo KeyID Algorithm + GetKeyInfoResponse PublicKey Signature SignatureRequest @@ -40,6 +42,17 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package +type CreateKeyRequest struct { + Algorithm string `protobuf:"bytes,1,opt,name=algorithm" json:"algorithm,omitempty"` + Gun string `protobuf:"bytes,2,opt,name=gun" json:"gun,omitempty"` + Role string `protobuf:"bytes,3,opt,name=role" json:"role,omitempty"` +} + +func (m *CreateKeyRequest) Reset() { *m = CreateKeyRequest{} } +func (m *CreateKeyRequest) String() string { return proto1.CompactTextString(m) } +func (*CreateKeyRequest) ProtoMessage() {} +func (*CreateKeyRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + // KeyInfo holds a KeyID that is used to reference the key and it's algorithm type KeyInfo struct { KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` @@ -49,7 +62,7 @@ type KeyInfo struct { func (m *KeyInfo) Reset() { *m = KeyInfo{} } func (m *KeyInfo) String() string { return proto1.CompactTextString(m) } func (*KeyInfo) ProtoMessage() {} -func (*KeyInfo) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } +func (*KeyInfo) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } func (m *KeyInfo) GetKeyID() *KeyID { if m != nil { @@ -73,7 +86,7 @@ type KeyID struct { func (m *KeyID) Reset() { *m = KeyID{} } func (m *KeyID) String() string { return proto1.CompactTextString(m) } func (*KeyID) ProtoMessage() {} -func (*KeyID) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } +func (*KeyID) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } // Type holds the type of crypto algorithm used type Algorithm struct { @@ -83,7 +96,27 @@ type Algorithm struct { func (m *Algorithm) Reset() { *m = Algorithm{} } func (m *Algorithm) String() string { return proto1.CompactTextString(m) } func (*Algorithm) ProtoMessage() {} -func (*Algorithm) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } +func (*Algorithm) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } + +// GetKeyInfoResponse returns the public key, the role, and the algorithm and key ID. +// For backwards compatibility, it doesn't embed a PublicKey object +type GetKeyInfoResponse struct { + KeyInfo *KeyInfo `protobuf:"bytes,1,opt,name=keyInfo" json:"keyInfo,omitempty"` + PublicKey []byte `protobuf:"bytes,2,opt,name=publicKey,proto3" json:"publicKey,omitempty"` + Role string `protobuf:"bytes,3,opt,name=role" json:"role,omitempty"` +} + +func (m *GetKeyInfoResponse) Reset() { *m = GetKeyInfoResponse{} } +func (m *GetKeyInfoResponse) String() string { return proto1.CompactTextString(m) } +func (*GetKeyInfoResponse) ProtoMessage() {} +func (*GetKeyInfoResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } + +func (m *GetKeyInfoResponse) GetKeyInfo() *KeyInfo { + if m != nil { + return m.KeyInfo + } + return nil +} // PublicKey has a KeyInfo that is used to reference the key, and opaque bytes of a publicKey type PublicKey struct { @@ -94,7 +127,7 @@ type PublicKey struct { func (m *PublicKey) Reset() { *m = PublicKey{} } func (m *PublicKey) String() string { return proto1.CompactTextString(m) } func (*PublicKey) ProtoMessage() {} -func (*PublicKey) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } +func (*PublicKey) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } func (m *PublicKey) GetKeyInfo() *KeyInfo { if m != nil { @@ -113,7 +146,7 @@ type Signature struct { func (m *Signature) Reset() { *m = Signature{} } func (m *Signature) String() string { return proto1.CompactTextString(m) } func (*Signature) ProtoMessage() {} -func (*Signature) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } +func (*Signature) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } func (m *Signature) GetKeyInfo() *KeyInfo { if m != nil { @@ -138,7 +171,7 @@ type SignatureRequest struct { func (m *SignatureRequest) Reset() { *m = SignatureRequest{} } func (m *SignatureRequest) String() string { return proto1.CompactTextString(m) } func (*SignatureRequest) ProtoMessage() {} -func (*SignatureRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } +func (*SignatureRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } func (m *SignatureRequest) GetKeyID() *KeyID { if m != nil { @@ -154,7 +187,7 @@ type Void struct { func (m *Void) Reset() { *m = Void{} } func (m *Void) String() string { return proto1.CompactTextString(m) } func (*Void) ProtoMessage() {} -func (*Void) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } +func (*Void) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } // A mapping of health check name to the check result message type HealthStatus struct { @@ -164,7 +197,7 @@ type HealthStatus struct { func (m *HealthStatus) Reset() { *m = HealthStatus{} } func (m *HealthStatus) String() string { return proto1.CompactTextString(m) } func (*HealthStatus) ProtoMessage() {} -func (*HealthStatus) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } +func (*HealthStatus) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } func (m *HealthStatus) GetStatus() map[string]string { if m != nil { @@ -174,9 +207,11 @@ func (m *HealthStatus) GetStatus() map[string]string { } func init() { + proto1.RegisterType((*CreateKeyRequest)(nil), "proto.CreateKeyRequest") proto1.RegisterType((*KeyInfo)(nil), "proto.KeyInfo") proto1.RegisterType((*KeyID)(nil), "proto.KeyID") proto1.RegisterType((*Algorithm)(nil), "proto.Algorithm") + proto1.RegisterType((*GetKeyInfoResponse)(nil), "proto.GetKeyInfoResponse") proto1.RegisterType((*PublicKey)(nil), "proto.PublicKey") proto1.RegisterType((*Signature)(nil), "proto.Signature") proto1.RegisterType((*SignatureRequest)(nil), "proto.SignatureRequest") @@ -196,11 +231,11 @@ const _ = grpc.SupportPackageIsVersion3 type KeyManagementClient interface { // CreateKey creates as asymmetric key pair and returns the PublicKey - CreateKey(ctx context.Context, in *Algorithm, opts ...grpc.CallOption) (*PublicKey, error) + CreateKey(ctx context.Context, in *CreateKeyRequest, opts ...grpc.CallOption) (*PublicKey, error) // DeleteKey deletes the key associated with a KeyID DeleteKey(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*Void, error) // GetKeyInfo returns the PublicKey associated with a KeyID - GetKeyInfo(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*PublicKey, error) + GetKeyInfo(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*GetKeyInfoResponse, error) // CheckHealth returns the HealthStatus with the service - note: since the // KeyManagement service is probably deployed on the same server as the Signer // service, we're only putting the CheckHealth function on the KeyManagement @@ -216,7 +251,7 @@ func NewKeyManagementClient(cc *grpc.ClientConn) KeyManagementClient { return &keyManagementClient{cc} } -func (c *keyManagementClient) CreateKey(ctx context.Context, in *Algorithm, opts ...grpc.CallOption) (*PublicKey, error) { +func (c *keyManagementClient) CreateKey(ctx context.Context, in *CreateKeyRequest, opts ...grpc.CallOption) (*PublicKey, error) { out := new(PublicKey) err := grpc.Invoke(ctx, "/proto.KeyManagement/CreateKey", in, out, c.cc, opts...) if err != nil { @@ -234,8 +269,8 @@ func (c *keyManagementClient) DeleteKey(ctx context.Context, in *KeyID, opts ... return out, nil } -func (c *keyManagementClient) GetKeyInfo(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*PublicKey, error) { - out := new(PublicKey) +func (c *keyManagementClient) GetKeyInfo(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*GetKeyInfoResponse, error) { + out := new(GetKeyInfoResponse) err := grpc.Invoke(ctx, "/proto.KeyManagement/GetKeyInfo", in, out, c.cc, opts...) if err != nil { return nil, err @@ -256,11 +291,11 @@ func (c *keyManagementClient) CheckHealth(ctx context.Context, in *Void, opts .. type KeyManagementServer interface { // CreateKey creates as asymmetric key pair and returns the PublicKey - CreateKey(context.Context, *Algorithm) (*PublicKey, error) + CreateKey(context.Context, *CreateKeyRequest) (*PublicKey, error) // DeleteKey deletes the key associated with a KeyID DeleteKey(context.Context, *KeyID) (*Void, error) // GetKeyInfo returns the PublicKey associated with a KeyID - GetKeyInfo(context.Context, *KeyID) (*PublicKey, error) + GetKeyInfo(context.Context, *KeyID) (*GetKeyInfoResponse, error) // CheckHealth returns the HealthStatus with the service - note: since the // KeyManagement service is probably deployed on the same server as the Signer // service, we're only putting the CheckHealth function on the KeyManagement @@ -273,7 +308,7 @@ func RegisterKeyManagementServer(s *grpc.Server, srv KeyManagementServer) { } func _KeyManagement_CreateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Algorithm) + in := new(CreateKeyRequest) if err := dec(in); err != nil { return nil, err } @@ -285,7 +320,7 @@ func _KeyManagement_CreateKey_Handler(srv interface{}, ctx context.Context, dec FullMethod: "/proto.KeyManagement/CreateKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(KeyManagementServer).CreateKey(ctx, req.(*Algorithm)) + return srv.(KeyManagementServer).CreateKey(ctx, req.(*CreateKeyRequest)) } return interceptor(ctx, in, info, handler) } @@ -435,35 +470,38 @@ var _Signer_serviceDesc = grpc.ServiceDesc{ Metadata: fileDescriptor0, } -func init() { proto1.RegisterFile("signer.proto", fileDescriptor0) } +func init() { proto1.RegisterFile("proto/signer.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 417 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x94, 0x93, 0xcf, 0x0b, 0xd3, 0x30, - 0x14, 0xc7, 0xdb, 0x6e, 0xed, 0xc8, 0x6b, 0x1d, 0x23, 0x0a, 0x2b, 0x45, 0x50, 0x72, 0x9a, 0x97, - 0xc2, 0xb6, 0x83, 0x3f, 0xc0, 0x83, 0xac, 0xa2, 0x22, 0xc2, 0x48, 0xc1, 0x9b, 0x87, 0x6e, 0xc6, - 0xad, 0xac, 0x6b, 0x67, 0x9b, 0x0a, 0x3b, 0x09, 0xfe, 0x83, 0xfe, 0x4b, 0xa6, 0x49, 0xda, 0x75, - 0xba, 0x83, 0x3b, 0xe5, 0xe5, 0xbd, 0xef, 0xfb, 0xec, 0x9b, 0xf7, 0x56, 0xf0, 0xaa, 0x74, 0x97, - 0xb3, 0x32, 0x3c, 0x95, 0x05, 0x2f, 0xb0, 0x2d, 0x0f, 0xf2, 0x05, 0x46, 0x1f, 0xd9, 0xf9, 0x43, - 0xfe, 0xad, 0xc0, 0x04, 0xec, 0x83, 0x08, 0x23, 0xdf, 0x7c, 0x6a, 0xce, 0xdc, 0x85, 0xa7, 0x84, - 0x61, 0x53, 0x8e, 0xa8, 0x2a, 0xe1, 0x10, 0x50, 0x92, 0xed, 0x8a, 0x32, 0xe5, 0xfb, 0xa3, 0x6f, - 0x49, 0xdd, 0x44, 0xeb, 0xde, 0xb4, 0x79, 0x7a, 0x91, 0x90, 0x29, 0xd8, 0xb2, 0x1f, 0x8f, 0xc1, - 0xd2, 0x64, 0x44, 0xad, 0x34, 0x22, 0xcf, 0x00, 0x75, 0x0d, 0xf8, 0x71, 0x9f, 0xaa, 0x34, 0x3d, - 0x46, 0x0c, 0x68, 0x5d, 0x6f, 0xb2, 0x74, 0x2b, 0x48, 0x78, 0x06, 0xa3, 0x83, 0xf2, 0xab, 0x6d, - 0x8e, 0x7b, 0x36, 0x45, 0x96, 0xb6, 0xe5, 0x06, 0x7a, 0x6a, 0xdb, 0xa4, 0x55, 0x8f, 0x5e, 0x12, - 0xe4, 0x27, 0xa0, 0x58, 0x8c, 0x23, 0xe1, 0x75, 0xc9, 0xee, 0x80, 0xde, 0xf9, 0x7e, 0xec, 0xc3, - 0x68, 0x5b, 0xe4, 0x9c, 0xe5, 0xdc, 0x1f, 0x48, 0x0b, 0xed, 0x95, 0xac, 0x61, 0xd2, 0x19, 0xa0, - 0xec, 0x7b, 0xcd, 0x2a, 0xfe, 0x5f, 0x1b, 0xe8, 0x11, 0xad, 0x6b, 0xa2, 0x03, 0xc3, 0xcf, 0x45, - 0xfa, 0x95, 0xfc, 0x32, 0xc1, 0x7b, 0xcf, 0x92, 0x8c, 0xef, 0x63, 0x2e, 0xf0, 0x15, 0x7e, 0x0e, - 0x4e, 0x25, 0x23, 0xc1, 0x1d, 0x08, 0xee, 0x13, 0xcd, 0xed, 0x8b, 0x42, 0x75, 0xbc, 0xcd, 0x79, - 0x79, 0xa6, 0x5a, 0x1e, 0xbc, 0x04, 0xb7, 0x97, 0xc6, 0x13, 0x18, 0x08, 0x0f, 0x7a, 0x41, 0x4d, - 0x88, 0x1f, 0x81, 0xfd, 0x23, 0xc9, 0x6a, 0x26, 0xad, 0x20, 0xaa, 0x2e, 0xaf, 0xac, 0x17, 0xe6, - 0xe2, 0xb7, 0x09, 0x0f, 0x84, 0xef, 0x4f, 0x49, 0x9e, 0xec, 0xd8, 0x51, 0xd8, 0xc3, 0x73, 0x40, - 0xab, 0x92, 0x25, 0x9c, 0x35, 0x6b, 0xfc, 0x67, 0x68, 0x41, 0x9b, 0xe9, 0x56, 0x4d, 0x0c, 0xb1, - 0x17, 0x14, 0xb1, 0x8c, 0xa9, 0x96, 0xab, 0x69, 0x04, 0xae, 0xbe, 0xc9, 0x17, 0x1b, 0x62, 0x2f, - 0xf0, 0x8e, 0xf1, 0xf6, 0x9f, 0x7c, 0x2d, 0xbd, 0x45, 0x9e, 0x83, 0xbb, 0xda, 0xb3, 0xed, 0x41, - 0x8d, 0x00, 0xf7, 0x69, 0xc1, 0xc3, 0x1b, 0xe3, 0x21, 0xc6, 0xe2, 0x35, 0x38, 0xb1, 0xfc, 0x80, - 0xf0, 0x12, 0x86, 0x4d, 0x84, 0xa7, 0x5a, 0xf8, 0xf7, 0x1e, 0xbb, 0x5f, 0xec, 0x0a, 0xc4, 0xd8, - 0x38, 0x32, 0xb5, 0xfc, 0x13, 0x00, 0x00, 0xff, 0xff, 0x14, 0x63, 0xc3, 0x53, 0x86, 0x03, 0x00, - 0x00, + // 479 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x54, 0xdf, 0x6b, 0xd4, 0x40, + 0x10, 0x4e, 0x72, 0xbf, 0xc8, 0xe4, 0x2c, 0xc7, 0x28, 0x34, 0x1e, 0x82, 0x65, 0x9f, 0xce, 0x97, + 0x13, 0xaf, 0x0f, 0xad, 0x82, 0x0f, 0xd2, 0x13, 0x2d, 0x87, 0x50, 0x72, 0xd0, 0x37, 0x1f, 0xb6, + 0xe7, 0x98, 0x0b, 0x97, 0xee, 0xc6, 0x64, 0x23, 0xe4, 0x49, 0xf0, 0x1f, 0xf5, 0x5f, 0x91, 0x6c, + 0x36, 0x7b, 0xe9, 0x59, 0xc4, 0x82, 0x4f, 0x99, 0xf9, 0xe6, 0xdb, 0x2f, 0xdf, 0xcc, 0x4e, 0x02, + 0x98, 0xe5, 0x52, 0xc9, 0x97, 0x45, 0x12, 0x0b, 0xca, 0xe7, 0x3a, 0xc1, 0x81, 0x7e, 0xb0, 0x6b, + 0x98, 0x5c, 0xe4, 0xc4, 0x15, 0xad, 0xa8, 0x8a, 0xe8, 0x5b, 0x49, 0x85, 0xc2, 0x67, 0xe0, 0xf3, + 0x34, 0x96, 0x79, 0xa2, 0xb6, 0xb7, 0xa1, 0x7b, 0xe2, 0xce, 0xfc, 0x68, 0x0f, 0xe0, 0x04, 0x7a, + 0x71, 0x29, 0x42, 0x4f, 0xe3, 0x75, 0x88, 0x08, 0xfd, 0x5c, 0xa6, 0x14, 0xf6, 0x34, 0xa4, 0x63, + 0xf6, 0x19, 0x46, 0x2b, 0xaa, 0x2e, 0xc5, 0x57, 0x89, 0x0c, 0x06, 0x3b, 0xaa, 0x2e, 0x97, 0x5a, + 0x2a, 0x58, 0x8c, 0x1b, 0x03, 0xf3, 0xba, 0xbc, 0x8c, 0x9a, 0x12, 0xce, 0xbb, 0xaf, 0xf4, 0x34, + 0x6f, 0x62, 0x78, 0xef, 0x5a, 0xbc, 0x63, 0x82, 0x1d, 0xc3, 0x40, 0x9f, 0xc7, 0x23, 0xf0, 0x8c, + 0xb2, 0x1f, 0x79, 0xc9, 0x92, 0xbd, 0x00, 0xdf, 0x1e, 0xf8, 0x7b, 0x23, 0x2c, 0x03, 0xfc, 0x40, + 0xca, 0xb8, 0x8c, 0xa8, 0xc8, 0xa4, 0x28, 0x08, 0x67, 0x30, 0xda, 0x35, 0x90, 0xf1, 0x7b, 0xd4, + 0xf1, 0x5b, 0x13, 0xdb, 0x72, 0xad, 0x9e, 0x95, 0x37, 0x69, 0xb2, 0x59, 0x51, 0xa5, 0x3d, 0x8f, + 0xa3, 0x3d, 0x70, 0xef, 0x50, 0xd6, 0xe0, 0x5f, 0x59, 0xc2, 0x7f, 0x7a, 0x11, 0xfb, 0x01, 0xfe, + 0x3a, 0x89, 0x05, 0x57, 0x65, 0xfe, 0x10, 0xf7, 0x0f, 0x9c, 0x38, 0x86, 0x30, 0xda, 0x48, 0xa1, + 0x48, 0x28, 0xdd, 0xd2, 0x38, 0x6a, 0x53, 0x76, 0x05, 0x13, 0x6b, 0xa0, 0x5d, 0xa1, 0x7f, 0xb9, + 0xf3, 0x8e, 0xa2, 0x77, 0x57, 0x71, 0x08, 0xfd, 0x6b, 0x99, 0x7c, 0x61, 0x3f, 0x5d, 0x18, 0x7f, + 0x24, 0x9e, 0xaa, 0xed, 0x5a, 0x71, 0x55, 0x16, 0x78, 0x06, 0xc3, 0x42, 0x47, 0xa1, 0x7b, 0xd2, + 0x9b, 0x05, 0x8b, 0xe7, 0x46, 0xb7, 0x4b, 0x9a, 0x37, 0x8f, 0xf7, 0x42, 0xe5, 0x55, 0x64, 0xe8, + 0xd3, 0xd7, 0x10, 0x74, 0xe0, 0x7a, 0x87, 0x77, 0x54, 0x99, 0x95, 0xa8, 0x43, 0x7c, 0x02, 0x83, + 0xef, 0x3c, 0x2d, 0xc9, 0xec, 0x75, 0x93, 0xbc, 0xf1, 0xce, 0xdd, 0xc5, 0x2f, 0x17, 0x1e, 0xad, + 0xa8, 0xfa, 0xc4, 0x05, 0x8f, 0xe9, 0x96, 0x84, 0xc2, 0x73, 0xf0, 0xed, 0x37, 0x83, 0xc7, 0xc6, + 0xc2, 0xe1, 0x57, 0x34, 0x6d, 0xa7, 0x69, 0x6f, 0x9c, 0x39, 0x38, 0x03, 0x7f, 0x49, 0x29, 0x35, + 0x27, 0xef, 0x0c, 0x65, 0x1a, 0x98, 0x4c, 0x37, 0xee, 0xe0, 0x19, 0xc0, 0x7e, 0x39, 0x0f, 0xa8, + 0x4f, 0x4d, 0xf6, 0xe7, 0xf6, 0x32, 0x07, 0x5f, 0x41, 0x70, 0xb1, 0xa5, 0xcd, 0xae, 0x19, 0x09, + 0x76, 0x65, 0xa7, 0x8f, 0xef, 0x19, 0x17, 0x73, 0x16, 0x6f, 0x61, 0xb8, 0xd6, 0xbf, 0x06, 0x3c, + 0x85, 0x7e, 0x1d, 0xd9, 0xa6, 0x0e, 0xef, 0xd5, 0x36, 0x65, 0x0b, 0xcc, 0xb9, 0x19, 0x6a, 0xe8, + 0xf4, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe4, 0x13, 0xe6, 0xe8, 0x66, 0x04, 0x00, 0x00, } diff --git a/proto/signer.proto b/proto/signer.proto index f49ceb41b..0425cdceb 100644 --- a/proto/signer.proto +++ b/proto/signer.proto @@ -6,13 +6,13 @@ package proto; service KeyManagement { // CreateKey creates as asymmetric key pair and returns the PublicKey - rpc CreateKey(Algorithm) returns (PublicKey) {} + rpc CreateKey(CreateKeyRequest) returns (PublicKey) {} // DeleteKey deletes the key associated with a KeyID rpc DeleteKey(KeyID) returns (Void) {} // GetKeyInfo returns the PublicKey associated with a KeyID - rpc GetKeyInfo(KeyID) returns (PublicKey) {} + rpc GetKeyInfo(KeyID) returns (GetKeyInfoResponse) {} // CheckHealth returns the HealthStatus with the service - note: since the // KeyManagement service is probably deployed on the same server as the Signer @@ -27,6 +27,12 @@ service Signer { rpc Sign(SignatureRequest) returns (Signature) {} } +message CreateKeyRequest { + string algorithm = 1; + string gun = 2; + string role = 3; +} + // KeyInfo holds a KeyID that is used to reference the key and it's algorithm message KeyInfo { KeyID keyID = 1; @@ -43,6 +49,15 @@ message Algorithm { string algorithm = 1; } +// GetKeyInfoResponse returns the public key, the role, and the algorithm and key ID. +// For backwards compatibility, it doesn't embed a PublicKey object +message GetKeyInfoResponse { + KeyInfo keyInfo = 1; + bytes publicKey = 2; + string role = 3; +} + + // PublicKey has a KeyInfo that is used to reference the key, and opaque bytes of a publicKey message PublicKey { KeyInfo keyInfo = 1; diff --git a/server/handlers/default.go b/server/handlers/default.go index b338c3d2e..3eb04437e 100644 --- a/server/handlers/default.go +++ b/server/handlers/default.go @@ -184,43 +184,12 @@ func GetKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) } func getKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - gun, ok := vars["imageName"] - logger := ctxu.GetLoggerWithField(ctx, gun, "gun") - if !ok || gun == "" { - logger.Info("400 GET no gun in request") - return errors.ErrUnknown.WithDetail("no gun") - } - - role, ok := vars["tufRole"] - if !ok || role == "" { - logger.Info("400 GET no role in request") - return errors.ErrUnknown.WithDetail("no role") - } - - s := ctx.Value("metaStore") - store, ok := s.(storage.MetaStore) - if !ok || store == nil { - logger.Error("500 GET storage not configured") - return errors.ErrNoStorage.WithDetail(nil) - } - c := ctx.Value("cryptoService") - crypto, ok := c.(signed.CryptoService) - if !ok || crypto == nil { - logger.Error("500 GET crypto service not configured") - return errors.ErrNoCryptoService.WithDetail(nil) - } - algo := ctx.Value("keyAlgorithm") - keyAlgo, ok := algo.(string) - if !ok || keyAlgo == "" { - logger.Error("500 GET key algorithm not configured") - return errors.ErrNoKeyAlgorithm.WithDetail(nil) + role, gun, keyAlgorithm, store, crypto, err := setupKeyHandler(ctx, w, r, vars, http.MethodGet) + if err != nil { + return err } - keyAlgorithm := keyAlgo - - var ( - key data.PublicKey - err error - ) + var key data.PublicKey + logger := ctxu.GetLoggerWithField(ctx, gun, "gun") switch role { case data.CanonicalTimestampRole: key, err = timestamp.GetOrCreateTimestampKey(gun, store, crypto, keyAlgorithm) @@ -245,6 +214,81 @@ func getKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, return nil } +// RotateKeyHandler rotates the remote key for the specified role, returning the public key +func RotateKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + defer r.Body.Close() + vars := mux.Vars(r) + return rotateKeyHandler(ctx, w, r, vars) +} + +func rotateKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + role, gun, keyAlgorithm, store, crypto, err := setupKeyHandler(ctx, w, r, vars, http.MethodPost) + if err != nil { + return err + } + var key data.PublicKey + logger := ctxu.GetLoggerWithField(ctx, gun, "gun") + switch role { + case data.CanonicalTimestampRole: + key, err = timestamp.RotateTimestampKey(gun, store, crypto, keyAlgorithm) + case data.CanonicalSnapshotRole: + key, err = snapshot.RotateSnapshotKey(gun, store, crypto, keyAlgorithm) + default: + logger.Infof("400 POST %s key: %v", role, err) + return errors.ErrInvalidRole.WithDetail(role) + } + if err != nil { + logger.Errorf("500 POST %s key: %v", role, err) + return errors.ErrUnknown.WithDetail(err) + } + + out, err := json.Marshal(key) + if err != nil { + logger.Errorf("500 POST %s key", role) + return errors.ErrUnknown.WithDetail(err) + } + logger.Debugf("200 POST %s key", role) + w.Write(out) + return nil +} + +// To be called before getKeyHandler or rotateKeyHandler +func setupKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string, actionVerb string) (string, string, string, storage.MetaStore, signed.CryptoService, error) { + gun, ok := vars["imageName"] + logger := ctxu.GetLoggerWithField(ctx, gun, "gun") + if !ok || gun == "" { + logger.Infof("400 %s no gun in request", actionVerb) + return "", "", "", nil, nil, errors.ErrUnknown.WithDetail("no gun") + } + + role, ok := vars["tufRole"] + if !ok || role == "" { + logger.Infof("400 %s no role in request", actionVerb) + return "", "", "", nil, nil, errors.ErrUnknown.WithDetail("no role") + } + + s := ctx.Value("metaStore") + store, ok := s.(storage.MetaStore) + if !ok || store == nil { + logger.Errorf("500 %s storage not configured", actionVerb) + return "", "", "", nil, nil, errors.ErrNoStorage.WithDetail(nil) + } + c := ctx.Value("cryptoService") + crypto, ok := c.(signed.CryptoService) + if !ok || crypto == nil { + logger.Errorf("500 %s crypto service not configured", actionVerb) + return "", "", "", nil, nil, errors.ErrNoCryptoService.WithDetail(nil) + } + algo := ctx.Value("keyAlgorithm") + keyAlgo, ok := algo.(string) + if !ok || keyAlgo == "" { + logger.Errorf("500 %s key algorithm not configured", actionVerb) + return "", "", "", nil, nil, errors.ErrNoKeyAlgorithm.WithDetail(nil) + } + + return role, gun, keyAlgo, store, crypto, nil +} + // NotFoundHandler is used as a generic catch all handler to return the ErrMetadataNotFound // 404 response func NotFoundHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { diff --git a/server/handlers/default_test.go b/server/handlers/default_test.go index 29646a737..9e1d5ae09 100644 --- a/server/handlers/default_test.go +++ b/server/handlers/default_test.go @@ -76,9 +76,11 @@ func TestMainHandlerNotGet(t *testing.T) { } } -// GetKeyHandler needs to have access to a metadata store and cryptoservice, +type simplerHandler func(context.Context, http.ResponseWriter, *http.Request, map[string]string) error + +// GetKeyHandler and RotateKeyHandler needs to have access to a metadata store and cryptoservice, // a key algorithm -func TestGetKeyHandlerInvalidConfiguration(t *testing.T) { +func TestKeyHandlersInvalidConfiguration(t *testing.T) { noStore := defaultState() noStore.store = nil @@ -108,53 +110,60 @@ func TestGetKeyHandlerInvalidConfiguration(t *testing.T) { "tufRole": data.CanonicalTimestampRole, } req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} - for errString, states := range invalidStates { - for _, s := range states { - err := getKeyHandler(getContext(s), httptest.NewRecorder(), req, vars) - require.Error(t, err) - require.Contains(t, err.Error(), errString) + for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { + for errString, states := range invalidStates { + for _, s := range states { + err := keyHandler(getContext(s), httptest.NewRecorder(), req, vars) + require.Error(t, err) + require.Contains(t, err.Error(), errString) + } } } } -// GetKeyHandler needs to be set up such that an imageName and tufRole are both +// GetKeyHandler and RotateKeyHandler need to be set up such that an imageName and tufRole are both // provided and non-empty. -func TestGetKeyHandlerNoRoleOrRepo(t *testing.T) { +func TestKeyHandlersNoRoleOrRepo(t *testing.T) { state := defaultState() req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} + for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { + for _, key := range []string{"imageName", "tufRole"} { + vars := map[string]string{ + "imageName": "gun", + "tufRole": data.CanonicalTimestampRole, + } + + // not provided + delete(vars, key) + err := keyHandler(getContext(state), httptest.NewRecorder(), req, vars) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown") - for _, key := range []string{"imageName", "tufRole"} { - vars := map[string]string{ - "imageName": "gun", - "tufRole": data.CanonicalTimestampRole, + // empty + vars[key] = "" + err = keyHandler(getContext(state), httptest.NewRecorder(), req, vars) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown") } - - // not provided - delete(vars, key) - err := getKeyHandler(getContext(state), httptest.NewRecorder(), req, vars) - require.Error(t, err) - require.Contains(t, err.Error(), "unknown") - - // empty - vars[key] = "" - err = getKeyHandler(getContext(state), httptest.NewRecorder(), req, vars) - require.Error(t, err) - require.Contains(t, err.Error(), "unknown") } } -// Getting a key for a non-supported role results in a 400. -func TestGetKeyHandlerInvalidRole(t *testing.T) { +// GetKeyHandler and RotateKeyHandler called for a non-supported role results in a 400. +func TestKeyHandlersInvalidRole(t *testing.T) { state := defaultState() - vars := map[string]string{ - "imageName": "gun", - "tufRole": data.CanonicalRootRole, + for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { + for _, role := range []string{data.CanonicalRootRole, data.CanonicalTargetsRole, "targets/a", "invalidrole"} { + vars := map[string]string{ + "imageName": "gun", + "tufRole": role, + } + req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} + + err := keyHandler(getContext(state), httptest.NewRecorder(), req, vars) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid role") + } } - req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} - - err := getKeyHandler(getContext(state), httptest.NewRecorder(), req, vars) - require.Error(t, err) - require.Contains(t, err.Error(), "invalid role") } // Getting the key for a valid role and gun succeeds @@ -172,6 +181,37 @@ func TestGetKeyHandlerCreatesOnce(t *testing.T) { } } +// Getting or rotating the key fails if we don't pass a valid key algorithm +func TestKeyHandlersInvalidKeyAlgo(t *testing.T) { + roles := []string{data.CanonicalTimestampRole, data.CanonicalSnapshotRole} + req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} + for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { + for _, role := range roles { + vars := map[string]string{"imageName": "gun", "tufRole": role} + recorder := httptest.NewRecorder() + invalidKeyAlgoState := defaultState() + invalidKeyAlgoState.keyAlgo = "notactuallyakeyalgorithm" + err := keyHandler(getContext(invalidKeyAlgoState), recorder, req, vars) + require.Error(t, err) + } + } +} + +// Rotating the key for a valid role and gun succeeds +func TestRotateKeyHandlerSuccessfulRotation(t *testing.T) { + state := defaultState() + roles := []string{data.CanonicalTimestampRole, data.CanonicalSnapshotRole} + req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} + + for _, role := range roles { + vars := map[string]string{"imageName": "gun", "tufRole": role} + recorder := httptest.NewRecorder() + err := rotateKeyHandler(getContext(state), recorder, req, vars) + require.NoError(t, err) + require.True(t, len(recorder.Body.String()) > 0) + } +} + func TestGetHandlerRoot(t *testing.T) { metaStore := storage.NewMemStorage() repo, _, err := testutils.EmptyRepo("gun") diff --git a/server/handlers/validation_test.go b/server/handlers/validation_test.go index afda9c796..afd156f53 100644 --- a/server/handlers/validation_test.go +++ b/server/handlers/validation_test.go @@ -675,13 +675,6 @@ func TestValidateSnapshotGenerateNoPrev(t *testing.T) { repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() - snapRole, err := repo.GetBaseRole(data.CanonicalSnapshotRole) - require.NoError(t, err) - - for _, k := range snapRole.Keys { - err := store.SetKey(gun, data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) - require.NoError(t, err) - } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) @@ -700,13 +693,6 @@ func TestValidateSnapshotGenerateWithPrev(t *testing.T) { repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() - snapRole, err := repo.GetBaseRole(data.CanonicalSnapshotRole) - require.NoError(t, err) - - for _, k := range snapRole.Keys { - err := store.SetKey(gun, data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) - require.NoError(t, err) - } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) @@ -742,13 +728,6 @@ func TestValidateSnapshotGeneratePrevCorrupt(t *testing.T) { repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() - snapRole, err := repo.GetBaseRole(data.CanonicalSnapshotRole) - require.NoError(t, err) - - for _, k := range snapRole.Keys { - err := store.SetKey(gun, data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) - require.NoError(t, err) - } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) @@ -778,13 +757,6 @@ func TestValidateSnapshotGenerateStoreGetCurrentSnapshotBroken(t *testing.T) { MetaStore: storage.NewMemStorage(), errsToReturn: map[string]error{data.CanonicalSnapshotRole: data.ErrNoSuchRole{}}, } - snapRole, err := repo.GetBaseRole(data.CanonicalSnapshotRole) - require.NoError(t, err) - - for _, k := range snapRole.Keys { - err := store.SetKey(gun, data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) - require.NoError(t, err) - } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) @@ -804,13 +776,6 @@ func TestValidateSnapshotGenerateNoTargets(t *testing.T) { repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() - snapRole, err := repo.GetBaseRole(data.CanonicalSnapshotRole) - require.NoError(t, err) - - for _, k := range snapRole.Keys { - err := store.SetKey(gun, data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) - require.NoError(t, err) - } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) @@ -829,13 +794,6 @@ func TestValidateSnapshotGenerate(t *testing.T) { repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() - snapRole, err := repo.GetBaseRole(data.CanonicalSnapshotRole) - require.NoError(t, err) - - for _, k := range snapRole.Keys { - err := store.SetKey(gun, data.CanonicalSnapshotRole, k.Algorithm(), k.Public()) - require.NoError(t, err) - } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) diff --git a/server/server.go b/server/server.go index 324dc7b1f..35d32df67 100644 --- a/server/server.go +++ b/server/server.go @@ -173,6 +173,13 @@ func RootHandler(ac auth.AccessController, ctx context.Context, trust signed.Cry ServerHandler: handlers.GetKeyHandler, PermissionsRequired: []string{"push", "pull"}, })) + r.Methods("POST").Path( + "/v2/{imageName:.*}/_trust/tuf/{tufRole:snapshot|timestamp}.key").Handler(createHandler(_serverEndpoint{ + OperationName: "RotateKey", + ErrorIfGUNInvalid: notFoundError, + ServerHandler: handlers.RotateKeyHandler, + PermissionsRequired: []string{"*"}, + })) r.Methods("DELETE").Path("/v2/{imageName:.*}/_trust/tuf/").Handler(createHandler(_serverEndpoint{ OperationName: "DeleteTUF", ErrorIfGUNInvalid: notFoundError, diff --git a/server/server_test.go b/server/server_test.go index ea117f399..c31d19a58 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -304,3 +304,32 @@ func verifyGetResponse(t *testing.T, r *http.Response, expectedBytes []byte) { require.NotEqual(t, "", r.Header.Get("Last-Modified")) require.Equal(t, "", r.Header.Get("Pragma")) } + +// RotateKey supports only timestamp and snapshot key rotation +func TestRotateKeyEndpoint(t *testing.T) { + ctx := context.WithValue( + context.Background(), "metaStore", storage.NewMemStorage()) + ctx = context.WithValue(ctx, "keyAlgorithm", data.ED25519Key) + + ccc := utils.NewCacheControlConfig(10, false) + handler := RootHandler(nil, ctx, signed.NewEd25519(), ccc, ccc, nil) + ts := httptest.NewServer(handler) + defer ts.Close() + + rolesToStatus := map[string]int{ + data.CanonicalTimestampRole: http.StatusOK, + data.CanonicalSnapshotRole: http.StatusOK, + data.CanonicalTargetsRole: http.StatusNotFound, + data.CanonicalRootRole: http.StatusNotFound, + "targets/delegation": http.StatusNotFound, + "somerandomrole": http.StatusNotFound, + } + var buf bytes.Buffer + for role, expectedStatus := range rolesToStatus { + res, err := http.Post( + fmt.Sprintf("%s/v2/gun/_trust/tuf/%s.key", ts.URL, role), + "text/plain", &buf) + require.NoError(t, err) + require.Equal(t, expectedStatus, res.StatusCode) + } +} diff --git a/server/snapshot/snapshot.go b/server/snapshot/snapshot.go index aff585822..d752cf0e2 100644 --- a/server/snapshot/snapshot.go +++ b/server/snapshot/snapshot.go @@ -16,33 +16,49 @@ import ( // GetOrCreateSnapshotKey either creates a new snapshot key, or returns // the existing one. Only the PublicKey is returned. The private part // is held by the CryptoService. -func GetOrCreateSnapshotKey(gun string, store storage.KeyStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { - keyAlgorithm, public, err := store.GetKey(gun, data.CanonicalSnapshotRole) - if err == nil { - return data.NewPublicKey(keyAlgorithm, public), nil - } - - if _, ok := err.(*storage.ErrNoKey); ok { - key, err := crypto.Create(data.CanonicalSnapshotRole, gun, createAlgorithm) - if err != nil { +func GetOrCreateSnapshotKey(gun string, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { + _, rootJSON, err := store.GetCurrent(gun, data.CanonicalRootRole) + if err != nil { + // If the error indicates we couldn't find the root, create a new key + if _, ok := err.(storage.ErrNotFound); !ok { + logrus.Errorf("Error when retrieving root role for GUN %s: %v", gun, err) return nil, err } - logrus.Debug("Creating new snapshot key for ", gun, ". With algo: ", key.Algorithm()) - err = store.SetKey(gun, data.CanonicalSnapshotRole, key.Algorithm(), key.Public()) - if err == nil { - return key, nil - } + return crypto.Create(data.CanonicalSnapshotRole, gun, createAlgorithm) + } + + // If we have a current root, parse out the public key for the snapshot role, and return it + repoSignedRoot := new(data.SignedRoot) + if err := json.Unmarshal(rootJSON, repoSignedRoot); err != nil { + logrus.Errorf("Failed to unmarshal existing root for GUN %s to retrieve snapshot key ID", gun) + return nil, err + } - if _, ok := err.(*storage.ErrKeyExists); ok { - keyAlgorithm, public, err = store.GetKey(gun, data.CanonicalSnapshotRole) - if err != nil { - return nil, err - } - return data.NewPublicKey(keyAlgorithm, public), nil + snapshotRole, err := repoSignedRoot.BuildBaseRole(data.CanonicalSnapshotRole) + if err != nil { + logrus.Errorf("Failed to extract snapshot role from root for GUN %s", gun) + return nil, err + } + + // We currently only support single keys for snapshot and timestamp, so we can return the first and only key in the map if the signer has it + for keyID := range snapshotRole.Keys { + if pubKey := crypto.GetKey(keyID); pubKey != nil { + return pubKey, nil } + } + logrus.Debugf("Failed to find any snapshot keys in cryptosigner from root for GUN %s, generating new key", gun) + return crypto.Create(data.CanonicalSnapshotRole, gun, createAlgorithm) +} + +// RotateSnapshotKey attempts to rotate a snapshot key in the signer, but might be rate-limited by the signer +func RotateSnapshotKey(gun string, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { + // Always attempt to create a new key, but this might be rate-limited + key, err := crypto.Create(data.CanonicalSnapshotRole, gun, createAlgorithm) + if err != nil { return nil, err } - return nil, err + logrus.Debug("Created new pending snapshot key ", key.ID(), "to rotate to for ", gun, ". With algo: ", key.Algorithm()) + return key, nil } // GetOrCreateSnapshot either returns the existing latest snapshot, or uses diff --git a/server/snapshot/snapshot_test.go b/server/snapshot/snapshot_test.go index 9bc5db149..2eac5b5f3 100644 --- a/server/snapshot/snapshot_test.go +++ b/server/snapshot/snapshot_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "fmt" "testing" "time" @@ -46,60 +47,67 @@ func TestGetSnapshotKeyCreate(t *testing.T) { require.Nil(t, err, "Expected nil error") - // trying to get the same key again should return the same value - require.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") + // trying to get the key for the same gun and role will create a new key unless we have existing TUF metadata + require.NotEqual(t, k, k2, "Did not receive same key when attempting to recreate.") require.NotNil(t, k2, "Key should not be nil") } -func TestGetSnapshotKeyExisting(t *testing.T) { - store := storage.NewMemStorage() - crypto := signed.NewEd25519() - key, err := crypto.Create(data.CanonicalSnapshotRole, "gun", data.ED25519Key) - require.NoError(t, err) +type FailingStore struct { + *storage.MemStorage +} - store.SetKey("gun", data.CanonicalSnapshotRole, data.ED25519Key, key.Public()) +func (f FailingStore) GetCurrent(role, gun string) (*time.Time, []byte, error) { + return nil, nil, fmt.Errorf("failing store failed") +} +func TestGetSnapshotKeyCreateWithFailingStore(t *testing.T) { + store := FailingStore{storage.NewMemStorage()} + crypto := signed.NewEd25519() k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) - require.Nil(t, err, "Expected nil error") - require.NotNil(t, k, "Key should not be nil") - require.Equal(t, key, k, "Did not receive same key when attempting to recreate.") - require.NotNil(t, k, "Key should not be nil") - - k2, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) - - require.Nil(t, err, "Expected nil error") - - // trying to get the same key again should return the same value - require.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") - require.NotNil(t, k2, "Key should not be nil") + require.Error(t, err, "Expected error") + require.Nil(t, k, "Key should be nil") } -type keyStore struct { - getCalled bool - k data.PublicKey +type CorruptedStore struct { + *storage.MemStorage } -func (ks *keyStore) GetKey(gun, role string) (string, []byte, error) { - defer func() { ks.getCalled = true }() - if ks.getCalled { - return ks.k.Algorithm(), ks.k.Public(), nil - } - return "", nil, &storage.ErrNoKey{} +func (c CorruptedStore) GetCurrent(role, gun string) (*time.Time, []byte, error) { + return &time.Time{}, []byte("junk"), nil } -func (ks keyStore) SetKey(gun, role, algorithm string, public []byte) error { - return &storage.ErrKeyExists{} +func TestGetSnapshotKeyCreateWithCorruptedStore(t *testing.T) { + store := CorruptedStore{storage.NewMemStorage()} + crypto := signed.NewEd25519() + k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + require.Error(t, err, "Expected error") + require.Nil(t, k, "Key should be nil") } -// Tests the race condition where the server is being asked to generate a new key -// by 2 parallel requests and the second insert to be executed by the DB fails -// due to duplicate key (gun + role). It should then return the key added by the -// first insert. -func TestGetSnapshotKeyExistsOnSet(t *testing.T) { +func TestGetSnapshotKeyCreateWithInvalidAlgo(t *testing.T) { + store := storage.NewMemStorage() crypto := signed.NewEd25519() - key, err := crypto.Create(data.CanonicalSnapshotRole, "gun", data.ED25519Key) + k, err := GetOrCreateSnapshotKey("gun", store, crypto, "notactuallyanalgorithm") + require.Error(t, err, "Expected error") + require.Nil(t, k, "Key should be nil") +} + +func TestGetSnapshotKeyExistingMetadata(t *testing.T) { + repo, crypto, err := testutils.EmptyRepo("gun") + require.NoError(t, err) + + sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + require.NoError(t, err) + rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) - store := &keyStore{k: key} + store := storage.NewMemStorage() + require.NoError(t, + store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) + + snapshotRole, err := repo.Root.BuildBaseRole(data.CanonicalSnapshotRole) + require.NoError(t, err) + key, ok := snapshotRole.Keys[repo.Root.Signed.Roles[data.CanonicalSnapshotRole].KeyIDs[0]] + require.True(t, ok) k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") @@ -111,9 +119,16 @@ func TestGetSnapshotKeyExistsOnSet(t *testing.T) { require.Nil(t, err, "Expected nil error") - // trying to get the same key again should return the same value require.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") require.NotNil(t, k2, "Key should not be nil") + + // try wiping out the cryptoservice data, and ensure we create a new key because the signer doesn't hold the key specified by TUF + crypto = signed.NewEd25519() + k3, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) + require.Nil(t, err, "Expected nil error") + require.NotEqual(t, k, k3, "Received same key when attempting to recreate.") + require.NotEqual(t, k2, k3, "Received same key when attempting to recreate.") + require.NotNil(t, k3, "Key should not be nil") } // If there is no previous snapshot or the previous snapshot is corrupt, then @@ -140,11 +155,6 @@ func TestGetSnapshotNoPreviousSnapshot(t *testing.T) { storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: snapshotJSON})) } - // create a key to be used by GetOrCreateSnapshot - key, err := crypto.Create(data.CanonicalSnapshotRole, "gun", data.ECDSAKey) - require.NoError(t, err) - require.NoError(t, store.SetKey("gun", data.CanonicalSnapshotRole, key.Algorithm(), key.Public())) - hashBytes := sha256.Sum256(snapshotJSON) hashHex := hex.EncodeToString(hashBytes[:]) diff --git a/server/storage/errors.go b/server/storage/errors.go index 04c07cfce..abee09a9f 100644 --- a/server/storage/errors.go +++ b/server/storage/errors.go @@ -4,10 +4,10 @@ import ( "fmt" ) -// ErrOldVersion is returned when a newer version of TUF metadada is already available +// ErrOldVersion is returned when a newer version of TUF metadata is already available type ErrOldVersion struct{} -// ErrOldVersion is returned when a newer version of TUF metadada is already available +// ErrOldVersion is returned when a newer version of TUF metadata is already available func (err ErrOldVersion) Error() string { return fmt.Sprintf("Error updating metadata. A newer version is already available") } diff --git a/server/storage/interface.go b/server/storage/interface.go index 965e26381..0c11a93ee 100644 --- a/server/storage/interface.go +++ b/server/storage/interface.go @@ -39,6 +39,4 @@ type MetaStore interface { // Delete removes all metadata for a given GUN. It does not return an // error if no metadata exists for the given GUN. Delete(gun string) error - - KeyStore } diff --git a/server/storage/memory.go b/server/storage/memory.go index 1fd35637a..c75036843 100644 --- a/server/storage/memory.go +++ b/server/storage/memory.go @@ -158,42 +158,6 @@ func (st *MemStorage) Delete(gun string) error { return nil } -// GetKey returns the public key material of the timestamp key of a given gun -func (st *MemStorage) GetKey(gun, role string) (algorithm string, public []byte, err error) { - // no need for lock. It's ok to return nil if an update - // wasn't observed - g, ok := st.keys[gun] - if !ok { - return "", nil, &ErrNoKey{gun: gun} - } - k, ok := g[role] - if !ok { - return "", nil, &ErrNoKey{gun: gun} - } - - return k.algorithm, k.public, nil -} - -// SetKey sets a key under a gun and role -func (st *MemStorage) SetKey(gun, role, algorithm string, public []byte) error { - k := &key{algorithm: algorithm, public: public} - st.lock.Lock() - defer st.lock.Unlock() - - // we hold the lock so nothing will be able to race to write a key - // between checking and setting - _, _, err := st.GetKey(gun, role) - if _, ok := err.(*ErrNoKey); !ok { - return &ErrKeyExists{gun: gun, role: role} - } - _, ok := st.keys[gun] - if !ok { - st.keys[gun] = make(map[string]*key) - } - st.keys[gun][role] = k - return nil -} - func entryKey(gun, role string) string { return fmt.Sprintf("%s.%s", gun, role) } diff --git a/server/storage/memory_test.go b/server/storage/memory_test.go index fe51d1af1..12ca12108 100644 --- a/server/storage/memory_test.go +++ b/server/storage/memory_test.go @@ -5,7 +5,6 @@ package storage import ( "testing" - "github.com/docker/notary/tuf/data" "github.com/stretchr/testify/require" ) @@ -85,60 +84,6 @@ func TestGetCurrent(t *testing.T) { require.Equal(t, []byte("test"), d, "Data was incorrect") } -func TestGetTimestampKey(t *testing.T) { - s := NewMemStorage() - - s.SetKey("gun", data.CanonicalTimestampRole, data.RSAKey, []byte("test")) - - c, k, err := s.GetKey("gun", data.CanonicalTimestampRole) - require.Nil(t, err, "Expected error to be nil") - require.Equal(t, data.RSAKey, c, "Expected algorithm rsa, received %s", c) - require.Equal(t, []byte("test"), k, "Key data was wrong") -} - -func TestSetKey(t *testing.T) { - s := NewMemStorage() - err := s.SetKey("gun", data.CanonicalTimestampRole, data.RSAKey, []byte("test")) - require.NoError(t, err) - - k := s.keys["gun"][data.CanonicalTimestampRole] - require.Equal(t, data.RSAKey, k.algorithm, "Expected algorithm to be rsa, received %s", k.algorithm) - require.Equal(t, []byte("test"), k.public, "Public key did not match expected") - -} - -func TestSetKeyMultipleRoles(t *testing.T) { - s := NewMemStorage() - err := s.SetKey("gun", data.CanonicalTimestampRole, data.RSAKey, []byte("test")) - require.NoError(t, err) - - err = s.SetKey("gun", data.CanonicalSnapshotRole, data.RSAKey, []byte("test")) - require.NoError(t, err) - - k := s.keys["gun"][data.CanonicalTimestampRole] - require.Equal(t, data.RSAKey, k.algorithm, "Expected algorithm to be rsa, received %s", k.algorithm) - require.Equal(t, []byte("test"), k.public, "Public key did not match expected") - - k = s.keys["gun"][data.CanonicalSnapshotRole] - require.Equal(t, data.RSAKey, k.algorithm, "Expected algorithm to be rsa, received %s", k.algorithm) - require.Equal(t, []byte("test"), k.public, "Public key did not match expected") -} - -func TestSetKeySameRoleGun(t *testing.T) { - s := NewMemStorage() - err := s.SetKey("gun", data.CanonicalTimestampRole, data.RSAKey, []byte("test")) - require.NoError(t, err) - - // set diff algo and bytes so we can confirm data didn't get replaced - err = s.SetKey("gun", data.CanonicalTimestampRole, data.ECDSAKey, []byte("test2")) - require.IsType(t, &ErrKeyExists{}, err, "Expected err to be ErrKeyExists") - - k := s.keys["gun"][data.CanonicalTimestampRole] - require.Equal(t, data.RSAKey, k.algorithm, "Expected algorithm to be rsa, received %s", k.algorithm) - require.Equal(t, []byte("test"), k.public, "Public key did not match expected") - -} - func TestGetChecksumNotFound(t *testing.T) { s := NewMemStorage() _, _, err := s.GetChecksum("gun", "root", "12345") diff --git a/server/storage/mysql_test.go b/server/storage/mysql_test.go index 1dac141f7..3194f4ea9 100644 --- a/server/storage/mysql_test.go +++ b/server/storage/mysql_test.go @@ -43,7 +43,6 @@ func init() { // drop all tables, if they exist gormDB.DropTable(&TUFFile{}) - gormDB.DropTable(&Key{}) } cleanup1() dbStore := SetupSQLDB(t, "mysql", dburl) diff --git a/server/storage/rethink_realdb_test.go b/server/storage/rethink_realdb_test.go index ca77bbedd..4a913e130 100644 --- a/server/storage/rethink_realdb_test.go +++ b/server/storage/rethink_realdb_test.go @@ -36,7 +36,6 @@ func rethinkDBSetup(t *testing.T) (RethinkDB, func()) { cleanup() require.NoError(t, rethinkdb.SetupDB(session, dbName, []rethinkdb.Table{ TUFFilesRethinkTable, - PubKeysRethinkTable, })) return NewRethinkDBStorage(dbName, "", "", session), cleanup } @@ -98,10 +97,6 @@ func TestRethinkCheckHealth(t *testing.T) { require.NoError(t, gorethink.DB(dbStore.dbName).TableDrop(TUFFilesRethinkTable.Name).Exec(dbStore.sess)) require.Error(t, dbStore.CheckHealth()) - // No tables, health check fails - require.NoError(t, gorethink.DB(dbStore.dbName).TableDrop(PubKeysRethinkTable.Name).Exec(dbStore.sess)) - require.Error(t, dbStore.CheckHealth()) - // No DB, health check fails cleanup() require.Error(t, dbStore.CheckHealth()) diff --git a/server/storage/rethinkdb.go b/server/storage/rethinkdb.go index 8383b56b9..1a27176d4 100644 --- a/server/storage/rethinkdb.go +++ b/server/storage/rethinkdb.go @@ -31,20 +31,6 @@ func (r RDBTUFFile) TableName() string { return "tuf_files" } -// RDBKey is the public key record -type RDBKey struct { - rethinkdb.Timing - Gun string `gorethink:"gun"` - Role string `gorethink:"role"` - Cipher string `gorethink:"cipher"` - Public []byte `gorethink:"public"` -} - -// TableName returns the table name for the record type -func (r RDBKey) TableName() string { - return "tuf_keys" -} - // gorethink can't handle an UnmarshalJSON function (see https://github.com/dancannon/gorethink/issues/201), // so do this here in an anonymous struct func rdbTUFFileFromJSON(data []byte) (interface{}, error) { @@ -78,14 +64,6 @@ func rdbTUFFileFromJSON(data []byte) (interface{}, error) { }, nil } -func rdbKeyFromJSON(data []byte) (interface{}, error) { - rdb := RDBKey{} - if err := json.Unmarshal(data, &rdb); err != nil { - return RDBKey{}, err - } - return rdb, nil -} - // RethinkDB implements a MetaStore against the Rethink Database type RethinkDB struct { dbName string @@ -104,42 +82,6 @@ func NewRethinkDBStorage(dbName, user, password string, sess *gorethink.Session) } } -// GetKey returns the cipher and public key for the given GUN and role. -// If the GUN+role don't exist, returns an error. -func (rdb RethinkDB) GetKey(gun, role string) (cipher string, public []byte, err error) { - var key RDBKey - res, err := gorethink.DB(rdb.dbName).Table(key.TableName()).GetAllByIndex( - rdbGunRoleIdx, []string{gun, role}, - ).Run(rdb.sess) - if err != nil { - return "", nil, err - } - defer res.Close() - err = res.One(&key) - if err == gorethink.ErrEmptyResult { - return "", nil, &ErrNoKey{gun: gun} - } - return key.Cipher, key.Public, err -} - -// SetKey sets the cipher and public key for the given GUN and role if -// it doesn't already exist. Otherwise an error is returned. -func (rdb RethinkDB) SetKey(gun, role, cipher string, public []byte) error { - now := time.Now() - key := RDBKey{ - Timing: rethinkdb.Timing{ - CreatedAt: now, - UpdatedAt: now, - }, - Gun: gun, - Role: role, - Cipher: cipher, - Public: public, - } - _, err := gorethink.DB(rdb.dbName).Table(key.TableName()).Insert(key).RunWrite(rdb.sess) - return err -} - // UpdateCurrent adds new metadata version for the given GUN if and only // if it's a new role, or the version is greater than the current version // for the role. Otherwise an error is returned. @@ -317,7 +259,6 @@ func (rdb RethinkDB) deleteByTSChecksum(tsChecksum string) error { func (rdb RethinkDB) Bootstrap() error { if err := rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{ TUFFilesRethinkTable, - PubKeysRethinkTable, }); err != nil { return err } @@ -326,12 +267,10 @@ func (rdb RethinkDB) Bootstrap() error { // CheckHealth checks that all tables and databases exist and are query-able func (rdb RethinkDB) CheckHealth() error { - for _, table := range []string{TUFFilesRethinkTable.Name, PubKeysRethinkTable.Name} { - res, err := gorethink.DB(rdb.dbName).Table(table).Info().Run(rdb.sess) - if err != nil { - return fmt.Errorf("%s is unavailable, or missing one or more tables, or permissions are incorrectly set", rdb.dbName) - } - defer res.Close() + res, err := gorethink.DB(rdb.dbName).Table(TUFFilesRethinkTable.Name).Info().Run(rdb.sess) + if err != nil { + return fmt.Errorf("%s is unavailable, or missing one or more tables, or permissions are incorrectly set", rdb.dbName) } + defer res.Close() return nil } diff --git a/server/storage/rethinkdb_models.go b/server/storage/rethinkdb_models.go index 8e60f8e8d..e9db28e45 100644 --- a/server/storage/rethinkdb_models.go +++ b/server/storage/rethinkdb_models.go @@ -29,14 +29,4 @@ var ( }, JSONUnmarshaller: rdbTUFFileFromJSON, } - - // PubKeysRethinkTable is the table definition of notary server's public key information for TUF roles - PubKeysRethinkTable = rethinkdb.Table{ - Name: RDBKey{}.TableName(), - PrimaryKey: "id", - SecondaryIndexes: map[string][]string{ - rdbGunRoleIdx: {"gun", "role"}, - }, - JSONUnmarshaller: rdbKeyFromJSON, - } ) diff --git a/server/storage/rethinkdb_test.go b/server/storage/rethinkdb_test.go index dbaca2eab..c2f27b084 100644 --- a/server/storage/rethinkdb_test.go +++ b/server/storage/rethinkdb_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/docker/notary/storage/rethinkdb" "github.com/stretchr/testify/require" ) @@ -118,69 +117,3 @@ func TestRDBTUFFileJSONUnmarshallingFailure(t *testing.T) { require.Error(t, err) } } - -func TestRDBTUFKeyJSONUnmarshalling(t *testing.T) { - rdb := RDBKey{ - Timing: rethinkdb.Timing{ - CreatedAt: time.Now().AddDate(-1, -1, -1), - UpdatedAt: time.Now().AddDate(0, -5, 0), - DeletedAt: time.Time{}, - }, - Gun: "namespaced/name", - Role: "timestamp", - Cipher: "ecdsa", - Public: []byte("Hello world"), - } - jsonBytes, err := json.Marshal(rdb) - require.NoError(t, err) - - unmarshalledAnon, err := PubKeysRethinkTable.JSONUnmarshaller(jsonBytes) - require.NoError(t, err) - unmarshalled, ok := unmarshalledAnon.(RDBKey) - require.True(t, ok) - - // There is some weirdness with comparing time.Time due to a location pointer, - // so let's use time.Time's equal function to compare times, and then re-assign - // the timing struct to compare the rest of the RDBTUFFile struct - require.True(t, rdb.CreatedAt.Equal(unmarshalled.CreatedAt)) - require.True(t, rdb.UpdatedAt.Equal(unmarshalled.UpdatedAt)) - require.True(t, rdb.DeletedAt.Equal(unmarshalled.DeletedAt)) - unmarshalled.Timing = rdb.Timing - - require.Equal(t, rdb, unmarshalled) -} - -func TestRDBKeyJSONUnmarshallingFailure(t *testing.T) { - validTimeMarshalled, err := json.Marshal(time.Now()) - require.NoError(t, err) - dataMarshalled, err := json.Marshal([]byte("Hello world!")) - require.NoError(t, err) - - invalids := []string{ - fmt.Sprintf(` - { - "created_at": "not a time", - "updated_at": %s, - "deleted_at": %s, - "gun": "namespaced/name", - "role": "timestamp", - "cipher": "ecdsa", - "public": %s, - }`, validTimeMarshalled, validTimeMarshalled, dataMarshalled), - fmt.Sprintf(` - { - "created_at": %s, - "updated_at": %s, - "deleted_at": %s, - "gun": "namespaced/name", - "role": "timestamp", - "cipher": "ecdsa", - "public": 12345, - }`, validTimeMarshalled, validTimeMarshalled, validTimeMarshalled), - } - - for _, invalid := range invalids { - _, err := PubKeysRethinkTable.JSONUnmarshaller([]byte(invalid)) - require.Error(t, err) - } -} diff --git a/server/storage/sql_models.go b/server/storage/sql_models.go index c475add40..21ef72c4c 100644 --- a/server/storage/sql_models.go +++ b/server/storage/sql_models.go @@ -17,20 +17,6 @@ func (g TUFFile) TableName() string { return "tuf_files" } -// Key represents a single timestamp key in the database -type Key struct { - gorm.Model - Gun string `sql:"type:varchar(255);not null;unique_index:gun_role"` - Role string `sql:"type:varchar(255);not null;unique_index:gun_role"` - Cipher string `sql:"type:varchar(30);not null"` - Public []byte `sql:"type:blob;not null"` -} - -// TableName sets a specific table name for our TimestampKey -func (g Key) TableName() string { - return "timestamp_keys" -} - // CreateTUFTable creates the DB table for TUFFile func CreateTUFTable(db gorm.DB) error { // TODO: gorm @@ -45,17 +31,3 @@ func CreateTUFTable(db gorm.DB) error { } return nil } - -// CreateKeyTable creates the DB table for TUFFile -func CreateKeyTable(db gorm.DB) error { - query := db.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8").CreateTable(&Key{}) - if query.Error != nil { - return query.Error - } - query = db.Model(&Key{}).AddUniqueIndex( - "idx_gun_role", "gun", "role") - if query.Error != nil { - return query.Error - } - return nil -} diff --git a/server/storage/sqldb.go b/server/storage/sqldb.go index 4f1b89893..e56bb4e74 100644 --- a/server/storage/sqldb.go +++ b/server/storage/sqldb.go @@ -159,56 +159,15 @@ func (db *SQLStorage) Delete(gun string) error { return db.Unscoped().Where(&TUFFile{Gun: gun}).Delete(TUFFile{}).Error } -// GetKey returns the Public Key data for a gun+role -func (db *SQLStorage) GetKey(gun, role string) (algorithm string, public []byte, err error) { - logrus.Debugf("retrieving timestamp key for %s:%s", gun, role) - - var row Key - query := db.Select("cipher, public").Where(&Key{Gun: gun, Role: role}).Find(&row) - - if query.RecordNotFound() { - return "", nil, &ErrNoKey{gun: gun} - } else if query.Error != nil { - return "", nil, query.Error - } - - return row.Cipher, row.Public, nil -} - -// SetKey attempts to write a key and returns an error if it already exists for the gun and role -func (db *SQLStorage) SetKey(gun, role, algorithm string, public []byte) error { - - entry := Key{ - Gun: gun, - Role: role, - } - - if !db.Where(&entry).First(&Key{}).RecordNotFound() { - return &ErrKeyExists{gun: gun, role: role} - } - - entry.Cipher = algorithm - entry.Public = public - - return translateOldVersionError( - db.FirstOrCreate(&Key{}, &entry).Error) -} - -// CheckHealth asserts that both required tables are present +// CheckHealth asserts that the tuf_files table is present func (db *SQLStorage) CheckHealth() error { - interfaces := []interface { - TableName() string - }{&TUFFile{}, &Key{}} - - for _, model := range interfaces { - tableOk := db.HasTable(model) - if db.Error != nil { - return db.Error - } - if !tableOk { - return fmt.Errorf( - "Cannot access table: %s", model.TableName()) - } + tableOk := db.HasTable(&TUFFile{}) + if db.Error != nil { + return db.Error + } + if !tableOk { + return fmt.Errorf( + "Cannot access table: %s", TUFFile{}.TableName()) } return nil } diff --git a/server/storage/sqldb_test.go b/server/storage/sqldb_test.go index 53d21e0ef..c3ccf843a 100644 --- a/server/storage/sqldb_test.go +++ b/server/storage/sqldb_test.go @@ -22,16 +22,11 @@ func SetupSQLDB(t *testing.T, dbtype, dburl string) *SQLStorage { err = CreateTUFTable(dbStore.DB) require.NoError(t, err) - err = CreateKeyTable(dbStore.DB) - require.NoError(t, err) - // verify that the tables are empty var count int - for _, model := range [2]interface{}{&TUFFile{}, &Key{}} { - query := dbStore.DB.Model(model).Count(&count) - require.NoError(t, query.Error) - require.Equal(t, 0, count) - } + query := dbStore.DB.Model(&TUFFile{}).Count(&count) + require.NoError(t, query.Error) + require.Equal(t, 0, count) return dbStore } @@ -139,148 +134,16 @@ func TestSQLDelete(t *testing.T) { dbStore.DB.Close() } -func TestSQLGetKeyNoKey(t *testing.T) { - dbStore, cleanup := sqldbSetup(t) - defer cleanup() - - cipher, public, err := dbStore.GetKey("testGUN", data.CanonicalTimestampRole) - require.Equal(t, "", cipher) - require.Nil(t, public) - require.IsType(t, &ErrNoKey{}, err, - "Expected ErrNoKey from GetKey") - - query := dbStore.DB.Create(&Key{ - Gun: "testGUN", - Role: data.CanonicalTimestampRole, - Cipher: "testCipher", - Public: []byte("1"), - }) - require.NoError( - t, query.Error, "Inserting timestamp into empty DB should succeed") - - cipher, public, err = dbStore.GetKey("testGUN", data.CanonicalTimestampRole) - require.NoError(t, err) - require.Equal(t, "testCipher", cipher, - "Returned cipher was incorrect") - require.Equal(t, []byte("1"), public, "Returned pubkey was incorrect") -} - -func TestSQLSetKeyExists(t *testing.T) { - dbStore, cleanup := sqldbSetup(t) - defer cleanup() - - err := dbStore.SetKey("testGUN", data.CanonicalTimestampRole, "testCipher", []byte("1")) - require.NoError(t, err, "Inserting timestamp into empty DB should succeed") - - err = dbStore.SetKey("testGUN", data.CanonicalTimestampRole, "testCipher", []byte("1")) - require.Error(t, err) - require.IsType(t, &ErrKeyExists{}, err, - "Expected ErrKeyExists from SetKey") - - var rows []Key - query := dbStore.DB.Select("id, gun, cipher, public").Find(&rows) - require.NoError(t, query.Error) - - expected := Key{Gun: "testGUN", Cipher: "testCipher", - Public: []byte("1")} - expected.Model = gorm.Model{ID: 1} - - require.Equal(t, []Key{expected}, rows) - - dbStore.DB.Close() -} - -func TestSQLSetKeyMultipleRoles(t *testing.T) { - dbStore, cleanup := sqldbSetup(t) - defer cleanup() - - err := dbStore.SetKey("testGUN", data.CanonicalTimestampRole, "testCipher", []byte("1")) - require.NoError(t, err, "Inserting timestamp into empty DB should succeed") - - err = dbStore.SetKey("testGUN", data.CanonicalSnapshotRole, "testCipher", []byte("1")) - require.NoError(t, err, "Inserting snapshot key into DB with timestamp key should succeed") - - var rows []Key - query := dbStore.DB.Select("id, gun, role, cipher, public").Find(&rows) - require.NoError(t, query.Error) - - expectedTS := Key{Gun: "testGUN", Role: "timestamp", Cipher: "testCipher", - Public: []byte("1")} - expectedTS.Model = gorm.Model{ID: 1} - - expectedSN := Key{Gun: "testGUN", Role: "snapshot", Cipher: "testCipher", - Public: []byte("1")} - expectedSN.Model = gorm.Model{ID: 2} - - require.Equal(t, []Key{expectedTS, expectedSN}, rows) - - dbStore.DB.Close() -} - -func TestSQLSetKeyMultipleGuns(t *testing.T) { - dbStore, cleanup := sqldbSetup(t) - defer cleanup() - - err := dbStore.SetKey("testGUN", data.CanonicalTimestampRole, "testCipher", []byte("1")) - require.NoError(t, err, "Inserting timestamp into empty DB should succeed") - - err = dbStore.SetKey("testAnotherGUN", data.CanonicalTimestampRole, "testCipher", []byte("1")) - require.NoError(t, err, "Inserting snapshot key into DB with timestamp key should succeed") - - var rows []Key - query := dbStore.DB.Select("id, gun, role, cipher, public").Find(&rows) - require.NoError(t, query.Error) - - expected1 := Key{Gun: "testGUN", Role: "timestamp", Cipher: "testCipher", - Public: []byte("1")} - expected1.Model = gorm.Model{ID: 1} - - expected2 := Key{Gun: "testAnotherGUN", Role: "timestamp", Cipher: "testCipher", - Public: []byte("1")} - expected2.Model = gorm.Model{ID: 2} - - require.Equal(t, []Key{expected1, expected2}, rows) - - dbStore.DB.Close() -} - -func TestSQLSetKeySameRoleGun(t *testing.T) { - dbStore, cleanup := sqldbSetup(t) - defer cleanup() - - err := dbStore.SetKey("testGUN", data.CanonicalTimestampRole, "testCipher", []byte("1")) - require.NoError(t, err, "Inserting timestamp into empty DB should succeed") - - err = dbStore.SetKey("testGUN", data.CanonicalTimestampRole, "testCipher", []byte("2")) - require.Error(t, err) - require.IsType(t, &ErrKeyExists{}, err, - "Expected ErrKeyExists from SetKey") - - dbStore.DB.Close() -} - -// TestSQLDBCheckHealthTableMissing asserts that the health check fails if one or -// both the tables are missing. +// TestSQLDBCheckHealthTableMissing asserts that the health check fails if the table is missing func TestSQLDBCheckHealthTableMissing(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() dbStore.DropTable(&TUFFile{}) - dbStore.DropTable(&Key{}) // No tables, health check fails err := dbStore.CheckHealth() require.Error(t, err, "Cannot access table:") - - // only one table existing causes health check to fail - CreateTUFTable(dbStore.DB) - err = dbStore.CheckHealth() - require.Error(t, err, "Cannot access table:") - dbStore.DropTable(&TUFFile{}) - - CreateKeyTable(dbStore.DB) - err = dbStore.CheckHealth() - require.Error(t, err, "Cannot access table:") } // TestSQLDBCheckHealthDBConnection asserts that if the DB is not connectable, the diff --git a/server/timestamp/timestamp.go b/server/timestamp/timestamp.go index 0ddb1942a..9a69804b1 100644 --- a/server/timestamp/timestamp.go +++ b/server/timestamp/timestamp.go @@ -22,32 +22,48 @@ import ( // create the key at the same time by simply querying the store a second time if it // receives a conflict when writing. func GetOrCreateTimestampKey(gun string, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { - keyAlgorithm, public, err := store.GetKey(gun, data.CanonicalTimestampRole) - if err == nil { - return data.NewPublicKey(keyAlgorithm, public), nil - } - - if _, ok := err.(*storage.ErrNoKey); ok { - key, err := crypto.Create("timestamp", gun, createAlgorithm) - if err != nil { + _, rootJSON, err := store.GetCurrent(gun, data.CanonicalRootRole) + if err != nil { + // If the error indicates we couldn't find the root, create a new key + if _, ok := err.(storage.ErrNotFound); !ok { + logrus.Errorf("Error when retrieving root role for GUN %s: %v", gun, err) return nil, err } - logrus.Debug("Creating new timestamp key for ", gun, ". With algo: ", key.Algorithm()) - err = store.SetKey(gun, data.CanonicalTimestampRole, key.Algorithm(), key.Public()) - if err == nil { - return key, nil - } + return crypto.Create(data.CanonicalTimestampRole, gun, createAlgorithm) + } + + // If we have a current root, parse out the public key for the timestamp role, and return it + repoSignedRoot := new(data.SignedRoot) + if err := json.Unmarshal(rootJSON, repoSignedRoot); err != nil { + logrus.Errorf("Failed to unmarshal existing root for GUN %s to retrieve timestamp key ID", gun) + return nil, err + } - if _, ok := err.(*storage.ErrKeyExists); ok { - keyAlgorithm, public, err = store.GetKey(gun, data.CanonicalTimestampRole) - if err != nil { - return nil, err - } - return data.NewPublicKey(keyAlgorithm, public), nil + timestampRole, err := repoSignedRoot.BuildBaseRole(data.CanonicalTimestampRole) + if err != nil { + logrus.Errorf("Failed to extract timestamp role from root for GUN %s", gun) + return nil, err + } + + // We currently only support single keys for snapshot and timestamp, so we can return the first and only key in the map if the signer has it + for keyID := range timestampRole.Keys { + if pubKey := crypto.GetKey(keyID); pubKey != nil { + return pubKey, nil } + } + logrus.Debugf("Failed to find any timestamp keys in cryptosigner from root for GUN %s, generating new key", gun) + return crypto.Create(data.CanonicalTimestampRole, gun, createAlgorithm) +} + +// RotateTimestampKey attempts to rotate a timestamp key in the signer, but might be rate-limited by the signer +func RotateTimestampKey(gun string, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { + // Always attempt to create a new key, but this might be rate-limited + key, err := crypto.Create(data.CanonicalTimestampRole, gun, createAlgorithm) + if err != nil { return nil, err } - return nil, err + logrus.Debug("Created new pending timestamp key ", key.ID(), "to rotate to for ", gun, ". With algo: ", key.Algorithm()) + return key, nil } // GetOrCreateTimestamp returns the current timestamp for the gun. This may mean diff --git a/server/timestamp/timestamp_test.go b/server/timestamp/timestamp_test.go index 456c0ac21..8a0513e4a 100644 --- a/server/timestamp/timestamp_test.go +++ b/server/timestamp/timestamp_test.go @@ -2,16 +2,16 @@ package timestamp import ( "bytes" + "fmt" "testing" "time" "github.com/docker/go/canonical/json" + "github.com/docker/notary/server/storage" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/testutils" "github.com/stretchr/testify/require" - - "github.com/docker/notary/server/storage" ) func TestTimestampExpired(t *testing.T) { @@ -45,8 +45,9 @@ func TestGetTimestampKey(t *testing.T) { require.Nil(t, err, "Expected nil error") - // trying to get the same key again should return the same value - require.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") + // Note that this cryptoservice does not perform any rate-limiting, unlike the notary-signer, + // so we get a different key until we've published valid TUF metadata in the store + require.NotEqual(t, k, k2, "Received same key when attempting to recreate.") require.NotNil(t, k2, "Key should not be nil") } @@ -76,11 +77,6 @@ func TestGetTimestampNoPreviousTimestamp(t *testing.T) { storage.MetaUpdate{Role: data.CanonicalTimestampRole, Version: 0, Data: timestampJSON})) } - // create a key to be used by GetOrCreateTimestamp - key, err := crypto.Create(data.CanonicalTimestampRole, "gun", data.ECDSAKey) - require.NoError(t, err) - require.NoError(t, store.SetKey("gun", data.CanonicalTimestampRole, key.Algorithm(), key.Public())) - _, _, err = GetOrCreateTimestamp("gun", store, crypto) require.Error(t, err, "GetTimestamp should have failed") if timestampJSON == nil { @@ -230,3 +226,82 @@ func TestCreateTimestampNoKeyInCrypto(t *testing.T) { require.Error(t, err) require.IsType(t, signed.ErrInsufficientSignatures{}, err) } + +type FailingStore struct { + *storage.MemStorage +} + +func (f FailingStore) GetCurrent(role, gun string) (*time.Time, []byte, error) { + return nil, nil, fmt.Errorf("failing store failed") +} + +func TestGetTimestampKeyCreateWithFailingStore(t *testing.T) { + store := FailingStore{storage.NewMemStorage()} + crypto := signed.NewEd25519() + k, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) + require.Error(t, err, "Expected error") + require.Nil(t, k, "Key should be nil") +} + +type CorruptedStore struct { + *storage.MemStorage +} + +func (c CorruptedStore) GetCurrent(role, gun string) (*time.Time, []byte, error) { + return &time.Time{}, []byte("junk"), nil +} + +func TestGetTimestampKeyCreateWithCorruptedStore(t *testing.T) { + store := CorruptedStore{storage.NewMemStorage()} + crypto := signed.NewEd25519() + k, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) + require.Error(t, err, "Expected error") + require.Nil(t, k, "Key should be nil") +} + +func TestGetTimestampKeyCreateWithInvalidAlgo(t *testing.T) { + store := storage.NewMemStorage() + crypto := signed.NewEd25519() + k, err := GetOrCreateTimestampKey("gun", store, crypto, "notactuallyanalgorithm") + require.Error(t, err, "Expected error") + require.Nil(t, k, "Key should be nil") +} + +func TestGetTimestampKeyExistingMetadata(t *testing.T) { + repo, crypto, err := testutils.EmptyRepo("gun") + require.NoError(t, err) + + sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole)) + require.NoError(t, err) + rootJSON, err := json.Marshal(sgnd) + require.NoError(t, err) + store := storage.NewMemStorage() + require.NoError(t, + store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) + + timestampRole, err := repo.Root.BuildBaseRole(data.CanonicalTimestampRole) + require.NoError(t, err) + key, ok := timestampRole.Keys[repo.Root.Signed.Roles[data.CanonicalTimestampRole].KeyIDs[0]] + require.True(t, ok) + + k, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) + require.Nil(t, err, "Expected nil error") + require.NotNil(t, k, "Key should not be nil") + require.Equal(t, key, k, "Did not receive same key when attempting to recreate.") + require.NotNil(t, k, "Key should not be nil") + + k2, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) + + require.Nil(t, err, "Expected nil error") + + require.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") + require.NotNil(t, k2, "Key should not be nil") + + // try wiping out the cryptoservice data, and ensure we create a new key because the signer doesn't hold the key specified by TUF + crypto = signed.NewEd25519() + k3, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) + require.Nil(t, err, "Expected nil error") + require.NotEqual(t, k, k3, "Received same key when attempting to recreate.") + require.NotEqual(t, k2, k3, "Received same key when attempting to recreate.") + require.NotNil(t, k3, "Key should not be nil") +} diff --git a/signer/api/find_key.go b/signer/api/find_key.go index 135b76f55..2f57f5c1e 100644 --- a/signer/api/find_key.go +++ b/signer/api/find_key.go @@ -4,23 +4,20 @@ import ( "github.com/docker/notary/signer" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/signed" pb "github.com/docker/notary/proto" ) -// FindKeyByID looks for the key with the given ID in each of the +// findKeyByID looks for the key with the given ID in each of the // signing services in sigServices. It returns the first matching key it finds, // or ErrInvalidKeyID if the key is not found in any of the signing services. -// It also returns the CryptoService associated with the key, so the caller -// can perform operations with the key (such as signing). -func FindKeyByID(cryptoServices signer.CryptoServiceIndex, keyID *pb.KeyID) (data.PublicKey, signed.CryptoService, error) { +func findKeyByID(cryptoServices signer.CryptoServiceIndex, keyID *pb.KeyID) (data.PrivateKey, string, error) { for _, service := range cryptoServices { - key := service.GetKey(keyID.ID) - if key != nil { - return key, service, nil + key, role, err := service.GetPrivateKey(keyID.ID) + if err == nil { + return key, role, nil } } - return nil, nil, trustmanager.ErrKeyNotFound{KeyID: keyID.ID} + return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID.ID} } diff --git a/signer/api/rpc_api.go b/signer/api/rpc_api.go index 9bf6d5d59..6aaa3e9e3 100644 --- a/signer/api/rpc_api.go +++ b/signer/api/rpc_api.go @@ -7,6 +7,7 @@ import ( ctxu "github.com/docker/distribution/context" "github.com/docker/notary/signer" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" "golang.org/x/net/context" "google.golang.org/grpc" @@ -28,24 +29,26 @@ type SignerServer struct { } //CreateKey returns a PublicKey created using KeyManagementServer's SigningService -func (s *KeyManagementServer) CreateKey(ctx context.Context, algorithm *pb.Algorithm) (*pb.PublicKey, error) { - keyAlgo := algorithm.Algorithm - - service := s.CryptoServices[keyAlgo] +func (s *KeyManagementServer) CreateKey(ctx context.Context, req *pb.CreateKeyRequest) (*pb.PublicKey, error) { + service := s.CryptoServices[req.Algorithm] logger := ctxu.GetLogger(ctx) if service == nil { - logger.Error("CreateKey: unsupported algorithm: ", algorithm.Algorithm) - return nil, fmt.Errorf("algorithm %s not supported for create key", algorithm.Algorithm) + logger.Error("CreateKey: unsupported algorithm: ", req.Algorithm) + return nil, fmt.Errorf("algorithm %s not supported for create key", req.Algorithm) } - tufKey, err := service.Create("", "", keyAlgo) + var tufKey data.PublicKey + var err error + + tufKey, err = service.Create(req.Role, req.Gun, req.Algorithm) if err != nil { logger.Error("CreateKey: failed to create key: ", err) return nil, grpc.Errorf(codes.Internal, "Key creation failed") } logger.Info("CreateKey: Created KeyID ", tufKey.ID()) + return &pb.PublicKey{ KeyInfo: &pb.KeyInfo{ KeyID: &pb.KeyID{ID: tufKey.ID()}, @@ -57,24 +60,11 @@ func (s *KeyManagementServer) CreateKey(ctx context.Context, algorithm *pb.Algor //DeleteKey deletes they key associated with a KeyID func (s *KeyManagementServer) DeleteKey(ctx context.Context, keyID *pb.KeyID) (*pb.Void, error) { - _, service, err := FindKeyByID(s.CryptoServices, keyID) - logger := ctxu.GetLogger(ctx) - - if err != nil { - logger.Debugf("DeleteKey: key %s not found", keyID.ID) - return &pb.Void{}, nil - } - - err = service.RemoveKey(keyID.ID) - logger.Info("DeleteKey: Deleted KeyID ", keyID.ID) - if err != nil { - switch err.(type) { - case trustmanager.ErrKeyNotFound: - logger.Debugf("DeleteKey: key %s not found", keyID.ID) - return &pb.Void{}, nil - default: - logger.Error("DeleteKey: deleted key ", keyID.ID) + // delete key ID from all services + for _, service := range s.CryptoServices { + if err := service.RemoveKey(keyID.ID); err != nil { + logger.Errorf("Failed to delete key %s", keyID.ID) return nil, grpc.Errorf(codes.Internal, "Key deletion for KeyID %s failed", keyID.ID) } } @@ -83,8 +73,8 @@ func (s *KeyManagementServer) DeleteKey(ctx context.Context, keyID *pb.KeyID) (* } //GetKeyInfo returns they PublicKey associated with a KeyID -func (s *KeyManagementServer) GetKeyInfo(ctx context.Context, keyID *pb.KeyID) (*pb.PublicKey, error) { - _, service, err := FindKeyByID(s.CryptoServices, keyID) +func (s *KeyManagementServer) GetKeyInfo(ctx context.Context, keyID *pb.KeyID) (*pb.GetKeyInfoResponse, error) { + privKey, role, err := findKeyByID(s.CryptoServices, keyID) logger := ctxu.GetLogger(ctx) @@ -93,18 +83,14 @@ func (s *KeyManagementServer) GetKeyInfo(ctx context.Context, keyID *pb.KeyID) ( return nil, grpc.Errorf(codes.NotFound, "key %s not found", keyID.ID) } - tufKey := service.GetKey(keyID.ID) - if tufKey == nil { - logger.Errorf("GetKeyInfo: key %s not found", keyID.ID) - return nil, grpc.Errorf(codes.NotFound, "key %s not found", keyID.ID) - } logger.Debug("GetKeyInfo: Returning PublicKey for KeyID ", keyID.ID) - return &pb.PublicKey{ + return &pb.GetKeyInfoResponse{ KeyInfo: &pb.KeyInfo{ - KeyID: &pb.KeyID{ID: tufKey.ID()}, - Algorithm: &pb.Algorithm{Algorithm: tufKey.Algorithm()}, + KeyID: &pb.KeyID{ID: privKey.ID()}, + Algorithm: &pb.Algorithm{Algorithm: privKey.Algorithm()}, }, - PublicKey: tufKey.Public(), + PublicKey: privKey.Public(), + Role: role, }, nil } @@ -117,20 +103,22 @@ func (s *KeyManagementServer) CheckHealth(ctx context.Context, v *pb.Void) (*pb. //Sign signs a message and returns the signature using a private key associate with the KeyID from the SignatureRequest func (s *SignerServer) Sign(ctx context.Context, sr *pb.SignatureRequest) (*pb.Signature, error) { - tufKey, service, err := FindKeyByID(s.CryptoServices, sr.KeyID) + privKey, _, err := findKeyByID(s.CryptoServices, sr.KeyID) logger := ctxu.GetLogger(ctx) - if err != nil { + switch err.(type) { + case trustmanager.ErrKeyNotFound: logger.Errorf("Sign: key %s not found", sr.KeyID.ID) - return nil, grpc.Errorf(codes.NotFound, "key %s not found", sr.KeyID.ID) - } + return nil, grpc.Errorf(codes.NotFound, err.Error()) + case nil: + break + default: + logger.Errorf("Getting key %s failed: %s", sr.KeyID.ID, err.Error()) + return nil, grpc.Errorf(codes.Internal, err.Error()) - privKey, _, err := service.GetPrivateKey(tufKey.ID()) - if err != nil { - logger.Errorf("Sign: key %s not found", sr.KeyID.ID) - return nil, grpc.Errorf(codes.NotFound, "key %s not found", sr.KeyID.ID) } + sig, err := privKey.Sign(rand.Reader, sr.Content, nil) if err != nil { logger.Errorf("Sign: signing failed for KeyID %s on hash %s", sr.KeyID.ID, sr.Content) @@ -141,8 +129,8 @@ func (s *SignerServer) Sign(ctx context.Context, sr *pb.SignatureRequest) (*pb.S signature := &pb.Signature{ KeyInfo: &pb.KeyInfo{ - KeyID: &pb.KeyID{ID: tufKey.ID()}, - Algorithm: &pb.Algorithm{Algorithm: tufKey.Algorithm()}, + KeyID: &pb.KeyID{ID: privKey.ID()}, + Algorithm: &pb.Algorithm{Algorithm: privKey.Algorithm()}, }, Algorithm: &pb.Algorithm{Algorithm: privKey.SignatureAlgorithm().String()}, Content: sig, diff --git a/signer/client/signer_trust.go b/signer/client/signer_trust.go index 7f4227b19..a3d492960 100644 --- a/signer/client/signer_trust.go +++ b/signer/client/signer_trust.go @@ -13,7 +13,6 @@ import ( "time" pb "github.com/docker/notary/proto" - "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "golang.org/x/net/context" "google.golang.org/grpc" @@ -122,7 +121,8 @@ func NewNotarySigner(conn *grpc.ClientConn) *NotarySigner { // Create creates a remote key and returns the PublicKey associated with the remote private key func (trust *NotarySigner) Create(role, gun, algorithm string) (data.PublicKey, error) { - publicKey, err := trust.kmClient.CreateKey(context.Background(), &pb.Algorithm{Algorithm: algorithm}) + publicKey, err := trust.kmClient.CreateKey(context.Background(), + &pb.CreateKeyRequest{Algorithm: algorithm, Role: role, Gun: gun}) if err != nil { return nil, err } @@ -143,21 +143,29 @@ func (trust *NotarySigner) RemoveKey(keyid string) error { // GetKey retrieves a key by ID - returns nil if the key doesn't exist func (trust *NotarySigner) GetKey(keyid string) data.PublicKey { - publicKey, err := trust.kmClient.GetKeyInfo(context.Background(), &pb.KeyID{ID: keyid}) + pubKey, _, err := trust.getKeyInfo(keyid) if err != nil { return nil } - return data.NewPublicKey(publicKey.KeyInfo.Algorithm.Algorithm, publicKey.PublicKey) + return pubKey +} + +func (trust *NotarySigner) getKeyInfo(keyid string) (data.PublicKey, string, error) { + keyInfo, err := trust.kmClient.GetKeyInfo(context.Background(), &pb.KeyID{ID: keyid}) + if err != nil { + return nil, "", err + } + return data.NewPublicKey(keyInfo.KeyInfo.Algorithm.Algorithm, keyInfo.PublicKey), keyInfo.Role, nil } // GetPrivateKey retrieves by ID an object that can be used to sign, but that does // not contain any private bytes. If the key doesn't exist, returns an error. func (trust *NotarySigner) GetPrivateKey(keyid string) (data.PrivateKey, string, error) { - pubKey := trust.GetKey(keyid) - if pubKey == nil { - return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyid} + pubKey, role, err := trust.getKeyInfo(keyid) + if err != nil { + return nil, "", err } - return NewRemotePrivateKey(pubKey, trust.sClient), "", nil + return NewRemotePrivateKey(pubKey, trust.sClient), role, nil } // ListKeys not supported for NotarySigner diff --git a/signer/keydbstore/cachedkeystore.go b/signer/keydbstore/cachedcryptoservice.go similarity index 55% rename from signer/keydbstore/cachedkeystore.go rename to signer/keydbstore/cachedcryptoservice.go index 3460bce7c..ea9b677e1 100644 --- a/signer/keydbstore/cachedkeystore.go +++ b/signer/keydbstore/cachedcryptoservice.go @@ -3,14 +3,12 @@ package keydbstore import ( "sync" - "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" ) -// Note: once trustmanager's file KeyStore has been flattened, this can be moved to trustmanager - -type cachedKeyStore struct { - trustmanager.KeyStore +type cachedKeyService struct { + signed.CryptoService lock *sync.Mutex cachedKeys map[string]*cachedKey } @@ -20,19 +18,19 @@ type cachedKey struct { key data.PrivateKey } -// NewCachedKeyStore returns a new trustmanager.KeyStore that includes caching -func NewCachedKeyStore(baseStore trustmanager.KeyStore) trustmanager.KeyStore { - return &cachedKeyStore{ - KeyStore: baseStore, - lock: &sync.Mutex{}, - cachedKeys: make(map[string]*cachedKey), +// NewCachedKeyService returns a new signed.CryptoService that includes caching +func NewCachedKeyService(baseKeyService signed.CryptoService) signed.CryptoService { + return &cachedKeyService{ + CryptoService: baseKeyService, + lock: &sync.Mutex{}, + cachedKeys: make(map[string]*cachedKey), } } // AddKey stores the contents of a private key. Both role and gun are ignored, // we always use Key IDs as name, and don't support aliases -func (s *cachedKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error { - if err := s.KeyStore.AddKey(keyInfo, privKey); err != nil { +func (s *cachedKeyService) AddKey(role, gun string, privKey data.PrivateKey) error { + if err := s.CryptoService.AddKey(role, gun, privKey); err != nil { return err } @@ -40,7 +38,7 @@ func (s *cachedKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.Priva s.lock.Lock() defer s.lock.Unlock() s.cachedKeys[privKey.ID()] = &cachedKey{ - role: keyInfo.Role, + role: role, key: privKey, } @@ -48,14 +46,14 @@ func (s *cachedKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.Priva } // GetKey returns the PrivateKey given a KeyID -func (s *cachedKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { +func (s *cachedKeyService) GetPrivateKey(keyID string) (data.PrivateKey, string, error) { cachedKeyEntry, ok := s.cachedKeys[keyID] if ok { return cachedKeyEntry.key, cachedKeyEntry.role, nil } // retrieve the key from the underlying store and put it into the cache - privKey, role, err := s.KeyStore.GetKey(keyID) + privKey, role, err := s.CryptoService.GetPrivateKey(keyID) if err == nil { s.lock.Lock() defer s.lock.Unlock() @@ -67,10 +65,10 @@ func (s *cachedKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { } // RemoveKey removes the key from the keyfilestore -func (s *cachedKeyStore) RemoveKey(keyID string) error { +func (s *cachedKeyService) RemoveKey(keyID string) error { s.lock.Lock() defer s.lock.Unlock() delete(s.cachedKeys, keyID) - return s.KeyStore.RemoveKey(keyID) + return s.CryptoService.RemoveKey(keyID) } diff --git a/signer/keydbstore/cachedkeystore_test.go b/signer/keydbstore/cachedcryptoservice_test.go similarity index 54% rename from signer/keydbstore/cachedkeystore_test.go rename to signer/keydbstore/cachedcryptoservice_test.go index 450813933..bad40fbeb 100644 --- a/signer/keydbstore/cachedkeystore_test.go +++ b/signer/keydbstore/cachedcryptoservice_test.go @@ -5,59 +5,70 @@ import ( "fmt" "testing" + "github.com/docker/notary/cryptoservice" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/utils" "github.com/stretchr/testify/require" ) // gets a key from the DB store, and asserts that the key is the expected key -func requireGetKeySuccess(t *testing.T, dbStore trustmanager.KeyStore, expectedRole string, expectedKey data.PrivateKey) { - retrKey, role, err := dbStore.GetKey(expectedKey.ID()) +func requireGetKeySuccess(t *testing.T, dbKeyService signed.CryptoService, expectedRole string, expectedKey data.PrivateKey) { + retrKey, retrRole, err := dbKeyService.GetPrivateKey(expectedKey.ID()) require.NoError(t, err) - require.Equal(t, expectedRole, role) - require.Equal(t, retrKey.Private(), expectedKey.Private()) + require.Equal(t, retrKey.ID(), expectedKey.ID()) require.Equal(t, retrKey.Algorithm(), expectedKey.Algorithm()) require.Equal(t, retrKey.Public(), expectedKey.Public()) - require.Equal(t, retrKey.SignatureAlgorithm(), expectedKey.SignatureAlgorithm()) + require.Equal(t, retrKey.Private(), expectedKey.Private()) + require.Equal(t, retrRole, expectedRole) +} + +func requireGetPubKeySuccess(t *testing.T, dbKeyService signed.CryptoService, expectedRole string, expectedPubKey data.PublicKey) { + retrPubKey := dbKeyService.GetKey(expectedPubKey.ID()) + require.Equal(t, retrPubKey.Public(), expectedPubKey.Public()) + require.Equal(t, retrPubKey.ID(), expectedPubKey.ID()) + require.Equal(t, retrPubKey.Algorithm(), expectedPubKey.Algorithm()) } // closes the DB connection first so we can test that the successful get was // from the cache -func requireGetKeySuccessFromCache(t *testing.T, cachedStore, underlyingStore trustmanager.KeyStore, expectedRole string, expectedKey data.PrivateKey) { +func requireGetKeySuccessFromCache(t *testing.T, cachedStore, underlyingStore signed.CryptoService, expectedRole string, expectedKey data.PrivateKey) { require.NoError(t, underlyingStore.RemoveKey(expectedKey.ID())) requireGetKeySuccess(t, cachedStore, expectedRole, expectedKey) } -func requireGetKeyFailure(t *testing.T, dbStore trustmanager.KeyStore, keyID string) { - _, _, err := dbStore.GetKey(keyID) - require.IsType(t, trustmanager.ErrKeyNotFound{}, err) +func requireGetKeyFailure(t *testing.T, dbStore signed.CryptoService, keyID string) { + _, _, err := dbStore.GetPrivateKey(keyID) + require.Error(t, err) + k := dbStore.GetKey(keyID) + require.Nil(t, k) } -type unAddableKeyStore struct { - trustmanager.KeyStore +type unAddableKeyService struct { + signed.CryptoService } -func (u unAddableKeyStore) AddKey(_ trustmanager.KeyInfo, _ data.PrivateKey) error { - return fmt.Errorf("Can't add to keystore!") +func (u unAddableKeyService) AddKey(_, _ string, _ data.PrivateKey) error { + return fmt.Errorf("Can't add to keyservice!") } -type unRemoveableKeyStore struct { - trustmanager.KeyStore +type unRemoveableKeyService struct { + signed.CryptoService failToRemove bool } -func (u unRemoveableKeyStore) RemoveKey(keyID string) error { +func (u unRemoveableKeyService) RemoveKey(keyID string) error { if u.failToRemove { return fmt.Errorf("Can't remove from keystore!") } - return u.KeyStore.RemoveKey(keyID) + return u.CryptoService.RemoveKey(keyID) } -// Getting a key, on succcess, populates the cache. +// Getting a key, on success, populates the cache. func TestGetSuccessPopulatesCache(t *testing.T) { - underlying := trustmanager.NewKeyMemoryStore(constRetriever) - cached := NewCachedKeyStore(underlying) + underlying := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(constRetriever)) + cached := NewCachedKeyService(underlying) testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) @@ -66,7 +77,7 @@ func TestGetSuccessPopulatesCache(t *testing.T) { requireGetKeyFailure(t, cached, testKey.ID()) // Add key to underlying store only - err = underlying.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun"}, testKey) + err = underlying.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) // getting for the first time is successful, and after that getting from cache should be too @@ -74,11 +85,10 @@ func TestGetSuccessPopulatesCache(t *testing.T) { requireGetKeySuccessFromCache(t, cached, underlying, data.CanonicalTimestampRole, testKey) } -// Creating a key, on succcess, populates the cache, but does not do so on failure +// Creating a key, on success, populates the cache, but does not do so on failure func TestAddKeyPopulatesCacheIfSuccessful(t *testing.T) { - var underlying trustmanager.KeyStore - underlying = trustmanager.NewKeyMemoryStore(constRetriever) - cached := NewCachedKeyStore(underlying) + underlying := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(constRetriever)) + cached := NewCachedKeyService(underlying) testKeys := make([]data.PrivateKey, 2) for i := 0; i < 2; i++ { @@ -87,17 +97,16 @@ func TestAddKeyPopulatesCacheIfSuccessful(t *testing.T) { testKeys[i] = privKey } - // Writing in the keystore succeeds - err := cached.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun"}, testKeys[0]) + // Writing in the key service succeeds + err := cached.AddKey(data.CanonicalTimestampRole, "gun", testKeys[0]) require.NoError(t, err) // Now even if it's deleted from the underlying database, it's fine because it's cached requireGetKeySuccessFromCache(t, cached, underlying, data.CanonicalTimestampRole, testKeys[0]) - // Writing in the keystore fails - underlying = unAddableKeyStore{KeyStore: underlying} - cached = NewCachedKeyStore(underlying) - err = cached.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun"}, testKeys[1]) + // Writing in the key service fails + cached = NewCachedKeyService(unAddableKeyService{underlying}) + err = cached.AddKey(data.CanonicalTimestampRole, "gun", testKeys[1]) require.Error(t, err) // And now it can't be found in either DB @@ -106,14 +115,14 @@ func TestAddKeyPopulatesCacheIfSuccessful(t *testing.T) { // Deleting a key, no matter whether we succeed in the underlying layer or not, evicts the cached key. func TestDeleteKeyRemovesKeyFromCache(t *testing.T) { - underlying := trustmanager.NewKeyMemoryStore(constRetriever) - cached := NewCachedKeyStore(underlying) + underlying := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(constRetriever)) + cached := NewCachedKeyService(underlying) testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) // Write the key, which puts it in the cache - err = cached.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun"}, testKey) + err = cached.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) // Deleting removes the key from the cache and the underlying store @@ -122,9 +131,9 @@ func TestDeleteKeyRemovesKeyFromCache(t *testing.T) { requireGetKeyFailure(t, cached, testKey.ID()) // Now set up an underlying store where the key can't be deleted - failingUnderlying := unRemoveableKeyStore{KeyStore: underlying, failToRemove: true} - cached = NewCachedKeyStore(failingUnderlying) - err = cached.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun"}, testKey) + failingUnderlying := unRemoveableKeyService{CryptoService: underlying, failToRemove: true} + cached = NewCachedKeyService(failingUnderlying) + err = cached.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) // Deleting fails to remove the key from the underlying store diff --git a/signer/keydbstore/keydbstore.go b/signer/keydbstore/keydbstore.go new file mode 100644 index 000000000..6848d8e1d --- /dev/null +++ b/signer/keydbstore/keydbstore.go @@ -0,0 +1,51 @@ +package keydbstore + +import ( + "crypto" + "crypto/rand" + "fmt" + "io" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/utils" +) + +type activatingPrivateKey struct { + data.PrivateKey + activationFunc func(keyID string) error +} + +func (a activatingPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + keyID := a.PrivateKey.ID() + sig, err := a.PrivateKey.Sign(rand, digest, opts) + if err == nil { + if activationErr := a.activationFunc(keyID); activationErr != nil { + logrus.Errorf("Key %s was just used to sign hash %s, error when trying to mark key as active: %s", + keyID, digest, activationErr.Error()) + } + } + return sig, err +} + +// helper function to generate private keys for the signer databases - does not implement RSA since that is not +// supported by the signer +func generatePrivateKey(algorithm string) (data.PrivateKey, error) { + var privKey data.PrivateKey + var err error + switch algorithm { + case data.ECDSAKey: + privKey, err = utils.GenerateECDSAKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate EC key: %v", err) + } + case data.ED25519Key: + privKey, err = utils.GenerateED25519Key(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate ED25519 key: %v", err) + } + default: + return nil, fmt.Errorf("private key type not supported for key generation: %s", algorithm) + } + return privKey, nil +} diff --git a/signer/keydbstore/keydbstore_test.go b/signer/keydbstore/keydbstore_test.go index 2e426dff1..2d029b937 100644 --- a/signer/keydbstore/keydbstore_test.go +++ b/signer/keydbstore/keydbstore_test.go @@ -3,10 +3,11 @@ package keydbstore import ( "crypto/rand" "errors" + "fmt" "testing" - "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/utils" "github.com/stretchr/testify/require" ) @@ -25,16 +26,16 @@ func multiAliasRetriever(_, alias string, _ bool, _ int) (string, bool, error) { if passwd, ok := validAliasesAndPasswds[alias]; ok { return passwd, false, nil } - return "", false, errors.New("password alias no found") + return "", false, errors.New("password alias not found") } type keyRotator interface { - trustmanager.KeyStore + signed.CryptoService RotateKeyPassphrase(keyID, newPassphraseAlias string) error } -// A key can only be added to the DB once -func testKeyCanOnlyBeAddedOnce(t *testing.T, dbStore trustmanager.KeyStore) []data.PrivateKey { +// A key can only be added to the DB once. Returns a list of expected keys, and which keys are expected to exist. +func testKeyCanOnlyBeAddedOnce(t *testing.T, dbStore signed.CryptoService) []data.PrivateKey { expectedKeys := make([]data.PrivateKey, 2) for i := 0; i < len(expectedKeys); i++ { testKey, err := utils.GenerateECDSAKey(rand.Reader) @@ -43,60 +44,193 @@ func testKeyCanOnlyBeAddedOnce(t *testing.T, dbStore trustmanager.KeyStore) []da } // Test writing new key in database alone, not cache - err := dbStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun/ignored"}, expectedKeys[0]) + err := dbStore.AddKey(data.CanonicalTimestampRole, "gun", expectedKeys[0]) require.NoError(t, err) - // Currently we ignore roles - requireGetKeySuccess(t, dbStore, "", expectedKeys[0]) + requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole, expectedKeys[0]) // Test writing the same key in the database. Should fail. - err = dbStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun/ignored"}, expectedKeys[0]) + err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", expectedKeys[0]) require.Error(t, err, "failed to add private key to database:") // Test writing new key succeeds - err = dbStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun/ignored"}, expectedKeys[1]) + err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", expectedKeys[1]) require.NoError(t, err) return expectedKeys } -// a key can be deleted -func testCreateDelete(t *testing.T, dbStore trustmanager.KeyStore) { - testKey, err := utils.GenerateECDSAKey(rand.Reader) +// a key can be deleted - returns a list of expected keys +func testCreateDelete(t *testing.T, dbStore signed.CryptoService) []data.PrivateKey { + testKeys := make([]data.PrivateKey, 2) + for i := 0; i < len(testKeys); i++ { + testKey, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + testKeys[i] = testKey + + // Add them to the DB + err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) + require.NoError(t, err) + requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole, testKey) + } + + // Deleting the key should succeed and only remove the key that was deleted + require.NoError(t, dbStore.RemoveKey(testKeys[0].ID())) + requireGetKeyFailure(t, dbStore, testKeys[0].ID()) + requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole, testKeys[1]) + + // Deleting the key again should succeed even though it's not in the DB + require.NoError(t, dbStore.RemoveKey(testKeys[0].ID())) + requireGetKeyFailure(t, dbStore, testKeys[0].ID()) + + return testKeys[1:] +} + +// key rotation is successful provided the other alias is valid. +// Returns the key that was rotated and one that was not rotated +func testKeyRotation(t *testing.T, dbStore keyRotator, newValidAlias string) (data.PrivateKey, data.PrivateKey) { + testKeys := make([]data.PrivateKey, 2) + for i := 0; i < len(testKeys); i++ { + testKey, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + testKeys[i] = testKey + + // Add them to the DB + err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) + require.NoError(t, err) + } + + // Try rotating the key to a valid alias + err := dbStore.RotateKeyPassphrase(testKeys[0].ID(), newValidAlias) + require.NoError(t, err) + + // Try rotating the key to an invalid alias + err = dbStore.RotateKeyPassphrase(testKeys[0].ID(), "invalidAlias") + require.Error(t, err, "there should be no password for invalidAlias so rotation should fail") + + return testKeys[0], testKeys[1] +} + +type badReader struct{} + +func (b badReader) Read([]byte) (n int, err error) { + return 0, fmt.Errorf("Nope, not going to read") +} + +// Signing with a key marks it as active if the signing is successful. Marking as active is successful no matter what, +// but should only activate a key that exists in the DB. +// Returns the key that was used and one that was not +func testSigningWithKeyMarksAsActive(t *testing.T, dbStore signed.CryptoService) (data.PrivateKey, data.PrivateKey) { + testKeys := make([]data.PrivateKey, 3) + for i := 0; i < len(testKeys); i++ { + testKey, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + + // Add them to the DB + err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) + require.NoError(t, err) + requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole, testKey) + + // store the gotten key, because that key is special + gottenKey, _, err := dbStore.GetPrivateKey(testKey.ID()) + require.NoError(t, err) + testKeys[i] = gottenKey + } + + // sign successfully with the first key - this key will become active + msg := []byte("successful") + sig, err := testKeys[0].Sign(rand.Reader, msg, nil) require.NoError(t, err) + require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( + data.PublicKeyFromPrivate(testKeys[0]), sig, msg)) + + // sign unsuccessfully with the second key - this key should remain inactive + sig, err = testKeys[1].Sign(badReader{}, []byte("unsuccessful"), nil) + require.Error(t, err) + require.Equal(t, "Nope, not going to read", err.Error()) + require.Nil(t, sig) + + // delete the third key from the DB - sign should still succeed, even though + // this key cannot be marked as active anymore due to it not existing + // (this probably won't return an error) + require.NoError(t, dbStore.RemoveKey(testKeys[2].ID())) + requireGetKeyFailure(t, dbStore, testKeys[2].ID()) + msg = []byte("successful, not active") + sig, err = testKeys[2].Sign(rand.Reader, msg, nil) + require.NoError(t, err) + require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( + data.PublicKeyFromPrivate(testKeys[2]), sig, msg)) + + return testKeys[0], testKeys[1] // testKeys[2] should no longer exist in the DB +} + +func testCreateKey(t *testing.T, dbStore signed.CryptoService) (data.PrivateKey, data.PrivateKey, data.PrivateKey) { + // Create a test key, and check that it is successfully added to the database + role := data.CanonicalSnapshotRole + gun := "gun" - // Add a key to the DB - err = dbStore.AddKey(trustmanager.KeyInfo{Role: "", Gun: ""}, testKey) + // First create an ECDSA key + createdECDSAKey, err := dbStore.Create(role, gun, data.ECDSAKey) require.NoError(t, err) - // Currently we ignore roles - requireGetKeySuccess(t, dbStore, "", testKey) + require.NotNil(t, createdECDSAKey) + require.Equal(t, data.ECDSAKey, createdECDSAKey.Algorithm()) - // Deleting the key should succeed - err = dbStore.RemoveKey(testKey.ID()) + // Retrieve the key from the database by ID, and check that it is correct + requireGetPubKeySuccess(t, dbStore, role, createdECDSAKey) + + // Calling Create with the same parameters will return the same key because it is inactive + createdSameECDSAKey, err := dbStore.Create(role, gun, data.ECDSAKey) require.NoError(t, err) - requireGetKeyFailure(t, dbStore, testKey.ID()) + require.Equal(t, createdECDSAKey.Algorithm(), createdSameECDSAKey.Algorithm()) + require.Equal(t, createdECDSAKey.Public(), createdSameECDSAKey.Public()) + require.Equal(t, createdECDSAKey.ID(), createdSameECDSAKey.ID()) - // Deleting the key again should succeed even though it's not in the DB - err = dbStore.RemoveKey(testKey.ID()) + // Calling Create with the same role and gun but a different algorithm will create a new key + createdED25519Key, err := dbStore.Create(role, gun, data.ED25519Key) require.NoError(t, err) - requireGetKeyFailure(t, dbStore, testKey.ID()) -} + require.NotEqual(t, createdECDSAKey.Algorithm(), createdED25519Key.Algorithm()) + require.NotEqual(t, createdECDSAKey.Public(), createdED25519Key.Public()) + require.NotEqual(t, createdECDSAKey.ID(), createdED25519Key.ID()) -// key rotation is successful provided the other alias is valid -func testKeyRotation(t *testing.T, dbStore keyRotator, newValidAlias string) data.PrivateKey { - testKey, err := utils.GenerateECDSAKey(rand.Reader) + // Retrieve the key from the database by ID, and check that it is correct + requireGetPubKeySuccess(t, dbStore, role, createdED25519Key) + + // Sign with the ED25519 key from the DB to mark it as active + activeED25519Key, _, err := dbStore.GetPrivateKey(createdED25519Key.ID()) + require.NoError(t, err) + _, err = activeED25519Key.Sign(rand.Reader, []byte("msg"), nil) require.NoError(t, err) - // Test writing new key in database/cache - err = dbStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun/ignored"}, testKey) + // Calling Create for the same role, gun and ED25519 algorithm will now create a new key + createdNewED25519Key, err := dbStore.Create(role, gun, data.ED25519Key) require.NoError(t, err) + require.Equal(t, activeED25519Key.Algorithm(), createdNewED25519Key.Algorithm()) + require.NotEqual(t, activeED25519Key.Public(), createdNewED25519Key.Public()) + require.NotEqual(t, activeED25519Key.ID(), createdNewED25519Key.ID()) - // Try rotating the key to a valid alias - err = dbStore.RotateKeyPassphrase(testKey.ID(), newValidAlias) + // Get the inactive ED25519 key from the database explicitly to return + inactiveED25519Key, _, err := dbStore.GetPrivateKey(createdNewED25519Key.ID()) require.NoError(t, err) - // Try rotating the key to an invalid alias - err = dbStore.RotateKeyPassphrase(testKey.ID(), "invalidAlias") - require.Error(t, err, "there should be no password for invalidAlias so rotation should fail") + // Get the inactive ECDSA key from the database explicitly to return + inactiveECDSAKey, _, err := dbStore.GetPrivateKey(createdSameECDSAKey.ID()) + require.NoError(t, err) + + // Calling Create with an invalid algorithm gives an error + _, err = dbStore.Create(role, gun, "invalid") + require.Error(t, err) + + return activeED25519Key, inactiveED25519Key, inactiveECDSAKey +} + +func testUnimplementedInterfaceMethods(t *testing.T, dbStore signed.CryptoService) { + // add one key to the db + testKey, err := utils.GenerateECDSAKey(rand.Reader) + require.NoError(t, err) + err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) + require.NoError(t, err) + requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole, testKey) - return testKey + // these are unimplemented/unused, and return nil + require.Nil(t, dbStore.ListAllKeys()) + require.Nil(t, dbStore.ListKeys(data.CanonicalTimestampRole)) } diff --git a/signer/keydbstore/rethink_keydbstore.go b/signer/keydbstore/rethink_keydbstore.go index 42c81ce26..1198c8ee5 100644 --- a/signer/keydbstore/rethink_keydbstore.go +++ b/signer/keydbstore/rethink_keydbstore.go @@ -21,6 +21,7 @@ type RethinkDBKeyStore struct { retriever notary.PassRetriever user string password string + nowFunc func() time.Time } // RDBPrivateKey represents a PrivateKey in the rethink database @@ -31,6 +32,8 @@ type RDBPrivateKey struct { KeywrapAlg string `gorethink:"keywrap_alg"` Algorithm string `gorethink:"algorithm"` PassphraseAlias string `gorethink:"passphrase_alias"` + Gun string `gorethink:"gun"` + Role string `gorethink:"role"` // gorethink specifically supports binary types, and says to pass it in as // a byteslice. Currently our encryption method for the private key bytes @@ -39,6 +42,9 @@ type RDBPrivateKey struct { // too Public []byte `gorethink:"public"` Private []byte `gorethink:"private"` + + // whether this key is active or not + LastUsed time.Time `gorethink:"last_used"` } // gorethink can't handle an UnmarshalJSON function (see https://github.com/dancannon/gorethink/issues/201), @@ -53,8 +59,11 @@ func rdbPrivateKeyFromJSON(data []byte) (interface{}, error) { KeywrapAlg string `json:"keywrap_alg"` Algorithm string `json:"algorithm"` PassphraseAlias string `json:"passphrase_alias"` + Gun string `json:"gun"` + Role string `json:"role"` Public []byte `json:"public"` Private []byte `json:"private"` + LastUsed time.Time `json:"last_used"` }{} if err := json.Unmarshal(data, &a); err != nil { return RDBPrivateKey{}, err @@ -70,8 +79,11 @@ func rdbPrivateKeyFromJSON(data []byte) (interface{}, error) { KeywrapAlg: a.KeywrapAlg, Algorithm: a.Algorithm, PassphraseAlias: a.PassphraseAlias, + Gun: a.Gun, + Role: a.Role, Public: a.Public, Private: a.Private, + LastUsed: a.LastUsed, }, nil } @@ -97,6 +109,7 @@ func NewRethinkDBKeyStore(dbName, username, password string, passphraseRetriever retriever: passphraseRetriever, user: username, password: password, + nowFunc: time.Now, } } @@ -107,7 +120,7 @@ func (rdb *RethinkDBKeyStore) Name() string { // AddKey stores the contents of a private key. Both role and gun are ignored, // we always use Key IDs as name, and don't support aliases -func (rdb *RethinkDBKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error { +func (rdb *RethinkDBKeyStore) AddKey(role, gun string, privKey data.PrivateKey) error { passphrase, _, err := rdb.retriever(privKey.ID(), rdb.defaultPassAlias, false, 1) if err != nil { return err @@ -118,7 +131,7 @@ func (rdb *RethinkDBKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data. return err } - now := time.Now() + now := rdb.nowFunc() rethinkPrivKey := RDBPrivateKey{ Timing: rethinkdb.Timing{ CreatedAt: now, @@ -129,6 +142,8 @@ func (rdb *RethinkDBKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data. KeywrapAlg: KeywrapAlg, PassphraseAlias: rdb.defaultPassAlias, Algorithm: privKey.Algorithm(), + Gun: gun, + Role: role, Public: privKey.Public(), Private: []byte(encryptedKey), } @@ -172,8 +187,8 @@ func (rdb *RethinkDBKeyStore) getKey(keyID string) (*RDBPrivateKey, string, erro return &dbPrivateKey, decryptedPrivKey, nil } -// GetKey returns the PrivateKey given a KeyID -func (rdb *RethinkDBKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { +// GetPrivateKey returns the PrivateKey given a KeyID +func (rdb *RethinkDBKeyStore) GetPrivateKey(keyID string) (data.PrivateKey, string, error) { dbPrivateKey, decryptedPrivKey, err := rdb.getKey(keyID) if err != nil { return nil, "", err @@ -187,16 +202,26 @@ func (rdb *RethinkDBKeyStore) GetKey(keyID string) (data.PrivateKey, string, err return nil, "", err } - return privKey, "", nil + return activatingPrivateKey{PrivateKey: privKey, activationFunc: rdb.markActive}, dbPrivateKey.Role, nil +} + +// GetKey returns the PublicKey given a KeyID, and does not activate the key +func (rdb *RethinkDBKeyStore) GetKey(keyID string) data.PublicKey { + dbPrivateKey, _, err := rdb.getKey(keyID) + if err != nil { + return nil + } + + return data.NewPublicKey(dbPrivateKey.Algorithm, dbPrivateKey.Public) } -// GetKeyInfo always returns empty and an error. This method is here to satisfy the KeyStore interface -func (rdb RethinkDBKeyStore) GetKeyInfo(name string) (trustmanager.KeyInfo, error) { - return trustmanager.KeyInfo{}, fmt.Errorf("GetKeyInfo currently not supported for RethinkDBKeyStore, as it does not track roles or GUNs") +// ListKeys always returns nil. This method is here to satisfy the CryptoService interface +func (rdb RethinkDBKeyStore) ListKeys(role string) []string { + return nil } -// ListKeys always returns nil. This method is here to satisfy the KeyStore interface -func (rdb RethinkDBKeyStore) ListKeys() map[string]trustmanager.KeyInfo { +// ListAllKeys always returns nil. This method is here to satisfy the CryptoService interface +func (rdb RethinkDBKeyStore) ListAllKeys() map[string]string { return nil } @@ -241,6 +266,46 @@ func (rdb RethinkDBKeyStore) RotateKeyPassphrase(keyID, newPassphraseAlias strin return nil } +// markActive marks a particular key as active +func (rdb RethinkDBKeyStore) markActive(keyID string) error { + _, err := gorethink.DB(rdb.dbName).Table(PrivateKeysRethinkTable.Name).Get(keyID).Update(map[string]interface{}{ + "last_used": rdb.nowFunc(), + }).RunWrite(rdb.sess) + return err +} + +// Create will attempt to first re-use an inactive key for the same role, gun, and algorithm. +// If one isn't found, it will create a private key and add it to the DB as an inactive key +func (rdb RethinkDBKeyStore) Create(role, gun, algorithm string) (data.PublicKey, error) { + dbPrivateKey := RDBPrivateKey{} + res, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()). + Filter(gorethink.Row.Field("gun").Eq(gun)). + Filter(gorethink.Row.Field("role").Eq(role)). + Filter(gorethink.Row.Field("algorithm").Eq(algorithm)). + Filter(gorethink.Row.Field("last_used").Eq(time.Time{})). + OrderBy(gorethink.Row.Field("key_id")). + Run(rdb.sess) + if err != nil { + return nil, err + } + defer res.Close() + + err = res.One(&dbPrivateKey) + if err == nil { + return data.NewPublicKey(dbPrivateKey.Algorithm, dbPrivateKey.Public), nil + } + + privKey, err := generatePrivateKey(algorithm) + if err != nil { + return nil, err + } + if err = rdb.AddKey(role, gun, privKey); err != nil { + return nil, fmt.Errorf("failed to store key: %v", err) + } + + return privKey, nil +} + // Bootstrap sets up the database and tables, also creating the notary signer user with appropriate db permission func (rdb RethinkDBKeyStore) Bootstrap() error { if err := rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{ diff --git a/signer/keydbstore/rethink_realkeydbstore_test.go b/signer/keydbstore/rethink_realkeydbstore_test.go index 36ec813fa..f58ffed92 100644 --- a/signer/keydbstore/rethink_realkeydbstore_test.go +++ b/signer/keydbstore/rethink_realkeydbstore_test.go @@ -5,18 +5,23 @@ package keydbstore import ( + "crypto/rand" "os" "testing" + "time" "github.com/docker/go-connections/tlsconfig" "github.com/docker/notary/storage/rethinkdb" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" "github.com/dvsekhvalnov/jose2go" "github.com/stretchr/testify/require" "gopkg.in/dancannon/gorethink.v2" ) var tlsOpts = tlsconfig.Options{InsecureSkipVerify: true} +var rdbNow = time.Date(2016, 12, 31, 1, 1, 1, 0, time.UTC) func rethinkSessionSetup(t *testing.T) (*gorethink.Session, string) { // Get the Rethink connection string from an environment variable @@ -41,6 +46,8 @@ func rethinkDBSetup(t *testing.T, dbName string) (*RethinkDBKeyStore, func()) { dbStore := NewRethinkDBKeyStore(dbName, "", "", multiAliasRetriever, validAliases[0], session) require.Equal(t, "RethinkDB", dbStore.Name()) + dbStore.nowFunc = func() time.Time { return rdbNow } + return dbStore, cleanup } @@ -67,62 +74,107 @@ func TestRethinkBootstrapSetsUsernamePassword(t *testing.T) { userSession, err := rethinkdb.UserConnection(tlsOpts, source, otherUser, otherPass) require.NoError(t, err) s = NewRethinkDBKeyStore(dbname, otherUser, otherPass, constRetriever, "ignored", userSession) - _, _, err = s.GetKey("nonexistent") + _, _, err = s.GetPrivateKey("nonexistent") require.Error(t, err) require.IsType(t, gorethink.RQLRuntimeError{}, err) + key := s.GetKey("nonexistent") + require.Nil(t, key) require.Error(t, s.CheckHealth()) // our user can access the DB though userSession, err = rethinkdb.UserConnection(tlsOpts, source, username, password) require.NoError(t, err) s = NewRethinkDBKeyStore(dbname, username, password, constRetriever, "ignored", userSession) - _, _, err = s.GetKey("nonexistent") + _, _, err = s.GetPrivateKey("nonexistent") require.Error(t, err) require.IsType(t, trustmanager.ErrKeyNotFound{}, err) require.NoError(t, s.CheckHealth()) } -func getRethinkDBRows(t *testing.T, dbStore *RethinkDBKeyStore) []RDBPrivateKey { +// Checks that the DB contains the expected keys, and returns a map of the GormPrivateKey object by key ID +func requireExpectedRDBKeys(t *testing.T, dbStore *RethinkDBKeyStore, expectedKeys []data.PrivateKey) map[string]RDBPrivateKey { res, err := gorethink.DB(dbStore.dbName).Table(PrivateKeysRethinkTable.Name).Run(dbStore.sess) require.NoError(t, err) var rows []RDBPrivateKey require.NoError(t, res.All(&rows)) - return rows + require.Len(t, rows, len(expectedKeys)) + result := make(map[string]RDBPrivateKey) + + for _, rdbKey := range rows { + result[rdbKey.KeyID] = rdbKey + } + + for _, key := range expectedKeys { + rdbKey, ok := result[key.ID()] + require.True(t, ok) + require.NotNil(t, rdbKey) + require.Equal(t, key.Public(), rdbKey.Public) + require.Equal(t, key.Algorithm(), rdbKey.Algorithm) + + // because we have to manually set the created and modified times + require.True(t, rdbKey.CreatedAt.Equal(rdbNow)) + require.True(t, rdbKey.UpdatedAt.Equal(rdbNow)) + require.True(t, rdbKey.DeletedAt.Equal(time.Time{})) + } + + return result } func TestRethinkKeyCanOnlyBeAddedOnce(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerAddTests") defer cleanup() + expectedKeys := testKeyCanOnlyBeAddedOnce(t, dbStore) - rows := getRethinkDBRows(t, dbStore) - require.Len(t, rows, len(expectedKeys)) + rdbKeys := requireExpectedRDBKeys(t, dbStore, expectedKeys) + + // none of these keys are active, since they have not been activated + for _, rdbKey := range rdbKeys { + require.True(t, rdbKey.LastUsed.Equal(time.Time{})) + } } func TestRethinkCreateDelete(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerDeleteTests") defer cleanup() - testCreateDelete(t, dbStore) + expectedKeys := testCreateDelete(t, dbStore) + + rdbKeys := requireExpectedRDBKeys(t, dbStore, expectedKeys) - rows := getRethinkDBRows(t, dbStore) - require.Len(t, rows, 0) + // none of these keys are active, since they have not been activated + for _, rdbKey := range rdbKeys { + require.True(t, rdbKey.LastUsed.Equal(time.Time{})) + } } func TestRethinkKeyRotation(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerRotationTests") defer cleanup() - privKey := testKeyRotation(t, dbStore, validAliases[1]) - rows := getRethinkDBRows(t, dbStore) - require.Len(t, rows, 1) + rotatedKey, nonRotatedKey := testKeyRotation(t, dbStore, validAliases[1]) + + rdbKeys := requireExpectedRDBKeys(t, dbStore, []data.PrivateKey{rotatedKey, nonRotatedKey}) - // require that the key is encrypted with the new passphrase - require.Equal(t, validAliases[1], rows[0].PassphraseAlias) - decryptedKey, _, err := jose.Decode(string(rows[0].Private), validAliasesAndPasswds[validAliases[1]]) + // none of these keys are active, since they have not been activated + for _, rdbKey := range rdbKeys { + require.True(t, rdbKey.LastUsed.Equal(time.Time{})) + } + + // require that the rotated key is encrypted with the new passphrase + rotatedRDBKey := rdbKeys[rotatedKey.ID()] + require.Equal(t, validAliases[1], rotatedRDBKey.PassphraseAlias) + decryptedKey, _, err := jose.Decode(string(rotatedRDBKey.Private), validAliasesAndPasswds[validAliases[1]]) + require.NoError(t, err) + require.Equal(t, string(rotatedKey.Private()), decryptedKey) + + // require that the nonrotated key is encrypted with the old passphrase + nonRotatedRDBKey := rdbKeys[nonRotatedKey.ID()] + require.Equal(t, validAliases[0], nonRotatedRDBKey.PassphraseAlias) + decryptedKey, _, err = jose.Decode(string(nonRotatedRDBKey.Private), validAliasesAndPasswds[validAliases[0]]) require.NoError(t, err) - require.Equal(t, string(privKey.Private()), decryptedKey) + require.Equal(t, string(nonRotatedKey.Private()), decryptedKey) } func TestRethinkCheckHealth(t *testing.T) { @@ -148,3 +200,45 @@ func TestRethinkCheckHealth(t *testing.T) { cleanup() require.Error(t, dbStore.CheckHealth()) } + +func TestRethinkSigningMarksKeyActive(t *testing.T) { + dbStore, cleanup := rethinkDBSetup(t, "signerActivationTests") + defer cleanup() + + activeKey, nonActiveKey := testSigningWithKeyMarksAsActive(t, dbStore) + + rdbKeys := requireExpectedRDBKeys(t, dbStore, []data.PrivateKey{activeKey, nonActiveKey}) + + // check that activation updates the activated key but not the unactivated key + require.True(t, rdbKeys[activeKey.ID()].LastUsed.Equal(rdbNow)) + require.True(t, rdbKeys[nonActiveKey.ID()].LastUsed.Equal(time.Time{})) + + // check that signing succeeds even if the DB connection is closed and hence + // mark as active errors + dbStore.sess.Close() + msg := []byte("successful, db closed") + sig, err := nonActiveKey.Sign(rand.Reader, msg, nil) + require.NoError(t, err) + require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( + data.PublicKeyFromPrivate(nonActiveKey), sig, msg)) +} + +func TestRethinkCreateKey(t *testing.T) { + dbStore, cleanup := rethinkDBSetup(t, "signerCreationTests") + defer cleanup() + + activeED25519Key, pendingED25519Key, pendingECDSAKey := testCreateKey(t, dbStore) + + rdbKeys := requireExpectedRDBKeys(t, dbStore, []data.PrivateKey{activeED25519Key, pendingED25519Key, pendingECDSAKey}) + + // check that activation updates the activated key but not the unactivated keys + require.True(t, rdbKeys[activeED25519Key.ID()].LastUsed.Equal(rdbNow)) + require.True(t, rdbKeys[pendingED25519Key.ID()].LastUsed.Equal(time.Time{})) + require.True(t, rdbKeys[pendingECDSAKey.ID()].LastUsed.Equal(time.Time{})) +} + +func TestRethinkUnimplementedInterfaceBehavior(t *testing.T) { + dbStore, cleanup := rethinkDBSetup(t, "signerInterfaceTests") + defer cleanup() + testUnimplementedInterfaceMethods(t, dbStore) +} diff --git a/signer/keydbstore/sql_keydbstore.go b/signer/keydbstore/sql_keydbstore.go index d59213630..b6493e615 100644 --- a/signer/keydbstore/sql_keydbstore.go +++ b/signer/keydbstore/sql_keydbstore.go @@ -2,6 +2,7 @@ package keydbstore import ( "fmt" + "time" "github.com/docker/notary" "github.com/docker/notary/trustmanager" @@ -22,18 +23,22 @@ type SQLKeyDBStore struct { dbType string defaultPassAlias string retriever notary.PassRetriever + nowFunc func() time.Time } // GormPrivateKey represents a PrivateKey in the database type GormPrivateKey struct { gorm.Model - KeyID string `sql:"type:varchar(255);not null;unique;index:key_id_idx"` - EncryptionAlg string `sql:"type:varchar(255);not null"` - KeywrapAlg string `sql:"type:varchar(255);not null"` - Algorithm string `sql:"type:varchar(50);not null"` - PassphraseAlias string `sql:"type:varchar(50);not null"` - Public string `sql:"type:blob;not null"` - Private string `sql:"type:blob;not null"` + KeyID string `sql:"type:varchar(255);not null;unique;index:key_id_idx"` + EncryptionAlg string `sql:"type:varchar(255);not null"` + KeywrapAlg string `sql:"type:varchar(255);not null"` + Algorithm string `sql:"type:varchar(50);not null"` + PassphraseAlias string `sql:"type:varchar(50);not null"` + Gun string `sql:"type:varchar(255);not null"` + Role string `sql:"type:varchar(255);not null"` + Public string `sql:"type:blob;not null"` + Private string `sql:"type:blob;not null"` + LastUsed time.Time `sql:"type:datetime;null;default:null"` } // TableName sets a specific table name for our GormPrivateKey @@ -55,6 +60,7 @@ func NewSQLKeyDBStore(passphraseRetriever notary.PassRetriever, defaultPassAlias dbType: dbDialect, defaultPassAlias: defaultPassAlias, retriever: passphraseRetriever, + nowFunc: time.Now, }, nil } @@ -65,7 +71,7 @@ func (s *SQLKeyDBStore) Name() string { // AddKey stores the contents of a private key. Both role and gun are ignored, // we always use Key IDs as name, and don't support aliases -func (s *SQLKeyDBStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error { +func (s *SQLKeyDBStore) AddKey(role, gun string, privKey data.PrivateKey) error { passphrase, _, err := s.retriever(privKey.ID(), s.defaultPassAlias, false, 1) if err != nil { return err @@ -82,8 +88,11 @@ func (s *SQLKeyDBStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.Privat KeywrapAlg: KeywrapAlg, PassphraseAlias: s.defaultPassAlias, Algorithm: privKey.Algorithm(), + Gun: gun, + Role: role, Public: string(privKey.Public()), - Private: encryptedKey} + Private: encryptedKey, + } // Add encrypted private key to the database s.db.Create(&gormPrivKey) @@ -96,8 +105,7 @@ func (s *SQLKeyDBStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.Privat return nil } -// GetKey returns the PrivateKey given a KeyID -func (s *SQLKeyDBStore) GetKey(keyID string) (data.PrivateKey, string, error) { +func (s *SQLKeyDBStore) getKey(keyID string, markActive bool) (*GormPrivateKey, string, error) { // Retrieve the GORM private key from the database dbPrivateKey := GormPrivateKey{} if s.db.Where(&GormPrivateKey{KeyID: keyID}).First(&dbPrivateKey).RecordNotFound() { @@ -116,6 +124,17 @@ func (s *SQLKeyDBStore) GetKey(keyID string) (data.PrivateKey, string, error) { return nil, "", err } + return &dbPrivateKey, decryptedPrivKey, nil +} + +// GetPrivateKey returns the PrivateKey given a KeyID +func (s *SQLKeyDBStore) GetPrivateKey(keyID string) (data.PrivateKey, string, error) { + // Retrieve the GORM private key from the database + dbPrivateKey, decryptedPrivKey, err := s.getKey(keyID, true) + if err != nil { + return nil, "", err + } + pubKey := data.NewPublicKey(dbPrivateKey.Algorithm, []byte(dbPrivateKey.Public)) // Create a new PrivateKey with unencrypted bytes privKey, err := data.NewPrivateKey(pubKey, []byte(decryptedPrivKey)) @@ -123,16 +142,16 @@ func (s *SQLKeyDBStore) GetKey(keyID string) (data.PrivateKey, string, error) { return nil, "", err } - return privKey, "", nil + return activatingPrivateKey{PrivateKey: privKey, activationFunc: s.markActive}, dbPrivateKey.Role, nil } -// GetKeyInfo returns the PrivateKey's role and gun in a KeyInfo given a KeyID -func (s *SQLKeyDBStore) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) { - return trustmanager.KeyInfo{}, fmt.Errorf("GetKeyInfo currently not supported for SQLKeyDBStore, as it does not track roles or GUNs") +// ListKeys always returns nil. This method is here to satisfy the CryptoService interface +func (s *SQLKeyDBStore) ListKeys(role string) []string { + return nil } -// ListKeys always returns nil. This method is here to satisfy the KeyStore interface -func (s *SQLKeyDBStore) ListKeys() map[string]trustmanager.KeyInfo { +// ListAllKeys always returns nil. This method is here to satisfy the CryptoService interface +func (s *SQLKeyDBStore) ListAllKeys() map[string]string { return nil } @@ -147,19 +166,7 @@ func (s *SQLKeyDBStore) RemoveKey(keyID string) error { // RotateKeyPassphrase rotates the key-encryption-key func (s *SQLKeyDBStore) RotateKeyPassphrase(keyID, newPassphraseAlias string) error { // Retrieve the GORM private key from the database - dbPrivateKey := GormPrivateKey{} - if s.db.Where(&GormPrivateKey{KeyID: keyID}).First(&dbPrivateKey).RecordNotFound() { - return trustmanager.ErrKeyNotFound{KeyID: keyID} - } - - // Get the current passphrase to use for this key - passphrase, _, err := s.retriever(dbPrivateKey.KeyID, dbPrivateKey.PassphraseAlias, false, 1) - if err != nil { - return err - } - - // Decrypt private bytes from the gorm key - decryptedPrivKey, _, err := jose.Decode(dbPrivateKey.Private, passphrase) + dbPrivateKey, decryptedPrivKey, err := s.getKey(keyID, false) if err != nil { return err } @@ -176,12 +183,49 @@ func (s *SQLKeyDBStore) RotateKeyPassphrase(keyID, newPassphraseAlias string) er return err } - // Update the database object - dbPrivateKey.Private = newEncryptedKey - dbPrivateKey.PassphraseAlias = newPassphraseAlias - s.db.Save(dbPrivateKey) + // want to only update 2 fields, not save the whole row - we have to use the where clause because key_id is not + // the primary key + return s.db.Model(GormPrivateKey{}).Where("key_id = ?", keyID).Updates(GormPrivateKey{ + Private: newEncryptedKey, + PassphraseAlias: newPassphraseAlias, + }).Error +} - return nil +// markActive marks a particular key as active +func (s *SQLKeyDBStore) markActive(keyID string) error { + // we have to use the where clause because key_id is not the primary key + return s.db.Model(GormPrivateKey{}).Where("key_id = ?", keyID).Updates(GormPrivateKey{LastUsed: s.nowFunc()}).Error +} + +// Create will attempt to first re-use an inactive key for the same role, gun, and algorithm. +// If one isn't found, it will create a private key and add it to the DB as an inactive key +func (s *SQLKeyDBStore) Create(role, gun, algorithm string) (data.PublicKey, error) { + // If an unused key exists, simply return it. Else, error because SQL can't make keys + dbPrivateKey := GormPrivateKey{} + if !s.db.Model(GormPrivateKey{}).Where("role = ? AND gun = ? AND algorithm = ? AND last_used IS NULL", role, gun, algorithm).Order("key_id").First(&dbPrivateKey).RecordNotFound() { + // Just return the public key component if we found one + return data.NewPublicKey(dbPrivateKey.Algorithm, []byte(dbPrivateKey.Public)), nil + } + + privKey, err := generatePrivateKey(algorithm) + if err != nil { + return nil, err + } + + if err = s.AddKey(role, gun, privKey); err != nil { + return nil, fmt.Errorf("failed to store key: %v", err) + } + + return privKey, nil +} + +// GetKey performs the same get as GetPrivateKey, but does not mark the as active and only returns the public bytes +func (s *SQLKeyDBStore) GetKey(keyID string) data.PublicKey { + privKey, _, err := s.getKey(keyID, false) + if err != nil { + return nil + } + return data.NewPublicKey(privKey.Algorithm, []byte(privKey.Public)) } // HealthCheck verifies that DB exists and is query-able diff --git a/signer/keydbstore/sql_keydbstore_test.go b/signer/keydbstore/sql_keydbstore_test.go index b3477ab00..d7ca0f806 100644 --- a/signer/keydbstore/sql_keydbstore_test.go +++ b/signer/keydbstore/sql_keydbstore_test.go @@ -1,15 +1,25 @@ +// !build rethinkdb + package keydbstore import ( + "crypto/rand" "testing" + "time" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" "github.com/dvsekhvalnov/jose2go" "github.com/stretchr/testify/require" ) +// not to the nanosecond scale because mysql timestamps ignore nanoseconds +var gormActiveTime = time.Date(2016, 12, 31, 1, 1, 1, 0, time.UTC) + func SetupSQLDB(t *testing.T, dbtype, dburl string) *SQLKeyDBStore { dbStore, err := NewSQLKeyDBStore(multiAliasRetriever, validAliases[0], dbtype, dburl) require.NoError(t, err) + dbStore.nowFunc = func() time.Time { return gormActiveTime } // Create the DB tables if they don't exist dbStore.db.CreateTable(&GormPrivateKey{}) @@ -58,42 +68,123 @@ func TestSQLDBHealthCheckNoConnection(t *testing.T) { require.Error(t, dbStore.HealthCheck()) } -func getSQLDBRows(t *testing.T, dbStore *SQLKeyDBStore) []GormPrivateKey { +// Checks that the DB contains the expected keys, and returns a map of the GormPrivateKey object by key ID +func requireExpectedGORMKeys(t *testing.T, dbStore *SQLKeyDBStore, expectedKeys []data.PrivateKey) map[string]GormPrivateKey { var rows []GormPrivateKey query := dbStore.db.Find(&rows) require.NoError(t, query.Error) - return rows + + require.Len(t, rows, len(expectedKeys)) + result := make(map[string]GormPrivateKey) + + for _, gormKey := range rows { + result[gormKey.KeyID] = gormKey + } + + for _, key := range expectedKeys { + gormKey, ok := result[key.ID()] + require.True(t, ok) + require.NotNil(t, gormKey) + require.Equal(t, string(key.Public()), gormKey.Public) + require.Equal(t, key.Algorithm(), gormKey.Algorithm) + } + + return result } func TestSQLKeyCanOnlyBeAddedOnce(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() + expectedKeys := testKeyCanOnlyBeAddedOnce(t, dbStore) - rows := getSQLDBRows(t, dbStore) - require.Len(t, rows, len(expectedKeys)) + gormKeys := requireExpectedGORMKeys(t, dbStore, expectedKeys) + + // none of these keys are active, since they have not been activated + for _, gormKey := range gormKeys { + require.True(t, gormKey.LastUsed.Equal(time.Time{})) + } } func TestSQLCreateDelete(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() - testCreateDelete(t, dbStore) + expectedKeys := testCreateDelete(t, dbStore) + + gormKeys := requireExpectedGORMKeys(t, dbStore, expectedKeys) - rows := getSQLDBRows(t, dbStore) - require.Len(t, rows, 0) + // none of these keys are active, since they have not been activated + for _, gormKey := range gormKeys { + require.True(t, gormKey.LastUsed.Equal(time.Time{})) + } } func TestSQLKeyRotation(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() - privKey := testKeyRotation(t, dbStore, validAliases[1]) - rows := getSQLDBRows(t, dbStore) - require.Len(t, rows, 1) + rotatedKey, nonRotatedKey := testKeyRotation(t, dbStore, validAliases[1]) + + gormKeys := requireExpectedGORMKeys(t, dbStore, []data.PrivateKey{rotatedKey, nonRotatedKey}) - // require that the key is encrypted with the new passphrase - require.Equal(t, validAliases[1], rows[0].PassphraseAlias) - decryptedKey, _, err := jose.Decode(string(rows[0].Private), validAliasesAndPasswds[validAliases[1]]) + // none of these keys are active, since they have not been activated + for _, gormKey := range gormKeys { + require.True(t, gormKey.LastUsed.Equal(time.Time{})) + } + + // require that the rotated key is encrypted with the new passphrase + rotatedGormKey := gormKeys[rotatedKey.ID()] + require.Equal(t, validAliases[1], rotatedGormKey.PassphraseAlias) + decryptedKey, _, err := jose.Decode(string(rotatedGormKey.Private), validAliasesAndPasswds[validAliases[1]]) require.NoError(t, err) - require.Equal(t, string(privKey.Private()), decryptedKey) + require.Equal(t, string(rotatedKey.Private()), decryptedKey) + + // require that the nonrotated key is encrypted with the old passphrase + nonRotatedGormKey := gormKeys[nonRotatedKey.ID()] + require.Equal(t, validAliases[0], nonRotatedGormKey.PassphraseAlias) + decryptedKey, _, err = jose.Decode(string(nonRotatedGormKey.Private), validAliasesAndPasswds[validAliases[0]]) + require.NoError(t, err) + require.Equal(t, string(nonRotatedKey.Private()), decryptedKey) +} + +func TestSQLSigningMarksKeyActive(t *testing.T) { + dbStore, cleanup := sqldbSetup(t) + defer cleanup() + + activeKey, nonActiveKey := testSigningWithKeyMarksAsActive(t, dbStore) + + gormKeys := requireExpectedGORMKeys(t, dbStore, []data.PrivateKey{activeKey, nonActiveKey}) + + // check that activation updates the activated key but not the unactivated key + require.True(t, gormKeys[activeKey.ID()].LastUsed.Equal(gormActiveTime)) + require.True(t, gormKeys[nonActiveKey.ID()].LastUsed.Equal(time.Time{})) + + // check that signing succeeds even if the DB connection is closed and hence + // mark as active errors + dbStore.db.Close() + msg := []byte("successful, db closed") + sig, err := nonActiveKey.Sign(rand.Reader, msg, nil) + require.NoError(t, err) + require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( + data.PublicKeyFromPrivate(nonActiveKey), sig, msg)) +} + +func TestSQLCreateKey(t *testing.T) { + dbStore, cleanup := sqldbSetup(t) + defer cleanup() + + activeED25519Key, pendingED25519Key, pendingECDSAKey := testCreateKey(t, dbStore) + + gormKeys := requireExpectedGORMKeys(t, dbStore, []data.PrivateKey{activeED25519Key, pendingED25519Key, pendingECDSAKey}) + + // check that activation updates the activated key but not the pending key + require.True(t, gormKeys[activeED25519Key.ID()].LastUsed.Equal(gormActiveTime)) + require.True(t, gormKeys[pendingED25519Key.ID()].LastUsed.Equal(time.Time{})) + require.True(t, gormKeys[pendingECDSAKey.ID()].LastUsed.Equal(time.Time{})) +} + +func TestSQLUnimplementedInterfaceBehavior(t *testing.T) { + dbStore, cleanup := sqldbSetup(t) + defer cleanup() + testUnimplementedInterfaceMethods(t, dbStore) } diff --git a/signer/rpc_and_client_test.go b/signer/rpc_and_client_test.go index 6fecddeb3..db817a77d 100644 --- a/signer/rpc_and_client_test.go +++ b/signer/rpc_and_client_test.go @@ -63,7 +63,7 @@ type stubServer struct { stubHealthFunc func() (map[string]string, error) } -func (s stubServer) CreateKey(ctx context.Context, req *pb.Algorithm) (*pb.PublicKey, error) { +func (s stubServer) CreateKey(ctx context.Context, req *pb.CreateKeyRequest) (*pb.PublicKey, error) { return nil, fmt.Errorf("not implemented") } @@ -71,7 +71,7 @@ func (s stubServer) DeleteKey(ctx context.Context, keyID *pb.KeyID) (*pb.Void, e return nil, fmt.Errorf("not implemented") } -func (s stubServer) GetKeyInfo(ctx context.Context, keyID *pb.KeyID) (*pb.PublicKey, error) { +func (s stubServer) GetKeyInfo(ctx context.Context, keyID *pb.KeyID) (*pb.GetKeyInfoResponse, error) { return nil, fmt.Errorf("not implemented") } @@ -125,7 +125,6 @@ func TestHealthCheckKMTimeout(t *testing.T) { err := signerClient.CheckHealth(0 * time.Second) require.Error(t, err) - fmt.Println(err) require.True(t, strings.Contains(err.Error(), "Timed out")) } @@ -168,10 +167,12 @@ func setUpSignerServer(t *testing.T, store trustmanager.KeyStore) *grpc.Server { grpcServer := grpc.NewServer() pb.RegisterKeyManagementServer(grpcServer, &api.KeyManagementServer{ CryptoServices: cryptoServices, - HealthChecker: fakeHealth}) + HealthChecker: fakeHealth, + }) pb.RegisterSignerServer(grpcServer, &api.SignerServer{ CryptoServices: cryptoServices, - HealthChecker: fakeHealth}) + HealthChecker: fakeHealth, + }) return grpcServer } @@ -187,8 +188,9 @@ func TestGetPrivateKeyAndSignWithExistingKey(t *testing.T) { signerClient, _, cleanup := setUpSignerClient(t, setUpSignerServer(t, memStore)) defer cleanup() - privKey, _, err := signerClient.GetPrivateKey(key.ID()) + privKey, role, err := signerClient.GetPrivateKey(key.ID()) require.NoError(t, err) + require.Equal(t, data.CanonicalTimestampRole, role) require.NotNil(t, privKey) msg := []byte("message!") @@ -202,6 +204,7 @@ func TestGetPrivateKeyAndSignWithExistingKey(t *testing.T) { func TestCannotSignWithKeyThatDoesntExist(t *testing.T) { memStore := trustmanager.NewKeyMemoryStore(constPass) + _, conn, cleanup := setUpSignerClient(t, setUpSignerServer(t, memStore)) defer cleanup() @@ -213,7 +216,8 @@ func TestCannotSignWithKeyThatDoesntExist(t *testing.T) { msg := []byte("message!") _, err = remotePrivKey.Sign(rand.Reader, msg, nil) require.Error(t, err) - require.Contains(t, err.Error(), "not found") + // error translated into grpc error, so compare the text + require.Equal(t, trustmanager.ErrKeyNotFound{KeyID: key.ID()}.Error(), grpc.ErrorDesc(err)) } // Signer conforms to the signed.CryptoService interface behavior @@ -223,7 +227,7 @@ func TestCryptoSignerInterfaceBehavior(t *testing.T) { defer cleanup() interfaces.EmptyCryptoServiceInterfaceBehaviorTests(t, signerClient) - interfaces.CreateGetKeyCryptoServiceInterfaceBehaviorTests(t, signerClient, data.ECDSAKey, false) + interfaces.CreateGetKeyCryptoServiceInterfaceBehaviorTests(t, signerClient, data.ECDSAKey) // can't test AddKey, because the signer does not support adding keys, and can't test listing - // keys because the signer doesn't support listing keys or keeping track of roles. + // keys because the signer doesn't support listing keys. } diff --git a/signer/signer.go b/signer/signer.go index 074282ac9..20d99114e 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -4,6 +4,8 @@ import ( "crypto/tls" pb "github.com/docker/notary/proto" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" ) @@ -41,4 +43,5 @@ type Config struct { GRPCAddr string TLSConfig *tls.Config CryptoServices CryptoServiceIndex + PendingKeyFunc func(trustmanager.KeyInfo) (data.PublicKey, error) } diff --git a/storage/httpstore.go b/storage/httpstore.go index 1a0d9c583..4eee08db3 100644 --- a/storage/httpstore.go +++ b/storage/httpstore.go @@ -309,6 +309,31 @@ func (s HTTPStore) GetKey(role string) ([]byte, error) { return body, nil } +// RotateKey rotates a private key and returns the public component from the remote server +func (s HTTPStore) RotateKey(role string) ([]byte, error) { + url, err := s.buildKeyURL(role) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", url.String(), nil) + if err != nil { + return nil, err + } + resp, err := s.roundTrip.RoundTrip(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err := translateStatusToError(resp, role+" key"); err != nil { + return nil, err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} + // Location returns a human readable name for the storage location func (s HTTPStore) Location() string { return s.baseURL.String() diff --git a/storage/httpstore_test.go b/storage/httpstore_test.go index 359fe82f2..09bceb6b0 100644 --- a/storage/httpstore_test.go +++ b/storage/httpstore_test.go @@ -206,6 +206,20 @@ func TestHTTPStoreRemoveAll(t *testing.T) { require.NoError(t, err) } +func TestHTTPStoreRotateKey(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testRootKey)) + } + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + store, err := NewHTTPStore(server.URL, "metadata", "json", "key", http.DefaultTransport) + require.NoError(t, err) + + pubKeyBytes, err := store.RotateKey(data.CanonicalSnapshotRole) + require.NoError(t, err) + require.Equal(t, pubKeyBytes, []byte(testRootKey)) +} + func TestHTTPOffline(t *testing.T) { s, err := NewHTTPStore("https://localhost/", "", "", "", nil) require.NoError(t, err) diff --git a/storage/interfaces.go b/storage/interfaces.go index f7813247e..867c9f181 100644 --- a/storage/interfaces.go +++ b/storage/interfaces.go @@ -16,6 +16,7 @@ type MetadataStore interface { // PublicKeyStore must be implemented by a key service type PublicKeyStore interface { GetKey(role string) ([]byte, error) + RotateKey(role string) ([]byte, error) } // RemoteStore is similar to LocalStore with the added expectation that it should diff --git a/storage/offlinestore.go b/storage/offlinestore.go index a9433c1ad..fd297601e 100644 --- a/storage/offlinestore.go +++ b/storage/offlinestore.go @@ -38,6 +38,11 @@ func (es OfflineStore) GetKey(role string) ([]byte, error) { return nil, err } +// RotateKey returns ErrOffline +func (es OfflineStore) RotateKey(role string) ([]byte, error) { + return nil, err +} + // RemoveAll return ErrOffline func (es OfflineStore) RemoveAll() error { return err diff --git a/storage/offlinestore_test.go b/storage/offlinestore_test.go index 659211e42..57f9073e4 100644 --- a/storage/offlinestore_test.go +++ b/storage/offlinestore_test.go @@ -24,6 +24,10 @@ func TestOfflineStore(t *testing.T) { require.Error(t, err) require.IsType(t, ErrOffline{}, err) + _, err = s.RotateKey("") + require.Error(t, err) + require.IsType(t, ErrOffline{}, err) + err = s.RemoveAll() require.Error(t, err) require.IsType(t, ErrOffline{}, err) diff --git a/tuf/builder.go b/tuf/builder.go index 9e1d3efb6..6b77be5bf 100644 --- a/tuf/builder.go +++ b/tuf/builder.go @@ -18,7 +18,7 @@ var ErrBuildDone = fmt.Errorf( "the builder has finished building and cannot accept any more input or produce any more output") // ErrInvalidBuilderInput is returned when RepoBuilder.Load is called -// with the wrong type of metadata for thes tate that it's in +// with the wrong type of metadata for the state that it's in type ErrInvalidBuilderInput struct{ msg string } func (e ErrInvalidBuilderInput) Error() string { @@ -349,7 +349,7 @@ func (rb *repoBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, in return nil, 0, ErrInvalidBuilderInput{msg: "timestamp has already been loaded"} } - // SignTimetamp always serializes the loaded snapshot and signs in the data, so we must always + // SignTimestamp always serializes the loaded snapshot and signs in the data, so we must always // have the snapshot loaded first if err := rb.checkPrereqsLoaded([]string{data.CanonicalRootRole, data.CanonicalSnapshotRole}); err != nil { return nil, 0, err @@ -422,7 +422,6 @@ func (rb *repoBuilder) loadRoot(content []byte, minVersion int, allowExpired boo if err != nil { // this should never happen since the root has been validated return err } - rb.repo.Root = signedRoot rb.repo.originalRootRole = rootRole return nil diff --git a/tuf/signed/ed25519.go b/tuf/signed/ed25519.go index eef673b9d..7a70739e4 100644 --- a/tuf/signed/ed25519.go +++ b/tuf/signed/ed25519.go @@ -96,7 +96,10 @@ func (e *Ed25519) PublicKeys(keyIDs ...string) (map[string]data.PublicKey, error // GetKey returns a single public key based on the ID func (e *Ed25519) GetKey(keyID string) data.PublicKey { - return data.PublicKeyFromPrivate(e.keys[keyID].privKey) + if privKey, _, err := e.GetPrivateKey(keyID); err == nil { + return data.PublicKeyFromPrivate(privKey) + } + return nil } // GetPrivateKey returns a single private key and role if present, based on the ID diff --git a/tuf/signed/ed25519_test.go b/tuf/signed/ed25519_test.go index 674790351..31a70ed5a 100644 --- a/tuf/signed/ed25519_test.go +++ b/tuf/signed/ed25519_test.go @@ -22,3 +22,29 @@ func TestListKeys(t *testing.T) { require.Len(t, c.ListKeys(data.CanonicalTargetsRole), 0) } + +// GetKey and GetPrivateKey only gets keys that we've added to this service +func TestGetKeys(t *testing.T) { + c := NewEd25519() + tskey, err := c.Create(data.CanonicalTimestampRole, "", data.ED25519Key) + require.NoError(t, err) + + pubKey := c.GetKey(tskey.ID()) + require.NotNil(t, pubKey) + require.Equal(t, tskey.Public(), pubKey.Public()) + require.Equal(t, tskey.Algorithm(), pubKey.Algorithm()) + require.Equal(t, tskey.ID(), pubKey.ID()) + + privKey, role, err := c.GetPrivateKey(tskey.ID()) + require.NoError(t, err) + require.Equal(t, data.CanonicalTimestampRole, role) + require.Equal(t, tskey.Public(), privKey.Public()) + require.Equal(t, tskey.Algorithm(), privKey.Algorithm()) + require.Equal(t, tskey.ID(), privKey.ID()) + + // if the key doesn't exist, GetKey returns nil and GetPrivateKey errors out + randomKey := c.GetKey("someID") + require.Nil(t, randomKey) + _, _, err = c.GetPrivateKey("someID") + require.Error(t, err) +} diff --git a/tuf/testutils/interfaces/cryptoservice.go b/tuf/testutils/interfaces/cryptoservice.go index d1c31dda1..71736ba51 100644 --- a/tuf/testutils/interfaces/cryptoservice.go +++ b/tuf/testutils/interfaces/cryptoservice.go @@ -41,8 +41,7 @@ func EmptyCryptoServiceInterfaceBehaviorTests(t *testing.T, empty signed.CryptoS // 1. Creating a key succeeds and returns a non-nil public key // 2. Getting the key should return the same key, without error // 3. Removing the key succeeds -func CreateGetKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.CryptoService, algo string, - checkRole bool) { +func CreateGetKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.CryptoService, algo string) { expectedRolesToKeys := make(map[string]string) for i := 0; i < 2; i++ { @@ -53,15 +52,14 @@ func CreateGetKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.Cry expectedRolesToKeys[role] = createdPubKey.ID() } - testGetKey(t, cs, expectedRolesToKeys, algo, checkRole) + testGetKey(t, cs, expectedRolesToKeys, algo) } // CreateListKeyCryptoServiceInterfaceBehaviorTests tests expected behavior for -// creating keys in a signed.CryptoService and other read operations on the -// crypto service after keys are present +// creating keys in a signed.CryptoService and listing keys after keys are +// present // 1. Creating a key succeeds and returns a non-nil public key // 2. Listing returns the correct number of keys and right roles -// We allow skipping some tests because for now, signer does not support role checking or listing keys. func CreateListKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.CryptoService, algo string) { expectedRolesToKeys := make(map[string]string) for i := 0; i < 2; i++ { @@ -105,7 +103,7 @@ func AddGetKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.Crypto expectedRolesToKeys[role] = addedPrivKey.ID() } - testGetKey(t, cs, expectedRolesToKeys, algo, true) + testGetKey(t, cs, expectedRolesToKeys, algo) } // AddListKeyCryptoServiceInterfaceBehaviorTests tests expected behavior for @@ -140,9 +138,7 @@ func AddListKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.Crypt testListKeys(t, cs, expectedRolesToKeys) } -func testGetKey(t *testing.T, cs signed.CryptoService, expectedRolesToKeys map[string]string, algo string, - checkRole bool) { - +func testGetKey(t *testing.T, cs signed.CryptoService, expectedRolesToKeys map[string]string, algo string) { for role, keyID := range expectedRolesToKeys { pubKey := cs.GetKey(keyID) require.NotNil(t, pubKey) @@ -154,17 +150,13 @@ func testGetKey(t *testing.T, cs signed.CryptoService, expectedRolesToKeys map[s require.NotNil(t, privKey) require.Equal(t, keyID, privKey.ID()) require.Equal(t, algo, privKey.Algorithm()) - if checkRole { - require.Equal(t, role, gotRole) - } + require.Equal(t, role, gotRole) require.NoError(t, cs.RemoveKey(keyID)) require.Nil(t, cs.GetKey(keyID)) } } -// The signer does not yet support listing keys or tracking roles, so skip those parts of this test if we're testing -// the signer func testListKeys(t *testing.T, cs signed.CryptoService, expectedRolesToKeys map[string]string) { for _, role := range append(data.BaseRoles, "targets/delegation", "invalid") { keys := cs.ListKeys(role)