diff --git a/builtin/logical/transit/api_utils.go b/builtin/logical/transit/api_utils.go new file mode 100644 index 000000000000..75ba53a2b220 --- /dev/null +++ b/builtin/logical/transit/api_utils.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package transit + +import ( + "fmt" + + "github.com/hashicorp/vault/sdk/helper/keysutil" +) + +// parsePaddingSchemeArg validate that the provided padding scheme argument received on the api can be used. +func parsePaddingSchemeArg(keyType keysutil.KeyType, rawPs any) (keysutil.PaddingScheme, error) { + ps, ok := rawPs.(string) + if !ok { + return "", fmt.Errorf("argument was not a string: %T", rawPs) + } + + paddingScheme, err := keysutil.ParsePaddingScheme(ps) + if err != nil { + return "", err + } + + if !keyType.PaddingSchemesSupported() { + return "", fmt.Errorf("unsupported key type %s for padding scheme", keyType.String()) + } + + return paddingScheme, nil +} diff --git a/builtin/logical/transit/api_utils_test.go b/builtin/logical/transit/api_utils_test.go new file mode 100644 index 000000000000..96223a6c69cd --- /dev/null +++ b/builtin/logical/transit/api_utils_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package transit + +import ( + "testing" + + "github.com/hashicorp/vault/sdk/helper/keysutil" +) + +// Test_parsePaddingSchemeArg validate the various use cases we have around parsing +// the various padding_scheme arg possible values. +func Test_parsePaddingSchemeArg(t *testing.T) { + type args struct { + keyType keysutil.KeyType + rawPs any + } + tests := []struct { + name string + args args + want keysutil.PaddingScheme + wantErr bool + }{ + // Error cases + {name: "nil-ps", args: args{keyType: keysutil.KeyType_RSA2048, rawPs: nil}, wantErr: true}, + {name: "nonstring-ps", args: args{keyType: keysutil.KeyType_RSA2048, rawPs: 5}, wantErr: true}, + {name: "invalid-ps", args: args{keyType: keysutil.KeyType_RSA2048, rawPs: "unknown"}, wantErr: true}, + {name: "bad-keytype-oaep", args: args{keyType: keysutil.KeyType_AES128_CMAC, rawPs: "oaep"}, wantErr: true}, + {name: "bad-keytype-pkcs1", args: args{keyType: keysutil.KeyType_ECDSA_P256, rawPs: "pkcs1v15"}, wantErr: true}, + {name: "oaep-capped", args: args{keyType: keysutil.KeyType_RSA4096, rawPs: "OAEP"}, wantErr: true}, + {name: "pkcs1-whitespace", args: args{keyType: keysutil.KeyType_RSA3072, rawPs: " pkcs1v15 "}, wantErr: true}, + + // Valid cases + {name: "oaep-2048", args: args{keyType: keysutil.KeyType_RSA2048, rawPs: "oaep"}, want: keysutil.PaddingScheme_OAEP}, + {name: "oaep-3072", args: args{keyType: keysutil.KeyType_RSA3072, rawPs: "oaep"}, want: keysutil.PaddingScheme_OAEP}, + {name: "oaep-4096", args: args{keyType: keysutil.KeyType_RSA4096, rawPs: "oaep"}, want: keysutil.PaddingScheme_OAEP}, + {name: "pkcs1", args: args{keyType: keysutil.KeyType_RSA3072, rawPs: "pkcs1v15"}, want: keysutil.PaddingScheme_PKCS1v15}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parsePaddingSchemeArg(tt.args.keyType, tt.args.rawPs) + if (err != nil) != tt.wantErr { + t.Errorf("parsePaddingSchemeArg() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parsePaddingSchemeArg() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index 528ccf68218c..ff3afb1189c6 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -148,83 +148,96 @@ func testTransit_RSA(t *testing.T, keyType string) { plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox" - encryptReq := &logical.Request{ - Path: "encrypt/rsa", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "plaintext": plaintext, - }, - } + for _, padding := range []keysutil.PaddingScheme{keysutil.PaddingScheme_OAEP, keysutil.PaddingScheme_PKCS1v15, ""} { + encryptReq := &logical.Request{ + Path: "encrypt/rsa", + Operation: logical.UpdateOperation, + Storage: storage, + Data: map[string]interface{}{ + "plaintext": plaintext, + }, + } - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } + if padding != "" { + encryptReq.Data["padding_scheme"] = padding + } - ciphertext1 := resp.Data["ciphertext"].(string) + resp, err = b.HandleRequest(context.Background(), encryptReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: err: %v\nresp: %#v", err, resp) + } - decryptReq := &logical.Request{ - Path: "decrypt/rsa", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "ciphertext": ciphertext1, - }, - } + ciphertext1 := resp.Data["ciphertext"].(string) - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } + decryptReq := &logical.Request{ + Path: "decrypt/rsa", + Operation: logical.UpdateOperation, + Storage: storage, + Data: map[string]interface{}{ + "ciphertext": ciphertext1, + }, + } + if padding != "" { + decryptReq.Data["padding_scheme"] = padding + } - decryptedPlaintext := resp.Data["plaintext"] + resp, err = b.HandleRequest(context.Background(), decryptReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: err: %v\nresp: %#v", err, resp) + } - if plaintext != decryptedPlaintext { - t.Fatalf("bad: plaintext; expected: %q\nactual: %q", plaintext, decryptedPlaintext) - } + decryptedPlaintext := resp.Data["plaintext"] - // Rotate the key - rotateReq := &logical.Request{ - Path: "keys/rsa/rotate", - Operation: logical.UpdateOperation, - Storage: storage, - } - resp, err = b.HandleRequest(context.Background(), rotateReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } + if plaintext != decryptedPlaintext { + t.Fatalf("bad: plaintext; expected: %q\nactual: %q", plaintext, decryptedPlaintext) + } - // Encrypt again - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - ciphertext2 := resp.Data["ciphertext"].(string) + // Rotate the key + rotateReq := &logical.Request{ + Path: "keys/rsa/rotate", + Operation: logical.UpdateOperation, + Storage: storage, + } + resp, err = b.HandleRequest(context.Background(), rotateReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: err: %v\nresp: %#v", err, resp) + } - if ciphertext1 == ciphertext2 { - t.Fatalf("expected different ciphertexts") - } + // Encrypt again + resp, err = b.HandleRequest(context.Background(), encryptReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: err: %v\nresp: %#v", err, resp) + } + ciphertext2 := resp.Data["ciphertext"].(string) - // See if the older ciphertext can still be decrypted - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if resp.Data["plaintext"].(string) != plaintext { - t.Fatal("failed to decrypt old ciphertext after rotating the key") - } + if ciphertext1 == ciphertext2 { + t.Fatalf("expected different ciphertexts") + } - // Decrypt the new ciphertext - decryptReq.Data = map[string]interface{}{ - "ciphertext": ciphertext2, - } - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if resp.Data["plaintext"].(string) != plaintext { - t.Fatal("failed to decrypt ciphertext after rotating the key") + // See if the older ciphertext can still be decrypted + resp, err = b.HandleRequest(context.Background(), decryptReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: err: %v\nresp: %#v", err, resp) + } + if resp.Data["plaintext"].(string) != plaintext { + t.Fatal("failed to decrypt old ciphertext after rotating the key") + } + + // Decrypt the new ciphertext + decryptReq.Data = map[string]interface{}{ + "ciphertext": ciphertext2, + } + if padding != "" { + decryptReq.Data["padding_scheme"] = padding + } + + resp, err = b.HandleRequest(context.Background(), decryptReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: err: %v\nresp: %#v", err, resp) + } + if resp.Data["plaintext"].(string) != plaintext { + t.Fatal("failed to decrypt ciphertext after rotating the key") + } } signReq := &logical.Request{ diff --git a/builtin/logical/transit/path_datakey.go b/builtin/logical/transit/path_datakey.go index 53aff54690bb..47969673d72b 100644 --- a/builtin/logical/transit/path_datakey.go +++ b/builtin/logical/transit/path_datakey.go @@ -39,6 +39,12 @@ func (b *backend) pathDatakey() *framework.Path { ciphertext; "wrapped" will return the ciphertext only.`, }, + "padding_scheme": { + Type: framework.TypeString, + Description: `The padding scheme to use for decrypt. Currently only applies to RSA key types. +Options are 'oaep' or 'pkcs1v15'. Defaults to 'oaep'`, + }, + "context": { Type: framework.TypeString, Description: "Context for key derivation. Required for derived keys.", @@ -142,23 +148,31 @@ func (b *backend) pathDatakeyWrite(ctx context.Context, req *logical.Request, d return nil, err } - var managedKeyFactory ManagedKeyFactory + factories := make([]any, 0) + if ps, ok := d.GetOk("padding_scheme"); ok { + paddingScheme, err := parsePaddingSchemeArg(p.Type, ps) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("padding_scheme argument invalid: %s", err.Error())), logical.ErrInvalidRequest + } + factories = append(factories, paddingScheme) + + } if p.Type == keysutil.KeyType_MANAGED_KEY { managedKeySystemView, ok := b.System().(logical.ManagedKeySystemView) if !ok { return nil, errors.New("unsupported system view") } - managedKeyFactory = ManagedKeyFactory{ + factories = append(factories, ManagedKeyFactory{ managedKeyParams: keysutil.ManagedKeyParameters{ ManagedKeySystemView: managedKeySystemView, BackendUUID: b.backendUUID, Context: ctx, }, - } + }) } - ciphertext, err := p.EncryptWithFactory(ver, context, nonce, base64.StdEncoding.EncodeToString(newKey), nil, managedKeyFactory) + ciphertext, err := p.EncryptWithFactory(ver, context, nonce, base64.StdEncoding.EncodeToString(newKey), factories...) if err != nil { switch err.(type) { case errutil.UserError: diff --git a/builtin/logical/transit/path_datakey_test.go b/builtin/logical/transit/path_datakey_test.go new file mode 100644 index 000000000000..2207419f84e4 --- /dev/null +++ b/builtin/logical/transit/path_datakey_test.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package transit + +import ( + "context" + "testing" + + "github.com/hashicorp/vault/sdk/logical" + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/require" +) + +// TestDataKeyWithPaddingScheme validates that we properly leverage padding scheme +// args for the returned keys +func TestDataKeyWithPaddingScheme(t *testing.T) { + b, s := createBackendWithStorage(t) + keyName := "test" + createKeyReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "keys/" + keyName, + Storage: s, + Data: map[string]interface{}{ + "type": "rsa-2048", + }, + } + + resp, err := b.HandleRequest(context.Background(), createKeyReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("failed key creation: err: %v resp: %#v", err, resp) + } + + tests := []struct { + Name string + PaddingScheme string + DecryptPaddingScheme string + ShouldFailToDecrypt bool + }{ + {"no-padding-scheme", "", "", false}, + {"oaep", "oaep", "oaep", false}, + {"pkcs1v15", "pkcs1v15", "pkcs1v15", false}, + {"mixed-should-fail", "pkcs1v15", "oaep", true}, + {"mixed-based-on-default-should-fail", "", "pkcs1v15", true}, + } + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + dataKeyReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "datakey/wrapped/" + keyName, + Storage: s, + Data: map[string]interface{}{}, + } + if len(tc.PaddingScheme) > 0 { + dataKeyReq.Data["padding_scheme"] = tc.PaddingScheme + } + + resp, err = b.HandleRequest(context.Background(), dataKeyReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("failed data key api: err: %v resp: %#v", err, resp) + } + require.NotNil(t, resp, "Got nil nil response") + var d struct { + Ciphertext string `mapstructure:"ciphertext"` + } + err = mapstructure.Decode(resp.Data, &d) + require.NoError(t, err, "failed decoding datakey api response") + require.NotEmpty(t, d.Ciphertext, "ciphertext should not be empty") + + // Attempt to decrypt with data key with the same padding scheme + decryptReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "decrypt/" + keyName, + Storage: s, + Data: map[string]interface{}{ + "ciphertext": d.Ciphertext, + }, + } + if len(tc.DecryptPaddingScheme) > 0 { + decryptReq.Data["padding_scheme"] = tc.DecryptPaddingScheme + } + + resp, err = b.HandleRequest(context.Background(), decryptReq) + if tc.ShouldFailToDecrypt { + require.Error(t, err, "Should have failed decryption as padding schemes are mixed") + } else { + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("failed to decrypt data key: err: %v resp: %#v", err, resp) + } + } + }) + } +} + +// TestDataKeyWithPaddingSchemeInvalidKeyType validates we fail when we specify a +// padding_scheme value on an invalid key type (non-RSA) +func TestDataKeyWithPaddingSchemeInvalidKeyType(t *testing.T) { + b, s := createBackendWithStorage(t) + keyName := "test" + createKeyReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "keys/" + keyName, + Storage: s, + Data: map[string]interface{}{}, + } + + resp, err := b.HandleRequest(context.Background(), createKeyReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("failed key creation: err: %v resp: %#v", err, resp) + } + + dataKeyReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "datakey/wrapped/" + keyName, + Storage: s, + Data: map[string]interface{}{ + "padding_scheme": "oaep", + }, + } + + resp, err = b.HandleRequest(context.Background(), dataKeyReq) + require.ErrorContains(t, err, "invalid request") + require.NotNil(t, resp, "response should not be nil") + require.Contains(t, resp.Error().Error(), "padding_scheme argument invalid: unsupported key") +} diff --git a/builtin/logical/transit/path_decrypt.go b/builtin/logical/transit/path_decrypt.go index 1daf74daf5d1..dc1bcbf608ce 100644 --- a/builtin/logical/transit/path_decrypt.go +++ b/builtin/logical/transit/path_decrypt.go @@ -50,6 +50,12 @@ func (b *backend) pathDecrypt() *framework.Path { The ciphertext to decrypt, provided as returned by encrypt.`, }, + "padding_scheme": { + Type: framework.TypeString, + Description: `The padding scheme to use for decrypt. Currently only applies to RSA key types. +Options are 'oaep' or 'pkcs1v15'. Defaults to 'oaep'`, + }, + "context": { Type: framework.TypeString, Description: ` @@ -130,6 +136,9 @@ func (b *backend) pathDecryptWrite(ctx context.Context, req *logical.Request, d Nonce: d.Get("nonce").(string), AssociatedData: d.Get("associated_data").(string), } + if ps, ok := d.GetOk("padding_scheme"); ok { + batchInputItems[0].PaddingScheme = ps.(string) + } } batchResponseItems := make([]DecryptBatchResponseItem, len(batchInputItems)) @@ -192,33 +201,40 @@ func (b *backend) pathDecryptWrite(ctx context.Context, req *logical.Request, d continue } - var factory interface{} + var factories []any + if item.PaddingScheme != "" { + paddingScheme, err := parsePaddingSchemeArg(p.Type, item.PaddingScheme) + if err != nil { + batchResponseItems[i].Error = fmt.Sprintf("'[%d].padding_scheme' invalid: %s", i, err.Error()) + continue + } + factories = append(factories, paddingScheme) + } if item.AssociatedData != "" { if !p.Type.AssociatedDataSupported() { batchResponseItems[i].Error = fmt.Sprintf("'[%d].associated_data' provided for non-AEAD cipher suite %v", i, p.Type.String()) continue } - factory = AssocDataFactory{item.AssociatedData} + factories = append(factories, AssocDataFactory{item.AssociatedData}) } - var managedKeyFactory ManagedKeyFactory if p.Type == keysutil.KeyType_MANAGED_KEY { managedKeySystemView, ok := b.System().(logical.ManagedKeySystemView) if !ok { batchResponseItems[i].Error = errors.New("unsupported system view").Error() } - managedKeyFactory = ManagedKeyFactory{ + factories = append(factories, ManagedKeyFactory{ managedKeyParams: keysutil.ManagedKeyParameters{ ManagedKeySystemView: managedKeySystemView, BackendUUID: b.backendUUID, Context: ctx, }, - } + }) } - plaintext, err := p.DecryptWithFactory(item.DecodedContext, item.DecodedNonce, item.Ciphertext, factory, managedKeyFactory) + plaintext, err := p.DecryptWithFactory(item.DecodedContext, item.DecodedNonce, item.Ciphertext, factories...) if err != nil { switch err.(type) { case errutil.InternalError: diff --git a/builtin/logical/transit/path_encrypt.go b/builtin/logical/transit/path_encrypt.go index 38c618f9b363..c0502db4292a 100644 --- a/builtin/logical/transit/path_encrypt.go +++ b/builtin/logical/transit/path_encrypt.go @@ -34,6 +34,9 @@ type BatchRequestItem struct { // Ciphertext for decryption Ciphertext string `json:"ciphertext" structs:"ciphertext" mapstructure:"ciphertext"` + // PaddingScheme for encryption/decryption + PaddingScheme string `json:"padding_scheme" structs:"padding_scheme" mapstructure:"padding_scheme"` + // Nonce to be used when v1 convergent encryption is used Nonce string `json:"nonce" structs:"nonce" mapstructure:"nonce"` @@ -105,6 +108,12 @@ func (b *backend) pathEncrypt() *framework.Path { Description: "Base64 encoded plaintext value to be encrypted", }, + "padding_scheme": { + Type: framework.TypeString, + Description: `The padding scheme to use for decrypt. Currently only applies to RSA key types. +Options are 'oaep' or 'pkcs1v15'. Defaults to 'oaep'`, + }, + "context": { Type: framework.TypeString, Description: "Base64 encoded context for key derivation. Required if key derivation is enabled", @@ -259,6 +268,13 @@ func decodeBatchRequestItems(src interface{}, requirePlaintext bool, requireCiph } else if requirePlaintext { errs.Errors = append(errs.Errors, fmt.Sprintf("'[%d].plaintext' missing plaintext to encrypt", i)) } + if v, has := item["padding_scheme"]; has { + if casted, ok := v.(string); ok { + (*dst)[i].PaddingScheme = casted + } else { + errs.Errors = append(errs.Errors, fmt.Sprintf("'[%d].padding_scheme' expected type 'string', got unconvertible type '%T'", i, item["padding_scheme"])) + } + } if v, has := item["nonce"]; has { if !reflect.ValueOf(v).IsValid() { @@ -358,6 +374,13 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d KeyVersion: d.Get("key_version").(int), AssociatedData: d.Get("associated_data").(string), } + if psRaw, ok := d.GetOk("padding_scheme"); ok { + if ps, ok := psRaw.(string); ok { + batchInputItems[0].PaddingScheme = ps + } else { + return logical.ErrorResponse("padding_scheme was not a string"), logical.ErrInvalidRequest + } + } } batchResponseItems := make([]EncryptBatchResponseItem, len(batchInputItems)) @@ -435,6 +458,12 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d polReq.KeyType = keysutil.KeyType_AES256_GCM96 case "chacha20-poly1305": polReq.KeyType = keysutil.KeyType_ChaCha20_Poly1305 + case "rsa-2048": + polReq.KeyType = keysutil.KeyType_RSA2048 + case "rsa-3072": + polReq.KeyType = keysutil.KeyType_RSA3072 + case "rsa-4096": + polReq.KeyType = keysutil.KeyType_RSA4096 case "ecdsa-p256", "ecdsa-p384", "ecdsa-p521": return logical.ErrorResponse(fmt.Sprintf("key type %v not supported for this operation", keyType)), logical.ErrInvalidRequest case "managed_key": @@ -482,33 +511,40 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d warnAboutNonceUsage = true } - var factory interface{} + var factories []any + if item.PaddingScheme != "" { + paddingScheme, err := parsePaddingSchemeArg(p.Type, item.PaddingScheme) + if err != nil { + batchResponseItems[i].Error = fmt.Sprintf("'[%d].padding_scheme' invalid: %s", i, err.Error()) + continue + } + factories = append(factories, paddingScheme) + } if item.AssociatedData != "" { if !p.Type.AssociatedDataSupported() { batchResponseItems[i].Error = fmt.Sprintf("'[%d].associated_data' provided for non-AEAD cipher suite %v", i, p.Type.String()) continue } - factory = AssocDataFactory{item.AssociatedData} + factories = append(factories, AssocDataFactory{item.AssociatedData}) } - var managedKeyFactory ManagedKeyFactory if p.Type == keysutil.KeyType_MANAGED_KEY { managedKeySystemView, ok := b.System().(logical.ManagedKeySystemView) if !ok { batchResponseItems[i].Error = errors.New("unsupported system view").Error() } - managedKeyFactory = ManagedKeyFactory{ + factories = append(factories, ManagedKeyFactory{ managedKeyParams: keysutil.ManagedKeyParameters{ ManagedKeySystemView: managedKeySystemView, BackendUUID: b.backendUUID, Context: ctx, }, - } + }) } - ciphertext, err := p.EncryptWithFactory(item.KeyVersion, item.DecodedContext, item.DecodedNonce, item.Plaintext, factory, managedKeyFactory) + ciphertext, err := p.EncryptWithFactory(item.KeyVersion, item.DecodedContext, item.DecodedNonce, item.Plaintext, factories...) if err != nil { switch err.(type) { case errutil.InternalError: diff --git a/builtin/logical/transit/path_rewrap.go b/builtin/logical/transit/path_rewrap.go index 49b69c7255e1..ea5f8cccd4a7 100644 --- a/builtin/logical/transit/path_rewrap.go +++ b/builtin/logical/transit/path_rewrap.go @@ -19,6 +19,39 @@ import ( var ErrNonceNotAllowed = errors.New("provided nonce not allowed for this key") +type RewrapBatchRequestItem struct { + // Context for key derivation. This is required for derived keys. + Context string `json:"context" structs:"context" mapstructure:"context"` + + // DecodedContext is the base64 decoded version of Context + DecodedContext []byte + + // Ciphertext for decryption + Ciphertext string `json:"ciphertext" structs:"ciphertext" mapstructure:"ciphertext"` + + // Nonce to be used when v1 convergent encryption is used + Nonce string `json:"nonce" structs:"nonce" mapstructure:"nonce"` + + // The key version to be used for encryption + KeyVersion int `json:"key_version" structs:"key_version" mapstructure:"key_version"` + + // DecodedNonce is the base64 decoded version of Nonce + DecodedNonce []byte + + // Associated Data for AEAD ciphers + AssociatedData string `json:"associated_data" struct:"associated_data" mapstructure:"associated_data"` + + // Reference is an arbitrary caller supplied string value that will be placed on the + // batch response to ease correlation between inputs and outputs + Reference string `json:"reference" structs:"reference" mapstructure:"reference"` + + // EncryptPaddingScheme specifies the RSA padding scheme for encryption + EncryptPaddingScheme string `json:"encrypt_padding_scheme" structs:"encrypt_padding_scheme" mapstructure:"encrypt_padding_scheme"` + + // DecryptPaddingScheme specifies the RSA padding scheme for decryption + DecryptPaddingScheme string `json:"decrypt_padding_scheme" structs:"decrypt_padding_scheme" mapstructure:"decrypt_padding_scheme"` +} + func (b *backend) pathRewrap() *framework.Path { return &framework.Path{ Pattern: "rewrap/" + framework.GenericNameRegex("name"), @@ -39,6 +72,18 @@ func (b *backend) pathRewrap() *framework.Path { Description: "Ciphertext value to rewrap", }, + "encrypt_padding_scheme": { + Type: framework.TypeString, + Description: `The padding scheme to use for rewrap's encrypt step. Currently only applies to RSA key types. +Options are 'oaep' or 'pkcs1v15'. Defaults to 'oaep'`, + }, + + "decrypt_padding_scheme": { + Type: framework.TypeString, + Description: `The padding scheme to use for rewrap's decrypt step. Currently only applies to RSA key types. +Options are 'oaep' or 'pkcs1v15'. Defaults to 'oaep'`, + }, + "context": { Type: framework.TypeString, Description: "Base64 encoded context for key derivation. Required for derived keys.", @@ -76,7 +121,7 @@ Any batch output will preserve the order of the batch input.`, func (b *backend) pathRewrapWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { batchInputRaw := d.Raw["batch_input"] - var batchInputItems []BatchRequestItem + var batchInputItems []RewrapBatchRequestItem var err error if batchInputRaw != nil { err = mapstructure.Decode(batchInputRaw, &batchInputItems) @@ -93,13 +138,19 @@ func (b *backend) pathRewrapWrite(ctx context.Context, req *logical.Request, d * return logical.ErrorResponse("missing ciphertext to decrypt"), logical.ErrInvalidRequest } - batchInputItems = make([]BatchRequestItem, 1) - batchInputItems[0] = BatchRequestItem{ + batchInputItems = make([]RewrapBatchRequestItem, 1) + batchInputItems[0] = RewrapBatchRequestItem{ Ciphertext: ciphertext, Context: d.Get("context").(string), Nonce: d.Get("nonce").(string), KeyVersion: d.Get("key_version").(int), } + if ps, ok := d.GetOk("decrypt_padding_scheme"); ok { + batchInputItems[0].DecryptPaddingScheme = ps.(string) + } + if ps, ok := d.GetOk("encrypt_padding_scheme"); ok { + batchInputItems[0].EncryptPaddingScheme = ps.(string) + } } batchResponseItems := make([]EncryptBatchResponseItem, len(batchInputItems)) @@ -156,12 +207,21 @@ func (b *backend) pathRewrapWrite(ctx context.Context, req *logical.Request, d * continue } + var factories []any + if item.DecryptPaddingScheme != "" { + paddingScheme, err := parsePaddingSchemeArg(p.Type, item.DecryptPaddingScheme) + if err != nil { + batchResponseItems[i].Error = fmt.Sprintf("'[%d].decrypt_padding_scheme' invalid: %s", i, err.Error()) + continue + } + factories = append(factories, paddingScheme) + } if item.Nonce != "" && !nonceAllowed(p) { batchResponseItems[i].Error = ErrNonceNotAllowed.Error() continue } - plaintext, err := p.Decrypt(item.DecodedContext, item.DecodedNonce, item.Ciphertext) + plaintext, err := p.DecryptWithFactory(item.DecodedContext, item.DecodedNonce, item.Ciphertext, factories...) if err != nil { switch err.(type) { case errutil.UserError: @@ -172,11 +232,21 @@ func (b *backend) pathRewrapWrite(ctx context.Context, req *logical.Request, d * } } + factories = make([]any, 0) + if item.EncryptPaddingScheme != "" { + paddingScheme, err := parsePaddingSchemeArg(p.Type, item.EncryptPaddingScheme) + if err != nil { + batchResponseItems[i].Error = fmt.Sprintf("'[%d].encrypt_padding_scheme' invalid: %s", i, err.Error()) + continue + } + factories = append(factories, paddingScheme) + factories = append(factories, keysutil.PaddingScheme(item.EncryptPaddingScheme)) + } if !warnAboutNonceUsage && shouldWarnAboutNonceUsage(p, item.DecodedNonce) { warnAboutNonceUsage = true } - ciphertext, err := p.Encrypt(item.KeyVersion, item.DecodedContext, item.DecodedNonce, plaintext) + ciphertext, err := p.EncryptWithFactory(item.KeyVersion, item.DecodedContext, item.DecodedNonce, plaintext, factories...) if err != nil { switch err.(type) { case errutil.UserError: diff --git a/builtin/logical/transit/path_rewrap_test.go b/builtin/logical/transit/path_rewrap_test.go index 55f28874656e..4018d63ae8fa 100644 --- a/builtin/logical/transit/path_rewrap_test.go +++ b/builtin/logical/transit/path_rewrap_test.go @@ -326,3 +326,116 @@ func TestTransit_BatchRewrapCase3(t *testing.T) { } } + +// TestTransit_BatchRewrapCase4 batch rewrap leveraging RSA padding schemes +func TestTransit_BatchRewrapCase4(t *testing.T) { + var resp *logical.Response + var err error + + b, s := createBackendWithStorage(t) + + batchEncryptionInput := []interface{}{ + map[string]interface{}{"plaintext": "dmlzaGFsCg==", "reference": "ek", "padding_scheme": "pkcs1v15"}, + map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "do", "padding_scheme": "pkcs1v15"}, + } + batchEncryptionData := map[string]interface{}{ + "type": "rsa-2048", + "batch_input": batchEncryptionInput, + } + batchReq := &logical.Request{ + Operation: logical.CreateOperation, + Path: "encrypt/upserted_key", + Storage: s, + Data: batchEncryptionData, + } + resp, err = b.HandleRequest(context.Background(), batchReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + batchEncryptionResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) + + batchRewrapInput := make([]interface{}, len(batchEncryptionResponseItems)) + for i, item := range batchEncryptionResponseItems { + batchRewrapInput[i] = map[string]interface{}{ + "ciphertext": item.Ciphertext, + "reference": item.Reference, + "decrypt_padding_scheme": "pkcs1v15", + "encrypt_padding_scheme": "oaep", + } + } + + batchRewrapData := map[string]interface{}{ + "batch_input": batchRewrapInput, + } + + rotateReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "keys/upserted_key/rotate", + Storage: s, + } + resp, err = b.HandleRequest(context.Background(), rotateReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + rewrapReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "rewrap/upserted_key", + Storage: s, + Data: batchRewrapData, + } + + resp, err = b.HandleRequest(context.Background(), rewrapReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + batchRewrapResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) + + if len(batchRewrapResponseItems) != len(batchEncryptionResponseItems) { + t.Fatalf("bad: length of input and output or rewrap are not matching; expected: %d, actual: %d", len(batchEncryptionResponseItems), len(batchRewrapResponseItems)) + } + + decReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "decrypt/upserted_key", + Storage: s, + } + + for i, eItem := range batchEncryptionResponseItems { + rItem := batchRewrapResponseItems[i] + + inputRef := batchEncryptionInput[i].(map[string]interface{})["reference"] + if eItem.Reference != inputRef { + t.Fatalf("bad: reference mismatch. Expected %s, Actual: %s", inputRef, eItem.Reference) + } + + if eItem.Ciphertext == rItem.Ciphertext { + t.Fatalf("bad: rewrap input and output are the same") + } + + if !strings.HasPrefix(rItem.Ciphertext, "vault:v2") { + t.Fatalf("bad: invalid version of ciphertext in rewrap response; expected: 'vault:v2', actual: %s", rItem.Ciphertext) + } + + if rItem.KeyVersion != 2 { + t.Fatalf("unexpected key version; got: %d, expected: %d", rItem.KeyVersion, 2) + } + + decReq.Data = map[string]interface{}{ + "ciphertext": rItem.Ciphertext, + } + + resp, err = b.HandleRequest(context.Background(), decReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + plaintext1 := "dGhlIHF1aWNrIGJyb3duIGZveA==" + plaintext2 := "dmlzaGFsCg==" + if resp.Data["plaintext"] != plaintext1 && resp.Data["plaintext"] != plaintext2 { + t.Fatalf("bad: plaintext. Expected: %q or %q, Actual: %q", plaintext1, plaintext2, resp.Data["plaintext"]) + } + } +} diff --git a/changelog/25486.txt b/changelog/25486.txt new file mode 100644 index 000000000000..9293c69dba04 --- /dev/null +++ b/changelog/25486.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/transit: Add support for RSA padding scheme pkcs1v15 for encryption +``` diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 873f99149eda..234b4413cfe2 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -82,6 +82,30 @@ const ( DefaultVersionTemplate = "vault:v{{version}}:" ) +type PaddingScheme string + +const ( + PaddingScheme_OAEP = PaddingScheme("oaep") + PaddingScheme_PKCS1v15 = PaddingScheme("pkcs1v15") +) + +func (p PaddingScheme) String() string { + return string(p) +} + +// ParsePaddingScheme expects a lower case string that can be directly compared to +// a defined padding scheme or returns an error. +func ParsePaddingScheme(s string) (PaddingScheme, error) { + switch s { + case PaddingScheme_OAEP.String(): + return PaddingScheme_OAEP, nil + case PaddingScheme_PKCS1v15.String(): + return PaddingScheme_PKCS1v15, nil + default: + return "", fmt.Errorf("unknown padding scheme: %s", s) + } +} + type AEADFactory interface { GetAEAD(iv []byte) (cipher.AEAD, error) } @@ -199,6 +223,15 @@ func (kt KeyType) ImportPublicKeySupported() bool { return false } +func (kt KeyType) PaddingSchemesSupported() bool { + switch kt { + case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096: + return true + default: + return false + } +} + func (kt KeyType) String() string { switch kt { case KeyType_AES128_GCM96: @@ -939,7 +972,7 @@ func (p *Policy) Decrypt(context, nonce []byte, value string) (string, error) { return p.DecryptWithFactory(context, nonce, value, nil) } -func (p *Policy) DecryptWithFactory(context, nonce []byte, value string, factories ...interface{}) (string, error) { +func (p *Policy) DecryptWithFactory(context, nonce []byte, value string, factories ...any) (string, error) { if !p.Type.DecryptionSupported() { return "", errutil.UserError{Err: fmt.Sprintf("message decryption not supported for key type %v", p.Type)} } @@ -1034,15 +1067,24 @@ func (p *Policy) DecryptWithFactory(context, nonce []byte, value string, factori return "", err } case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096: + paddingScheme, err := getPaddingScheme(factories) + if err != nil { + return "", err + } keyEntry, err := p.safeGetKeyEntry(ver) if err != nil { return "", err } key := keyEntry.RSAKey - if key == nil { - return "", errutil.InternalError{Err: fmt.Sprintf("cannot decrypt ciphertext, key version does not have a private counterpart")} + + switch paddingScheme { + case PaddingScheme_PKCS1v15: + plain, err = rsa.DecryptPKCS1v15(rand.Reader, key, decoded) + case PaddingScheme_OAEP: + plain, err = rsa.DecryptOAEP(sha256.New(), rand.Reader, key, decoded, nil) + default: + return "", errutil.InternalError{Err: fmt.Sprintf("unsupported RSA padding scheme %s", paddingScheme)} } - plain, err = rsa.DecryptOAEP(sha256.New(), rand.Reader, key, decoded, nil) if err != nil { return "", errutil.InternalError{Err: fmt.Sprintf("failed to RSA decrypt the ciphertext: %v", err)} } @@ -2033,7 +2075,7 @@ func (p *Policy) SymmetricDecryptRaw(encKey, ciphertext []byte, opts SymmetricOp return plain, nil } -func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value string, factories ...interface{}) (string, error) { +func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value string, factories ...any) (string, error) { if !p.Type.EncryptionSupported() { return "", errutil.UserError{Err: fmt.Sprintf("message encryption not supported for key type %v", p.Type)} } @@ -2128,6 +2170,10 @@ func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value return "", err } case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096: + paddingScheme, err := getPaddingScheme(factories) + if err != nil { + return "", err + } keyEntry, err := p.safeGetKeyEntry(ver) if err != nil { return "", err @@ -2138,7 +2184,15 @@ func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value } else { publicKey = keyEntry.RSAPublicKey } - ciphertext, err = rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, plaintext, nil) + switch paddingScheme { + case PaddingScheme_PKCS1v15: + ciphertext, err = rsa.EncryptPKCS1v15(rand.Reader, publicKey, plaintext) + case PaddingScheme_OAEP: + ciphertext, err = rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, plaintext, nil) + default: + return "", errutil.InternalError{Err: fmt.Sprintf("unsupported RSA padding scheme %s", paddingScheme)} + } + if err != nil { return "", errutil.InternalError{Err: fmt.Sprintf("failed to RSA encrypt the plaintext: %v", err)} } @@ -2184,6 +2238,19 @@ func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value return encoded, nil } +func getPaddingScheme(factories []any) (PaddingScheme, error) { + for _, rawFactory := range factories { + if rawFactory == nil { + continue + } + + if p, ok := rawFactory.(PaddingScheme); ok && p != "" { + return p, nil + } + } + return PaddingScheme_OAEP, nil +} + func (p *Policy) KeyVersionCanBeUpdated(keyVersion int, isPrivateKey bool) error { keyEntry, err := p.safeGetKeyEntry(keyVersion) if err != nil { @@ -2379,7 +2446,7 @@ 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) { +func (p *Policy) WrapKey(ver int, targetKey any, targetKeyType KeyType, hash hash.Hash) (string, error) { if !p.Type.SigningSupported() { return "", fmt.Errorf("message signing not supported for key type %v", p.Type) } @@ -2403,7 +2470,7 @@ func (p *Policy) WrapKey(ver int, targetKey interface{}, targetKeyType KeyType, return keyEntry.WrapKey(targetKey, targetKeyType, hash) } -func (ke *KeyEntry) WrapKey(targetKey interface{}, targetKeyType KeyType, hash hash.Hash) (string, error) { +func (ke *KeyEntry) WrapKey(targetKey any, 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 { diff --git a/sdk/helper/keysutil/policy_test.go b/sdk/helper/keysutil/policy_test.go index fd753f22ba7e..cd921a52065b 100644 --- a/sdk/helper/keysutil/policy_test.go +++ b/sdk/helper/keysutil/policy_test.go @@ -11,6 +11,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/base64" "errors" "fmt" mathrand "math/rand" @@ -933,6 +934,25 @@ func autoVerify(depth int, t *testing.T, p *Policy, input []byte, sig *SigningRe } } +func autoVerifyDecrypt(depth int, t *testing.T, p *Policy, input []byte, ct string, factories ...any) { + tabs := strings.Repeat("\t", depth) + t.Log(tabs, "Automatically decrypting with options:", factories) + + tabs = strings.Repeat("\t", depth+1) + ptb64, err := p.DecryptWithFactory(nil, nil, ct, factories...) + if err != nil { + t.Fatal(tabs, "❌ Failed to automatically verify signature:", err) + } + + pt, err := base64.StdEncoding.DecodeString(ptb64) + if err != nil { + t.Fatal(tabs, "❌ Failed decoding plaintext:", err) + } + if !bytes.Equal(input, pt) { + t.Fatal(tabs, "❌ Failed to automatically decrypt") + } +} + func Test_RSA_PSS(t *testing.T) { t.Log("Testing RSA PSS") mathrand.Seed(time.Now().UnixNano()) @@ -1083,8 +1103,64 @@ func Test_RSA_PSS(t *testing.T) { } } -func Test_RSA_PKCS1(t *testing.T) { - t.Log("Testing RSA PKCS#1v1.5") +func Test_RSA_PKCS1Encryption(t *testing.T) { + t.Log("Testing RSA PKCS#1v1.5 padded encryption") + + ctx := context.Background() + storage := &logical.InmemStorage{} + // https://crypto.stackexchange.com/a/1222 + pt := []byte("Sphinx of black quartz, judge my vow") + input := base64.StdEncoding.EncodeToString(pt) + + tabs := make(map[int]string) + for i := 1; i <= 6; i++ { + tabs[i] = strings.Repeat("\t", i) + } + + test_RSA_PKCS1 := func(t *testing.T, p *Policy, rsaKey *rsa.PrivateKey, padding PaddingScheme) { + // 1. Make a signature with the given key size and hash algorithm. + t.Log(tabs[3], "Make an automatic signature") + ct, err := p.EncryptWithFactory(0, nil, nil, string(input), padding) + if err != nil { + t.Fatal(tabs[4], "❌ Failed to automatically encrypt:", err) + } + + // 1.1 Verify this signature using the *inferred* salt length. + autoVerifyDecrypt(4, t, p, pt, ct, padding) + } + + rsaKeyTypes := []KeyType{KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096} + testKeys, err := generateTestKeys() + if err != nil { + t.Fatalf("error generating test keys: %s", err) + } + + // 1. For each standard RSA key size 2048, 3072, and 4096... + for _, rsaKeyType := range rsaKeyTypes { + t.Log("Key size: ", rsaKeyType) + p := &Policy{ + Name: fmt.Sprint(rsaKeyType), // NOTE: crucial to create a new key per key size + Type: rsaKeyType, + } + + rsaKeyBytes := testKeys[rsaKeyType] + err := p.Import(ctx, storage, rsaKeyBytes, rand.Reader) + if err != nil { + t.Fatal(tabs[1], "❌ Failed to import key:", err) + } + rsaKeyAny, err := x509.ParsePKCS8PrivateKey(rsaKeyBytes) + if err != nil { + t.Fatalf("error parsing test keys: %s", err) + } + rsaKey := rsaKeyAny.(*rsa.PrivateKey) + for _, padding := range []PaddingScheme{PaddingScheme_OAEP, PaddingScheme_PKCS1v15, ""} { + t.Run(fmt.Sprintf("%s/%s", rsaKeyType.String(), padding), func(t *testing.T) { test_RSA_PKCS1(t, p, rsaKey, padding) }) + } + } +} + +func Test_RSA_PKCS1Signing(t *testing.T) { + t.Log("Testing RSA PKCS#1v1.5 signatures") ctx := context.Background() storage := &logical.InmemStorage{} diff --git a/ui/app/components/transit-key-actions.hbs b/ui/app/components/transit-key-actions.hbs index d9070392a9e6..5579a0a1e9cc 100644 --- a/ui/app/components/transit-key-actions.hbs +++ b/ui/app/components/transit-key-actions.hbs @@ -13,6 +13,7 @@ @nonce={{this.props.nonce}} @bits={{this.props.bits}} @key_version={{this.props.key_version}} + @padding_scheme={{this.props.padding_scheme}} @encodedBase64={{this.props.encodedBase64}} @toggleEncodeBase64={{this.toggleEncodeBase64}} @plaintext={{this.props.plaintext}} @@ -27,6 +28,7 @@ @ciphertext={{this.props.ciphertext}} @context={{this.props.context}} @nonce={{this.props.nonce}} + @padding_scheme={{this.props.padding_scheme}} @isModalActive={{this.isModalActive}} @plaintext={{this.props.plaintext}} @doSubmit={{perform this.doSubmit}} @@ -40,6 +42,7 @@ @nonce={{this.props.nonce}} @bits={{this.props.bits}} @plaintext={{this.props.plaintext}} + @padding_scheme={{this.props.padding_scheme}} @ciphertext={{this.props.ciphertext}} @doSubmit={{perform this.doSubmit}} @isModalActive={{this.isModalActive}} @@ -54,6 +57,8 @@ @key_version={{this.props.key_version}} @ciphertext={{this.props.ciphertext}} @isModalActive={{this.isModalActive}} + @decrypt_padding_scheme={{this.props.decrypt_padding_scheme}} + @encrypt_padding_scheme={{this.props.encrypt_padding_scheme}} @doSubmit={{perform this.doSubmit}} data-test-transit-action={{@selectedAction}} /> diff --git a/ui/app/components/transit-key-actions.js b/ui/app/components/transit-key-actions.js index 854908bfeeed..078733584aca 100644 --- a/ui/app/components/transit-key-actions.js +++ b/ui/app/components/transit-key-actions.js @@ -31,6 +31,9 @@ const STARTING_TRANSIT_PROPS = { hash_algorithm: 'sha2-256', algorithm: 'sha2-256', signature_algorithm: 'pss', + padding_scheme: 'oaep', + decrypt_padding_scheme: 'oaep', + encrypt_padding_scheme: 'oaep', bits: 256, bytes: 32, ciphertext: null, @@ -59,12 +62,19 @@ const STARTING_TRANSIT_PROPS = { }; const PROPS_TO_KEEP = { - encrypt: ['plaintext', 'context', 'nonce', 'key_version'], - decrypt: ['ciphertext', 'context', 'nonce'], + encrypt: ['plaintext', 'context', 'padding_scheme', 'nonce', 'key_version'], + decrypt: ['ciphertext', 'context', 'padding_scheme', 'nonce'], sign: ['input', 'hash_algorithm', 'key_version', 'prehashed', 'signature_algorithm'], verify: ['input', 'hmac', 'signature', 'hash_algorithm', 'prehashed'], hmac: ['input', 'algorithm', 'key_version'], - rewrap: ['ciphertext', 'context', 'nonce', 'key_version'], + rewrap: [ + 'ciphertext', + 'context', + 'decrypt_padding_scheme', + 'encrypt_padding_scheme', + 'nonce', + 'key_version', + ], datakey: [], }; @@ -182,6 +192,12 @@ export default class TransitKeyActions extends Component { formData.input = encodeString(formData.input); } } + if (!this.keyIsRSA) { + // Remove various rsa specific padding_scheme arguments if we aren't an RSA key + delete formData.encrypt_padding_scheme; + delete formData.decrypt_padding_scheme; + delete formData.padding_scheme; + } const payload = formData ? this.compactData(formData) : null; try { diff --git a/ui/app/templates/components/transit-key-action/datakey.hbs b/ui/app/templates/components/transit-key-action/datakey.hbs index 04c090071593..ab4562c4d886 100644 --- a/ui/app/templates/components/transit-key-action/datakey.hbs +++ b/ui/app/templates/components/transit-key-action/datakey.hbs @@ -3,7 +3,10 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -
+
@@ -65,6 +68,27 @@
+ {{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}} +
+ +
+
+ +
+
+
+ {{/if}}
diff --git a/ui/app/templates/components/transit-key-action/decrypt.hbs b/ui/app/templates/components/transit-key-action/decrypt.hbs index 2ece29f34fed..cba4bd26384f 100644 --- a/ui/app/templates/components/transit-key-action/decrypt.hbs +++ b/ui/app/templates/components/transit-key-action/decrypt.hbs @@ -3,7 +3,10 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - +

