Skip to content

Commit

Permalink
add import_version
Browse files Browse the repository at this point in the history
  • Loading branch information
rculpepper committed Apr 29, 2022
1 parent 70e0a8a commit 5492c0a
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 22 deletions.
1 change: 1 addition & 0 deletions builtin/logical/transit/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
99 changes: 85 additions & 14 deletions builtin/logical/transit/path_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
}

Expand Down Expand Up @@ -102,18 +164,32 @@ 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)
if err != nil {
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
}
Expand All @@ -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"
Expand Down
78 changes: 78 additions & 0 deletions builtin/logical/transit/path_import_version..go
Original file line number Diff line number Diff line change
@@ -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 = ""
5 changes: 5 additions & 0 deletions builtin/logical/transit/path_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions sdk/helper/keysutil/lock_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 30 additions & 6 deletions sdk/helper/keysutil/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down

0 comments on commit 5492c0a

Please sign in to comment.