backport of commit 63ccb60b9a6dadf717e6813f9789c7194a2375de (#20877)

Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
hc-github-team-secure-vault-core 2023-05-30 19:49:24 -04:00 committed by GitHub
parent 1ab8ade40e
commit a486b13957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 589 additions and 5 deletions

View File

@ -57,6 +57,7 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error)
b.pathImportVersion(),
b.pathKeys(),
b.pathListKeys(),
b.pathBYOKExportKeys(),
b.pathExportKeys(),
b.pathKeysConfig(),
b.pathEncrypt(),

View File

@ -0,0 +1,206 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package transit
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"errors"
"fmt"
"strconv"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/keysutil"
"github.com/hashicorp/vault/sdk/logical"
)
func (b *backend) pathBYOKExportKeys() *framework.Path {
return &framework.Path{
Pattern: "byok-export/" + framework.GenericNameRegex("destination") + "/" + framework.GenericNameRegex("source") + framework.OptionalParamRegex("version"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixTransit,
OperationVerb: "byok",
OperationSuffix: "key|key-version",
},
Fields: map[string]*framework.FieldSchema{
"destination": {
Type: framework.TypeString,
Description: "Destination key to export to; usually the public wrapping key of another Transit instance.",
},
"source": {
Type: framework.TypeString,
Description: "Source key to export; could be any present key within Transit.",
},
"version": {
Type: framework.TypeString,
Description: "Optional version of the key to export, else all key versions are exported.",
},
"hash": {
Type: framework.TypeString,
Description: "Hash function to use for inner OAEP encryption. Defaults to SHA256.",
Default: "SHA256",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathPolicyBYOKExportRead,
},
HelpSynopsis: pathBYOKExportHelpSyn,
HelpDescription: pathBYOKExportHelpDesc,
}
}
func (b *backend) pathPolicyBYOKExportRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
dst := d.Get("destination").(string)
src := d.Get("source").(string)
version := d.Get("version").(string)
hash := d.Get("hash").(string)
dstP, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Name: dst,
}, b.GetRandomReader())
if err != nil {
return nil, err
}
if dstP == nil {
return nil, fmt.Errorf("no such destination key to export to")
}
if !b.System().CachingDisabled() {
dstP.Lock(false)
}
defer dstP.Unlock()
srcP, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Name: src,
}, b.GetRandomReader())
if err != nil {
return nil, err
}
if srcP == nil {
return nil, fmt.Errorf("no such source key for export")
}
if !b.System().CachingDisabled() {
srcP.Lock(false)
}
defer srcP.Unlock()
if !srcP.Exportable {
return logical.ErrorResponse("key is not exportable"), nil
}
retKeys := map[string]string{}
switch version {
case "":
for k, v := range srcP.Keys {
exportKey, err := getBYOKExportKey(dstP, srcP, &v, hash)
if err != nil {
return nil, err
}
retKeys[k] = exportKey
}
default:
var versionValue int
if version == "latest" {
versionValue = srcP.LatestVersion
} else {
version = strings.TrimPrefix(version, "v")
versionValue, err = strconv.Atoi(version)
if err != nil {
return logical.ErrorResponse("invalid key version"), logical.ErrInvalidRequest
}
}
if versionValue < srcP.MinDecryptionVersion {
return logical.ErrorResponse("version for export is below minimum decryption version"), logical.ErrInvalidRequest
}
key, ok := srcP.Keys[strconv.Itoa(versionValue)]
if !ok {
return logical.ErrorResponse("version does not exist or cannot be found"), logical.ErrInvalidRequest
}
exportKey, err := getBYOKExportKey(dstP, srcP, &key, hash)
if err != nil {
return nil, err
}
retKeys[strconv.Itoa(versionValue)] = exportKey
}
resp := &logical.Response{
Data: map[string]interface{}{
"name": srcP.Name,
"type": srcP.Type.String(),
"keys": retKeys,
},
}
return resp, nil
}
func getBYOKExportKey(dstP *keysutil.Policy, srcP *keysutil.Policy, key *keysutil.KeyEntry, hash string) (string, error) {
if dstP == nil || srcP == nil {
return "", errors.New("nil policy provided")
}
var targetKey interface{}
switch srcP.Type {
case keysutil.KeyType_AES128_GCM96, keysutil.KeyType_AES256_GCM96, keysutil.KeyType_ChaCha20_Poly1305, keysutil.KeyType_HMAC:
targetKey = key.Key
case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096:
targetKey = key.RSAKey
case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521:
var curve elliptic.Curve
switch srcP.Type {
case keysutil.KeyType_ECDSA_P384:
curve = elliptic.P384()
case keysutil.KeyType_ECDSA_P521:
curve = elliptic.P521()
default:
curve = elliptic.P256()
}
pubKey := ecdsa.PublicKey{
Curve: curve,
X: key.EC_X,
Y: key.EC_Y,
}
targetKey = &ecdsa.PrivateKey{
PublicKey: pubKey,
D: key.EC_D,
}
case keysutil.KeyType_ED25519:
targetKey = ed25519.PrivateKey(key.Key)
default:
return "", fmt.Errorf("unable to export to unknown key type: %v", srcP.Type)
}
hasher, err := parseHashFn(hash)
if err != nil {
return "", err
}
return dstP.WrapKey(0, targetKey, srcP.Type, hasher)
}
const pathBYOKExportHelpSyn = `Securely export named encryption or signing key`
const pathBYOKExportHelpDesc = `
This path is used to export the named keys that are configured as
exportable.
Unlike the regular /export/:name[/:version] paths, this path uses
the same encryption specification /import, allowing secure migration
of keys between clusters to enable workloads to communicate between
them.
Presently this only works for RSA destination keys.
`

