Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transit BYOK export capabilities #20736

Merged
merged 8 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"),
stevendpclark marked this conversation as resolved.
Show resolved Hide resolved

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a potential deadlock here, but I don't think there is much we can do about it and I don't think it's realistic people would call this with inverted params in parallel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed. In general, I think it'd be hard to do inverted semantics unless you were using non-Transit BYOK importing. Given dest is a Transit key, it lacks a private counterpart, and so cannot be exported...

Maybe we delay this !b.System().CachingDisabled() check and first check that the src key has private counterparts, but I don't think that'll help the default case...

Copy link
Contributor

@stevendpclark stevendpclark May 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah and if caching is enabled disabled the lock is acquired within the b.GetPolicy so we are still hosed.

}
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