From c32032c1f817f5a7e53d0ed777b518ed40060118 Mon Sep 17 00:00:00 2001 From: Larroyo <95649169+DeLuci@users.noreply.github.com> Date: Thu, 25 May 2023 15:33:27 -0500 Subject: [PATCH] Make transit import command work for the transform backend (#20668) * Add import and import-version commands for the transform backend --- changelog/20668.txt | 3 + command/commands.go | 15 +++++ command/transform.go | 41 +++++++++++++ command/transform_import_key.go | 76 +++++++++++++++++++++++++ command/transform_import_key_version.go | 59 +++++++++++++++++++ command/transit_import_key.go | 39 ++++++++----- command/transit_import_key_version.go | 2 +- 7 files changed, 221 insertions(+), 14 deletions(-) create mode 100644 changelog/20668.txt create mode 100644 command/transform.go create mode 100644 command/transform_import_key.go create mode 100644 command/transform_import_key_version.go diff --git a/changelog/20668.txt b/changelog/20668.txt new file mode 100644 index 000000000..f3f840c47 --- /dev/null +++ b/changelog/20668.txt @@ -0,0 +1,3 @@ +```release-note:bug +secrets/transform: Added importing of keys and key versions into the Transform secrets engine using the command 'vault transform import' and 'vault transform import-version'. +``` diff --git a/command/commands.go b/command/commands.go index c3867dc16..ba264a293 100644 --- a/command/commands.go +++ b/command/commands.go @@ -721,6 +721,21 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co BaseCommand: getBaseCommand(), }, nil }, + "transform": func() (cli.Command, error) { + return &TransformCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, + "transform import": func() (cli.Command, error) { + return &TransformImportCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, + "transform import-version": func() (cli.Command, error) { + return &TransformImportVersionCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "transit": func() (cli.Command, error) { return &TransitCommand{ BaseCommand: getBaseCommand(), diff --git a/command/transform.go b/command/transform.go new file mode 100644 index 000000000..27345a441 --- /dev/null +++ b/command/transform.go @@ -0,0 +1,41 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*TransformCommand)(nil) + +type TransformCommand struct { + *BaseCommand +} + +func (c *TransformCommand) Synopsis() string { + return "Interact with Vault's Transform Secrets Engine" +} + +func (c *TransformCommand) Help() string { + helpText := ` +Usage: vault transform [options] [args] + + This command has subcommands for interacting with Vault's Transform Secrets + Engine. Here are some simple examples, and more detailed examples are + available in the subcommands or the documentation. + + To import a key into a new FPE transformation: + + $ vault transform import transform/transformations/fpe/new-transformation @path/to/key \ + template=identifier \ + allowed_roles=physical-access + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *TransformCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/transform_import_key.go b/command/transform_import_key.go new file mode 100644 index 000000000..e41fa625e --- /dev/null +++ b/command/transform_import_key.go @@ -0,0 +1,76 @@ +package command + +import ( + "errors" + "regexp" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*TransformImportCommand)(nil) + _ cli.CommandAutocomplete = (*TransformImportCommand)(nil) + transformKeyPath = regexp.MustCompile("^(.*)/transformations/(fpe|tokenization)/([^/]*)$") +) + +type TransformImportCommand struct { + *BaseCommand +} + +func (c *TransformImportCommand) Synopsis() string { + return "Import a key into the Transform secrets engines." +} + +func (c *TransformImportCommand) Help() string { + helpText := ` +Usage: vault transform import PATH KEY [options...] + + Using the Transform key wrapping system, imports key material from + the base64 encoded KEY (either directly on the CLI or via @path notation), + into a new FPE or tokenization transformation whose API path is PATH. + + To import a new key version into an existing tokenization transformation, + use import_version. + + The remaining options after KEY (key=value style) are passed on to + Create/Update FPE Transformation or Create/Update Tokenization Transformation + API endpoints. + + For example: + $ vault transform import transform/transformations/tokenization/application-form @path/to/key \ + allowed_roles=legacy-system +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *TransformImportCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *TransformImportCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *TransformImportCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *TransformImportCommand) Run(args []string) int { + return ImportKey(c.BaseCommand, "import", transformImportKeyPath, c.Flags(), args) +} + +func transformImportKeyPath(s string, operation string) (path string, apiPath string, err error) { + parts := transformKeyPath.FindStringSubmatch(s) + if len(parts) != 4 { + return "", "", errors.New("expected transform path and key name in the form :path:/transformations/fpe|tokenization/:name:") + } + path = parts[1] + transformation := parts[2] + keyName := parts[3] + apiPath = path + "/transformations/" + transformation + "/" + keyName + "/" + operation + + return path, apiPath, nil +} diff --git a/command/transform_import_key_version.go b/command/transform_import_key_version.go new file mode 100644 index 000000000..6ed8cb18b --- /dev/null +++ b/command/transform_import_key_version.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package command + +import ( + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*TransformImportVersionCommand)(nil) + _ cli.CommandAutocomplete = (*TransformImportVersionCommand)(nil) +) + +type TransformImportVersionCommand struct { + *BaseCommand +} + +func (c *TransformImportVersionCommand) Synopsis() string { + return "Import key material into a new key version in the Transform secrets engines." +} + +func (c *TransformImportVersionCommand) Help() string { + helpText := ` +Usage: vault transform import-version PATH KEY [...] + + Using the Transform key wrapping system, imports new key material from + the base64 encoded KEY (either directly on the CLI or via @path notation), + into an existing tokenization transformation whose API path is PATH. + + The remaining options after KEY (key=value style) are passed on to + Create/Update Tokenization Transformation API endpoint. + + For example: + $ vault transform import-version transform/transformations/tokenization/application-form @path/to/new_version \ + allowed_roles=legacy-system +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *TransformImportVersionCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *TransformImportVersionCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *TransformImportVersionCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *TransformImportVersionCommand) Run(args []string) int { + return ImportKey(c.BaseCommand, "import_version", transformImportKeyPath, c.Flags(), args) +} diff --git a/command/transit_import_key.go b/command/transit_import_key.go index 795545b69..35801f43a 100644 --- a/command/transit_import_key.go +++ b/command/transit_import_key.go @@ -10,6 +10,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" "os" "regexp" @@ -68,11 +69,25 @@ func (c *TransitImportCommand) AutocompleteFlags() complete.Flags { } func (c *TransitImportCommand) Run(args []string) int { - return importKey(c.BaseCommand, "import", c.Flags(), args) + return ImportKey(c.BaseCommand, "import", transitImportKeyPath, c.Flags(), args) } +func transitImportKeyPath(s string, operation string) (path string, apiPath string, err error) { + parts := keyPath.FindStringSubmatch(s) + if len(parts) != 3 { + return "", "", errors.New("expected transit path and key name in the form :path:/keys/:name:") + } + path = parts[1] + keyName := parts[2] + apiPath = path + "/keys/" + keyName + "/" + operation + + return path, apiPath, nil +} + +type ImportKeyFunc func(s string, operation string) (path string, apiPath string, err error) + // error codes: 1: user error, 2: internal computation error, 3: remote api call error -func importKey(c *BaseCommand, operation string, flags *FlagSets, args []string) int { +func ImportKey(c *BaseCommand, operation string, pathFunc ImportKeyFunc, flags *FlagSets, args []string) int { // Parse and validate the arguments. if err := flags.Parse(args); err != nil { c.UI.Error(err.Error()) @@ -96,14 +111,11 @@ func importKey(c *BaseCommand, operation string, flags *FlagSets, args []string) if err != nil { c.UI.Error(fmt.Sprintf("failed to generate ephemeral key: %v", err)) } - parts := keyPath.FindStringSubmatch(args[0]) - if len(parts) != 3 { - c.UI.Error("expected transit path and key name in the form :path:/keys/:name:") + path, apiPath, err := pathFunc(args[0], operation) + if err != nil { + c.UI.Error(err.Error()) return 1 } - path := parts[1] - keyName := parts[2] - keyMaterial := args[1] if keyMaterial[0] == '@' { keyMaterialBytes, err := os.ReadFile(keyMaterial[1:]) @@ -121,7 +133,7 @@ func importKey(c *BaseCommand, operation string, flags *FlagSets, args []string) return 1 } // Fetch the wrapping key - c.UI.Output("Retrieving transit wrapping key.") + c.UI.Output("Retrieving wrapping key.") wrappingKey, err := fetchWrappingKey(c, client, path) if err != nil { c.UI.Error(fmt.Sprintf("failed to fetch wrapping key: %v", err)) @@ -138,7 +150,7 @@ func importKey(c *BaseCommand, operation string, flags *FlagSets, args []string) c.UI.Error(fmt.Sprintf("failure wrapping source key: %v", err)) return 2 } - c.UI.Output("Encrypting ephemeral key with transit wrapping key.") + c.UI.Output("Encrypting ephemeral key with wrapping key.") wrappedAESKey, err := rsa.EncryptOAEP( sha256.New(), rand.Reader, @@ -165,9 +177,10 @@ func importKey(c *BaseCommand, operation string, flags *FlagSets, args []string) data["ciphertext"] = importCiphertext - c.UI.Output("Submitting wrapped key to Vault transit.") + c.UI.Output("Submitting wrapped key.") // Finally, call import - _, err = client.Logical().Write(path+"/keys/"+keyName+"/"+operation, data) + + _, err = client.Logical().Write(apiPath, data) if err != nil { c.UI.Error(fmt.Sprintf("failed to call import:%v", err)) return 3 @@ -183,7 +196,7 @@ func fetchWrappingKey(c *BaseCommand, client *api.Client, path string) (any, err return nil, fmt.Errorf("error fetching wrapping key: %w", err) } if resp == nil { - return nil, fmt.Errorf("transit not mounted at %s: %v", path, err) + return nil, fmt.Errorf("no mount found at %s: %v", path, err) } key, ok := resp.Data["public_key"] if !ok { diff --git a/command/transit_import_key_version.go b/command/transit_import_key_version.go index 272fe01d1..4cf9602d4 100644 --- a/command/transit_import_key_version.go +++ b/command/transit_import_key_version.go @@ -54,5 +54,5 @@ func (c *TransitImportVersionCommand) AutocompleteFlags() complete.Flags { } func (c *TransitImportVersionCommand) Run(args []string) int { - return importKey(c.BaseCommand, "import_version", c.Flags(), args) + return ImportKey(c.BaseCommand, "import_version", transitImportKeyPath, c.Flags(), args) }