backport of commit 63ccb60b9a6dadf717e6813f9789c7194a2375de (#20877)
Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
parent
1ab8ade40e
commit
a486b13957
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
`
|
|
@ -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")
|
||||
}
|
|
@ -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
2
go.mod
|
@ -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
14
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue