Skip to content

Commit a5edc66

Browse files
backport of commit d35be2d (#19375)
Co-authored-by: Alexander Scheel <[email protected]>
1 parent a0beacd commit a5edc66

File tree

7 files changed

+347
-23
lines changed

7 files changed

+347
-23
lines changed

changelog/19373.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
cli/transit: Fix import, import-version command invocation
3+
```

command/transit_import_key.go

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/base64"
99
"encoding/pem"
1010
"fmt"
11+
"os"
1112
"regexp"
1213
"strings"
1314

@@ -38,17 +39,20 @@ func (c *TransitImportCommand) Help() string {
3839
Usage: vault transit import PATH KEY [options...]
3940
4041
Using the Transit or Transform key wrapping system, imports key material from
41-
the base64 encoded KEY, into a new key whose API path is PATH. To import a new version
42-
into an existing key, use import_version. The remaining options after KEY (key=value style) are passed
43-
on to the transit/transform create key endpoint. If your system or device natively supports
44-
the RSA AES key wrap mechanism, you should use it directly rather than this command.
42+
the base64 encoded KEY (either directly on the CLI or via @path notation),
43+
into a new key whose API path is PATH. To import a new version into an
44+
existing key, use import_version. The remaining options after KEY (key=value
45+
style) are passed on to the transit/transform create key endpoint. If your
46+
system or device natively supports the RSA AES key wrap mechanism (such as
47+
the PKCS#11 mechanism CKM_RSA_AES_KEY_WRAP), you should use it directly
48+
rather than this command.
4549
` + c.Flags().Help()
4650

4751
return strings.TrimSpace(helpText)
4852
}
4953

5054
func (c *TransitImportCommand) Flags() *FlagSets {
51-
return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
55+
return c.flagSet(FlagSetHTTP)
5256
}
5357

