| // 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. |
| ` |