From 63ccb60b9a6dadf717e6813f9789c7194a2375de Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 30 May 2023 17:42:41 -0400 Subject: [PATCH] Transit BYOK export capabilities (#20736) * 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 * 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 * Add missing dependency to sdk/, vault/ Also updates to a newer version while we're here. Signed-off-by: Alexander Scheel * Add documentation on BYOK export Signed-off-by: Alexander Scheel * Add tests for BYOK export/import Signed-off-by: Alexander Scheel * Add changelog entry Signed-off-by: Alexander Scheel * Update website/content/api-docs/secret/transit.mdx * Update builtin/logical/transit/path_byok.go Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com> --------- Signed-off-by: Alexander Scheel Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com> --- builtin/logical/transit/backend.go | 1 + builtin/logical/transit/path_byok.go | 206 ++++++++++++++++++ builtin/logical/transit/path_byok_test.go | 225 ++++++++++++++++++++ changelog/20736.txt | 3 + go.mod | 2 +- go.sum | 14 +- sdk/go.mod | 1 + sdk/go.sum | 2 + sdk/helper/keysutil/policy.go | 88 ++++++++ website/content/api-docs/secret/transit.mdx | 52 +++++ 10 files changed, 589 insertions(+), 5 deletions(-) create mode 100644 builtin/logical/transit/path_byok.go create mode 100644 builtin/logical/transit/path_byok_test.go create mode 100644 changelog/20736.txt diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 3dbce416be2c..03c3b2fda4e4 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -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(), diff --git a/builtin/logical/transit/path_byok.go b/builtin/logical/transit/path_byok.go new file mode 100644 index 000000000000..40f7cac1a472 --- /dev/null +++ b/builtin/logical/transit/path_byok.go @@ -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. +` diff --git a/builtin/logical/transit/path_byok_test.go b/builtin/logical/transit/path_byok_test.go new file mode 100644 index 000000000000..7fc0c9946da3 --- /dev/null +++ b/builtin/logical/transit/path_byok_test.go @@ -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") +} diff --git a/changelog/20736.txt b/changelog/20736.txt new file mode 100644 index 000000000000..1c4c3d4d256e --- /dev/null +++ b/changelog/20736.txt @@ -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. +``` diff --git a/go.mod b/go.mod index 095e809a595b..5b64b33a7692 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0334f731b27d..777f2d0ca223 100644 --- a/go.sum +++ b/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= diff --git a/sdk/go.mod b/sdk/go.mod index 96ab9f03f3f9..c4a2ec99fdbc 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -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 diff --git a/sdk/go.sum b/sdk/go.sum index 8929d377bd5b..a6b0f59b096c 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -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= diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 36d5a9b26831..99eadeac1fe1 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -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 +} diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index bfff32770b66..1b7a9e201677 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -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: )` - 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: )` - 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