Update marcellanz/transit_pkcs1v15 RSA encryption support (#25486)

* [transit-pkcs1v15] transit support for the pkcs1v15 padding scheme – without UI tests (yet).

* [transit-pkcs1v15] renamed padding_scheme parameter in transit documentation.

* [transit-pkcs1v15] add changelog file.

* [transit-pkcs1v15] remove the algorithm path as padding_scheme is chosen by parameter.

* Update ui/app/templates/components/transit-key-action/datakey.hbs

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>

* Update ui/app/templates/components/transit-key-action/datakey.hbs

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>

* Update ui/app/templates/components/transit-key-action/datakey.hbs

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>

* Update website/content/api-docs/secret/transit.mdx

Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>

* Update website/content/api-docs/secret/transit.mdx

Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>

* Update website/content/api-docs/secret/transit.mdx

Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>

* Add warnings to PKCS1v1.5 usage

* Update transit

* Update transit, including separating encrypt/decrypt paddings for rewrap

* Clean up factory use in the presence of padding

* address review feedback

* remove defaults

* lint

* more lint

* Some fixes for UI issues

 - Fix padding scheme dropdown console error by adding values
   to the transit-key-actions.hbs
 - Populate both padding scheme drop down menus within rewrap,
   not just the one padding_scheme
 - Do not submit a padding_scheme value through POST for non-rsa keys

* Fix Transit rewrap API to use decrypt_padding_scheme, encrypt_padding_scheme

 - Map the appropriate API fields for the RSA padding scheme to the
   batch items within the rewrap API
 - Add the ability to create RSA keys within the encrypt API endpoint
 - Add test case for rewrap api that leverages the padding_scheme fields

* Fix code linting issues

* simply padding scheme enum

* Apply suggestions from code review

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>

* Fix padding_scheme processing on data key api

 - The data key api was using the incorrect parameter name for
   the padding scheme
 - Enforce that padding_scheme is only used on RSA keys, we
   are punting on supporting it for managed keys at the moment.

* Add tests for parsePaddingSchemeArg

* Add missing copywrite headers

* Some small UI fixes

* Add missing param to datakey in api-docs

* Do not send padding_scheme for non-RSA key types within UI

* add UI tests for transit key actions form

---------

Co-authored-by: Marcel Lanz <marcellanz@n-1.ch>
Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>
Co-authored-by: Steve Clark <steven.clark@hashicorp.com>
Co-authored-by: claire bontempo <cbontempo@hashicorp.com>
This commit is contained in:
Scott Miller
2024-10-09 09:30:14 -05:00
committed by GitHub
parent 770d902f60
commit 3c0656e4c4
20 changed files with 949 additions and 104 deletions

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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{

View File

@@ -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:

View File

@@ -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")
}

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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"])
}
}
}

