mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 02:57:59 +00:00
Transit Import Key CLI functionality (#18887)
* wip * Transit byok cli * It works! * changelog * document return codes * Update command/transit_import_key.go Co-authored-by: Steven Clark <steven.clark@hashicorp.com> * make fmt --------- Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
This commit is contained in:
3
changelog/18887.txt
Normal file
3
changelog/18887.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
cli: Add transit import key helper commands for BYOK to Transit/Transform.
|
||||||
|
```
|
||||||
@@ -697,6 +697,16 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
|||||||
BaseCommand: getBaseCommand(),
|
BaseCommand: getBaseCommand(),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
"transit import": func() (cli.Command, error) {
|
||||||
|
return &TransitImportCommand{
|
||||||
|
BaseCommand: getBaseCommand(),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
"transit import-version": func() (cli.Command, error) {
|
||||||
|
return &TransitImportVersionCommand{
|
||||||
|
BaseCommand: getBaseCommand(),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
"token": func() (cli.Command, error) {
|
"token": func() (cli.Command, error) {
|
||||||
return &TokenCommand{
|
return &TokenCommand{
|
||||||
BaseCommand: getBaseCommand(),
|
BaseCommand: getBaseCommand(),
|
||||||
|
|||||||
168
command/transit_import_key.go
Normal file
168
command/transit_import_key.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
|
||||||
|
"github.com/google/tink/go/kwp/subtle"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/posener/complete"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cli.Command = (*TransitImportCommand)(nil)
|
||||||
|
_ cli.CommandAutocomplete = (*TransitImportCommand)(nil)
|
||||||
|
keyPath = regexp.MustCompile("^(.*)/keys/([^/]*)$")
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransitImportCommand struct {
|
||||||
|
*BaseCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportCommand) Synopsis() string {
|
||||||
|
return "Import a key into the Transit or Transform secrets engines."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: vault transit import PATH KEY [options...]
|
||||||
|
|
||||||
|
Using the Transit or Transform key wrapping system, imports key material from
|
||||||
|
the base64 encoded KEY, into a new key whose API path is PATH. To import a new version
|
||||||
|
into an existing key, use import_version. The remaining options after KEY (key=value style) are passed
|
||||||
|
on to the transit/transform create key endpoint. If your system or device natively supports
|
||||||
|
the RSA AES key wrap mechanism, you should use it directly rather than this command.
|
||||||
|
` + c.Flags().Help()
|
||||||
|
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportCommand) Flags() *FlagSets {
|
||||||
|
return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportCommand) AutocompleteArgs() complete.Predictor {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportCommand) AutocompleteFlags() complete.Flags {
|
||||||
|
return c.Flags().Completions()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportCommand) Run(args []string) int {
|
||||||
|
return importKey(c.BaseCommand, "import", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// error codes: 1: user error, 2: internal computation error, 3: remote api call error
|
||||||
|
func importKey(c *BaseCommand, operation string, args []string) int {
|
||||||
|
if len(args) != 2 {
|
||||||
|
c.UI.Error(fmt.Sprintf("Incorrect argument count (expected 2, got %d)", len(args)))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := c.Client()
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(err.Error())
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
ephemeralAESKey := make([]byte, 32)
|
||||||
|
_, err = rand.Read(ephemeralAESKey)
|
||||||
|
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:")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
path := parts[1]
|
||||||
|
keyName := parts[2]
|
||||||
|
|
||||||
|
key, err := base64.StdEncoding.DecodeString(args[1])
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("error base64 decoding source key material: %v", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// Fetch the wrapping key
|
||||||
|
c.UI.Output("Retrieving transit wrapping key.")
|
||||||
|
wrappingKey, err := fetchWrappingKey(c, client, path)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("failed to fetch wrapping key: %v", err))
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
c.UI.Output("Wrapping source key with ephemeral key.")
|
||||||
|
wrapKWP, err := subtle.NewKWP(ephemeralAESKey)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("failure building key wrapping key: %v", err))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
wrappedTargetKey, err := wrapKWP.Wrap(key)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("failure wrapping source key: %v", err))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
c.UI.Output("Encrypting ephemeral key with transit wrapping key.")
|
||||||
|
wrappedAESKey, err := rsa.EncryptOAEP(
|
||||||
|
sha256.New(),
|
||||||
|
rand.Reader,
|
||||||
|
wrappingKey.(*rsa.PublicKey),
|
||||||
|
ephemeralAESKey,
|
||||||
|
[]byte{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("failure encrypting wrapped key: %v", err))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
combinedCiphertext := append(wrappedAESKey, wrappedTargetKey...)
|
||||||
|
importCiphertext := base64.StdEncoding.EncodeToString(combinedCiphertext)
|
||||||
|
// Parse all the key options
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"ciphertext": importCiphertext,
|
||||||
|
}
|
||||||
|
for _, v := range args[2:] {
|
||||||
|
parts := strings.Split(v, "=")
|
||||||
|
data[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
c.UI.Output("Submitting wrapped key to Vault transit.")
|
||||||
|
// Finally, call import
|
||||||
|
_, err = client.Logical().Write(path+"/keys/"+keyName+"/"+operation, data)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("failed to call import:%v", err))
|
||||||
|
return 3
|
||||||
|
} else {
|
||||||
|
c.UI.Output("Success!")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWrappingKey(c *BaseCommand, client *api.Client, path string) (any, error) {
|
||||||
|
resp, err := client.Logical().Read(path + "/wrapping_key")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
key, ok := resp.Data["public_key"]
|
||||||
|
if !ok {
|
||||||
|
c.UI.Error("could not find wrapping key")
|
||||||
|
}
|
||||||
|
keyBlock, _ := pem.Decode([]byte(key.(string)))
|
||||||
|
parsedKey, err := x509.ParsePKIXPublicKey(keyBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing wrapping key: %w", err)
|
||||||
|
}
|
||||||
|
return parsedKey, nil
|
||||||
|
}
|
||||||
52
command/transit_import_key_version.go
Normal file
52
command/transit_import_key_version.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/posener/complete"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cli.Command = (*TransitImportVersionCommand)(nil)
|
||||||
|
_ cli.CommandAutocomplete = (*TransitImportVersionCommand)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransitImportVersionCommand struct {
|
||||||
|
*BaseCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportVersionCommand) Synopsis() string {
|
||||||
|
return "Import key material into a new key version in the Transit or Transform secrets engines."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportVersionCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: vault transit import-version PATH KEY
|
||||||
|
|
||||||
|
Using the Transit or Transform key wrapping system, imports key material from
|
||||||
|
the base64 encoded KEY, into a new key whose API path is PATH. To import a new transit/transform key,
|
||||||
|
use import. The remaining options after KEY (key=value style) are passed on to the transit/transform create key
|
||||||
|
endpoint.
|
||||||
|
If your system or device natively supports the RSA AES key wrap mechanism, you should use it directly
|
||||||
|
rather than this command.
|
||||||
|
` + c.Flags().Help()
|
||||||
|
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportVersionCommand) Flags() *FlagSets {
|
||||||
|
return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportVersionCommand) AutocompleteArgs() complete.Predictor {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportVersionCommand) AutocompleteFlags() complete.Flags {
|
||||||
|
return c.Flags().Completions()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TransitImportVersionCommand) Run(args []string) int {
|
||||||
|
return importKey(c.BaseCommand, "import_version", args)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user