diff --git a/cmd/kes/migrate.go b/cmd/kes/migrate.go index cb486def..07033cac 100644 --- a/cmd/kes/migrate.go +++ b/cmd/kes/migrate.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "io" "os" "os/signal" "path/filepath" @@ -16,8 +17,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/kesconf" flag "github.com/spf13/pflag" "golang.org/x/term" ) @@ -86,26 +87,15 @@ func migrateCmd(args []string) { ctx, cancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) defer cancel() - file, err := os.Open(fromPath) + sourceConfig, err := kesconf.ReadFile(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() - - file, err = os.Open(toPath) - if err != nil { - cli.Fatalf("failed to read '--to' config file: %v", err) - } - targetConfig, err := edge.ReadServerConfigYAML(file) + targetConfig, err := kesconf.ReadFile(toPath) if err != nil { cli.Fatalf("failed to read '--to' config file: %v", err) } - file.Close() src, err := sourceConfig.KeyStore.Connect(ctx) if err != nil { @@ -123,9 +113,8 @@ func migrateCmd(args []string) { defer uiTicker.Stop() // Now, we start listing the keys at the source. - iterator, err := src.List(ctx) - if err != nil { - cli.Fatal(err) + iterator := &kes.ListIter[string]{ + NextFunc: src.List, } // Then, we start the UI which prints how many keys have @@ -145,10 +134,15 @@ func migrateCmd(args []string) { // Finally, we start the actual migration. for { - name, ok := iterator.Next() - if !ok { + name, err := iterator.Next(ctx) + if err == io.EOF { break } + if err != nil { + quiet.ClearLine() + cli.Fatalf("failed to migrate %q: %v\nMigrated keys: %d", name, err, atomic.LoadUint64(&n)) + } + if ok, _ := filepath.Match(pattern, name); !ok { continue } @@ -176,10 +170,6 @@ func migrateCmd(args []string) { } atomic.AddUint64(&n, 1) } - if err = iterator.Close(); err != nil { - quiet.ClearLine() - cli.Fatalf("failed to list keys: %v\nMigrated keys: %d", err, atomic.LoadUint64(&n)) - } cancel() // At the end we show how many keys we have migrated successfully. diff --git a/cmd/kes/server.go b/cmd/kes/server.go index db2793e8..a6231adf 100644 --- a/cmd/kes/server.go +++ b/cmd/kes/server.go @@ -6,16 +6,21 @@ package main import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" "encoding/hex" + "encoding/pem" "errors" "fmt" "log/slog" + "math/big" "net" "os" "os/signal" - "path/filepath" "runtime" "slices" "strings" @@ -25,11 +30,9 @@ import ( tui "github.com/charmbracelet/lipgloss" "github.com/minio/kes" kesdk "github.com/minio/kes-go" - "github.com/minio/kes/edge" "github.com/minio/kes/internal/cli" - "github.com/minio/kes/internal/https" "github.com/minio/kes/internal/sys" - "github.com/minio/kes/kv" + "github.com/minio/kes/kesconf" flag "github.com/spf13/pflag" ) @@ -37,43 +40,33 @@ const serverCmdUsage = `Usage: kes server [options] Options: - --addr The address of the server (default: 0.0.0.0:7373) - --config Path to the server configuration file - - --key Path to the TLS private key. It takes precedence over - the config file - --cert Path to the TLS certificate. It takes precedence over - the config file - - --auth {on|off} Controls how the server handles mTLS authentication. - By default, the server requires a client certificate - and verifies that certificate has been issued by a - trusted CA. - Valid options are: - Require and verify : --auth=on (default) - Require but don't verify: --auth=off + --addr <[ip]:port> The network interface the KES server will listen on. + The default is '0.0.0.0:7373' which causes the server + to listen on all available network interfaces. + + --config Path to the KES server config file. + + --dev Start the KES server in development mode. The server + uses a volatile in-memory key store. -h, --help Show list of command-line options -Starts a KES server. The server address can be specified in the config file but -may be overwritten by the --addr flag. If omitted the IP defaults to 0.0.0.0 and -the PORT to 7373. -The client TLS verification can be disabled by setting --auth=off. The server then -accepts arbitrary client certificates but still maps them to policies. So, it disables -authentication but not authorization. +MinIO KES is a high-performance distributed key management server. +It is a stateless, self-contained server that uses a separate key +store as persistence layer. KES servers can be added or removed at +any point in time to scale out infinitely. + Quick Start: https://github.com/minio/kes#quick-start + Docs: https://min.io/docs/kes/ + Examples: - $ kes server --config config.yml --auth =off -` + 1. Start a new KES server on '127.0.0.1:7373' in development mode. + $ kes server --dev -type serverArgs struct { - Address string - ConfigFile string - PrivateKey string - Certificate string - TLSAuth string -} + 2. Start a new KES server with a confg file on '127.0.0.1:7000'. + $ kes server --addr :7000 --config ./kes/config.yml +` func serverCmd(args []string) { cmd := flag.NewFlagSet(args[0], flag.ContinueOnError) @@ -85,12 +78,14 @@ func serverCmd(args []string) { tlsKeyFlag string tlsCertFlag string mtlsAuthFlag string + devFlag bool ) cmd.StringVar(&addrFlag, "addr", "", "The address of the server") cmd.StringVar(&configFlag, "config", "", "Path to the server configuration file") cmd.StringVar(&tlsKeyFlag, "key", "", "Path to the TLS private key") cmd.StringVar(&tlsCertFlag, "cert", "", "Path to the TLS certificate") cmd.StringVar(&mtlsAuthFlag, "auth", "", "Controls how the server handles mTLS authentication") + cmd.BoolVar(&devFlag, "dev", false, "Start the KES server in development mode") if err := cmd.Parse(args[1:]); err != nil { if errors.Is(err, flag.ErrHelp) { os.Exit(2) @@ -98,37 +93,127 @@ func serverCmd(args []string) { cli.Fatalf("%v. See 'kes server --help'", err) } + warnPrefix := tui.NewStyle().Foreground(tui.Color("#ac0000")).Render("WARNING:") + if tlsKeyFlag != "" { + fmt.Fprintln(os.Stderr, warnPrefix, "'--key' flag is deprecated and no longer honored. Specify the private key in the config file") + } + if tlsCertFlag != "" { + fmt.Fprintln(os.Stderr, warnPrefix, "'--cert' flag is deprecated and no longer honored. Specify the certificate in the config file") + } + if mtlsAuthFlag != "" { + fmt.Fprintln(os.Stderr, warnPrefix, "'--auth' flag is deprecated and no longer honored. Specify the client certificate verification in the config file") + } + if cmd.NArg() > 0 { cli.Fatal("too many arguments. See 'kes server --help'") } + if devFlag { + if addrFlag == "" { + addrFlag = "0.0.0.0:7373" + } + if configFlag != "" { + cli.Fatal("'--config' flag is not supported in development mode") + } + + if err := startDevServer(addrFlag); err != nil { + cli.Fatal(err) + } + return + } + + if err := startServer(addrFlag, configFlag); err != nil { + cli.Fatal(err) + } +} + +func startServer(addrFlag, configFlag string) error { var memLocked bool if runtime.GOOS == "linux" { memLocked = mlockall() == nil defer munlockall() } + info, err := sys.ReadBinaryInfo() + if err != nil { + return err + } + + host, port, err := net.SplitHostPort(addrFlag) + if err != nil { + return err + } + ip := net.IPv4zero + if host != "" { + if ip = net.ParseIP(host); ip == nil { + return fmt.Errorf("'%s' is not a valid IP address", host) + } + } + ifaceIPs, err := lookupInterfaceIPs(ip) + if err != nil { + return err + } + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() - addr, config, err := readServerConfig(ctx, serverArgs{ - Address: addrFlag, - ConfigFile: configFlag, - PrivateKey: tlsKeyFlag, - Certificate: tlsCertFlag, - TLSAuth: mtlsAuthFlag, - }) + file, err := kesconf.ReadFile(configFlag) if err != nil { - cli.Fatal(err) + return err + } + if addrFlag == "" { + addrFlag = file.Addr } + conf, err := file.Config(ctx) + if err != nil { + return err + } + defer conf.Keys.Close() srv := &kes.Server{} - srv.ErrLevel.Set(slog.LevelWarn) - + if file.Log != nil { + srv.ErrLevel.Set(file.Log.ErrLevel) + srv.AuditLevel.Set(file.Log.AuditLevel) + } sighup := make(chan os.Signal, 10) signal.Notify(sighup, syscall.SIGHUP) defer signal.Stop(sighup) + startupMessage := func(conf *kes.Config) *strings.Builder { + blue := tui.NewStyle().Foreground(tui.Color("#268BD2")) + faint := tui.NewStyle().Faint(true) + + buf := &strings.Builder{} + fmt.Fprintf(buf, "%-33s %-23s %s\n", blue.Render("Version"), info.Version, faint.Render("commit="+info.CommitID)) + fmt.Fprintf(buf, "%-33s %-23s %s\n", blue.Render("Runtime"), fmt.Sprintf("%s %s/%s", info.Runtime, runtime.GOOS, runtime.GOARCH), faint.Render("compiler="+info.Compiler)) + fmt.Fprintf(buf, "%-33s %-23s %s\n", blue.Render("License"), "AGPLv3", faint.Render("https://www.gnu.org/licenses/agpl-3.0.html")) + fmt.Fprintf(buf, "%-33s %-12s 2015-%d %s\n", blue.Render("Copyright"), "MinIO, Inc.", time.Now().Year(), faint.Render("https://min.io")) + fmt.Fprintln(buf) + fmt.Fprintf(buf, "%-33s %v\n", blue.Render("KMS"), conf.Keys) + fmt.Fprintf(buf, "%-33s · https://%s\n", blue.Render("API"), net.JoinHostPort(ifaceIPs[0].String(), port)) + for _, ifaceIP := range ifaceIPs[1:] { + fmt.Fprintf(buf, "%-11s · https://%s\n", " ", net.JoinHostPort(ifaceIP.String(), port)) + } + + fmt.Fprintln(buf) + fmt.Fprintf(buf, "%-33s https://min.io/docs/kes\n", blue.Render("Docs")) + + fmt.Fprintln(buf) + if _, err := hex.DecodeString(conf.Admin.String()); err == nil { + fmt.Fprintf(buf, "%-33s %s\n", blue.Render("Admin"), conf.Admin) + } else { + fmt.Fprintf(buf, "%-33s \n", blue.Render("Admin")) + } + fmt.Fprintf(buf, "%-33s error=stderr level=%s\n", blue.Render("Logs"), srv.ErrLevel.Level()) + if srv.AuditLevel.Level() <= slog.LevelInfo { + fmt.Fprintf(buf, "%-11s audit=stdout level=%s\n", " ", srv.AuditLevel.Level()) + } + if memLocked { + fmt.Fprintf(buf, "%-33s %s\n", blue.Render("MLock"), "enabled") + } + return buf + } + go func(ctx context.Context) { for { select { @@ -136,34 +221,35 @@ func serverCmd(args []string) { return case <-sighup: fmt.Fprintln(os.Stderr, "SIGHUP signal received. Reloading configuration...") - _, config, err := readServerConfig(ctx, serverArgs{ - Address: addrFlag, - ConfigFile: configFlag, - PrivateKey: tlsKeyFlag, - Certificate: tlsCertFlag, - TLSAuth: mtlsAuthFlag, - }) + + file, err := kesconf.ReadFile(configFlag) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to reload server config: %v\n", err) + continue + } + config, err := file.Config(ctx) if err != nil { fmt.Fprintf(os.Stderr, "Failed to reload server config: %v\n", err) continue } - config.Keys = &kes.MemKeyStore{} closer, err := srv.Update(config) if err != nil { fmt.Fprintf(os.Stderr, "Failed to update server configuration: %v\n", err) continue } + if file.Log != nil { + srv.ErrLevel.Set(file.Log.ErrLevel) + srv.AuditLevel.Set(file.Log.AuditLevel) + } if err = closer.Close(); err != nil { fmt.Fprintf(os.Stderr, "Failed to close previous keystore connections: %v\n", err) } - buf, err := printServerStartup(srv, addrFlag, config, memLocked) - if err == nil { - fmt.Fprintln(buf) - fmt.Fprintln(buf, "=> Reloading configuration after SIGHUP signal completed.") - fmt.Println(buf.String()) - } + buf := startupMessage(config) + fmt.Fprintln(buf) + fmt.Fprintln(buf, "=> Reloading configuration after SIGHUP signal completed.") + fmt.Println(buf.String()) } } }(ctx) @@ -177,60 +263,82 @@ func serverCmd(args []string) { case <-ctx.Done(): return case <-ticker.C: - config, err := readServerTLSConfig(serverArgs{ - Address: addrFlag, - ConfigFile: configFlag, - PrivateKey: tlsKeyFlag, - Certificate: tlsCertFlag, - TLSAuth: mtlsAuthFlag, - }) + file, err := kesconf.ReadFile(configFlag) if err != nil { fmt.Fprintf(os.Stderr, "Failed to reload TLS configuration: %v\n", err) continue } - if err = srv.UpdateTLS(config); err != nil { + conf, err := file.TLSConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to reload TLS configuration: %v\n", err) + continue + } + if err = srv.UpdateTLS(conf); err != nil { fmt.Fprintf(os.Stderr, "Failed to update TLS configuration: %v\n", err) } } } }(ctx) - buf, err := printServerStartup(srv, addr, config, memLocked) - if err != nil { - cli.Fatal(err) - } + buf := startupMessage(conf) fmt.Fprintln(buf) fmt.Fprintln(buf, "=> Server is up and running...") fmt.Println(buf.String()) - if err = srv.ListenAndStart(ctx, addrFlag, config); err != nil { - cli.Fatal(err) + if err = srv.ListenAndStart(ctx, addrFlag, conf); err != nil { + return err } fmt.Println("\n=> Stopping server... Goodbye.") + return nil } -func printServerStartup(srv *kes.Server, addr string, config *kes.Config, memLocked bool) (*strings.Builder, error) { +func startDevServer(addr string) error { info, err := sys.ReadBinaryInfo() if err != nil { - return nil, err + return err } host, port, err := net.SplitHostPort(addr) if err != nil { - return nil, err + return err } ip := net.IPv4zero if host != "" { if ip = net.ParseIP(host); ip == nil { - return nil, fmt.Errorf("'%s' is not a valid IP address", host) + return fmt.Errorf("'%s' is not a valid IP address", host) } } ifaceIPs, err := lookupInterfaceIPs(ip) if err != nil { - return nil, err + return err + } + srvCert, err := generateDevServerCertificate(ifaceIPs...) + if err != nil { + return err + } + + apiKey, err := kesdk.GenerateAPIKey(nil) + if err != nil { + return err } - keys := config.Keys.(adapter) + tlsConf := &tls.Config{ + MinVersion: tls.VersionTLS12, + NextProtos: []string{"h2", "http/1.1"}, + Certificates: []tls.Certificate{srvCert}, + ClientAuth: tls.RequestClientCert, + } + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + conf := &kes.Config{ + Admin: apiKey.Identity(), + TLS: tlsConf, + Cache: &kes.CacheConfig{}, + Keys: &kes.MemKeyStore{}, + } + srv := &kes.Server{} blue := tui.NewStyle().Foreground(tui.Color("#268BD2")) faint := tui.NewStyle().Faint(true) @@ -241,351 +349,27 @@ func printServerStartup(srv *kes.Server, addr string, config *kes.Config, memLoc fmt.Fprintf(buf, "%-33s %-23s %s\n", blue.Render("License"), "AGPLv3", faint.Render("https://www.gnu.org/licenses/agpl-3.0.html")) fmt.Fprintf(buf, "%-33s %-12s 2015-%d %s\n", blue.Render("Copyright"), "MinIO, Inc.", time.Now().Year(), faint.Render("https://min.io")) fmt.Fprintln(buf) - fmt.Fprintf(buf, "%-33s %s: %s\n", blue.Render("KMS"), keys.Type, keys.Endpoint) + fmt.Fprintf(buf, "%-33s %v\n", blue.Render("KMS"), conf.Keys) fmt.Fprintf(buf, "%-33s · https://%s\n", blue.Render("API"), net.JoinHostPort(ifaceIPs[0].String(), port)) for _, ifaceIP := range ifaceIPs[1:] { fmt.Fprintf(buf, "%-11s · https://%s\n", " ", net.JoinHostPort(ifaceIP.String(), port)) } - fmt.Fprintln(buf) fmt.Fprintf(buf, "%-33s https://min.io/docs/kes\n", blue.Render("Docs")) - fmt.Fprintln(buf) - if _, err := hex.DecodeString(config.Admin.String()); err == nil { - fmt.Fprintf(buf, "%-33s %s\n", blue.Render("Admin"), config.Admin) - } else { - fmt.Fprintf(buf, "%-33s \n", blue.Render("Admin")) - } + fmt.Fprintf(buf, "%-33s %s\n", blue.Render("API Key"), apiKey.String()) + fmt.Fprintf(buf, "%-33s %s\n", blue.Render("Admin"), apiKey.Identity()) fmt.Fprintf(buf, "%-33s error=stderr level=%s\n", blue.Render("Logs"), srv.ErrLevel.Level()) - if srv.AuditLevel.Level() <= slog.LevelInfo { - fmt.Fprintf(buf, "%-11s audit=stdout level=%s\n", " ", srv.AuditLevel.Level()) - } - if memLocked { - fmt.Fprintf(buf, "%-33s %s\n", blue.Render("MLock"), "enabled") - } - return buf, nil -} - -func readServerTLSConfig(args serverArgs) (*tls.Config, error) { - file, err := os.Open(args.ConfigFile) - if err != nil { - return nil, err - } - defer file.Close() - - config, err := edge.ReadServerConfigYAML(file) - if err != nil { - return nil, fmt.Errorf("failed to read config file: %v", err) - } - if err = file.Close(); err != nil { - return nil, err - } - if args.PrivateKey != "" { - config.TLS.PrivateKey = args.PrivateKey - } - if args.Certificate != "" { - config.TLS.Certificate = args.Certificate - } - if args.PrivateKey != "" { - config.TLS.PrivateKey = args.PrivateKey - } - if args.Certificate != "" { - config.TLS.Certificate = args.Certificate - } - - certificate, err := https.CertificateFromFile(config.TLS.Certificate, config.TLS.PrivateKey, config.TLS.Password) - if err != nil { - return nil, fmt.Errorf("failed to read TLS certificate: %v", err) - } - if certificate.Leaf != nil { - if len(certificate.Leaf.DNSNames) == 0 && len(certificate.Leaf.IPAddresses) == 0 { - // Support for TLS certificates with a subject CN but without any SAN - // has been removed in Go 1.15. Ref: https://go.dev/doc/go1.15#commonname - // Therefore, we require at least one SAN for the server certificate. - return nil, fmt.Errorf("invalid TLS certificate: certificate does not contain any DNS or IP address as SAN") - } - } - - var rootCAs *x509.CertPool - if config.TLS.CAPath != "" { - rootCAs, err = https.CertPoolFromFile(config.TLS.CAPath) - if err != nil { - return nil, fmt.Errorf("failed to read TLS CA certificates: %v", err) - } - } - return &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{certificate}, - RootCAs: rootCAs, - ClientAuth: tls.RequestClientCert, - }, nil -} - -func readServerConfig(ctx context.Context, args serverArgs) (string, *kes.Config, error) { - file, err := os.Open(args.ConfigFile) - if err != nil { - return "", nil, err - } - defer file.Close() - - config, err := edge.ReadServerConfigYAML(file) - if err != nil { - return "", nil, fmt.Errorf("failed to read config file: %v", err) - } - if err = file.Close(); err != nil { - return "", nil, err - } - - if args.Address != "" { - config.Addr = args.Address - } - if args.PrivateKey != "" { - config.TLS.PrivateKey = args.PrivateKey - } - if args.Certificate != "" { - config.TLS.Certificate = args.Certificate - } - - // Set config defaults - if config.Addr == "" { - config.Addr = "0.0.0.0:7373" - } - if config.Cache.Expiry == 0 { - config.Cache.Expiry = 5 * time.Minute - } - if config.Cache.ExpiryUnused == 0 { - config.Cache.ExpiryUnused = 30 * time.Second - } - - // Verify config - if config.Admin.IsUnknown() { - return "", nil, errors.New("no admin identity specified") - } - if config.TLS.PrivateKey == "" { - return "", nil, errors.New("no TLS private key specified") - } - if config.TLS.Certificate == "" { - return "", nil, errors.New("no TLS certificate specified") - } - - certificate, err := https.CertificateFromFile(config.TLS.Certificate, config.TLS.PrivateKey, config.TLS.Password) - if err != nil { - return "", nil, fmt.Errorf("failed to read TLS certificate: %v", err) - } - if certificate.Leaf != nil { - if len(certificate.Leaf.DNSNames) == 0 && len(certificate.Leaf.IPAddresses) == 0 { - // Support for TLS certificates with a subject CN but without any SAN - // has been removed in Go 1.15. Ref: https://go.dev/doc/go1.15#commonname - // Therefore, we require at least one SAN for the server certificate. - return "", nil, fmt.Errorf("invalid TLS certificate: certificate does not contain any DNS or IP address as SAN") - } - } - - var rootCAs *x509.CertPool - if config.TLS.CAPath != "" { - rootCAs, err = https.CertPoolFromFile(config.TLS.CAPath) - if err != nil { - return "", nil, fmt.Errorf("failed to read TLS CA certificates: %v", err) - } - } - - var errorLog slog.Handler - var auditLog kes.AuditHandler - if config.Log.Error { - errorLog = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}) - } - if config.Log.Audit { - auditLog = &kes.AuditLogHandler{Handler: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})} - } - - // TODO(aead): support TLS proxies - - var apiConfig map[string]kes.RouteConfig - if config.API != nil && len(config.API.Paths) > 0 { - apiConfig = make(map[string]kes.RouteConfig, len(config.API.Paths)) - for k, v := range config.API.Paths { - k = strings.TrimSpace(k) // Ensure that the API path starts with a '/' - if !strings.HasPrefix(k, "/") { - k = "/" + k - } - - if _, ok := apiConfig[k]; ok { - return "", nil, fmt.Errorf("ambiguous API configuration for '%s'", k) - } - apiConfig[k] = kes.RouteConfig{ - Timeout: v.Timeout, - InsecureSkipAuth: v.InsecureSkipAuth, - } - } - } - - policies := make(map[string]kes.Policy, len(config.Policies)) - for name, policy := range config.Policies { - p := kes.Policy{ - Allow: make(map[string]kesdk.Rule, len(policy.Allow)), - Deny: make(map[string]kesdk.Rule, len(policy.Deny)), - Identities: slices.Clone(policy.Identities), - } - for _, pattern := range policy.Allow { - p.Allow[pattern] = kesdk.Rule{} - } - for _, pattern := range policy.Deny { - p.Deny[pattern] = kesdk.Rule{} - } - policies[name] = p - } - - kmsKind, kmsEndpoint, err := description(config) - if err != nil { - return "", nil, err - } - - store, err := config.KeyStore.Connect(ctx) - if err != nil { - return "", nil, err - } - - keys := adapter{ - store: store, - Type: kmsKind, - Endpoint: kmsEndpoint, - } - - return config.Addr, &kes.Config{ - Admin: config.Admin, - TLS: &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{certificate}, - RootCAs: rootCAs, - ClientAuth: tls.RequestClientCert, - }, - Cache: &kes.CacheConfig{ - Expiry: config.Cache.Expiry, - ExpiryUnused: config.Cache.ExpiryUnused, - ExpiryOffline: config.Cache.ExpiryOffline, - }, - Keys: keys, - Policies: policies, - Routes: apiConfig, - ErrorLog: errorLog, - AuditLog: auditLog, - }, nil -} - -// TODO(aead): temp adapater - remove once keystores are ported to KeyStore interface -type adapter struct { - Type string - - Endpoint string - - store kv.Store[string, []byte] -} - -func (a adapter) Status(ctx context.Context) (kes.KeyStoreState, error) { - s, err := a.store.Status(ctx) - if err != nil { - return kes.KeyStoreState{}, err - } - return kes.KeyStoreState{ - Latency: s.Latency, - }, nil -} - -func (a adapter) Create(ctx context.Context, name string, value []byte) error { - return a.store.Create(ctx, name, value) -} - -func (a adapter) Delete(ctx context.Context, name string) error { - return a.store.Delete(ctx, name) -} - -func (a adapter) Get(ctx context.Context, name string) ([]byte, error) { - return a.store.Get(ctx, name) -} - -func (a adapter) List(ctx context.Context, prefix string, n int) ([]string, string, error) { - if n == 0 { - return []string{}, prefix, nil - } - - iter, err := a.store.List(ctx) - if err != nil { - return nil, "", err - } - defer iter.Close() - - var keys []string - for key, ok := iter.Next(); ok; key, ok = iter.Next() { - keys = append(keys, key) - } - if err = iter.Close(); err != nil { - return nil, "", err - } - slices.Sort(keys) - - if prefix == "" { - if n < 0 || n >= len(keys) { - return keys, "", nil - } - return keys[:n], keys[n], nil - } - - i := slices.IndexFunc(keys, func(key string) bool { return strings.HasPrefix(key, prefix) }) - if i < 0 { - return []string{}, "", nil - } - - for j, key := range keys[i:] { - if !strings.HasPrefix(key, prefix) { - return keys[i : i+j], "", nil - } - if n > 0 && j == n { - return keys[i : i+j], key, nil - } - } - return keys[i:], "", nil -} - -func (a adapter) Close() error { return a.store.Close() } + fmt.Fprintf(buf, "%-11s audit=stdout level=%s\n", " ", srv.AuditLevel.Level()) + fmt.Fprintln(buf) + fmt.Fprintln(buf, "=> Server is up and running...") + fmt.Println(buf.String()) -func description(config *edge.ServerConfig) (kind string, endpoint string, err error) { - if config.KeyStore == nil { - return "", "", errors.New("no KMS backend specified") + if err := srv.ListenAndStart(ctx, addr, conf); err != nil { + return err } - - switch kms := config.KeyStore.(type) { - case *edge.FSKeyStore: - kind = "Filesystem" - if abs, err := filepath.Abs(kms.Path); err == nil { - endpoint = abs - } else { - endpoint = kms.Path - } - case *edge.VaultKeyStore: - kind = "Hashicorp Vault" - endpoint = kms.Endpoint - case *edge.FortanixKeyStore: - kind = "Fortanix SDKMS" - endpoint = kms.Endpoint - case *edge.AWSSecretsManagerKeyStore: - kind = "AWS SecretsManager" - endpoint = kms.Endpoint - case *edge.KeySecureKeyStore: - kind = "Gemalto KeySecure" - endpoint = kms.Endpoint - case *edge.GCPSecretManagerKeyStore: - kind = "GCP SecretManager" - endpoint = "Project: " + kms.ProjectID - case *edge.AzureKeyVaultKeyStore: - kind = "Azure KeyVault" - endpoint = kms.Endpoint - case *edge.EntrustKeyControlKeyStore: - kind = "Entrust KeyControl" - endpoint = kms.Endpoint - default: - return "", "", fmt.Errorf("unknown KMS backend %T", kms) - } - return kind, endpoint, nil + fmt.Println("\n=> Stopping server... Goodbye.") + return nil } // lookupInterfaceIPs returns a list of IP addrs for which a listener @@ -644,3 +428,51 @@ func lookupInterfaceIPs(listenerIP net.IP) ([]net.IP, error) { } return nil, errors.New("no IPv4 or IPv6 addresses available") } + +func generateDevServerCertificate(ipSANs ...net.IP) (tls.Certificate, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return tls.Certificate{}, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "localhost", + }, + NotBefore: time.Now().UTC(), + NotAfter: time.Now().UTC().Add(90 * 24 * time.Hour), // 90 days + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + BasicConstraintsValid: true, + IPAddresses: ipSANs, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) + if err != nil { + return tls.Certificate{}, err + } + privPKCS8, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return tls.Certificate{}, err + } + cert, err := tls.X509KeyPair( + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}), + pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privPKCS8}), + ) + if err != nil { + return tls.Certificate{}, err + } + if cert.Leaf == nil { + cert.Leaf, _ = x509.ParseCertificate(cert.Certificate[0]) + } + return cert, nil +} diff --git a/edge/edge.go b/edge/edge.go deleted file mode 100644 index a8f5cd69..00000000 --- a/edge/edge.go +++ /dev/null @@ -1,36 +0,0 @@ -// 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 edge - -import ( - "fmt" - "io" - - "gopkg.in/yaml.v3" -) - -// ReadServerConfigYAML returns a new ServerConfig unmarshalled -// from the YAML read from r. -func ReadServerConfigYAML(r io.Reader) (*ServerConfig, error) { - var node yaml.Node - if err := yaml.NewDecoder(r).Decode(&node); err != nil { - return nil, err - } - - version, err := findVersion(&node) - if err != nil { - return nil, err - } - const Version = "v1" - if version != "" && version != Version { - return nil, fmt.Errorf("edge: invalid server config version '%s'", version) - } - - var y yml - if err := node.Decode(&y); err != nil { - return nil, err - } - return ymlToServerConfig(&y) -} diff --git a/edge/mem_test.go b/edge/mem_test.go deleted file mode 100644 index dee2617a..00000000 --- a/edge/mem_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// 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 edge_test - -import ( - "testing" - - "github.com/minio/kes/internal/keystore/mem" -) - -func TestInMem(t *testing.T) { - ctx, cancel := testingContext(t) - defer cancel() - - store := &mem.Store{} - - t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) -} diff --git a/internal/cache/cow.go b/internal/cache/cow.go index 201a1952..ed0ad7f8 100644 --- a/internal/cache/cow.go +++ b/internal/cache/cow.go @@ -216,7 +216,7 @@ func (c *Cow[K, V]) Clone() *Cow[K, V] { // It never returns nil. func (c *Cow[K, _]) Keys() []K { p := c.ptr.Load() - if len(*p) == 0 { + if p == nil || len(*p) == 0 { return []K{} } diff --git a/internal/keystore/aws/secrets-manager.go b/internal/keystore/aws/secrets-manager.go index edcb2937..a2821fae 100644 --- a/internal/keystore/aws/secrets-manager.go +++ b/internal/keystore/aws/secrets-manager.go @@ -16,8 +16,9 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "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/kv" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" + "github.com/minio/kes/internal/keystore" ) // Credentials represents static AWS credentials: @@ -100,21 +101,21 @@ type Store struct { client *secretsmanager.SecretsManager } -var _ kv.Store[string, []byte] = (*Store)(nil) +func (s *Store) String() string { return "AWS SecretsManager: " + s.config.Addr } // Status returns the current state of the AWS SecretsManager instance. // In particular, whether it is reachable and the network latency. -func (s *Store) Status(ctx context.Context) (kv.State, error) { +func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.client.Endpoint, nil) if err != nil { - return kv.State{}, err + return kes.KeyStoreState{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kv.State{}, &kv.Unreachable{Err: err} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} } - return kv.State{ + return kes.KeyStoreState{ Latency: time.Since(start), }, nil } @@ -141,7 +142,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { if err, ok := err.(awserr.Error); ok { switch err.Code() { case secretsmanager.ErrCodeResourceExistsException: - return kes.ErrKeyExists + return kesdk.ErrKeyExists } } return fmt.Errorf("aws: failed to create '%s': %v", name, err) @@ -175,7 +176,7 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { case secretsmanager.ErrCodeDecryptionFailure: return nil, fmt.Errorf("aws: cannot access '%s': %v", name, err) case secretsmanager.ErrCodeResourceNotFoundException: - return nil, kes.ErrKeyNotFound + return nil, kesdk.ErrKeyNotFound } } return nil, fmt.Errorf("aws: failed to read '%s': %v", name, err) @@ -210,7 +211,7 @@ func (s *Store) Delete(ctx context.Context, name string) error { } if err, ok := err.(awserr.Error); ok { if err.Code() == secretsmanager.ErrCodeResourceNotFoundException { - return kes.ErrKeyNotFound + return kesdk.ErrKeyNotFound } } return fmt.Errorf("aws: failed to delete '%s': %v", name, err) @@ -220,50 +221,33 @@ func (s *Store) Delete(ctx context.Context, name string) error { // List returns a new Iterator over the names of // all stored keys. -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) - err := s.client.ListSecretsPagesWithContext(ctx, &secretsmanager.ListSecretsInput{}, func(page *secretsmanager.ListSecretsOutput, lastPage bool) bool { - for _, secret := range page.SecretList { - values <- *secret.Name - } - - // The pagination is stopped once we return false. - // If lastPage is true then we reached the end. Therefore, - // we return !lastPage which then is false. - return !lastPage - }) - if err != nil { - cancel(err) +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. +// +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty. +func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { + var names []string + err := s.client.ListSecretsPagesWithContext(ctx, &secretsmanager.ListSecretsInput{}, func(page *secretsmanager.ListSecretsOutput, lastPage bool) bool { + for _, secret := range page.SecretList { + names = append(names, *secret.Name) } - }() - return &iter{ - ch: values, - ctx: ctx, - }, nil -} -// Close closes the Store. -func (s *Store) Close() error { return nil } - -type iter struct { - ch <-chan string - ctx context.Context -} - -func (i *iter) Next() (string, bool) { - select { - case v, ok := <-i.ch: - return v, ok - case <-i.ctx.Done(): - return "", false + // The pagination is stopped once we return false. + // If lastPage is true then we reached the end. Therefore, + // we return !lastPage which then is false. + return !lastPage + }) + if err != nil { + return nil, "", err } + return keystore.List(names, prefix, n) } -func (i *iter) Close() error { - return context.Cause(i.ctx) -} +// Close closes the Store. +func (s *Store) Close() error { return nil } diff --git a/internal/keystore/azure/key-vault.go b/internal/keystore/azure/key-vault.go index 75d7d560..a3ba7024 100644 --- a/internal/keystore/azure/key-vault.go +++ b/internal/keystore/azure/key-vault.go @@ -14,8 +14,9 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure/auth" - "github.com/minio/kes-go" - "github.com/minio/kes/kv" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" + "github.com/minio/kes/internal/keystore" ) // Credentials are Azure client credentials to authenticate an application @@ -41,21 +42,21 @@ type Store struct { client client } -var _ kv.Store[string, []byte] = (*Store)(nil) +func (s *Store) String() string { return "Azure KeyVault: " + s.endpoint } // Status returns the current state of the Azure KeyVault instance. // In particular, whether it is reachable and the network latency. -func (s *Store) Status(ctx context.Context) (kv.State, error) { +func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.client.Endpoint, nil) if err != nil { - return kv.State{}, err + return kes.KeyStoreState{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kv.State{}, &kv.Unreachable{Err: err} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} } - return kv.State{ + return kes.KeyStoreState{ Latency: time.Since(start), }, nil } @@ -89,7 +90,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { } switch { case stat.StatusCode == http.StatusOK: - return kes.ErrKeyExists + return kesdk.ErrKeyExists case stat.StatusCode == http.StatusForbidden && stat.ErrorCode == "ForbiddenByPolicy": return fmt.Errorf("azure: failed to create '%s': insufficient permissions to check whether '%s' already exists: %s (%s)", name, name, stat.Message, stat.ErrorCode) case stat.StatusCode != http.StatusNotFound: @@ -213,7 +214,7 @@ func (s *Store) Delete(ctx context.Context, name string) error { return fmt.Errorf("azure: failed to delete '%s': %v", name, err) } if stat.StatusCode != http.StatusNotFound { - return kes.ErrKeyNotFound + return kesdk.ErrKeyNotFound } if stat.StatusCode != http.StatusOK && stat.StatusCode != http.StatusNotFound { return fmt.Errorf("azure: failed to delete '%s': %s (%s)", name, stat.Message, stat.ErrorCode) @@ -274,7 +275,7 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { return nil, fmt.Errorf("azure: failed to get '%s': failed to list versions: %v", name, err) } if stat.StatusCode == http.StatusNotFound && stat.ErrorCode == "NoObjectVersions" { - return nil, kes.ErrKeyNotFound + return nil, kesdk.ErrKeyNotFound } if stat.StatusCode != http.StatusOK { return nil, fmt.Errorf("azure: failed to get '%s': failed to list versions: %s (%s)", name, stat.Message, stat.ErrorCode) @@ -295,48 +296,40 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { // List returns a new Iterator over the names of // all stored keys. -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 := s.client.ListSecrets(ctx, nextLink) - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - cancel(err) - break - } - if err != nil { - cancel(fmt.Errorf("azure: failed to list keys: %v", err)) - break - } - if status.StatusCode != http.StatusOK { - cancel(fmt.Errorf("azure: failed to list keys: %s (%s)", status.Message, status.ErrorCode)) - break - } +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. +// +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty +func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { + var ( + names []string + nextLink string + ) + for { + secrets, link, status, err := s.client.ListSecrets(ctx, nextLink) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil, "", err + } + if err != nil { + return nil, "", fmt.Errorf("azure: failed to list keys: %v", err) + } + if status.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("azure: failed to list keys: %s (%s)", status.Message, status.ErrorCode) + } - nextLink = link - for _, secret := range secrets { - select { - case values <- secret: - case <-ctx.Done(): - return - } - } - if nextLink == "" { - break - } + nextLink = link + names = append(names, secrets...) + if nextLink == "" { + break } - }() - return &iter{ - ch: values, - ctx: ctx, - cancel: cancel, - }, nil + } + return keystore.List(names, prefix, n) } // Close closes the Store. @@ -382,23 +375,3 @@ func ConnectWithIdentity(_ context.Context, endpoint string, msi ManagedIdentity }, }, nil } - -type iter struct { - ch <-chan string - ctx context.Context - cancel context.CancelCauseFunc -} - -func (i *iter) Next() (string, bool) { - select { - case v, ok := <-i.ch: - return v, ok - case <-i.ctx.Done(): - return "", false - } -} - -func (i *iter) Close() error { - i.cancel(context.Canceled) - return context.Cause(i.ctx) -} diff --git a/internal/keystore/entrust/keycontrol.go b/internal/keystore/entrust/keycontrol.go index 983ea264..65655387 100644 --- a/internal/keystore/entrust/keycontrol.go +++ b/internal/keystore/entrust/keycontrol.go @@ -21,9 +21,10 @@ import ( "time" "aead.dev/mem" - "github.com/minio/kes-go" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" xhttp "github.com/minio/kes/internal/http" - "github.com/minio/kes/kv" + "github.com/minio/kes/internal/keystore" ) // Config is a structure containing the Entrust KeyControl configuration. @@ -99,11 +100,11 @@ type KeyControl struct { stop context.CancelFunc } -var _ kv.Store[string, []byte] = (*KeyControl)(nil) +func (kc *KeyControl) String() string { return "Entrust KeyControl: " + kc.config.Endpoint } // Status returns the current state of the KeyControl instance. // In particular, whether it is reachable and the network latency. -func (kc *KeyControl) Status(ctx context.Context) (kv.State, error) { +func (kc *KeyControl) Status(ctx context.Context) (kes.KeyStoreState, error) { const ( Method = http.MethodPost Path = "/vault/1.0/GetBox/" @@ -116,15 +117,15 @@ func (kc *KeyControl) Status(ctx context.Context) (kv.State, error) { BoxID: kc.config.BoxID, }) if err != nil { - return kv.State{}, fmt.Errorf("keycontrol: failed to fetch status: %v", err) + return kes.KeyStoreState{}, fmt.Errorf("keycontrol: failed to fetch status: %v", err) } url, err := url.JoinPath(kc.config.Endpoint, Path) if err != nil { - return kv.State{}, fmt.Errorf("keycontrol: failed to fetch status: %v", err) + return kes.KeyStoreState{}, fmt.Errorf("keycontrol: failed to fetch status: %v", err) } req, err := http.NewRequestWithContext(ctx, Method, url, xhttp.RetryReader(bytes.NewReader(body))) if err != nil { - return kv.State{}, fmt.Errorf("keycontrol: failed to fetch status: %v", err) + return kes.KeyStoreState{}, fmt.Errorf("keycontrol: failed to fetch status: %v", err) } req.ContentLength = int64(len(body)) req.Header.Set(VaultToken, *kc.token.Load()) @@ -132,16 +133,16 @@ func (kc *KeyControl) Status(ctx context.Context) (kv.State, error) { start := time.Now() resp, err := kc.client.Do(req) if err != nil { - return kv.State{}, &kv.Unreachable{ + return kes.KeyStoreState{}, &keystore.ErrUnreachable{ Err: fmt.Errorf("keycontrol: failed to fetch status: %v", err), } } latency := time.Since(start) if resp.StatusCode != http.StatusOK { - return kv.State{}, parseErrorResponse(resp) + return kes.KeyStoreState{}, parseErrorResponse(resp) } - return kv.State{ + return kes.KeyStoreState{ Latency: latency, }, nil } @@ -289,28 +290,6 @@ func (kc *KeyControl) Delete(ctx context.Context, name string) error { return nil } -// List returns a new Iterator over the names of all stored keys. -func (kc *KeyControl) List(ctx context.Context) (kv.Iter[string], error) { - var ( - names []string - prefix string - err error - ) - for { - var ids []string - ids, prefix, err = kc.list(ctx, prefix, 250) - if err != nil { - return nil, err - } - names = append(names, ids...) - - if prefix == "" || len(ids) == 0 { - break - } - } - return &iter{names: names}, nil -} - // Close closes the KeyControl client. It stops any // authentication renewal in the background. func (kc *KeyControl) Close() error { @@ -318,7 +297,21 @@ func (kc *KeyControl) Close() error { return nil } -func (kc *KeyControl) list(ctx context.Context, prefix string, n int) ([]string, string, error) { +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. +// +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty. +func (kc *KeyControl) List(ctx context.Context, prefix string, n int) ([]string, string, error) { + const N = 256 + if n <= 0 { + n = N + } const ( Method = http.MethodPost Path = "/vault/1.0/ListSecretIds/" @@ -568,9 +561,9 @@ func parseErrorResponse(resp *http.Response) error { } switch { case resp.StatusCode == http.StatusConflict && response.Error == "Secret already exists": - return kes.ErrKeyExists + return kesdk.ErrKeyExists case resp.StatusCode == http.StatusNotFound && response.Error == "Secret not found": - return kes.ErrKeyNotFound + return kesdk.ErrKeyNotFound } return errors.New("keycontrol: " + response.Error) } @@ -580,19 +573,3 @@ func parseErrorResponse(resp *http.Response) error { } return errors.New("keycontrol: " + resp.Status + ": " + sb.String()) } - -type iter struct { - names []string -} - -func (i *iter) Next() (string, bool) { - if len(i.names) == 0 { - return "", false - } - - name := i.names[0] - i.names = i.names[1:] - return name, true -} - -func (i *iter) Close() error { return nil } diff --git a/internal/keystore/fortanix/keystore.go b/internal/keystore/fortanix/keystore.go index 587f472f..e41e4640 100644 --- a/internal/keystore/fortanix/keystore.go +++ b/internal/keystore/fortanix/keystore.go @@ -14,10 +14,8 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "net/http" - "net/url" "os" "path" "path/filepath" @@ -25,10 +23,11 @@ import ( "time" "aead.dev/mem" - "github.com/minio/kes-go" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" xhttp "github.com/minio/kes/internal/http" "github.com/minio/kes/internal/key" - "github.com/minio/kes/kv" + "github.com/minio/kes/internal/keystore" ) // APIKey is a Fortanix API key for authenticating to @@ -72,8 +71,6 @@ type Store struct { client xhttp.Retry } -var _ kv.Store[string, []byte] = (*Store)(nil) // compiler check - // Connect establishes and returns a Store to a Fortanix SDKMS server // using the given config. func Connect(ctx context.Context, config *Config) (*Store, error) { @@ -182,19 +179,21 @@ func Connect(ctx context.Context, config *Config) (*Store, error) { }, nil } +func (s *Store) String() string { return "Fortanix SDKMS: " + s.config.Endpoint } + // Status returns the current state of the Fortanix SDKMS instance. // In particular, whether it is reachable and the network latency. -func (s *Store) Status(ctx context.Context) (kv.State, error) { +func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.config.Endpoint, nil) if err != nil { - return kv.State{}, err + return kes.KeyStoreState{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kv.State{}, &kv.Unreachable{Err: err} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} } - return kv.State{ + return kes.KeyStoreState{ Latency: time.Since(start), }, nil } @@ -250,7 +249,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { case err == nil: return fmt.Errorf("fortanix: failed to create key '%s': %s (%q)", name, resp.Status, resp.StatusCode) case resp.StatusCode == http.StatusConflict && err.Error() == "sobject already exists": - return kes.ErrKeyExists + return kesdk.ErrKeyExists default: return fmt.Errorf("fortanix: failed to create key '%s': %v", name, err) } @@ -303,7 +302,7 @@ func (s *Store) Delete(ctx context.Context, name string) error { case err == nil: return fmt.Errorf("fortanix: failed to delete '%s': failed fetch key metadata: %s (%d)", name, resp.Status, resp.StatusCode) case resp.StatusCode == http.StatusNotFound && err.Error() == "sobject does not exist": - return kes.ErrKeyNotFound + return kesdk.ErrKeyNotFound default: return fmt.Errorf("fortanix: failed to delete '%s': failed to fetch key metadata: %v", name, err) } @@ -377,7 +376,7 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { case err == nil: return nil, fmt.Errorf("fortanix: failed to fetch '%s': %s (%d)", name, resp.Status, resp.StatusCode) case resp.StatusCode == http.StatusNotFound && err.Error() == "sobject does not exist": - return nil, kes.ErrKeyNotFound + return nil, kesdk.ErrKeyNotFound default: return nil, fmt.Errorf("fortanix: failed to fetch '%s': %v", name, err) } @@ -401,103 +400,61 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { return value, nil } -// List returns a new Iterator over the Fortanix SDKMS keys. +// List returns a new Iterator over the names of +// all stored keys. +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. // -// The returned iterator may or may not reflect any -// concurrent changes to the Fortanix SDKMS instance - i.e. -// creates or deletes. Further, it does not provide any -// ordering guarantees. -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 start string - for { - 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 { - cancel(fmt.Errorf("fortanix: failed to list keys: %v", err)) - return - } - req.Header.Set("Authorization", s.config.APIKey.String()) - - resp, err := s.client.Do(req) - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - cancel(err) - return - } - if err != nil { - cancel(fmt.Errorf("fortanix: failed to list keys: %v", err)) - return - } - - type Response struct { - Name string `json:"name"` - } - var keys []Response - if err := json.NewDecoder(mem.LimitReader(resp.Body, 10*key.MaxSize)).Decode(&keys); err != nil { - cancel(fmt.Errorf("fortanix: failed to list keys: failed to parse server response: %v", err)) - return - } - if len(keys) == 0 { - return - } - for _, k := range keys { - select { - case values <- k.Name: - case <-ctx.Done(): - return - } - } - start = url.QueryEscape(keys[len(keys)-1].Name) +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty. +func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { + var ( + names []string + start = prefix + ) + for { + reqURL := endpoint(s.config.Endpoint, "/crypto/v1/keys") + "?sort=name:asc&limit=100" + if start != "" { + reqURL += "&start=" + start } - }() - return &iterator{ - ch: values, - ctx: ctx, - cancel: cancel, - }, nil -} - -// Close closes the Store. -func (s *Store) Close() error { return nil } - -type iterator struct { - ch <-chan string - ctx context.Context - cancel context.CancelCauseFunc -} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return nil, "", fmt.Errorf("fortanix: failed to list keys: %v", err) + } + req.Header.Set("Authorization", s.config.APIKey.String()) -var _ kv.Iter[string] = (*iterator)(nil) + resp, err := s.client.Do(req) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil, "", err + } + if err != nil { + return nil, "", fmt.Errorf("fortanix: failed to list keys: %v", err) + } -// Next moves the iterator to the next key, if any. -// This key is available until Next is called again. -// -// 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() (string, bool) { - select { - case v, ok := <-i.ch: - return v, ok - case <-i.ctx.Done(): - return "", false + type Response struct { + Name string `json:"name"` + } + var keys []Response + if err := json.NewDecoder(mem.LimitReader(resp.Body, 10*key.MaxSize)).Decode(&keys); err != nil { + return nil, "", fmt.Errorf("fortanix: failed to list keys: failed to parse server response: %v", err) + } + if len(keys) == 0 { + break + } + for _, k := range keys { + names = append(names, k.Name) + } } + return keystore.List(names, prefix, n) } -// Err returns the first error, if any, encountered -// while iterating over the set of keys. -func (i *iterator) Close() error { - i.cancel(context.Canceled) - return context.Cause(i.ctx) -} +// Close closes the Store. +func (s *Store) Close() error { return nil } // parseErrorResponse returns an error containing // the response status code and response body @@ -515,7 +472,7 @@ func parseErrorResponse(resp *http.Response) error { return nil } if resp.Body == nil { - return kes.NewError(resp.StatusCode, resp.Status) + return kesdk.NewError(resp.StatusCode, resp.Status) } defer resp.Body.Close() @@ -533,14 +490,14 @@ func parseErrorResponse(resp *http.Response) error { if err := json.NewDecoder(mem.LimitReader(resp.Body, size)).Decode(&response); err != nil { return err } - return kes.NewError(resp.StatusCode, response.Message) + return kesdk.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()) + return kesdk.NewError(resp.StatusCode, sb.String()) } // loadCustomCAs returns a new RootCA certificate pool @@ -568,7 +525,7 @@ func loadCustomCAs(path string) (*x509.CertPool, error) { return rootCAs, err } if !stat.IsDir() { - bytes, err := ioutil.ReadAll(f) + bytes, err := io.ReadAll(f) if err != nil { return rootCAs, err } @@ -588,7 +545,7 @@ func loadCustomCAs(path string) (*x509.CertPool, error) { } name := filepath.Join(path, file.Name()) - bytes, err := ioutil.ReadFile(name) + bytes, err := os.ReadFile(name) if err != nil { return rootCAs, err } diff --git a/internal/keystore/fs/fs.go b/internal/keystore/fs/fs.go index 2eb9a33a..85e810c3 100644 --- a/internal/keystore/fs/fs.go +++ b/internal/keystore/fs/fs.go @@ -11,7 +11,6 @@ import ( "context" "errors" "io" - "io/fs" "os" "path/filepath" "strings" @@ -19,8 +18,9 @@ import ( "time" "aead.dev/mem" - "github.com/minio/kes-go" - "github.com/minio/kes/kv" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" + "github.com/minio/kes/internal/keystore" ) // NewStore returns a new Store that reads @@ -57,18 +57,18 @@ type Store struct { lock sync.RWMutex } -var _ kv.Store[string, []byte] = (*Store)(nil) +func (s *Store) String() string { return "Filesystem: " + s.dir } // Status returns the current state of the Conn. // // In particular, it reports whether the underlying // filesystem is accessible. -func (s *Store) Status(context.Context) (kv.State, error) { +func (s *Store) Status(context.Context) (kes.KeyStoreState, error) { start := time.Now() if _, err := os.Stat(s.dir); err != nil { - return kv.State{}, &kv.Unreachable{Err: err} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} } - return kv.State{ + return kes.KeyStoreState{ Latency: time.Since(start), }, nil } @@ -87,7 +87,7 @@ func (s *Store) Create(_ context.Context, name string, value []byte) error { filename := filepath.Join(s.dir, name) switch err := s.create(filename, value); { case errors.Is(err, os.ErrExist): - return kes.ErrKeyExists + return kesdk.ErrKeyExists case err != nil: os.Remove(filename) return err @@ -95,14 +95,6 @@ func (s *Store) 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. @@ -117,7 +109,7 @@ func (s *Store) Get(_ context.Context, name string) ([]byte, error) { file, err := os.Open(filepath.Join(s.dir, name)) if errors.Is(err, os.ErrNotExist) { - return nil, kes.ErrKeyNotFound + return nil, kesdk.ErrKeyNotFound } if err != nil { return nil, err @@ -143,21 +135,44 @@ func (s *Store) Delete(_ context.Context, name string) error { } switch err := os.Remove(filepath.Join(s.dir, name)); { case errors.Is(err, os.ErrNotExist): - return kes.ErrKeyNotFound + return kesdk.ErrKeyNotFound default: return err } } -// 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 (s *Store) List(ctx context.Context) (kv.Iter[string], error) { +// List returns a new Iterator over the names of +// all stored keys. +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. +// +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty +func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { dir, err := os.Open(s.dir) if err != nil { - return nil, err + return nil, "", err + } + defer dir.Close() + + names, err := dir.Readdirnames(-1) + if err != nil { + return nil, "", err + } + select { + case <-ctx.Done(): + if err := ctx.Err(); err != nil { + return nil, "", err + } + return nil, "", context.Canceled + default: + return keystore.List(names, prefix, n) } - return NewIter(ctx, dir), nil } // Close closes the Store. @@ -183,87 +198,6 @@ func (s *Store) create(filename string, value []byte) error { return file.Close() } -// Iter is an iterator over all files within a -// directory. It must be closed to release any -// filesystem resources. -type Iter struct { - ctx context.Context - dir fs.ReadDirFile - names []fs.DirEntry - err error - closed bool -} - -var _ kv.Iter[string] = (*Iter)(nil) - -// NewIter returns an Iter all files within the given -// directory. The Iter does not iterator recursively -// into subdirectories. -func NewIter(ctx context.Context, dir fs.ReadDirFile) *Iter { - return &Iter{ - ctx: ctx, - dir: dir, - } -} - -// Next reports whether there are more directory entries. -// It returns false when there are no more entries, the -// Iter got closed or once it encounters an error. -// -// The name of the next directory entry is availbale via -// the Name method. -func (i *Iter) Next() (string, bool) { - if i.closed || i.err != nil { - return "", false - } - if len(i.names) > 0 { - entry := i.names[0] - i.names = i.names[1:] - return entry.Name(), true - } - - if i.ctx != nil { - select { - case <-i.ctx.Done(): - if i.err = i.ctx.Err(); i.err == nil { - i.err = context.Canceled - } - return "", false - default: - } - } - - const N = 256 - i.names, i.err = i.dir.ReadDir(N) - if errors.Is(i.err, io.EOF) { - i.err = nil - } - if i.err != nil { - i.Close() - return "", false - } - 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 -// resources back to the OS. -func (i *Iter) Close() error { - if i.closed { - return i.err - } - - i.closed = true - if err := i.dir.Close(); i.err == nil { - i.err = err - } - return i.err -} - func validName(name string) error { if name == "" || strings.IndexFunc(name, func(c rune) bool { return c == '/' || c == '\\' || c == '.' diff --git a/internal/keystore/gcp/iterator.go b/internal/keystore/gcp/iterator.go deleted file mode 100644 index d470bd32..00000000 --- a/internal/keystore/gcp/iterator.go +++ /dev/null @@ -1,41 +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 gcp - -import ( - "path" - - secretmanager "cloud.google.com/go/secretmanager/apiv1" - gcpiterator "google.golang.org/api/iterator" -) - -type iterator struct { - src *secretmanager.SecretIterator - err error - closed bool -} - -func (i *iterator) Next() (string, bool) { - if i.closed { - return "", false - } - - v, err := i.src.Next() - if err != nil { - i.err = err - if err == gcpiterator.Done { - i.err = i.Close() - } - return "", false - } - return path.Base(v.GetName()), true -} - -func (i *iterator) Close() error { - if !i.closed { - i.closed = true - } - return i.err -} diff --git a/internal/keystore/gcp/secret-manager.go b/internal/keystore/gcp/secret-manager.go index aa6baded..ae334540 100644 --- a/internal/keystore/gcp/secret-manager.go +++ b/internal/keystore/gcp/secret-manager.go @@ -12,8 +12,10 @@ import ( "path" "time" - "github.com/minio/kes-go" - "github.com/minio/kes/kv" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" + "github.com/minio/kes/internal/keystore" + gcpiterator "google.golang.org/api/iterator" "google.golang.org/api/option" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -28,8 +30,6 @@ type Store struct { config *Config } -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) (*Store, error) { @@ -101,19 +101,21 @@ func Connect(ctx context.Context, c *Config) (*Store, error) { return conn, nil } +func (s *Store) String() string { return "GCP SecretManager: Project=" + s.config.ProjectID } + // Status returns the current state of the GCP SecretManager instance. // In particular, whether it is reachable and the network latency. -func (s *Store) Status(ctx context.Context) (kv.State, error) { +func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.config.Endpoint, nil) if err != nil { - return kv.State{}, err + return kes.KeyStoreState{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kv.State{}, &kv.Unreachable{Err: err} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} } - return kv.State{ + return kes.KeyStoreState{ Latency: time.Since(start), }, nil } @@ -142,7 +144,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { return err } if status.Code(err) == codes.AlreadyExists { - return kes.ErrKeyExists + return kesdk.ErrKeyExists } return fmt.Errorf("gcp: failed to create '%s': %v", name, err) } @@ -183,7 +185,7 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { return nil, err } if status.Code(err) == codes.NotFound { - return nil, kes.ErrKeyNotFound + return nil, kesdk.ErrKeyNotFound } return nil, fmt.Errorf("gcp: failed to read '%s': %v", name, err) } @@ -207,7 +209,7 @@ func (s *Store) Delete(ctx context.Context, name string) error { return err } if status.Code(err) == codes.NotFound { - return kes.ErrKeyNotFound + return kesdk.ErrKeyNotFound } return fmt.Errorf("gcp: failed to delete '%s': %v", name, err) } @@ -216,13 +218,31 @@ func (s *Store) Delete(ctx context.Context, name string) error { // List returns a new Iterator over the names of // all stored keys. -func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. +// +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty. +func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { location := path.Join("projects", s.config.ProjectID) - return &iterator{ - src: s.client.ListSecrets(ctx, &secretmanagerpb.ListSecretsRequest{ - Parent: location, - }), - }, nil + + iter := s.client.ListSecrets(ctx, &secretmanagerpb.ListSecretsRequest{ + Parent: location, + }) + + var names []string + for resp, err := iter.Next(); err != gcpiterator.Done; resp, err = iter.Next() { + if err != nil { + return nil, "", err + } + names = append(names, path.Base(resp.GetName())) + } + return keystore.List(names, prefix, n) } // Close closes the Store. diff --git a/internal/keystore/gemalto/key-secure.go b/internal/keystore/gemalto/key-secure.go index a53ce11d..d641184b 100644 --- a/internal/keystore/gemalto/key-secure.go +++ b/internal/keystore/gemalto/key-secure.go @@ -15,7 +15,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "os" @@ -24,9 +23,10 @@ import ( "time" "aead.dev/mem" - "github.com/minio/kes-go" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" xhttp "github.com/minio/kes/internal/http" - "github.com/minio/kes/kv" + "github.com/minio/kes/internal/keystore" ) // Credentials represents a Gemalto KeySecure @@ -65,8 +65,6 @@ type Store struct { stop context.CancelFunc } -var _ kv.Store[string, []byte] = (*Store)(nil) - // Connect returns a Store to a Gemalto KeySecure // server using the given config. func Connect(ctx context.Context, config *Config) (c *Store, err error) { @@ -113,19 +111,21 @@ func Connect(ctx context.Context, config *Config) (c *Store, err error) { }, nil } +func (s *Store) String() string { return "Gemalto KeySecure: " + s.config.Endpoint } + // Status returns the current state of the Gemalto KeySecure instance. // In particular, whether it is reachable and the network latency. -func (s *Store) Status(ctx context.Context) (kv.State, error) { +func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.config.Endpoint, nil) if err != nil { - return kv.State{}, err + return kes.KeyStoreState{}, err } start := time.Now() if _, err = http.DefaultClient.Do(req); err != nil { - return kv.State{}, &kv.Unreachable{Err: err} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} } - return kv.State{ + return kes.KeyStoreState{ Latency: time.Since(start), }, nil } @@ -167,7 +167,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { defer resp.Body.Close() if resp.StatusCode == http.StatusConflict { - return kes.ErrKeyExists + return kesdk.ErrKeyExists } if resp.StatusCode != http.StatusCreated { response, err := parseServerError(resp) @@ -210,7 +210,7 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { - return nil, kes.ErrKeyNotFound + return nil, kesdk.ErrKeyNotFound } if resp.StatusCode != http.StatusOK { response, err := parseServerError(resp) @@ -271,7 +271,17 @@ func (s *Store) Delete(ctx context.Context, name string) error { // List returns a new Iterator over the names of // all stored keys. -func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. +// +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty +func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, 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 @@ -284,91 +294,65 @@ func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { } `json:"resources"` } - var cancel context.CancelCauseFunc - ctx, cancel = context.WithCancelCause(ctx) - values := make(chan string, 10) - - // The following go-routine keeps listing keys (in pages of size 'limit') - // and writes the keys names to the Iterator. - // If there are so many items such that they don't fit on a single page it - // requests another page by making another request and skipping all items - // processed so far. - go func() { - defer close(values) - - const limit = 200 // We limit a listing page to 200. This an arbitrary but reasonable value. - var ( - skip uint64 // Keep track of the items processed so far and skip them. - response Response - ) - 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", s.config.Endpoint, limit, skip) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - cancel(fmt.Errorf("gemalto: failed to list keys: %v", err)) - break - } - req.Header.Set("Authorization", s.client.AuthToken()) + const limit = 200 // We limit a listing page to 200. This an arbitrary but reasonable value. + var ( + skip uint64 // Keep track of the items processed so far and skip them. + response Response + names []string + ) + 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", s.config.Endpoint, limit, skip) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, "", fmt.Errorf("gemalto: failed to list keys: %v", err) + } + req.Header.Set("Authorization", s.client.AuthToken()) - resp, err := s.client.Do(req) - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - cancel(err) - break - } + resp, err := s.client.Do(req) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil, "", err + } + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + response, err := parseServerError(resp) if err != nil { - 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 { - cancel(fmt.Errorf("gemalto: %s: failed to parse server response: %v", resp.Status, err)) - } else { - cancel(fmt.Errorf("gemalto: failed to list keys: '%s' (%d)", response.Message, response.Code)) - } - break + return nil, "", fmt.Errorf("gemalto: %s: failed to parse server response: %v", resp.Status, err) } + return nil, "", fmt.Errorf("gemalto: failed to list keys: '%s' (%d)", response.Message, response.Code) - 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) { - cancel(err) - } else { - cancel(fmt.Errorf("gemalto: failed to list keys: listing page too large: %v", err)) - } - break - } + } - // We check that the invariant that the KeySecure instance has skipped as many items - // as we requested is true. If both numbers are off then the KeySecure would either - // return items that we've already served to the client or skip items that we haven't - // served, yet. - if response.Skip != 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(): - return - } + 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) { + return nil, "", err } + return nil, "", fmt.Errorf("gemalto: failed to list keys: listing page too large: %v", err) + } - skip += uint64(len(response.Resources)) - if response.Skip >= response.Total { // Stop once we've reached the end of the listing. - break - } + // We check that the invariant that the KeySecure instance has skipped as many items + // as we requested is true. If both numbers are off then the KeySecure would either + // return items that we've already served to the client or skip items that we haven't + // served, yet. + if response.Skip != skip { + return nil, "", fmt.Errorf("gemalto: failed to list keys: pagination is out-of-sync: tried to skip %d but skipped %d", skip, response.Skip) } - }() - return &iter{ - ch: values, - ctx: ctx, - cancel: cancel, - }, nil + for _, v := range response.Resources { + names = append(names, v.Name) + } + + skip += uint64(len(response.Resources)) + if response.Skip >= response.Total { // Stop once we've reached the end of the listing. + break + } + } + return keystore.List(names, prefix, n) } // Close closes the Store. It stops any authentication renewal in the background. @@ -377,26 +361,6 @@ func (s *Store) Close() error { return nil } -type iter struct { - ch <-chan string - ctx context.Context - cancel context.CancelCauseFunc -} - -func (i *iter) Next() (string, bool) { - select { - case v, ok := <-i.ch: - return v, ok - case <-i.ctx.Done(): - return "", false - } -} - -func (i *iter) Close() error { - i.cancel(context.Canceled) - return context.Cause(i.ctx) -} - // errResponse represents a KeySecure API error // response. type errResponse struct { @@ -468,7 +432,7 @@ func loadCustomCAs(path string) (*x509.CertPool, error) { return rootCAs, err } if !stat.IsDir() { - bytes, err := ioutil.ReadAll(f) + bytes, err := io.ReadAll(f) if err != nil { return rootCAs, err } @@ -488,7 +452,7 @@ func loadCustomCAs(path string) (*x509.CertPool, error) { } name := filepath.Join(path, file.Name()) - bytes, err := ioutil.ReadFile(name) + bytes, err := os.ReadFile(name) if err != nil { return rootCAs, err } diff --git a/internal/keystore/kes/kes.go b/internal/keystore/kes/kes.go deleted file mode 100644 index 24e3969c..00000000 --- a/internal/keystore/kes/kes.go +++ /dev/null @@ -1,190 +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 kes - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "io" - "time" - - "github.com/minio/kes-go" - "github.com/minio/kes/internal/https" - "github.com/minio/kes/kv" -) - -// Config is a structure containing configuration -// options for connecting to a KES server. -type Config struct { - // Endpoints contains one or multiple KES - // server endpoints. - // - // A Conn will automatically load balance - // between multiple endpoints. - Endpoints []string - - // Enclave is an optional KES enclave name. - // If empty, the default enclave is used. - Enclave string - - // PrivateKey is a path to a file containing - // a X.509 private key for mTLS authentication. - PrivateKey string - - // Certificate is a path to a file containing - // a X.509 certificate for mTLS authentication. - Certificate string - - // CAPath is an optional path to the root CA - // certificate(s) used to verify the TLS - // certificate of the KES server. If empty, - // the host's root CA set is used. - CAPath string -} - -// Connect connects to a KES server with the given configuration. -func Connect(ctx context.Context, config *Config) (*Store, error) { - if len(config.Endpoints) == 0 { - return nil, errors.New("kes: no endpoints provided") - } - if config.Certificate == "" { - return nil, errors.New("kes: no certificate provided") - } - if config.PrivateKey == "" { - return nil, errors.New("kes: no private key provided") - } - - cert, err := https.CertificateFromFile(config.Certificate, config.PrivateKey, "") - if err != nil { - return nil, err - } - var rootCAs *x509.CertPool - if config.CAPath != "" { - rootCAs, err = https.CertPoolFromFile(config.CAPath) - if err != nil { - return nil, err - } - } - - store := &Store{ - client: kes.NewClientWithConfig("", &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: rootCAs, - }), - enclave: config.Enclave, - } - store.client.Endpoints = config.Endpoints - - if _, err := store.Status(ctx); err != nil { - return nil, err - } - return store, nil -} - -// 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 (s *Store) Status(ctx context.Context) (kv.State, error) { - start := time.Now() - _, err := s.client.Status(ctx) - latency := time.Since(start) - - if connErr, ok := kes.IsConnError(err); ok { - return kv.State{}, &kv.Unreachable{Err: connErr} - } - if err != nil { - return kv.State{}, &kv.Unavailable{Err: err} - } - return kv.State{ - Latency: latency, - }, nil -} - -// 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 (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 - } - 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 (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 - } - return secret, err -} - -// 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 (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 - } - return err -} - -// List returns a new kms.Iter over all stored entries. -func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { - return &iter{ - ctx: ctx, - iter: &kes.ListIter[string]{ - NextFunc: s.client.Enclave(s.enclave).ListSecrets, - }, - }, nil -} - -// Close closes the Store. -func (s *Store) Close() error { return nil } - -type iter struct { - ctx context.Context - iter *kes.ListIter[string] - err error -} - -func (i *iter) Next() (string, bool) { - if i.err != nil { - return "", false - } - - next, err := i.iter.Next(i.ctx) - i.err = err - return next, err == nil -} - -func (i *iter) Close() error { - if i.err == io.EOF { - return nil - } - return i.err -} diff --git a/internal/keystore/keystore.go b/internal/keystore/keystore.go new file mode 100644 index 00000000..8b3a1097 --- /dev/null +++ b/internal/keystore/keystore.go @@ -0,0 +1,77 @@ +// 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 keystore + +import ( + "errors" + "slices" + "strings" +) + +// List sorts the names lexicographically and returns the +// first n, if n > 0, names that match the given prefix. +// If n <= 0, List limits the returned slice to a reasonable +// default. If len(names) is greater than n then List returns +// the next name from which to continue. +func List(names []string, prefix string, n int) ([]string, string, error) { + const N = 1024 + + slices.Sort(names) + if prefix != "" { + i := slices.IndexFunc(names, func(name string) bool { + return strings.HasPrefix(name, prefix) + }) + if i < 0 { + return []string{}, "", nil + } + names = names[i:] + + for i, name := range names { + if !strings.HasPrefix(name, prefix) { + return names[:i], "", nil + } + if (n > 0 && i > n) || i == N { + if i == len(names)-1 { + return names, "", nil + } + return names[:i], names[i], nil + } + } + } + + switch { + case (n <= 0 && len(names) <= N) || len(names) <= n: + return names, "", nil + case n <= 0: + return names[:N], names[N], nil + default: + return names[:n], names[n], nil + } +} + +// ErrUnreachable is an error that indicates that the +// Store is not reachable - for example due to a +// a network error. +type ErrUnreachable struct { + Err error +} + +func (e *ErrUnreachable) Error() string { + if e.Err == nil { + return "kes: keystore unreachable" + } + return "kes: keystore unreachable: " + e.Err.Error() +} + +// IsUnreachable reports whether err is an Unreachable +// error. If IsUnreachable returns true it returns err +// as Unreachable error. +func IsUnreachable(err error) (*ErrUnreachable, bool) { + var u *ErrUnreachable + if errors.As(err, &u) { + return u, true + } + return nil, false +} diff --git a/internal/keystore/keystore_test.go b/internal/keystore/keystore_test.go new file mode 100644 index 00000000..8606d852 --- /dev/null +++ b/internal/keystore/keystore_test.go @@ -0,0 +1,56 @@ +// 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 keystore + +import ( + "slices" + "testing" +) + +func TestList(t *testing.T) { + for i, test := range listTests { + list, continueAt, err := List(test.Names, test.Prefix, test.N) + if err != nil { + t.Fatalf("Test %d: failed to list: %v", i, err) + } + + if !slices.Equal(list, test.List) { + t.Fatalf("Test %d: listing does not match: got '%v' - want '%v'", i, list, test.List) + } + if continueAt != test.ContinueAt { + t.Fatalf("Test %d: continue at does not match: got '%s' - want '%s'", i, continueAt, test.ContinueAt) + } + } +} + +var listTests = []struct { + Names []string + Prefix string + N int + + List []string + ContinueAt string +}{ + { + Names: []string{}, + List: []string{}, + }, + { + Names: []string{"my-key", "my-key2", "0-key", "1-key"}, + List: []string{"0-key", "1-key", "my-key", "my-key2"}, + }, + { + Names: []string{"my-key", "my-key2", "0-key", "1-key"}, + Prefix: "my", + List: []string{"my-key", "my-key2"}, + }, + { + Names: []string{"my-key", "my-key2", "0-key", "1-key"}, + Prefix: "my", + N: 1, + List: []string{"my-key"}, + ContinueAt: "my-key2", + }, +} diff --git a/internal/keystore/mem/mem.go b/internal/keystore/mem/mem.go deleted file mode 100644 index af706844..00000000 --- a/internal/keystore/mem/mem.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2019 - 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 mem implements an in-memory key-value store. -package mem - -import ( - "context" - "sync" - - "github.com/minio/kes-go" - "github.com/minio/kes/kv" -) - -// Store is an in-memory key-value store. Its zero value is -// ready to use. -type Store struct { - lock sync.RWMutex - store map[string][]byte -} - -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 -} - -// Create adds the given key to the store if and only if -// no entry for the given name exists. If an entry already -// exists it returns kes.ErrKeyExists. -func (s *Store) Create(_ context.Context, name string, value []byte) error { - s.lock.Lock() - defer s.lock.Unlock() - - if s.store == nil { - s.store = map[string][]byte{} - } - if _, ok := s.store[name]; ok { - return kes.ErrKeyExists - } - s.store[name] = value - return nil -} - -// Set adds the given key to the store if and only if -// no entry for the given name exists. If 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) -} - -// Delete removes the key with the given value, if it exists. -func (s *Store) Delete(_ context.Context, name string) error { - s.lock.Lock() - defer s.lock.Unlock() - - delete(s.store, name) - return nil -} - -// Get returns the key associated with the given name. If no -// entry for this name exists it returns kes.ErrKeyNotFound. -func (s *Store) Get(_ context.Context, name string) ([]byte, error) { - s.lock.RLock() - defer s.lock.RUnlock() - - k, ok := s.store[name] - if !ok { - return nil, kes.ErrKeyNotFound - } - return k, nil -} - -// List returns a new iterator over the metadata of all stored keys. -func (s *Store) List(context.Context) (kv.Iter[string], error) { - s.lock.RLock() - defer s.lock.RUnlock() - - names := make([]string, 0, len(s.store)) - for name := range s.store { - names = append(names, name) - } - return &iterator{ - values: names, - }, nil -} - -// Close closes the Store. -func (s *Store) Close() error { return nil } - -type iterator struct { - values []string -} - -var _ kv.Iter[string] = (*iterator)(nil) - -func (i *iterator) Next() (string, bool) { - if len(i.values) > 0 { - v := i.values[0] - i.values = i.values[1:] - return v, true - } - return "", false -} - -func (*iterator) Close() error { return nil } diff --git a/internal/keystore/vault/iterator.go b/internal/keystore/vault/iterator.go deleted file mode 100644 index d12eb7fd..00000000 --- a/internal/keystore/vault/iterator.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2019 - 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 vault - -import ( - "fmt" - "strings" - - "github.com/minio/kes/kv" -) - -type iterator struct { - values []interface{} -} - -var _ kv.Iter[string] = (*iterator)(nil) - -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 - return v, true - } - } - return "", false -} - -func (*iterator) Close() error { return nil } diff --git a/internal/keystore/vault/vault.go b/internal/keystore/vault/vault.go index 0b9907b2..933fd0eb 100644 --- a/internal/keystore/vault/vault.go +++ b/internal/keystore/vault/vault.go @@ -25,8 +25,9 @@ import ( "aead.dev/mem" vaultapi "github.com/hashicorp/vault/api" - "github.com/minio/kes-go" - "github.com/minio/kes/kv" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" + "github.com/minio/kes/internal/keystore" ) // Store is a Hashicorp Vault secret store. @@ -153,20 +154,20 @@ func Connect(ctx context.Context, c *Config) (*Store, error) { }, nil } -var _ kv.Store[string, []byte] = (*Store)(nil) - var errSealed = errors.New("vault: key store is sealed") +func (s *Store) String() string { return "Hashicorp Vault: " + s.config.Endpoint } + // Status returns the current state of the Hashicorp Vault instance. // In particular, whether it is reachable and the network latency. -func (s *Store) Status(ctx context.Context) (kv.State, error) { +func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, 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 kv.State{}, err + return kes.KeyStoreState{}, err } client.ClearNamespace() @@ -175,17 +176,17 @@ func (s *Store) Status(ctx context.Context) (kv.State, error) { if err == nil { switch { case !health.Initialized: - return kv.State{}, &kv.Unavailable{Err: errors.New("vault: not initialized")} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: errors.New("vault: not initialized")} case health.Sealed: - return kv.State{}, &kv.Unavailable{Err: errSealed} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: errSealed} default: - return kv.State{Latency: time.Since(start)}, nil + return kes.KeyStoreState{Latency: time.Since(start)}, nil } } if errors.Is(err, context.Canceled) && errors.Is(err, context.DeadlineExceeded) { - return kv.State{}, &kv.Unreachable{Err: err} + return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} } - return kv.State{}, err + return kes.KeyStoreState{}, err } // Create creates the given key-value pair at Vault if and only @@ -235,7 +236,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { if _, ok := secret.Data[name]; !ok { return fmt.Errorf("vault: entry exist but failed to read '%s': invalid K/V v1 format", location) } - return kes.ErrKeyExists + return kesdk.ErrKeyExists case err == nil && secret != nil && s.config.APIVersion == APIv2 && len(secret.Data) > 0: data := secret.Data v, ok := data["data"] @@ -249,7 +250,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { if _, ok := data[name]; !ok { return fmt.Errorf("vault: failed to read '%s': entry exists but no secret key is present", location) } - return kes.ErrKeyExists + return kesdk.ErrKeyExists case err != nil: return fmt.Errorf("vault: failed to create '%s': %v", location, err) } @@ -371,7 +372,7 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { // Vault will not return an error if e.g. the key existed but has // been deleted. However, it will return (nil, nil) in this case. if err == nil && entry == nil { - return nil, kes.ErrKeyNotFound + return nil, kesdk.ErrKeyNotFound } return nil, fmt.Errorf("vault: failed to read '%s': %v", location, err) } @@ -483,11 +484,19 @@ func (s *Store) Delete(ctx context.Context, name string) error { return nil } -// List returns a new Iterator over the names of -// all stored keys. -func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { +// List returns the first n key names, that start with the given +// prefix, and the next prefix from which the listing should +// continue. +// +// It returns all keys with the prefix if n < 0 and less than n +// names if n is greater than the number of keys with the prefix. +// +// An empty prefix matches any key name. At the end of the listing +// or when there are no (more) keys starting with the prefix, the +// returned prefix is empty. +func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { if s.client.Sealed() { - return nil, errSealed + return nil, "", errSealed } // We don't use the Vault SDK vault.Logical.List(string) API @@ -510,7 +519,7 @@ func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { resp, err := s.client.RawRequestWithContext(ctx, r) if err != nil { - return nil, fmt.Errorf("vault: failed to list '%s': %v", location, err) + return nil, "", fmt.Errorf("vault: failed to list '%s': %v", location, err) } defer resp.Body.Close() @@ -521,10 +530,10 @@ func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { const MaxBody = 32 * mem.MiB secret, err := vaultapi.ParseSecret(mem.LimitReader(resp.Body, MaxBody)) if err != nil { - return nil, fmt.Errorf("vault: failed to list '%s': %v", location, err) + return nil, "", fmt.Errorf("vault: failed to list '%s': %v", location, err) } if secret == nil { // The secret may be nil even when there was no error. - return &iterator{}, nil // We return an empty iterator in this case. + return []string{}, "", nil // We return an empty iterator in this case. } // Vault returns a generic map that should contain @@ -533,9 +542,13 @@ func (s *Store) List(ctx context.Context) (kv.Iter[string], error) { // of a dedicated type or []string. values, ok := secret.Data["keys"].([]interface{}) if !ok { - return nil, fmt.Errorf("vault: failed to list '%s': invalid key listing format", location) + return nil, "", fmt.Errorf("vault: failed to list '%s': invalid key listing format", location) + } + names := make([]string, 0, len(values)) + for _, v := range values { + names = append(names, fmt.Sprint(v)) } - return &iterator{values: values}, nil + return keystore.List(names, prefix, n) } // Close closes the Store. It stops any authentication renewal in the background. diff --git a/edge/aws_test.go b/kesconf/aws_test.go similarity index 70% rename from edge/aws_test.go rename to kesconf/aws_test.go index 5e2337a6..07c20ba7 100644 --- a/edge/aws_test.go +++ b/kesconf/aws_test.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" - "os" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var awsConfigFile = flag.String("aws.config", "", "Path to a KES config file with AWS SecretsManager config") @@ -19,18 +18,12 @@ func TestAWS(t *testing.T) { t.Skip("AWS SecretsManager tests disabled. Use -aws.config= to enable them") } - file, err := os.Open(*awsConfigFile) + config, err := kesconf.ReadFile(*awsConfigFile) if err != nil { t.Fatal(err) } - defer file.Close() - - config, err := edge.ReadServerConfigYAML(file) - if err != nil { - t.Fatal(err) - } - if _, ok := config.KeyStore.(*edge.AWSSecretsManagerKeyStore); !ok { - t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &edge.AWSSecretsManagerKeyStore{}) + if _, ok := config.KeyStore.(*kesconf.AWSSecretsManagerKeyStore); !ok { + t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.AWSSecretsManagerKeyStore{}) } ctx, cancel := testingContext(t) @@ -42,7 +35,6 @@ func TestAWS(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/edge/azure_test.go b/kesconf/azure_test.go similarity index 78% rename from edge/azure_test.go rename to kesconf/azure_test.go index 31b73a20..06d7e8cf 100644 --- a/edge/azure_test.go +++ b/kesconf/azure_test.go @@ -2,14 +2,14 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" "os" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var azureConfigFile = flag.String("azure.config", "", "Path to a KES config file with Azure KeyVault config") @@ -24,12 +24,12 @@ func TestAzure(t *testing.T) { } defer file.Close() - config, err := edge.ReadServerConfigYAML(file) + config, err := kesconf.ReadFile(*azureConfigFile) if err != nil { t.Fatal(err) } - if _, ok := config.KeyStore.(*edge.AzureKeyVaultKeyStore); !ok { - t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &edge.AzureKeyVaultKeyStore{}) + if _, ok := config.KeyStore.(*kesconf.AzureKeyVaultKeyStore); !ok { + t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.AzureKeyVaultKeyStore{}) } ctx, cancel := testingContext(t) @@ -41,7 +41,6 @@ func TestAzure(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/edge/server-config-yml.go b/kesconf/config.go similarity index 68% rename from edge/server-config-yml.go rename to kesconf/config.go index e0c761c9..4e9040b1 100644 --- a/edge/server-config-yml.go +++ b/kesconf/config.go @@ -2,12 +2,15 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge +package kesconf import ( + "crypto/tls" "errors" "fmt" + "log/slog" "os" + "strconv" "strings" "time" @@ -15,7 +18,7 @@ import ( "gopkg.in/yaml.v3" ) -type yml struct { +type ymlFile struct { Version string `yaml:"version"` Addr env[string] `yaml:"address"` @@ -29,6 +32,7 @@ type yml struct { Certificate env[string] `yaml:"cert"` CAPath env[string] `yaml:"ca"` Password env[string] `yaml:"password"` + ClientAuth env[string] `yaml:"auth"` Proxy struct { Identities []env[kes.Identity] `yaml:"identities"` @@ -207,27 +211,27 @@ type yml struct { func findVersion(root *yaml.Node) (string, error) { if root == nil { - return "", errors.New("edge: invalid server config") + return "", errors.New("kesconf: invalid config") } if root.Kind != yaml.DocumentNode { - return "", errors.New("edge: invalid server config") + return "", errors.New("kesconf: invalid config format") } if len(root.Content) != 1 { - return "", errors.New("edge: invalid server config") + return "", errors.New("kesconf: invalid config format") } doc := root.Content[0] for i, n := range doc.Content { if n.Value == "version" { if n.Kind != yaml.ScalarNode { - return "", fmt.Errorf("edge: invalid server config version at line '%d'", n.Line) + return "", fmt.Errorf("kesconf: invalid config version at line '%d'", n.Line) } if i == len(doc.Content)-1 { - return "", fmt.Errorf("edge: invalid server config version at line '%d'", n.Line) + return "", fmt.Errorf("kesconf: invalid config version at line '%d'", n.Line) } v := doc.Content[i+1] if v.Kind != yaml.ScalarNode { - return "", fmt.Errorf("edge: invalid server config version at line '%d'", v.Line) + return "", fmt.Errorf("kesconf: invalid config version at line '%d'", v.Line) } return v.Value, nil } @@ -235,59 +239,68 @@ func findVersion(root *yaml.Node) (string, error) { return "", nil } -func ymlToServerConfig(y *yml) (*ServerConfig, error) { +func ymlToServerConfig(y *ymlFile) (*File, error) { if y.Version != "" && y.Version != "v1" { - return nil, fmt.Errorf("edge: invalid version '%s'", y.Version) + return nil, fmt.Errorf("kesconf: invalid config version '%s'", y.Version) } if y.Admin.Identity.Value.IsUnknown() { - return nil, errors.New("edge: invalid admin identity: no admin identity") + return nil, errors.New("kesconf: invalid admin identity: no admin identity") } if y.TLS.PrivateKey.Value == "" { - return nil, errors.New("edge: invalid tls config: no private key") + return nil, errors.New("kesconf: invalid tls config: no private key") } if y.TLS.Certificate.Value == "" { - return nil, errors.New("edge: invalid tls config: no certificate") + return nil, errors.New("kesconf: invalid tls config: no certificate") + } + + clientAuth := tls.RequestClientCert + if v := strings.ToLower(y.TLS.ClientAuth.Value); v != "" && v != "on" && v != "off" { + return nil, fmt.Errorf("kesconf: invalid tls config: invalid auth '%s'", y.TLS.ClientAuth) + } else if v == "on" { + clientAuth = tls.VerifyClientCertIfGiven } for _, proxy := range y.TLS.Proxy.Identities { if proxy.Value == y.Admin.Identity.Value { - return nil, fmt.Errorf("edge: invalid tls proxy: identity '%s' is already admin", proxy.Value) + return nil, fmt.Errorf("kesconf: invalid tls proxy: identity '%s' is already admin", proxy.Value) } } for name, policy := range y.Policies { for _, identity := range policy.Identities { if identity.Value == y.Admin.Identity.Value { - return nil, fmt.Errorf("edge: invalid policy '%s': identity '%s' is already admin", name, identity.Value) + return nil, fmt.Errorf("kesconf: invalid policy '%s': identity '%s' is already admin", name, identity.Value) } for _, proxy := range y.TLS.Proxy.Identities { if identity.Value == proxy.Value { - return nil, fmt.Errorf("edge: invalid policy '%s': identity '%s' is already a TLS proxy", name, identity.Value) + return nil, fmt.Errorf("kesconf: invalid policy '%s': identity '%s' is already a TLS proxy", name, identity.Value) } } } } if y.Cache.Expiry.Any.Value < 0 { - return nil, fmt.Errorf("edge: invalid cache expiry '%v'", y.Cache.Expiry.Any.Value) + return nil, fmt.Errorf("kesconf: invalid cache expiry '%v'", y.Cache.Expiry.Any.Value) } if y.Cache.Expiry.Unused.Value < 0 { - return nil, fmt.Errorf("edge: invalid cache unused expiry '%v'", y.Cache.Expiry.Unused.Value) + return nil, fmt.Errorf("kesconf: invalid cache unused expiry '%v'", y.Cache.Expiry.Unused.Value) } if y.Cache.Expiry.Offline.Value < 0 { - return nil, fmt.Errorf("edge: invalid offline cache expiry '%v'", y.Cache.Expiry.Offline.Value) + return nil, fmt.Errorf("kesconf: invalid offline cache expiry '%v'", y.Cache.Expiry.Offline.Value) } - if v := strings.ToLower(strings.TrimSpace(y.Log.Error.Value)); v != "on" && v != "off" && v != "" { - return nil, fmt.Errorf("edge: invalid error log config '%v'", y.Log.Error.Value) + errLevel, err := parseLogLevel(y.Log.Error.Value) + if err != nil { + return nil, err } - if v := strings.ToLower(strings.TrimSpace(y.Log.Audit.Value)); v != "on" && v != "off" && v != "" { - return nil, fmt.Errorf("edge: invalid audit log config '%v'", y.Log.Audit.Value) + auditLevel, err := parseLogLevel(y.Log.Audit.Value) + if err != nil { + return nil, err } for path, api := range y.API.Paths { if api.Timeout.Value < 0 { - return nil, fmt.Errorf("edge: invalid timeout '%d' for API '%s'", api.Timeout.Value, path) + return nil, fmt.Errorf("kesconf: invalid timeout '%d' for API '%s'", api.Timeout.Value, path) } } @@ -295,7 +308,7 @@ func ymlToServerConfig(y *yml) (*ServerConfig, error) { names := make(map[string]struct{}, len(y.Keys)) for _, key := range y.Keys { if _, ok := names[key.Name.Value]; ok { - return nil, fmt.Errorf("edge: invalid key config: key '%s' is defined multiple times", key.Name.Value) + return nil, fmt.Errorf("kesconf: invalid key config: key '%s' is defined multiple times", key.Name.Value) } names[key.Name.Value] = struct{}{} } @@ -306,13 +319,14 @@ func ymlToServerConfig(y *yml) (*ServerConfig, error) { return nil, err } - c := &ServerConfig{ + c := &File{ Addr: y.Addr.Value, Admin: y.Admin.Identity.Value, TLS: &TLSConfig{ PrivateKey: y.TLS.PrivateKey.Value, Certificate: y.TLS.Certificate.Value, Password: y.TLS.Password.Value, + ClientAuth: clientAuth, CAPath: y.TLS.CAPath.Value, ForwardCertHeader: y.TLS.Proxy.Header.ClientCert.Value, }, @@ -322,8 +336,8 @@ func ymlToServerConfig(y *yml) (*ServerConfig, error) { ExpiryOffline: y.Cache.Expiry.Offline.Value, }, Log: &LogConfig{ - Error: strings.TrimSpace(strings.ToLower(y.Log.Error.Value)) != "off", // default is "on" behavior - Audit: strings.TrimSpace(strings.ToLower(y.Log.Audit.Value)) == "on", // default is "off" behavior + ErrLevel: errLevel, + AuditLevel: auditLevel, }, KeyStore: keystore, } @@ -361,7 +375,7 @@ func ymlToServerConfig(y *yml) (*ServerConfig, error) { } for path, api := range y.API.Paths { if api.Timeout.Value < 0 { - return nil, fmt.Errorf("edge: invalid timeout '%d' for API '%s'", api.Timeout.Value, path) + return nil, fmt.Errorf("kesconf: invalid timeout '%d' for API '%s'", api.Timeout.Value, path) } } if len(y.Keys) > 0 { @@ -373,73 +387,44 @@ func ymlToServerConfig(y *yml) (*ServerConfig, error) { return c, nil } -func ymlToKeyStore(y *yml) (KeyStore, error) { +func ymlToKeyStore(y *ymlFile) (KeyStore, error) { var keystore KeyStore // FS Keystore if y.KeyStore.FS != nil { if y.KeyStore.FS.Path.Value == "" { - return nil, errors.New("edge: invalid fs keystore: no path specified") + return nil, errors.New("kesconf: invalid fs keystore: no path specified") } keystore = &FSKeyStore{ Path: y.KeyStore.FS.Path.Value, } } - // KES Keystore - if y.KeyStore.KES != nil { - if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") - } - endpoints := make([]string, 0, len(y.KeyStore.KES.Endpoint)) - for _, endpoint := range y.KeyStore.KES.Endpoint { - if e := strings.TrimSpace(endpoint.Value); e != "" { - endpoints = append(endpoints, e) - } - } - if len(endpoints) == 0 { - return nil, errors.New("edge: invalid kes keystore: no endpoint specified") - } - if y.KeyStore.KES.TLS.PrivateKey.Value == "" { - return nil, errors.New("edge: invalid kes keystore: no TLS private key specified") - } - if y.KeyStore.KES.TLS.Certificate.Value == "" { - return nil, errors.New("edge: invalid kes keystore: no TLS certificate specified") - } - keystore = &KESKeyStore{ - Endpoints: endpoints, - Enclave: y.KeyStore.KES.Enclave.Value, - PrivateKeyFile: y.KeyStore.KES.TLS.PrivateKey.Value, - CertificateFile: y.KeyStore.KES.TLS.Certificate.Value, - CAPath: y.KeyStore.KES.TLS.CAPath.Value, - } - } - // Hashicorp Vault Keystore if y.KeyStore.Vault != nil { if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") + return nil, errors.New("kesconf: invalid keystore config: more than once keystore specified") } if y.KeyStore.Vault.Endpoint.Value == "" { - return nil, errors.New("edge: invalid vault keystore: no endpoint specified") + return nil, errors.New("kesconf: invalid vault keystore: no endpoint specified") } if y.KeyStore.Vault.AppRole == nil && y.KeyStore.Vault.Kubernetes == nil { - return nil, errors.New("edge: invalid vault keystore: no authentication method specified") + return nil, errors.New("kesconf: invalid vault keystore: no authentication method specified") } if y.KeyStore.Vault.AppRole != nil && y.KeyStore.Vault.Kubernetes != nil { - return nil, errors.New("edge: invalid vault keystore: more than one authentication method specified") + return nil, errors.New("kesconf: invalid vault keystore: more than one authentication method specified") } if y.KeyStore.Vault.AppRole != nil { if y.KeyStore.Vault.AppRole.ID.Value == "" { - return nil, errors.New("edge: invalid vault keystore: invalid approle config: no approle ID specified") + return nil, errors.New("kesconf: invalid vault keystore: invalid approle config: no approle ID specified") } if y.KeyStore.Vault.AppRole.Secret.Value == "" { - return nil, errors.New("edge: invalid vault keystore: invalid approle config: no approle secret specified") + return nil, errors.New("kesconf: invalid vault keystore: invalid approle config: no approle secret specified") } } if y.KeyStore.Vault.Kubernetes != nil { if y.KeyStore.Vault.Kubernetes.JWT.Value == "" { - return nil, errors.New("edge: invalid vault keystore: invalid kubernetes config: no JWT specified") + return nil, errors.New("kesconf: invalid vault keystore: invalid kubernetes config: no JWT specified") } // If the passed JWT value contains a path separator we assume it's a file. @@ -448,22 +433,22 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { if jwt := y.KeyStore.Vault.Kubernetes.JWT.Value; strings.ContainsRune(jwt, '/') || strings.ContainsRune(jwt, os.PathSeparator) { b, err := os.ReadFile(y.KeyStore.Vault.Kubernetes.JWT.Value) if err != nil { - return nil, fmt.Errorf("edge: failed to read vault kubernetes JWT from '%s': %v", y.KeyStore.Vault.Kubernetes.JWT.Value, err) + return nil, fmt.Errorf("kesconf: failed to read vault kubernetes JWT from '%s': %v", y.KeyStore.Vault.Kubernetes.JWT.Value, err) } y.KeyStore.Vault.Kubernetes.JWT.Value = string(b) } } if y.KeyStore.Vault.Transit != nil { if y.KeyStore.Vault.Transit.KeyName.Value == "" { - return nil, errors.New("edge: invalid vault keystore: invalid transit config: no key name specified") + return nil, errors.New("kesconf: invalid vault keystore: invalid transit config: no key name specified") } } if y.KeyStore.Vault.TLS.PrivateKey.Value != "" && y.KeyStore.Vault.TLS.Certificate.Value == "" { - return nil, errors.New("edge: invalid vault keystore: invalid tls config: no TLS certificate provided") + return nil, errors.New("kesconf: invalid vault keystore: invalid tls config: no TLS certificate provided") } if y.KeyStore.Vault.TLS.PrivateKey.Value == "" && y.KeyStore.Vault.TLS.Certificate.Value != "" { - return nil, errors.New("edge: invalid vault keystore: invalid tls config: no TLS private key provided") + return nil, errors.New("kesconf: invalid vault keystore: invalid tls config: no TLS private key provided") } s := &VaultKeyStore{ Endpoint: y.KeyStore.Vault.Endpoint.Value, @@ -502,13 +487,13 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { // Fortanix SDKMS if y.KeyStore.Fortanix != nil && y.KeyStore.Fortanix.SDKMS != nil { if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") + return nil, errors.New("kesconf: invalid keystore config: more than once keystore specified") } if y.KeyStore.Fortanix.SDKMS.Endpoint.Value == "" { - return nil, errors.New("edge: invalid fortanix SDKMS keystore: no endpoint specified") + return nil, errors.New("kesconf: invalid fortanix SDKMS keystore: no endpoint specified") } if y.KeyStore.Fortanix.SDKMS.Login.APIKey.Value == "" { - return nil, errors.New("edge: invalid fortanix SDKMS keystore: no API key specified") + return nil, errors.New("kesconf: invalid fortanix SDKMS keystore: no API key specified") } keystore = &FortanixKeyStore{ Endpoint: y.KeyStore.Fortanix.SDKMS.Endpoint.Value, @@ -521,13 +506,13 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { // Thales CipherTrust / Gemalto KeySecure if y.KeyStore.Gemalto != nil && y.KeyStore.Gemalto.KeySecure != nil { if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") + return nil, errors.New("kesconf: invalid keystore config: more than once keystore specified") } if y.KeyStore.Gemalto.KeySecure.Endpoint.Value == "" { - return nil, errors.New("edge: invalid gemalto keysecure keystore: no endpoint specified") + return nil, errors.New("kesconf: invalid gemalto keysecure keystore: no endpoint specified") } if y.KeyStore.Gemalto.KeySecure.Login.Token.Value == "" { - return nil, errors.New("edge: invalid gemalto keysecure keystore: no token specified") + return nil, errors.New("kesconf: invalid gemalto keysecure keystore: no token specified") } keystore = &KeySecureKeyStore{ Endpoint: y.KeyStore.Gemalto.KeySecure.Endpoint.Value, @@ -540,10 +525,10 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { // GCP SecretManager if y.KeyStore.GCP != nil && y.KeyStore.GCP.SecretManager != nil { if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") + return nil, errors.New("kesconf: invalid keystore config: more than once keystore specified") } if y.KeyStore.GCP.SecretManager.ProjectID.Value == "" { - return nil, errors.New("edge: invalid GCP secretmanager keystore: no project ID specified") + return nil, errors.New("kesconf: invalid GCP secretmanager keystore: no project ID specified") } var scopes []string if len(y.KeyStore.GCP.SecretManager.Scopes) > 0 { @@ -566,13 +551,13 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { // AWS SecretsManager if y.KeyStore.AWS != nil && y.KeyStore.AWS.SecretsManager != nil { if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") + return nil, errors.New("kesconf: invalid keystore config: more than once keystore specified") } if y.KeyStore.AWS.SecretsManager.Endpoint.Value == "" { - return nil, errors.New("edge: invalid AWS secretsmanager keystore: no endpoint specified") + return nil, errors.New("kesconf: invalid AWS secretsmanager keystore: no endpoint specified") } if y.KeyStore.AWS.SecretsManager.Region.Value == "" { - return nil, errors.New("edge: invalid AWS secretsmanager keystore: no region specified") + return nil, errors.New("kesconf: invalid AWS secretsmanager keystore: no region specified") } keystore = &AWSSecretsManagerKeyStore{ Endpoint: y.KeyStore.AWS.SecretsManager.Endpoint.Value, @@ -587,31 +572,31 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { // Azure KeyVault if y.KeyStore.Azure != nil && y.KeyStore.Azure.KeyVault != nil { if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") + return nil, errors.New("kesconf: invalid keystore config: more than once keystore specified") } if y.KeyStore.Azure.KeyVault.Endpoint.Value == "" { - return nil, errors.New("edge: invalid Azure keyvault keystore: no endpoint specified") + return nil, errors.New("kesconf: invalid Azure keyvault keystore: no endpoint specified") } if y.KeyStore.Azure.KeyVault.Credentials == nil && y.KeyStore.Azure.KeyVault.ManagedIdentity == nil { - return nil, errors.New("edge: invalid Azure keyvault keystore: no authentication method specified") + return nil, errors.New("kesconf: invalid Azure keyvault keystore: no authentication method specified") } if y.KeyStore.Azure.KeyVault.Credentials != nil && y.KeyStore.Azure.KeyVault.ManagedIdentity != nil { - return nil, errors.New("edge: invalid Azure keyvault keystore: more than one authentication method specified") + return nil, errors.New("kesconf: invalid Azure keyvault keystore: more than one authentication method specified") } if y.KeyStore.Azure.KeyVault.Credentials != nil { if y.KeyStore.Azure.KeyVault.Credentials.TenantID.Value == "" { - return nil, errors.New("edge: invalid Azure keyvault keystore: no tenant ID specified") + return nil, errors.New("kesconf: invalid Azure keyvault keystore: no tenant ID specified") } if y.KeyStore.Azure.KeyVault.Credentials.ClientID.Value == "" { - return nil, errors.New("edge: invalid Azure keyvault keystore: no client ID specified") + return nil, errors.New("kesconf: invalid Azure keyvault keystore: no client ID specified") } if y.KeyStore.Azure.KeyVault.Credentials.Secret.Value == "" { - return nil, errors.New("edge: invalid Azure keyvault keystore: no client secret specified") + return nil, errors.New("kesconf: invalid Azure keyvault keystore: no client secret specified") } } if y.KeyStore.Azure.KeyVault.ManagedIdentity != nil { if y.KeyStore.Azure.KeyVault.ManagedIdentity.ClientID.Value == "" { - return nil, errors.New("edge: invalid Azure keyvault keystore: no client ID specified") + return nil, errors.New("kesconf: invalid Azure keyvault keystore: no client ID specified") } } s := &AzureKeyVaultKeyStore{ @@ -629,22 +614,22 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { } if y.KeyStore.Entrust != nil && y.KeyStore.Entrust.KeyControl != nil { if keystore != nil { - return nil, errors.New("edge: invalid keystore config: more than once keystore specified") + return nil, errors.New("kesconf: invalid keystore config: more than once keystore specified") } if y.KeyStore.Entrust.KeyControl.Endpoint.Value == "" { - return nil, errors.New("edge: invalid Entrust KeyControl keystore: no endpoint specified") + return nil, errors.New("kesconf: invalid Entrust KeyControl keystore: no endpoint specified") } if y.KeyStore.Entrust.KeyControl.VaultID.Value == "" { - return nil, errors.New("edge: invalid Entrust KeyControl keystore: no vault ID specified") + return nil, errors.New("kesconf: invalid Entrust KeyControl keystore: no vault ID specified") } if y.KeyStore.Entrust.KeyControl.BoxID.Value == "" { - return nil, errors.New("edge: invalid Entrust KeyControl keystore: no box ID specified") + return nil, errors.New("kesconf: invalid Entrust KeyControl keystore: no box ID specified") } if y.KeyStore.Entrust.KeyControl.Login.Username.Value == "" { - return nil, errors.New("edge: invalid Entrust KeyControl keystore: no username specified") + return nil, errors.New("kesconf: invalid Entrust KeyControl keystore: no username specified") } if y.KeyStore.Entrust.KeyControl.Login.Password.Value == "" { - return nil, errors.New("edge: invalid Entrust KeyControl keystore: no password specified") + return nil, errors.New("kesconf: invalid Entrust KeyControl keystore: no password specified") } keystore = &EntrustKeyControlKeyStore{ Endpoint: y.KeyStore.Entrust.KeyControl.Endpoint.Value, @@ -657,7 +642,7 @@ func ymlToKeyStore(y *yml) (KeyStore, error) { } if keystore == nil { - return nil, errors.New("edge: no keystore specified") + return nil, errors.New("kesconf: no keystore specified") } return keystore, nil } @@ -675,7 +660,7 @@ func (r env[T]) MarshalYAML() (any, error) { case !p && !s: return "${" + env + "}", nil default: - return nil, fmt.Errorf("edge: invalid env. variable reference '%s'", r.Var) + return nil, fmt.Errorf("kesconf: invalid env. variable reference '%s'", r.Var) } } return r.Value, nil @@ -687,7 +672,7 @@ func (r *env[T]) UnmarshalYAML(node *yaml.Node) error { env = strings.TrimSpace(v[2 : len(v)-1]) v, ok := os.LookupEnv(env) if !ok { - return fmt.Errorf("edge: line '%d' in YAML document: referenced env. variable '%s' not found", node.Line, env) + return fmt.Errorf("kesconf: referenced env. variable '%s' in line '%d' not found", env, node.Line) } node.Value = v } @@ -700,3 +685,67 @@ func (r *env[T]) UnmarshalYAML(node *yaml.Node) error { r.Value = v return nil } + +func parseLogLevel(s string) (slog.Level, error) { + const ( + LevelDebug = "DEBUG" + LevelInfo = "INFO" + LevelWarn = "WARN" + LevelError = "ERROR" + + // Pseudo-levels for backward compatibility. + LevelOn = "ON" // Equal to LevelInfo + LevelOff = "OFF" // Equal to LevelError+1 + ) + if s = strings.TrimSpace(strings.ToUpper(s)); s == "" { + return slog.LevelInfo, nil + } + if s == LevelOn { + return slog.LevelInfo, nil + } + if s == LevelOff { + return slog.LevelError + 1, nil + } + + parseLevel := func(val string, base slog.Level) (slog.Level, error) { + level, suffix, ok := strings.Cut(val, "+") + if !ok || strings.TrimSpace(level) != base.String() { + return 0, fmt.Errorf("kesconf: invalid log level '%s'", val) + } + + n, err := strconv.Atoi(suffix) + if err != nil { + return 0, fmt.Errorf("kesconf: invalid log level suffix '%s': %v", suffix, err) + } + return base + slog.Level(n), nil + } + + switch { + case strings.HasPrefix(s, LevelDebug): + if s == LevelDebug { + return slog.LevelDebug, nil + } + return parseLevel(s, slog.LevelDebug) + case strings.HasPrefix(s, LevelInfo): + if s == LevelInfo { + return slog.LevelInfo, nil + } + return parseLevel(s, slog.LevelInfo) + case strings.HasPrefix(s, LevelWarn): + if s == LevelWarn { + return slog.LevelWarn, nil + } + return parseLevel(s, slog.LevelWarn) + case strings.HasPrefix(s, LevelError): + if s == LevelError { + return slog.LevelError, nil + } + return parseLevel(s, slog.LevelError) + default: + n, err := strconv.Atoi(s) + if err != nil { + return 0, err + } + return slog.Level(n), nil + } +} diff --git a/edge/config_test.go b/kesconf/config_test.go similarity index 90% rename from edge/config_test.go rename to kesconf/config_test.go index 724f897b..b12e4fda 100644 --- a/edge/config_test.go +++ b/kesconf/config_test.go @@ -2,10 +2,9 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge +package kesconf import ( - "os" "testing" "time" ) @@ -13,16 +12,10 @@ import ( func TestReadServerConfigYAML_FS(t *testing.T) { const ( Filename = "./testdata/fs.yml" - - FSPath = "/tmp/keys" + FSPath = "/tmp/keys" ) - file, err := os.Open(Filename) - if err != nil { - t.Fatalf("Failed to access file '%s': %v", Filename, err) - } - - config, err := ReadServerConfigYAML(file) + config, err := ReadFile(Filename) if err != nil { t.Fatalf("Failed to read file '%s': %v", Filename, err) } @@ -49,12 +42,7 @@ func TestReadServerConfigYAML_CustomAPI(t *testing.T) { MetricsSkipAuth = true ) - file, err := os.Open(Filename) - if err != nil { - t.Fatalf("Failed to access file '%s': %v", Filename, err) - } - - config, err := ReadServerConfigYAML(file) + config, err := ReadFile(Filename) if err != nil { t.Fatalf("Failed to read file '%s': %v", Filename, err) } @@ -96,12 +84,7 @@ func TestReadServerConfigYAML_VaultWithAppRole(t *testing.T) { AppRoleSecret = "6a174c20-f6de-a53c-74d2-6018fcceff64" ) - file, err := os.Open(Filename) - if err != nil { - t.Fatalf("Failed to access file '%s': %v", Filename, err) - } - - config, err := ReadServerConfigYAML(file) + config, err := ReadFile(Filename) if err != nil { t.Fatalf("Failed to read file '%s': %v", Filename, err) } @@ -148,12 +131,7 @@ func TestReadServerConfigYAML_VaultWithK8S(t *testing.T) { K8SJWT = "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJQbGNNeTdBeXdLQmZMaGw2N1dFZkJvUmtsdnVvdkxXWGsteTc5TmJPeGMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJteS1uYW1lc3BhY2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoibXktc2VydmljZS1hY2NvdW50LXRva2VuLXA5NWRyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Im15LXNlcnZpY2UtYWNjb3VudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjdiYmViZGE2LTViMDUtNGFlNC05Yjg2LTBkODE0NWMwNzdhNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpteS1uYW1lc3BhY2U6bXktc2VydmljZS1hY2NvdW50In0.dnvJE3LU7L8XxsIOwea3lUZAULdwAjV9_crHFLKBGNxEu70lk3MQmUbGTEFvawryArmxMa1bWF9wbK1GHEsNipDgWAmc0rmBYByP_ahlf9bI2EEzpaGU5s194csB_eG7kvfi1AHED_nkVTfvCjIJM-9oGICCjDJcoNOP1NAXICFmqvWfXl6SY3UoZvtzUOcH9-0hbARY3p6V5pPecW4Dm-yGub9PKZLJNzv7GxChM-uvBvHAt6o0UBIL4iSy6Bx2l91ojB-RSkm_oy0W9gKi9ZFQPgyvcvQnEfjoGdvNGlOEdFEdXvl-dP6iLBPnZ5xwhAk8lK0oOONWvQg6VDNd9w" ) - file, err := os.Open(Filename) - if err != nil { - t.Fatalf("Failed to access file '%s': %v", Filename, err) - } - - config, err := ReadServerConfigYAML(file) + config, err := ReadFile(Filename) if err != nil { t.Fatalf("Failed to read file '%s': %v", Filename, err) } @@ -200,12 +178,7 @@ func TestReadServerConfigYAML_VaultWithK8S_JWTFile(t *testing.T) { K8SJWT = "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJQbGNNeTdBeXdLQmZMaGw2N1dFZkJvUmtsdnVvdkxXWGsteTc5TmJPeGMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJteS1uYW1lc3BhY2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoibXktc2VydmljZS1hY2NvdW50LXRva2VuLXA5NWRyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Im15LXNlcnZpY2UtYWNjb3VudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjdiYmViZGE2LTViMDUtNGFlNC05Yjg2LTBkODE0NWMwNzdhNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpteS1uYW1lc3BhY2U6bXktc2VydmljZS1hY2NvdW50In0.dnvJE3LU7L8XxsIOwea3lUZAULdwAjV9_crHFLKBGNxEu70lk3MQmUbGTEFvawryArmxMa1bWF9wbK1GHEsNipDgWAmc0rmBYByP_ahlf9bI2EEzpaGU5s194csB_eG7kvfi1AHED_nkVTfvCjIJM-9oGICCjDJcoNOP1NAXICFmqvWfXl6SY3UoZvtzUOcH9-0hbARY3p6V5pPecW4Dm-yGub9PKZLJNzv7GxChM-uvBvHAt6o0UBIL4iSy6Bx2l91ojB-RSkm_oy0W9gKi9ZFQPgyvcvQnEfjoGdvNGlOEdFEdXvl-dP6iLBPnZ5xwhAk8lK0oOONWvQg6VDNd9w" ) - file, err := os.Open(Filename) - if err != nil { - t.Fatalf("Failed to access file '%s': %v", Filename, err) - } - - config, err := ReadServerConfigYAML(file) + config, err := ReadFile(Filename) if err != nil { t.Fatalf("Failed to read file '%s': %v", Filename, err) } @@ -248,12 +221,7 @@ func TestReadServerConfigYAML_AWS(t *testing.T) { Secretkey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" ) - file, err := os.Open(Filename) - if err != nil { - t.Fatalf("Failed to access file '%s': %v", Filename, err) - } - - config, err := ReadServerConfigYAML(file) + config, err := ReadFile(Filename) if err != nil { t.Fatalf("Failed to read file '%s': %v", Filename, err) } @@ -291,12 +259,7 @@ func TestReadServerConfigYAML_AWS_NoCredentials(t *testing.T) { SessionToken = "" ) - file, err := os.Open(Filename) - if err != nil { - t.Fatalf("Failed to access file '%s': %v", Filename, err) - } - - config, err := ReadServerConfigYAML(file) + config, err := ReadFile(Filename) if err != nil { t.Fatalf("Failed to read file '%s': %v", Filename, err) } diff --git a/edge/edge_test.go b/kesconf/edge_test.go similarity index 60% rename from edge/edge_test.go rename to kesconf/edge_test.go index 9851dc10..addb7fb4 100644 --- a/edge/edge_test.go +++ b/kesconf/edge_test.go @@ -2,23 +2,24 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "bytes" "context" "errors" "fmt" + "io" "math/rand" "os" "os/signal" "testing" - "github.com/minio/kes-go" - "github.com/minio/kes/kv" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" ) -type SetupFunc func(context.Context, kv.Store[string, []byte], string) error +type SetupFunc func(context.Context, kes.KeyStore, string) error const ranStringLength = 8 @@ -32,14 +33,14 @@ var createTests = []struct { }, { // 1 Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, - Setup: func(ctx context.Context, s kv.Store[string, []byte], suffix string) error { + Setup: func(ctx context.Context, s kes.KeyStore, suffix string) error { return s.Create(ctx, "edge-test-"+suffix, []byte("t")) }, ShouldFail: true, }, } -func testCreate(ctx context.Context, store kv.Store[string, []byte], t *testing.T, seed string) { +func testCreate(ctx context.Context, store kes.KeyStore, t *testing.T, seed string) { defer clean(ctx, store, t) for i, test := range createTests { if test.Setup != nil { @@ -61,45 +62,6 @@ func testCreate(ctx context.Context, store kv.Store[string, []byte], t *testing. } } -var setTests = []struct { - Args map[string][]byte - Setup SetupFunc - ShouldFail bool -}{ - { // 0 - Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, - }, - { // 1 - Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, - Setup: func(ctx context.Context, s kv.Store[string, []byte], sufffix string) error { - return s.Create(ctx, "edge-test-"+sufffix, []byte("t")) - }, - ShouldFail: true, - }, -} - -func testSet(ctx context.Context, store kv.Store[string, []byte], t *testing.T, seed string) { - defer clean(ctx, store, t) - for i, test := range setTests { - if test.Setup != nil { - if err := test.Setup(ctx, store, fmt.Sprintf("%s-%d", seed, i)); err != nil { - t.Fatalf("Test %d: failed to setup: %v", i, err) - } - } - - for key, value := range test.Args { - secretKet := fmt.Sprintf("%s-%s-%d", key, seed, i) - err := store.Create(ctx, secretKet, value) - if err != nil && !test.ShouldFail { - t.Errorf("Test %d: failed to set key '%s': %v", i, secretKet, err) - } - if err == nil && test.ShouldFail { - t.Errorf("Test %d: setting key '%s' should have failed: %v", i, secretKet, err) - } - } - } -} - var getTests = []struct { Args map[string][]byte Setup SetupFunc @@ -107,7 +69,7 @@ var getTests = []struct { }{ { // 0 Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, - Setup: func(ctx context.Context, s kv.Store[string, []byte], suffix string) error { + Setup: func(ctx context.Context, s kes.KeyStore, suffix string) error { return s.Create(ctx, "edge-test-"+suffix, []byte("edge-test-value")) }, }, @@ -117,14 +79,14 @@ var getTests = []struct { }, { // 1 Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, - Setup: func(ctx context.Context, s kv.Store[string, []byte], suffix string) error { + Setup: func(ctx context.Context, s kes.KeyStore, suffix string) error { return s.Create(ctx, "edge-test-"+suffix, []byte("edge-test-value2")) }, ShouldFail: true, }, } -func testGet(ctx context.Context, store kv.Store[string, []byte], t *testing.T, seed string) { +func testGet(ctx context.Context, store kes.KeyStore, t *testing.T, seed string) { defer clean(ctx, store, t) for i, test := range getTests { if test.Setup != nil { @@ -151,7 +113,7 @@ func testGet(ctx context.Context, store kv.Store[string, []byte], t *testing.T, } } -func testStatus(ctx context.Context, store kv.Store[string, []byte], t *testing.T) { +func testStatus(ctx context.Context, store kes.KeyStore, t *testing.T) { if _, err := store.Status(ctx); err != nil { t.Fatalf("Failed to fetch status: %v", err) } @@ -167,19 +129,20 @@ func testingContext(t *testing.T) (context.Context, context.CancelFunc) { return context.WithDeadline(osCtx, d) } -func clean(ctx context.Context, store kv.Store[string, []byte], t *testing.T) { - iter, err := store.List(ctx) - if err != nil { - t.Fatalf("Cleanup: failed to list keys: %v", err) +func clean(ctx context.Context, store kes.KeyStore, t *testing.T) { + iter := kesdk.ListIter[string]{ + NextFunc: store.List, } - defer iter.Close() var names []string - for next, ok := iter.Next(); ok; next, ok = iter.Next() { + for next, err := iter.Next(ctx); err != io.EOF; next, err = iter.Next(ctx) { + if err != nil { + t.Errorf("Cleanup: failed to list: %v", err) + } names = append(names, next) } for _, name := range names { - if err = store.Delete(ctx, name); err != nil && !errors.Is(err, kes.ErrKeyNotFound) { + if err := store.Delete(ctx, name); err != nil && !errors.Is(err, kesdk.ErrKeyNotFound) { t.Errorf("Cleanup: failed to delete '%s': %v", name, err) } } diff --git a/edge/server-config.go b/kesconf/file.go similarity index 79% rename from edge/server-config.go rename to kesconf/file.go index 8cecabb9..250048f2 100644 --- a/edge/server-config.go +++ b/kesconf/file.go @@ -2,16 +2,22 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge +package kesconf import ( "context" "crypto/tls" "crypto/x509" "errors" + "fmt" + "io" + "log/slog" + "os" + "slices" "time" - "github.com/minio/kes-go" + "github.com/minio/kes" + kesdk "github.com/minio/kes-go" "github.com/minio/kes/internal/https" "github.com/minio/kes/internal/keystore/aws" "github.com/minio/kes/internal/keystore/azure" @@ -20,14 +26,53 @@ import ( "github.com/minio/kes/internal/keystore/fs" "github.com/minio/kes/internal/keystore/gcp" "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/kv" + yaml "gopkg.in/yaml.v3" ) -// ServerConfig is a structure that holds configuration -// for a KES edge server. -type ServerConfig struct { +// ReadFile opens the given file and reads the KES configuration +// from it by calling ReadFrom. +func ReadFile(filename string) (*File, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() // make sure to close file in case of panic + + file, err := ReadFrom(f) + if cErr := f.Close(); err == nil { + err = cErr + } + return file, err +} + +// ReadFrom parses and returns a new KES server configuration file +// from r. +func ReadFrom(r io.Reader) (*File, error) { + var node yaml.Node + if err := yaml.NewDecoder(r).Decode(&node); err != nil { + return nil, err + } + + version, err := findVersion(&node) + if err != nil { + return nil, err + } + const Version = "v1" + if version != "" && version != Version { + return nil, fmt.Errorf("edge: invalid server config version '%s'", version) + } + + var y ymlFile + if err := node.Decode(&y); err != nil { + return nil, err + } + return ymlToServerConfig(&y) +} + +// File is a structure that holds the content of a KES server +// configuration file. +type File struct { // Addr is the network interface address // and optional port the KES server will // listen on and accept HTTP requests. @@ -54,6 +99,7 @@ type ServerConfig struct { // Log contains the KES server logging configuration. Log *LogConfig + // APU contains the KES server API configuration. API *APIConfig // Policies contains the KES server policy definitions @@ -68,8 +114,107 @@ type ServerConfig struct { // The KeyStore manages the keys used by the KES server for // encryption and decryption. KeyStore KeyStore +} + +// TLSConfig returns a new TLS configuration as specified by +// the File. It returns nil and no error if File.TLS is nil. +func (f *File) TLSConfig() (*tls.Config, error) { + if f.TLS == nil { + return nil, nil + } + + certificate, err := https.CertificateFromFile(f.TLS.Certificate, f.TLS.PrivateKey, f.TLS.Password) + if err != nil { + return nil, fmt.Errorf("failed to read TLS certificate: %v", err) + } + if certificate.Leaf != nil { + if len(certificate.Leaf.DNSNames) == 0 && len(certificate.Leaf.IPAddresses) == 0 { + // Support for TLS certificates with a subject CN but without any SAN + // has been removed in Go 1.15. Ref: https://go.dev/doc/go1.15#commonname + // Therefore, we require at least one SAN for the server certificate. + return nil, fmt.Errorf("invalid TLS certificate: certificate does not contain any DNS or IP address as SAN") + } + } - _ [0]int // force usage of struct composite literals with field names + var rootCAs *x509.CertPool + if f.TLS.CAPath != "" { + rootCAs, err = https.CertPoolFromFile(f.TLS.CAPath) + if err != nil { + return nil, fmt.Errorf("failed to read TLS CA certificates: %v", err) + } + } + + return &tls.Config{ + MinVersion: tls.VersionTLS12, + ClientAuth: f.TLS.ClientAuth, + Certificates: []tls.Certificate{certificate}, + NextProtos: []string{"h2", "http/1.1"}, + RootCAs: rootCAs, + }, nil +} + +// Config returns a new KES configuration as specified by +// the File. It connects to the KeyStore using the given +// context. +func (f *File) Config(ctx context.Context) (*kes.Config, error) { + conf := &kes.Config{ + Admin: f.Admin, + } + + if f.TLS != nil { + tlsConf, err := f.TLSConfig() + if err != nil { + return nil, err + } + conf.TLS = tlsConf + } + + if f.Cache != nil { + conf.Cache = &kes.CacheConfig{ + Expiry: f.Cache.Expiry, + ExpiryUnused: f.Cache.ExpiryUnused, + ExpiryOffline: f.Cache.ExpiryOffline, + } + } + + if f.API != nil && len(f.API.Paths) > 0 { + conf.Routes = make(map[string]kes.RouteConfig, len(f.API.Paths)) + for path, config := range f.API.Paths { + conf.Routes[path] = kes.RouteConfig{ + Timeout: config.Timeout, + InsecureSkipAuth: config.InsecureSkipAuth, + } + } + } + + var policies map[string]kes.Policy + if len(f.Policies) > 0 { + policies = make(map[string]kes.Policy, len(f.Policies)) + for name, policy := range f.Policies { + p := kes.Policy{ + Allow: make(map[string]kesdk.Rule, len(policy.Allow)), + Deny: make(map[string]kesdk.Rule, len(policy.Deny)), + Identities: slices.Clone(policy.Identities), + } + for _, pattern := range policy.Allow { + p.Allow[pattern] = struct{}{} + } + for _, pattern := range policy.Deny { + p.Deny[pattern] = struct{}{} + } + policies[name] = p + } + conf.Policies = policies + } + + if f.KeyStore != nil { + keystore, err := f.KeyStore.Connect(ctx) + if err != nil { + return nil, err + } + conf.Keys = keystore + } + return conf, nil } // TLSConfig is a structure that holds the TLS configuration @@ -85,6 +230,12 @@ type TLSConfig struct { // private key. Password string + // ClientAuth is the client authentication type the KES server + // uses to verify client certificates. + // + // Most applications should use tls.RequestClientCert. + ClientAuth tls.ClientAuthType + // 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 @@ -105,8 +256,6 @@ type TLSConfig struct { // TLS / HTTPS proxy to forward the actual client certificate // to KES. ForwardCertHeader string - - _ [0]int } // CacheConfig is a structure that holds the Cache configuration @@ -134,8 +283,6 @@ type CacheConfig struct { // available. As long as the keystore is available, the regular // cache expiry periods apply. ExpiryOffline time.Duration - - _ [0]int } // LogConfig is a structure that holds the logging configuration @@ -143,13 +290,11 @@ type CacheConfig struct { type LogConfig struct { // Error determines whether the KES server logs error events to STDERR. // It does not en/disable error logging in general. - Error bool + ErrLevel slog.Level // Audit determines whether the KES server logs audit events to STDOUT. // It does not en/disable audit logging in general. - Audit bool - - _ [0]int + AuditLevel slog.Level } // APIConfig is a structure that holds the API configuration @@ -158,8 +303,6 @@ 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 @@ -179,8 +322,6 @@ type APIPathConfig struct { // cases for APIs that don't expose sensitive information, // like metrics. InsecureSkipAuth bool - - _ [0]int } // Policy is a structure defining a KES policy. @@ -204,8 +345,6 @@ type Policy struct { // It must not contain the admin or any // TLS proxy identity. Identities []kes.Identity - - _ [0]int } // Key is a structure defining a cryptographic key @@ -214,8 +353,6 @@ type Policy struct { type Key struct { // Name is the name of the cryptographic key. Name string - - _ [0]int } // KeyStore is a KES keystore configuration. @@ -225,7 +362,7 @@ type Key struct { type KeyStore interface { // Connect establishes and returns a new connection // to the keystore. - Connect(ctx context.Context) (kv.Store[string, []byte], error) + Connect(ctx context.Context) (kes.KeyStore, error) } // FSKeyStore is a structure containing the configuration @@ -239,57 +376,13 @@ type FSKeyStore struct { // If the directory does not exist, it // will be created. Path string - - _ [0]int } // 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) { +func (s *FSKeyStore) Connect(context.Context) (kes.KeyStore, error) { return fs.NewStore(s.Path) } -// KESKeyStore is a structure containing the configuration -// for using a KES server/cluster as key store. -type KESKeyStore struct { - // Endpoints is a set of KES server endpoints. - // - // If multiple endpoints are provided, the requests - // will be automatically balanced across them. - Endpoints []string - - // Enclave is an optional enclave name. If empty, - // the default enclave name will be used. - Enclave string - - // CertificateFile is a path to a mTLS client - // certificate file used to authenticate to - // the KES server. - CertificateFile string - - // PrivateKeyFile is a path to a mTLS private - // key used to authenticate to the KES server. - PrivateKeyFile 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 string -} - -// 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 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 // for Hashicorp Vault. type VaultKeyStore struct { @@ -359,8 +452,6 @@ type VaultKeyStore struct { // is checked. // If not set, defaults to 10s. StatusPing time.Duration - - _ [0]int } // VaultAppRoleAuth is a structure containing the configuration @@ -408,7 +499,7 @@ type VaultTransit struct { } // Connect returns a kv.Store that stores key-value pairs on a Hashicorp Vault server. -func (s *VaultKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { +func (s *VaultKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { if s.AppRole == nil && s.Kubernetes == nil { return nil, errors.New("edge: failed to connect to hashicorp vault: no authentication method specified") } @@ -469,12 +560,10 @@ type FortanixKeyStore struct { // If empty, the OS default root CA set is // used. CAPath string - - _ [0]int } // 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) { +func (s *FortanixKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { return fortanix.Connect(ctx, &fortanix.Config{ Endpoint: s.Endpoint, GroupID: s.GroupID, @@ -506,12 +595,10 @@ type KeySecureKeyStore struct { // If empty, the OS default root CA set is // used. CAPath string - - _ [0]int } // 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) { +func (s *KeySecureKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { return gemalto.Connect(ctx, &gemalto.Config{ Endpoint: s.Endpoint, CAPath: s.CAPath, @@ -557,12 +644,10 @@ type GCPSecretManagerKeyStore struct { // service account used to access the // SecretManager. Key string - - _ [0]int } // 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) { +func (s *GCPSecretManagerKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { return gcp.Connect(ctx, &gcp.Config{ Endpoint: s.Endpoint, ProjectID: s.ProjectID, @@ -603,12 +688,10 @@ type AWSSecretsManagerKeyStore struct { // SessionToken is an optional session token for authenticating // to AWS. SessionToken string - - _ [0]int } // 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) { +func (s *AWSSecretsManagerKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { return aws.Connect(ctx, &aws.Config{ Addr: s.Endpoint, Region: s.Region, @@ -641,12 +724,10 @@ type AzureKeyVaultKeyStore struct { // ManagedIdentityClientID is the client ID of the // Azure managed identity that access the KeyVault. ManagedIdentityClientID string - - _ [0]int } // Connect returns a kv.Store that stores key-value pairs on Azure KeyVault. -func (s *AzureKeyVaultKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { +func (s *AzureKeyVaultKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { if (s.TenantID != "" || s.ClientID != "" || s.ClientSecret != "") && s.ManagedIdentityClientID != "" { return nil, errors.New("edge: failed to connect to Azure KeyVault: more than one authentication method specified") } @@ -696,7 +777,7 @@ type EntrustKeyControlKeyStore struct { } // Connect returns a kv.Store that stores key-value pairs on Entrust KeyControl. -func (s *EntrustKeyControlKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) { +func (s *EntrustKeyControlKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { var rootCAs *x509.CertPool if s.CAPath != "" { ca, err := https.CertPoolFromFile(s.CAPath) diff --git a/edge/fortanix_test.go b/kesconf/fortanix_test.go similarity index 71% rename from edge/fortanix_test.go rename to kesconf/fortanix_test.go index a69df17f..3e8ebe85 100644 --- a/edge/fortanix_test.go +++ b/kesconf/fortanix_test.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" - "os" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var fortanixConfigFile = flag.String("fortanix.config", "", "Path to a KES config file with Fortanix SDKMS config") @@ -18,19 +17,14 @@ func TestFortanix(t *testing.T) { if *fortanixConfigFile == "" { t.Skip("Fortanix tests disabled. Use -fortanix.config= to enable them") } - file, err := os.Open(*fortanixConfigFile) - if err != nil { - t.Fatal(err) - } - defer file.Close() - config, err := edge.ReadServerConfigYAML(file) + config, err := kesconf.ReadFile(*fortanixConfigFile) if err != nil { t.Fatal(err) } - if _, ok := config.KeyStore.(*edge.FortanixKeyStore); !ok { - t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &edge.FortanixKeyStore{}) + if _, ok := config.KeyStore.(*kesconf.FortanixKeyStore); !ok { + t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.FortanixKeyStore{}) } ctx, cancel := testingContext(t) @@ -42,7 +36,6 @@ func TestFortanix(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/edge/fs_test.go b/kesconf/fs_test.go similarity index 82% rename from edge/fs_test.go rename to kesconf/fs_test.go index 52d9d640..2336b873 100644 --- a/edge/fs_test.go +++ b/kesconf/fs_test.go @@ -2,13 +2,13 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var FSPath = flag.String("fs.path", "", "Path used for FS tests") @@ -17,7 +17,7 @@ func TestFS(t *testing.T) { if *FSPath == "" { t.Skip("FS tests disabled. Use -fs.path= to enable them") } - config := edge.FSKeyStore{ + config := kesconf.FSKeyStore{ Path: *FSPath, } @@ -30,7 +30,6 @@ func TestFS(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/edge/gcp_test.go b/kesconf/gcp_test.go similarity index 70% rename from edge/gcp_test.go rename to kesconf/gcp_test.go index caccb8ad..f3e6d131 100644 --- a/edge/gcp_test.go +++ b/kesconf/gcp_test.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" - "os" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var gcpConfigFile = flag.String("gcp.config", "", "Path to a KES config file with GCP SecretManager config") @@ -18,19 +17,14 @@ func TestGCP(t *testing.T) { if *gcpConfigFile == "" { t.Skip("GCP tests disabled. Use -gcp.config= to enable them") } - file, err := os.Open(*gcpConfigFile) - if err != nil { - t.Fatal(err) - } - defer file.Close() - config, err := edge.ReadServerConfigYAML(file) + config, err := kesconf.ReadFile(*gcpConfigFile) if err != nil { t.Fatal(err) } - if _, ok := config.KeyStore.(*edge.GCPSecretManagerKeyStore); !ok { - t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &edge.GCPSecretManagerKeyStore{}) + if _, ok := config.KeyStore.(*kesconf.GCPSecretManagerKeyStore); !ok { + t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.GCPSecretManagerKeyStore{}) } ctx, cancel := testingContext(t) @@ -42,7 +36,6 @@ func TestGCP(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/edge/gemalto_test.go b/kesconf/gemalto_test.go similarity index 71% rename from edge/gemalto_test.go rename to kesconf/gemalto_test.go index bdefbbc8..41785706 100644 --- a/edge/gemalto_test.go +++ b/kesconf/gemalto_test.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" - "os" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var gemaltoConfigFile = flag.String("gemalto.config", "", "Path to a KES config file with Gemalto KeySecure config") @@ -18,19 +17,14 @@ func TestGemalto(t *testing.T) { if *gemaltoConfigFile == "" { t.Skip("Gemalto tests disabled. Use -gemalto.config= to enable them") } - file, err := os.Open(*gemaltoConfigFile) - if err != nil { - t.Fatal(err) - } - defer file.Close() - config, err := edge.ReadServerConfigYAML(file) + config, err := kesconf.ReadFile(*gemaltoConfigFile) if err != nil { t.Fatal(err) } - if _, ok := config.KeyStore.(*edge.KeySecureKeyStore); !ok { - t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &edge.KeySecureKeyStore{}) + if _, ok := config.KeyStore.(*kesconf.KeySecureKeyStore); !ok { + t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.KeySecureKeyStore{}) } ctx, cancel := testingContext(t) @@ -42,7 +36,6 @@ func TestGemalto(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/edge/keycontrol_test.go b/kesconf/keycontrol_test.go similarity index 70% rename from edge/keycontrol_test.go rename to kesconf/keycontrol_test.go index 11608a5c..e5d10a8d 100644 --- a/edge/keycontrol_test.go +++ b/kesconf/keycontrol_test.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" - "os" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var keyControlConfigFile = flag.String("entrust.config", "", "Path to a KES config file with Entrust KeyControl config") @@ -18,19 +17,14 @@ func TestKeyControl(t *testing.T) { if *keyControlConfigFile == "" { t.Skip("KeyControl tests disabled. Use -entrust.config= to enable them") } - file, err := os.Open(*keyControlConfigFile) - if err != nil { - t.Fatal(err) - } - defer file.Close() - config, err := edge.ReadServerConfigYAML(file) + config, err := kesconf.ReadFile(*keyControlConfigFile) if err != nil { t.Fatal(err) } - if _, ok := config.KeyStore.(*edge.EntrustKeyControlKeyStore); !ok { - t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &edge.EntrustKeyControlKeyStore{}) + if _, ok := config.KeyStore.(*kesconf.EntrustKeyControlKeyStore); !ok { + t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.EntrustKeyControlKeyStore{}) } ctx, cancel := testingContext(t) @@ -42,7 +36,6 @@ func TestKeyControl(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/edge/testdata/aws-no-credentials.yml b/kesconf/testdata/aws-no-credentials.yml similarity index 100% rename from edge/testdata/aws-no-credentials.yml rename to kesconf/testdata/aws-no-credentials.yml diff --git a/edge/testdata/aws.yml b/kesconf/testdata/aws.yml similarity index 100% rename from edge/testdata/aws.yml rename to kesconf/testdata/aws.yml diff --git a/edge/testdata/custom-api.yml b/kesconf/testdata/custom-api.yml similarity index 100% rename from edge/testdata/custom-api.yml rename to kesconf/testdata/custom-api.yml diff --git a/edge/testdata/fs.yml b/kesconf/testdata/fs.yml similarity index 100% rename from edge/testdata/fs.yml rename to kesconf/testdata/fs.yml diff --git a/edge/testdata/vault-approle.yml b/kesconf/testdata/vault-approle.yml similarity index 100% rename from edge/testdata/vault-approle.yml rename to kesconf/testdata/vault-approle.yml diff --git a/edge/testdata/vault-k8s-service-account b/kesconf/testdata/vault-k8s-service-account similarity index 100% rename from edge/testdata/vault-k8s-service-account rename to kesconf/testdata/vault-k8s-service-account diff --git a/edge/testdata/vault-k8s-with-service-account-file.yml b/kesconf/testdata/vault-k8s-with-service-account-file.yml similarity index 100% rename from edge/testdata/vault-k8s-with-service-account-file.yml rename to kesconf/testdata/vault-k8s-with-service-account-file.yml diff --git a/edge/testdata/vault-k8s.yml b/kesconf/testdata/vault-k8s.yml similarity index 100% rename from edge/testdata/vault-k8s.yml rename to kesconf/testdata/vault-k8s.yml diff --git a/edge/testdata/vault.yml b/kesconf/testdata/vault.yml similarity index 100% rename from edge/testdata/vault.yml rename to kesconf/testdata/vault.yml diff --git a/edge/vault_test.go b/kesconf/vault_test.go similarity index 71% rename from edge/vault_test.go rename to kesconf/vault_test.go index 77da9d8a..00f15d8c 100644 --- a/edge/vault_test.go +++ b/kesconf/vault_test.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the AGPLv3 // license that can be found in the LICENSE file. -package edge_test +package kesconf_test import ( "flag" - "os" "testing" - "github.com/minio/kes/edge" + "github.com/minio/kes/kesconf" ) var vaultConfigFile = flag.String("vault.config", "", "Path to a KES config file with Hashicorp Vault config") @@ -18,19 +17,14 @@ func TestVault(t *testing.T) { if *vaultConfigFile == "" { t.Skip("Vault tests disabled. Use -vault.config= to enable them") } - file, err := os.Open(*vaultConfigFile) - if err != nil { - t.Fatal(err) - } - defer file.Close() - config, err := edge.ReadServerConfigYAML(file) + config, err := kesconf.ReadFile(*vaultConfigFile) if err != nil { t.Fatal(err) } - if _, ok := config.KeyStore.(*edge.VaultKeyStore); !ok { - t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &edge.VaultKeyStore{}) + if _, ok := config.KeyStore.(*kesconf.VaultKeyStore); !ok { + t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.VaultKeyStore{}) } ctx, cancel := testingContext(t) @@ -42,7 +36,6 @@ func TestVault(t *testing.T) { } t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) - t.Run("Set", func(t *testing.T) { testSet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) } diff --git a/keystore.go b/keystore.go index ba3dccdd..e2542ded 100644 --- a/keystore.go +++ b/keystore.go @@ -16,7 +16,7 @@ import ( "github.com/minio/kes-go" "github.com/minio/kes/internal/cache" "github.com/minio/kes/internal/key" - "github.com/minio/kes/kv" + "github.com/minio/kes/internal/keystore" ) // A KeyStore stores key-value pairs. It provides durable storage for a @@ -72,6 +72,8 @@ type MemKeyStore struct { var _ KeyStore = (*MemKeyStore)(nil) // compiler check +func (ks *MemKeyStore) String() string { return "In Memory" } + // Status returns the current state of the MemKeyStore. // It never returns an error. func (ks *MemKeyStore) Status(context.Context) (KeyStoreState, error) { @@ -244,7 +246,7 @@ type cacheEntry struct { // reachable and offline caching is enabled. func (c *keyCache) Status(ctx context.Context) (KeyStoreState, error) { if c.offline.Load() { - return KeyStoreState{}, &kv.Unreachable{Err: errors.New("keystore is offline")} + return KeyStoreState{}, &keystore.ErrUnreachable{Err: errors.New("keystore is offline")} } return c.store.Status(ctx) } diff --git a/kv/error.go b/kv/error.go deleted file mode 100644 index 0ac7f2a1..00000000 --- a/kv/error.go +++ /dev/null @@ -1,79 +0,0 @@ -// 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 - -import ( - "errors" - "net" -) - -// Unreachable is an error that indicates that the -// Store 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 "kv: not reachable" - } - return "kv: not reachable: " + 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 { - var err net.Error - if errors.As(e.Err, &err) { - return err.Timeout() - } - return false -} - -// Unavailable is an error that indicates that the -// Store is reachable over the network but not ready -// to process requests - e.g. the Store 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 "kv: not available" - } - return "kv: 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 deleted file mode 100644 index 7490348e..00000000 --- a/kv/example_test.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index 1b32fdde..00000000 --- a/kv/iter.go +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index 6cc16c4f..00000000 --- a/kv/store.go +++ /dev/null @@ -1,88 +0,0 @@ -// 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 provides abstractions over key-value based -// storage. -package kv - -import ( - "context" - "errors" - "io" - "time" -) - -var ( - // ErrExists is returned by a Store when trying to create - // an entry but the key already exists. - ErrExists = errors.New("kv: key already exists") - - // ErrNotExists is returned by a Store when trying to - // access an entry but the key does not exist. - ErrNotExists = errors.New("kv: key does not exist") -) - -// Store stores key-value pairs. -// -// Multiple goroutines may invoke methods -// on a Store simultaneously. -type Store[K comparable, V any] interface { - // Status returns the current state of the - // Store or an error explaining why fetching - // status information failed. - // - // Status returns Unreachable when it fails - // to reach the storage. - // - // Status returns Unavailable when it reached - // the store but the storage is currently not - // able to process any requests or load/store - // entries. - Status(context.Context) (State, error) - - // Create creates a new entry at the - // storage if and only if no entry for - // the give key exists. - // - // If such an entry already exists, - // Create returns ErrExists. - Create(context.Context, K, V) error - - // Set writes the key-value pair to the - // storage. - // - // The store may return ErrExists if such - // an entry already exists. Further, if - // no such entry exists, Set may return - // ErrNotExists to signal that an entry - // has to be created first. - Set(context.Context, K, V) error - - // Get returns the value associated with - // the given key. - // - // It returns ErrNotExists if no such - // entry exists. - Get(context.Context, K) (V, error) - - // Delete deletes the key and the associated - // value from the storage. - // - // It returns ErrNotExists if no such - // entry exists. - Delete(context.Context, K) error - - // List returns an Iter enumerating the stored - // entries. - List(context.Context) (Iter[K], error) - - io.Closer -} - -// State describes the state of a Store. -type State struct { - // Latency is the connection latency - // to the Store. - Latency time.Duration -} diff --git a/server-config.yaml b/server-config.yaml index fb6cd739..22e9abe7 100644 --- a/server-config.yaml +++ b/server-config.yaml @@ -20,6 +20,11 @@ tls: key: ./server.key # Path to the TLS private key cert: ./server.cert # Path to the TLS certificate password: "" # An optional password to decrypt the TLS private key + + # Specify how/whether the KES server verifies certificates presented + # by clients. Valid values are "on" and "off". Defaults to off, which + # is recommended for most use cases. + auth: "" # An optional path to a file or directory containing X.509 certificate(s). # If set, the certificate(s) get added to the list of CA certificates for diff --git a/server.go b/server.go index 5b3de36f..95a571d5 100644 --- a/server.go +++ b/server.go @@ -28,12 +28,17 @@ import ( "github.com/minio/kes/internal/fips" "github.com/minio/kes/internal/headers" "github.com/minio/kes/internal/key" + "github.com/minio/kes/internal/keystore" "github.com/minio/kes/internal/metric" "github.com/minio/kes/internal/sys" - "github.com/minio/kes/kv" "github.com/prometheus/common/expfmt" ) +// An Identity should uniquely identify a client and +// is computed from the X.509 certificate presented +// by the client during the TLS handshake. +type Identity = kes.Identity + // ServerShutdownTimeout is the default time period the server // waits while trying to shutdown gracefully before forcefully // closing connections. @@ -463,7 +468,7 @@ func (s *Server) version(resp *api.Response, req *api.Request) { func (s *Server) ready(resp *api.Response, req *api.Request) { _, err := s.state.Load().Keys.Status(req.Context()) - if _, ok := kv.IsUnreachable(err); ok { + if _, ok := keystore.IsUnreachable(err); ok { s.state.Load().Log.WarnContext(req.Context(), err.Error(), "req", req) resp.Fail(http.StatusGatewayTimeout, "key store is not reachable") return