View File

@ -0,0 +1,225 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package transit
import (
"context"
"testing"
"github.com/hashicorp/vault/sdk/logical"
)
func TestTransit_BYOKExportImport(t *testing.T) {
// Test encryption/decryption after a restore for supported keys
testBYOKExportImport(t, "aes128-gcm96", "encrypt-decrypt")
testBYOKExportImport(t, "aes256-gcm96", "encrypt-decrypt")
testBYOKExportImport(t, "chacha20-poly1305", "encrypt-decrypt")
testBYOKExportImport(t, "rsa-2048", "encrypt-decrypt")
testBYOKExportImport(t, "rsa-3072", "encrypt-decrypt")
testBYOKExportImport(t, "rsa-4096", "encrypt-decrypt")
// Test signing/verification after a restore for supported keys
testBYOKExportImport(t, "ecdsa-p256", "sign-verify")
testBYOKExportImport(t, "ecdsa-p384", "sign-verify")
testBYOKExportImport(t, "ecdsa-p521", "sign-verify")
testBYOKExportImport(t, "ed25519", "sign-verify")
testBYOKExportImport(t, "rsa-2048", "sign-verify")
testBYOKExportImport(t, "rsa-3072", "sign-verify")
testBYOKExportImport(t, "rsa-4096", "sign-verify")
// Unlike backup, we don't support importing HMAC keys here.
}
func testBYOKExportImport(t *testing.T, keyType, feature string) {
var resp *logical.Response
var err error
b, s := createBackendWithStorage(t)
// Create a key
keyReq := &logical.Request{
Path: "keys/test-source",
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"type": keyType,
"exportable": true,
},
}
resp, err = b.HandleRequest(context.Background(), keyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
// Read the wrapping key.
wrapKeyReq := &logical.Request{
Path: "wrapping_key",
Operation: logical.ReadOperation,
Storage: s,
}
resp, err = b.HandleRequest(context.Background(), wrapKeyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
// Import the wrapping key.
wrapKeyImportReq := &logical.Request{
Path: "keys/wrapper/import",
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"public_key": resp.Data["public_key"],
"type": "rsa-4096",
},
}
resp, err = b.HandleRequest(context.Background(), wrapKeyImportReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
// Export the key
backupReq := &logical.Request{
Path: "byok-export/wrapper/test-source",
Operation: logical.ReadOperation,
Storage: s,
}
resp, err = b.HandleRequest(context.Background(), backupReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
keys := resp.Data["keys"].(map[string]string)
// Import the key to a new name.
restoreReq := &logical.Request{
Path: "keys/test/import",
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"ciphertext": keys["1"],
"type": keyType,
},
}
resp, err = b.HandleRequest(context.Background(), restoreReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
plaintextB64 := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox"
// Perform encryption, signing or hmac-ing based on the set 'feature'
var encryptReq, signReq, hmacReq *logical.Request
var ciphertext, signature, hmac string
switch feature {
case "encrypt-decrypt":
encryptReq = &logical.Request{
Path: "encrypt/test-source",
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"plaintext": plaintextB64,
},
}
resp, err = b.HandleRequest(context.Background(), encryptReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
ciphertext = resp.Data["ciphertext"].(string)
case "sign-verify":
signReq = &logical.Request{
Path: "sign/test-source",
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"input": plaintextB64,
},
}
resp, err = b.HandleRequest(context.Background(), signReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
signature = resp.Data["signature"].(string)
case "hmac-verify":
hmacReq = &logical.Request{
Path: "hmac/test-source",
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"input": plaintextB64,
},
}
resp, err = b.HandleRequest(context.Background(), hmacReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
hmac = resp.Data["hmac"].(string)
}
// validationFunc verifies the ciphertext, signature or hmac based on the
// set 'feature'
validationFunc := func(keyName string) {
var decryptReq *logical.Request
var verifyReq *logical.Request
switch feature {
case "encrypt-decrypt":
decryptReq = &logical.Request{
Path: "decrypt/" + keyName,
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"ciphertext": ciphertext,
},
}
resp, err = b.HandleRequest(context.Background(), decryptReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
if resp.Data["plaintext"].(string) != plaintextB64 {
t.Fatalf("bad: plaintext; expected: %q, actual: %q", plaintextB64, resp.Data["plaintext"].(string))
}
case "sign-verify":
verifyReq = &logical.Request{
Path: "verify/" + keyName,
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"signature": signature,
"input": plaintextB64,
},
}
resp, err = b.HandleRequest(context.Background(), verifyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
if resp.Data["valid"].(bool) != true {
t.Fatalf("bad: signature verification failed for key type %q", keyType)
}
case "hmac-verify":
verifyReq = &logical.Request{
Path: "verify/" + keyName,
Operation: logical.UpdateOperation,
Storage: s,
Data: map[string]interface{}{
"hmac": hmac,
"input": plaintextB64,
},
}
resp, err = b.HandleRequest(context.Background(), verifyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v\nerr: %v", resp, err)
}
if resp.Data["valid"].(bool) != true {
t.Fatalf("bad: HMAC verification failed for key type %q", keyType)
}
}
}
// Ensure that the restored key is functional
validationFunc("test")
// Ensure the original key is functional
validationFunc("test-source")
}

3
changelog/20736.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/transit: Support BYOK-encrypted export of keys to securely allow synchronizing specific keys and version across clusters.
```

2
go.mod
View File

@ -69,7 +69,7 @@ require (
github.com/google/go-cmp v0.5.9
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-metrics-stackdriver v0.2.0
github.com/google/tink/go v1.6.1
github.com/google/tink/go v1.7.0
github.com/hashicorp/cap v0.3.0
github.com/hashicorp/consul-template v0.32.0
github.com/hashicorp/consul/api v1.20.0

14
go.sum
View File

@ -739,6 +739,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@ -755,7 +756,7 @@ github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/aws/aws-sdk-go v1.36.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.43.9/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.268 h1:WoK20tlAvsvQzTcE6TajoprbXmTbcud6MjhErL4P/38=
github.com/aws/aws-sdk-go v1.44.268/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
@ -1614,8 +1615,8 @@ github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -1752,6 +1753,7 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
github.com/hashicorp/go-plugin v1.4.9/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
@ -1776,6 +1778,7 @@ github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1 h1:9um9R8i0+HbRHS9d64kd
github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1/go.mod h1:6RoRTSMDK2H/rKh3P/JIsk1tK8aatKTt3JyvIopi3GQ=
github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.2 h1:NS6BHieb/pDfx3M9jDdaPpGyyVp+aD4A3DjX3dgRmzs=
github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.2/go.mod h1:rf5JPE13wi+NwjgsmGkbg4b2CgHq8v7Htn/F0nDe/hg=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 h1:kH3Rhiht36xhAfhuHyWJDgdXXEx9IIZhDGRk24CDhzg=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.3/go.mod h1:ov1Q0oEDjC3+A4BwsG2YdKltrmEw8sf9Pau4V9JQ4Vo=
@ -1808,6 +1811,7 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
@ -2228,6 +2232,7 @@ github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
@ -2428,6 +2433,7 @@ github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rK
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@ -2941,6 +2947,7 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@ -3509,7 +3516,6 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=

View File

@ -14,6 +14,7 @@ require (
github.com/go-test/deep v1.1.0
github.com/golang/protobuf v1.5.2
github.com/golang/snappy v0.0.4
github.com/google/tink/go v1.7.0
github.com/hashicorp/errwrap v1.1.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v1.4.0

View File

@ -76,6 +76,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=

View File

@ -22,6 +22,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"hash"
"io"
"math/big"
"path"
@ -41,6 +42,8 @@ import (
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/kdf"
"github.com/hashicorp/vault/sdk/logical"
"github.com/google/tink/go/kwp/subtle"
)
// Careful with iota; don't put anything before it in this const block because
@ -2295,3 +2298,88 @@ func (ke *KeyEntry) parseFromKey(PolKeyType KeyType, parsedKey any) error {
return nil
}
func (p *Policy) WrapKey(ver int, targetKey interface{}, targetKeyType KeyType, hash hash.Hash) (string, error) {
if !p.Type.SigningSupported() {
return "", fmt.Errorf("message signing not supported for key type %v", p.Type)
}
switch {
case ver == 0:
ver = p.LatestVersion
case ver < 0:
return "", errutil.UserError{Err: "requested version for key wrapping is negative"}
case ver > p.LatestVersion:
return "", errutil.UserError{Err: "requested version for key wrapping is higher than the latest key version"}
case p.MinEncryptionVersion > 0 && ver < p.MinEncryptionVersion:
return "", errutil.UserError{Err: "requested version for key wrapping is less than the minimum encryption key version"}
}
keyEntry, err := p.safeGetKeyEntry(ver)
if err != nil {
return "", err
}
return keyEntry.WrapKey(targetKey, targetKeyType, hash)
}
func (ke *KeyEntry) WrapKey(targetKey interface{}, targetKeyType KeyType, hash hash.Hash) (string, error) {
// Presently this method implements a CKM_RSA_AES_KEY_WRAP-compatible
// wrapping interface and only works on RSA keyEntries as a result.
if ke.RSAPublicKey == nil {
return "", fmt.Errorf("unsupported key type in use; must be a rsa key")
}
var preppedTargetKey []byte
switch targetKeyType {
case KeyType_AES128_GCM96, KeyType_AES256_GCM96, KeyType_ChaCha20_Poly1305, KeyType_HMAC:
var ok bool
preppedTargetKey, ok = targetKey.([]byte)
if !ok {
return "", fmt.Errorf("failed to wrap target key for import: symmetric key not provided in byte format (%T)", targetKey)
}
default:
var err error
preppedTargetKey, err = x509.MarshalPKCS8PrivateKey(targetKey)
if err != nil {
return "", fmt.Errorf("failed to wrap target key for import: %w", err)
}
}
result, err := wrapTargetPKCS8ForImport(ke.RSAPublicKey, preppedTargetKey, hash)
if err != nil {
return result, fmt.Errorf("failed to wrap target key for import: %w", err)
}
return result, nil
}
func wrapTargetPKCS8ForImport(wrappingKey *rsa.PublicKey, preppedTargetKey []byte, hash hash.Hash) (string, error) {
// Generate an ephemeral AES-256 key
ephKey, err := uuid.GenerateRandomBytes(32)
if err != nil {
return "", fmt.Errorf("failed to generate an ephemeral AES wrapping key: %w", err)
}
// Wrap ephemeral AES key with public wrapping key
ephKeyWrapped, err := rsa.EncryptOAEP(hash, rand.Reader, wrappingKey, ephKey, []byte{} /* label */)
if err != nil {
return "", fmt.Errorf("failed to encrypt ephemeral wrapping key with public key: %w", err)
}
// Create KWP instance for wrapping target key
kwp, err := subtle.NewKWP(ephKey)
if err != nil {
return "", fmt.Errorf("failed to generate new KWP from AES key: %w", err)
}
// Wrap target key with KWP
targetKeyWrapped, err := kwp.Wrap(preppedTargetKey)
if err != nil {
return "", fmt.Errorf("failed to wrap target key with KWP: %w", err)
}
// Combined wrapped keys into a single blob and base64 encode
wrappedKeys := append(ephKeyWrapped, targetKeyWrapped...)
return base64.StdEncoding.EncodeToString(wrappedKeys), nil
}

View File

@ -510,6 +510,58 @@ $ curl \
http://127.0.0.1:8200/v1/transit/keys/my-key/rotate
```
## Securely Export Key
This endpoint returns a wrapped copy of the `source` key, protected by the
`destination` key using BYOK method accepted by the
`/transit/keys/:name/import` API. This allows an operator using two separate
Vault instances to secure established shared key material, withing exposing
either key in plaintext and needing to run a manual BYOK import using the
CLI helper utility.
| Method | Path |
| :----- | :----------------------------------------------------- |
| `GET` | `/transit/byok-export/:destination/:source(/:version)` |
### Parameters
- `destination` `(string: <required>)` - Specifies the name of the key to
encrypt the `source` key to: this is usually another mount or cluster's
wrapping key (from `/transit/wrapping_key`). This is specified as part of
the URL.
~> Note: This destination key type must be an RSA key type.
- `source` `(string: <required>)` - Specifies the source key to encrypt, to
copy (encrypted) to another cluster. This is specified as part of the URL.
- `version` `(string: "")` - Specifies the version of the source key to
wrap. If omitted, all versions of the key will be returned. This is
specified as part of the URL. If the version is set to `latest`, the
current key will be returned.
### Sample Request
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
http://127.0.0.1:8200/v1/transit/byok-export/wrapping-key/to-be-shared-key/1
```
### Sample Response
```json
{
"data": {
"name": "foo",
"keys": {
"1": "H/0T+CKQ8I82KJWpPk ... additional response elided ...",
}
}
}
```
## Export Key
This endpoint returns the named key. The `keys` object shows the value of the