5458
func (c *TransitImportCommand) AutocompleteArgs() complete.Predictor {
@@ -60,13 +64,20 @@ func (c *TransitImportCommand) AutocompleteFlags() complete.Flags {
6064
}
6165

6266
func (c *TransitImportCommand) Run(args []string) int {
63-
return importKey(c.BaseCommand, "import", args)
67+
return importKey(c.BaseCommand, "import", c.Flags(), args)
6468
}
6569

6670
// error codes: 1: user error, 2: internal computation error, 3: remote api call error
67-
func importKey(c *BaseCommand, operation string, args []string) int {
68-
if len(args) != 2 {
69-
c.UI.Error(fmt.Sprintf("Incorrect argument count (expected 2, got %d)", len(args)))
71+
func importKey(c *BaseCommand, operation string, flags *FlagSets, args []string) int {
72+
// Parse and validate the arguments.
73+
if err := flags.Parse(args); err != nil {
74+
c.UI.Error(err.Error())
75+
return 1
76+
}
77+
78+
args = flags.Args()
79+
if len(args) < 2 {
80+
c.UI.Error(fmt.Sprintf("Incorrect argument count (expected 2+, got %d). Wanted PATH to import into and KEY material.", len(args)))
7081
return 1
7182
}
7283

@@ -89,7 +100,18 @@ func importKey(c *BaseCommand, operation string, args []string) int {
89100
path := parts[1]
90101
keyName := parts[2]
91102

92-
key, err := base64.StdEncoding.DecodeString(args[1])
103+
keyMaterial := args[1]
104+
if keyMaterial[0] == '@' {
105+
keyMaterialBytes, err := os.ReadFile(keyMaterial[1:])
106+
if err != nil {
107+
c.UI.Error(fmt.Sprintf("error reading key material file: %v", err))
108+
return 1
109+
}
110+
111+
keyMaterial = string(keyMaterialBytes)
112+
}
113+
114+
key, err := base64.StdEncoding.DecodeString(keyMaterial)
93115
if err != nil {
94116
c.UI.Error(fmt.Sprintf("error base64 decoding source key material: %v", err))
95117
return 1
@@ -126,15 +148,19 @@ func importKey(c *BaseCommand, operation string, args []string) int {
126148
}
127149
combinedCiphertext := append(wrappedAESKey, wrappedTargetKey...)
128150
importCiphertext := base64.StdEncoding.EncodeToString(combinedCiphertext)
151+
129152
// Parse all the key options
130-
data := map[string]interface{}{
131-
"ciphertext": importCiphertext,
153+
data, err := parseArgsData(os.Stdin, args[2:])
154+
if err != nil {
155+
c.UI.Error(fmt.Sprintf("Failed to parse extra K=V data: %s", err))
156+
return 1
132157
}
133-
for _, v := range args[2:] {
134-
parts := strings.Split(v, "=")
135-
data[parts[0]] = parts[1]
158+
if data == nil {
159+
data = make(map[string]interface{}, 1)
136160
}
137161

162+
data["ciphertext"] = importCiphertext
163+
138164
c.UI.Output("Submitting wrapped key to Vault transit.")
139165
// Finally, call import
140166
_, err = client.Logical().Write(path+"/keys/"+keyName+"/"+operation, data)

command/transit_import_key_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package command
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/x509"
8+
"encoding/base64"
9+
"testing"
10+
11+
"github.com/hashicorp/vault/api"
12+
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// Validate the `vault transit import` command works.
17+
func TestTransitImport(t *testing.T) {
18+
t.Parallel()
19+
20+
client, closer := testVaultServer(t)
21+
defer closer()
22+
23+
if err := client.Sys().Mount("transit", &api.MountInput{
24+
Type: "transit",
25+
}); err != nil {
26+
t.Fatalf("transit mount error: %#v", err)
27+
}
28+
29+
rsa1, rsa2, aes128, aes256 := generateKeys(t)
30+
31+
type testCase struct {
32+
variant string
33+
path string
34+
key []byte
35+
args []string
36+
shouldFail bool
37+
}
38+
tests := []testCase{
39+
{
40+
"import",
41+
"transit/keys/rsa1",
42+
rsa1,
43+
[]string{"type=rsa-2048"},
44+
false, /* first import */
45+
},
46+
{
47+
"import",
48+
"transit/keys/rsa1",
49+
rsa2,
50+
[]string{"type=rsa-2048"},
51+
true, /* already exists */
52+
},
53+
{
54+
"import-version",
55+
"transit/keys/rsa1",
56+
rsa2,
57+
[]string{"type=rsa-2048"},
58+
false, /* new version */
59+
},
60+
{
61+
"import",
62+
"transit/keys/rsa2",
63+
rsa2,
64+
[]string{"type=rsa-4096"},
65+
true, /* wrong type */
66+
},
67+
{
68+
"import",
69+
"transit/keys/rsa2",
70+
rsa2,
71+
[]string{"type=rsa-2048"},
72+
false, /* new name */
73+
},
74+
{
75+
"import",
76+
"transit/keys/aes1",
77+
aes128,
78+
[]string{"type=aes128-gcm96"},
79+
false, /* first import */
80+
},
81+
{
82+
"import",
83+
"transit/keys/aes1",
84+
aes256,
85+
[]string{"type=aes256-gcm96"},
86+
true, /* already exists */
87+
},
88+
{
89+
"import-version",
90+
"transit/keys/aes1",
91+
aes256,
92+
[]string{"type=aes256-gcm96"},
93+
true, /* new version, different type */
94+
},
95+
{
96+
"import-version",
97+
"transit/keys/aes1",
98+
aes128,
99+
[]string{"type=aes128-gcm96"},
100+
false, /* new version */
101+
},
102+
{
103+
"import",
104+
"transit/keys/aes2",
105+
aes256,
106+
[]string{"type=aes128-gcm96"},
107+
true, /* wrong type */
108+
},
109+
{
110+
"import",
111+
"transit/keys/aes2",
112+
aes256,
113+
[]string{"type=aes256-gcm96"},
114+
false, /* new name */
115+
},
116+
}
117+
118+
for index, tc := range tests {
119+
t.Logf("Running test case %d: %v", index, tc)
120+
execTransitImport(t, client, tc.variant, tc.path, tc.key, tc.args, tc.shouldFail)
121+
}
122+
}
123+
124+
func execTransitImport(t *testing.T, client *api.Client, method string, path string, key []byte, data []string, expectFailure bool) {
125+
t.Helper()
126+
127+
keyBase64 := base64.StdEncoding.EncodeToString(key)
128+
129+
var args []string
130+
args = append(args, "transit")
131+
args = append(args, method)
132+
args = append(args, path)
133+
args = append(args, keyBase64)
134+
args = append(args, data...)
135+
136+
stdout := bytes.NewBuffer(nil)
137+
stderr := bytes.NewBuffer(nil)
138+
runOpts := &RunOptions{
139+
Stdout: stdout,
140+
Stderr: stderr,
141+
Client: client,
142+
}
143+
144+
code := RunCustom(args, runOpts)
145+
combined := stdout.String() + stderr.String()
146+
147+
if code != 0 {
148+
if !expectFailure {
149+
t.Fatalf("Got unexpected failure from test (ret %d): %v", code, combined)
150+
}
151+
} else {
152+
if expectFailure {
153+
t.Fatalf("Expected failure, got success from test (ret %d): %v", code, combined)
154+
}
155+
}
156+
}
157+
158+
func generateKeys(t *testing.T) (rsa1 []byte, rsa2 []byte, aes128 []byte, aes256 []byte) {
159+
t.Helper()
160+
161+
priv1, err := rsa.GenerateKey(rand.Reader, 2048)
162+
require.NotNil(t, priv1, "failed generating RSA 1 key")
163+
require.NoError(t, err, "failed generating RSA 1 key")
164+
165+
rsa1, err = x509.MarshalPKCS8PrivateKey(priv1)
166+
require.NotNil(t, rsa1, "failed marshaling RSA 1 key")
167+
require.NoError(t, err, "failed marshaling RSA 1 key")
168+
169+
priv2, err := rsa.GenerateKey(rand.Reader, 2048)
170+
require.NotNil(t, priv2, "failed generating RSA 2 key")
171+
require.NoError(t, err, "failed generating RSA 2 key")
172+
173+
rsa2, err = x509.MarshalPKCS8PrivateKey(priv2)
174+
require.NotNil(t, rsa2, "failed marshaling RSA 2 key")
175+
require.NoError(t, err, "failed marshaling RSA 2 key")
176+
177+
aes128 = make([]byte, 128/8)
178+
_, err = rand.Read(aes128)
179+
require.NoError(t, err, "failed generating AES 128 key")
180+
181+
aes256 = make([]byte, 256/8)
182+
_, err = rand.Read(aes256)
183+
require.NoError(t, err, "failed generating AES 256 key")
184+
185+
return
186+
}

command/transit_import_key_version.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,23 @@ func (c *TransitImportVersionCommand) Synopsis() string {
2222

2323
func (c *TransitImportVersionCommand) Help() string {
2424
helpText := `
25-
Usage: vault transit import-version PATH KEY
25+
Usage: vault transit import-version PATH KEY [...]
2626
2727
Using the Transit or Transform key wrapping system, imports key material from
28-
the base64 encoded KEY, into a new key whose API path is PATH. To import a new transit/transform key,
29-
use import. The remaining options after KEY (key=value style) are passed on to the transit/transform create key
30-
endpoint.
31-
If your system or device natively supports the RSA AES key wrap mechanism, you should use it directly
32-
rather than this command.
28+
the base64 encoded KEY (either directly on the CLI or via @path notation),
29+
into a new key whose API path is PATH. To import a new transit/transform
30+
key, use the import command instead. The remaining options after KEY
31+
(key=value style) are passed on to the transit/transform create key endpoint.
32+
If your system or device natively supports the RSA AES key wrap mechanism
33+
(such as the PKCS#11 mechanism CKM_RSA_AES_KEY_WRAP), you should use it
34+
directly rather than this command.
3335
` + c.Flags().Help()
3436

3537
return strings.TrimSpace(helpText)
3638
}
3739

3840
func (c *TransitImportVersionCommand) Flags() *FlagSets {
39-
return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
41+
return c.flagSet(FlagSetHTTP)
4042
}
4143

4244
func (c *TransitImportVersionCommand) AutocompleteArgs() complete.Predictor {
@@ -48,5 +50,5 @@ func (c *TransitImportVersionCommand) AutocompleteFlags() complete.Flags {
4850
}
4951

5052
func (c *TransitImportVersionCommand) Run(args []string) int {
51-
return importKey(c.BaseCommand, "import_version", args)
53+
return importKey(c.BaseCommand, "import_version", c.Flags(), args)
5254
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
layout: docs
3+
page_title: transit import and transit import-version - Command
4+
description: |-
5+
The "transit import" and "transit import-version" commands import the
6+
specified key into Transit, via the Transit BYOK mechanism.
7+
---
8+
9+
# transit import and transit import-version
10+
11+
The `transit import` and `transit import-version` commands import the
12+
specified key into Transit, via the [Transit BYOK
13+
mechanism](/vault/docs/secrets/transit#bring-your-own-key-byok). The former
14+
imports this key as a new key, failing if it already exists, whereas the
15+
latter will only update an existing key in Transit to a new version of the
16+
key material.
17+
18+
This needs access to read the transit mount's wrapping key (at
19+
`transit/wrapping_key`) and the ability to write to either import
20+
endpoints (either `transit/keys/:name/import` or
21+
`transit/keys/:name/import_version`).
22+
23+
## Examples
24+
25+
Imports a 2048-bit RSA key as a new key:
26+
27+
```
28+
$ vault transit import transit/keys/test-key @test-key type=rsa-2048
29+
Retrieving transit wrapping key.
30+
Wrapping source key with ephemeral key.
31+
Encrypting ephemeral key with transit wrapping key.
32+
Submitting wrapped key to Vault transit.
33+
Success!
34+
```
35+
36+
Imports a new version of an existing key:
37+
38+
```
39+
$ vault transit import-version transit/keys/test-key @test-key-updated
40+
Retrieving transit wrapping key.
41+
Wrapping source key with ephemeral key.
42+
Encrypting ephemeral key with transit wrapping key.
43+
Submitting wrapped key to Vault transit.
44+
Success!
45+
```
46+
47+
## Usage
48+
49+
This command does not have any unique flags and respects core Vault CLI
50+
commands. See `vault transit import -help` for more information.
51+
52+
This command requires two positional arguments:
53+
54+
1. `PATH`, the path to the transit key to import in the format of
55+
`<mount>/keys/<key-name>`, where `<mount>` is the path to the mount
56+
(using `-namespace=<ns>` to specify any namespaces), and `<key-name>`
57+
is the desired name of the key.
58+
2. `KEY`, the key material to import in Standard Base64 encoding (either
59+
of a raw key in the case of symmetric keys such as AES, or of the DER
60+
encoded format for asymmetric keys such as RSA). If the value for `KEY`
61+
begins with an `@`, the CLI argument is assumed to be a path to a file
62+
on disk to be read.

0 commit comments

Comments
 (0)