mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
437 lines
13 KiB
Go
437 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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"`,
|
|
},
|
|
"version": {
|
|
Type: framework.TypeInt,
|
|
Description: `Key version to be updated, if left empty, a new version will be created unless
|
|
a private key is specified and the 'Latest' key is missing a private key.`,
|
|
},
|
|
},
|
|
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)
|
|
|
|
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()
|
|
|
|
key, resp, err := b.extractKeyFromFields(ctx, req, d, p.Type, isCiphertextSet)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
|
|
// Get param version if set else import a new version.
|
|
if version, ok := d.GetOk("version"); ok {
|
|
versionToUpdate := version.(int)
|
|
|
|
// 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)
|
|
}
|
|
} else {
|
|
err = p.ImportPublicOrPrivate(ctx, req.Storage, key, isCiphertextSet, b.GetRandomReader())
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (b *backend) decryptImportedKey(ctx context.Context, storage logical.Storage, ciphertext []byte, hashFn hash.Hash) ([]byte, error) {
|
|
// Bounds check the ciphertext to avoid panics
|
|
if len(ciphertext) <= EncryptedKeyBytes {
|
|
return nil, errors.New("provided ciphertext is too short")
|
|
}
|
|
|
|
wrappedEphKey := ciphertext[:EncryptedKeyBytes]
|
|
wrappedImportKey := ciphertext[EncryptedKeyBytes:]
|
|
|
|
wrappingKey, err := b.getWrappingKey(ctx, storage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wrappingKey == nil {
|
|
return nil, fmt.Errorf("error importing key: wrapping key was nil")
|
|
}
|
|
|
|
privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey
|
|
ephKey, err := rsa.DecryptOAEP(hashFn, b.GetRandomReader(), privWrappingKey, wrappedEphKey, []byte{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Zero out the ephemeral AES key just to be extra cautious. Note that this
|
|
// isn't a guarantee against memory analysis! See the documentation for the
|
|
// `vault.memzero` utility function for more information.
|
|
defer func() {
|
|
for i := range ephKey {
|
|
ephKey[i] = 0
|
|
}
|
|
}()
|
|
|
|
// Ensure the ephemeral AES key is 256-bit
|
|
if len(ephKey) != 32 {
|
|
return nil, errors.New("expected ephemeral AES key to be 256-bit")
|
|
}
|
|
|
|
kwp, err := subtle.NewKWP(ephKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
importKey, err := kwp.Unwrap(wrappedImportKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return importKey, nil
|
|
}
|
|
|
|
func (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."
|