Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make transit import command work for the transform backend #20668

Merged
merged 2 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/20668.txt
Original file line number Diff line number Diff line change
@@ -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'.
```
15 changes: 15 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,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(),
Expand Down
41 changes: 41 additions & 0 deletions command/transform.go
Original file line number Diff line number Diff line change
@@ -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 <subcommand> [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
}
76 changes: 76 additions & 0 deletions command/transform_import_key.go
Original file line number Diff line number Diff line change
@@ -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
}
59 changes: 59 additions & 0 deletions command/transform_import_key_version.go
Original file line number Diff line number Diff line change
@@ -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)
}
39 changes: 26 additions & 13 deletions command/transit_import_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
"regexp"
Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice abstraction!


// 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())
Expand All @@ -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:])
Expand All @@ -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))
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion command/transit_import_key_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}