Skip to content

Commit

Permalink
Transit BYOK export capabilities (hashicorp#20736)
Browse files Browse the repository at this point in the history
* Add WrapKey capabilities to keysutil

This allows one keysutil to wrap another key, assuming that key has an
type matching one of keysutil's allowed KeyTypes. This allows completing
the BYOK import loop with Transit, allowing imported wrapping keys to
export (wrap) other keys in transit, without having them leave in
plaintext.

Signed-off-by: Alexander Scheel <[email protected]>

* Add /byok-export/:dst/:src[/:version] to Transit

Still respecting exportable, we allow encrypted-only export of transit
keys to another cluster using the BYOK semantics.

In particular, this allows an operator to securely establish key
material between two separate Transit installations. This potentially
allows one cluster to be used as a source cluster (encrypting a large
amount of data) and a second cluster to decrypt this data later. This
might be useful in hybrid or site-specific deployments of Vault for
instance.

Signed-off-by: Alexander Scheel <[email protected]>

* Add missing dependency to sdk/, vault/

Also updates to a newer version while we're here.

Signed-off-by: Alexander Scheel <[email protected]>

* Add documentation on BYOK export

Signed-off-by: Alexander Scheel <[email protected]>

* Add tests for BYOK export/import

Signed-off-by: Alexander Scheel <[email protected]>

* Add changelog entry

Signed-off-by: Alexander Scheel <[email protected]>

* Update website/content/api-docs/secret/transit.mdx

* Update builtin/logical/transit/path_byok.go

Co-authored-by: Matt Schultz <[email protected]>

---------

Signed-off-by: Alexander Scheel <[email protected]>
Co-authored-by: Matt Schultz <[email protected]>
  • Loading branch information
cipherboy and schultz-is authored May 30, 2023
1 parent 10c16cc commit 63ccb60
Show file tree
Hide file tree
Showing 10 changed files with 589 additions and 5 deletions.
1 change: 1 addition & 0 deletions builtin/logical/transit/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
206 changes: 206 additions & 0 deletions builtin/logical/transit/path_byok.go
Original file line number Diff line number Diff line change
@@ -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.
`
Loading

0 comments on commit 63ccb60

Please sign in to comment.