From f37fe502ce80968f58ea9453c270f6d8b0206837 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Mon, 8 May 2023 16:25:29 +0200 Subject: [PATCH] remove package `kms` and `keserv` This commit removes the two packages `kms` and `keserv`. Both packages are no longer needed since better implementations are available via the `kv` and `edge` package. This cleans up a some code and removes two layers of indirection. Signed-off-by: Andreas Auernhammer --- cmd/kes/migrate.go | 29 +- edge/server-config.go | 96 +-- internal/api/key.go | 8 +- internal/api/status.go | 4 +- internal/keystore/aws/secrets-manager.go | 102 +-- internal/keystore/azure/key-vault.go | 140 ++-- internal/keystore/fortanix/keystore.go | 153 ++-- internal/keystore/fs/fs.go | 105 +-- internal/keystore/gcp/iterator.go | 21 +- internal/keystore/gcp/secret-manager.go | 59 +- internal/keystore/gemalto/key-secure.go | 134 ++-- internal/keystore/generic/client.go | 413 ---------- internal/keystore/generic/server.go | 203 ----- internal/keystore/generic/spec-v1.md | 207 ----- internal/keystore/kes/kes.go | 70 +- internal/keystore/mem/mem.go | 4 +- internal/keystore/vault/iterator.go | 14 +- internal/keystore/vault/vault.go | 47 +- internal/sys/enclave.go | 6 +- internal/sys/fs.go | 4 +- internal/sys/key-fs.go | 26 +- keserv/config.go | 936 ----------------------- keserv/config_test.go | 196 ----- keserv/env.go | 81 -- keserv/example_test.go | 29 - keserv/testdata/fs.yml | 27 - keserv/testdata/invalid_keys.yml | 27 - keserv/testdata/invalid_root.yml | 26 - keserv/testdata/invalid_version.yml | 28 - keserv/testdata/with_tls_ca.yml | 28 - keserv/testdata/with_version.yml | 28 - keserv/yml.go | 340 -------- kms/example_test.go | 57 -- kms/kms.go | 197 ----- kv/example_test.go | 53 ++ kv/iter.go | 39 + kv/store.go | 28 - 37 files changed, 586 insertions(+), 3379 deletions(-) delete mode 100644 internal/keystore/generic/client.go delete mode 100644 internal/keystore/generic/server.go delete mode 100644 internal/keystore/generic/spec-v1.md delete mode 100644 keserv/config.go delete mode 100644 keserv/config_test.go delete mode 100644 keserv/env.go delete mode 100644 keserv/example_test.go delete mode 100644 keserv/testdata/fs.yml delete mode 100644 keserv/testdata/invalid_keys.yml delete mode 100644 keserv/testdata/invalid_root.yml delete mode 100644 keserv/testdata/invalid_version.yml delete mode 100644 keserv/testdata/with_tls_ca.yml delete mode 100644 keserv/testdata/with_version.yml delete mode 100644 keserv/yml.go delete mode 100644 kms/example_test.go delete mode 100644 kms/kms.go create mode 100644 kv/example_test.go create mode 100644 kv/iter.go diff --git a/cmd/kes/migrate.go b/cmd/kes/migrate.go index e604b488..71e10a1a 100644 --- a/cmd/kes/migrate.go +++ b/cmd/kes/migrate.go @@ -16,8 +16,8 @@ import ( "github.com/fatih/color" "github.com/minio/kes-go" + "github.com/minio/kes/edge" "github.com/minio/kes/internal/cli" - "github.com/minio/kes/keserv" flag "github.com/spf13/pflag" "golang.org/x/term" ) @@ -86,21 +86,32 @@ func migrateCmd(args []string) { ctx, cancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) defer cancel() - sourceConfig, err := keserv.ReadServerConfig(fromPath) + file, err := os.Open(fromPath) if err != nil { cli.Fatalf("failed to read '--from' config file: %v", err) } + sourceConfig, err := edge.ReadServerConfigYAML(file) + if err != nil { + cli.Fatalf("failed to read '--from' config file: %v", err) + } + file.Close() - targetConfig, err := keserv.ReadServerConfig(toPath) + file, err = os.Open(toPath) if err != nil { cli.Fatalf("failed to read '--to' config file: %v", err) } - src, err := sourceConfig.KMS.Connect(ctx) + targetConfig, err := edge.ReadServerConfigYAML(file) + if err != nil { + cli.Fatalf("failed to read '--to' config file: %v", err) + } + file.Close() + + src, err := sourceConfig.KeyStore.Connect(ctx) if err != nil { cli.Fatal(err) } - dst, err := targetConfig.KMS.Connect(ctx) + dst, err := targetConfig.KeyStore.Connect(ctx) if err != nil { cli.Fatal(err) } @@ -133,11 +144,15 @@ func migrateCmd(args []string) { }() // Finally, we start the actual migration. - for iterator.Next() { - name := iterator.Name() + for { + name, ok := iterator.Next() + if !ok { + break + } if ok, _ := filepath.Match(pattern, name); !ok { continue } + fmt.Println(name) key, err := src.Get(ctx, name) if err != nil { diff --git a/edge/server-config.go b/edge/server-config.go index e5329ebb..021adbc5 100644 --- a/edge/server-config.go +++ b/edge/server-config.go @@ -18,7 +18,6 @@ import ( "github.com/minio/kes/internal/keystore/gemalto" kesstore "github.com/minio/kes/internal/keystore/kes" "github.com/minio/kes/internal/keystore/vault" - "github.com/minio/kes/kms" "github.com/minio/kes/kv" ) @@ -242,7 +241,7 @@ type FSKeyStore struct { // Connect returns a kv.Store that stores key-value pairs in a path on the filesystem. func (s *FSKeyStore) Connect(context.Context) (kv.Store[string, []byte], error) { - return wrap(fs.NewConn(s.Path)) + return fs.NewStore(s.Path) } // KESKeyStore is a structure containing the configuration @@ -278,13 +277,13 @@ type KESKeyStore struct { // Connect returns a kv.Store that stores key-value pairs on a KES server. func (s *KESKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { - return wrap(kesstore.Connect(ctx, &kesstore.Config{ + return kesstore.Connect(ctx, &kesstore.Config{ Endpoints: s.Endpoints, Enclave: s.Enclave, Certificate: s.CertificateFile, PrivateKey: s.PrivateKeyFile, CAPath: s.CAPath, - })) + }) } // VaultKeyStore is a structure containing the configuration @@ -417,7 +416,7 @@ func (s *VaultKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], JWT: s.Kubernetes.JWT, } } - return wrap(vault.Connect(ctx, c)) + return vault.Connect(ctx, c) } // FortanixKeyStore is a structure containing the @@ -446,12 +445,12 @@ type FortanixKeyStore struct { // Connect returns a kv.Store that stores key-value pairs on a Fortanix SDKMS server. func (s *FortanixKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { - return wrap(fortanix.Connect(ctx, &fortanix.Config{ + return fortanix.Connect(ctx, &fortanix.Config{ Endpoint: s.Endpoint, GroupID: s.GroupID, APIKey: fortanix.APIKey(s.APIKey), CAPath: s.CAPath, - })) + }) } // KeySecureKeyStore is a structure containing the @@ -483,14 +482,14 @@ type KeySecureKeyStore struct { // Connect returns a kv.Store that stores key-value pairs on a Gemalto KeySecure instance. func (s *KeySecureKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { - return wrap(gemalto.Connect(ctx, &gemalto.Config{ + return gemalto.Connect(ctx, &gemalto.Config{ Endpoint: s.Endpoint, CAPath: s.CAPath, Login: gemalto.Credentials{ Token: s.Token, Domain: s.Domain, }, - })) + }) } // GCPSecretManagerKeyStore is a structure containing the @@ -534,7 +533,7 @@ type GCPSecretManagerKeyStore struct { // Connect returns a kv.Store that stores key-value pairs on GCP SecretManager. func (s *GCPSecretManagerKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { - return wrap(gcp.Connect(ctx, &gcp.Config{ + return gcp.Connect(ctx, &gcp.Config{ Endpoint: s.Endpoint, ProjectID: s.ProjectID, Scopes: s.Scopes, @@ -544,7 +543,7 @@ func (s *GCPSecretManagerKeyStore) Connect(ctx context.Context) (kv.Store[string KeyID: s.KeyID, Key: s.Key, }, - })) + }) } // AWSSecretsManagerKeyStore is a structure containing the @@ -580,7 +579,7 @@ type AWSSecretsManagerKeyStore struct { // Connect returns a kv.Store that stores key-value pairs on AWS SecretsManager. func (s *AWSSecretsManagerKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { - return wrap(aws.Connect(ctx, &aws.Config{ + return aws.Connect(ctx, &aws.Config{ Addr: s.Endpoint, Region: s.Region, KMSKeyID: s.KMSKey, @@ -589,7 +588,7 @@ func (s *AWSSecretsManagerKeyStore) Connect(ctx context.Context) (kv.Store[strin SecretKey: s.SecretKey, SessionToken: s.SessionToken, }, - })) + }) } // AzureKeyVaultKeyStore is a structure containing the @@ -628,80 +627,13 @@ func (s *AzureKeyVaultKeyStore) Connect(ctx context.Context) (kv.Store[string, [ ClientID: s.ClientID, Secret: s.ClientSecret, } - return wrap(azure.ConnectWithCredentials(ctx, s.Endpoint, creds)) + return azure.ConnectWithCredentials(ctx, s.Endpoint, creds) case s.ManagedIdentityClientID != "": creds := azure.ManagedIdentity{ ClientID: s.ManagedIdentityClientID, } - return wrap(azure.ConnectWithIdentity(ctx, s.Endpoint, creds)) + return azure.ConnectWithIdentity(ctx, s.Endpoint, creds) default: return nil, errors.New("edge: failed to connect to Azure KeyVault: no authentication method specified") } } - -func wrap(conn kms.Conn, err error) (kv.Store[string, []byte], error) { - if err != nil { - return nil, err - } - return &store{conn: conn}, nil -} - -type store struct { - conn kms.Conn -} - -var _ kv.Store[string, []byte] = (*store)(nil) // compiler check - -func (s *store) Status(ctx context.Context) (kv.State, error) { - state, err := s.conn.Status(ctx) - if err == nil { - return kv.State(state), nil - } - - if uErr, ok := kms.IsUnreachable(err); ok { - return kv.State{}, &kv.Unreachable{Err: uErr.Err} - } - if uErr, ok := kms.IsUnavailable(err); ok { - return kv.State{}, &kv.Unreachable{Err: uErr.Err} - } - return kv.State{}, err -} - -func (s *store) Create(ctx context.Context, name string, value []byte) error { - return s.conn.Create(ctx, name, value) -} - -func (s *store) Set(ctx context.Context, name string, value []byte) error { - return s.conn.Create(ctx, name, value) -} - -func (s *store) Get(ctx context.Context, name string) ([]byte, error) { - return s.conn.Get(ctx, name) -} - -func (s *store) Delete(ctx context.Context, name string) error { - return s.conn.Delete(ctx, name) -} - -func (s *store) List(ctx context.Context) (kv.Iter[string], error) { - i, err := s.conn.List(ctx) - if err != nil { - return nil, err - } - return &iter{iter: i}, nil -} - -type iter struct { - iter kms.Iter -} - -var _ kv.Iter[string] = (*iter)(nil) // compiler check - -func (i *iter) Next() (string, bool) { - if next := i.iter.Next(); next { - return i.iter.Name(), next - } - return "", false -} - -func (i *iter) Close() error { return i.iter.Close() } diff --git a/internal/api/key.go b/internal/api/key.go index e013e776..d2e92f3f 100644 --- a/internal/api/key.go +++ b/internal/api/key.go @@ -992,11 +992,11 @@ func listKey(config *RouterConfig) API { var hasWritten bool encoder := json.NewEncoder(w) - for iterator.Next() { - if ok, _ := path.Match(pattern, iterator.Name()); !ok || iterator.Name() == "" { + for name, next := iterator.Next(); next; name, next = iterator.Next() { + if ok, _ := path.Match(pattern, name); !ok || name == "" { continue } - key, err := enclave.GetKey(r.Context(), iterator.Name()) + key, err := enclave.GetKey(r.Context(), name) if err != nil { return hasWritten, err } @@ -1007,7 +1007,7 @@ func listKey(config *RouterConfig) API { } err = encoder.Encode(Response{ - Name: iterator.Name(), + Name: name, ID: key.ID(), Algorithm: key.Algorithm(), CreatedAt: key.CreatedAt(), diff --git a/internal/api/status.go b/internal/api/status.go index ed208f9e..3020df8a 100644 --- a/internal/api/status.go +++ b/internal/api/status.go @@ -13,7 +13,7 @@ import ( "github.com/minio/kes/internal/audit" "github.com/minio/kes/internal/auth" "github.com/minio/kes/internal/sys" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) func status(config *RouterConfig) API { @@ -193,7 +193,7 @@ func edgeStatus(config *EdgeRouterConfig) API { state, err := config.Keys.Status(r.Context()) if err != nil { response.KeyStoreUnavailable = true - _, response.KeyStoreUnreachable = kms.IsUnreachable(err) + _, response.KeyStoreUnreachable = kv.IsUnreachable(err) } else { latency := state.Latency.Round(time.Millisecond) if latency == 0 { // Make sure we actually send a latency even if the key store respond time is < 1ms. diff --git a/internal/keystore/aws/secrets-manager.go b/internal/keystore/aws/secrets-manager.go index ad965e1c..da89d9f0 100644 --- a/internal/keystore/aws/secrets-manager.go +++ b/internal/keystore/aws/secrets-manager.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "net/http" - "sync" "time" "github.com/aws/aws-sdk-go/aws" @@ -18,7 +17,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/minio/kes-go" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // Credentials represents static AWS credentials: @@ -54,7 +53,7 @@ type Config struct { // Connect establishes and returns a Conn to a AWS SecretManager // using the given config. -func Connect(ctx context.Context, config *Config) (*Conn, error) { +func Connect(ctx context.Context, config *Config) (*Store, error) { credentials := credentials.NewStaticCredentials( config.Login.AccessKey, config.Login.SecretKey, @@ -85,7 +84,7 @@ func Connect(ctx context.Context, config *Config) (*Conn, error) { return nil, err } - c := &Conn{ + c := &Store{ config: *config, client: secretsmanager.New(session), } @@ -95,27 +94,27 @@ func Connect(ctx context.Context, config *Config) (*Conn, error) { return c, nil } -// Conn is a connection to an AWS SecretsManager. -type Conn struct { +// Store is an AWS SecretsManager secret store. +type Store struct { config Config client *secretsmanager.SecretsManager } -var _ kms.Conn = (*Conn)(nil) +var _ kv.Store[string, []byte] = (*Store)(nil) // Status returns the current state of the AWS SecretsManager instance. // In particular, whether it is reachable and the network latency. -func (c *Conn) Status(ctx context.Context) (kms.State, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.client.Endpoint, nil) +func (s *Store) Status(ctx context.Context) (kv.State, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.client.Endpoint, nil) if err != nil { - return kms.State{}, err + return kv.State{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kms.State{}, &kms.Unreachable{Err: err} + return kv.State{}, &kv.Unreachable{Err: err} } - return kms.State{ + return kv.State{ Latency: time.Since(start), }, nil } @@ -127,15 +126,15 @@ func (c *Conn) Status(ctx context.Context) (kms.State, error) { // If the SecretsManager.KMSKeyID is set AWS will use this key ID to // encrypt the values. Otherwise, AWS will use the default key ID for // encrypting secrets at the AWS SecretsManager. -func (c *Conn) Create(ctx context.Context, name string, value []byte) error { +func (s *Store) Create(ctx context.Context, name string, value []byte) error { createOpt := secretsmanager.CreateSecretInput{ Name: aws.String(name), SecretString: aws.String(string(value)), } - if c.config.KMSKeyID != "" { - createOpt.KmsKeyId = aws.String(c.config.KMSKeyID) + if s.config.KMSKeyID != "" { + createOpt.KmsKeyId = aws.String(s.config.KMSKeyID) } - if _, err := c.client.CreateSecretWithContext(ctx, &createOpt); err != nil { + if _, err := s.client.CreateSecretWithContext(ctx, &createOpt); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -150,10 +149,21 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return nil } +// Set stores the given key-value pair at the AWS SecretsManager +// if and only if it doesn't exists. If such an entry already exists +// it returns kes.ErrKeyExists. +// +// If the SecretsManager.KMSKeyID is set AWS will use this key ID to +// encrypt the values. Otherwise, AWS will use the default key ID for +// encrypting secrets at the AWS SecretsManager. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Get returns the value associated with the given key. // If no entry for key exists, it returns kes.ErrKeyNotFound. -func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { - response, err := c.client.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{ +func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { + response, err := s.client.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{ SecretId: aws.String(name), }) if err != nil { @@ -189,8 +199,8 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { // Delete removes the key-value pair from the AWS SecretsManager, if // it exists. -func (c *Conn) Delete(ctx context.Context, name string) error { - _, err := c.client.DeleteSecretWithContext(ctx, &secretsmanager.DeleteSecretInput{ +func (s *Store) Delete(ctx context.Context, name string) error { + _, err := s.client.DeleteSecretWithContext(ctx, &secretsmanager.DeleteSecretInput{ SecretId: aws.String(name), ForceDeleteWithoutRecovery: aws.Bool(true), }) @@ -210,14 +220,14 @@ func (c *Conn) Delete(ctx context.Context, name string) error { // List returns a new Iterator over the names of // all stored keys. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { + var cancel context.CancelCauseFunc + ctx, cancel = context.WithCancelCause(ctx) values := make(chan string, 10) - iterator := &iterator{ - values: values, - } + go func() { defer close(values) - err := c.client.ListSecretsPagesWithContext(ctx, &secretsmanager.ListSecretsInput{}, func(page *secretsmanager.ListSecretsOutput, lastPage bool) bool { + err := s.client.ListSecretsPagesWithContext(ctx, &secretsmanager.ListSecretsInput{}, func(page *secretsmanager.ListSecretsOutput, lastPage bool) bool { for _, secret := range page.SecretList { values <- *secret.Name } @@ -228,39 +238,29 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { return !lastPage }) if err != nil { - iterator.SetErr(fmt.Errorf("aws: failed to list keys: %v", err)) + cancel(err) } }() - return iterator, nil + return &iter{ + ch: values, + ctx: ctx, + }, nil } -type iterator struct { - values <-chan string - last string - - lock sync.Mutex - err error +type iter struct { + ch <-chan string + ctx context.Context } -func (i *iterator) Next() bool { - v, ok := <-i.values - if !ok { - return false +func (i *iter) Next() (string, bool) { + select { + case v, ok := <-i.ch: + return v, ok + case <-i.ctx.Done(): + return "", false } - i.last = v - return true -} - -func (i *iterator) Name() string { return i.last } - -func (i *iterator) Close() error { - i.lock.Lock() - defer i.lock.Unlock() - return i.err } -func (i *iterator) SetErr(err error) { - i.lock.Lock() - i.err = err - i.lock.Unlock() +func (i *iter) Close() error { + return context.Cause(i.ctx) } diff --git a/internal/keystore/azure/key-vault.go b/internal/keystore/azure/key-vault.go index a9840384..9eafa112 100644 --- a/internal/keystore/azure/key-vault.go +++ b/internal/keystore/azure/key-vault.go @@ -10,13 +10,12 @@ import ( "fmt" "math/rand" "net/http" - "sync" "time" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/minio/kes-go" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // Credentials are Azure client credentials to authenticate an application @@ -36,27 +35,27 @@ type ManagedIdentity struct { ClientID string // The Azure managed identity client ID } -// Conn is a connection to a Azure KeyVault. -type Conn struct { +// Store is an Azure KeyVault secret store. +type Store struct { endpoint string client client } -var _ kms.Conn = (*Conn)(nil) +var _ kv.Store[string, []byte] = (*Store)(nil) // Status returns the current state of the Azure KeyVault instance. // In particular, whether it is reachable and the network latency. -func (c *Conn) Status(ctx context.Context) (kms.State, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.client.Endpoint, nil) +func (s *Store) Status(ctx context.Context) (kv.State, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.client.Endpoint, nil) if err != nil { - return kms.State{}, err + return kv.State{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kms.State{}, &kms.Unreachable{Err: err} + return kv.State{}, &kv.Unreachable{Err: err} } - return kms.State{ + return kv.State{ Latency: time.Since(start), }, nil } @@ -80,8 +79,8 @@ func (c *Conn) Status(ctx context.Context) (kms.State, error) { // purging but will eventually give up and fail. However, // a subsequent create may succeed once KeyVault has purged // the secret completely. -func (c *Conn) Create(ctx context.Context, name string, value []byte) error { - _, stat, err := c.client.GetSecret(ctx, name, "") +func (s *Store) Create(ctx context.Context, name string, value []byte) error { + _, stat, err := s.client.GetSecret(ctx, name, "") if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -97,7 +96,7 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return fmt.Errorf("azure: failed to create '%s': failed to check whether '%s' already exists: %s (%s)", name, name, stat.Message, stat.ErrorCode) } - stat, err = c.client.CreateSecret(ctx, name, string(value)) + stat, err = s.client.CreateSecret(ctx, name, string(value)) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -105,7 +104,7 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return fmt.Errorf("azure: failed to create '%s': %v", name, err) } if stat.StatusCode == http.StatusConflict && stat.ErrorCode == "ObjectIsDeletedButRecoverable" { - stat, err = c.client.PurgeSecret(ctx, name) + stat, err = s.client.PurgeSecret(ctx, name) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -122,7 +121,7 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { Jitter = 800 * time.Millisecond ) for i := 0; i < Retry; i++ { - stat, err = c.client.CreateSecret(ctx, name, string(value)) + stat, err = s.client.CreateSecret(ctx, name, string(value)) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -148,6 +147,29 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { } } +// Set creates the given key-value pair as KeyVault secret. +// +// Since KeyVault does not support an atomic create resp. +// create-only-if-not-exists, Set cannot exclude data +// race situations when multiple clients try to create +// the same secret at the same time. +// +// However, Set checks whether a secret with the given +// name exists, and if it does, returns kes.ErrKeyExists. +// +// Further, a secret may not exist but may be in a soft delete +// state. In this case, Set tries to purge the deleted +// secret and then tries to create it. However, KeyVault +// purges deleted secrets in the background such that +// an incoming create fails with HTTP 409 Conflict. Therefore, +// Set tries to create the secret multiple times after +// purging but will eventually give up and fail. However, +// a subsequent create may succeed once KeyVault has purged +// the secret completely. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Delete deletes and purges the secret from KeyVault. // // A full delete is a two-step process. So, Delete first @@ -164,7 +186,7 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { // // Since KeyVault only supports two-steps deletes, KES cannot // guarantee that a Delete operation has atomic semantics. -func (c *Conn) Delete(ctx context.Context, name string) error { +func (s *Store) Delete(ctx context.Context, name string) error { // Deleting a key from KeyVault is a two-step // process. First, the key has to be deleted // (soft delete) and then purged. It is not @@ -183,7 +205,7 @@ func (c *Conn) Delete(ctx context.Context, name string) error { // key - hoping that KeyVault finishes the // internal soft-delete process. - stat, err := c.client.DeleteSecret(ctx, name) + stat, err := s.client.DeleteSecret(ctx, name) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -211,7 +233,7 @@ func (c *Conn) Delete(ctx context.Context, name string) error { Jitter = 800 * time.Millisecond ) for i := 0; i < Retry; i++ { - stat, err = c.client.PurgeSecret(ctx, name) + stat, err = s.client.PurgeSecret(ctx, name) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -243,8 +265,8 @@ func (c *Conn) Delete(ctx context.Context, name string) error { // Since Get has to fetch and filter the secrets versions first // before actually accessing the secret, Get may return inconsistent // responses when the secret is modified concurrently. -func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { - version, stat, err := c.client.GetFirstVersion(ctx, name) +func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { + version, stat, err := s.client.GetFirstVersion(ctx, name) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return nil, err } @@ -258,7 +280,7 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { return nil, fmt.Errorf("azure: failed to get '%s': failed to list versions: %s (%s)", name, stat.Message, stat.ErrorCode) } - value, stat, err := c.client.GetSecret(ctx, name, version) + value, stat, err := s.client.GetSecret(ctx, name, version) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return nil, err } @@ -273,40 +295,35 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { // List returns a new Iterator over the names of // all stored keys. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { - var ( - values = make(chan string, 10) - iterator = &iterator{ - values: values, - } - ) +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { + var cancel context.CancelCauseFunc + ctx, cancel = context.WithCancelCause(ctx) + values := make(chan string, 10) + go func() { defer close(values) var nextLink string for { - secrets, link, status, err := c.client.ListSecrets(ctx, nextLink) + secrets, link, status, err := s.client.ListSecrets(ctx, nextLink) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - iterator.SetErr(err) + cancel(err) break } if err != nil { - iterator.SetErr(fmt.Errorf("azure: failed to list keys: %v", err)) + cancel(fmt.Errorf("azure: failed to list keys: %v", err)) break } if status.StatusCode != http.StatusOK { - iterator.SetErr(fmt.Errorf("azure: failed to list keys: %s (%s)", status.Message, status.ErrorCode)) + cancel(fmt.Errorf("azure: failed to list keys: %s (%s)", status.Message, status.ErrorCode)) break } + nextLink = link for _, secret := range secrets { select { case values <- secret: case <-ctx.Done(): - if err = ctx.Err(); err == nil { - err = context.Canceled - } - iterator.SetErr(err) return } } @@ -315,12 +332,16 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { } } }() - return iterator, nil + return &iter{ + ch: values, + ctx: ctx, + cancel: cancel, + }, nil } // ConnectWithCredentials tries to establish a connection to a Azure KeyVault // instance using Azure client credentials. -func ConnectWithCredentials(_ context.Context, endpoint string, creds Credentials) (*Conn, error) { +func ConnectWithCredentials(_ context.Context, endpoint string, creds Credentials) (*Store, error) { const Scope = "https://vault.azure.net" c := auth.NewClientCredentialsConfig(creds.ClientID, creds.Secret, creds.TenantID) @@ -329,7 +350,7 @@ func ConnectWithCredentials(_ context.Context, endpoint string, creds Credential if err != nil { return nil, fmt.Errorf("azure: failed to obtain ServicePrincipalToken from client credentials: %v", err) } - return &Conn{ + return &Store{ endpoint: endpoint, client: client{ Endpoint: endpoint, @@ -340,7 +361,7 @@ func ConnectWithCredentials(_ context.Context, endpoint string, creds Credential // ConnectWithIdentity tries to establish a connection to a Azure KeyVault // instance using an Azure managed identity. -func ConnectWithIdentity(_ context.Context, endpoint string, msi ManagedIdentity) (*Conn, error) { +func ConnectWithIdentity(_ context.Context, endpoint string, msi ManagedIdentity) (*Store, error) { const Scope = "https://vault.azure.net" c := auth.NewMSIConfig() @@ -350,7 +371,7 @@ func ConnectWithIdentity(_ context.Context, endpoint string, msi ManagedIdentity if err != nil { return nil, fmt.Errorf("azure: failed to obtain ServicePrincipalToken from managed identity: %v", err) } - return &Conn{ + return &Store{ endpoint: endpoint, client: client{ Endpoint: endpoint, @@ -359,33 +380,22 @@ func ConnectWithIdentity(_ context.Context, endpoint string, msi ManagedIdentity }, nil } -type iterator struct { - values <-chan string - last string - - lock sync.Mutex - err error +type iter struct { + ch <-chan string + ctx context.Context + cancel context.CancelCauseFunc } -func (i *iterator) Next() bool { - v, ok := <-i.values - if !ok { - return false +func (i *iter) Next() (string, bool) { + select { + case v, ok := <-i.ch: + return v, ok + case <-i.ctx.Done(): + return "", false } - i.last = v - return true -} - -func (i *iterator) Name() string { return i.last } - -func (i *iterator) Close() error { - i.lock.Lock() - defer i.lock.Unlock() - return i.err } -func (i *iterator) SetErr(err error) { - i.lock.Lock() - i.err = err - i.lock.Unlock() +func (i *iter) Close() error { + i.cancel(context.Canceled) + return context.Cause(i.ctx) } diff --git a/internal/keystore/fortanix/keystore.go b/internal/keystore/fortanix/keystore.go index ed90a985..a61fca30 100644 --- a/internal/keystore/fortanix/keystore.go +++ b/internal/keystore/fortanix/keystore.go @@ -22,14 +22,13 @@ import ( "path" "path/filepath" "strings" - "sync" "time" "aead.dev/mem" "github.com/minio/kes-go" xhttp "github.com/minio/kes/internal/http" "github.com/minio/kes/internal/key" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // APIKey is a Fortanix API key for authenticating to @@ -67,17 +66,17 @@ type Config struct { CAPath string } -// Conn is a connection to a Fortanix SDKMS server. -type Conn struct { +// Store is a Fortanix SDKMS secret store. +type Store struct { config Config client xhttp.Retry } -var _ kms.Conn = (*Conn)(nil) // compiler check +var _ kv.Store[string, []byte] = (*Store)(nil) // compiler check -// Connect establishes and returns a Conn to a Fortanix SDKMS server +// Connect establishes and returns a Store to a Fortanix SDKMS server // using the given config. -func Connect(ctx context.Context, config *Config) (*Conn, error) { +func Connect(ctx context.Context, config *Config) (*Store, error) { if config.Endpoint == "" { return nil, errors.New("fortanix: endpoint is empty") } @@ -177,7 +176,7 @@ func Connect(ctx context.Context, config *Config) (*Conn, error) { } return nil, fmt.Errorf("fortanix: failed to authenticate to '%s': %s (%d)", config.Endpoint, resp.Status, resp.StatusCode) } - return &Conn{ + return &Store{ config: *config, client: client, }, nil @@ -185,26 +184,26 @@ func Connect(ctx context.Context, config *Config) (*Conn, error) { // Status returns the current state of the Fortanix SDKMS instance. // In particular, whether it is reachable and the network latency. -func (c *Conn) Status(ctx context.Context) (kms.State, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.config.Endpoint, nil) +func (s *Store) Status(ctx context.Context) (kv.State, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.config.Endpoint, nil) if err != nil { - return kms.State{}, err + return kv.State{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kms.State{}, &kms.Unreachable{Err: err} + return kv.State{}, &kv.Unreachable{Err: err} } - return kms.State{ + return kv.State{ Latency: time.Since(start), }, nil } -// Create stors the given key at the Fortanix SDKMS if and only +// Create stores the given key at the Fortanix SDKMS if and only // if no entry with the given name exists. // // If no such entry exists, Create returns kes.ErrKeyExists. -func (c *Conn) Create(ctx context.Context, name string, value []byte) error { +func (s *Store) Create(ctx context.Context, name string, value []byte) error { type Request struct { Type string `json:"obj_type"` Name string `json:"name"` @@ -222,7 +221,7 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { request, err := json.Marshal(Request{ Type: Type, Name: name, - GroupID: c.config.GroupID, + GroupID: s.config.GroupID, Operations: []string{OpExport, OpAppManageable}, Value: base64.StdEncoding.EncodeToString(value), // Fortanix expects base64-encoded values and will not accept raw strings Enabled: true, @@ -231,15 +230,15 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return err } - url := endpoint(c.config.Endpoint, "/crypto/v1/keys") + url := endpoint(s.config.Endpoint, "/crypto/v1/keys") req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, xhttp.RetryReader(bytes.NewReader(request))) if err != nil { return fmt.Errorf("fortanix: failed to create key '%s': %v", name, err) } - req.Header.Set("Authorization", c.config.APIKey.String()) + req.Header.Set("Authorization", s.config.APIKey.String()) req.Header.Set("Content-Type", "application/json") - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err @@ -259,10 +258,18 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return nil } +// Set stores the given key at the Fortanix SDKMS if and only +// if no entry with the given name exists. +// +// If no such entry exists, Create returns kes.ErrKeyExists. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Delete deletes the key associated with the given name // from the Fortanix SDKMS. It may not return an error if no // entry for the given name exists. -func (c *Conn) Delete(ctx context.Context, name string) error { +func (s *Store) Delete(ctx context.Context, name string) error { // In order to detele a key, we need to fetch its key ID first. // Fortanix SDKMS API does not provide a way to delete a key // using just its name. @@ -276,15 +283,15 @@ func (c *Conn) Delete(ctx context.Context, name string) error { return fmt.Errorf("fortanix: failed to delete '%s': %v", name, err) } - url := endpoint(c.config.Endpoint, "/crypto/v1/keys/export") + url := endpoint(s.config.Endpoint, "/crypto/v1/keys/export") req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, xhttp.RetryReader(bytes.NewReader(request))) if err != nil { return fmt.Errorf("fortanix: failed to delete '%s': %v", name, err) } - req.Header.Set("Authorization", c.config.APIKey.String()) + req.Header.Set("Authorization", s.config.APIKey.String()) req.Header.Set("Content-Type", "application/json") - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -311,14 +318,14 @@ func (c *Conn) Delete(ctx context.Context, name string) error { } // Now, we can delete the key using its key ID. - url = endpoint(c.config.Endpoint, "/crypto/v1/keys", response.KeyID) + url = endpoint(s.config.Endpoint, "/crypto/v1/keys", response.KeyID) req, err = http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) if err != nil { return err } - req.Header.Set("Authorization", c.config.APIKey.String()) + req.Header.Set("Authorization", s.config.APIKey.String()) - resp, err = c.client.Do(req) + resp, err = s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -339,7 +346,7 @@ func (c *Conn) Delete(ctx context.Context, name string) error { // Get returns the key associated with the given name. // // If there is no such entry, Get returns kes.ErrKeyNotFound. -func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { +func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { type Request struct { Name string `json:"name"` } @@ -350,15 +357,15 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { return nil, fmt.Errorf("fortanix: failed to fetch %q: %v", name, err) } - url := endpoint(c.config.Endpoint, "/crypto/v1/keys/export") + url := endpoint(s.config.Endpoint, "/crypto/v1/keys/export") req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, xhttp.RetryReader(bytes.NewReader(request))) if err != nil { return nil, fmt.Errorf("fortanix: failed to fetch '%s': %v", name, err) } - req.Header.Set("Authorization", c.config.APIKey.String()) + req.Header.Set("Authorization", s.config.APIKey.String()) req.Header.Set("Content-Type", "application/json") - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return nil, err } @@ -400,37 +407,34 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { // concurrent changes to the Fortanix SDKMS instance - i.e. // creates or deletes. Further, it does not provide any // ordering guarantees. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { - var ( - names = make(chan string, 10) - iter = &iterator{ - values: names, - } - ) +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { + var cancel context.CancelCauseFunc + ctx, cancel = context.WithCancelCause(ctx) + values := make(chan string, 10) go func() { - defer close(names) + defer close(values) var start string for { - reqURL := endpoint(c.config.Endpoint, "/crypto/v1/keys") + "?sort=name:asc&limit=100" + reqURL := endpoint(s.config.Endpoint, "/crypto/v1/keys") + "?sort=name:asc&limit=100" if start != "" { reqURL += "&start=" + start } req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { - iter.SetErr(fmt.Errorf("fortanix: failed to list keys: %v", err)) + cancel(fmt.Errorf("fortanix: failed to list keys: %v", err)) return } - req.Header.Set("Authorization", c.config.APIKey.String()) + req.Header.Set("Authorization", s.config.APIKey.String()) - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - iter.SetErr(err) + cancel(err) return } if err != nil { - iter.SetErr(fmt.Errorf("fortanix: failed to list keys: %v", err)) + cancel(fmt.Errorf("fortanix: failed to list keys: %v", err)) return } @@ -439,7 +443,7 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { } var keys []Response if err := json.NewDecoder(mem.LimitReader(resp.Body, 10*key.MaxSize)).Decode(&keys); err != nil { - iter.SetErr(fmt.Errorf("fortanix: failed to list keys: failed to parse server response: %v", err)) + cancel(fmt.Errorf("fortanix: failed to list keys: failed to parse server response: %v", err)) return } if len(keys) == 0 { @@ -447,27 +451,28 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { } for _, k := range keys { select { - case names <- k.Name: + case values <- k.Name: case <-ctx.Done(): - iter.SetErr(context.Canceled) return } } start = url.QueryEscape(keys[len(keys)-1].Name) } }() - return kms.FuseIter(iter), nil + return &iterator{ + ch: values, + ctx: ctx, + cancel: cancel, + }, nil } type iterator struct { - values <-chan string - - lock sync.RWMutex - last string - err error + ch <-chan string + ctx context.Context + cancel context.CancelCauseFunc } -var _ kms.Iter = (*iterator)(nil) +var _ kv.Iter[string] = (*iterator)(nil) // Next moves the iterator to the next key, if any. // This key is available until Next is called again. @@ -475,44 +480,20 @@ var _ kms.Iter = (*iterator)(nil) // It returns true if and only if there is a new key // available. If there are no more keys or an error // has been encountered, Next returns false. -func (i *iterator) Next() bool { - v, ok := <-i.values - if !ok { - return false +func (i *iterator) Next() (string, bool) { + select { + case v, ok := <-i.ch: + return v, ok + case <-i.ctx.Done(): + return "", false } - - i.lock.Lock() - defer i.lock.Unlock() - - i.last = v - return i.err == nil -} - -// Name returns the name of the current key. Name -// can be called multiple times an returns the -// same value until Next is called again. -func (i *iterator) Name() string { - i.lock.RLock() - defer i.lock.RUnlock() - return i.last } // Err returns the first error, if any, encountered // while iterating over the set of keys. func (i *iterator) Close() error { - i.lock.RLock() - defer i.lock.RUnlock() - return i.err -} - -// SetErr sets the iteration error to indicate -// that the iteration failed. Subsequent calls -// to Next will return false. -func (i *iterator) SetErr(err error) { - i.lock.Lock() - defer i.lock.Unlock() - - i.err = err + i.cancel(context.Canceled) + return context.Cause(i.ctx) } // parseErrorResponse returns an error containing diff --git a/internal/keystore/fs/fs.go b/internal/keystore/fs/fs.go index badaac9d..a2f06de9 100644 --- a/internal/keystore/fs/fs.go +++ b/internal/keystore/fs/fs.go @@ -20,18 +20,18 @@ import ( "aead.dev/mem" "github.com/minio/kes-go" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) -// NewConn returns a new Conn that reads +// NewStore returns a new Store that reads // from and writes to the given directory. // // If the directory or any parent directory -// does not exist, NewConn creates them all. +// does not exist, NewStore creates them all. // // It returns an error if dir exists but is // not a directory. -func NewConn(dir string) (*Conn, error) { +func NewStore(dir string) (*Store, error) { switch file, err := os.Stat(dir); { case errors.Is(err, os.ErrNotExist): if err = os.MkdirAll(dir, 0o755); err != nil { @@ -44,31 +44,31 @@ func NewConn(dir string) (*Conn, error) { return nil, errors.New("fs: '" + dir + "' is not a directory") } } - return &Conn{dir: dir}, nil + return &Store{dir: dir}, nil } -// Conn is a connection to a directory on +// Store is a connection to a directory on // the filesystem. // -// It implements the kms.Conn interface and +// It implements the kms.Store interface and // acts as KMS abstraction over a fileystem. -type Conn struct { +type Store struct { dir string lock sync.RWMutex } -var _ kms.Conn = (*Conn)(nil) +var _ kv.Store[string, []byte] = (*Store)(nil) // Status returns the current state of the Conn. // // In particular, it reports whether the underlying // filesystem is accessible. -func (c *Conn) Status(context.Context) (kms.State, error) { +func (s *Store) Status(context.Context) (kv.State, error) { start := time.Now() - if _, err := os.Stat(c.dir); err != nil { - return kms.State{}, &kms.Unreachable{Err: err} + if _, err := os.Stat(s.dir); err != nil { + return kv.State{}, &kv.Unreachable{Err: err} } - return kms.State{ + return kv.State{ Latency: time.Since(start), }, nil } @@ -77,15 +77,15 @@ func (c *Conn) Status(context.Context) (kms.State, error) { // the Conn directory if and only if no such file exists. // // It returns kes.ErrKeyExists if such a file already exists. -func (c *Conn) Create(_ context.Context, name string, value []byte) error { +func (s *Store) Create(_ context.Context, name string, value []byte) error { if err := validName(name); err != nil { return err } - c.lock.Lock() - defer c.lock.Unlock() + s.lock.Lock() + defer s.lock.Unlock() - filename := filepath.Join(c.dir, name) - switch err := c.create(filename, value); { + filename := filepath.Join(s.dir, name) + switch err := s.create(filename, value); { case errors.Is(err, os.ErrExist): return kes.ErrKeyExists case err != nil: @@ -95,19 +95,27 @@ func (c *Conn) Create(_ context.Context, name string, value []byte) error { return nil } +// Set creates a new file with the given name inside +// the Conn directory if and only if no such file exists. +// +// It returns kes.ErrKeyExists if such a file already exists. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Get reads the content of the named file within the Conn // directory. It returns kes.ErrKeyNotFound if no such file // exists. -func (c *Conn) Get(_ context.Context, name string) ([]byte, error) { +func (s *Store) Get(_ context.Context, name string) ([]byte, error) { const MaxSize = 1 * mem.MiB if err := validName(name); err != nil { return nil, err } - c.lock.RLock() - defer c.lock.RUnlock() + s.lock.RLock() + defer s.lock.RUnlock() - file, err := os.Open(filepath.Join(c.dir, name)) + file, err := os.Open(filepath.Join(s.dir, name)) if errors.Is(err, os.ErrNotExist) { return nil, kes.ErrKeyNotFound } @@ -129,11 +137,11 @@ func (c *Conn) Get(_ context.Context, name string) ([]byte, error) { // Delete deletes the named file within the Conn directory if // and only if it exists. It returns kes.ErrKeyNotFound if // no such file exists. -func (c *Conn) Delete(_ context.Context, name string) error { +func (s *Store) Delete(_ context.Context, name string) error { if err := validName(name); err != nil { return err } - switch err := os.Remove(filepath.Join(c.dir, name)); { + switch err := os.Remove(filepath.Join(s.dir, name)); { case errors.Is(err, os.ErrNotExist): return kes.ErrKeyNotFound default: @@ -144,15 +152,15 @@ func (c *Conn) Delete(_ context.Context, name string) error { // List returns a Iter over the files within the Conn directory. // The Iter must be closed to release any filesystem resources // back to the OS. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { - dir, err := os.Open(c.dir) +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { + dir, err := os.Open(s.dir) if err != nil { return nil, err } return NewIter(ctx, dir), nil } -func (c *Conn) create(filename string, value []byte) error { +func (s *Store) create(filename string, value []byte) error { file, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err != nil { return err @@ -183,7 +191,7 @@ type Iter struct { closed bool } -var _ kms.Iter = (*Iter)(nil) +var _ kv.Iter[string] = (*Iter)(nil) // NewIter returns an Iter all files within the given // directory. The Iter does not iterator recursively @@ -201,13 +209,14 @@ func NewIter(ctx context.Context, dir fs.ReadDirFile) *Iter { // // The name of the next directory entry is availbale via // the Name method. -func (i *Iter) Next() bool { +func (i *Iter) Next() (string, bool) { if i.closed || i.err != nil { - return false + return "", false } if len(i.names) > 0 { + entry := i.names[0] i.names = i.names[1:] - return true + return entry.Name(), true } if i.ctx != nil { @@ -216,7 +225,7 @@ func (i *Iter) Next() bool { if i.err = i.ctx.Err(); i.err == nil { i.err = context.Canceled } - return false + return "", false default: } } @@ -225,23 +234,17 @@ func (i *Iter) Next() bool { i.names, i.err = i.dir.ReadDir(N) if errors.Is(i.err, io.EOF) { i.err = nil - if len(i.names) > 0 { - return true - } - i.err = i.Close() - return false } - return i.err == nil -} - -// Name returns the current name of the directory entry. -// It returns the empty string if there are no more -// entries or once the Iter has encountered an error. -func (i *Iter) Name() string { - if len(i.names) > 0 && !i.closed && i.err == nil { - return i.names[0].Name() + if i.err != nil { + i.Close() + return "", false } - return "" + if len(i.names) > 0 { + entry := i.names[0] + i.names = i.names[1:] + return entry.Name(), true + } + return "", false } // Close closes the Iter and releases and filesystem @@ -252,12 +255,10 @@ func (i *Iter) Close() error { } i.closed = true - err := i.dir.Close() - if i.err != nil { - return i.err + if err := i.dir.Close(); i.err == nil { + i.err = err } - i.err = err - return err + return i.err } func validName(name string) error { diff --git a/internal/keystore/gcp/iterator.go b/internal/keystore/gcp/iterator.go index 2ba5a517..d470bd32 100644 --- a/internal/keystore/gcp/iterator.go +++ b/internal/keystore/gcp/iterator.go @@ -13,34 +13,29 @@ import ( type iterator struct { src *secretmanager.SecretIterator - last string err error closed bool } -func (i *iterator) Next() bool { +func (i *iterator) Next() (string, bool) { if i.closed { - return false + return "", false } + v, err := i.src.Next() - if err == gcpiterator.Done { - i.err = i.Close() - return false - } if err != nil { i.err = err - return false + if err == gcpiterator.Done { + i.err = i.Close() + } + return "", false } - i.last = path.Base(v.GetName()) - return true + return path.Base(v.GetName()), true } -func (i *iterator) Name() string { return i.last } - func (i *iterator) Close() error { if !i.closed { i.closed = true - i.last = "" } return i.err } diff --git a/internal/keystore/gcp/secret-manager.go b/internal/keystore/gcp/secret-manager.go index 477bef02..8bcfeae9 100644 --- a/internal/keystore/gcp/secret-manager.go +++ b/internal/keystore/gcp/secret-manager.go @@ -13,7 +13,7 @@ import ( "time" "github.com/minio/kes-go" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" "google.golang.org/api/option" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -22,17 +22,17 @@ import ( secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" ) -// Conn is a connection to a GCP SecretManager. -type Conn struct { +// Store is a GCP SecretManager secret store. +type Store struct { client *secretmanager.Client config *Config } -var _ kms.Conn = (*Conn)(nil) // compiler check +var _ kv.Store[string, []byte] = (*Store)(nil) // compiler check // Connect connects and authenticates to a GCP SecretManager // server. -func Connect(ctx context.Context, c *Config) (*Conn, error) { +func Connect(ctx context.Context, c *Config) (*Store, error) { c = c.Clone() if c == nil { c = &Config{} @@ -91,7 +91,7 @@ func Connect(ctx context.Context, c *Config) (*Conn, error) { return nil, err } - conn := &Conn{ + conn := &Store{ client: client, config: c, } @@ -103,17 +103,17 @@ func Connect(ctx context.Context, c *Config) (*Conn, error) { // Status returns the current state of the GCP SecretManager instance. // In particular, whether it is reachable and the network latency. -func (c *Conn) Status(ctx context.Context) (kms.State, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.config.Endpoint, nil) +func (s *Store) Status(ctx context.Context) (kv.State, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.config.Endpoint, nil) if err != nil { - return kms.State{}, err + return kv.State{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kms.State{}, &kms.Unreachable{Err: err} + return kv.State{}, &kv.Unreachable{Err: err} } - return kms.State{ + return kv.State{ Latency: time.Since(start), }, nil } @@ -125,9 +125,9 @@ func (c *Conn) Status(ctx context.Context) (kms.State, error) { // Creating a secret at the GCP SecretManager requires first creating // secret itself and then adding a secret version with some payload // data. The payload data contains the actual value. -func (c *Conn) Create(ctx context.Context, name string, value []byte) error { - secret, err := c.client.CreateSecret(ctx, &secretmanagerpb.CreateSecretRequest{ - Parent: path.Join("projects", c.config.ProjectID), +func (s *Store) Create(ctx context.Context, name string, value []byte) error { + secret, err := s.client.CreateSecret(ctx, &secretmanagerpb.CreateSecretRequest{ + Parent: path.Join("projects", s.config.ProjectID), SecretId: name, Secret: &secretmanagerpb.Secret{ Replication: &secretmanagerpb.Replication{ @@ -147,7 +147,7 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return fmt.Errorf("gcp: failed to create '%s': %v", name, err) } - _, err = c.client.AddSecretVersion(ctx, &secretmanagerpb.AddSecretVersionRequest{ + _, err = s.client.AddSecretVersion(ctx, &secretmanagerpb.AddSecretVersionRequest{ Parent: secret.Name, Payload: &secretmanagerpb.SecretPayload{ Data: value, @@ -162,10 +162,21 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return nil } +// Set stores the given key-value pair at GCP secret manager +// if and only if it doesn't exists. If such an entry already exists +// it returns kes.ErrKeyExists. +// +// Creating a secret at the GCP SecretManager requires first creating +// secret itself and then adding a secret version with some payload +// data. The payload data contains the actual value. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Get returns the value associated with the given key. -func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { - result, err := c.client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{ - Name: path.Join("projects", c.config.ProjectID, "secrets", name, "versions", "1"), +func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { + result, err := s.client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{ + Name: path.Join("projects", s.config.ProjectID, "secrets", name, "versions", "1"), }) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { @@ -187,9 +198,9 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { // versions through e.g. the GCP CLI. However, KES does not // support multiple secret versions and expects a different // mechanism for "key-rotation". -func (c *Conn) Delete(ctx context.Context, name string) error { - err := c.client.DeleteSecret(ctx, &secretmanagerpb.DeleteSecretRequest{ - Name: path.Join("projects", c.config.ProjectID, "secrets", name), +func (s *Store) Delete(ctx context.Context, name string) error { + err := s.client.DeleteSecret(ctx, &secretmanagerpb.DeleteSecretRequest{ + Name: path.Join("projects", s.config.ProjectID, "secrets", name), }) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { @@ -205,10 +216,10 @@ func (c *Conn) Delete(ctx context.Context, name string) error { // List returns a new Iterator over the names of // all stored keys. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { - location := path.Join("projects", c.config.ProjectID) +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { + location := path.Join("projects", s.config.ProjectID) return &iterator{ - src: c.client.ListSecrets(ctx, &secretmanagerpb.ListSecretsRequest{ + src: s.client.ListSecrets(ctx, &secretmanagerpb.ListSecretsRequest{ Parent: location, }), }, nil diff --git a/internal/keystore/gemalto/key-secure.go b/internal/keystore/gemalto/key-secure.go index df1807b9..1df0bec7 100644 --- a/internal/keystore/gemalto/key-secure.go +++ b/internal/keystore/gemalto/key-secure.go @@ -21,13 +21,12 @@ import ( "os" "path/filepath" "strings" - "sync" "time" "aead.dev/mem" "github.com/minio/kes-go" xhttp "github.com/minio/kes/internal/http" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // Credentials represents a Gemalto KeySecure @@ -59,17 +58,17 @@ type Config struct { Login Credentials } -// Conn is a connection to a Gemalto KeySecure server. -type Conn struct { +// Store is a Gemalto KeySecure secret store. +type Store struct { config Config client *client } -var _ kms.Conn = (*Conn)(nil) +var _ kv.Store[string, []byte] = (*Store)(nil) -// Connect establishes and returns a Conn to a Gemalto -// KeySecure server using the given config. -func Connect(ctx context.Context, config *Config) (c *Conn, err error) { +// Connect returns a Store to a Gemalto KeySecure +// server using the given config. +func Connect(ctx context.Context, config *Config) (c *Store, err error) { var rootCAs *x509.CertPool if config.CAPath != "" { rootCAs, err = loadCustomCAs(config.CAPath) @@ -104,7 +103,7 @@ func Connect(ctx context.Context, config *Config) (c *Conn, err error) { return nil, err } go client.RenewAuthToken(context.Background(), config.Endpoint, config.Login) - return &Conn{ + return &Store{ config: *config, client: client, }, nil @@ -112,17 +111,17 @@ func Connect(ctx context.Context, config *Config) (c *Conn, err error) { // Status returns the current state of the Gemalto KeySecure instance. // In particular, whether it is reachable and the network latency. -func (c *Conn) Status(ctx context.Context) (kms.State, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.config.Endpoint, nil) +func (s *Store) Status(ctx context.Context) (kv.State, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.config.Endpoint, nil) if err != nil { - return kms.State{}, err + return kv.State{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kms.State{}, &kms.Unreachable{Err: err} + return kv.State{}, &kv.Unreachable{Err: err} } - return kms.State{ + return kv.State{ Latency: time.Since(start), }, nil } @@ -130,7 +129,7 @@ func (c *Conn) Status(ctx context.Context) (kms.State, error) { // Create creates the given key-value pair at Gemalto if and only // if the given key does not exist. If such an entry already exists // it returns kes.ErrKeyExists. -func (c *Conn) Create(ctx context.Context, name string, value []byte) error { +func (s *Store) Create(ctx context.Context, name string, value []byte) error { type Request struct { Type string `json:"dataType"` Value string `json:"material"` @@ -146,15 +145,15 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return fmt.Errorf("gemalto: failed to create key '%s': %v", name, err) } - url := fmt.Sprintf("%s/api/v1/vault/secrets", c.config.Endpoint) + url := fmt.Sprintf("%s/api/v1/vault/secrets", s.config.Endpoint) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, xhttp.RetryReader(bytes.NewReader(body))) if err != nil { return fmt.Errorf("gemalto: failed to create key '%s': %v", name, err) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", c.client.AuthToken()) + req.Header.Set("Authorization", s.client.AuthToken()) - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -176,21 +175,28 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return nil } +// Set creates the given key-value pair at Gemalto if and only +// if the given key does not exist. If such an entry already exists +// it returns kes.ErrKeyExists. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Get returns the value associated with the given key. // If no entry for the key exists it returns kes.ErrKeyNotFound. -func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { +func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { type Response struct { Value string `json:"material"` } - url := fmt.Sprintf("%s/api/v1/vault/secrets/%s/export?type=name", c.config.Endpoint, name) + url := fmt.Sprintf("%s/api/v1/vault/secrets/%s/export?type=name", s.config.Endpoint, name) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) if err != nil { return nil, fmt.Errorf("gemalto: failed to access key '%s': %v", name, err) } - req.Header.Set("Authorization", c.client.AuthToken()) + req.Header.Set("Authorization", s.client.AuthToken()) - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return nil, err } @@ -222,15 +228,15 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { // Delete removes a the value associated with the given key // from Gemalto, if it exists. -func (c *Conn) Delete(ctx context.Context, name string) error { - url := fmt.Sprintf("%s/api/v1/vault/secrets/%s?type=name", c.config.Endpoint, name) +func (s *Store) Delete(ctx context.Context, name string) error { + url := fmt.Sprintf("%s/api/v1/vault/secrets/%s?type=name", s.config.Endpoint, name) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) if err != nil { return fmt.Errorf("gemalto: failed to delete key '%s': %v", name, err) } - req.Header.Set("Authorization", c.client.AuthToken()) + req.Header.Set("Authorization", s.client.AuthToken()) - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } @@ -261,7 +267,7 @@ func (c *Conn) Delete(ctx context.Context, name string) error { // List returns a new Iterator over the names of // all stored keys. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { // Response is the JSON response returned by KeySecure. // It only contains the fields that we need to implement // paginated listing. The raw response contains much more @@ -274,10 +280,9 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { } `json:"resources"` } + var cancel context.CancelCauseFunc + ctx, cancel = context.WithCancelCause(ctx) values := make(chan string, 10) - iterator := &iterator{ - values: values, - } // The following go-routine keeps listing keys (in pages of size 'limit') // and writes the keys names to the Iterator. @@ -295,30 +300,30 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { for { // We have to tell KeySecure how many items we want to process per page and how many // items we want to skip - resp. how many items we have processed already. - url := fmt.Sprintf("%s/api/v1/vault/secrets?limit=%d&skip=%d", c.config.Endpoint, limit, skip) + url := fmt.Sprintf("%s/api/v1/vault/secrets?limit=%d&skip=%d", s.config.Endpoint, limit, skip) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - iterator.SetErr(fmt.Errorf("gemalto: failed to list keys: %v", err)) + cancel(fmt.Errorf("gemalto: failed to list keys: %v", err)) break } - req.Header.Set("Authorization", c.client.AuthToken()) + req.Header.Set("Authorization", s.client.AuthToken()) - resp, err := c.client.Do(req) + resp, err := s.client.Do(req) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - iterator.SetErr(err) + cancel(err) break } if err != nil { - iterator.SetErr(fmt.Errorf("gemalto: failed to list keys: %v", err)) + cancel(fmt.Errorf("gemalto: failed to list keys: %v", err)) break } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { if response, err := parseServerError(resp); err != nil { - iterator.SetErr(fmt.Errorf("gemalto: %s: failed to parse server response: %v", resp.Status, err)) + cancel(fmt.Errorf("gemalto: %s: failed to parse server response: %v", resp.Status, err)) } else { - iterator.SetErr(fmt.Errorf("gemalto: failed to list keys: '%s' (%d)", response.Message, response.Code)) + cancel(fmt.Errorf("gemalto: failed to list keys: '%s' (%d)", response.Message, response.Code)) } break } @@ -326,9 +331,9 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { const MaxBody = 32 * mem.MiB // A page should not be larger than 32 MiB. if err := json.NewDecoder(mem.LimitReader(resp.Body, MaxBody)).Decode(&response); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - iterator.SetErr(err) + cancel(err) } else { - iterator.SetErr(fmt.Errorf("gemalto: failed to list keys: listing page too large: %v", err)) + cancel(fmt.Errorf("gemalto: failed to list keys: listing page too large: %v", err)) } break } @@ -338,17 +343,13 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { // return items that we've already served to the client or skip items that we haven't // served, yet. if response.Skip != skip { - iterator.SetErr(fmt.Errorf("gemalto: failed to list keys: pagination is out-of-sync: tried to skip %d but skipped %d", skip, response.Skip)) + cancel(fmt.Errorf("gemalto: failed to list keys: pagination is out-of-sync: tried to skip %d but skipped %d", skip, response.Skip)) break } for _, v := range response.Resources { select { case values <- v.Name: case <-ctx.Done(): - if err = ctx.Err(); err == nil { - err = context.Canceled - } - iterator.SetErr(err) return } } @@ -359,38 +360,31 @@ func (c *Conn) List(ctx context.Context) (kms.Iter, error) { } } }() - return iterator, nil + return &iter{ + ch: values, + ctx: ctx, + cancel: cancel, + }, nil } -type iterator struct { - values <-chan string - last string - - lock sync.Mutex - err error +type iter struct { + ch <-chan string + ctx context.Context + cancel context.CancelCauseFunc } -func (i *iterator) Next() bool { - v, ok := <-i.values - if !ok { - return false +func (i *iter) Next() (string, bool) { + select { + case v, ok := <-i.ch: + return v, ok + case <-i.ctx.Done(): + return "", false } - i.last = v - return true -} - -func (i *iterator) Name() string { return i.last } - -func (i *iterator) Close() error { - i.lock.Lock() - defer i.lock.Unlock() - return i.err } -func (i *iterator) SetErr(err error) { - i.lock.Lock() - i.err = err - i.lock.Unlock() +func (i *iter) Close() error { + i.cancel(context.Canceled) + return context.Cause(i.ctx) } // errResponse represents a KeySecure API error diff --git a/internal/keystore/generic/client.go b/internal/keystore/generic/client.go deleted file mode 100644 index 0bb84119..00000000 --- a/internal/keystore/generic/client.go +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright 2021 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package generic - -import ( - "bytes" - "context" - "crypto/tls" - "crypto/x509" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "strings" - "time" - - "aead.dev/mem" - "github.com/minio/kes-go" - xhttp "github.com/minio/kes/internal/http" - "github.com/minio/kes/internal/key" - "github.com/minio/kes/kms" -) - -// Config is a structure containing -// all generic KMS plugin configuration. -type Config struct { - // Endpoint is the endpoint of the - // KMS plugin. - Endpoint string - - // PrivateKey is an optional path to a - // TLS private key file containing a - // TLS private key for mTLS authentication. - PrivateKey string - - // Certificate is an optional path to a - // TLS certificate file containing a - // TLS certificate for mTLS authentication. - Certificate string - - // CAPath is an optional path to the root - // CA certificate(s) for verifying the TLS - // certificate of the KMS plugin. If empty, - // the OS default root CA set is used. - CAPath string -} - -// Conn is a connection to a generic KMS plugin. -type Conn struct { - config Config - client xhttp.Retry -} - -// Connect connects to the KMS plugin using the -// given configuration. -func Connect(_ context.Context, config *Config) (*Conn, error) { - if config == nil || config.Endpoint == "" { - return nil, errors.New("generic: endpoint is empty") - } - - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS13, - } - if config.Certificate != "" || config.PrivateKey != "" { - certificate, err := tls.LoadX509KeyPair(config.Certificate, config.PrivateKey) - if err != nil { - return nil, err - } - tlsConfig.Certificates = append(tlsConfig.Certificates, certificate) - } - if config.CAPath != "" { - rootCAs, err := loadCustomCAs(config.CAPath) - if err != nil { - return nil, err - } - tlsConfig.RootCAs = rootCAs - } - return &Conn{ - config: *config, - client: xhttp.Retry{ - Client: http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConfig, - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 10 * time.Second, - DualStack: true, - }).DialContext, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 30 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - }, - }, - }, nil -} - -var _ kms.Conn = (*Conn)(nil) - -// Status returns the current state of the generic KeyStore instance. -// In particular, whether it is reachable and the network latency. -func (c *Conn) Status(context.Context) (kms.State, error) { return kms.State{}, nil } - -// Create creates the given key-value pair at the generic KeyStore if -// and only if the given key does not exist. If such an entry already -// exists it returns kes.ErrKeyExists. -func (c *Conn) Create(ctx context.Context, name string, value []byte) error { - type Request struct { - Bytes []byte `json:"bytes"` - } - body, err := json.Marshal(Request{ - Bytes: value, - }) - if err != nil { - return fmt.Errorf("generic: failed to create key '%s': %v", name, err) - } - - url := endpoint(c.config.Endpoint, "/v1/key", url.PathEscape(name)) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, xhttp.RetryReader(bytes.NewReader(body))) - if err != nil { - return fmt.Errorf("generic: failed to create key '%s': %v", name, err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := c.client.Do(req) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return err - } - return fmt.Errorf("generic: failed to create key '%s': %v", name, err) - } - if resp.StatusCode != http.StatusCreated { - switch err = parseErrorResponse(resp); { - case err == kes.ErrKeyExists: - return kes.ErrKeyExists - default: - return fmt.Errorf("generic: failed to create key '%s': %v", name, err) - } - } - return nil -} - -// Delete removes a the value associated with the given key -// from the generic KeyStore, if it exists. -func (c *Conn) Delete(ctx context.Context, name string) error { - url := endpoint(c.config.Endpoint, "/v1/key", url.PathEscape(name)) - req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) - if err != nil { - return fmt.Errorf("generic: failed to delete key '%s': %v", name, err) - } - resp, err := c.client.Do(req) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return err - } - return fmt.Errorf("generic: failed to delete key '%s': %v", name, err) - } - if resp.StatusCode != http.StatusOK { - if err = parseErrorResponse(resp); err != nil { - return fmt.Errorf("generic: failed to delete key '%s': %v", name, err) - } - return fmt.Errorf("generic: failed to delete key '%s': %s (%d)", name, http.StatusText(resp.StatusCode), resp.StatusCode) - } - return nil -} - -// Get returns the value associated with the given key. -// If no entry for the key exists it returns kes.ErrKeyNotFound. -func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { - type Response struct { - Bytes []byte `json:"bytes"` - } - - url := endpoint(c.config.Endpoint, "/v1/key", url.PathEscape(name)) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, fmt.Errorf("generic: failed to access key '%s': %v", name, err) - } - resp, err := c.client.Do(req) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, err - } - return nil, fmt.Errorf("generic: failed to access key '%s': %v", name, err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - switch err = parseErrorResponse(resp); { - case err == kes.ErrKeyNotFound: - return nil, kes.ErrKeyNotFound - case err == nil: - return nil, fmt.Errorf("generic: failed to access key '%s': %s (%d)", name, http.StatusText(resp.StatusCode), resp.StatusCode) - default: - return nil, fmt.Errorf("generic: failed to access key '%s': %v", name, err) - } - } - - var ( - decoder = json.NewDecoder(mem.LimitReader(resp.Body, key.MaxSize)) - response Response - ) - if err = decoder.Decode(&response); err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, err - } - return nil, fmt.Errorf("generic: failed to parse server response: %v", err) - } - return response.Bytes, nil -} - -// List returns a new Iterator over the names of all stored keys. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { - url := endpoint(c.config.Endpoint, "/v1/key") - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, fmt.Errorf("generic: failed to list keys: %v", err) - } - resp, err := c.client.Do(req) - if err != nil { - return nil, fmt.Errorf("generic: failed to list keys: %v", err) - } - if resp.StatusCode != http.StatusOK { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, err - } - if err = parseErrorResponse(resp); err != nil { - return nil, fmt.Errorf("generic: failed to list keys: %v", err) - } - return nil, fmt.Errorf("generic: failed to list keys: %s (%d)", http.StatusText(resp.StatusCode), resp.StatusCode) - } - - decoder := json.NewDecoder(resp.Body) - return kms.FuseIter(&iterator{ - response: resp, - decoder: decoder, - }), nil -} - -type keyDescription struct { - Name string `json:"name"` - Last bool `json:"last"` -} - -type iterator struct { - response *http.Response - decoder *json.Decoder - - last keyDescription - err error -} - -func (i *iterator) Next() bool { - if err := i.decoder.Decode(&i.last); err != nil { - if err == io.EOF { - i.err = i.Close() - if i.err == nil && !i.last.Last { - i.err = io.ErrUnexpectedEOF - } - } else { - i.err = err - } - return false - } - if i.last.Last { - i.err = i.Close() - return i.err == nil - } - return true -} - -func (i *iterator) Name() string { return i.last.Name } - -func (i *iterator) Close() error { - if err := i.response.Body.Close(); i.err == nil { - i.err = err - } - return i.err -} - -// endpoint returns an endpoint URL starting with the -// given endpoint followed by the path elements. -// -// For example: -// - endpoint("https://127.0.0.1:7373", "version") => "https://127.0.0.1:7373/version" -// - endpoint("https://127.0.0.1:7373/", "/key/create", "my-key") => "https://127.0.0.1:7373/key/create/my-key" -// -// Any leading or trailing whitespaces are removed from -// the endpoint before it is concatenated with the path -// elements. -// -// The path elements will not be URL-escaped. -func endpoint(endpoint string, elems ...string) string { - endpoint = strings.TrimSpace(endpoint) - endpoint = strings.TrimSuffix(endpoint, "/") - - if len(elems) > 0 && !strings.HasPrefix(elems[0], "/") { - endpoint += "/" - } - return endpoint + path.Join(elems...) -} - -// parseErrorResponse returns an error containing -// the response status code and response body -// as error message if the response is an error -// response - i.e. status code >= 400. -// -// If the response status code is < 400, e.g. 200 OK, -// parseErrorResponse returns nil and does not attempt -// to read or close the response body. -// -// If resp is an error response, parseErrorResponse reads -// and closes the response body. -func parseErrorResponse(resp *http.Response) error { - if resp == nil || resp.StatusCode < 400 { - return nil - } - if resp.Body == nil { - return kes.NewError(resp.StatusCode, "") - } - defer resp.Body.Close() - - const MaxBodySize = 1 * mem.MiB - size := mem.Size(resp.ContentLength) - if size < 0 || size > MaxBodySize { - size = MaxBodySize - } - - contentType := strings.TrimSpace(resp.Header.Get("Content-Type")) - if strings.HasPrefix(contentType, "application/json") { - type Response struct { - Message string `json:"message"` - } - var response Response - if err := json.NewDecoder(mem.LimitReader(resp.Body, size)).Decode(&response); err != nil { - return err - } - return kes.NewError(resp.StatusCode, response.Message) - } - - var sb strings.Builder - if _, err := io.Copy(&sb, mem.LimitReader(resp.Body, size)); err != nil { - return err - } - return kes.NewError(resp.StatusCode, sb.String()) -} - -// loadCustomCAs returns a new RootCA certificate pool -// that contains one or multiple certificates found at -// the given path. -// -// If path is a file then loadCustomCAs tries to parse -// the file as a PEM-encoded certificate. -// -// If path is a directory then loadCustomCAs tries to -// parse any file inside path as PEM-encoded certificate. -// It returns a non-nil error if one file is not a valid -// PEM-encoded X.509 certificate. -func loadCustomCAs(path string) (*x509.CertPool, error) { - rootCAs := x509.NewCertPool() - - f, err := os.Open(path) - if err != nil { - return rootCAs, err - } - defer f.Close() - - stat, err := f.Stat() - if err != nil { - return rootCAs, err - } - if !stat.IsDir() { - bytes, err := ioutil.ReadAll(f) - if err != nil { - return rootCAs, err - } - if !rootCAs.AppendCertsFromPEM(bytes) { - return rootCAs, fmt.Errorf("'%s' does not contain a valid X.509 PEM-encoded certificate", path) - } - return rootCAs, nil - } - - files, err := f.Readdir(0) - if err != nil { - return rootCAs, err - } - for _, file := range files { - if file.IsDir() { - continue - } - - name := filepath.Join(path, file.Name()) - bytes, err := ioutil.ReadFile(name) - if err != nil { - return rootCAs, err - } - if !rootCAs.AppendCertsFromPEM(bytes) { - return rootCAs, fmt.Errorf("'%s' does not contain a valid X.509 PEM-encoded certificate", name) - } - } - return rootCAs, nil -} diff --git a/internal/keystore/generic/server.go b/internal/keystore/generic/server.go deleted file mode 100644 index e422fee8..00000000 --- a/internal/keystore/generic/server.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2021 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -//go:build ignore -// +build ignore - -// go run server.go --endpoint --key --cert - -package main - -import ( - "context" - "crypto/tls" - "encoding/json" - "flag" - "io" - "log" - "net" - "net/http" - "os" - "os/signal" - "strings" - "sync" - "time" - - "github.com/minio/kes-go" - xhttp "github.com/minio/kes/internal/http" -) - -func main() { - log.SetFlags(0) - var ( - address string - certPath string - keyPath string - ) - flag.StringVar(&address, "addr", "0.0.0.0:7001", "") - flag.StringVar(&certPath, "cert", "", "") - flag.StringVar(&keyPath, "key", "", "") - - mux := http.NewServeMux() - mux.Handle("/v1/key/", &KeyStore{}) - - server := http.Server{ - Addr: address, - Handler: mux, - TLSConfig: &tls.Config{ - MinVersion: tls.VersionTLS13, - }, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - - serveChan := make(chan error, 1) - go func() { - host, port, err := net.SplitHostPort(server.Addr) - if err != nil { - log.Fatalf("Error: invalid server address: %q", server.Addr) - } - if host == "" { - host = "0.0.0.0" - } - ip := net.ParseIP(host) - if ip == nil { - log.Fatalf("Error: invalid server address: %q", server.Addr) - } - if ip.IsUnspecified() { - ip = net.IPv4(127, 0, 0, 1) - } - if certPath == "" && keyPath == "" { - log.Printf("Starting server listening on http://%s:%s ...", ip.String(), port) - serveChan <- server.ListenAndServe() - } else { - log.Printf("Starting server listening on https://%s:%s ...", ip.String(), port) - serveChan <- server.ListenAndServeTLS(certPath, keyPath) - } - }() - - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) - defer cancel() - select { - case <-ctx.Done(): - timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 800*time.Millisecond) - defer timeoutCancel() - - log.Println("\nStopping server... ") - if err := server.Shutdown(timeoutCtx); err != nil { - log.Fatalf("Error: Failed to shutdown server gracefully: %v", err) - } - case err := <-serveChan: - if err != nil { - log.Fatalf("Error: Failed to serve requests: %v", err) - } - } -} - -func (s *KeyStore) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.URL.Path, "/") { - r.URL.Path = "/" + r.URL.Path - } - - switch r.Method { - case http.MethodPost: - s.CreateKey(w, r) - case http.MethodDelete: - s.DeleteKey(w, r) - case http.MethodGet: - if r.URL.Path == "/v1/key" || r.URL.Path == "/v1/key/" { - s.ListKey(w, r) - } else { - s.GetKey(w, r) - } - default: - w.Header().Add("Allow", http.MethodPost) - w.Header().Add("Allow", http.MethodGet) - w.Header().Add("Allow", http.MethodDelete) - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -type KeyStore struct { - lock sync.RWMutex - store map[string][]byte -} - -func (s *KeyStore) CreateKey(w http.ResponseWriter, r *http.Request) { - type Request struct { - Bytes []byte `json:"bytes"` - } - const MaxSize = 1 * mem.MiB - var request Request - if err := json.NewDecoder(mem.LimitReader(r.Body, MaxSize)).Decode(&request); err != nil { - xhttp.Error(w, kes.NewError(http.StatusBadRequest, err.Error())) - return - } - - s.lock.Lock() - defer s.lock.Unlock() - if s.store == nil { - s.store = map[string][]byte{} - } - key := strings.TrimPrefix(r.URL.Path, "/v1/key/") - if _, ok := s.store[key]; ok { - xhttp.Error(w, kes.ErrKeyExists) - return - } - s.store[key] = request.Bytes - w.WriteHeader(http.StatusCreated) -} - -func (s *KeyStore) DeleteKey(w http.ResponseWriter, r *http.Request) { - s.lock.Lock() - defer s.lock.Unlock() - if s.store == nil { - s.store = map[string][]byte{} - } - key := strings.TrimPrefix(r.URL.Path, "/v1/key/") - delete(s.store, key) - w.WriteHeader(http.StatusOK) -} - -func (s *KeyStore) GetKey(w http.ResponseWriter, r *http.Request) { - type Response struct { - Bytes []byte `json:"bytes"` - } - s.lock.RLock() - defer s.lock.RUnlock() - - key := strings.TrimPrefix(r.URL.Path, "/v1/key/") - v, ok := s.store[key] - if !ok { - xhttp.Error(w, kes.ErrKeyNotFound) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(Response{ - Bytes: v, - }) -} - -func (s *KeyStore) ListKey(w http.ResponseWriter, r *http.Request) { - type Response struct { - Name string `json:"name"` - Last bool `json:"last,omitempty"` - } - s.lock.RLock() - defer s.lock.RUnlock() - - var ( - encoder = json.NewEncoder(w) - i int - ) - w.Header().Set("Content-Type", "application/x-ndjson") - w.WriteHeader(http.StatusOK) - for key := range s.store { - encoder.Encode(Response{ - Name: key, - Last: i == len(s.store)-1, - }) - } -} diff --git a/internal/keystore/generic/spec-v1.md b/internal/keystore/generic/spec-v1.md deleted file mode 100644 index 59ce09e1..00000000 --- a/internal/keystore/generic/spec-v1.md +++ /dev/null @@ -1,207 +0,0 @@ -## KES KeyStore Plugin Specification - -### Introduction - -KES supports various KMS / KeyStore implementations. However, some KMS / KeyStore -implementations cannot or should not be supported directly for multiple reasons. For example, -the KMS / KeyStore may be a proprietary custom solution. Adding a direct integration would -disclose IP or confidential information about the KMS / KeyStore implementation or about the -underlying infrastructure. Usually, those details must not be disclosed due to compliance rules -or even by law. - -Therefore, KES provides a plugin interface. The plugin interface defines an REST API that has -to be implemented by a KES KeyStore plugin. The KES server can then create, access, list and -delete keys indirectly at the KeyStore by talking to the plugin service. - -``` - ┌────────────────────────────────────────────────────┐ -┌────────────┐ REST API │ ┌─────────────────────┐ ┌───────────┐ │ -│ KES Server ├────────────┼─┤ KES KeyStore Plugin ├─────────────┤ Key Store │ │ -└────────────┘ │ └─────────────────────┘ └───────────┘ │ - └────────────────────────────────────────────────────┘ - internal/proprietary -``` - -Hence, the KES plugin interface enables integrations of arbitrary KMS / KeyStore implementations. - -### KeyStore Plugin - -A `v1` compatible key store plugin implements and exposes four HTTP API endpoints -for creating, accessing, listing and deleting keys. - -#### 1. CreateKey - -The `CreateKey` API endpoint creates a new key if and only if no key with the same -name exists. It MUST be exposed as: -``` -POST {HTTP | HTTPS}://{HOSTNAME}/v1/key/{KEY_NAME} -``` -The `{KEY_NAME}` MUST be a valid URL path segment. - -A key creation request MUST contain the key value as JSON object in the request body: -``` -{ - "bytes":"eyJieXRlcyI6Ilg4RUEwU3dkbUQzOXB4YzdUa293c0theWw1bC9sc0tJK1B6Zko1NDBCeW89In0K", -} -``` - -If the key creation succeeded `CreateKey` MUST respond with HTTP status code `201` -to the client. - -When a request tries to create a key but an entry with this key name already exists then -the `CreateKey` API endpoint MUST return the HTTP status code `400` with the following -JSON object as response body: -``` -{ - "message":"key already exists", -} -``` - -`CreateKey` MUST be implemented as atomic operation that only succeeds if and only if -no key with the same name exists. If multiple requests try to create a key with the same -name concurrently then only one MAY succeed and all other requests MUST fail. - -**Example:** - -``` -POST https://127.0.0.1:7373/v1/key/my-key -{ - "bytes":"eyJieXRlcyI6Ilg4RUEwU3dkbUQzOXB4YzdUa293c0theWw1bC9sc0tJK1B6Zko1NDBCeW89In0K", -} - - -HTTP/1.1 201 Created -``` - -#### 2. GetKey - -The `GetKey` API endpoint returns the value for a given key, if it exists. It MUST be -exposed as: -``` -GET {HTTP | HTTPS}://{HOSTNAME}/v1/key/{KEY_NAME} -``` - -The `{KEY_NAME}` MUST be a valid URL path segment. - -If a key with the requested key name exists then `GetKey` MUST respond with HTTP status -code `200` and a respond body that contains the value of the key: -``` -{ - "bytes":{KEY_VALUE}, -} -``` - -When a request tries to access a key that does not exist resp. no entry could be found then -the `GetKey` API endpoint MUST return the HTTP status code `404` with the following -error message as response body: -``` -{ - "message":"key does not exist", -} -``` - -**Example:** - -``` -GET https://127.0.0.1:7337/v1/key/my-key - - -HTTP/1.1 200 OK -{ - "bytes":"eyJieXRlcyI6Ilg4RUEwU3dkbUQzOXB4YzdUa293c0theWw1bC9sc0tJK1B6Zko1NDBCeW89In0K", -} -``` - -#### 3. DeleteKey - -The `DeleteKey` API endpoint deletes the key with the given key, if it exists. It MUST be -exposed as: -``` -DELETE {HTTP | HTTPS}://{HOSTNAME}/v1/key/{KEY_NAME} -``` - -The `{KEY_NAME}` MUST be a valid URL path segment. - -If a key has been deleted successfully the `DeleteKey` endpoint MUST respond with the HTTP -status code `200`. It MAY also respond with the HTTP status code `200` if the requested key -does not exist. - -**Example:** - -``` -DELETE https://127.0.0.1:7373/v1/key/my-key - - -HTTP/1.1 200 OK -``` - -#### 4. ListKeys - -The `ListKeys` API endpoint lists all key names. It MUST be exposed as: -``` -GET {HTTP | HTTPS}://{HOSTNAME}/v1/key -``` - -The `ListKeys` API endpoint returns a stream of JSON objects as `nd-json`: -``` -{ - "name":{KEY_NAME}, - "last":[true | false], -} -``` -Each JSON object MAY contain an additional `last` field. It MUST always be -set to `false`, if present, unless the JSON object is the last element of -the `nd-json` stream. The last JSON object MUST contain the `last` field -and it MUST be set to `true`. Multiple JSON objects MUST be separated by the -new-line character (`\n`). - -The response SHOULD set the HTTP header `Content-Type` to `application/x-ndjson`. - -**Example:** - -``` -GET https://127.0.0.1:7373/v1/key - -HTTP/1.1 200 OK -{"name":"my-key"} -{"name":"some-key"} -{"name":"another-key","last":true} -``` - -### Errors - -Whenever a `v1` compatible plugin encounters an error while processing a client request -it SHOULD respond with an appropriate HTTP status code and error message. All returned -errors MUST be JSON objects of the following format: -``` -{ - "message":{ERROR_MESSAGE}, -} -``` - -A plugin SHOULD only send a HTTP status code `5xx` if appropriate and no HTTP status code `4xx` -describes the error situation adequately. A error response MUST always contain an HTTP status code -`4xx` or `5xx`. - -### Security Considerations - -A KES KeyStore plugin implementation SHOULD only accept TLS/HTTPS request. The usage of plaintext -HTTP will leak cryptographic key material exchanged between the KES server and the plugin. Therefore, -it SHOULD NOT accept plaintext HTTP connection attempts except during development or for testing purposes. - -If a plugin implementation accepts TLS/HTTPS connections it MUST support at least TLS 1.2. If it only supports -TLS 1.2 then it MUST support at least one of the following TLS 1.2 cipher suites: - - `ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` - - `ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` - - `ECDHE_RSA_WITH_AES_128_GCM_SHA256` - - `ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` - - `ECDHE_RSA_WITH_AES_256_GCM_SHA384` - - `ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` - -The KES server and the KeyStore plugin SHOULD mutually authenticate via X.509 certificates (mTLS). Therefore, -the KES server will verify the X.509 certificate of the plugin and the plugin SHOULD verify the X.509 client -certificate of the KES server. - -Additionally, the plugin MAY verify that the cryptographic hash of the public key within the KES server client -certificate matches an expected value (TLS HPKP). This cryptographic hash should be computed as SHA-256 of the -raw subject public key of the X.509 certificate. diff --git a/internal/keystore/kes/kes.go b/internal/keystore/kes/kes.go index 5ef200e2..6864cd18 100644 --- a/internal/keystore/kes/kes.go +++ b/internal/keystore/kes/kes.go @@ -13,7 +13,7 @@ import ( "github.com/minio/kes-go" "github.com/minio/kes/internal/https" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // Config is a structure containing configuration @@ -46,7 +46,7 @@ type Config struct { } // Connect connects to a KES server with the given configuration. -func Connect(ctx context.Context, config *Config) (*Conn, error) { +func Connect(ctx context.Context, config *Config) (*Store, error) { if len(config.Endpoints) == 0 { return nil, errors.New("kes: no endpoints provided") } @@ -69,41 +69,43 @@ func Connect(ctx context.Context, config *Config) (*Conn, error) { } } - conn := &Conn{ + store := &Store{ client: kes.NewClientWithConfig("", &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: rootCAs, }), enclave: config.Enclave, } - conn.client.Endpoints = config.Endpoints + store.client.Endpoints = config.Endpoints - if _, err := conn.Status(ctx); err != nil { + if _, err := store.Status(ctx); err != nil { return nil, err } - return conn, nil + return store, nil } -// Conn is a connection to a KES server. -type Conn struct { +// Store is a connection to a KES server. +type Store struct { client *kes.Client enclave string } +var _ kv.Store[string, []byte] = (*Store)(nil) + // Status returns the current state of the KES connection. // I particular, whether it is reachable and the network latency. -func (c *Conn) Status(ctx context.Context) (kms.State, error) { +func (s *Store) Status(ctx context.Context) (kv.State, error) { start := time.Now() - _, err := c.client.Status(ctx) + _, err := s.client.Status(ctx) latency := time.Since(start) if connErr, ok := kes.IsConnError(err); ok { - return kms.State{}, &kms.Unreachable{Err: connErr} + return kv.State{}, &kv.Unreachable{Err: connErr} } if err != nil { - return kms.State{}, &kms.Unavailable{Err: err} + return kv.State{}, &kv.Unavailable{Err: err} } - return kms.State{ + return kv.State{ Latency: latency, }, nil } @@ -111,8 +113,8 @@ func (c *Conn) Status(ctx context.Context) (kms.State, error) { // Create creates the given key-value pair at the KES server // as a seret if and only no such secret already exists. // If such an entry already exists it returns kes.ErrKeyExists. -func (c *Conn) Create(ctx context.Context, name string, value []byte) error { - enclave := c.client.Enclave(c.enclave) +func (s *Store) Create(ctx context.Context, name string, value []byte) error { + enclave := s.client.Enclave(s.enclave) err := enclave.CreateSecret(ctx, name, value, nil) if errors.Is(err, kes.ErrSecretExists) { return kes.ErrKeyExists @@ -120,10 +122,17 @@ func (c *Conn) Create(ctx context.Context, name string, value []byte) error { return err } +// Set creates the given key-value pair at the KES server +// as a seret if and only no such secret already exists. +// If such an entry already exists it returns kes.ErrKeyExists. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Get returns the value associated with the given name. // If no entry for the key exists it returns kes.ErrKeyNotFound. -func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { - enclave := c.client.Enclave(c.enclave) +func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { + enclave := s.client.Enclave(s.enclave) secret, _, err := enclave.ReadSecret(ctx, name) if errors.Is(err, kes.ErrSecretNotFound) { return nil, kes.ErrKeyNotFound @@ -134,8 +143,8 @@ func (c *Conn) Get(ctx context.Context, name string) ([]byte, error) { // Delete removes a the value associated with the given name // from KES, if it exists. If no such entry exists it returns // kes.ErrKeyNotFound. -func (c *Conn) Delete(ctx context.Context, name string) error { - enclave := c.client.Enclave(c.enclave) +func (s *Store) Delete(ctx context.Context, name string) error { + enclave := s.client.Enclave(s.enclave) err := enclave.DeleteSecret(ctx, name) if errors.Is(err, kes.ErrSecretNotFound) { return kes.ErrKeyNotFound @@ -144,7 +153,24 @@ func (c *Conn) Delete(ctx context.Context, name string) error { } // List returns a new kms.Iter over all stored entries. -func (c *Conn) List(ctx context.Context) (kms.Iter, error) { - enclave := c.client.Enclave(c.enclave) - return enclave.ListSecrets(ctx, "*") +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { + enclave := s.client.Enclave(s.enclave) + i, err := enclave.ListSecrets(ctx, "*") + if err != nil { + return nil, err + } + return &iter{i}, nil +} + +type iter struct { + *kes.SecretIter } + +func (i *iter) Next() (string, bool) { + if i.SecretIter.Next() { + return i.Name(), true + } + return "", false +} + +func (i *iter) Close() error { return i.SecretIter.Close() } diff --git a/internal/keystore/mem/mem.go b/internal/keystore/mem/mem.go index 653942cc..c41f340f 100644 --- a/internal/keystore/mem/mem.go +++ b/internal/keystore/mem/mem.go @@ -25,9 +25,7 @@ var _ kv.Store[string, []byte] = (*Store)(nil) // Status returns the state of the in-memory key store which is // always healthy. func (s *Store) Status(_ context.Context) (kv.State, error) { - return kv.State{ - Latency: 0, - }, nil + return kv.State{Latency: 0}, nil } // Create adds the given key to the store if and only if diff --git a/internal/keystore/vault/iterator.go b/internal/keystore/vault/iterator.go index 6af6130c..d12eb7fd 100644 --- a/internal/keystore/vault/iterator.go +++ b/internal/keystore/vault/iterator.go @@ -8,29 +8,25 @@ import ( "fmt" "strings" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) type iterator struct { values []interface{} - last string } -var _ kms.Iter = (*iterator)(nil) +var _ kv.Iter[string] = (*iterator)(nil) -func (i *iterator) Next() bool { +func (i *iterator) Next() (string, bool) { for len(i.values) > 0 { v := fmt.Sprint(i.values[0]) i.values = i.values[1:] if !strings.HasSuffix(v, "/") { // Ignore prefixes; only iterator over actual entries - i.last = v - return true + return v, true } } - return false + return "", false } -func (i *iterator) Name() string { return i.last } - func (*iterator) Close() error { return nil } diff --git a/internal/keystore/vault/vault.go b/internal/keystore/vault/vault.go index 272393dc..adb1916d 100644 --- a/internal/keystore/vault/vault.go +++ b/internal/keystore/vault/vault.go @@ -24,18 +24,18 @@ import ( "aead.dev/mem" vaultapi "github.com/hashicorp/vault/api" "github.com/minio/kes-go" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) -// Conn is a connection to a Hashicorp Vault server. -type Conn struct { +// Store is a Hashicorp Vault secret store. +type Store struct { client *client config *Config } // Connect connects to a Hashicorp Vault server with // the given configuration. -func Connect(ctx context.Context, c *Config) (*Conn, error) { +func Connect(ctx context.Context, c *Config) (*Store, error) { c = c.Clone() if c.Engine == "" { @@ -133,26 +133,26 @@ func Connect(ctx context.Context, c *Config) (*Conn, error) { go client.CheckStatus(ctx, c.StatusPingAfter) go client.RenewToken(ctx, authenticate, ttl, retry) - return &Conn{ + return &Store{ config: c, client: client, }, nil } -var _ kms.Conn = (*Conn)(nil) +var _ kv.Store[string, []byte] = (*Store)(nil) var errSealed = errors.New("vault: key store is sealed") // Status returns the current state of the Hashicorp Vault instance. // In particular, whether it is reachable and the network latency. -func (s *Conn) Status(ctx context.Context) (kms.State, error) { +func (s *Store) Status(ctx context.Context) (kv.State, error) { // This is a workaround for https://github.com/hashicorp/vault/issues/14934 // The Vault SDK should not set the X-Vault-Namespace header // for root-only API paths. // Otherwise, Vault may respond with: 404 - unsupported path client, err := s.client.Clone() if err != nil { - return kms.State{}, err + return kv.State{}, err } client.ClearNamespace() @@ -165,23 +165,23 @@ func (s *Conn) Status(ctx context.Context) (kms.State, error) { if err == nil { switch { case !health.Initialized: - return kms.State{}, &kms.Unavailable{Err: errors.New("vault: not initialized")} + return kv.State{}, &kv.Unavailable{Err: errors.New("vault: not initialized")} case health.Sealed: - return kms.State{}, &kms.Unavailable{Err: errSealed} + return kv.State{}, &kv.Unavailable{Err: errSealed} default: - return kms.State{Latency: time.Since(start)}, nil + return kv.State{Latency: time.Since(start)}, nil } } if errors.Is(err, context.Canceled) && errors.Is(err, context.DeadlineExceeded) { - return kms.State{}, &kms.Unreachable{Err: err} + return kv.State{}, &kv.Unreachable{Err: err} } start = time.Now() req := s.client.Client.NewRequest(http.MethodGet, "") if _, err = s.client.Client.RawRequestWithContext(ctx, req); err != nil { - return kms.State{}, &kms.Unreachable{Err: err} + return kv.State{}, &kv.Unreachable{Err: err} } - return kms.State{ + return kv.State{ Latency: time.Since(start), }, nil } @@ -189,7 +189,7 @@ func (s *Conn) Status(ctx context.Context) (kms.State, error) { // Create creates the given key-value pair at Vault if and only // if the given key does not exist. If such an entry already exists // it returns kes.ErrKeyExists. -func (s *Conn) Create(ctx context.Context, name string, value []byte) error { +func (s *Store) Create(ctx context.Context, name string, value []byte) error { if s.client == nil { return errors.New("vault: no connection to " + s.config.Endpoint) } @@ -304,9 +304,16 @@ func (s *Conn) Create(ctx context.Context, name string, value []byte) error { return nil } +// Set creates the given key-value pair at Vault if and only +// if the given key does not exist. If such an entry already exists +// it returns kes.ErrKeyExists. +func (s *Store) Set(ctx context.Context, name string, value []byte) error { + return s.Create(ctx, name, value) +} + // Get returns the value associated with the given key. // If no entry for the key exists it returns kes.ErrKeyNotFound. -func (s *Conn) Get(_ context.Context, name string) ([]byte, error) { +func (s *Store) Get(_ context.Context, name string) ([]byte, error) { if s.client.Sealed() { return nil, errSealed } @@ -355,7 +362,7 @@ func (s *Conn) Get(_ context.Context, name string) ([]byte, error) { // Delete removes a the value associated with the given key // from Vault, if it exists. -func (s *Conn) Delete(ctx context.Context, name string) error { +func (s *Store) Delete(ctx context.Context, name string) error { if s.client.Sealed() { return errSealed } @@ -395,7 +402,7 @@ func (s *Conn) Delete(ctx context.Context, name string) error { // List returns a new Iterator over the names of // all stored keys. -func (s *Conn) List(ctx context.Context) (kms.Iter, error) { +func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { if s.client.Sealed() { return nil, errSealed } @@ -445,7 +452,5 @@ func (s *Conn) List(ctx context.Context) (kms.Iter, error) { if !ok { return nil, fmt.Errorf("vault: failed to list '%s': invalid key listing format", location) } - return kms.FuseIter(&iterator{ - values: values, - }), nil + return &iterator{values: values}, nil } diff --git a/internal/sys/enclave.go b/internal/sys/enclave.go index 398e99fc..56cbe2b5 100644 --- a/internal/sys/enclave.go +++ b/internal/sys/enclave.go @@ -20,7 +20,7 @@ import ( "github.com/minio/kes/internal/auth" "github.com/minio/kes/internal/key" "github.com/minio/kes/internal/secret" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // DefaultEnclaveName is the default Enclave name used @@ -147,7 +147,7 @@ func (e *Enclave) RLocker() sync.Locker { return e.lock.RLocker() } // due to a network error - it returns a // StoreState with StoreUnreachable and no // error. -func (e *Enclave) Status(context.Context) (kms.State, error) { return kms.State{}, nil } +func (e *Enclave) Status(context.Context) (kv.State, error) { return kv.State{}, nil } // CreateKey stores the given key if and only if no entry with // the given name exists. @@ -194,7 +194,7 @@ func (e *Enclave) GetKey(ctx context.Context, name string) (key.Key, error) { // The iterator makes no guarantees about whether concurrent changes // to the enclave - i.e. creation or deletion of keys - are reflected. // It does not provide any ordering guarantees. -func (e *Enclave) ListKeys(ctx context.Context) (kms.Iter, error) { +func (e *Enclave) ListKeys(ctx context.Context) (kv.Iter[string], error) { return e.keys.ListKeys(ctx) } diff --git a/internal/sys/fs.go b/internal/sys/fs.go index c13d5301..3e09357b 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -17,7 +17,7 @@ import ( "github.com/minio/kes/internal/auth" "github.com/minio/kes/internal/key" "github.com/minio/kes/internal/secret" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // VaultFS provides access to Vault state. @@ -75,7 +75,7 @@ type KeyFS interface { DeleteKey(ctx context.Context, name string) error // ListKeys returns an iterator over all key entries. - ListKeys(ctx context.Context) (kms.Iter, error) + ListKeys(ctx context.Context) (kv.Iter[string], error) } // SecretFS provides access to secrets within a particular diff --git a/internal/sys/key-fs.go b/internal/sys/key-fs.go index 27bb817b..11392699 100644 --- a/internal/sys/key-fs.go +++ b/internal/sys/key-fs.go @@ -15,7 +15,7 @@ import ( "aead.dev/mem" "github.com/minio/kes-go" "github.com/minio/kes/internal/key" - "github.com/minio/kes/kms" + "github.com/minio/kes/kv" ) // NewKeyFS returns a new KeyFS that @@ -125,7 +125,7 @@ func (fs *keyFS) DeleteKey(_ context.Context, name string) error { return err } -func (fs *keyFS) ListKeys(ctx context.Context) (kms.Iter, error) { +func (fs *keyFS) ListKeys(ctx context.Context) (kv.Iter[string], error) { file, err := os.Open(fs.rootDir) if err != nil { return nil, err @@ -144,7 +144,7 @@ type keyIterator struct { err error } -func (i *keyIterator) Next() bool { +func (i *keyIterator) Next() (string, bool) { const TmpFile = ".key.tmp" if len(i.names) > 0 { if i.names[0] == TmpFile { @@ -152,36 +152,38 @@ func (i *keyIterator) Next() bool { } } if len(i.names) > 0 { - i.next, i.names = i.names[0], i.names[1:] - return true + v := i.names[0] + i.names = i.names[1:] + return v, true } if i.err != nil { - return false + return "", false } select { case <-i.ctx.Done(): i.err = i.ctx.Err() - return false + return "", false default: } const N = 250 i.names, i.err = i.dir.Readdirnames(N) if i.err != nil && i.err != io.EOF { - return false + return "", false } if len(i.names) == 0 && i.err == io.EOF { - return false + return "", false } if i.names[0] == TmpFile { i.names = i.names[1:] } if len(i.names) > 0 { - i.next, i.names = i.names[0], i.names[1:] - return true + v := i.names[0] + i.names = i.names[1:] + return v, true } - return false + return "", false } func (i *keyIterator) Name() string { return i.next } diff --git a/keserv/config.go b/keserv/config.go deleted file mode 100644 index f0e2f46f..00000000 --- a/keserv/config.go +++ /dev/null @@ -1,936 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package keserv - -import ( - "context" - "errors" - "io" - "os" - "strconv" - "time" - - "github.com/minio/kes-go" - "github.com/minio/kes/internal/keystore/aws" - "github.com/minio/kes/internal/keystore/azure" - "github.com/minio/kes/internal/keystore/fortanix" - "github.com/minio/kes/internal/keystore/fs" - "github.com/minio/kes/internal/keystore/gcp" - "github.com/minio/kes/internal/keystore/gemalto" - "github.com/minio/kes/internal/keystore/generic" - kesstore "github.com/minio/kes/internal/keystore/kes" - "github.com/minio/kes/internal/keystore/vault" - "github.com/minio/kes/kms" - "gopkg.in/yaml.v3" -) - -// DecodeServerConfig parses and returns a new ServerConfig -// from an io.Reader. -func DecodeServerConfig(r io.Reader) (*ServerConfig, error) { - const Version = "v1" - - decoder := yaml.NewDecoder(r) - decoder.KnownFields(false) - - var node yaml.Node - if err := decoder.Decode(&node); err != nil { - return nil, err - } - - version, err := findVersion(&node) - if err != nil { - return nil, err - } - if version != "" && version != Version { - return nil, errors.New("keserv: invalid server config version '" + version + "'") - } - - var config serverConfigYAML - if err := node.Decode(&config); err != nil { - return nil, err - } - return yamlToServerConfig(&config), nil -} - -// EncodeServerConfig encodes and writes the ServerConfig to -// an io.Writer -func EncodeServerConfig(w io.Writer, config *ServerConfig) error { - return yaml.NewEncoder(w).Encode(serverConfigToYAML(config)) -} - -// ReadServerConfig parses and returns a new ServerConfig from -// a file. -func ReadServerConfig(filename string) (*ServerConfig, error) { - file, err := os.Open(filename) - if err != nil { - return nil, err - } - defer file.Close() - - config, err := DecodeServerConfig(file) - if err != nil { - return nil, err - } - if err := file.Close(); err != nil { - return nil, err - } - return config, nil -} - -// WriteServerConfig encodes and writes the ServerConfig to -// a file. -func WriteServerConfig(filename string, config *ServerConfig) error { - file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) - if err != nil { - return err - } - defer file.Close() - - if err := EncodeServerConfig(file, config); err != nil { - return err - } - return file.Close() -} - -// findVersion finds the version field in the -// the given YAML document AST. -// -// If the top level of the AST does not contain -// a version field the returned version is empty. -func findVersion(root *yaml.Node) (string, error) { - if root == nil { - return "", errors.New("keserv: invalid server config") - } - if root.Kind != yaml.DocumentNode { - return "", errors.New("keserv: invalid server config") - } - if len(root.Content) != 1 { - return "", errors.New("keserv: invalid server config") - } - - doc := root.Content[0] - for i, n := range doc.Content { - if n.Value == "version" { - if n.Kind != yaml.ScalarNode { - return "", errors.New("keserv: invalid server config version at line " + strconv.Itoa(n.Line)) - } - if i == len(doc.Content)-1 { - return "", errors.New("keserv: invalid server config version at line " + strconv.Itoa(n.Line)) - } - v := doc.Content[i+1] - if v.Kind != yaml.ScalarNode { - return "", errors.New("keserv: invalid server config version at line " + strconv.Itoa(v.Line)) - } - return v.Value, nil - } - } - return "", nil -} - -// ServerConfig is a structure that holds configuration -// for a (stateless) KES server. -type ServerConfig struct { - // Addr is the KES server address. - // - // It should be an IP or FQDN with - // an optional port number separated - // by ':'. - Addr Env[string] - - // Admin is the KES server admin identity. - Admin Env[kes.Identity] - - // TLS holds the KES server TLS configuration. - TLS TLSConfig - - // API holds the KES server API configuration. - API APIConfig - - // Cache holds the KES server cache configuration. - Cache CacheConfig - - // Log holds the KES server logging configuration. - Log LogConfig - - // Policies contains the KES server policy definitions - // and static identity assignments. - Policies map[string]Policy - - // Keys contains pre-defined keys that the KES server - // will create on before startup. - Keys []Key - - // KMS holds the KES server KMS backend configuration. - KMS KMSConfig - - _ [0]int // force usage of struct composite literals with field names -} - -// TLSConfig is a structure that holds the TLS configuration -// for a (stateless) KES server. -type TLSConfig struct { - // PrivateKey is the path to the KES server's TLS private key. - PrivateKey Env[string] - - // Certificate is the path to the KES server's TLS certificate. - Certificate Env[string] - - // CAPath is an optional path to a X.509 certificate or directory - // containing X.509 certificates that the KES server uses, in - // addition to the system root certificates, as authorities when - // verify client certificates. - // - // If empty, the KES server will only use the system root - // certificates. - CAPath Env[string] - - // Password is an optional password to decrypt the KES server's - // private key. - Password Env[string] - - // Proxies contains a list of TLS proxy identities. - // The KES identity of any TLS/HTTPS proxy sitting directly - // in-front of KES has to be included in this list. A KES - // server will only accept forwarded client requests from - // proxies listed here. - Proxies []Env[kes.Identity] - - // ForwardCertHeader is the HTTP header key used by any - // TLS / HTTPS proxy to forward the actual client certificate - // to KES. - ForwardCertHeader Env[string] - - _ [0]int -} - -// APIConfig is a structure that holds the API configuration -// for a (stateless) KES server. -type APIConfig struct { - // Paths contains a set of API paths and there - // API configuration. - Paths map[string]APIPathConfig - - _ [0]int -} - -// APIPathConfig is a structure that holds the API configuration -// for one particular KES API. -type APIPathConfig struct { - // Timeout is the duration after which the API response with - // a HTTP timeout error response. - // If Timeout <= 0 the API default is used. - Timeout Env[time.Duration] - - // InsecureSkipAuth controls whether the API verifies - // client identities. If InsecureSkipAuth is true, - // the API accepts requests from arbitrary identities. - // In this mode, the API can be used by anyone who can - // communicate to the KES server over HTTPS. - // This should only be set for testing or in certain - // cases for APIs that don't expose sensitive information, - // like metrics. - InsecureSkipAuth Env[bool] - - _ [0]int -} - -// CacheConfig is a structure that holds the Cache configuration -// for a (stateless) KES server. -type CacheConfig struct { - // Expiry is the time period after which any cache entries - // are discarded. It determines how often the KES server has - // to fetch a secret key from the KMS backend. - Expiry Env[time.Duration] - - // ExpiryUnused is the time period after which all unused - // cache entries are discarded. It determines how often - // "not frequently" used secret keys must be fetched from - // the KMS backend. - ExpiryUnused Env[time.Duration] - - // ExpiryOffline is the time period after which any cache - // entries in the offline cache are discarded. - // - // It determines how long the KES server can serve stateless - // requests when the KMS has become unavailable - - // e.g. due to a network outage. - // - // ExpiryOffline is only used while the KMS backend is not - // available. As long as the KMS is available, the regular - // cache expiry periods apply. - ExpiryOffline Env[time.Duration] - - _ [0]int -} - -// LogConfig is a structure that holds the logging configuration -// for a (stateless) KES server. -type LogConfig struct { - // Error enables/disables logging audit events to STDOUT. - // Valid values are "on" and "off". - Audit Env[string] - - // Error enables/disables logging error events to STDERR. - // Valid values are "on" and "off". - Error Env[string] - - _ [0]int -} - -// Policy is a structure defining a KES policy. -// -// Any request issued by a KES identity is validated -// by the associated allow and deny patterns. A -// request is accepted if and only if no deny pattern -// and at least one allow pattern matches the request. -type Policy struct { - // Allow is the list of API path patterns - // that are explicitly allowed. - Allow []string - - // Deny is the list of API path patterns - // that are explicitly denied. - Deny []string - - // Identities is a list of KES identities - // that are assigned to this policy. - Identities []Env[kes.Identity] - - _ [0]int -} - -// Key is a structure defining a cryptographic key -// that the KES server will create before startup. -type Key struct { - // Name is the name of the cryptographic key. - Name Env[string] - - _ [0]int -} - -// KMSConfig represents a KMS configuration. -// -// Concrete instances implement Connect to -// return a connection to a concrete KMS instance. -type KMSConfig interface { - // Connect establishes and returns a new connection - // to the KMS. - Connect(ctx context.Context) (kms.Conn, error) - - toYAML(yml *serverConfigYAML) - - fromYAML(yml *serverConfigYAML) -} - -// FSConfig is a structure containing the configuration -// for a simple filesystem KMS. -// -// A FSConfig should only be used when testing a KES server. -type FSConfig struct { - // Dir is the path to the directory that - // contains the keys. - // - // If the directory does not exist, it - // will be created when establishing - // a connection to the filesystem. - Dir Env[string] - - _ [0]int -} - -// Connect establishes and returns a kms.Conn to the OS filesystem. -func (c *FSConfig) Connect(context.Context) (kms.Conn, error) { return fs.NewConn(c.Dir.Value) } - -func (c *FSConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.Fs.Path = c.Dir -} - -func (c *FSConfig) fromYAML(yml *serverConfigYAML) { - c.Dir = yml.KeyStore.Fs.Path -} - -// KESConfig is a structure containing the configuration -// for using a KES server/cluster as KMS. -type KESConfig struct { - // Endpoints is a set of KES server endpoints. - // - // If multiple endpoints are provided, the requests - // will be automatically balanced across them. - Endpoints []Env[string] - - // Enclave is an optional enclave name. If empty, - // the default enclave name will be used. - Enclave Env[string] - - // CertificateFile is a path to a mTLS client - // certificate file used to authenticate to - // the KES server. - CertificateFile Env[string] - - // PrivateKeyFile is a path to a mTLS private - // key used to authenticate to the KES server. - PrivateKeyFile Env[string] - - // CAPath is an optional path to the root - // CA certificate(s) for verifying the TLS - // certificate of the KES server. - // - // If empty, the OS default root CA set is - // used. - CAPath Env[string] -} - -// Connect establishes and returns a kms.Conn to the -// KES server. -func (c *KESConfig) Connect(ctx context.Context) (kms.Conn, error) { - endpoints := make([]string, 0, len(c.Endpoints)) - for _, endpoint := range c.Endpoints { - endpoints = append(endpoints, endpoint.Value) - } - return kesstore.Connect(ctx, &kesstore.Config{ - Endpoints: endpoints, - Enclave: c.Enclave.Value, - Certificate: c.CertificateFile.Value, - PrivateKey: c.PrivateKeyFile.Value, - CAPath: c.CAPath.Value, - }) -} - -func (c *KESConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.KES.Endpoint = c.Endpoints - yml.KeyStore.KES.TLS.Certificate = c.CertificateFile - yml.KeyStore.KES.TLS.PrivateKey = c.PrivateKeyFile - yml.KeyStore.KES.TLS.CAPath = c.CAPath -} - -func (c *KESConfig) fromYAML(yml *serverConfigYAML) { - c.Endpoints = yml.KeyStore.KES.Endpoint - c.Enclave = yml.KeyStore.KES.Enclave - c.CertificateFile = yml.KeyStore.KES.TLS.Certificate - c.PrivateKeyFile = yml.KeyStore.KES.TLS.PrivateKey - c.CAPath = yml.KeyStore.KES.TLS.CAPath -} - -// KMSPluginConfig is a structure containing the -// configuration for a KMS plugin. -type KMSPluginConfig struct { - // Endpoint is the endpoint of the KMS plugin. - Endpoint Env[string] - - // PrivateKey is an optional path to a - // TLS private key file containing a - // TLS private key for mTLS authentication. - // - // If empty, mTLS authentication is disabled. - PrivateKey Env[string] - - // Certificate is an optional path to a - // TLS certificate file containing a - // TLS certificate for mTLS authentication. - // - // If empty, mTLS authentication is disabled. - Certificate Env[string] - - // CAPath is an optional path to the root - // CA certificate(s) for verifying the TLS - // certificate of the KMS plugin. - // - // If empty, the OS default root CA set is - // used. - CAPath Env[string] -} - -// Connect establishes and returns a kms.Conn to the -// KMS plugin. -func (c *KMSPluginConfig) Connect(ctx context.Context) (kms.Conn, error) { - return generic.Connect(ctx, &generic.Config{ - Endpoint: c.Endpoint.Value, - PrivateKey: c.PrivateKey.Value, - Certificate: c.Certificate.Value, - CAPath: c.CAPath.Value, - }) -} - -func (c *KMSPluginConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.Generic.Endpoint = c.Endpoint - yml.KeyStore.Generic.TLS.PrivateKey = c.PrivateKey - yml.KeyStore.Generic.TLS.Certificate = c.Certificate - yml.KeyStore.Generic.TLS.CAPath = c.CAPath -} - -func (c *KMSPluginConfig) fromYAML(yml *serverConfigYAML) { - c.Endpoint = yml.KeyStore.Generic.Endpoint - c.PrivateKey = yml.KeyStore.Generic.TLS.PrivateKey - c.Certificate = yml.KeyStore.Generic.TLS.Certificate - c.CAPath = yml.KeyStore.Generic.TLS.CAPath -} - -// VaultConfig is a structure containing the -// configuration for Hashicorp Vault. -type VaultConfig struct { - // Endpoint is the Hashicorp Vault endpoint. - Endpoint Env[string] - - // Namespace is an optional Hashicorp Vault namespace. - // An empty namespace means no particular namespace - // is used. - Namespace Env[string] - - // APIVersion is the API version of the Hashicorp Vault - // K/V engine. Valid values are: "v1" and "v2". - // If empty, defaults to "v1". - APIVersion Env[string] - - // Engine is the Hashicorp Vault K/V engine path. - // If empty, defaults to "kv". - Engine Env[string] - - // Prefix is an optional prefix / directory within the - // K/V engine. - // If empty, keys will be stored at the K/V engine top - // level. - Prefix Env[string] - - // AppRoleEngine is the AppRole authentication engine path. - // If empty, defaults to "approle". - AppRoleEngine Env[string] - - // AppRoleID is the AppRole access ID for authenticating - // to Hashicorp Vault via the AppRole method. - AppRoleID Env[string] - - // AppRoleSecret is the AppRole access secret for authenticating - // to Hashicorp Vault via the AppRole method. - AppRoleSecret Env[string] - - // AppRoleRetry is the retry delay between authentication attempts. - // If not set, defaults to 15s. - AppRoleRetry Env[time.Duration] - - // KubernetesEngine is the Kubernetes authentication engine path. - // If empty, defaults to "kubernetes". - KubernetesEngine Env[string] - - // KubernetesRole is the login role for authenticating via the - // kubernetes authentication method. - KubernetesRole Env[string] - - // KubernetesJWT is either the JWT or a path to a file containing - // the JWT for for authenticating via the kubernetes authentication - // method. - KubernetesJWT Env[string] - - // KubernetesRetry is the retry delay between authentication attempts. - // If not set, defaults to 15s. - KubernetesRetry Env[time.Duration] - - // PrivateKey is an optional path to a - // TLS private key file containing a - // TLS private key for mTLS authentication. - // - // If empty, mTLS authentication is disabled. - PrivateKey Env[string] - - // Certificate is an optional path to a - // TLS certificate file containing a - // TLS certificate for mTLS authentication. - // - // If empty, mTLS authentication is disabled. - Certificate Env[string] - - // CAPath is an optional path to the root - // CA certificate(s) for verifying the TLS - // certificate of the Hashicorp Vault server. - // - // If empty, the OS default root CA set is - // used. - CAPath Env[string] - - // StatusPing controls how often to Vault health status - // is checked. - // If not set, defaults to 10s. - StatusPing Env[time.Duration] - - _ [0]int -} - -// Connect establishes and returns a kms.Conn to Hashicorp Vault. -func (c *VaultConfig) Connect(ctx context.Context) (kms.Conn, error) { - config := &vault.Config{ - Endpoint: c.Endpoint.Value, - Engine: c.Engine.Value, - APIVersion: c.APIVersion.Value, - Namespace: c.Namespace.Value, - Prefix: c.Prefix.Value, - AppRole: vault.AppRole{ - Engine: c.AppRoleEngine.Value, - ID: c.AppRoleID.Value, - Secret: c.AppRoleSecret.Value, - Retry: c.AppRoleRetry.Value, - }, - K8S: vault.Kubernetes{ - Engine: c.KubernetesEngine.Value, - Role: c.KubernetesRole.Value, - JWT: c.KubernetesJWT.Value, - Retry: c.KubernetesRetry.Value, - }, - PrivateKey: c.PrivateKey.Value, - Certificate: c.Certificate.Value, - CAPath: c.CAPath.Value, - StatusPingAfter: c.StatusPing.Value, - } - return vault.Connect(ctx, config) -} - -func (c *VaultConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.Vault.Endpoint = c.Endpoint - yml.KeyStore.Vault.Namespace = c.Namespace - yml.KeyStore.Vault.APIVersion = c.APIVersion - yml.KeyStore.Vault.Engine = c.Engine - yml.KeyStore.Vault.Prefix = c.Prefix - yml.KeyStore.Vault.AppRole.Engine = c.AppRoleEngine - yml.KeyStore.Vault.AppRole.ID = c.AppRoleID - yml.KeyStore.Vault.AppRole.Secret = c.AppRoleSecret - yml.KeyStore.Vault.AppRole.Retry = c.AppRoleRetry - yml.KeyStore.Vault.Kubernetes.Engine = c.KubernetesEngine - yml.KeyStore.Vault.Kubernetes.Role = c.KubernetesRole - yml.KeyStore.Vault.Kubernetes.JWT = c.KubernetesJWT - yml.KeyStore.Vault.Kubernetes.Retry = c.KubernetesRetry - yml.KeyStore.Vault.TLS.PrivateKey = c.PrivateKey - yml.KeyStore.Vault.TLS.Certificate = c.Certificate - yml.KeyStore.Vault.TLS.CAPath = c.CAPath - yml.KeyStore.Vault.Status.Ping = c.StatusPing -} - -func (c *VaultConfig) fromYAML(yml *serverConfigYAML) { - c.Endpoint = yml.KeyStore.Vault.Endpoint - c.Namespace = yml.KeyStore.Vault.Namespace - c.APIVersion = yml.KeyStore.Vault.APIVersion - c.Engine = yml.KeyStore.Vault.Engine - c.Prefix = yml.KeyStore.Vault.Prefix - c.AppRoleEngine = yml.KeyStore.Vault.AppRole.Engine - c.AppRoleID = yml.KeyStore.Vault.AppRole.ID - c.AppRoleSecret = yml.KeyStore.Vault.AppRole.Secret - c.AppRoleRetry = yml.KeyStore.Vault.AppRole.Retry - c.KubernetesEngine = yml.KeyStore.Vault.Kubernetes.Engine - c.KubernetesRole = yml.KeyStore.Vault.Kubernetes.Role - c.KubernetesJWT = yml.KeyStore.Vault.Kubernetes.JWT - c.KubernetesRetry = yml.KeyStore.Vault.Kubernetes.Retry - c.PrivateKey = yml.KeyStore.Vault.TLS.PrivateKey - c.Certificate = yml.KeyStore.Vault.TLS.Certificate - c.CAPath = yml.KeyStore.Vault.TLS.CAPath - c.StatusPing = yml.KeyStore.Vault.Status.Ping -} - -// FortanixConfig is a structure containing the -// configuration for FortanixConfig SDKMS. -type FortanixConfig struct { - // Endpoint is the endpoint of the Fortanix KMS. - Endpoint Env[string] - - // GroupID is the ID of the access control group. - GroupID Env[string] - - // APIKey is the API key for authenticating to - // the Fortanix KMS. - APIKey Env[string] - - // CAPath is an optional path to the root - // CA certificate(s) for verifying the TLS - // certificate of the Hashicorp Vault server. - // - // If empty, the OS default root CA set is - // used. - CAPath Env[string] - - _ [0]int -} - -// Connect establishes and returns a kms.Conn to the Fortanix KMS. -func (c *FortanixConfig) Connect(ctx context.Context) (kms.Conn, error) { - return fortanix.Connect(ctx, &fortanix.Config{ - Endpoint: c.Endpoint.Value, - GroupID: c.GroupID.Value, - APIKey: fortanix.APIKey(c.APIKey.Value), - CAPath: c.CAPath.Value, - }) -} - -func (c *FortanixConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.Fortanix.SDKMS.Endpoint = c.Endpoint - yml.KeyStore.Fortanix.SDKMS.GroupID = c.GroupID - yml.KeyStore.Fortanix.SDKMS.Login.APIKey = c.APIKey - yml.KeyStore.Fortanix.SDKMS.TLS.CAPath = c.CAPath -} - -func (c *FortanixConfig) fromYAML(yml *serverConfigYAML) { - c.Endpoint = yml.KeyStore.Fortanix.SDKMS.Endpoint - c.GroupID = yml.KeyStore.Fortanix.SDKMS.GroupID - c.APIKey = yml.KeyStore.Fortanix.SDKMS.Login.APIKey - c.CAPath = yml.KeyStore.Fortanix.SDKMS.TLS.CAPath -} - -// SecretsManagerConfig is a structure containing the -// configuration for AWS SecretsManager. -type SecretsManagerConfig struct { - // Endpoint is the AWS SecretsManager endpoint. - // AWS SecretsManager endpoints have the following - // schema: - // secrestmanager[-fips]..amanzonaws.com - Endpoint Env[string] - - // Region is the AWS region the SecretsManager is - // located. - Region Env[string] - - // KMSKey is the AWS-KMS key ID (CMK-ID) used to - // to en/decrypt secrets managed by the SecretsManager. - // If empty, the default AWS KMS key is used. - KMSKey Env[string] - - // AccessKey is the access key for authenticating to AWS. - AccessKey Env[string] - - // SecretKey is the secret key for authenticating to AWS. - SecretKey Env[string] - - // SessionToken is an optional session token for authenticating - // to AWS. - SessionToken Env[string] - - _ [0]int -} - -// Connect establishes and returns a kms.Conn to the AWS SecretsManager. -func (c *SecretsManagerConfig) Connect(ctx context.Context) (kms.Conn, error) { - return aws.Connect(ctx, &aws.Config{ - Addr: c.Endpoint.Value, - Region: c.Region.Value, - KMSKeyID: c.KMSKey.Value, - Login: aws.Credentials{ - AccessKey: c.AccessKey.Value, - SecretKey: c.SecretKey.Value, - SessionToken: c.SessionToken.Value, - }, - }) -} - -func (c *SecretsManagerConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.Aws.SecretsManager.Endpoint = c.Endpoint - yml.KeyStore.Aws.SecretsManager.Region = c.Region - yml.KeyStore.Aws.SecretsManager.KmsKey = c.KMSKey - yml.KeyStore.Aws.SecretsManager.Login.AccessKey = c.AccessKey - yml.KeyStore.Aws.SecretsManager.Login.SecretKey = c.SecretKey - yml.KeyStore.Aws.SecretsManager.Login.SessionToken = c.SessionToken -} - -func (c *SecretsManagerConfig) fromYAML(yml *serverConfigYAML) { - c.Endpoint = yml.KeyStore.Aws.SecretsManager.Endpoint - c.Region = yml.KeyStore.Aws.SecretsManager.Region - c.KMSKey = yml.KeyStore.Aws.SecretsManager.KmsKey - c.AccessKey = yml.KeyStore.Aws.SecretsManager.Login.AccessKey - c.SecretKey = yml.KeyStore.Aws.SecretsManager.Login.SecretKey - c.SessionToken = yml.KeyStore.Aws.SecretsManager.Login.SessionToken -} - -// SecretManagerConfig is a structure containing the -// configuration for GCP SecretManager. -type SecretManagerConfig struct { - // ProjectID is the GCP project ID. - ProjectID Env[string] - - // Endpoint is the GCP project ID. If empty, - // defaults to: - // secretmanager.googleapis.com:443 - Endpoint Env[string] - - // Scopes are GCP OAuth2 scopes for accessing - // GCP APIs. If empty, defaults to the GCP - // default scopes. - Scopes []Env[string] - - // ClientEmail is the Client email of the - // GCP service account used to access the - // SecretManager. - ClientEmail Env[string] - - // ClientID is the Client ID of the GCP - // service account used to access the - // SecretManager. - ClientID Env[string] - - // KeyID is the private key ID of the GCP - // service account used to access the - // SecretManager. - KeyID Env[string] - - // Key is the private key of the GCP - // service account used to access the - // SecretManager. - Key Env[string] - - _ [0]int -} - -// Connect establishes and returns a kms.Conn to the GCP SecretManager. -func (c *SecretManagerConfig) Connect(ctx context.Context) (kms.Conn, error) { - config := &gcp.Config{ - Endpoint: c.Endpoint.Value, - ProjectID: c.ProjectID.Value, - Credentials: gcp.Credentials{ - ClientID: c.ClientID.Value, - Client: c.ClientEmail.Value, - KeyID: c.KeyID.Value, - Key: c.Key.Value, - }, - } - for _, scope := range c.Scopes { - config.Scopes = append(config.Scopes, scope.Value) - } - return gcp.Connect(ctx, config) -} - -func (c *SecretManagerConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.GCP.SecretManager.ProjectID = c.ProjectID - yml.KeyStore.GCP.SecretManager.Endpoint = c.Endpoint - yml.KeyStore.GCP.SecretManager.Scopes = c.Scopes - yml.KeyStore.GCP.SecretManager.Credentials.Client = c.ClientEmail - yml.KeyStore.GCP.SecretManager.Credentials.ClientID = c.ClientID - yml.KeyStore.GCP.SecretManager.Credentials.KeyID = c.KeyID - yml.KeyStore.GCP.SecretManager.Credentials.Key = c.Key -} - -func (c *SecretManagerConfig) fromYAML(yml *serverConfigYAML) { - c.ProjectID = yml.KeyStore.GCP.SecretManager.ProjectID - c.Endpoint = yml.KeyStore.GCP.SecretManager.Endpoint - c.Scopes = yml.KeyStore.GCP.SecretManager.Scopes - c.ClientEmail = yml.KeyStore.GCP.SecretManager.Credentials.Client - c.ClientID = yml.KeyStore.GCP.SecretManager.Credentials.ClientID - c.KeyID = yml.KeyStore.GCP.SecretManager.Credentials.KeyID - c.Key = yml.KeyStore.GCP.SecretManager.Credentials.Key -} - -// KeyVaultConfig is a structure containing the -// configuration for Azure KeyVault. -type KeyVaultConfig struct { - // Endpoint is the Azure KeyVault endpoint. - Endpoint Env[string] - - // TenantID is the ID of the Azure KeyVault tenant. - TenantID Env[string] - - // ClientID is the ID of the client accessing - // Azure KeyVault. - ClientID Env[string] - - // ClientSecret is the client secret accessing the - // Azure KeyVault. - ClientSecret Env[string] - - // ManagedIdentityClientID is the client ID of the - // Azure managed identity that access the KeyVault. - ManagedIdentityClientID Env[string] - - _ [0]int -} - -// Connect establishes and returns a kms.Conn to the Azure KeyVault. -func (c *KeyVaultConfig) Connect(ctx context.Context) (kms.Conn, error) { - if (c.TenantID.Value != "" || c.ClientID.Value != "" || c.ClientSecret.Value != "") && c.ManagedIdentityClientID.Value != "" { - return nil, errors.New("") // TODO - } - switch { - case c.TenantID.Value != "" || c.ClientID.Value != "" || c.ClientSecret.Value != "": - creds := azure.Credentials{ - TenantID: c.Endpoint.Value, - ClientID: c.ClientID.Value, - Secret: c.ClientSecret.Value, - } - return azure.ConnectWithCredentials(ctx, c.Endpoint.Value, creds) - case c.ManagedIdentityClientID.Value != "": - creds := azure.ManagedIdentity{ - ClientID: c.ManagedIdentityClientID.Value, - } - return azure.ConnectWithIdentity(ctx, c.Endpoint.Value, creds) - default: - return nil, errors.New("") // TODO - } -} - -func (c *KeyVaultConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.Azure.KeyVault.Endpoint = c.Endpoint - yml.KeyStore.Azure.KeyVault.Credentials.TenantID = c.TenantID - yml.KeyStore.Azure.KeyVault.Credentials.ClientID = c.ClientID - yml.KeyStore.Azure.KeyVault.Credentials.Secret = c.ClientSecret - yml.KeyStore.Azure.KeyVault.ManagedIdentity.ClientID = c.ManagedIdentityClientID -} - -func (c *KeyVaultConfig) fromYAML(yml *serverConfigYAML) { - c.Endpoint = yml.KeyStore.Azure.KeyVault.Endpoint - c.TenantID = yml.KeyStore.Azure.KeyVault.Credentials.TenantID - c.ClientID = yml.KeyStore.Azure.KeyVault.Credentials.ClientID - c.ClientSecret = yml.KeyStore.Azure.KeyVault.Credentials.Secret - c.ManagedIdentityClientID = yml.KeyStore.Azure.KeyVault.ManagedIdentity.ClientID -} - -// KeySecureConfig is a structure containing the -// configuration for Gemalto KeySecure / Thales -// CipherTrust Manager. -type KeySecureConfig struct { - // Endpoint is the endpoint to the KeySecure server. - Endpoint Env[string] - - // Token is the refresh authentication token to - // access the KeySecure server. - Token Env[string] - - // Domain is the isolated namespace within the - // KeySecure server. If empty, defaults to the - // top-level / root domain. - Domain Env[string] - - // Retry is the retry delay between authentication attempts. - // If not set, defaults to 15s. - Retry Env[time.Duration] - - // CAPath is an optional path to the root - // CA certificate(s) for verifying the TLS - // certificate of the KeySecure server. - // - // If empty, the OS default root CA set is - // used. - CAPath Env[string] - - _ [0]int -} - -// Connect establishes and returns a kms.Conn to the KeySecure server. -func (c *KeySecureConfig) Connect(ctx context.Context) (kms.Conn, error) { - return gemalto.Connect(ctx, &gemalto.Config{ - Endpoint: c.Endpoint.Value, - CAPath: c.CAPath.Value, - Login: gemalto.Credentials{ - Token: c.Token.Value, - Domain: c.Domain.Value, - Retry: c.Retry.Value, - }, - }) -} - -func (c *KeySecureConfig) toYAML(yml *serverConfigYAML) { - yml.KeyStore.Gemalto.KeySecure.Endpoint = c.Endpoint - yml.KeyStore.Gemalto.KeySecure.Login.Token = c.Token - yml.KeyStore.Gemalto.KeySecure.Login.Domain = c.Domain - yml.KeyStore.Gemalto.KeySecure.Login.Retry = c.Retry - yml.KeyStore.Gemalto.KeySecure.TLS.CAPath = c.CAPath -} - -func (c *KeySecureConfig) fromYAML(yml *serverConfigYAML) { - c.Endpoint = yml.KeyStore.Gemalto.KeySecure.Endpoint - c.Token = yml.KeyStore.Gemalto.KeySecure.Login.Token - c.Domain = yml.KeyStore.Gemalto.KeySecure.Login.Domain - c.Retry = yml.KeyStore.Gemalto.KeySecure.Login.Retry - c.CAPath = yml.KeyStore.Gemalto.KeySecure.TLS.CAPath -} diff --git a/keserv/config_test.go b/keserv/config_test.go deleted file mode 100644 index 57cff210..00000000 --- a/keserv/config_test.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package keserv - -import ( - "bytes" - "testing" - "time" - - "github.com/minio/kes-go" - "gopkg.in/yaml.v3" -) - -func TestReadServerConfig(t *testing.T) { - for i, test := range readServerConfigTests { - _, err := ReadServerConfig(test.Filename) - if err == nil && test.ShouldFail { - t.Fatalf("Test %d should fail but passed", i) - } - if err != nil && !test.ShouldFail { - t.Fatalf("Test %d: failed to read server config: %v", i, err) - } - } -} - -var readServerConfigTests = []struct { - Filename string - ShouldFail bool -}{ - {Filename: "./testdata/fs.yml"}, - {Filename: "./testdata/with_tls_ca.yml"}, - {Filename: "./testdata/with_version.yml"}, - - {Filename: "./testdata/invalid_keys.yml", ShouldFail: true}, - {Filename: "./testdata/invalid_root.yml", ShouldFail: true}, - {Filename: "./testdata/invalid_version.yml", ShouldFail: true}, -} - -func TestRoundtripServerConfig(t *testing.T) { - for i, config := range roundtripServerConfigTests { - var buffer bytes.Buffer - if err := EncodeServerConfig(&buffer, &config); err != nil { - t.Fatalf("Test %d: failed to encode config: %v", i, err) - } - if _, err := DecodeServerConfig(&buffer); err != nil { - t.Fatalf("Test %d: failed to encode config: %v", i, err) - } - } -} - -var roundtripServerConfigTests = []ServerConfig{ - { - Addr: Env[string]{Value: "0.0.0.0:7373"}, - Admin: Env[kes.Identity]{Value: "disabled"}, - TLS: TLSConfig{ - Password: Env[string]{Value: "horse battery staple"}, - PrivateKey: Env[string]{Value: "/tmp/private.key"}, - Certificate: Env[string]{Value: "/tmp/public.crt"}, - CAPath: Env[string]{Value: "/tmp/CAs"}, - Proxies: []Env[kes.Identity]{ - {Value: "bf8d6fd2cffc6bf98f423013c13559ae2c25cfd3cd0c76f626901c95aa8c3eff"}, - }, - ForwardCertHeader: Env[string]{Value: "Client-Cert"}, - }, - Cache: CacheConfig{ - Expiry: Env[time.Duration]{Value: 5*time.Minute + 30*time.Second}, - ExpiryUnused: Env[time.Duration]{Value: 30 * time.Second}, - ExpiryOffline: Env[time.Duration]{Value: 1 * time.Hour}, - }, - Log: LogConfig{ - Audit: Env[string]{Value: "off"}, - Error: Env[string]{Value: "on"}, - }, - Policies: map[string]Policy{ - "my-policy": { - Allow: []string{ - "/v1/key/create/*", - "/v1/key/generate/*", - "/v1/key/decrypt/*", - "/v1/key/delete/*", - }, - Deny: []string{ - "/v1/key/decrypt/disallowed-key", - }, - Identities: []Env[kes.Identity]{ - {Value: "74c51d3e53094d1a6c35c667ae0d122150b867deb564dc17cc2249b9a1af3a78"}, - }, - }, - }, - Keys: []Key{ - { - Name: Env[string]{Value: "my-key-1"}, - }, - { - Name: Env[string]{Value: "my-key-2"}, - }, - }, - KMS: &FSConfig{ - Dir: Env[string]{Value: "/tmp/keys"}, - }, - }, -} - -func TestFindVersion(t *testing.T) { - for i, test := range findVersionsTests { - version, err := findVersion(test.Root) - if err == nil && test.ShouldFail { - t.Fatalf("Test %d should fail but passed", i) - } - if err != nil && !test.ShouldFail { - t.Fatalf("Test %d: failed to find version: %v", i, err) - } - if !test.ShouldFail && version != test.Version { - t.Fatalf("Test %d: got '%s' - want '%s'", i, version, test.Version) - } - } -} - -var findVersionsTests = []struct { - Version string - Root *yaml.Node - ShouldFail bool -}{ - { // 0 - Document tree without a "version" node - Version: "", - Root: &yaml.Node{ - Kind: yaml.DocumentNode, - Content: []*yaml.Node{{Content: []*yaml.Node{{}}}}, - }, - }, - { // 1 - Document tree with a "version" node - Version: "v1", - Root: &yaml.Node{ - Kind: yaml.DocumentNode, - Content: []*yaml.Node{ - {Content: []*yaml.Node{ - { - Kind: yaml.ScalarNode, - Value: "version", - }, - { - Kind: yaml.ScalarNode, - Value: "v1", - }, - }}, - }, - }, - }, - - { // 2 - Root: nil, - ShouldFail: true, - }, - { // 3 - Root: &yaml.Node{Kind: yaml.ScalarNode}, - ShouldFail: true, - }, - { // 3 - Root: &yaml.Node{Kind: yaml.DocumentNode}, - ShouldFail: true, - }, - { // 4 - Root: &yaml.Node{Kind: yaml.DocumentNode, Content: make([]*yaml.Node, 2)}, - ShouldFail: true, - }, - { // 5 - Root: &yaml.Node{ - Kind: yaml.DocumentNode, - Content: []*yaml.Node{ - {Content: []*yaml.Node{ - { - Kind: yaml.DocumentNode, - Value: "version", - }, - }}, - }, - }, - ShouldFail: true, - }, - { // 6 - Root: &yaml.Node{ - Kind: yaml.DocumentNode, - Content: []*yaml.Node{ - {Content: []*yaml.Node{ - { - Kind: yaml.ScalarNode, - Value: "version", - }, - }}, - }, - }, - ShouldFail: true, - }, -} diff --git a/keserv/env.go b/keserv/env.go deleted file mode 100644 index f6cdf5d8..00000000 --- a/keserv/env.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package keserv - -import ( - "errors" - "fmt" - "os" - "strings" - - "gopkg.in/yaml.v3" -) - -// Env wraps a type T and an optional environment -// variable name. -// -// It can be used to replace env. variable references -// with values from the environment during unmarshalling. -// Further, Env preserves any env. variable references -// during marshaling. -// -// During unmarshalling, Env replaces T's value -// with the value obtained from the referenced -// environment variable, if any. -// -// During marshaling, Env preserves and encodes -// the env. variable reference, if not empty. -// Otherwise, it encodes Env generic type value. -type Env[T any] struct { - Name string // Name of the env. variable - Value T // Value obtained by unmarshalling or from the environment -} - -// MarshalYAML returns the Env[T]'s YAML representation. -// -// If Env[T] refers to an environment variable then MarshalYAML -// returns the environment variable name as "${name}". -// Otherwise, it returns the YAML representation of T. -func (e Env[_]) MarshalYAML() (any, error) { - if e.Name != "" { - name := strings.TrimSpace(e.Name) - switch hasPrefix, hasSuffix := strings.HasPrefix(name, "${"), strings.HasSuffix(name, "}"); { - case hasPrefix && hasSuffix: - return name, nil - case !hasPrefix && !hasSuffix: - return "${" + name + "}", nil - default: - return nil, errors.New("keserv: invalid env variable name '" + e.Name + "'") - } - } - return e.Value, nil -} - -// UnmarshalYAML decodes the YAML node into the Env[T]. -// -// If the YAML node refers to an environment variable then -// UnmarshalYAML first looks up the value from the environment -// before unmarshaling it. -func (e *Env[_]) UnmarshalYAML(node *yaml.Node) error { - if name := strings.TrimSpace(node.Value); strings.HasPrefix(name, "${") && strings.HasSuffix(name, "}") { - name = strings.TrimSpace(name[2 : len(name)-1]) // We know that there is a '${' prefix and '}' suffix - value, ok := os.LookupEnv(name) - if !ok { - return fmt.Errorf("keserv: line %d: env. variable '%s' not found", node.Line, name) - } - - node.Value = value - if err := node.Decode(&e.Value); err != nil { - return err - } - e.Name = name - return nil - } - if err := node.Decode(&e.Value); err != nil { - return err - } - e.Name = "" - return nil -} diff --git a/keserv/example_test.go b/keserv/example_test.go deleted file mode 100644 index 62f66494..00000000 --- a/keserv/example_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package keserv - -import ( - "fmt" - "log" - "os" - - "gopkg.in/yaml.v3" -) - -func ExampleEnv() { - const Text = `addr: ${SERVER_ADDR}` - - os.Setenv("SERVER_ADDR", "127.0.0.1:7373") - - type Config struct { - Addr Env[string] `yaml:"addr"` - } - var config Config - if err := yaml.Unmarshal([]byte(Text), &config); err != nil { - log.Fatalln(err) - } - fmt.Println(config.Addr.Name, "=", config.Addr.Value) - // Output: SERVER_ADDR = 127.0.0.1:7373 -} diff --git a/keserv/testdata/fs.yml b/keserv/testdata/fs.yml deleted file mode 100644 index 65c4104b..00000000 --- a/keserv/testdata/fs.yml +++ /dev/null @@ -1,27 +0,0 @@ -address: 0.0.0.0:7373 -admin: - identity: disabled - -tls: - key: ./private.key - cert: ./public.crt - -cache: - expiry: - any: 5m0s - unused: 30s - offline: 0s - -policy: - minio: - allow: - - /v1/key/create/* - - /v1/key/generate/* - - /v1/key/decrypt/* - - /v1/key/bulk/decrypt/* - deny: - - /v1/key/decrypt/2022-10-31_my-bucket-1 - -keystore: - fs: - path: /tmp/kes \ No newline at end of file diff --git a/keserv/testdata/invalid_keys.yml b/keserv/testdata/invalid_keys.yml deleted file mode 100644 index 24b8f650..00000000 --- a/keserv/testdata/invalid_keys.yml +++ /dev/null @@ -1,27 +0,0 @@ -address: 0.0.0.0:7373 -admin: - identity: disabled - -tls: - key: ./private.key - cert: ./public.crt - -cache: - expiry: - any: 5m0s - unused: 30s - offline: 0s - -policy: - minio: - allow: - - /v1/key/create/* - - /v1/key/generate/* - - /v1/key/decrypt/* - - /v1/key/bulk/decrypt/* - deny: - - /v1/key/decrypt/2022-10-31_my-bucket-1 - -keys: - fs: - path: /tmp/kes \ No newline at end of file diff --git a/keserv/testdata/invalid_root.yml b/keserv/testdata/invalid_root.yml deleted file mode 100644 index b006fb07..00000000 --- a/keserv/testdata/invalid_root.yml +++ /dev/null @@ -1,26 +0,0 @@ -address: 0.0.0.0:7373 -root: disabled - -tls: - key: ./private.key - cert: ./public.crt - -cache: - expiry: - any: 5m0s - unused: 30s - offline: 0s - -policy: - minio: - allow: - - /v1/key/create/* - - /v1/key/generate/* - - /v1/key/decrypt/* - - /v1/key/bulk/decrypt/* - deny: - - /v1/key/decrypt/2022-10-31_my-bucket-1 - -keys: - fs: - path: /tmp/kes \ No newline at end of file diff --git a/keserv/testdata/invalid_version.yml b/keserv/testdata/invalid_version.yml deleted file mode 100644 index a5b9e48e..00000000 --- a/keserv/testdata/invalid_version.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: v2 -address: 0.0.0.0:7373 -admin: - identity: disabled - -tls: - key: ./private.key - cert: ./public.crt - -cache: - expiry: - any: 5m0s - unused: 30s - offline: 0s - -policy: - minio: - allow: - - /v1/key/create/* - - /v1/key/generate/* - - /v1/key/decrypt/* - - /v1/key/bulk/decrypt/* - deny: - - /v1/key/decrypt/2022-10-31_my-bucket-1 - -keystore: - fs: - path: /tmp/kes \ No newline at end of file diff --git a/keserv/testdata/with_tls_ca.yml b/keserv/testdata/with_tls_ca.yml deleted file mode 100644 index 3d674ce6..00000000 --- a/keserv/testdata/with_tls_ca.yml +++ /dev/null @@ -1,28 +0,0 @@ -address: 0.0.0.0:7373 -admin: - identity: disabled - -tls: - key: ./private.key - cert: ./public.crt - ca: "./CAs" - -cache: - expiry: - any: 5m0s - unused: 30s - offline: 0s - -policy: - minio: - allow: - - /v1/key/create/* - - /v1/key/generate/* - - /v1/key/decrypt/* - - /v1/key/bulk/decrypt/* - deny: - - /v1/key/decrypt/2022-10-31_my-bucket-1 - -keystore: - fs: - path: /tmp/kes \ No newline at end of file diff --git a/keserv/testdata/with_version.yml b/keserv/testdata/with_version.yml deleted file mode 100644 index 373c6d67..00000000 --- a/keserv/testdata/with_version.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: v1 -address: 0.0.0.0:7373 -admin: - identity: disabled - -tls: - key: ./private.key - cert: ./public.crt - -cache: - expiry: - any: 5m0s - unused: 30s - offline: 0s - -policy: - minio: - allow: - - /v1/key/create/* - - /v1/key/generate/* - - /v1/key/decrypt/* - - /v1/key/bulk/decrypt/* - deny: - - /v1/key/decrypt/2022-10-31_my-bucket-1 - -keystore: - fs: - path: /tmp/kes \ No newline at end of file diff --git a/keserv/yml.go b/keserv/yml.go deleted file mode 100644 index 9246cdab..00000000 --- a/keserv/yml.go +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package keserv - -import ( - "time" - - "github.com/minio/kes-go" -) - -type serverConfigYAML struct { - Addr Env[string] `yaml:"address,omitempty"` - - Admin struct { - Identity Env[kes.Identity] `yaml:"identity"` - } `yaml:"admin"` - - TLS struct { - PrivateKey Env[string] `yaml:"key"` - Certificate Env[string] `yaml:"cert"` - CAPath Env[string] `yaml:"ca,omitempty"` - Password Env[string] `yaml:"password,omitempty"` - - Proxy struct { - Identities []Env[kes.Identity] `yaml:"identities,omitempty"` - Header struct { - ClientCert Env[string] `yaml:"cert,omitempty"` - } `yaml:"header,omitempty"` - } `yaml:"proxy,omitempty"` - } `yaml:"tls"` - - Policies map[string]struct { - Allow []string `yaml:"allow,omitempty"` - Deny []string `yaml:"deny,omitempty"` - Identities []Env[kes.Identity] `yaml:"identities,omitempty"` - } `yaml:"policy,omitempty"` - - Cache struct { - Expiry struct { - Any Env[time.Duration] `yaml:"any,omitempty"` - Unused Env[time.Duration] `yaml:"unused,omitempty"` - Offline Env[time.Duration] `yaml:"offline,omitempty"` - } `yaml:"expiry,omitempty"` - } `yaml:"cache,omitempty"` - - Log struct { - Error Env[string] `yaml:"error,omitempty"` - Audit Env[string] `yaml:"audit,omitempty"` - } `yaml:"log,omitempty"` - - API struct { - Paths map[string]struct { - InsecureSkipAuth Env[bool] `yaml:"skip_auth,omitempty"` - Timeout Env[time.Duration] `yaml:"timeout,omitempty"` - } `yaml:",omitempty,inline"` - } `yaml:"api,omitempty"` - - Keys []struct { - Name Env[string] `yaml:"name,omitempty"` - } `yaml:"keys,omitempty"` - - KeyStore struct { - Fs struct { - Path Env[string] `yaml:"path,omitempty"` - } `yaml:"fs,omitempty"` - - KES struct { - Endpoint []Env[string] `yaml:"endpoint,omitempty"` - Enclave Env[string] `yaml:"enclave,omitempty"` - TLS struct { - Certificate Env[string] `yaml:"cert,omitempty"` - PrivateKey Env[string] `yaml:"key,omitempty"` - CAPath Env[string] `yaml:"ca,omitempty"` - } `yaml:"tls,omitempty"` - } `yaml:"kes,omitempty"` - - Generic struct { - Endpoint Env[string] `yaml:"endpoint,omitempty"` - TLS struct { - PrivateKey Env[string] `yaml:"key,omitempty"` - Certificate Env[string] `yaml:"cert,omitempty"` - CAPath Env[string] `yaml:"ca,omitempty"` - } `yaml:"tls,omitempty"` - } `yaml:"generic,omitempty"` - - Vault struct { - Endpoint Env[string] `yaml:"endpoint,omitempty"` - Engine Env[string] `yaml:"engine,omitempty"` - APIVersion Env[string] `yaml:"version,omitempty"` - Namespace Env[string] `yaml:"namespace,omitempty"` - Prefix Env[string] `yaml:"prefix,omitempty"` - - AppRole struct { - Engine Env[string] `yaml:"engine,omitempty"` - ID Env[string] `yaml:"id,omitempty"` - Secret Env[string] `yaml:"secret,omitempty"` - Retry Env[time.Duration] `yaml:"retry,omitempty"` - } `yaml:"approle,omitempty"` - - Kubernetes struct { - Engine Env[string] `yaml:"engine,omitempty"` - Role Env[string] `yaml:"role,omitempty"` - JWT Env[string] `yaml:"jwt,omitempty"` // Can be either a JWT or a path to a file containing a JWT - Retry Env[time.Duration] `yaml:"retry,omitempty"` - } `yaml:"kubernetes,omitempty"` - - TLS struct { - PrivateKey Env[string] `yaml:"key,omitempty"` - Certificate Env[string] `yaml:"cert,omitempty"` - CAPath Env[string] `yaml:"ca,omitempty"` - } `yaml:"tls,omitempty"` - - Status struct { - Ping Env[time.Duration] `yaml:"ping,omitempty"` - } `yaml:"status,omitempty"` - } `yaml:"vault,omitempty"` - - Fortanix struct { - SDKMS struct { - Endpoint Env[string] `yaml:"endpoint,omitempty"` - GroupID Env[string] `yaml:"group_id,omitempty"` - - Login struct { - APIKey Env[string] `yaml:"key,omitempty"` - } `yaml:"credentials,omitempty"` - - TLS struct { - CAPath Env[string] `yaml:"ca,omitempty"` - } `yaml:"tls,omitempty"` - } `yaml:"sdkms,omitempty"` - } `yaml:"fortanix,omitempty"` - - Aws struct { - SecretsManager struct { - Endpoint Env[string] `yaml:"endpoint,omitempty"` - Region Env[string] `yaml:"region,omitempty"` - KmsKey Env[string] ` yaml:"kmskey,omitempty"` - - Login struct { - AccessKey Env[string] `yaml:"accesskey,omitempty"` - SecretKey Env[string] `yaml:"secretkey,omitempty"` - SessionToken Env[string] `yaml:"token,omitempty"` - } `yaml:"credentials,omitempty"` - } `yaml:"secretsmanager,omitempty"` - } `yaml:"aws,omitempty"` - - GCP struct { - SecretManager struct { - ProjectID Env[string] `yaml:"project_id,omitempty"` - Endpoint Env[string] `yaml:"endpoint,omitempty"` - Scopes []Env[string] `yaml:"scopes,omitempty"` - Credentials struct { - Client Env[string] `yaml:"client_email,omitempty"` - ClientID Env[string] `yaml:"client_id,omitempty"` - KeyID Env[string] `yaml:"private_key_id,omitempty"` - Key Env[string] `yaml:"private_key,omitempty"` - } `yaml:"credentials,omitempty"` - } `yaml:"secretmanager,omitempty"` - } `yaml:"gcp,omitempty"` - - Azure struct { - KeyVault struct { - Endpoint Env[string] `yaml:"endpoint,omitempty"` - Credentials struct { - TenantID Env[string] `yaml:"tenant_id,omitempty"` - ClientID Env[string] `yaml:"client_id,omitempty"` - Secret Env[string] `yaml:"client_secret,omitempty"` - } `yaml:"credentials,omitempty"` - ManagedIdentity struct { - ClientID Env[string] `yaml:"client_id,omitempty"` - } `yaml:"managed_identity,omitempty"` - } `yaml:"keyvault,omitempty"` - } `yaml:"azure,omitempty"` - - Gemalto struct { - KeySecure struct { - Endpoint Env[string] `yaml:"endpoint,omitempty"` - - Login struct { - Token Env[string] `yaml:"token,omitempty"` - Domain Env[string] `yaml:"domain,omitempty"` - Retry Env[time.Duration] `yaml:"retry,omitempty"` - } `yaml:"credentials,omitempty"` - - TLS struct { - CAPath Env[string] `yaml:"ca,omitempty"` - } `yaml:"tls,omitempty"` - } `yaml:"keysecure,omitempty"` - } `yaml:"gemalto,omitempty"` - } `yaml:"keystore,omitempty"` -} - -func serverConfigToYAML(config *ServerConfig) *serverConfigYAML { - yml := new(serverConfigYAML) - yml.Addr = config.Addr - yml.Admin.Identity = config.Admin - - // TLS - yml.TLS.PrivateKey = config.TLS.PrivateKey - yml.TLS.Certificate = config.TLS.Certificate - yml.TLS.CAPath = config.TLS.CAPath - yml.TLS.Password = config.TLS.Password - yml.TLS.Proxy.Identities = config.TLS.Proxies - yml.TLS.Proxy.Header.ClientCert = config.TLS.ForwardCertHeader - - // API - yml.API.Paths = make(map[string]struct { - InsecureSkipAuth Env[bool] `yaml:"skip_auth,omitempty"` - Timeout Env[time.Duration] `yaml:"timeout,omitempty"` - }, len(config.API.Paths)) - for path, api := range config.API.Paths { - type API struct { - InsecureSkipAuth Env[bool] `yaml:"skip_auth,omitempty"` - Timeout Env[time.Duration] `yaml:"timeout,omitempty"` - } - yml.API.Paths[path] = API{ - InsecureSkipAuth: api.InsecureSkipAuth, - Timeout: api.Timeout, - } - } - - // Cache - yml.Cache.Expiry.Any = config.Cache.Expiry - yml.Cache.Expiry.Unused = config.Cache.ExpiryUnused - yml.Cache.Expiry.Offline = config.Cache.ExpiryOffline - - // Log - yml.Log.Audit = config.Log.Audit - yml.Log.Error = config.Log.Error - - // Policies - yml.Policies = make(map[string]struct { - Allow []string `yaml:"allow,omitempty"` - Deny []string `yaml:"deny,omitempty"` - Identities []Env[kes.Identity] `yaml:"identities,omitempty"` - }, len(config.Policies)) - for name, policy := range config.Policies { - type Item struct { - Allow []string `yaml:"allow,omitempty"` - Deny []string `yaml:"deny,omitempty"` - Identities []Env[kes.Identity] `yaml:"identities,omitempty"` - } - yml.Policies[name] = Item{ - Allow: policy.Allow, - Deny: policy.Deny, - Identities: policy.Identities, - } - } - - // Keys - for _, key := range config.Keys { - type Item struct { - Name Env[string] `yaml:"name,omitempty"` - } - yml.Keys = append(yml.Keys, Item{ - Name: key.Name, - }) - } - - // KeyStore - if config.KMS != nil { - config.KMS.toYAML(yml) - } - return yml -} - -func yamlToServerConfig(yml *serverConfigYAML) *ServerConfig { - config := new(ServerConfig) - config.Addr = yml.Addr - config.Admin = yml.Admin.Identity - - // TLS - config.TLS.PrivateKey = yml.TLS.PrivateKey - config.TLS.Certificate = yml.TLS.Certificate - config.TLS.CAPath = yml.TLS.CAPath - config.TLS.Password = yml.TLS.Password - config.TLS.Proxies = yml.TLS.Proxy.Identities - config.TLS.ForwardCertHeader = yml.TLS.Proxy.Header.ClientCert - - // API - config.API.Paths = make(map[string]APIPathConfig, len(yml.API.Paths)) - for path, api := range yml.API.Paths { - config.API.Paths[path] = APIPathConfig{ - InsecureSkipAuth: api.InsecureSkipAuth, - Timeout: api.Timeout, - } - } - - // Cache - config.Cache.Expiry = yml.Cache.Expiry.Any - config.Cache.ExpiryUnused = yml.Cache.Expiry.Unused - config.Cache.ExpiryOffline = yml.Cache.Expiry.Offline - - // Log - config.Log.Audit = yml.Log.Audit - config.Log.Error = yml.Log.Error - - // Policies - config.Policies = make(map[string]Policy, len(yml.Policies)) - for name, policy := range yml.Policies { - config.Policies[name] = Policy{ - Allow: policy.Allow, - Deny: policy.Deny, - Identities: policy.Identities, - } - } - - // Keys - for _, key := range yml.Keys { - config.Keys = append(config.Keys, Key{ - Name: key.Name, - }) - } - - // Keystore - switch { - case yml.KeyStore.Fs.Path.Value != "": - config.KMS = new(FSConfig) - case len(yml.KeyStore.KES.Endpoint) != 0: - config.KMS = new(KESConfig) - case yml.KeyStore.Generic.Endpoint.Value != "": - config.KMS = new(KMSPluginConfig) - case yml.KeyStore.Vault.Endpoint.Value != "": - config.KMS = new(VaultConfig) - case yml.KeyStore.Fortanix.SDKMS.Endpoint.Value != "": - config.KMS = new(FortanixConfig) - case yml.KeyStore.Aws.SecretsManager.Endpoint.Value != "": - config.KMS = new(SecretsManagerConfig) - case yml.KeyStore.GCP.SecretManager.ProjectID.Value != "": - config.KMS = new(SecretManagerConfig) - case yml.KeyStore.Azure.KeyVault.Endpoint.Value != "": - config.KMS = new(KeyVaultConfig) - case yml.KeyStore.Gemalto.KeySecure.Endpoint.Value != "": - config.KMS = new(KeySecureConfig) - } - config.KMS.fromYAML(yml) - return config -} diff --git a/kms/example_test.go b/kms/example_test.go deleted file mode 100644 index 29e9307a..00000000 --- a/kms/example_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package kms_test - -import ( - "fmt" - "log" -) - -func ExampleIter() { - names := []string{ - "key-1", - "key-2", - "key-3", - } - iter := &Iter{ - Values: names, - } - defer iter.Close() // Make sure we close the iterator - - // Loop over the Iter until Next returns false. - // Then we either reached EOF or encountered an - // error. - for iter.Next() { - fmt.Println(iter.Name()) - } - - // Check whether we encountered an error while - // iterating or encounter an error when closing - // the iterator. - if err := iter.Close(); err != nil { - log.Fatalln(err) - } - // Output: key-1 - // key-2 - // key-3 -} - -type Iter struct { - Values []string - name string - closed bool -} - -func (i *Iter) Next() bool { - if i.closed || len(i.Values) == 0 { - return false - } - i.name = i.Values[0] - i.Values = i.Values[1:] - return true -} - -func (i *Iter) Name() string { return i.name } - -func (i *Iter) Close() error { - i.closed, i.name = true, "" - return nil -} diff --git a/kms/kms.go b/kms/kms.go deleted file mode 100644 index 86b48524..00000000 --- a/kms/kms.go +++ /dev/null @@ -1,197 +0,0 @@ -package kms - -import ( - "context" - "errors" - "net" - "time" -) - -// Conn is a connection to a KMS. -// -// Multiple goroutines may invoke methods -// on a Conn simultaneously. -type Conn interface { - // Status returns the current state of the - // Conn or an error explaining why fetching - // the State failed. - // - // Status returns Unreachable when it fails - // to reach the KMS. - // It returns Unavailable when it successfully - // reaches the KMS but the KMS seems to cannot - // process some or any requests. One example, - // would be a KMS that listens and accepts - // network requests but hasn't been initialized. - Status(context.Context) (State, error) - - // Create creates a new name-value entry at - // the KMS if and only if no entry for the - // given name exists. - // - // If such an entry already exists, Create - // returns kes.ErrKeyExists. - Create(ctx context.Context, name string, value []byte) error - - // Get returns the value for the given name or - // an error explaining why fetching the value - // from the KMS failed. - // - // If no entry for the given name exists, Get - // returns kes.ErrKeyNotFound. - Get(ctx context.Context, name string) ([]byte, error) - - // Delete deletes the specified entry at the KMS. - // - // If no entry for the given name exists, Delete - // returns kes.ErrKeyNotFound. - Delete(ctx context.Context, name string) error - - // List returns an iterator over the entries - // at the KMS. - // - // The returned Iter stops fetching entries - // from the KMS once ctx.Done() returns. - List(context.Context) (Iter, error) -} - -// Iter is an iterator over entries at a KMS. -type Iter interface { - // Next fetches the next entry from the KMS. - // It returns false when there are no more entries - // or once it encounters an error. - // - // Once Next returns false, it returns false on any - // subsequent Next call. - Next() bool - - // Name returns the name of the latest fetched entry. - // It returns the same name until Next is called again. - // - // As long as Next hasn't been called once or once Next - // returns false, Name returns the empty string. - Name() string - - // Close closes the Iter. Once closed, any subsequent - // Next call returns false. - // - // Close returns the first error encountered while iterating - // over the entires, if any. Otherwise, it returns the error - // encountered while cleaning up any resources, if any. - // Subsequent calls to Close return the same error. - Close() error -} - -// FuseIter wraps iter and returns an Iter that -// guarantees: -// - Next always returns false once it gets closed or -// encounters an error. -// - Name always returns the empty string once it gets -// closed or encounters an error. -// - Close closes the underlying Iter and always returns -// the same error on any subsequent call. -func FuseIter(iter Iter) Iter { return &fuseIter{iter: iter} } - -type fuseIter struct { - iter Iter - - closed bool - err error -} - -func (f *fuseIter) Next() bool { - if f.closed || f.err != nil { - return false - } - return f.iter.Next() -} - -func (f *fuseIter) Name() string { - if f.closed || f.err != nil { - return "" - } - return f.iter.Name() -} - -func (f *fuseIter) Close() error { - f.closed = true - if err := f.iter.Close(); f.err == nil { - f.err = err - } - return f.err -} - -// State is a structure describing the state of -// a KMS Conn. -type State struct { - // Latency is the connection latency. - Latency time.Duration -} - -// Unreachable is an error that indicates that the -// KMS is not reachable - for example due to a -// a network error. -type Unreachable struct { - Err error -} - -// IsUnreachable reports whether err is an Unreachable -// error. If IsUnreachable returns true it returns err -// as Unreachable error. -func IsUnreachable(err error) (*Unreachable, bool) { - var u *Unreachable - if errors.As(err, &u) { - return u, true - } - return nil, false -} - -func (e *Unreachable) Error() string { - if e.Err == nil { - return "kms is unreachable" - } - return "kms is unreachable: " + e.Err.Error() -} - -// Unwrap returns the Unreachable's underlying error, -// if any. -func (e *Unreachable) Unwrap() error { return e.Err } - -// Timeout reports whether the Unreachable error -// is caused by a network timeout. -func (e *Unreachable) Timeout() bool { - if err, ok := e.Err.(net.Error); ok { - return err.Timeout() - } - return false -} - -// Unavailable is an error that indicates that the -// KMS is reachable over the network but not ready -// to process requests - e.g. the KMS might not be -// initialized. -type Unavailable struct { - Err error -} - -// IsUnavailable reports whether err is an Unavailable -// error. If IsUnavailable returns true it returns err -// as Unavailable error. -func IsUnavailable(err error) (*Unavailable, bool) { - var u *Unavailable - if errors.As(err, &u) { - return u, true - } - return nil, false -} - -func (e *Unavailable) Error() string { - if e.Err == nil { - return "kms is not available" - } - return "kms is not available: " + e.Err.Error() -} - -// Unwrap returns the Unavailable's underlying error, -// if any. -func (e *Unavailable) Unwrap() error { return e.Err } diff --git a/kv/example_test.go b/kv/example_test.go new file mode 100644 index 00000000..7490348e --- /dev/null +++ b/kv/example_test.go @@ -0,0 +1,53 @@ +// Copyright 2023 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package kv_test + +import ( + "fmt" + "log" + + "github.com/minio/kes/kv" +) + +func ExampleIter() { + iter := SliceIter("Hello", "World", "!") + defer iter.Close() + + for v, ok := iter.Next(); ok; v, ok = iter.Next() { + fmt.Println(v) + } + if err := iter.Close(); err != nil { + log.Fatalln(err) + } + // Output: + // Hello + // World + // ! +} + +func SliceIter[T any](v ...T) kv.Iter[T] { + return &iter[T]{ + values: v, + } +} + +type iter[T any] struct { + values []T + off int + closed bool +} + +func (i *iter[T]) Next() (v T, ok bool) { + if i.off < len(i.values) && !i.closed { + v, ok = i.values[i.off], true + i.off++ + } + return +} + +func (i *iter[T]) Close() error { + i.closed = true + return nil +} diff --git a/kv/iter.go b/kv/iter.go new file mode 100644 index 00000000..1b32fdde --- /dev/null +++ b/kv/iter.go @@ -0,0 +1,39 @@ +// Copyright 2023 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package kv + +// An Iter traverses a list of elements. +// +// Its Next method returns the next +// element as long as there is one. +// +// Closing an Iter causes Next to return +// false and releases associated resources. +// +// A common use of an Iter is a for loop: +// +// for v, ok := iter.Next(); ok; v, ok = iter.Next() { +// _ = v +// } +// if err := iter.Close() { +// // release resources and handle potential errors +// } +type Iter[T any] interface { + // Next returns the next element, if any, + // and reports whether there may be more + // elements (true) or whether the end of + // the Iter has been reached (false). + // + // Once Next returns false, Close returns + // the first error encountered, if any. + Next() (T, bool) + + // Close stops the Iter and releases + // associated resources. + // + // Once closed, Next no longer returns + // elements but reports false. + Close() error +} diff --git a/kv/store.go b/kv/store.go index 4bf3cfbe..defcfc92 100644 --- a/kv/store.go +++ b/kv/store.go @@ -77,34 +77,6 @@ type Store[K comparable, V any] interface { List(context.Context) (Iter[K], error) } -// Iter iterates over a list of keys. -// -// Its Next method returns the next key -// and a bool indicating whether there -// are more entries. -// -// An Iter should be closed to release -// associated resources. -type Iter[K any] interface { - // Next returns the next entry, if any, - // and a bool indicating whether the - // end of the Iter has been reached. - // - // After Next returns false, the Close - // method returns any error occurred - // while iterating. - Next() (K, bool) - - // Close closes the Iter and releases - // associated resources. - // - // It returns either the first error - // encountered during iterating, if - // any, or any error that occurrs in - // the closing process. - Close() error -} - // State describes the state of a Store. type State struct { // Latency is the connection latency