3
changelog/25486.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
secrets/transit: Add support for RSA padding scheme pkcs1v15 for encryption
```

View File

@@ -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 {

View File

@@ -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{}

View File

@@ -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}}
/>

View File

@@ -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 {

View File

@@ -3,7 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form {{on "submit" (fn @doSubmit (hash param=@param context=@context nonce=@nonce bits=@bits))}} ...attributes>
<form
{{on "submit" (fn @doSubmit (hash param=@param context=@context padding_scheme=@padding_scheme nonce=@nonce bits=@bits))}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="datakey creation" />
<div class="content has-bottom-margin-l">
@@ -65,6 +68,27 @@
</div>
</div>
</div>
{{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}}
<div class="field">
<label for="padding_scheme" class="is-label">Padding scheme</label>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name="padding_scheme"
id="padding_scheme"
data-test-padding-scheme
onchange={{action (mut @padding_scheme) value="target.value"}}
>
{{#each (array "oaep" "pkcs1v15") as |scheme|}}
<option selected={{eq @padding_scheme scheme}} value={{scheme}}>
{{scheme}}
</option>
{{/each}}
</select>
</div>
</div>
</div>
{{/if}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">

View File

@@ -3,7 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form {{on "submit" (fn @doSubmit (hash ciphertext=@ciphertext context=@context nonce=@nonce))}} ...attributes>
<form
{{on "submit" (fn @doSubmit (hash ciphertext=@ciphertext padding_scheme=@padding_scheme context=@context nonce=@nonce))}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="content has-bottom-margin-l">
<p>You can decrypt ciphertext using <code>{{@key.name}}</code> as the cryptographic key.</p>
@@ -33,6 +36,27 @@
</div>
</div>
{{/if}}
{{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}}
<div class="field">
<label for="padding_scheme" class="is-label">Padding Scheme</label>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name="padding_scheme"
id="padding_scheme"
data-test-padding-scheme
onchange={{action (mut @padding_scheme) value="target.value"}}
>
{{#each (array "oaep" "pkcs1v15") as |scheme|}}
<option selected={{eq @padding_scheme scheme}} value={{scheme}}>
{{scheme}}
</option>
{{/each}}
</select>
</div>
</div>
</div>
{{/if}}
{{#if (eq @key.convergentEncryptionVersion 1)}}
<div class="field">
<label for="nonce" class="is-label">Nonce</label>

View File

@@ -4,7 +4,13 @@
~}}
<form
{{on "submit" (fn @doSubmit (hash plaintext=@plaintext context=@context nonce=@nonce key_version=@key_version))}}
{{on
"submit"
(fn
@doSubmit
(hash plaintext=@plaintext padding_scheme=@padding_scheme context=@context nonce=@nonce key_version=@key_version)
)
}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
@@ -49,6 +55,27 @@
</div>
</div>
{{/if}}
{{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}}
<div class="field">
<label for="padding_scheme" class="is-label">Padding scheme</label>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name="padding_scheme"
id="padding_scheme"
data-test-padding-scheme
onchange={{action (mut @padding_scheme) value="target.value"}}
>
{{#each (array "oaep" "pkcs1v15") as |scheme|}}
<option selected={{eq @padding_scheme scheme}} value={{scheme}}>
{{scheme}}
</option>
{{/each}}
</select>
</div>
</div>
</div>
{{/if}}
{{#if (eq @key.convergentEncryptionVersion 1)}}
<div class="field">
<div class="level">

View File

@@ -4,7 +4,20 @@
~}}
<form
{{on "submit" (fn @doSubmit (hash ciphertext=@ciphertext context=@context nonce=@nonce key_version=@key_version))}}
{{on
"submit"
(fn
@doSubmit
(hash
ciphertext=@ciphertext
decrypt_padding_scheme=@decrypt_padding_scheme
encrypt_padding_scheme=@encrypt_padding_scheme
context=@context
nonce=@nonce
key_version=@key_version
)
)
}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
@@ -37,6 +50,42 @@
</div>
</div>
{{/if}}
{{#if (includes @key.type (array "rsa-2048" "rsa-3072" "rsa-4096"))}}
<div class="field">
<label for="decrypt_padding_scheme" class="is-label">Decrypt padding scheme</label>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name="decrypt_padding_scheme"
id="decrypt_padding_scheme"
data-test-padding-scheme="decrypt"
onchange={{action (mut @decrypt_padding_scheme) value="target.value"}}
>
{{#each (array "oaep" "pkcs1v15") as |scheme|}}
<option selected={{eq @decrypt_padding_scheme scheme}} value={{scheme}}>{{scheme}}</option>
{{/each}}
</select>
</div>
</div>
</div>
<div class="field">
<label for="encrypt_padding_scheme" class="is-label">Encrypt padding scheme</label>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name="encrypt_padding_scheme"
id="encrypt_padding_scheme"
data-test-padding-scheme="encrypt"
onchange={{action (mut @encrypt_padding_scheme) value="target.value"}}
>
{{#each (array "oaep" "pkcs1v15") as |scheme|}}
<option selected={{eq @encrypt_padding_scheme scheme}} value={{scheme}}>{{scheme}}</option>
{{/each}}
</select>
</div>
</div>
</div>
{{/if}}
{{#if (eq @key.convergentEncryptionVersion 1)}}
<div class="field">
<label for="nonce" class="is-label">Nonce</label>

View File

@@ -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`
<TransitKeyActions @selectedAction={{this.selectedAction}} @key={{this.key}} />`);
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`
<TransitKeyActions @selectedAction={{this.selectedAction}} @key={{this.key}} />`);
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) };

View File

@@ -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: <required>)`  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