05f3236c15
* 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>
446 lines
14 KiB
Go
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."
|