Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transit byok import endpoints #15414

Merged
merged 16 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions builtin/logical/transit/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error)
b.pathRotate(),
b.pathRewrap(),
b.pathWrappingKey(),
b.pathImport(),
b.pathImportVersion(),
b.pathKeys(),
b.pathListKeys(),
b.pathExportKeys(),
Expand Down Expand Up @@ -248,6 +250,11 @@ func (b *backend) rotateIfRequired(ctx context.Context, req *logical.Request, ke
}
defer p.Unlock()

// If the key is imported, it can only be rotated from within Vault if allowed.
if p.Imported && !p.AllowImportedKeyRotation {
return nil
}

// If the policy's automatic rotation period is 0, it should not
// automatically rotate.
if p.AutoRotatePeriod == 0 {
Expand Down
346 changes: 346 additions & 0 deletions builtin/logical/transit/path_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
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",
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"`,
sgmiller marked this conversation as resolved.
Show resolved Hide resolved
},
"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.`,
},
"allow_rotation": {
sgmiller marked this conversation as resolved.
Show resolved Hide resolved
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",
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.`,
},
"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"`,
},
},
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)
hashFnStr := d.Get("hash_function").(string)
exportable := d.Get("exportable").(bool)
allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool)
autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int))
ciphertextString := d.Get("ciphertext").(string)
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")
}

polReq := keysutil.PolicyRequest{
Storage: req.Storage,
Name: name,
Derived: derived,
Exportable: exportable,
AllowPlaintextBackup: allowPlaintextBackup,
AutoRotatePeriod: autoRotatePeriod,
AllowImportedKeyRotation: allowRotation,
}

switch keyType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since we test within parseHashFn in a case-insensitive fashion, should we do the same here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well!

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
default:
return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest
}

hashFn, err := parseHashFn(hashFnStr)
if err != nil {
return logical.ErrorResponse(err.Error()), 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")
}

ciphertext, err := base64.RawURLEncoding.DecodeString(ciphertextString)
if err != nil {
return nil, err
}

key, err := b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn)
if err != nil {
return nil, 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)
hashFnStr := d.Get("hash_function").(string)
ciphertextString := d.Get("ciphertext").(string)

polReq := keysutil.PolicyRequest{
Storage: req.Storage,
Name: name,
Upsert: false,
}

hashFn, err := parseHashFn(hashFnStr)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

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()

ciphertext, err := base64.RawURLEncoding.DecodeString(ciphertextString)
if err != nil {
return nil, err
}
importKey, err := b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn)
err = p.Import(ctx, req.Storage, importKey, 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 {
sgmiller marked this conversation as resolved.
Show resolved Hide resolved
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)
sgmiller marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

importKey, err := kwp.Unwrap(wrappedImportKey)
if err != nil {
return nil, err
}

return importKey, 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)
}
}

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."
Loading