open-vault/builtin/logical/transit/path_import.go
Gabriel Santos 05f3236c15
Provide public key encryption via transit engine (#17934)
* import rsa and ecdsa public keys

* allow import_version to update public keys - wip

* allow import_version to update public keys

* move check key fields into func

* put private/public keys in same switch cases

* fix method in UpdateKeyVersion

* move asymmetrics keys switch to its own method - WIP

* test import public and update it with private counterpart

* test import public keys

* use public_key to encrypt if RSAKey is not present and failed to decrypt
if key version does not have a private key

* move key to KeyEntry parsing from Policy to KeyEntry method

* move extracting of key from input fields into helper function

* change back policy Import signature to keep backwards compatibility and
add new method to import private or public keys

* test import with imported public rsa and ecdsa keys

* descriptions and error messages

* error messages, remove comments and unused code

* changelog

* documentation - wip

* suggested changes - error messages/typos and unwrap public key passed

* fix unwrap key error

* fail if both key fields have been set

* fix in extractKeyFromFields, passing a PolicyRequest wouldn't not work

* checks for read, sign and verify endpoints so they don't return errors when a private key was not imported and tests

* handle panic on "export key" endpoint if imported key is public

* fmt

* remove 'isPrivateKey' argument from 'UpdateKeyVersion' and
'parseFromKey' methods

also: rename 'UpdateKeyVersion' method to 'ImportPrivateKeyForVersion' and 'IsPublicKeyImported' to 'IsPrivateKeyMissing'

* delete 'RSAPublicKey' when private key is imported

* path_export: return public_key for ecdsa and rsa when there's no private key imported

* allow signed data validation with pss algorithm

* remove NOTE comment

* fix typo in EC public key export where empty derBytes was being used

* export rsa public key in pkcs8 format instead of pkcs1 and improve test

* change logic on how check for is private key missing is calculated

---------

Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
2023-05-11 11:56:46 +00:00

446 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package transit
import (
"context"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"hash"
"strconv"
"strings"
"time"
"github.com/google/tink/go/kwp/subtle"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/keysutil"
"github.com/hashicorp/vault/sdk/logical"
)
const EncryptedKeyBytes = 512
func (b *backend) pathImport() *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameRegex("name") + "/import",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixTransit,
OperationVerb: "import",
OperationSuffix: "key",
},
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "The name of the key",
},
"type": {
Type: framework.TypeString,
Default: "aes256-gcm96",
Description: `The type of key being imported. Currently, "aes128-gcm96" (symmetric), "aes256-gcm96" (symmetric), "ecdsa-p256"
(asymmetric), "ecdsa-p384" (asymmetric), "ecdsa-p521" (asymmetric), "ed25519" (asymmetric), "rsa-2048" (asymmetric), "rsa-3072"
(asymmetric), "rsa-4096" (asymmetric) are supported. Defaults to "aes256-gcm96".
`,
},
"hash_function": {
Type: framework.TypeString,
Default: "SHA256",
Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated,
ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`,
},
"ciphertext": {
Type: framework.TypeString,
Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP
with the wrapping key and then concatenated with the import key, wrapped by the AES key.`,
},
"public_key": {
Type: framework.TypeString,
Description: `The plaintext PEM public key to be imported. If "ciphertext" is set, this field is ignored.`,
},
"allow_rotation": {
Type: framework.TypeBool,
Description: "True if the imported key may be rotated within Vault; false otherwise.",
},
"derived": {
Type: framework.TypeBool,
Description: `Enables key derivation mode. This
allows for per-transaction unique
keys for encryption operations.`,
},
"exportable": {
Type: framework.TypeBool,
Description: `Enables keys to be exportable.
This allows for all the valid keys
in the key ring to be exported.`,
},
"allow_plaintext_backup": {
Type: framework.TypeBool,
Description: `Enables taking a backup of the named
key in plaintext format. Once set,
this cannot be disabled.`,
},
"context": {
Type: framework.TypeString,
Description: `Base64 encoded context for key derivation.
When reading a key with key derivation enabled,
if the key type supports public keys, this will
return the public key for the given context.`,
},
"auto_rotate_period": {
Type: framework.TypeDurationSecond,
Default: 0,
Description: `Amount of time the key should live before
being automatically rotated. A value of 0
(default) disables automatic rotation for the
key.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathImportWrite,
},
HelpSynopsis: pathImportWriteSyn,
HelpDescription: pathImportWriteDesc,
}
}
func (b *backend) pathImportVersion() *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameRegex("name") + "/import_version",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixTransit,
OperationVerb: "import",
OperationSuffix: "key-version",
},
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "The name of the key",
},
"ciphertext": {
Type: framework.TypeString,
Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP
with the wrapping key and then concatenated with the import key, wrapped by the AES key.`,
},
"public_key": {
Type: framework.TypeString,
Description: `The plaintext public key to be imported. If "ciphertext" is set, this field is ignored.`,
},
"hash_function": {
Type: framework.TypeString,
Default: "SHA256",
Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated,
ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`,
},
"bump_version": {
Type: framework.TypeBool,
Default: true,
Description: `By default, each operation will create a new key version.
If set to 'false', will try to update the 'Latest' version of the key, unless changed in the "version" field.`,
},
"version": {
Type: framework.TypeInt,
Description: `Key version to be updated, if left empty 'Latest' version will be updated.
If "bump_version" is set to 'true', this field is ignored.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathImportVersionWrite,
},
HelpSynopsis: pathImportVersionWriteSyn,
HelpDescription: pathImportVersionWriteDesc,
}
}
func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
derived := d.Get("derived").(bool)
keyType := d.Get("type").(string)
exportable := d.Get("exportable").(bool)
allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool)
autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int))
allowRotation := d.Get("allow_rotation").(bool)
// Ensure the caller didn't supply "convergent_encryption" as a field, since it's not supported on import.
if _, ok := d.Raw["convergent_encryption"]; ok {
return nil, errors.New("import cannot be used on keys with convergent encryption enabled")
}
if autoRotatePeriod > 0 && !allowRotation {
return nil, errors.New("allow_rotation must be set to true if auto-rotation is enabled")
}
// Ensure that at least on `key` field has been set
isCiphertextSet, err := checkKeyFieldsSet(d)
if err != nil {
return nil, err
}
polReq := keysutil.PolicyRequest{
Storage: req.Storage,
Name: name,
Derived: derived,
Exportable: exportable,
AllowPlaintextBackup: allowPlaintextBackup,
AutoRotatePeriod: autoRotatePeriod,
AllowImportedKeyRotation: allowRotation,
IsPrivateKey: isCiphertextSet,
}
switch strings.ToLower(keyType) {
case "aes128-gcm96":
polReq.KeyType = keysutil.KeyType_AES128_GCM96
case "aes256-gcm96":
polReq.KeyType = keysutil.KeyType_AES256_GCM96
case "chacha20-poly1305":
polReq.KeyType = keysutil.KeyType_ChaCha20_Poly1305
case "ecdsa-p256":
polReq.KeyType = keysutil.KeyType_ECDSA_P256
case "ecdsa-p384":
polReq.KeyType = keysutil.KeyType_ECDSA_P384
case "ecdsa-p521":
polReq.KeyType = keysutil.KeyType_ECDSA_P521
case "ed25519":
polReq.KeyType = keysutil.KeyType_ED25519
case "rsa-2048":
polReq.KeyType = keysutil.KeyType_RSA2048
case "rsa-3072":
polReq.KeyType = keysutil.KeyType_RSA3072
case "rsa-4096":
polReq.KeyType = keysutil.KeyType_RSA4096
case "hmac":
polReq.KeyType = keysutil.KeyType_HMAC
default:
return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest
}
p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader())
if err != nil {
return nil, err
}
if p != nil {
if b.System().CachingDisabled() {
p.Unlock()
}
return nil, errors.New("the import path cannot be used with an existing key; use import-version to rotate an existing imported key")
}
key, resp, err := b.extractKeyFromFields(ctx, req, d, polReq.KeyType, isCiphertextSet)
if err != nil {
return resp, err
}
err = b.lm.ImportPolicy(ctx, polReq, key, b.GetRandomReader())
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathImportVersionWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
bumpVersion := d.Get("bump_version").(bool)
isCiphertextSet, err := checkKeyFieldsSet(d)
if err != nil {
return nil, err
}
polReq := keysutil.PolicyRequest{
Storage: req.Storage,
Name: name,
Upsert: false,
IsPrivateKey: isCiphertextSet,
}
p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader())
if err != nil {
return nil, err
}
if p == nil {
return nil, fmt.Errorf("no key found with name %s; to import a new key, use the import/ endpoint", name)
}
if !p.Imported {
return nil, errors.New("the import_version endpoint can only be used with an imported key")
}
if p.ConvergentEncryption {
return nil, errors.New("import_version cannot be used on keys with convergent encryption enabled")
}
if !b.System().CachingDisabled() {
p.Lock(true)
}
defer p.Unlock()
// Get param version if set else LatestVersion
versionToUpdate := p.LatestVersion
if version, ok := d.Raw["version"]; ok {
versionToUpdate = version.(int)
}
key, resp, err := b.extractKeyFromFields(ctx, req, d, p.Type, isCiphertextSet)
if err != nil {
return resp, err
}
if bumpVersion {
err = p.ImportPublicOrPrivate(ctx, req.Storage, key, isCiphertextSet, b.GetRandomReader())
} else {
// Check if given version can be updated given input
err := p.KeyVersionCanBeUpdated(versionToUpdate, isCiphertextSet)
if err == nil {
err = p.ImportPrivateKeyForVersion(ctx, req.Storage, versionToUpdate, key)
}
}
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) decryptImportedKey(ctx context.Context, storage logical.Storage, ciphertext []byte, hashFn hash.Hash) ([]byte, error) {
// Bounds check the ciphertext to avoid panics
if len(ciphertext) <= EncryptedKeyBytes {
return nil, errors.New("provided ciphertext is too short")
}
wrappedEphKey := ciphertext[:EncryptedKeyBytes]
wrappedImportKey := ciphertext[EncryptedKeyBytes:]
wrappingKey, err := b.getWrappingKey(ctx, storage)
if err != nil {
return nil, err
}
if wrappingKey == nil {
return nil, fmt.Errorf("error importing key: wrapping key was nil")
}
privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey
ephKey, err := rsa.DecryptOAEP(hashFn, b.GetRandomReader(), privWrappingKey, wrappedEphKey, []byte{})
if err != nil {
return nil, err
}
// Zero out the ephemeral AES key just to be extra cautious. Note that this
// isn't a guarantee against memory analysis! See the documentation for the
// `vault.memzero` utility function for more information.
defer func() {
for i := range ephKey {
ephKey[i] = 0
}
}()
// Ensure the ephemeral AES key is 256-bit
if len(ephKey) != 32 {
return nil, errors.New("expected ephemeral AES key to be 256-bit")
}
kwp, err := subtle.NewKWP(ephKey)
if err != nil {
return nil, err
}
importKey, err := kwp.Unwrap(wrappedImportKey)
if err != nil {
return nil, err
}
return importKey, nil
}
func (b *backend) extractKeyFromFields(ctx context.Context, req *logical.Request, d *framework.FieldData, keyType keysutil.KeyType, isPrivateKey bool) ([]byte, *logical.Response, error) {
var key []byte
if isPrivateKey {
hashFnStr := d.Get("hash_function").(string)
hashFn, err := parseHashFn(hashFnStr)
if err != nil {
return key, logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
ciphertextString := d.Get("ciphertext").(string)
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextString)
if err != nil {
return key, nil, err
}
key, err = b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn)
if err != nil {
return key, nil, err
}
} else {
publicKeyString := d.Get("public_key").(string)
if !keyType.ImportPublicKeySupported() {
return key, nil, errors.New("provided type does not support public_key import")
}
key = []byte(publicKeyString)
}
return key, nil, nil
}
func parseHashFn(hashFn string) (hash.Hash, error) {
switch strings.ToUpper(hashFn) {
case "SHA1":
return sha1.New(), nil
case "SHA224":
return sha256.New224(), nil
case "SHA256":
return sha256.New(), nil
case "SHA384":
return sha512.New384(), nil
case "SHA512":
return sha512.New(), nil
default:
return nil, fmt.Errorf("unknown hash function: %s", hashFn)
}
}
// checkKeyFieldsSet: Checks which key fields are set. If both are set, an error is returned
func checkKeyFieldsSet(d *framework.FieldData) (bool, error) {
ciphertextSet := isFieldSet("ciphertext", d)
publicKeySet := isFieldSet("publicKey", d)
if ciphertextSet && publicKeySet {
return false, errors.New("only one of the following fields, ciphertext and public_key, can be set")
} else if ciphertextSet {
return true, nil
} else {
return false, nil
}
}
func isFieldSet(fieldName string, d *framework.FieldData) bool {
_, fieldSet := d.Raw[fieldName]
if !fieldSet {
return false
}
return true
}
const (
pathImportWriteSyn = "Imports an externally-generated key into a new transit key"
pathImportWriteDesc = "This path is used to import an externally-generated " +
"key into Vault. The import operation creates a new key and cannot be used to " +
"replace an existing key."
)
const pathImportVersionWriteSyn = "Imports an externally-generated key into an " +
"existing imported key"
const pathImportVersionWriteDesc = "This path is used to import a new version of an " +
"externally-generated key into an existing import key. The import_version endpoint " +
"only supports importing key material into existing imported keys."