Add datakey generation to transit.
Can specify 128 bits (defaults to 256) and control whether or not plaintext is returned (default true). Unit tests for all of the new functionality.
This commit is contained in:
parent
61398f1b01
commit
448249108c
|
@ -27,6 +27,7 @@ func Backend() *framework.Backend {
|
|||
pathKeys(),
|
||||
pathEncrypt(),
|
||||
pathDecrypt(),
|
||||
pathDatakey(),
|
||||
},
|
||||
|
||||
Secrets: []*framework.Secret{},
|
||||
|
|
|
@ -39,6 +39,20 @@ func TestBackend_basic(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackend_datakey(t *testing.T) {
|
||||
dataKeyInfo := make(map[string]interface{})
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
Backend: Backend(),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepWritePolicy(t, "test", false),
|
||||
testAccStepReadPolicy(t, "test", false, false),
|
||||
testAccStepWriteDatakey(t, "test", false, 256, dataKeyInfo),
|
||||
testAccStepDecryptDatakey(t, "test", dataKeyInfo),
|
||||
testAccStepWriteDatakey(t, "test", true, 128, dataKeyInfo),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackend_rotation(t *testing.T) {
|
||||
decryptData := make(map[string]interface{})
|
||||
encryptHistory := make(map[int]map[string]interface{})
|
||||
|
@ -423,6 +437,72 @@ func testAccStepRotate(t *testing.T, name string) logicaltest.TestStep {
|
|||
}
|
||||
}
|
||||
|
||||
func testAccStepWriteDatakey(t *testing.T, name string,
|
||||
noPlaintext bool, bits int,
|
||||
dataKeyInfo map[string]interface{}) logicaltest.TestStep {
|
||||
data := map[string]interface{}{}
|
||||
if noPlaintext {
|
||||
data["no_plaintext"] = true
|
||||
}
|
||||
if bits != 256 {
|
||||
data["bits"] = bits
|
||||
}
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "datakey/" + name,
|
||||
Data: data,
|
||||
Check: func(resp *logical.Response) error {
|
||||
var d struct {
|
||||
Plaintext string `mapstructure:"plaintext"`
|
||||
Ciphertext string `mapstructure:"ciphertext"`
|
||||
}
|
||||
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
||||
return err
|
||||
}
|
||||
if noPlaintext && len(d.Plaintext) != 0 {
|
||||
return fmt.Errorf("received plaintxt when we disabled it")
|
||||
}
|
||||
if !noPlaintext {
|
||||
if len(d.Plaintext) == 0 {
|
||||
return fmt.Errorf("did not get plaintext when we expected it")
|
||||
}
|
||||
dataKeyInfo["plaintext"] = d.Plaintext
|
||||
plainBytes, err := base64.StdEncoding.DecodeString(d.Plaintext)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not base64 decode plaintext string '%s'", d.Plaintext)
|
||||
}
|
||||
if len(plainBytes)*8 != bits {
|
||||
return fmt.Errorf("returned key does not have correct bit length")
|
||||
}
|
||||
}
|
||||
dataKeyInfo["ciphertext"] = d.Ciphertext
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepDecryptDatakey(t *testing.T, name string,
|
||||
dataKeyInfo map[string]interface{}) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "decrypt/" + name,
|
||||
Data: dataKeyInfo,
|
||||
Check: func(resp *logical.Response) error {
|
||||
var d struct {
|
||||
Plaintext string `mapstructure:"plaintext"`
|
||||
}
|
||||
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Plaintext != dataKeyInfo["plaintext"].(string) {
|
||||
return fmt.Errorf("plaintext mismatch: got '%s', expected '%s', decryptData was %#v", d.Plaintext, dataKeyInfo["plaintext"].(string))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyUpgrade(t *testing.T) {
|
||||
p := &Policy{
|
||||
Name: "test",
|
||||
|
|
129
builtin/logical/transit/path_datakey.go
Normal file
129
builtin/logical/transit/path_datakey.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package transit
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathDatakey() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "datakey/" + framework.GenericNameRegex("name"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The backend key used for encrypting the data key",
|
||||
},
|
||||
|
||||
"context": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Context for key derivation. Required for derived keys.",
|
||||
},
|
||||
|
||||
"bits": &framework.FieldSchema{
|
||||
Type: framework.TypeInt,
|
||||
Description: `Number of bits for the key; currently 128 and
|
||||
256 are supported. Defaults to 256.`,
|
||||
Default: 256,
|
||||
},
|
||||
|
||||
"no_plaintext": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Description: "If set, the plaintext of the key will not be returned",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: pathDatakeyWrite,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathDatakeyHelpSyn,
|
||||
HelpDescription: pathDatakeyHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func pathDatakeyWrite(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
|
||||
// Decode the context if any
|
||||
contextRaw := d.Get("context").(string)
|
||||
var context []byte
|
||||
if len(contextRaw) != 0 {
|
||||
var err error
|
||||
context, err = base64.StdEncoding.DecodeString(contextRaw)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("failed to decode context as base64"), logical.ErrInvalidRequest
|
||||
}
|
||||
}
|
||||
|
||||
// Get the policy
|
||||
p, err := getPolicy(req, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Error if invalid policy
|
||||
if p == nil {
|
||||
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
newKey := make([]byte, 32)
|
||||
bits := d.Get("bits").(int)
|
||||
switch bits {
|
||||
case 256:
|
||||
case 128:
|
||||
newKey = make([]byte, 16)
|
||||
default:
|
||||
return logical.ErrorResponse("invalid bit length"), logical.ErrInvalidRequest
|
||||
}
|
||||
_, err = rand.Read(newKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext, err := p.Encrypt(context, base64.StdEncoding.EncodeToString(newKey))
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case certutil.UserError:
|
||||
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
||||
case certutil.InternalError:
|
||||
return nil, err
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ciphertext == "" {
|
||||
return nil, fmt.Errorf("empty ciphertext returned")
|
||||
}
|
||||
|
||||
// Generate the response
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"ciphertext": ciphertext,
|
||||
},
|
||||
}
|
||||
|
||||
if !d.Get("no_plaintext").(bool) {
|
||||
resp.Data["plaintext"] = base64.StdEncoding.EncodeToString(newKey)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
const pathDatakeyHelpSyn = `Generate a data key`
|
||||
|
||||
const pathDatakeyHelpDesc = `
|
||||
This path can be used to generate a data key: a random
|
||||
key of a certain length that can be used for encryption
|
||||
and decryption, protected by the named backend key. 128
|
||||
or 256 bits can be specified; if not specified, the default
|
||||
is 256 bits. The "no_plaintext" parameter can be used to
|
||||
prevent the (base64-encoded) plaintext key from being
|
||||
returned along with the encrypted key.
|
||||
`
|
Loading…
Reference in a new issue