diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index f2454a80f..391b392c6 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -47,6 +47,8 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error) b.pathRotate(), b.pathRewrap(), b.pathWrappingKey(), + b.pathImport(), + b.pathImportVersion(), b.pathKeys(), b.pathListKeys(), b.pathExportKeys(), @@ -248,6 +250,11 @@ func (b *backend) rotateIfRequired(ctx context.Context, req *logical.Request, ke } defer p.Unlock() + // If the key is imported, it can only be rotated from within Vault if allowed. + if p.Imported && !p.AllowImportedKeyRotation { + return nil + } + // If the policy's automatic rotation period is 0, it should not // automatically rotate. if p.AutoRotatePeriod == 0 { diff --git a/builtin/logical/transit/path_import.go b/builtin/logical/transit/path_import.go new file mode 100644 index 000000000..0f86ea3ef --- /dev/null +++ b/builtin/logical/transit/path_import.go @@ -0,0 +1,346 @@ +package transit + +import ( + "context" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "hash" + "strconv" + "strings" + "time" + + "github.com/google/tink/go/kwp/subtle" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/keysutil" + "github.com/hashicorp/vault/sdk/logical" +) + +const EncryptedKeyBytes = 512 + +func (b *backend) pathImport() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/import", + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "The name of the key", + }, + "type": { + Type: framework.TypeString, + Default: "aes256-gcm96", + Description: `The type of key being imported. Currently, "aes128-gcm96" (symmetric), "aes256-gcm96" (symmetric), "ecdsa-p256" +(asymmetric), "ecdsa-p384" (asymmetric), "ecdsa-p521" (asymmetric), "ed25519" (asymmetric), "rsa-2048" (asymmetric), "rsa-3072" +(asymmetric), "rsa-4096" (asymmetric) are supported. Defaults to "aes256-gcm96". +`, + }, + "hash_function": { + Type: framework.TypeString, + Default: "SHA256", + Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated, +ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`, + }, + "ciphertext": { + Type: framework.TypeString, + Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP +with the wrapping key and then concatenated with the import key, wrapped by the AES key.`, + }, + "allow_rotation": { + Type: framework.TypeBool, + Description: "True if the imported key may be rotated within Vault; false otherwise.", + }, + "derived": { + Type: framework.TypeBool, + Description: `Enables key derivation mode. This +allows for per-transaction unique +keys for encryption operations.`, + }, + + "exportable": { + Type: framework.TypeBool, + Description: `Enables keys to be exportable. +This allows for all the valid keys +in the key ring to be exported.`, + }, + + "allow_plaintext_backup": { + Type: framework.TypeBool, + Description: `Enables taking a backup of the named +key in plaintext format. Once set, +this cannot be disabled.`, + }, + + "context": { + Type: framework.TypeString, + Description: `Base64 encoded context for key derivation. +When reading a key with key derivation enabled, +if the key type supports public keys, this will +return the public key for the given context.`, + }, + "auto_rotate_period": { + Type: framework.TypeDurationSecond, + Default: 0, + Description: `Amount of time the key should live before +being automatically rotated. A value of 0 +(default) disables automatic rotation for the +key.`, + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathImportWrite, + }, + HelpSynopsis: pathImportWriteSyn, + HelpDescription: pathImportWriteDesc, + } +} + +func (b *backend) pathImportVersion() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/import_version", + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "The name of the key", + }, + "ciphertext": { + Type: framework.TypeString, + Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP +with the wrapping key and then concatenated with the import key, wrapped by the AES key.`, + }, + "hash_function": { + Type: framework.TypeString, + Default: "SHA256", + Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated, +ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`, + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathImportVersionWrite, + }, + HelpSynopsis: pathImportVersionWriteSyn, + HelpDescription: pathImportVersionWriteDesc, + } +} + +func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + derived := d.Get("derived").(bool) + keyType := d.Get("type").(string) + hashFnStr := d.Get("hash_function").(string) + exportable := d.Get("exportable").(bool) + allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool) + autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int)) + ciphertextString := d.Get("ciphertext").(string) + allowRotation := d.Get("allow_rotation").(bool) + + // Ensure the caller didn't supply "convergent_encryption" as a field, since it's not supported on import. + if _, ok := d.Raw["convergent_encryption"]; ok { + return nil, errors.New("import cannot be used on keys with convergent encryption enabled") + } + + if autoRotatePeriod > 0 && !allowRotation { + return nil, errors.New("allow_rotation must be set to true if auto-rotation is enabled") + } + + polReq := keysutil.PolicyRequest{ + Storage: req.Storage, + Name: name, + Derived: derived, + Exportable: exportable, + AllowPlaintextBackup: allowPlaintextBackup, + AutoRotatePeriod: autoRotatePeriod, + AllowImportedKeyRotation: allowRotation, + } + + switch keyType { + case "aes128-gcm96": + polReq.KeyType = keysutil.KeyType_AES128_GCM96 + case "aes256-gcm96": + polReq.KeyType = keysutil.KeyType_AES256_GCM96 + case "chacha20-poly1305": + polReq.KeyType = keysutil.KeyType_ChaCha20_Poly1305 + case "ecdsa-p256": + polReq.KeyType = keysutil.KeyType_ECDSA_P256 + case "ecdsa-p384": + polReq.KeyType = keysutil.KeyType_ECDSA_P384 + case "ecdsa-p521": + polReq.KeyType = keysutil.KeyType_ECDSA_P521 + case "ed25519": + polReq.KeyType = keysutil.KeyType_ED25519 + case "rsa-2048": + polReq.KeyType = keysutil.KeyType_RSA2048 + case "rsa-3072": + polReq.KeyType = keysutil.KeyType_RSA3072 + case "rsa-4096": + polReq.KeyType = keysutil.KeyType_RSA4096 + default: + return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest + } + + hashFn, err := parseHashFn(hashFnStr) + if err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + + p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) + if err != nil { + return nil, err + } + + if p != nil { + if b.System().CachingDisabled() { + p.Unlock() + } + return nil, errors.New("the import path cannot be used with an existing key; use import-version to rotate an existing imported key") + } + + ciphertext, err := base64.RawURLEncoding.DecodeString(ciphertextString) + if err != nil { + return nil, err + } + + key, err := b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn) + if err != nil { + return nil, err + } + + err = b.lm.ImportPolicy(ctx, polReq, key, b.GetRandomReader()) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathImportVersionWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + hashFnStr := d.Get("hash_function").(string) + ciphertextString := d.Get("ciphertext").(string) + + polReq := keysutil.PolicyRequest{ + Storage: req.Storage, + Name: name, + Upsert: false, + } + + hashFn, err := parseHashFn(hashFnStr) + if err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + + p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) + if err != nil { + return nil, err + } + if p == nil { + return nil, fmt.Errorf("no key found with name %s; to import a new key, use the import/ endpoint", name) + } + if !p.Imported { + return nil, errors.New("the import_version endpoint can only be used with an imported key") + } + if p.ConvergentEncryption { + return nil, errors.New("import_version cannot be used on keys with convergent encryption enabled") + } + + if !b.System().CachingDisabled() { + p.Lock(true) + } + defer p.Unlock() + + ciphertext, err := base64.RawURLEncoding.DecodeString(ciphertextString) + if err != nil { + return nil, err + } + importKey, err := b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn) + err = p.Import(ctx, req.Storage, importKey, b.GetRandomReader()) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) decryptImportedKey(ctx context.Context, storage logical.Storage, ciphertext []byte, hashFn hash.Hash) ([]byte, error) { + // Bounds check the ciphertext to avoid panics + if len(ciphertext) <= EncryptedKeyBytes { + return nil, errors.New("provided ciphertext is too short") + } + + wrappedEphKey := ciphertext[:EncryptedKeyBytes] + wrappedImportKey := ciphertext[EncryptedKeyBytes:] + + wrappingKey, err := b.getWrappingKey(ctx, storage) + if err != nil { + return nil, err + } + if wrappingKey == nil { + return nil, fmt.Errorf("error importing key: wrapping key was nil") + } + + privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey + ephKey, err := rsa.DecryptOAEP(hashFn, b.GetRandomReader(), privWrappingKey, wrappedEphKey, []byte{}) + if err != nil { + return nil, err + } + + // Zero out the ephemeral AES key just to be extra cautious. Note that this + // isn't a guarantee against memory analysis! See the documentation for the + // `vault.memzero` utility function for more information. + defer func() { + for i := range ephKey { + ephKey[i] = 0 + } + }() + + // Ensure the ephemeral AES key is 256-bit + if len(ephKey) != 32 { + return nil, errors.New("expected ephemeral AES key to be 256-bit") + } + + kwp, err := subtle.NewKWP(ephKey) + if err != nil { + return nil, err + } + + importKey, err := kwp.Unwrap(wrappedImportKey) + if err != nil { + return nil, err + } + + return importKey, nil +} + +func parseHashFn(hashFn string) (hash.Hash, error) { + switch strings.ToUpper(hashFn) { + case "SHA1": + return sha1.New(), nil + case "SHA224": + return sha256.New224(), nil + case "SHA256": + return sha256.New(), nil + case "SHA384": + return sha512.New384(), nil + case "SHA512": + return sha512.New(), nil + default: + return nil, fmt.Errorf("unknown hash function: %s", hashFn) + } +} + +const ( + pathImportWriteSyn = "Imports an externally-generated key into a new transit key" + pathImportWriteDesc = "This path is used to import an externally-generated " + + "key into Vault. The import operation creates a new key and cannot be used to " + + "replace an existing key." +) + +const pathImportVersionWriteSyn = "Imports an externally-generated key into an " + + "existing imported key" + +const pathImportVersionWriteDesc = "This path is used to import a new version of an " + + "externally-generated key into an existing import key. The import_version endpoint " + + "only supports importing key material into existing imported keys." diff --git a/builtin/logical/transit/path_import_test.go b/builtin/logical/transit/path_import_test.go new file mode 100644 index 000000000..ce3b82abc --- /dev/null +++ b/builtin/logical/transit/path_import_test.go @@ -0,0 +1,583 @@ +package transit + +import ( + "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "fmt" + "strconv" + "sync" + "testing" + + "github.com/google/tink/go/kwp/subtle" + uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/sdk/logical" +) + +var keyTypes = []string{ + "aes256-gcm96", + "aes128-gcm96", + "chacha20-poly1305", + "ed25519", + "ecdsa-p256", + "ecdsa-p384", + "ecdsa-p521", + "rsa-2048", + "rsa-3072", + "rsa-4096", +} + +var hashFns = []string{ + "SHA256", + "SHA1", + "SHA224", + "SHA384", + "SHA512", +} + +var ( + keysLock sync.RWMutex + keys = map[string]interface{}{} +) + +func generateKeys(t *testing.T) { + t.Helper() + + keysLock.Lock() + defer keysLock.Unlock() + + if len(keys) > 0 { + return + } + + for _, keyType := range keyTypes { + key, err := generateKey(keyType) + if err != nil { + t.Fatalf("failed to generate %s key: %s", keyType, err) + } + keys[keyType] = key + } +} + +func getKey(t *testing.T, keyType string) interface{} { + t.Helper() + + keysLock.RLock() + defer keysLock.RUnlock() + + key, ok := keys[keyType] + if !ok { + t.Fatalf("no pre-generated key of type: %s", keyType) + } + + return key +} + +func TestTransit_Import(t *testing.T) { + generateKeys(t) + b, s := createBackendWithStorage(t) + + t.Run( + "import into a key fails before wrapping key is read", + func(t *testing.T) { + fakeWrappingKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + t.Fatalf("failed to generate fake wrapping key: %s", err) + } + // Roll an AES256 key and import + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + targetKey := getKey(t, "aes256-gcm96") + importBlob := wrapTargetKeyForImport(t, &fakeWrappingKey.PublicKey, targetKey, "aes256-gcm96", "SHA256") + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{ + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("import prior to wrapping key generation incorrectly succeeded") + } + }, + ) + + // Retrieve public wrapping key + wrappingKey, err := b.getWrappingKey(context.Background(), s) + if err != nil || wrappingKey == nil { + t.Fatalf("failed to retrieve public wrapping key: %s", err) + } + privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey + pubWrappingKey := &privWrappingKey.PublicKey + + t.Run( + "import into an existing key fails", + func(t *testing.T) { + // Generate a key ID + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate a key ID: %s", err) + } + + // Create an AES256 key within Transit + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s", keyID), + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("unexpected error creating key: %s", err) + } + + targetKey := getKey(t, "aes256-gcm96") + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") + req = &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{ + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("import into an existing key incorrectly succeeded") + } + }, + ) + + for _, keyType := range keyTypes { + priv := getKey(t, keyType) + for _, hashFn := range hashFns { + t.Run( + fmt.Sprintf("%s/%s", keyType, hashFn), + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, priv, keyType, hashFn) + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{ + "type": keyType, + "hash_function": hashFn, + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("failed to import valid key: %s", err) + } + }, + ) + + // Shouldn't need to test every combination of key and hash function + if keyType != "aes256-gcm96" { + break + } + } + } + + failures := []struct { + name string + ciphertext interface{} + keyType interface{} + hashFn interface{} + }{ + { + name: "nil ciphertext", + }, + { + name: "empty string ciphertext", + ciphertext: "", + }, + { + name: "ciphertext not base64", + ciphertext: "this isn't correct", + }, + { + name: "ciphertext too short", + ciphertext: "ZmFrZSBjaXBoZXJ0ZXh0Cg", + }, + { + name: "invalid key type", + keyType: "fake-key-type", + }, + { + name: "invalid hash function", + hashFn: "fake-hash-fn", + }, + } + for _, tt := range failures { + t.Run( + tt.name, + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{}, + } + if tt.ciphertext != nil { + req.Data["ciphertext"] = tt.ciphertext + } + if tt.keyType != nil { + req.Data["type"] = tt.keyType + } + if tt.hashFn != nil { + req.Data["hash_function"] = tt.hashFn + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("invalid import request incorrectly succeeded") + } + }, + ) + } + + t.Run( + "disallow import of convergent keys", + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + targetKey := getKey(t, "aes256-gcm96") + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{ + "convergent_encryption": true, + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("import of convergent key incorrectly succeeded") + } + }, + ) + + t.Run( + "allow_rotation=true enables rotation within vault", + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + targetKey := getKey(t, "aes256-gcm96") + + // Import key + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{ + "allow_rotation": true, + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("failed to import key: %s", err) + } + + // Rotate key + req = &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/rotate", keyID), + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("failed to rotate key: %s", err) + } + }, + ) + + t.Run( + "allow_rotation=false disables rotation within vault", + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + targetKey := getKey(t, "aes256-gcm96") + + // Import key + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{ + "allow_rotation": false, + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("failed to import key: %s", err) + } + + // Rotate key + req = &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/rotate", keyID), + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("rotation of key with allow_rotation incorrectly succeeded") + } + }, + ) +} + +func TestTransit_ImportVersion(t *testing.T) { + generateKeys(t) + b, s := createBackendWithStorage(t) + + t.Run( + "import into a key version fails before wrapping key is read", + func(t *testing.T) { + fakeWrappingKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + t.Fatalf("failed to generate fake wrapping key: %s", err) + } + // Roll an AES256 key and import + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + targetKey := getKey(t, "aes256-gcm96") + importBlob := wrapTargetKeyForImport(t, &fakeWrappingKey.PublicKey, targetKey, "aes256-gcm96", "SHA256") + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import_version", keyID), + Data: map[string]interface{}{ + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("import_version prior to wrapping key generation incorrectly succeeded") + } + }, + ) + + // Retrieve public wrapping key + wrappingKey, err := b.getWrappingKey(context.Background(), s) + if err != nil || wrappingKey == nil { + t.Fatalf("failed to retrieve public wrapping key: %s", err) + } + privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey + pubWrappingKey := &privWrappingKey.PublicKey + + t.Run( + "import into a non-existent key fails", + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + targetKey := getKey(t, "aes256-gcm96") + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import_version", keyID), + Data: map[string]interface{}{ + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("import_version into a non-existent key incorrectly succeeded") + } + }, + ) + + t.Run( + "import into an internally-generated key fails", + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + + // Roll a key within Transit + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s", keyID), + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("failed to generate a key within transit: %s", err) + } + + // Attempt to import into newly generated key + targetKey := getKey(t, "aes256-gcm96") + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") + req = &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import_version", keyID), + Data: map[string]interface{}{ + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("import_version into an internally-generated key incorrectly succeeded") + } + }, + ) + + t.Run( + "imported key version type must match existing key type", + func(t *testing.T) { + keyID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("failed to generate key ID: %s", err) + } + + // Import an RSA key + targetKey := getKey(t, "rsa-2048") + importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "rsa-2048", "SHA256") + req := &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import", keyID), + Data: map[string]interface{}{ + "ciphertext": importBlob, + "type": "rsa-2048", + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("failed to generate a key within transit: %s", err) + } + + // Attempt to import an AES key version into existing RSA key + targetKey = getKey(t, "aes256-gcm96") + importBlob = wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") + req = &logical.Request{ + Storage: s, + Operation: logical.UpdateOperation, + Path: fmt.Sprintf("keys/%s/import_version", keyID), + Data: map[string]interface{}{ + "ciphertext": importBlob, + }, + } + _, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("import_version into a key of a different type incorrectly succeeded") + } + }, + ) +} + +func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey interface{}, targetKeyType string, hashFnName string) string { + t.Helper() + + // Generate an ephemeral AES-256 key + ephKey, err := uuid.GenerateRandomBytes(32) + if err != nil { + t.Fatalf("failed to wrap target key for import: %s", err) + } + + // Parse the hash function name into an actual function + hashFn, err := parseHashFn(hashFnName) + if err != nil { + t.Fatalf("failed to wrap target key for import: %s", err) + } + + // Wrap ephemeral AES key with public wrapping key + ephKeyWrapped, err := rsa.EncryptOAEP(hashFn, rand.Reader, wrappingKey, ephKey, []byte{}) + if err != nil { + t.Fatalf("failed to wrap target key for import: %s", err) + } + + // Create KWP instance for wrapping target key + kwp, err := subtle.NewKWP(ephKey) + if err != nil { + t.Fatalf("failed to wrap target key for import: %s", err) + } + + // Format target key for wrapping + var preppedTargetKey []byte + var ok bool + switch targetKeyType { + case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305": + preppedTargetKey, ok = targetKey.([]byte) + if !ok { + t.Fatal("failed to wrap target key for import: symmetric key not provided in byte format") + } + default: + preppedTargetKey, err = x509.MarshalPKCS8PrivateKey(targetKey) + if err != nil { + t.Fatalf("failed to wrap target key for import: %s", err) + } + } + + // Wrap target key with KWP + targetKeyWrapped, err := kwp.Wrap(preppedTargetKey) + if err != nil { + t.Fatalf("failed to wrap target key for import: %s", err) + } + + // Combined wrapped keys into a single blob and base64 encode + wrappedKeys := append(ephKeyWrapped, targetKeyWrapped...) + return base64.RawURLEncoding.EncodeToString(wrappedKeys) +} + +func generateKey(keyType string) (interface{}, error) { + switch keyType { + case "aes128-gcm96": + return uuid.GenerateRandomBytes(16) + case "aes256-gcm96": + return uuid.GenerateRandomBytes(32) + case "chacha20-poly1305": + return uuid.GenerateRandomBytes(32) + case "ed25519": + _, priv, err := ed25519.GenerateKey(rand.Reader) + return priv, err + case "ecdsa-p256": + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case "ecdsa-p384": + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case "ecdsa-p521": + return ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + case "rsa-2048": + return rsa.GenerateKey(rand.Reader, 2048) + case "rsa-3072": + return rsa.GenerateKey(rand.Reader, 3072) + case "rsa-4096": + return rsa.GenerateKey(rand.Reader, 4096) + default: + return nil, fmt.Errorf("failed to generate unsupported key type: %s", keyType) + } +} diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index bcaf326c2..e64adb24a 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -239,9 +239,14 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f "supports_signing": p.Type.SigningSupported(), "supports_derivation": p.Type.DerivationSupported(), "auto_rotate_period": int64(p.AutoRotatePeriod.Seconds()), + "imported_key": p.Imported, }, } + if p.Imported { + resp.Data["imported_key_allow_rotation"] = p.AllowImportedKeyRotation + } + if p.BackupInfo != nil { resp.Data["backup_info"] = map[string]interface{}{ "time": p.BackupInfo.Time, diff --git a/builtin/logical/transit/path_wrapping_key.go b/builtin/logical/transit/path_wrapping_key.go index e878fbfd2..1a08318db 100644 --- a/builtin/logical/transit/path_wrapping_key.go +++ b/builtin/logical/transit/path_wrapping_key.go @@ -26,28 +26,10 @@ func (b *backend) pathWrappingKey() *framework.Path { } func (b *backend) pathWrappingKeyRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { - polReq := keysutil.PolicyRequest{ - Upsert: true, - Storage: req.Storage, - Name: fmt.Sprintf("import/%s", WrappingKeyName), - KeyType: keysutil.KeyType_RSA4096, - Derived: false, - Convergent: false, - Exportable: false, - AllowPlaintextBackup: false, - AutoRotatePeriod: 0, - } - p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) + p, err := b.getWrappingKey(ctx, req.Storage) if err != nil { return nil, err } - if p == nil { - return nil, fmt.Errorf("error retrieving wrapping key: returned policy was nil") - } - if b.System().CachingDisabled() { - p.Unlock() - } - wrappingKey := p.Keys[strconv.Itoa(p.LatestVersion)] derBytes, err := x509.MarshalPKIXPublicKey(wrappingKey.RSAKey.Public()) @@ -74,6 +56,32 @@ func (b *backend) pathWrappingKeyRead(ctx context.Context, req *logical.Request, return resp, nil } +func (b *backend) getWrappingKey(ctx context.Context, storage logical.Storage) (*keysutil.Policy, error) { + polReq := keysutil.PolicyRequest{ + Upsert: true, + Storage: storage, + Name: fmt.Sprintf("import/%s", WrappingKeyName), + KeyType: keysutil.KeyType_RSA4096, + Derived: false, + Convergent: false, + Exportable: false, + AllowPlaintextBackup: false, + AutoRotatePeriod: 0, + } + p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) + if err != nil { + return nil, err + } + if p == nil { + return nil, fmt.Errorf("error retrieving wrapping key: returned policy was nil") + } + if b.System().CachingDisabled() { + p.Unlock() + } + + return p, nil +} + const ( pathWrappingKeyHelpSyn = "Returns the public key to use for wrapping imported keys" pathWrappingKeyHelpDesc = "This path is used to retrieve the RSA-4096 wrapping key " + diff --git a/changelog/15414.txt b/changelog/15414.txt new file mode 100644 index 000000000..de6372bfd --- /dev/null +++ b/changelog/15414.txt @@ -0,0 +1,3 @@ +```release-note:feature +**Transit BYOK**: Allow import of externally-generated keys into the Transit secrets engine. +``` diff --git a/go.mod b/go.mod index 82408f664..f1df2ac77 100644 --- a/go.mod +++ b/go.mod @@ -274,6 +274,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/tink/go v1.4.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/googleapis/gnostic v0.5.5 // indirect diff --git a/go.sum b/go.sum index 9e423dd0c..234326cd6 100644 --- a/go.sum +++ b/go.sum @@ -214,6 +214,7 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.25.39/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.37.19 h1:/xKHoSsYfH9qe16pJAHIjqTVpMM2DRSsEt8Ok1bzYiw= @@ -751,6 +752,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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.4.0 h1:7Ihv6n6/0zPrm2GRAeeF408P9Y00HXC2J6tvUzgb2sg= +github.com/google/tink/go v1.4.0/go.mod h1:OdW+ACSIXwGiPOWJiRTdoKzStsnqo8ZOsTzchWLy2DY= 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= @@ -1654,6 +1657,7 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/sdk/helper/keysutil/lock_manager.go b/sdk/helper/keysutil/lock_manager.go index a424ca9e0..33b298041 100644 --- a/sdk/helper/keysutil/lock_manager.go +++ b/sdk/helper/keysutil/lock_manager.go @@ -53,6 +53,9 @@ type PolicyRequest struct { // How frequently the key should automatically rotate AutoRotatePeriod time.Duration + + // AllowImportedKeyRotation indicates whether an imported key may be rotated by Vault + AllowImportedKeyRotation bool } type LockManager struct { @@ -439,6 +442,63 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io return } +func (lm *LockManager) ImportPolicy(ctx context.Context, req PolicyRequest, key []byte, rand io.Reader) error { + var p *Policy + var err error + var ok bool + var pRaw interface{} + + // Check if it's in our cache + if lm.useCache { + pRaw, ok = lm.cache.Load(req.Name) + } + if ok { + p = pRaw.(*Policy) + if atomic.LoadUint32(&p.deleted) == 1 { + return nil + } + } + + // We're not using the cache, or it wasn't found; get an exclusive lock. + // This ensures that any other process writing the actual storage will be + // finished before we load from storage. + lock := locksutil.LockForKey(lm.keyLocks, req.Name) + lock.Lock() + + // Load it from storage + p, err = lm.getPolicyFromStorage(ctx, req.Storage, req.Name) + if err != nil { + return err + } + + if p == nil { + p = &Policy{ + l: new(sync.RWMutex), + Name: req.Name, + Type: req.KeyType, + Derived: req.Derived, + Exportable: req.Exportable, + AllowPlaintextBackup: req.AllowPlaintextBackup, + AutoRotatePeriod: req.AutoRotatePeriod, + AllowImportedKeyRotation: req.AllowImportedKeyRotation, + Imported: true, + } + } + + err = p.Import(ctx, req.Storage, key, rand) + if err != nil { + return fmt.Errorf("error importing key: %s", err) + } + + if lm.useCache { + lm.cache.Store(req.Name, p) + } + + lock.Unlock() + + return nil +} + func (lm *LockManager) DeletePolicy(ctx context.Context, storage logical.Storage, name string) error { var p *Policy var err error diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 59afa99fd..59029756d 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -381,6 +381,13 @@ type Policy struct { // versionPrefixCache stores caches of version prefix strings and the split // version template. versionPrefixCache sync.Map + + // Imported indicates whether the key was generated by Vault or imported + // from an external source + Imported bool + + // AllowImportedKeyRotation indicates whether an imported key may be rotated by Vault + AllowImportedKeyRotation bool } func (p *Policy) Lock(exclusive bool) { @@ -1355,6 +1362,117 @@ func (p *Policy) VerifySignature(context, input []byte, hashAlgorithm HashType, } } +func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte, randReader io.Reader) error { + now := time.Now() + entry := KeyEntry{ + CreationTime: now, + DeprecatedCreationTime: now.Unix(), + } + + hmacKey, err := uuid.GenerateRandomBytesWithReader(32, randReader) + if err != nil { + return err + } + entry.HMACKey = hmacKey + + if (p.Type == KeyType_AES128_GCM96 && len(key) != 16) || + ((p.Type == KeyType_AES256_GCM96 || p.Type == KeyType_ChaCha20_Poly1305) && len(key) != 32) { + return fmt.Errorf("invalid key size %d bytes for key type %s", len(key), p.Type) + } + + if p.Type == KeyType_AES128_GCM96 || p.Type == KeyType_AES256_GCM96 || p.Type == KeyType_ChaCha20_Poly1305 { + entry.Key = key + } else { + parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key) + if err != nil { + return fmt.Errorf("error parsing asymmetric key: %s", err) + } + + switch parsedPrivateKey.(type) { + case *ecdsa.PrivateKey: + if p.Type != KeyType_ECDSA_P256 && p.Type != KeyType_ECDSA_P384 && p.Type != KeyType_ECDSA_P521 { + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) + } + + ecdsaKey := parsedPrivateKey.(*ecdsa.PrivateKey) + curve := elliptic.P256() + if p.Type == KeyType_ECDSA_P384 { + curve = elliptic.P384() + } else if p.Type == KeyType_ECDSA_P521 { + curve = elliptic.P521() + } + + if ecdsaKey.Curve != curve { + return fmt.Errorf("invalid curve: expected %s, got %s", curve.Params().Name, ecdsaKey.Curve.Params().Name) + } + + entry.EC_D = ecdsaKey.D + entry.EC_X = ecdsaKey.X + entry.EC_Y = ecdsaKey.Y + derBytes, err := x509.MarshalPKIXPublicKey(ecdsaKey.Public()) + if err != nil { + return errwrap.Wrapf("error marshaling public key: {{err}}", err) + } + pemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: derBytes, + } + pemBytes := pem.EncodeToMemory(pemBlock) + if pemBytes == nil || len(pemBytes) == 0 { + return fmt.Errorf("error PEM-encoding public key") + } + entry.FormattedPublicKey = string(pemBytes) + case ed25519.PrivateKey: + if p.Type != KeyType_ED25519 { + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) + } + privateKey := parsedPrivateKey.(ed25519.PrivateKey) + + entry.Key = privateKey + publicKey := privateKey.Public().(ed25519.PublicKey) + entry.FormattedPublicKey = base64.StdEncoding.EncodeToString(publicKey) + case *rsa.PrivateKey: + if p.Type != KeyType_RSA2048 && p.Type != KeyType_RSA3072 && p.Type != KeyType_RSA4096 { + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) + } + + keyBytes := 256 + if p.Type == KeyType_RSA3072 { + keyBytes = 384 + } else if p.Type == KeyType_RSA4096 { + keyBytes = 512 + } + rsaKey := parsedPrivateKey.(*rsa.PrivateKey) + if rsaKey.Size() != keyBytes { + return fmt.Errorf("invalid key size: expected %d bytes, got %d bytes", keyBytes, rsaKey.Size()) + } + + entry.RSAKey = rsaKey + default: + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) + } + } + + p.LatestVersion += 1 + + if p.Keys == nil { + // This is an initial key rotation when generating a new policy. We + // don't need to call migrate here because if we've called getPolicy to + // get the policy in the first place it will have been run. + p.Keys = keyEntryMap{} + } + p.Keys[strconv.Itoa(p.LatestVersion)] = entry + + // This ensures that with new key creations min decryption version is set + // to 1 rather than the int default of 0, since keys start at 1 (either + // fresh or after migration to the key map) + if p.MinDecryptionVersion == 0 { + p.MinDecryptionVersion = 1 + } + + return p.Persist(ctx, storage) +} + // Rotate rotates the policy and persists it to storage. // If the rotation partially fails, the policy state will be restored. func (p *Policy) Rotate(ctx context.Context, storage logical.Storage, randReader io.Reader) (retErr error) { @@ -1362,6 +1480,10 @@ func (p *Policy) Rotate(ctx context.Context, storage logical.Storage, randReader priorMinDecryptionVersion := p.MinDecryptionVersion var priorKeys keyEntryMap + if p.Imported && !p.AllowImportedKeyRotation { + return fmt.Errorf("imported key %s does not allow rotation within Vault", p.Name) + } + if p.Keys != nil { priorKeys = keyEntryMap{} for k, v := range p.Keys { @@ -1381,6 +1503,7 @@ func (p *Policy) Rotate(ctx context.Context, storage logical.Storage, randReader return err } + p.Imported = false return p.Persist(ctx, storage) } diff --git a/sdk/helper/keysutil/policy_test.go b/sdk/helper/keysutil/policy_test.go index e1ad6dde7..a2d9206a8 100644 --- a/sdk/helper/keysutil/policy_test.go +++ b/sdk/helper/keysutil/policy_test.go @@ -3,13 +3,19 @@ package keysutil import ( "bytes" "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" + "crypto/x509" "reflect" "strconv" "sync" "testing" "time" + "golang.org/x/crypto/ed25519" + "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" "github.com/mitchellh/copystructure" @@ -615,6 +621,113 @@ func Test_BadArchive(t *testing.T) { } } +func Test_Import(t *testing.T) { + ctx := context.Background() + storage := &logical.InmemStorage{} + testKeys, err := generateTestKeys() + if err != nil { + t.Fatalf("error generating test keys: %s", err) + } + + tests := map[string]struct { + policy Policy + key []byte + shouldError bool + }{ + "import AES key": { + policy: Policy{ + Name: "test-aes-key", + Type: KeyType_AES256_GCM96, + }, + key: testKeys[KeyType_AES256_GCM96], + shouldError: false, + }, + "import RSA key": { + policy: Policy{ + Name: "test-rsa-key", + Type: KeyType_RSA2048, + }, + key: testKeys[KeyType_RSA2048], + shouldError: false, + }, + "import ECDSA key": { + policy: Policy{ + Name: "test-ecdsa-key", + Type: KeyType_ECDSA_P256, + }, + key: testKeys[KeyType_ECDSA_P256], + shouldError: false, + }, + "import ED25519 key": { + policy: Policy{ + Name: "test-ed25519-key", + Type: KeyType_ED25519, + }, + key: testKeys[KeyType_ED25519], + shouldError: false, + }, + "import incorrect key type": { + policy: Policy{ + Name: "test-ed25519-key", + Type: KeyType_ED25519, + }, + key: testKeys[KeyType_AES256_GCM96], + shouldError: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if err := test.policy.Import(ctx, storage, test.key, rand.Reader); (err != nil) != test.shouldError { + t.Fatalf("error importing key: %s", err) + } + }) + } +} + +func generateTestKeys() (map[KeyType][]byte, error) { + keyMap := make(map[KeyType][]byte) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + rsaKeyBytes, err := x509.MarshalPKCS8PrivateKey(rsaKey) + if err != nil { + return nil, err + } + keyMap[KeyType_RSA2048] = rsaKeyBytes + + ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + ecdsaKeyBytes, err := x509.MarshalPKCS8PrivateKey(ecdsaKey) + if err != nil { + return nil, err + } + keyMap[KeyType_ECDSA_P256] = ecdsaKeyBytes + + _, ed25519Key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + ed25519KeyBytes, err := x509.MarshalPKCS8PrivateKey(ed25519Key) + if err != nil { + return nil, err + } + keyMap[KeyType_ED25519] = ed25519KeyBytes + + aesKey := make([]byte, 32) + _, err = rand.Read(aesKey) + if err != nil { + return nil, err + } + keyMap[KeyType_AES256_GCM96] = aesKey + + return keyMap, nil +} + func BenchmarkSymmetric(b *testing.B) { ctx := context.Background() lm, _ := NewLockManager(true, 0)