open-vault/command/transit_import_key.go

212 lines
5.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package command
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
"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 secrets engines."
}
func (c *TransitImportCommand) Help() string {
helpText := `
Usage: vault transit import PATH KEY [options...]
Using the Transit key wrapping system, imports key material from
the base64 encoded KEY (either directly on the CLI or via @path notation),
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 create key endpoint. If your
system or device natively supports the RSA AES key wrap mechanism (such as
the PKCS#11 mechanism CKM_RSA_AES_KEY_WRAP), 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)
}
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", 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, pathFunc ImportKeyFunc, flags *FlagSets, args []string) int {
// Parse and validate the arguments.
if err := flags.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = flags.Args()
if len(args) < 2 {
c.UI.Error(fmt.Sprintf("Incorrect argument count (expected 2+, got %d). Wanted PATH to import into and KEY material.", 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))
}
path, apiPath, err := pathFunc(args[0], operation)
if err != nil {
c.UI.Error(err.Error())
return 1
}
keyMaterial := args[1]
if keyMaterial[0] == '@' {
keyMaterialBytes, err := os.ReadFile(keyMaterial[1:])
if err != nil {
c.UI.Error(fmt.Sprintf("error reading key material file: %v", err))
return 1
}
keyMaterial = string(keyMaterialBytes)
}
key, err := base64.StdEncoding.DecodeString(keyMaterial)
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 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 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, err := parseArgsData(os.Stdin, args[2:])
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse extra K=V data: %s", err))
return 1
}
if data == nil {
data = make(map[string]interface{}, 1)
}
data["ciphertext"] = importCiphertext
c.UI.Output("Submitting wrapped key.")
// Finally, call import
_, err = client.Logical().Write(apiPath, 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("no mount found 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
}