diff --git a/internal/integration/helpers.go b/internal/integration/helpers.go index 6b607bbbe..a16cd49bb 100644 --- a/internal/integration/helpers.go +++ b/internal/integration/helpers.go @@ -95,18 +95,11 @@ func NewTestServer(tb testing.TB, exportPeriod time.Duration) (*serverenv.Server } } - km, err := keys.NewInMemory(ctx) - if err != nil { - tb.Fatal(err) - } - if _, err := km.CreateEncryptionKey("tokenkey"); err != nil { - tb.Fatal(err) - } - if _, err := km.CreateSigningKey(ctx, "signing", "signingkey"); err != nil { - tb.Fatal(err) - } + kms := keys.TestKeyManager(tb) + tokenKey := keys.TestEncryptionKey(tb, kms) + // create an initial revision key. - revisionDB, err := revdb.New(db, &revdb.KMSConfig{WrapperKeyID: "tokenkey", KeyManager: km}) + revisionDB, err := revdb.New(db, &revdb.KMSConfig{WrapperKeyID: tokenKey, KeyManager: kms}) if err != nil { tb.Fatal(err) } @@ -144,7 +137,7 @@ func NewTestServer(tb testing.TB, exportPeriod time.Duration) (*serverenv.Server serverenv.WithAuthorizedAppProvider(aap), serverenv.WithBlobStorage(bs), serverenv.WithDatabase(db), - serverenv.WithKeyManager(km), + serverenv.WithKeyManager(kms), serverenv.WithSecretManager(sm), ) // Note: don't call env.Cleanup() because the database helper closes the @@ -208,7 +201,7 @@ func NewTestServer(tb testing.TB, exportPeriod time.Duration) (*serverenv.Server // TODO: this is a grpc listener and requires a lot of setup. revConfig := revision.Config{ - KeyID: "tokenkey", + KeyID: tokenKey, AAD: []byte{1, 2, 3}, MinLength: 28, } diff --git a/internal/keyrotation/rotate_test.go b/internal/keyrotation/rotate_test.go index 41b41478e..d9e0ecd21 100644 --- a/internal/keyrotation/rotate_test.go +++ b/internal/keyrotation/rotate_test.go @@ -35,11 +35,9 @@ func TestRotateKeys(t *testing.T) { t.Parallel() ctx := context.Background() - kms, _ := keys.NewInMemory(context.Background()) - keyID := "testKeyID" - if _, err := kms.CreateEncryptionKey(keyID); err != nil { - t.Fatal(err) - } + kms := keys.TestKeyManager(t) + keyID := keys.TestEncryptionKey(t, kms) + config := &Config{ RevisionToken: revision.Config{KeyID: keyID}, DeleteOldKeyPeriod: 14 * 24 * time.Hour, // two weeks diff --git a/internal/keyrotation/server_test.go b/internal/keyrotation/server_test.go index 3a36d5fa7..f2437711f 100644 --- a/internal/keyrotation/server_test.go +++ b/internal/keyrotation/server_test.go @@ -31,7 +31,7 @@ func TestNewRotationHandler(t *testing.T) { ctx := context.Background() testDB := database.NewTestDatabase(t) - kms, _ := keys.NewInMemory(context.Background()) + kms := keys.TestKeyManager(t) testCases := []struct { name string @@ -61,10 +61,8 @@ func TestNewRotationHandler(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - keyID := "test" + t.Name() - if _, err := kms.CreateEncryptionKey(keyID); err != nil { - t.Fatal(err) - } + keyID := keys.TestEncryptionKey(t, kms) + config := &Config{ RevisionToken: revision.Config{KeyID: keyID}, } diff --git a/internal/publish/publish_test.go b/internal/publish/publish_test.go index 5af096944..aa865fd97 100644 --- a/internal/publish/publish_test.go +++ b/internal/publish/publish_test.go @@ -387,17 +387,13 @@ func TestPublishWithBypass(t *testing.T) { } ctx := context.Background() + // Database init for all modules that will be used. testDB := coredb.NewTestDatabase(t) - // Make key manager - kms, err := keys.NewInMemory(ctx) - if err != nil { - t.Fatalf("can't make kms: %v", err) - } - keyID := "rev" - if _, err := kms.CreateEncryptionKey(keyID); err != nil { - t.Fatal(err) - } + + kms := keys.TestKeyManager(t) + keyID := keys.TestEncryptionKey(t, kms) + tokenAAD := make([]byte, 16) if _, err := rand.Read(tokenAAD); err != nil { t.Fatalf("not enough entropy: %v", err) @@ -707,17 +703,13 @@ func TestKeyRevision(t *testing.T) { } ctx := context.Background() + // Database init for all modules that will be used. testDB := coredb.NewTestDatabase(t) - // Make key manager - kms, err := keys.NewInMemory(ctx) - if err != nil { - t.Fatalf("can't make kms: %v", err) - } - keyID := "rev" - if _, err := kms.CreateEncryptionKey(keyID); err != nil { - t.Fatal(err) - } + + kms := keys.TestKeyManager(t) + keyID := keys.TestEncryptionKey(t, kms) + tokenAAD := make([]byte, 16) if _, err := rand.Read(tokenAAD); err != nil { t.Fatalf("not enough entropy: %v", err) diff --git a/internal/revision/database/revision_test.go b/internal/revision/database/revision_test.go index 2e1d8a259..2c4bd4690 100644 --- a/internal/revision/database/revision_test.go +++ b/internal/revision/database/revision_test.go @@ -35,14 +35,8 @@ func TestRevisionKey(t *testing.T) { testDB := database.NewTestDatabase(t) ctx := context.Background() - kms, err := keys.NewInMemory(ctx) - if err != nil { - t.Fatalf("unable to cerate in memory KMS") - } - keyID := "funkey" - if _, err := kms.CreateEncryptionKey(keyID); err != nil { - t.Fatalf("unable to generate key: %v", err) - } + kms := keys.TestKeyManager(t) + keyID := keys.TestEncryptionKey(t, kms) cfg := KMSConfig{keyID, kms} revDB, err := New(testDB, &cfg) @@ -71,14 +65,8 @@ func TestMultipleRevisionKeys(t *testing.T) { testDB := database.NewTestDatabase(t) ctx := context.Background() - kms, err := keys.NewInMemory(ctx) - if err != nil { - t.Fatalf("unable to cerate in memory KMS") - } - keyID := "funkey" - if _, err := kms.CreateEncryptionKey(keyID); err != nil { - t.Fatalf("unable to generate key: %v", err) - } + kms := keys.TestKeyManager(t) + keyID := keys.TestEncryptionKey(t, kms) cfg := KMSConfig{keyID, kms} revDB, err := New(testDB, &cfg) diff --git a/internal/revision/revision_test.go b/internal/revision/revision_test.go index 3f2f23827..a1d779049 100644 --- a/internal/revision/revision_test.go +++ b/internal/revision/revision_test.go @@ -16,9 +16,7 @@ package revision import ( "context" - "crypto/rand" "fmt" - "io" "testing" "time" @@ -151,18 +149,8 @@ func TestEncryptDecrypt(t *testing.T) { testDB := database.NewTestDatabase(t) ctx := context.Background() - kms, err := keys.NewInMemory(ctx) - if err != nil { - t.Fatalf("unable to cerate in memory KMS") - } - keyID := "skeleton" - if _, err := kms.CreateEncryptionKey(keyID); err != nil { - t.Fatalf("unable to generate key: %v", err) - } - key := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, key); err != nil { - t.Fatalf("unable to generate AES key: %v", err) - } + kms := keys.TestKeyManager(t) + keyID := keys.TestEncryptionKey(t, kms) cfg := revisiondb.KMSConfig{ WrapperKeyID: keyID, diff --git a/internal/setup/setup_test.go b/internal/setup/setup_test.go index 809bfcf4f..c1d41f7ef 100644 --- a/internal/setup/setup_test.go +++ b/internal/setup/setup_test.go @@ -61,7 +61,7 @@ func (t *testConfig) DatabaseConfig() *database.Config { func (t *testConfig) KeyManagerConfig() *keys.Config { return &keys.Config{ - KeyManagerType: keys.KeyManagerType("IN_MEMORY"), + KeyManagerType: keys.KeyManagerType("FILESYSTEM"), } } @@ -186,8 +186,8 @@ func TestSetupWith(t *testing.T) { t.Errorf("expected key manager to exist") } - if _, ok := km.(*keys.InMemory); !ok { - t.Errorf("expected %T to be InMemory", km) + if _, ok := km.(*keys.Filesystem); !ok { + t.Errorf("expected %T to be Filesystem", km) } }) diff --git a/pkg/keys/config.go b/pkg/keys/config.go index 57d379b9a..1d4f46aa1 100644 --- a/pkg/keys/config.go +++ b/pkg/keys/config.go @@ -22,7 +22,7 @@ const ( KeyManagerTypeAzureKeyVault KeyManagerType = "AZURE_KEY_VAULT" KeyManagerTypeGoogleCloudKMS KeyManagerType = "GOOGLE_CLOUD_KMS" KeyManagerTypeHashiCorpVault KeyManagerType = "HASHICORP_VAULT" - KeyManagerTypeInMemory KeyManagerType = "IN_MEMORY" + KeyManagerTypeFilesystem KeyManagerType = "FILESYSTEM" ) // Config defines configuration. @@ -34,4 +34,7 @@ type Config struct { // Adherence to this config setting is optional and based // upon the key manager implementation and underlying capabilities. CreateHSMKeys bool `env:"CREATE_HSM_KEYS, default=true"` + + // FilesystemRoot is the root path where keys are managed on the filesystem. + FilesystemRoot string `env:"KEY_FILESYSTEM_ROOT"` } diff --git a/pkg/keys/filesystem.go b/pkg/keys/filesystem.go new file mode 100644 index 000000000..adc894c76 --- /dev/null +++ b/pkg/keys/filesystem.go @@ -0,0 +1,418 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package keys + +import ( + "bytes" + "context" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +var _ EncryptionKeyManager = (*Filesystem)(nil) +var _ KeyManager = (*Filesystem)(nil) +var _ SigningKeyManager = (*Filesystem)(nil) + +// Filesystem is a key manager that uses the filesystem to store and retrieve +// keys. It should only be used for local development and testing. +type Filesystem struct { + root string + mu sync.RWMutex +} + +// NewFilesystem creates a new KeyManager backed by the local filesystem. It +// should only be used for development and testing. +// +// If root is provided and does not exist, it will be created. If root is a +// relative path, it's relative to where the process is currently executing. If +// root is not supplied, all data is dumped in the current working directory. +// +// In general, root should either be a hardcoded path like $(pwd)/local or a +// temporary directory like os.TempDir(). +func NewFilesystem(ctx context.Context, root string) (*Filesystem, error) { + if root != "" { + if err := os.MkdirAll(root, 0700); err != nil { + return nil, err + } + } + + return &Filesystem{ + root: root, + }, nil +} + +// NewSigner creates a new signer from the given key. If the key does not exist, +// it returns an error. If the key is not a signing key, it returns an error. +func (k *Filesystem) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + pth := filepath.Join(k.root, keyID) + b, err := ioutil.ReadFile(pth) + if err != nil { + return nil, fmt.Errorf("failed to read signing key: %w", err) + } + + pk, err := x509.ParseECPrivateKey(b) + if err != nil { + return nil, fmt.Errorf("failed to parse signing key: %w", err) + } + + return pk, nil +} + +// Encrypt encrypts the given plaintext and aad with the key. If the key does +// not exist, it returns an error. +func (k *Filesystem) Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + // Find the most recent DEK - that's what we'll use for encryption + pth := filepath.Join(k.root, keyID) + infos, err := ioutil.ReadDir(pth) + if err != nil { + return nil, fmt.Errorf("failed to list keys: %w", err) + } + if len(infos) < 1 { + return nil, fmt.Errorf("there are no key versions") + } + var latest os.FileInfo + for _, info := range infos { + if info.Name() == "metadata" { + continue + } + if latest == nil { + latest = info + continue + } + if info.Name() > latest.Name() { + latest = info + } + } + if latest == nil { + return nil, fmt.Errorf("key %q does not exist", keyID) + } + + latestPath := filepath.Join(pth, latest.Name()) + dek, err := ioutil.ReadFile(latestPath) + if err != nil { + return nil, fmt.Errorf("failed to read encryption key: %w", err) + } + + block, err := aes.NewCipher(dek) + if err != nil { + return nil, fmt.Errorf("bad cipher block: %w", err) + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to wrap cipher block: %w", err) + } + nonce := make([]byte, aesgcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %w", err) + } + ciphertext := aesgcm.Seal(nonce, nonce, plaintext, aad) + + // Append the keyID to the ciphertext so we know which key to use to decrypt. + id := []byte(latest.Name() + ":") + ciphertext = append(id, ciphertext...) + + return ciphertext, nil +} + +// Decrypt decrypts the ciphertext. It returns an error if decryption fails or +// if the key does not exist. +func (k *Filesystem) Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + // Figure out which DEK to use + parts := bytes.SplitN(ciphertext, []byte(":"), 2) + if len(parts) < 2 { + return nil, fmt.Errorf("invalid ciphertext: missing version") + } + version, ciphertext := parts[0], parts[1] + + versionPath := filepath.Join(k.root, keyID, string(version)) + dek, err := ioutil.ReadFile(versionPath) + if err != nil { + return nil, fmt.Errorf("failed to read encryption key: %w", err) + } + + block, err := aes.NewCipher(dek) + if err != nil { + return nil, fmt.Errorf("failed to create cipher from dek: %w", err) + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create gcm from dek: %w", err) + } + + size := aesgcm.NonceSize() + if len(ciphertext) < size { + return nil, fmt.Errorf("malformed ciphertext") + } + nonce, ciphertextPortion := ciphertext[:size], ciphertext[size:] + + plaintext, err := aesgcm.Open(nil, nonce, ciphertextPortion, aad) + if err != nil { + return nil, fmt.Errorf("failed to decrypt ciphertext with dek: %w", err) + } + + return plaintext, nil +} + +// SigningKeyVersions lists all the versions for the given parent. If the +// provided parent is not a signing key, it returns an error. +func (k *Filesystem) SigningKeyVersions(ctx context.Context, parent string) ([]SigningKeyVersion, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + metadata, err := k.metadataForKey(parent) + if err != nil { + return nil, fmt.Errorf("failed to list signing keys: %w", err) + } + if metadata.KeyType != "signing" { + return nil, fmt.Errorf("failed to list signing keys: key is not a signing key type") + } + + pth := filepath.Join(k.root, parent) + var versions []SigningKeyVersion + if err := filepath.Walk(pth, func(curr string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + if info.Name() == "metadata" { + return nil + } + + b, err := ioutil.ReadFile(curr) + if err != nil { + return err + } + + pk, err := x509.ParseECPrivateKey(b) + if err != nil { + return fmt.Errorf("failed to parse signing key: %w", err) + } + + versions = append(versions, &filesystemSigningKey{ + name: strings.TrimPrefix(curr, k.root), + created: info.ModTime(), + pk: pk, + }) + + return nil + }); err != nil { + return nil, fmt.Errorf("failed to list keys: failed to walk: %w", err) + } + + // Sort keys descending so the newest is first + sort.Slice(versions, func(i, j int) bool { + a := versions[i].(*filesystemSigningKey).name + b := versions[j].(*filesystemSigningKey).name + return a > b + }) + + return versions, nil +} + +// CreateSigningKey creates a signing key. For this implementation, that means +// it creates a folder on disk (but no keys inside). If the folder already +// exists, it returns its name. +func (k *Filesystem) CreateSigningKey(_ context.Context, parent, name string) (string, error) { + k.mu.Lock() + defer k.mu.Unlock() + + pth := filepath.Join(k.root, parent, name) + if err := os.MkdirAll(pth, 0700); err != nil { + return "", fmt.Errorf("failed to create directory for key: %w", err) + } + + metadataPath := filepath.Join(pth, "metadata") + b, err := ioutil.ReadFile(metadataPath) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("failed to read metadata file: %w", err) + } + if len(b) > 0 { + var metadata filesystemKeyInfo + if err := json.Unmarshal(b, &metadata); err != nil { + return "", fmt.Errorf("failed to parse metadata: %w", err) + } + if metadata.KeyType != "signing" { + return "", fmt.Errorf("found key, but is not signing type") + } + return strings.TrimPrefix(pth, k.root), nil + } + + // If we got this far, the metadata file does not exist, so create it. + metadata := &filesystemKeyInfo{KeyType: "signing"} + b, err = json.Marshal(metadata) + if err != nil { + return "", fmt.Errorf("failed to generate metadata file: %w", err) + } + if err := ioutil.WriteFile(metadataPath, b, 0600); err != nil { + return "", fmt.Errorf("failed to create metadata file: %w", err) + } + return strings.TrimPrefix(pth, k.root), nil +} + +// CreateEncryptionKey creates an encryption key. For this implementation, that +// means it creates a folder on disk (but no keys inside). If the folder already +// exists, it returns its name. +func (k *Filesystem) CreateEncryptionKey(_ context.Context, parent, name string) (string, error) { + k.mu.Lock() + defer k.mu.Unlock() + + pth := filepath.Join(k.root, parent, name) + if err := os.MkdirAll(pth, 0700); err != nil { + return "", fmt.Errorf("failed to create directory for key: %w", err) + } + + metadataPath := filepath.Join(pth, "metadata") + b, err := ioutil.ReadFile(metadataPath) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("failed to read metadata file: %w", err) + } + if len(b) > 0 { + var metadata filesystemKeyInfo + if err := json.Unmarshal(b, &metadata); err != nil { + return "", fmt.Errorf("failed to parse metadata: %w", err) + } + if metadata.KeyType != "encryption" { + return "", fmt.Errorf("found key, but is not encryption type") + } + return strings.TrimPrefix(pth, k.root), nil + } + + // If we got this far, the metadata file does not exist, so create it. + metadata := &filesystemKeyInfo{KeyType: "encryption"} + b, err = json.Marshal(metadata) + if err != nil { + return "", fmt.Errorf("failed to generate metadata file: %w", err) + } + if err := ioutil.WriteFile(metadataPath, b, 0600); err != nil { + return "", fmt.Errorf("failed to create metadata file: %w", err) + } + return strings.TrimPrefix(pth, k.root), nil +} + +// CreateKeyVersion creates a new key version for the parent. If the parent is a +// signing key, it creates a signing key. If the parent is an encryption key, it +// creates an encryption key. If the parent does not exist, it returns an error. +func (k *Filesystem) CreateKeyVersion(_ context.Context, parent string) (string, error) { + k.mu.Lock() + defer k.mu.Unlock() + + metadata, err := k.metadataForKey(parent) + if err != nil { + return "", fmt.Errorf("failed to create key version: %w", err) + } + + switch t := metadata.KeyType; t { + case "signing": + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return "", fmt.Errorf("failed to generate signing key: %w", err) + } + b, err := x509.MarshalECPrivateKey(pk) + if err != nil { + return "", fmt.Errorf("failed to marshal signing key: %w", err) + } + pth := filepath.Join(k.root, parent, strconv.FormatInt(time.Now().UnixNano(), 10)) + if err := ioutil.WriteFile(pth, b, 0600); err != nil { + return "", fmt.Errorf("failed to write signing key to disk: %w", err) + } + return strings.TrimPrefix(pth, k.root), nil + case "encryption": + ek := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, ek); err != nil { + return "", fmt.Errorf("failed to generate encryption key: %w", err) + } + pth := filepath.Join(k.root, parent, strconv.FormatInt(time.Now().UnixNano(), 10)) + if err := ioutil.WriteFile(pth, ek, 0600); err != nil { + return "", fmt.Errorf("failed to write encryption key to disk: %w", err) + } + return strings.TrimPrefix(pth, k.root), nil + default: + return "", fmt.Errorf("unknown key type %q", t) + } +} + +// DestroyKeyVersion destroys the given key version. It does nothing if the key +// does not exist. +func (k *Filesystem) DestroyKeyVersion(_ context.Context, id string) error { + k.mu.Lock() + defer k.mu.Unlock() + + pth := filepath.Join(k.root, id) + if err := os.Remove(pth); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to destroy key version: %w", err) + } + return nil +} + +type filesystemSigningKey struct { + name string + created time.Time + pk *ecdsa.PrivateKey +} + +func (k *filesystemSigningKey) KeyID() string { return k.name } +func (k *filesystemSigningKey) CreatedAt() time.Time { return k.created } +func (k *filesystemSigningKey) DestroyedAt() time.Time { return time.Time{} } +func (k *filesystemSigningKey) Signer(_ context.Context) (crypto.Signer, error) { + return k.pk, nil +} + +type filesystemKeyInfo struct { + KeyType string `json:"t"` +} + +func (k *Filesystem) metadataForKey(parent string) (*filesystemKeyInfo, error) { + metadataPath := filepath.Join(k.root, parent, "metadata") + b, err := ioutil.ReadFile(metadataPath) + if err != nil { + return nil, fmt.Errorf("failed to open metadata (does the key exist?): %w", err) + } + + var metadata filesystemKeyInfo + if err := json.Unmarshal(b, &metadata); err != nil { + return nil, fmt.Errorf("failed to parse metadata: %w", err) + } + return &metadata, nil +} diff --git a/pkg/keys/filesystem_test.go b/pkg/keys/filesystem_test.go new file mode 100644 index 000000000..d25096a1c --- /dev/null +++ b/pkg/keys/filesystem_test.go @@ -0,0 +1,389 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package keys + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestNewFilesystem(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("root_empty", func(t *testing.T) { + t.Parallel() + + if _, err := NewFilesystem(ctx, ""); err != nil { + t.Fatal(err) + } + }) + + t.Run("root_relative", func(t *testing.T) { + t.Parallel() + + t.Cleanup(func() { + if err := os.RemoveAll("tmp1"); err != nil { + t.Fatal(err) + } + }) + + fs, err := NewFilesystem(ctx, "tmp1") + if err != nil { + t.Fatal(err) + } + + if _, err := fs.CreateSigningKey(ctx, "foo", "bar"); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat("tmp1/foo/bar"); err != nil { + t.Fatal(err) + } + }) + + t.Run("root_absolute", func(t *testing.T) { + t.Parallel() + + tmp, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + if err := os.RemoveAll(tmp); err != nil { + t.Fatal(err) + } + }) + + fs, err := NewFilesystem(ctx, tmp) + if err != nil { + t.Fatal(err) + } + + if _, err := fs.CreateSigningKey(ctx, "foo", "bar"); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(tmp + "/foo/bar"); err != nil { + t.Fatal(err) + } + }) +} + +func TestFilesystem_NewSigner(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + keyID string + setup func(string) error + err string + }{ + { + name: "error_key_not_exist", + keyID: "totally_not_valid", + err: "failed to read signing key", + }, + { + name: "error_key_not_ecdsa", + keyID: "banana", + setup: func(dir string) error { + pth := filepath.Join(dir, "banana") + return ioutil.WriteFile(pth, []byte("dafd"), 0600) + }, + err: "failed to parse signing key", + }, + { + name: "happy", + keyID: "apple", + setup: func(dir string) error { + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + b, err := x509.MarshalECPrivateKey(pk) + if err != nil { + return err + } + + pth := filepath.Join(dir, "apple") + return ioutil.WriteFile(pth, b, 0600) + }, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + + if tc.setup != nil { + if err := tc.setup(dir); err != nil { + t.Fatal(err) + } + } + + fs, err := NewFilesystem(ctx, dir) + if err != nil { + t.Fatal(err) + } + + if _, err := fs.NewSigner(ctx, tc.keyID); err != nil { + if tc.err == "" { + t.Fatal(err) + } + + if !strings.Contains(err.Error(), tc.err) { + t.Fatalf("expected %#v to contain %#v", err.Error(), tc.err) + } + } + }) + } +} + +func TestFilesystem_EncryptDecrypt(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + keyID string + plaintext []byte + aad []byte + setup func(*Filesystem) error + err string + }{ + { + name: "error_key_not_exist", + keyID: "totally_not_valid", + err: "failed to list keys", + }, + { + name: "no_key_versions", + keyID: "banana", + setup: func(fs *Filesystem) error { + dir := filepath.Join(fs.root, "banana") + return os.MkdirAll(dir, 0700) + }, + err: "no key versions", + }, + { + name: "happy", + keyID: "apple", + plaintext: []byte("bacon"), + setup: func(fs *Filesystem) error { + ctx := context.Background() + id, err := fs.CreateEncryptionKey(ctx, "", "apple") + if err != nil { + return err + } + if _, err := fs.CreateKeyVersion(ctx, id); err != nil { + return err + } + return nil + }, + }, + { + name: "multi", + keyID: "apple", + plaintext: []byte("bacon"), + setup: func(fs *Filesystem) error { + ctx := context.Background() + id, err := fs.CreateEncryptionKey(ctx, "", "apple") + if err != nil { + return err + } + + for i := 0; i < 3; i++ { + if _, err := fs.CreateKeyVersion(ctx, id); err != nil { + return err + } + } + return nil + }, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + + fs, err := NewFilesystem(ctx, dir) + if err != nil { + t.Fatal(err) + } + + if tc.setup != nil { + if err := tc.setup(fs); err != nil { + t.Fatal(err) + } + } + + ciphertext, err := fs.Encrypt(ctx, tc.keyID, tc.plaintext, tc.aad) + if err != nil { + if tc.err == "" { + t.Fatal(err) + } + + if !strings.Contains(err.Error(), tc.err) { + t.Fatalf("expected %#v to contain %#v", err.Error(), tc.err) + } + } + + if len(ciphertext) > 0 { + // Create another key version - this will ensure our ciphertext -> key + // version mapping works. + for i := 0; i < 3; i++ { + if _, err := fs.CreateKeyVersion(ctx, tc.keyID); err != nil { + t.Fatal(err) + } + } + + plaintext, err := fs.Decrypt(ctx, tc.keyID, ciphertext, tc.aad) + if err != nil { + t.Fatal(err) + } + + if got, want := plaintext, tc.plaintext; !bytes.Equal(got, want) { + t.Errorf("expected %#v to be %#v", got, want) + } + } + }) + } +} + +func TestFilesystem_SigningKeyVersions(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + keyID string + setup func(*Filesystem) error + exp int + err string + }{ + { + name: "error_key_not_exist", + keyID: "totally_not_valid", + err: "failed to open metadata", + }, + { + name: "happy", + keyID: "apple", + setup: func(fs *Filesystem) error { + ctx := context.Background() + id, err := fs.CreateSigningKey(ctx, "", "apple") + if err != nil { + return err + } + if _, err := fs.CreateKeyVersion(ctx, id); err != nil { + return err + } + return nil + }, + exp: 1, + }, + { + name: "multi", + keyID: "apple", + setup: func(fs *Filesystem) error { + ctx := context.Background() + id, err := fs.CreateSigningKey(ctx, "", "apple") + if err != nil { + return err + } + + for i := 0; i < 3; i++ { + if _, err := fs.CreateKeyVersion(ctx, id); err != nil { + return err + } + } + return nil + }, + exp: 3, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + + fs, err := NewFilesystem(ctx, dir) + if err != nil { + t.Fatal(err) + } + + if tc.setup != nil { + if err := tc.setup(fs); err != nil { + t.Fatal(err) + } + } + + versions, err := fs.SigningKeyVersions(ctx, tc.keyID) + if err != nil { + if tc.err == "" { + t.Fatal(err) + } + + if !strings.Contains(err.Error(), tc.err) { + t.Fatalf("expected %#v to contain %#v", err.Error(), tc.err) + } + } + + if got, want := len(versions), tc.exp; got != want { + t.Errorf("expected %d version to be %d", got, want) + } + }) + } +} diff --git a/pkg/keys/google_cloud_kms.go b/pkg/keys/google_cloud_kms.go index cfb159047..e0e33b638 100644 --- a/pkg/keys/google_cloud_kms.go +++ b/pkg/keys/google_cloud_kms.go @@ -46,18 +46,9 @@ type CloudKMSSigningKeyVersion struct { keyManager *GoogleCloudKMS } -func (k *CloudKMSSigningKeyVersion) KeyID() string { - return k.keyID -} - -func (k *CloudKMSSigningKeyVersion) CreatedAt() time.Time { - return k.createdAt -} - -func (k *CloudKMSSigningKeyVersion) DestroyedAt() time.Time { - return k.destroyedAt -} - +func (k *CloudKMSSigningKeyVersion) KeyID() string { return k.keyID } +func (k *CloudKMSSigningKeyVersion) CreatedAt() time.Time { return k.createdAt } +func (k *CloudKMSSigningKeyVersion) DestroyedAt() time.Time { return k.destroyedAt } func (k *CloudKMSSigningKeyVersion) Signer(ctx context.Context) (crypto.Signer, error) { return k.keyManager.NewSigner(ctx, k.keyID) } diff --git a/pkg/keys/hashicorp_vault.go b/pkg/keys/hashicorp_vault.go index 117518cf9..9bfa74f71 100644 --- a/pkg/keys/hashicorp_vault.go +++ b/pkg/keys/hashicorp_vault.go @@ -92,18 +92,9 @@ type vaultKeyVersion struct { publicKey crypto.PublicKey } -func (v *vaultKeyVersion) KeyID() string { - return fmt.Sprintf("%s/%d", v.name, v.version) -} - -func (v *vaultKeyVersion) CreatedAt() time.Time { - return v.createdAt.UTC() -} - -func (v *vaultKeyVersion) DestroyedAt() time.Time { - return time.Time{} -} - +func (v *vaultKeyVersion) KeyID() string { return fmt.Sprintf("%s/%d", v.name, v.version) } +func (v *vaultKeyVersion) CreatedAt() time.Time { return v.createdAt.UTC() } +func (v *vaultKeyVersion) DestroyedAt() time.Time { return time.Time{} } func (v *vaultKeyVersion) Signer(ctx context.Context) (crypto.Signer, error) { return &HashiCorpVaultSigner{ client: v.client, diff --git a/pkg/keys/in_memory.go b/pkg/keys/in_memory.go deleted file mode 100644 index 73632c710..000000000 --- a/pkg/keys/in_memory.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package keys - -import ( - "context" - "crypto" - "crypto/aes" - "crypto/cipher" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "fmt" - "io" - "sync" -) - -var _ KeyManager = (*InMemory)(nil) -var _ SigningKeyAdder = (*InMemory)(nil) -var _ EncryptionKeyAdder = (*InMemory)(nil) - -// InMemory is useful for testing. Do NOT use in a running system as all -// keys are only kept in memory and will be lost across server reboots. -type InMemory struct { - mu sync.RWMutex - signingKeys map[string]*ecdsa.PrivateKey - cryptoKeys map[string][]byte -} - -// NewInMemory creates a new, local, in memory KeyManager. -func NewInMemory(ctx context.Context) (*InMemory, error) { - return &InMemory{ - signingKeys: make(map[string]*ecdsa.PrivateKey), - cryptoKeys: make(map[string][]byte), - }, nil -} - -// CreateSigningKey generates a new ECDSA P256 Signing Key identified by -// the provided keyID -func (k *InMemory) CreateSigningKey(ctx context.Context, parent, name string) (string, error) { - k.mu.Lock() - defer k.mu.Unlock() - - keyID := fmt.Sprintf("%s/%s", parent, name) - if _, ok := k.signingKeys[keyID]; ok { - return "", fmt.Errorf("key already exists: %v", keyID) - } - - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return "", fmt.Errorf("unable to generate private key: %w", err) - } - - k.signingKeys[keyID] = pk - return keyID, nil -} - -// AddSigningKey adds a new ECDSA P256 Signing Key identified by the provided -// keyID. -func (k *InMemory) AddSigningKey(keyID string, pk *ecdsa.PrivateKey) error { - k.mu.Lock() - defer k.mu.Unlock() - - if _, ok := k.signingKeys[keyID]; ok { - return fmt.Errorf("key already exists: %v", keyID) - } - - k.signingKeys[keyID] = pk - return nil -} - -// CreateEncryptionKey generates and stores new encryption key identified by the -// provided keyID. -func (k *InMemory) CreateEncryptionKey(keyID string) ([]byte, error) { - k.mu.Lock() - defer k.mu.Unlock() - - if _, ok := k.cryptoKeys[keyID]; ok { - return nil, fmt.Errorf("key already exists: %v", keyID) - } - - key := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, key); err != nil { - return nil, fmt.Errorf("failed to read random bytes: %w", err) - } - - k.cryptoKeys[keyID] = key - return key, nil -} - -// AddEncryptionKey stores the key on the system. -func (k *InMemory) AddEncryptionKey(keyID string, key []byte) error { - k.mu.Lock() - defer k.mu.Unlock() - - if _, ok := k.cryptoKeys[keyID]; ok { - return fmt.Errorf("key already exists: %v", keyID) - } - - k.cryptoKeys[keyID] = key - return nil -} - -func (k *InMemory) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) { - var pk *ecdsa.PrivateKey - { - k.mu.RLock() - defer k.mu.RUnlock() - - if k, ok := k.signingKeys[keyID]; !ok { - return nil, fmt.Errorf("key not found") - } else { - pk = k - } - } - - return pk, nil -} - -func (k *InMemory) getDEK(keyID string) ([]byte, error) { - k.mu.RLock() - defer k.mu.RUnlock() - - if dek, ok := k.cryptoKeys[keyID]; ok { - return dek, nil - } - return nil, fmt.Errorf("key not found") -} - -func (k *InMemory) Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error) { - dek, err := k.getDEK(keyID) - if err != nil { - return nil, err - } - - block, err := aes.NewCipher(dek) - if err != nil { - return nil, fmt.Errorf("bad cipher block: %w", err) - } - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("failed to wrap cipher block: %w", err) - } - nonce := make([]byte, aesgcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, fmt.Errorf("failed to generate nonce: %w", err) - } - ciphertext := aesgcm.Seal(nonce, nonce, plaintext, aad) - - return ciphertext, nil -} - -func (k *InMemory) Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error) { - dek, err := k.getDEK(keyID) - if err != nil { - return nil, err - } - - block, err := aes.NewCipher(dek) - if err != nil { - return nil, fmt.Errorf("failed to create cipher from dek: %w", err) - } - - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("failed to create gcm from dek: %w", err) - } - - size := aesgcm.NonceSize() - if len(ciphertext) < size { - return nil, fmt.Errorf("malformed ciphertext") - } - nonce, ciphertextPortion := ciphertext[:size], ciphertext[size:] - - plaintext, err := aesgcm.Open(nil, nonce, ciphertextPortion, aad) - if err != nil { - return nil, fmt.Errorf("failed to decrypt ciphertext with dek: %w", err) - } - - return plaintext, nil -} diff --git a/pkg/keys/keys.go b/pkg/keys/keys.go index db44c4696..f9b424307 100644 --- a/pkg/keys/keys.go +++ b/pkg/keys/keys.go @@ -23,7 +23,6 @@ package keys import ( "context" "crypto" - "crypto/ecdsa" "crypto/x509" "encoding/pem" "fmt" @@ -59,11 +58,6 @@ type KeyVersionCreator interface { CreateKeyVersion(ctx context.Context, parent string) (string, error) } -// EncryptionKeyAdder supports creating encryption keys. -type EncryptionKeyAdder interface { - AddEncryptionKey(string, []byte) error -} - // KeyVersionDestroyer supports destroying a key version. type KeyVersionDestroyer interface { // DestroyKeyVersion destroys the given key version, if it exists. If the @@ -71,11 +65,6 @@ type KeyVersionDestroyer interface { DestroyKeyVersion(ctx context.Context, id string) error } -// SigningKeyAdder supports creating signing keys. -type SigningKeyAdder interface { - AddSigningKey(string, *ecdsa.PrivateKey) error -} - // SigningKeyVersion represents the necessary details that this application needs // to manage signing keys in an external KMS. type SigningKeyVersion interface { @@ -100,6 +89,17 @@ type SigningKeyManager interface { KeyVersionDestroyer } +// EncryptionKeyManager supports extended management of encryption keys, +// versions, and rotation. +type EncryptionKeyManager interface { + // CreateEncryptionKey creates a new encryption key in the given parent, + // returning the id. If the key already exists, it returns the key's id. + CreateEncryptionKey(ctx context.Context, parent, name string) (string, error) + + KeyVersionCreator + KeyVersionDestroyer +} + // KeyManagerFor returns the appropriate key manager for the given type. func KeyManagerFor(ctx context.Context, config *Config) (KeyManager, error) { typ := config.KeyManagerType @@ -112,8 +112,8 @@ func KeyManagerFor(ctx context.Context, config *Config) (KeyManager, error) { return NewGoogleCloudKMS(ctx, config) case KeyManagerTypeHashiCorpVault: return NewHashiCorpVault(ctx) - case KeyManagerTypeInMemory: - return NewInMemory(ctx) + case KeyManagerTypeFilesystem: + return NewFilesystem(ctx, config.FilesystemRoot) } return nil, fmt.Errorf("unknown key manager type: %v", typ) diff --git a/pkg/keys/testing.go b/pkg/keys/testing.go new file mode 100644 index 000000000..b5b027eb1 --- /dev/null +++ b/pkg/keys/testing.go @@ -0,0 +1,92 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package keys + +import ( + "context" + "io/ioutil" + "os" + "testing" +) + +// TestKeyManager creates a new key manager suitable for use in tests. +func TestKeyManager(tb testing.TB) KeyManager { + tb.Helper() + + ctx := context.Background() + + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + tb.Fatal(err) + } + tb.Cleanup(func() { + if err := os.RemoveAll(tmpdir); err != nil { + tb.Fatal(err) + } + }) + + kms, err := NewFilesystem(ctx, tmpdir) + if err != nil { + tb.Fatal(err) + } + + return kms +} + +// TestEncryptionKey creates a new encryption key and key version in the given +// key manager. If the provided key manager does not support managing encryption +// keys, it calls t.Fatal. +func TestEncryptionKey(tb testing.TB, kms KeyManager) string { + tb.Helper() + + typ, ok := kms.(EncryptionKeyManager) + if !ok { + tb.Fatal("kms cannot manage encryption keys") + } + + ctx := context.Background() + parent, err := typ.CreateEncryptionKey(ctx, "parent", "key") + if err != nil { + tb.Fatal(err) + } + if _, err := typ.CreateKeyVersion(ctx, parent); err != nil { + tb.Fatal(err) + } + + return parent +} + +// TestSigningKey creates a new signing key and key version in the given key +// manager. If the provided key manager does not support managing signing keys, +// it calls t.Fatal. +func TestSigningKey(tb testing.TB, kms KeyManager) string { + tb.Helper() + + typ, ok := kms.(SigningKeyManager) + if !ok { + tb.Fatal("kms cannot manage signing keys") + } + + ctx := context.Background() + parent, err := typ.CreateSigningKey(ctx, "parent", "key") + if err != nil { + tb.Fatal(err) + } + if _, err := typ.CreateKeyVersion(ctx, parent); err != nil { + tb.Fatal(err) + } + + return parent +}