mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	Fix transit byok tool, add docs, tests (#19373)
* Fix Vault Transit BYOK helper argument parsing This commit fixes the following issues with the importer: - More than two arguments were not supported, causing the CLI to error out and resulting in a failure to import RSA keys. - The @file notation support was not accepted for KEY, meaning unencrypted keys had to be manually specified on the CLI. - Parsing of additional argument data was done in a non-standard way. - Fix parsing of command line options and ensure only relevant options are included. Additionally, some error messages and help text was clarified. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add missing documentation on Transit CLI to website Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tests for Transit BYOK vault subcommand Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Appease CI Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/19373.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/19373.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ```release-note:bug | ||||
| cli/transit: Fix import, import-version command invocation | ||||
| ``` | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | ||||
| @@ -38,17 +39,20 @@ func (c *TransitImportCommand) Help() string { | ||||
| 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.  | ||||
|   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/transform 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 | FlagSetOutputField | FlagSetOutputFormat) | ||||
| 	return c.flagSet(FlagSetHTTP) | ||||
| } | ||||
|  | ||||
| func (c *TransitImportCommand) AutocompleteArgs() complete.Predictor { | ||||
| @@ -60,13 +64,20 @@ func (c *TransitImportCommand) AutocompleteFlags() complete.Flags { | ||||
| } | ||||
|  | ||||
| func (c *TransitImportCommand) Run(args []string) int { | ||||
| 	return importKey(c.BaseCommand, "import", args) | ||||
| 	return importKey(c.BaseCommand, "import", c.Flags(), 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))) | ||||
| func importKey(c *BaseCommand, operation string, 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 | ||||
| 	} | ||||
|  | ||||
| @@ -89,7 +100,18 @@ func importKey(c *BaseCommand, operation string, args []string) int { | ||||
| 	path := parts[1] | ||||
| 	keyName := parts[2] | ||||
|  | ||||
| 	key, err := base64.StdEncoding.DecodeString(args[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 | ||||
| @@ -126,15 +148,19 @@ func importKey(c *BaseCommand, operation string, args []string) int { | ||||
| 	} | ||||
| 	combinedCiphertext := append(wrappedAESKey, wrappedTargetKey...) | ||||
| 	importCiphertext := base64.StdEncoding.EncodeToString(combinedCiphertext) | ||||
|  | ||||
| 	// Parse all the key options | ||||
| 	data := map[string]interface{}{ | ||||
| 		"ciphertext": importCiphertext, | ||||
| 	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 | ||||
| 	} | ||||
| 	for _, v := range args[2:] { | ||||
| 		parts := strings.Split(v, "=") | ||||
| 		data[parts[0]] = parts[1] | ||||
| 	if data == nil { | ||||
| 		data = make(map[string]interface{}, 1) | ||||
| 	} | ||||
|  | ||||
| 	data["ciphertext"] = importCiphertext | ||||
|  | ||||
| 	c.UI.Output("Submitting wrapped key to Vault transit.") | ||||
| 	// Finally, call import | ||||
| 	_, err = client.Logical().Write(path+"/keys/"+keyName+"/"+operation, data) | ||||
|   | ||||
							
								
								
									
										186
									
								
								command/transit_import_key_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								command/transit_import_key_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| package command | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/api" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| // Validate the `vault transit import` command works. | ||||
| func TestTransitImport(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	client, closer := testVaultServer(t) | ||||
| 	defer closer() | ||||
|  | ||||
| 	if err := client.Sys().Mount("transit", &api.MountInput{ | ||||
| 		Type: "transit", | ||||
| 	}); err != nil { | ||||
| 		t.Fatalf("transit mount error: %#v", err) | ||||
| 	} | ||||
|  | ||||
| 	rsa1, rsa2, aes128, aes256 := generateKeys(t) | ||||
|  | ||||
| 	type testCase struct { | ||||
| 		variant    string | ||||
| 		path       string | ||||
| 		key        []byte | ||||
| 		args       []string | ||||
| 		shouldFail bool | ||||
| 	} | ||||
| 	tests := []testCase{ | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/rsa1", | ||||
| 			rsa1, | ||||
| 			[]string{"type=rsa-2048"}, | ||||
| 			false, /* first import */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/rsa1", | ||||
| 			rsa2, | ||||
| 			[]string{"type=rsa-2048"}, | ||||
| 			true, /* already exists */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import-version", | ||||
| 			"transit/keys/rsa1", | ||||
| 			rsa2, | ||||
| 			[]string{"type=rsa-2048"}, | ||||
| 			false, /* new version */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/rsa2", | ||||
| 			rsa2, | ||||
| 			[]string{"type=rsa-4096"}, | ||||
| 			true, /* wrong type */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/rsa2", | ||||
| 			rsa2, | ||||
| 			[]string{"type=rsa-2048"}, | ||||
| 			false, /* new name */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/aes1", | ||||
| 			aes128, | ||||
| 			[]string{"type=aes128-gcm96"}, | ||||
| 			false, /* first import */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/aes1", | ||||
| 			aes256, | ||||
| 			[]string{"type=aes256-gcm96"}, | ||||
| 			true, /* already exists */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import-version", | ||||
| 			"transit/keys/aes1", | ||||
| 			aes256, | ||||
| 			[]string{"type=aes256-gcm96"}, | ||||
| 			true, /* new version, different type */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import-version", | ||||
| 			"transit/keys/aes1", | ||||
| 			aes128, | ||||
| 			[]string{"type=aes128-gcm96"}, | ||||
| 			false, /* new version */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/aes2", | ||||
| 			aes256, | ||||
| 			[]string{"type=aes128-gcm96"}, | ||||
| 			true, /* wrong type */ | ||||
| 		}, | ||||
| 		{ | ||||
| 			"import", | ||||
| 			"transit/keys/aes2", | ||||
| 			aes256, | ||||
| 			[]string{"type=aes256-gcm96"}, | ||||
| 			false, /* new name */ | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for index, tc := range tests { | ||||
| 		t.Logf("Running test case %d: %v", index, tc) | ||||
| 		execTransitImport(t, client, tc.variant, tc.path, tc.key, tc.args, tc.shouldFail) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func execTransitImport(t *testing.T, client *api.Client, method string, path string, key []byte, data []string, expectFailure bool) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	keyBase64 := base64.StdEncoding.EncodeToString(key) | ||||
|  | ||||
| 	var args []string | ||||
| 	args = append(args, "transit") | ||||
| 	args = append(args, method) | ||||
| 	args = append(args, path) | ||||
| 	args = append(args, keyBase64) | ||||
| 	args = append(args, data...) | ||||
|  | ||||
| 	stdout := bytes.NewBuffer(nil) | ||||
| 	stderr := bytes.NewBuffer(nil) | ||||
| 	runOpts := &RunOptions{ | ||||
| 		Stdout: stdout, | ||||
| 		Stderr: stderr, | ||||
| 		Client: client, | ||||
| 	} | ||||
|  | ||||
| 	code := RunCustom(args, runOpts) | ||||
| 	combined := stdout.String() + stderr.String() | ||||
|  | ||||
| 	if code != 0 { | ||||
| 		if !expectFailure { | ||||
| 			t.Fatalf("Got unexpected failure from test (ret %d): %v", code, combined) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if expectFailure { | ||||
| 			t.Fatalf("Expected failure, got success from test (ret %d): %v", code, combined) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func generateKeys(t *testing.T) (rsa1 []byte, rsa2 []byte, aes128 []byte, aes256 []byte) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	priv1, err := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 	require.NotNil(t, priv1, "failed generating RSA 1 key") | ||||
| 	require.NoError(t, err, "failed generating RSA 1 key") | ||||
|  | ||||
| 	rsa1, err = x509.MarshalPKCS8PrivateKey(priv1) | ||||
| 	require.NotNil(t, rsa1, "failed marshaling RSA 1 key") | ||||
| 	require.NoError(t, err, "failed marshaling RSA 1 key") | ||||
|  | ||||
| 	priv2, err := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 	require.NotNil(t, priv2, "failed generating RSA 2 key") | ||||
| 	require.NoError(t, err, "failed generating RSA 2 key") | ||||
|  | ||||
| 	rsa2, err = x509.MarshalPKCS8PrivateKey(priv2) | ||||
| 	require.NotNil(t, rsa2, "failed marshaling RSA 2 key") | ||||
| 	require.NoError(t, err, "failed marshaling RSA 2 key") | ||||
|  | ||||
| 	aes128 = make([]byte, 128/8) | ||||
| 	_, err = rand.Read(aes128) | ||||
| 	require.NoError(t, err, "failed generating AES 128 key") | ||||
|  | ||||
| 	aes256 = make([]byte, 256/8) | ||||
| 	_, err = rand.Read(aes256) | ||||
| 	require.NoError(t, err, "failed generating AES 256 key") | ||||
|  | ||||
| 	return | ||||
| } | ||||
| @@ -22,21 +22,23 @@ func (c *TransitImportVersionCommand) Synopsis() string { | ||||
|  | ||||
| func (c *TransitImportVersionCommand) Help() string { | ||||
| 	helpText := ` | ||||
| Usage: vault transit import-version PATH KEY | ||||
| 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. | ||||
|   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 transit/transform | ||||
|   key, use the import command instead.  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 | ||||
|   (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 *TransitImportVersionCommand) Flags() *FlagSets { | ||||
| 	return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) | ||||
| 	return c.flagSet(FlagSetHTTP) | ||||
| } | ||||
|  | ||||
| func (c *TransitImportVersionCommand) AutocompleteArgs() complete.Predictor { | ||||
| @@ -48,5 +50,5 @@ func (c *TransitImportVersionCommand) AutocompleteFlags() complete.Flags { | ||||
| } | ||||
|  | ||||
| func (c *TransitImportVersionCommand) Run(args []string) int { | ||||
| 	return importKey(c.BaseCommand, "import_version", args) | ||||
| 	return importKey(c.BaseCommand, "import_version", c.Flags(), args) | ||||
| } | ||||
|   | ||||
							
								
								
									
										62
									
								
								website/content/docs/commands/transit/import.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								website/content/docs/commands/transit/import.mdx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| --- | ||||
| layout: docs | ||||
| page_title: transit import and transit import-version - Command | ||||
| description: |- | ||||
|   The "transit import" and "transit import-version" commands import the | ||||
|   specified key into Transit, via the Transit BYOK mechanism. | ||||
| --- | ||||
|  | ||||
| # transit import and transit import-version | ||||
|  | ||||
| The `transit import` and `transit import-version` commands import the | ||||
| specified key into Transit, via the [Transit BYOK | ||||
| mechanism](/vault/docs/secrets/transit#bring-your-own-key-byok). The former | ||||
| imports this key as a new key, failing if it already exists, whereas the | ||||
| latter will only update an existing key in Transit to a new version of the | ||||
| key material. | ||||
|  | ||||
| This needs access to read the transit mount's wrapping key (at | ||||
| `transit/wrapping_key`) and the ability to write to either import | ||||
| endpoints (either `transit/keys/:name/import` or | ||||
| `transit/keys/:name/import_version`). | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| Imports a 2048-bit RSA key as a new key: | ||||
|  | ||||
| ``` | ||||
| $ vault transit import transit/keys/test-key @test-key type=rsa-2048 | ||||
| Retrieving transit wrapping key. | ||||
| Wrapping source key with ephemeral key. | ||||
| Encrypting ephemeral key with transit wrapping key. | ||||
| Submitting wrapped key to Vault transit. | ||||
| Success! | ||||
| ``` | ||||
|  | ||||
| Imports a new version of an existing key: | ||||
|  | ||||
| ``` | ||||
| $ vault transit import-version transit/keys/test-key @test-key-updated | ||||
| Retrieving transit wrapping key. | ||||
| Wrapping source key with ephemeral key. | ||||
| Encrypting ephemeral key with transit wrapping key. | ||||
| Submitting wrapped key to Vault transit. | ||||
| Success! | ||||
| ``` | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| This command does not have any unique flags and respects core Vault CLI | ||||
| commands. See `vault transit import -help` for more information. | ||||
|  | ||||
| This command requires two positional arguments: | ||||
|  | ||||
|  1. `PATH`, the path to the transit key to import in the format of | ||||
|     `<mount>/keys/<key-name>`, where `<mount>` is the path to the mount | ||||
|     (using `-namespace=<ns>` to specify any namespaces), and `<key-name>` | ||||
|     is the desired name of the key. | ||||
|  2. `KEY`, the key material to import in Standard Base64 encoding (either | ||||
|     of a raw key in the case of symmetric keys such as AES, or of the DER | ||||
|     encoded format for asymmetric keys such as RSA). If the value for `KEY` | ||||
|     begins with an `@`, the CLI argument is assumed to be a path to a file | ||||
|     on disk to be read. | ||||
							
								
								
									
										32
									
								
								website/content/docs/commands/transit/index.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								website/content/docs/commands/transit/index.mdx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| --- | ||||
| layout: docs | ||||
| page_title: transit - Command | ||||
| description: |- | ||||
|   The "transit" command groups subcommands for interacting with Vault's Transit | ||||
|   secrets engine. | ||||
| --- | ||||
|  | ||||
| # transit | ||||
|  | ||||
| The `transit` command groups subcommands for interacting with Vault's | ||||
| [Transit Secrets Engine](/vault/docs/secrets/transit). | ||||
|  | ||||
| ## Syntax | ||||
|  | ||||
| Option flags for a given subcommand are provided after the subcommand, but before the arguments. | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| To [import](/vault/docs/commands/transit/import) keys into a mount via the | ||||
| [Transit BYOK](/vault/docs/secrets/transit#bring-your-own-key-byok) | ||||
| mechanism, use the `vault transit import <path> <key>` or | ||||
| `vault transit import-version <path> <key>` commands: | ||||
|  | ||||
| ``` | ||||
| $ vault transit import transit/keys/test-key @test-key type=rsa-2048 | ||||
| Retrieving transit wrapping key. | ||||
| Wrapping source key with ephemeral key. | ||||
| Encrypting ephemeral key with transit wrapping key. | ||||
| Submitting wrapped key to Vault transit. | ||||
| Success! | ||||
| ``` | ||||
| @@ -832,6 +832,19 @@ | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         "title": "<code>transit</code>", | ||||
|         "routes": [ | ||||
|           { | ||||
|             "title": "Overview", | ||||
|             "path": "commands/transit" | ||||
|           }, | ||||
|           { | ||||
|             "title": "<code>import</code> and <code>import-version</code>", | ||||
|             "path": "commands/transit/import" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         "title": "<code>unwrap</code>", | ||||
|         "path": "commands/unwrap" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Alexander Scheel
					Alexander Scheel