You can decrypt ciphertext using {{@key.name}} as the cryptographic key.

@@ -33,6 +36,27 @@
{{/if}} + {{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}} +
+ +
+
+ +
+
+
+ {{/if}} {{#if (eq @key.convergentEncryptionVersion 1)}}
diff --git a/ui/app/templates/components/transit-key-action/encrypt.hbs b/ui/app/templates/components/transit-key-action/encrypt.hbs index fb1a3d4e5103..9c2181b0e187 100644 --- a/ui/app/templates/components/transit-key-action/encrypt.hbs +++ b/ui/app/templates/components/transit-key-action/encrypt.hbs @@ -4,7 +4,13 @@ ~}}
@@ -49,6 +55,27 @@
{{/if}} + {{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}} +
+ +
+
+ +
+
+
+ {{/if}} {{#if (eq @key.convergentEncryptionVersion 1)}}
diff --git a/ui/app/templates/components/transit-key-action/rewrap.hbs b/ui/app/templates/components/transit-key-action/rewrap.hbs index 966a62927f07..0524e54c80f0 100644 --- a/ui/app/templates/components/transit-key-action/rewrap.hbs +++ b/ui/app/templates/components/transit-key-action/rewrap.hbs @@ -4,7 +4,20 @@ ~}}
@@ -37,6 +50,42 @@
{{/if}} + {{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}} +
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ {{/if}} {{#if (eq @key.convergentEncryptionVersion 1)}}
diff --git a/ui/tests/integration/components/transit-key-actions-test.js b/ui/tests/integration/components/transit-key-actions-test.js index f6208965c52a..bd1d2c3b3d3d 100644 --- a/ui/tests/integration/components/transit-key-actions-test.js +++ b/ui/tests/integration/components/transit-key-actions-test.js @@ -95,6 +95,50 @@ module('Integration | Component | transit key actions', function (hooks) { .exists({ count: 1 }, 'renders signature_algorithm field on verify with rsa key'); }); + test('it renders: padding_scheme field for rsa key types', async function (assert) { + const supportedActions = ['datakey', 'decrypt', 'encrypt']; + const supportedKeyTypes = ['rsa-2048', 'rsa-3072', 'rsa-4096']; + + for (const key of supportedKeyTypes) { + this.set('key', { + type: key, + backend: 'transit', + supportedActions, + }); + for (const action of this.key.supportedActions) { + this.selectedAction = action; + await render(hbs` + `); + assert + .dom('[data-test-padding-scheme]') + .hasValue( + 'oaep', + `key type: ${key} renders padding_scheme field with default value for action: ${action}` + ); + } + } + }); + test('it renders: decrypt_padding_scheme and encrypt_padding_scheme fields for rsa key types', async function (assert) { + this.selectedAction = 'rewrap'; + const supportedKeyTypes = ['rsa-2048', 'rsa-3072', 'rsa-4096']; + const SELECTOR = (type) => `[data-test-padding-scheme="${type}"]`; + for (const key of supportedKeyTypes) { + this.set('key', { + type: key, + backend: 'transit', + supportedActions: [this.selectedAction], + }); + await render(hbs` + `); + assert + .dom(SELECTOR('encrypt')) + .hasValue('oaep', `key type: ${key} renders ${SELECTOR('encrypt')} field with default value`); + assert + .dom(SELECTOR('decrypt')) + .hasValue('oaep', `key type: ${key} renders ${SELECTOR('decrypt')} field with default value`); + } + }); + async function doEncrypt(assert, actions = [], keyattrs = {}) { const keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat(actions) }; diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index dbcafe5bd92c..248283cd6184 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -797,6 +797,16 @@ will be returned. data (also known as additional data or AAD) to also be authenticated with AEAD ciphers (`aes128-gcm96`, `aes256-gcm`, and `chacha20-poly1305`). +- `padding_scheme` `(string: "oaep")` – Specifies the RSA encryption padding + scheme for RSA keys. Must be one of the following supported signature types: + + - `oaep` + - `pkcs1v15` + + ~> **Warning:** `pkcs1v15` is a legacy padding scheme with security weaknesses. + It is recommended that the default of OAEP be used unless specific backwards + compatibility is required. + - `context` `(string: "")` – Specifies the **base64 encoded** context for key derivation. This is required if key derivation is enabled for this key. @@ -922,6 +932,12 @@ This endpoint decrypts the provided ciphertext using the named key. data (also known as additional data or AAD) to also be authenticated with AEAD ciphers (`aes128-gcm96`, `aes256-gcm`, and `chacha20-poly1305`). +- `padding_scheme` `(string: "oaep")` – Specifies the RSA decryption padding + scheme for RSA keys. Must be one of the following supported signature types: + + - `oaep` + - `pkcs1v15` + - `context` `(string: "")` – Specifies the **base64 encoded** context for key derivation. This is required if key derivation is enabled. @@ -1008,6 +1024,22 @@ functionality to untrusted users or scripts. - `ciphertext` `(string: )` – Specifies the ciphertext to re-encrypt. +- `decrypt_padding_scheme` `(string: "oaep")` – Specifies the RSA padding + scheme for RSA keys for the decrypt step. Must be one of the following supported signature types: + + - `oaep` + - `pkcs1v15` + +- `encrypt_padding_scheme` `(string: "oaep")` – Specifies the RSA padding + scheme for RSA keys for the encrypt step. Must be one of the following supported signature types: + + - `oaep` + - `pkcs1v15` + + ~> **Warning:** `pkcs1v15` is a legacy padding scheme with security weaknesses. + It is recommended that the default of OAEP be used unless specific backwards + compatibility is required. + - `context` `(string: "")` – Specifies the **base64 encoded** context for key derivation. This is required if key derivation is enabled. @@ -1109,6 +1141,16 @@ then made available to trusted users. - `bits` `(int: 256)` – Specifies the number of bits in the desired key. Can be 128, 256, or 512. +- `padding_scheme` `(string: "oaep")` – Specifies the RSA encryption padding + scheme for RSA keys. Must be one of the following supported signature types: + + - `oaep` + - `pkcs1v15` + + ~> **Warning:** `pkcs1v15` is a legacy padding scheme with security weaknesses. + It is recommended that the default of OAEP be used unless specific backwards + compatibility is required. + ### Sample payload ```json