From 752f18677323a7f9d374669f6e563592e67ad99e Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Wed, 7 Sep 2022 15:56:58 +0200 Subject: [PATCH] cmd: add enclave commands This commit adds the new enclave command group with two sub-commands: - `kes enclave create` - `kes enclave rm` Now, the KES system admin can create and delete enclaves programmatically via the CLI. Signed-off-by: Andreas Auernhammer --- client.go | 67 +++++++++++++++++++- cmd/kes/enclave.go | 148 +++++++++++++++++++++++++++++++++++++++++++++ cmd/kes/main.go | 2 + enclave.go | 2 +- 4 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 cmd/kes/enclave.go diff --git a/client.go b/client.go index 0f5c06c7..7cd12886 100644 --- a/client.go +++ b/client.go @@ -5,6 +5,7 @@ package kes import ( + "bytes" "context" "crypto/tls" "encoding/json" @@ -13,6 +14,7 @@ import ( "math" "net" "net/http" + "net/url" "path" "strings" "time" @@ -225,10 +227,64 @@ func (c *Client) APIs(ctx context.Context) ([]API, error) { return apis, nil } +// CreateEnclave creates a new enclave with the given +// identity as enclave admin. Only the KES system +// admin can create new enclaves. +// +// It returns ErrEnclaveExists if the enclave already +// exists. +func (c *Client) CreateEnclave(ctx context.Context, name string, admin Identity) error { + const ( + APIPath = "/v1/enclave/create" + Method = http.MethodPost + StatusOK = http.StatusOK + ) + type Request struct { + Admin Identity `json:"admin"` + } + body, err := json.Marshal(Request{ + Admin: admin, + }) + if err != nil { + return err + } + client := retry(c.HTTPClient) + resp, err := client.Send(ctx, Method, c.Endpoints, join(APIPath, name), bytes.NewReader(body)) + if err != nil { + return err + } + if resp.StatusCode != StatusOK { + return parseErrorResponse(resp) + } + return nil +} + +// DeleteEnclave delete the specified enclave. Only the +// KES system admin can delete enclaves. +// +// It returns ErrEnclaveNotFound if the enclave does not +// exist. +func (c *Client) DeleteEnclave(ctx context.Context, name string) error { + const ( + APIPath = "/v1/enclave/delete" + Method = http.MethodDelete + StatusOK = http.StatusOK + ) + client := retry(c.HTTPClient) + resp, err := client.Send(ctx, Method, c.Endpoints, join(APIPath, name), nil) + if err != nil { + return err + } + if resp.StatusCode != StatusOK { + return parseErrorResponse(resp) + } + return nil +} + // CreateKey creates a new cryptographic key. The key will // be generated by the KES server. // -// It returns ErrKeyExists if a key with the same key already +// It returns ErrKeyExists if a key with the same name already // exists. func (c *Client) CreateKey(ctx context.Context, name string) error { enclave := Enclave{ @@ -614,6 +670,15 @@ func (c *Client) Metrics(ctx context.Context) (Metric, error) { return metric, nil } +// Join joins any number of arguments with the API path. +// All arguments are path-escaped before joining. +func join(api string, args ...string) string { + for _, arg := range args { + api = path.Join(api, url.PathEscape(arg)) + } + return api +} + // endpoint returns an endpoint URL starting with the // given endpoint followed by the path elements. // diff --git a/cmd/kes/enclave.go b/cmd/kes/enclave.go new file mode 100644 index 00000000..904fdbf7 --- /dev/null +++ b/cmd/kes/enclave.go @@ -0,0 +1,148 @@ +// 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 main + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + + "github.com/minio/kes" + "github.com/minio/kes/internal/cli" + flag "github.com/spf13/pflag" +) + +const enclaveCmdUsage = `Usage: + kes enclave + +Commands: + create Create a new enclave. + rm Delete an enclave. + +Options: + -h, --help Print command line options. +` + +func enclaveCmd(args []string) { + cmd := flag.NewFlagSet(args[0], flag.ContinueOnError) + cmd.Usage = func() { fmt.Fprint(os.Stderr, enclaveCmdUsage) } + + subCmds := commands{ + "create": createEnclaveCmd, + "rm": deleteEnclaveCmd, + } + + if len(args) < 2 { + cmd.Usage() + os.Exit(2) + } + if cmd, ok := subCmds[args[1]]; ok { + cmd(args[1:]) + return + } + + if err := cmd.Parse(args[1:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + os.Exit(2) + } + cli.Fatalf("%v. See 'kes enclave --help'", err) + } + if cmd.NArg() > 0 { + cli.Fatalf("%q is not a enclave command. See 'kes enclave --help'", cmd.Arg(0)) + } + cmd.Usage() + os.Exit(2) +} + +const createEnclaveCmdUsage = `Usage: + kes enclave create [options] + +Options: + -k, --insecure Skip TLS certificate validation. + -h, --help Print command line options. + +Examples: + $ kes enclave create tenant-1 5f2f4ef3e0e340a07fc330f58ef0a1c4d661e564ab10795f9231f75fcfe572f1 +` + +func createEnclaveCmd(args []string) { + cmd := flag.NewFlagSet(args[0], flag.ContinueOnError) + cmd.Usage = func() { fmt.Fprint(os.Stderr, createEnclaveCmdUsage) } + + var insecureSkipVerify bool + cmd.BoolVarP(&insecureSkipVerify, "insecure", "k", false, "Skip TLS certificate validation") + if err := cmd.Parse(args[1:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + os.Exit(2) + } + cli.Fatalf("%v. See 'kes enclave create --help'", err) + } + + switch { + case cmd.NArg() == 0: + cli.Fatal("no enclave name specified. See 'kes enclave create --help'") + case cmd.NArg() == 1: + cli.Fatal("no admin identity specified. See 'kes enclave create --help'") + case cmd.NArg() > 2: + cli.Fatal("too many arguments. See 'kes enclave create --help'") + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer cancel() + + name := cmd.Arg(0) + admin := cmd.Arg(1) + client := newClient(insecureSkipVerify) + if err := client.CreateEnclave(ctx, name, kes.Identity(admin)); err != nil { + if errors.Is(err, context.Canceled) { + os.Exit(1) + } + cli.Fatalf("failed to create enclave '%s': %v", name, err) + } +} + +const deleteEnclaveCmdUsage = `Usage: + kes enclave rm [options] ... + +Options: + -k, --insecure Skip TLS certificate validation. + -h, --help Print command line options. + +Examples: + $ kes enclave rm tenant-1 +` + +func deleteEnclaveCmd(args []string) { + cmd := flag.NewFlagSet(args[0], flag.ContinueOnError) + cmd.Usage = func() { fmt.Fprint(os.Stderr, deleteEnclaveCmdUsage) } + + var insecureSkipVerify bool + cmd.BoolVarP(&insecureSkipVerify, "insecure", "k", false, "Skip TLS certificate validation") + if err := cmd.Parse(args[1:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + os.Exit(2) + } + cli.Fatalf("%v. See 'kes enclave delete --help'", err) + } + + if cmd.NArg() == 0 { + cli.Fatal("no enclave name specified. See 'kes enclave delete --help'") + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer cancel() + + client := newClient(insecureSkipVerify) + for _, name := range cmd.Args() { + if err := client.DeleteEnclave(ctx, name); err != nil { + if errors.Is(err, context.Canceled) { + os.Exit(1) + } + cli.Fatalf("failed to delete enclave '%s': %v", name, err) + } + } +} diff --git a/cmd/kes/main.go b/cmd/kes/main.go index 320c9430..e15057e7 100644 --- a/cmd/kes/main.go +++ b/cmd/kes/main.go @@ -30,6 +30,7 @@ Commands: server Start a KES server. init Initialize a stateful KES server or cluster. + enclave Manage KES enclaves. key Manage cryptographic keys. policy Manage KES policies. identity Manage KES identities. @@ -54,6 +55,7 @@ func main() { "server": serverCmd, "init": initCmd, + "enclave": enclaveCmd, "key": keyCmd, "policy": policyCmd, "identity": identityCmd, diff --git a/enclave.go b/enclave.go index cdbca1ef..889fe689 100644 --- a/enclave.go +++ b/enclave.go @@ -33,7 +33,7 @@ type Enclave struct { // CreateKey creates a new cryptographic key. The key will // be generated by the KES server. // -// It returns ErrKeyExists if a key with the same key already +// It returns ErrKeyExists if a key with the same name already // exists. func (e *Enclave) CreateKey(ctx context.Context, name string) error { const (