From 5492c0aa20080a8e3aefb64b590d07f35fa3717a Mon Sep 17 00:00:00 2001 From: rculpepper Date: Thu, 28 Apr 2022 20:26:58 -0400 Subject: [PATCH] add import_version --- builtin/logical/transit/backend.go | 1 + builtin/logical/transit/path_import.go | 99 ++++++++++++++++--- .../logical/transit/path_import_version..go | 78 +++++++++++++++ builtin/logical/transit/path_keys.go | 5 + sdk/helper/keysutil/lock_manager.go | 2 - sdk/helper/keysutil/policy.go | 36 +++++-- 6 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 builtin/logical/transit/path_import_version..go diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index d93e3b9135c1..f391bbe7d84f 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -49,6 +49,7 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error) b.pathRewrap(), b.pathWrappingKey(), b.pathImport(), + b.pathImportVersion(), b.pathKeys(), b.pathListKeys(), b.pathExportKeys(), diff --git a/builtin/logical/transit/path_import.go b/builtin/logical/transit/path_import.go index 357483f316a0..ce04766d76ee 100644 --- a/builtin/logical/transit/path_import.go +++ b/builtin/logical/transit/path_import.go @@ -5,7 +5,9 @@ import ( "crypto/rsa" "crypto/sha256" "encoding/base64" + "errors" "fmt" + "time" "github.com/google/tink/go/kwp/subtle" "github.com/hashicorp/vault/sdk/framework" @@ -31,12 +33,6 @@ The type of key being imported. Currently, "aes128-gcm96" (symmetric), "aes256-g (asymmetric), "ecdsa-p384" (asymmetric), "ecdsa-p521" (asymmetric), "ed25519" (asymmetric), "rsa-2048" (asymmetric), "rsa-3072" (asymmetric), "rsa-4096" (asymmetric) are supported. Defaults to "aes256-gcm96". `, - }, - "allow_plaintext_backup": { - Type: framework.TypeBool, - Description: `Enables taking a backup of the named -key in plaintext format. Once set, -this cannot be disabled.`, }, "ciphertext": { Type: framework.TypeString, @@ -47,6 +43,58 @@ with the wrapping key and then concatenated with the import key, wrapped by the 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.`, + }, + + "convergent_encryption": { + Type: framework.TypeBool, + Description: `Whether to support convergent encryption. +This is only supported when using a key with +key derivation enabled and will require all +requests to carry both a context and 96-bit +(12-byte) nonce. The given nonce will be used +in place of a randomly generated nonce. As a +result, when the same context and nonce are +supplied, the same ciphertext is generated. It +is *very important* when using this mode that +you ensure that all nonces are unique for a +given context. Failing to do so will severely +impact the ciphertext's security.`, + }, + + "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, @@ -58,13 +106,27 @@ with the wrapping key and then concatenated with the import key, wrapped by the 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) + convergent := d.Get("convergent_encryption").(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)) ciphertextString := d.Get("ciphertext").(string) allowRotation := d.Get("allow_rotation").(bool) + 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, + Convergent: convergent, + Exportable: exportable, + AllowPlaintextBackup: allowPlaintextBackup, + AutoRotatePeriod: autoRotatePeriod, AllowImportedKeyRotation: allowRotation, } @@ -102,7 +164,7 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * if b.System().CachingDisabled() { p.Unlock() } - return nil, errors.New("the import path cannot overwrite an existing key; use import-version to rotate an imported key") + 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) @@ -110,10 +172,24 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * return nil, err } + key, err := b.decryptImportedKey(ctx, req.Storage, ciphertext) + 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) decryptImportedKey(ctx context.Context, storage logical.Storage, ciphertext []byte) ([]byte, error) { wrappedAESKey := ciphertext[:EncryptedKeyBytes] wrappedImportKey := ciphertext[EncryptedKeyBytes:] - wrappingKey, err := getWrappingKey(ctx, req.Storage) + wrappingKey, err := getWrappingKey(ctx, storage) if err != nil { return nil, err } @@ -137,12 +213,7 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * return nil, err } - err = b.lm.ImportPolicy(ctx, polReq, importKey, b.GetRandomReader()) - if err != nil { - return nil, err - } - - return nil, nil + return importKey, nil } const pathImportWriteSyn = "Imports an externally-generated key into transit" diff --git a/builtin/logical/transit/path_import_version..go b/builtin/logical/transit/path_import_version..go new file mode 100644 index 000000000000..33f8e4693506 --- /dev/null +++ b/builtin/logical/transit/path_import_version..go @@ -0,0 +1,78 @@ +package transit + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/keysutil" + "github.com/hashicorp/vault/sdk/logical" +) + +func (b *backend) pathImportVersion() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/import_version", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + 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.`, + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathImportVersionWrite, + }, + HelpSynopsis: pathImportVersionWriteSyn, + HelpDescription: pathImportVersionWriteDesc, + } +} + +func (b *backend) pathImportVersionWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + ciphertextString := d.Get("ciphertext").(string) + + polReq := keysutil.PolicyRequest{ + Storage: req.Storage, + Name: name, + Upsert: false, + } + + 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) + err = p.Import(ctx, req.Storage, importKey, b.GetRandomReader()) + if err != nil { + return nil, err + } + + return nil, nil +} + +const pathImportVersionWriteSyn = "" +const pathImportVersionWriteDesc = "" diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index bcaf326c23dc..e64adb24a81c 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -239,9 +239,14 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f "supports_signing": p.Type.SigningSupported(), "supports_derivation": p.Type.DerivationSupported(), "auto_rotate_period": int64(p.AutoRotatePeriod.Seconds()), + "imported_key": p.Imported, }, } + if p.Imported { + resp.Data["imported_key_allow_rotation"] = p.AllowImportedKeyRotation + } + if p.BackupInfo != nil { resp.Data["backup_info"] = map[string]interface{}{ "time": p.BackupInfo.Time, diff --git a/sdk/helper/keysutil/lock_manager.go b/sdk/helper/keysutil/lock_manager.go index 7d46cee7556c..33b298041145 100644 --- a/sdk/helper/keysutil/lock_manager.go +++ b/sdk/helper/keysutil/lock_manager.go @@ -442,8 +442,6 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io return } -// When the function returns, if caching was disabled, the Policy's lock must -// be unlocked when the caller is done (and it should not be re-locked). func (lm *LockManager) ImportPolicy(ctx context.Context, req PolicyRequest, key []byte, rand io.Reader) error { var p *Policy var err error diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 1e256176e7ac..07645521879d 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -1389,17 +1389,22 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte } switch parsedPrivateKey.(type) { - case ecdsa.PrivateKey: - ecdsaKey := parsedPrivateKey.(ecdsa.PrivateKey) - curve := elliptic.P256() - if p.Type == KeyType_ECDSA_P384 { + case *ecdsa.PrivateKey: + ecdsaKey := parsedPrivateKey.(*ecdsa.PrivateKey) + var curve elliptic.Curve + if p.Type == KeyType_ECDSA_P256 { + curve = elliptic.P256() + } else if p.Type == KeyType_ECDSA_P384 { curve = elliptic.P384() } else if p.Type == KeyType_ECDSA_P521 { curve = elliptic.P521() } + if curve == nil { + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) + } if ecdsaKey.Curve != curve { - return fmt.Errorf("key type mismatch: provided key uses %s, expected %s", ecdsaKey.Curve, curve) + return fmt.Errorf("invalid curve: expected %s, got %s", p.Type, curve) } entry.EC_D = ecdsaKey.D @@ -1419,16 +1424,35 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte } entry.FormattedPublicKey = string(pemBytes) case ed25519.PrivateKey: + if p.Type != KeyType_ED25519 { + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) + } privateKey := parsedPrivateKey.(ed25519.PrivateKey) entry.Key = privateKey publicKey := privateKey.Public().(ed25519.PublicKey) entry.FormattedPublicKey = base64.StdEncoding.EncodeToString(publicKey) case *rsa.PrivateKey: + var keyBits int + if p.Type == KeyType_RSA2048 { + keyBits = 2048 + } else if p.Type == KeyType_RSA3072 { + keyBits = 3072 + } else if p.Type == KeyType_RSA4096 { + keyBits = 4096 + } rsaKey := parsedPrivateKey.(*rsa.PrivateKey) + + if keyBits == 0 { + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, rsaKey) + } + if rsaKey.Size() != keyBits { + return fmt.Errorf("invalid key size: expected %s, got %d", p.Type, rsaKey.Size()) + } + entry.RSAKey = rsaKey default: - return fmt.Errorf("error parsing key: invalid key type %T", parsedPrivateKey) + return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) } }