From 46073e44702db8b2df7e01685a323b6219a2029e Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 14 Sep 2015 16:28:46 -0400 Subject: [PATCH 1/4] Enhance transit backend: * Remove raw endpoint from transit * Add multi-key structure * Add enable, disable, rewrap, and rotate functionality * Upgrade functionality, and record creation time of keys in metadata. Add flag in config function to control the minimum decryption version, and enforce that in the decrypt function * Unit tests for everything --- builtin/logical/transit/backend.go | 9 +- builtin/logical/transit/backend_test.go | 284 +++++++++++--- builtin/logical/transit/path_config.go | 67 ++++ builtin/logical/transit/path_decrypt.go | 63 +-- .../logical/transit/path_enable_disable.go | 111 ++++++ builtin/logical/transit/path_encrypt.go | 73 +--- builtin/logical/transit/path_keys.go | 132 +------ builtin/logical/transit/path_raw.go | 58 --- builtin/logical/transit/path_rewrap.go | 120 ++++++ builtin/logical/transit/path_rotate.go | 56 +++ builtin/logical/transit/policy.go | 360 ++++++++++++++++++ logical/testing/testing.go | 13 +- .../source/docs/secrets/transit/index.html.md | 56 --- 13 files changed, 1020 insertions(+), 382 deletions(-) create mode 100644 builtin/logical/transit/path_config.go create mode 100644 builtin/logical/transit/path_enable_disable.go delete mode 100644 builtin/logical/transit/path_raw.go create mode 100644 builtin/logical/transit/path_rewrap.go create mode 100644 builtin/logical/transit/path_rotate.go create mode 100644 builtin/logical/transit/policy.go diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index bfe348b8cc..68a805d66b 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -15,13 +15,18 @@ func Backend() *framework.Backend { PathsSpecial: &logical.Paths{ Root: []string{ "keys/*", - "raw/*", }, }, Paths: []*framework.Path{ + // Rotate/Enable/Disable needs to come before Keys + // as the handler is greedy + pathEnable(), + pathDisable(), + pathConfig(), + pathRotate(), + pathRewrap(), pathKeys(), - pathRaw(), pathEncrypt(), pathDecrypt(), }, diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index 88caed93fb..f95c7847fe 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -3,6 +3,8 @@ package transit import ( "encoding/base64" "fmt" + "strconv" + "strings" "testing" "github.com/hashicorp/vault/logical" @@ -21,12 +23,76 @@ func TestBackend_basic(t *testing.T) { Steps: []logicaltest.TestStep{ testAccStepWritePolicy(t, "test", false), testAccStepReadPolicy(t, "test", false, false), - testAccStepReadRaw(t, "test", false, false), testAccStepEncrypt(t, "test", testPlaintext, decryptData), testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepDeleteNotDisabledPolicy(t, "test"), + testAccStepDisablePolicy(t, "test"), + testAccStepDeletePolicy(t, "test"), + testAccStepWritePolicy(t, "test", false), + testAccStepDisablePolicy(t, "test"), + testAccStepEnablePolicy(t, "test"), + testAccStepDeleteNotDisabledPolicy(t, "test"), + testAccStepDisablePolicy(t, "test"), + testAccStepDeletePolicy(t, "test"), + testAccStepReadPolicy(t, "test", true, false), + }, + }) +} + +func TestBackend_rotation(t *testing.T) { + decryptData := make(map[string]interface{}) + encryptHistory := make(map[int]map[string]interface{}) + logicaltest.Test(t, logicaltest.TestCase{ + Backend: Backend(), + Steps: []logicaltest.TestStep{ + testAccStepWritePolicy(t, "test", false), + testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 0, encryptHistory), + testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 1, encryptHistory), + testAccStepRotate(t, "test"), // now v2 + testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 2, encryptHistory), + testAccStepRotate(t, "test"), // now v3 + testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 3, encryptHistory), + testAccStepRotate(t, "test"), // now v4 + testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 4, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 99, encryptHistory), + testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 0, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 1, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 2, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 3, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 99, encryptHistory), + testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 4, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepDeleteNotDisabledPolicy(t, "test"), + testAccStepAdjustPolicy(t, "test", 3), + testAccStepLoadVX(t, "test", decryptData, 0, encryptHistory), + testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 1, encryptHistory), + testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 2, encryptHistory), + testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 3, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 4, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepAdjustPolicy(t, "test", 1), + testAccStepLoadVX(t, "test", decryptData, 0, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 1, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepLoadVX(t, "test", decryptData, 2, encryptHistory), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepRewrap(t, "test", decryptData, 4), + testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepDisablePolicy(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepReadPolicy(t, "test", true, false), - testAccStepReadRaw(t, "test", true, false), }, }) } @@ -40,6 +106,7 @@ func TestBackend_upsert(t *testing.T) { testAccStepEncrypt(t, "test", testPlaintext, decryptData), testAccStepReadPolicy(t, "test", false, false), testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepDisablePolicy(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepReadPolicy(t, "test", true, false), }, @@ -53,12 +120,11 @@ func TestBackend_basic_derived(t *testing.T) { Steps: []logicaltest.TestStep{ testAccStepWritePolicy(t, "test", true), testAccStepReadPolicy(t, "test", false, true), - testAccStepReadRaw(t, "test", false, true), testAccStepEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData), testAccStepDecrypt(t, "test", testPlaintext, decryptData), + testAccStepDisablePolicy(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepReadPolicy(t, "test", true, true), - testAccStepReadRaw(t, "test", true, true), }, }) } @@ -73,6 +139,30 @@ func testAccStepWritePolicy(t *testing.T, name string, derived bool) logicaltest } } +func testAccStepEnablePolicy(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "keys/" + name + "/enable", + } +} + +func testAccStepAdjustPolicy(t *testing.T, name string, minVer int) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "keys/" + name + "/config", + Data: map[string]interface{}{ + "min_decryption_version": minVer, + }, + } +} + +func testAccStepDisablePolicy(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "keys/" + name + "/disable", + } +} + func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.DeleteOperation, @@ -80,6 +170,20 @@ func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { } } +func testAccStepDeleteNotDisabledPolicy(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.DeleteOperation, + Path: "keys/" + name, + ErrorOk: true, + Check: func(resp *logical.Response) error { + if resp.IsError() { + return nil + } + return fmt.Errorf("expected error but did not get one") + }, + } +} + func testAccStepReadPolicy(t *testing.T, name string, expectNone, derived bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, @@ -94,11 +198,13 @@ func testAccStepReadPolicy(t *testing.T, name string, expectNone, derived bool) return nil } var d struct { - Name string `mapstructure:"name"` - Key []byte `mapstructure:"key"` - CipherMode string `mapstructure:"cipher_mode"` - Derived bool `mapstructure:"derived"` - KDFMode string `mapstructure:"kdf_mode"` + Name string `mapstructure:"name"` + Key []byte `mapstructure:"key"` + Keys [][]byte `mapstructure:"keys"` + CipherMode string `mapstructure:"cipher_mode"` + Derived bool `mapstructure:"derived"` + KDFMode string `mapstructure:"kdf_mode"` + Disabled bool `mapstructure:"disabled"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err @@ -114,48 +220,10 @@ func testAccStepReadPolicy(t *testing.T, name string, expectNone, derived bool) if d.Key != nil { return fmt.Errorf("bad: %#v", d) } - if d.Derived != derived { + if d.Keys != nil { return fmt.Errorf("bad: %#v", d) } - if derived && d.KDFMode != kdfMode { - return fmt.Errorf("bad: %#v", d) - } - return nil - }, - } -} - -func testAccStepReadRaw(t *testing.T, name string, expectNone, derived bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "raw/" + name, - Check: func(resp *logical.Response) error { - if resp == nil && !expectNone { - return fmt.Errorf("missing response") - } else if expectNone { - if resp != nil { - return fmt.Errorf("response when expecting none") - } - return nil - } - var d struct { - Name string `mapstructure:"name"` - Key []byte `mapstructure:"key"` - CipherMode string `mapstructure:"cipher_mode"` - Derived bool `mapstructure:"derived"` - KDFMode string `mapstructure:"kdf_mode"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if d.Name != name { - return fmt.Errorf("bad: %#v", d) - } - if d.CipherMode != "aes-gcm" { - return fmt.Errorf("bad: %#v", d) - } - if len(d.Key) != 32 { + if d.Disabled == true { return fmt.Errorf("bad: %#v", d) } if d.Derived != derived { @@ -240,9 +308,125 @@ func testAccStepDecrypt( } if string(plainRaw) != plaintext { - return fmt.Errorf("plaintext mismatch: %s expect: %s", plainRaw, plaintext) + return fmt.Errorf("plaintext mismatch: %s expect: %s, decryptData was %#v", plainRaw, plaintext, decryptData) } return nil }, } } + +func testAccStepRewrap( + t *testing.T, name string, decryptData map[string]interface{}, expectedVer int) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "rewrap/" + name, + Data: decryptData, + Check: func(resp *logical.Response) error { + var d struct { + Ciphertext string `mapstructure:"ciphertext"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + if d.Ciphertext == "" { + return fmt.Errorf("missing ciphertext") + } + splitStrings := strings.Split(d.Ciphertext, ":") + verString := splitStrings[1][1:] + ver, err := strconv.Atoi(verString) + if err != nil { + return fmt.Errorf("Error pulling out version from verString '%s', ciphertext was %s", verString, d.Ciphertext) + } + if ver != expectedVer { + return fmt.Errorf("Did not get expected version") + } + decryptData["ciphertext"] = d.Ciphertext + return nil + }, + } +} + +func testAccStepEncryptVX( + t *testing.T, name, plaintext string, decryptData map[string]interface{}, + ver int, encryptHistory map[int]map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "encrypt/" + name, + Data: map[string]interface{}{ + "plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)), + }, + Check: func(resp *logical.Response) error { + var d struct { + Ciphertext string `mapstructure:"ciphertext"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + if d.Ciphertext == "" { + return fmt.Errorf("missing ciphertext") + } + splitStrings := strings.Split(d.Ciphertext, ":") + splitStrings[1] = "v" + strconv.Itoa(ver) + ciphertext := strings.Join(splitStrings, ":") + decryptData["ciphertext"] = ciphertext + encryptHistory[ver] = map[string]interface{}{ + "ciphertext": ciphertext, + } + return nil + }, + } +} + +func testAccStepLoadVX( + t *testing.T, name string, decryptData map[string]interface{}, + ver int, encryptHistory map[int]map[string]interface{}) logicaltest.TestStep { + // This is really a no-op to allow us to do data manip in the check function + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "keys/" + name, + Check: func(resp *logical.Response) error { + decryptData["ciphertext"] = encryptHistory[ver]["ciphertext"].(string) + return nil + }, + } +} + +func testAccStepDecryptExpectFailure( + t *testing.T, name, plaintext string, decryptData map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "decrypt/" + name, + Data: decryptData, + ErrorOk: true, + Check: func(resp *logical.Response) error { + if !resp.IsError() { + return fmt.Errorf("expected error") + } + return nil + }, + } +} + +func testAccStepRotate(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "keys/" + name + "/rotate", + } +} + +func TestKeyUpgrade(t *testing.T) { + p := &Policy{ + Name: "test", + Key: []byte(testPlaintext), + CipherMode: "aes-gcm", + } + + p.migrateKeyToKeysMap() + + if p.Key != nil || + p.Keys == nil || + len(p.Keys) != 1 || + string(p.Keys[1].Key) != testPlaintext { + t.Errorf("bad key migration, result is %#v", p.Keys) + } +} diff --git a/builtin/logical/transit/path_config.go b/builtin/logical/transit/path_config.go new file mode 100644 index 0000000000..9e64530b05 --- /dev/null +++ b/builtin/logical/transit/path_config.go @@ -0,0 +1,67 @@ +package transit + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfig() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/config", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the key", + }, + + "min_decryption_version": &framework.FieldSchema{ + Type: framework.TypeInt, + Description: `If set, the minimum version of the key allowed +to be decrypted.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathConfigWrite, + }, + + HelpSynopsis: pathConfigHelpSyn, + HelpDescription: pathConfigHelpDesc, + } +} + +func pathConfigWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + // Check if the policy already exists + policy, err := getPolicy(req, name) + if err != nil { + return nil, err + } + if policy == nil { + return logical.ErrorResponse( + fmt.Sprintf("no existing role named %s could be found", name)), + logical.ErrInvalidRequest + } + + minDecryptionVersion := d.Get("min_decryption_version").(int) + if minDecryptionVersion == 0 || + minDecryptionVersion == policy.MinDecryptionVersion { + return nil, nil + } + + policy.MinDecryptionVersion = minDecryptionVersion + + return nil, policy.Persist(req.Storage, name) +} + +const pathConfigHelpSyn = `Configure a named encryption key` + +const pathConfigHelpDesc = ` +This path is used to configure the named key. Currently, this +supports adjusting the minimum version of the key allowed to +be used for decryption via the min_decryption_version paramter. +` diff --git a/builtin/logical/transit/path_decrypt.go b/builtin/logical/transit/path_decrypt.go index 091e1da5d3..8ea94da554 100644 --- a/builtin/logical/transit/path_decrypt.go +++ b/builtin/logical/transit/path_decrypt.go @@ -1,11 +1,10 @@ package transit import ( - "crypto/aes" - "crypto/cipher" "encoding/base64" - "strings" + "fmt" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -42,8 +41,8 @@ func pathDecrypt() *framework.Path { func pathDecryptWrite( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) - value := d.Get("ciphertext").(string) - if len(value) == 0 { + ciphertext := d.Get("ciphertext").(string) + if len(ciphertext) == 0 { return logical.ErrorResponse("missing ciphertext to decrypt"), logical.ErrInvalidRequest } @@ -69,56 +68,26 @@ func pathDecryptWrite( return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest } - // Derive the key that should be used - key, err := p.DeriveKey(context) + plaintext, err := p.Decrypt(context, ciphertext) if err != nil { - return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + case certutil.InternalError: + return nil, err + default: + return nil, err + } } - // Guard against a potentially invalid cipher-mode - switch p.CipherMode { - case "aes-gcm": - default: - return logical.ErrorResponse("unsupported cipher mode"), logical.ErrInvalidRequest - } - - // Verify the prefix - if !strings.HasPrefix(value, "vault:v0:") { - return logical.ErrorResponse("invalid ciphertext"), logical.ErrInvalidRequest - } - - // Decode the base64 - decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(value, "vault:v0:")) - if err != nil { - return logical.ErrorResponse("invalid ciphertext"), logical.ErrInvalidRequest - } - - // Setup the cipher - aesCipher, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - // Setup the GCM AEAD - gcm, err := cipher.NewGCM(aesCipher) - if err != nil { - return nil, err - } - - // Extract the nonce and ciphertext - nonce := decoded[:gcm.NonceSize()] - ciphertext := decoded[gcm.NonceSize():] - - // Verify and Decrypt - plain, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return logical.ErrorResponse("invalid ciphertext"), logical.ErrInvalidRequest + if plaintext == "" { + return nil, fmt.Errorf("empty plaintext returned") } // Generate the response resp := &logical.Response{ Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString(plain), + "plaintext": plaintext, }, } return resp, nil diff --git a/builtin/logical/transit/path_enable_disable.go b/builtin/logical/transit/path_enable_disable.go new file mode 100644 index 0000000000..d936821945 --- /dev/null +++ b/builtin/logical/transit/path_enable_disable.go @@ -0,0 +1,111 @@ +package transit + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathEnable() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/enable", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the key", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathEnableWrite, + }, + + HelpSynopsis: pathEnableHelpSyn, + HelpDescription: pathEnableHelpDesc, + } +} + +func pathDisable() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/disable", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the key", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathDisableWrite, + }, + + HelpSynopsis: pathDisableHelpSyn, + HelpDescription: pathDisableHelpDesc, + } +} + +func pathEnableWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + // Check if the policy already exists + policy, err := getPolicy(req, name) + if err != nil { + return nil, err + } + if policy == nil { + return logical.ErrorResponse( + fmt.Sprintf("no existing role named %s could be found", name)), + logical.ErrInvalidRequest + } + + if !policy.Disabled { + return nil, nil + } + + policy.Disabled = false + + return nil, policy.Persist(req.Storage, name) +} + +func pathDisableWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + // Check if the policy already exists + policy, err := getPolicy(req, name) + if err != nil { + return nil, err + } + if policy == nil { + return logical.ErrorResponse( + fmt.Sprintf("no existing role named %s could be found", name)), + logical.ErrInvalidRequest + } + + if policy.Disabled { + return nil, nil + } + + policy.Disabled = true + + return nil, policy.Persist(req.Storage, name) +} + +const pathEnableHelpSyn = `Enable a named encryption key` + +const pathEnableHelpDesc = ` +This path is used to enable the named key. After enabling, +the key will be available for use for encryption. +` + +const pathDisableHelpSyn = `Disable a named encryption key` + +const pathDisableHelpDesc = ` +This path is used to disable the named key. After disabling, +the key cannot be used to encrypt values. This is useful when +when switching to a new named key, but wanting to be able to +decrypt against old keys while guarding against additional +data being encrypted with the old key. +` diff --git a/builtin/logical/transit/path_encrypt.go b/builtin/logical/transit/path_encrypt.go index add5371e96..822cf63578 100644 --- a/builtin/logical/transit/path_encrypt.go +++ b/builtin/logical/transit/path_encrypt.go @@ -1,12 +1,10 @@ package transit import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" "encoding/base64" "fmt" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -48,10 +46,15 @@ func pathEncryptWrite( return logical.ErrorResponse("missing plaintext to encrypt"), logical.ErrInvalidRequest } - // Decode the plaintext value - plaintext, err := base64.StdEncoding.DecodeString(value) + // Get the policy + p, err := getPolicy(req, name) if err != nil { - return logical.ErrorResponse("failed to decode plaintext as base64"), logical.ErrInvalidRequest + return nil, err + } + + // Fast track disable checking + if p != nil && p.Disabled { + return logical.ErrorResponse("key is disabled and cannot be used for encryption"), logical.ErrInvalidRequest } // Decode the context if any @@ -65,12 +68,6 @@ func pathEncryptWrite( } } - // Get the policy - p, err := getPolicy(req, name) - if err != nil { - return nil, err - } - // Error if invalid policy if p == nil { isDerived := len(context) != 0 @@ -80,54 +77,26 @@ func pathEncryptWrite( } } - // Derive the key that should be used - key, err := p.DeriveKey(context) + ciphertext, err := p.Encrypt(context, value) if err != nil { - return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + case certutil.InternalError: + return nil, err + default: + return nil, err + } } - // Guard against a potentially invalid cipher-mode - switch p.CipherMode { - case "aes-gcm": - default: - return logical.ErrorResponse("unsupported cipher mode"), logical.ErrInvalidRequest + if ciphertext == "" { + return nil, fmt.Errorf("empty ciphertext returned") } - // Setup the cipher - aesCipher, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - // Setup the GCM AEAD - gcm, err := cipher.NewGCM(aesCipher) - if err != nil { - return nil, err - } - - // Compute random nonce - nonce := make([]byte, gcm.NonceSize()) - _, err = rand.Read(nonce) - if err != nil { - return nil, err - } - - // Encrypt and tag with GCM - out := gcm.Seal(nil, nonce, plaintext, nil) - - // Place the encrypted data after the nonce - full := append(nonce, out...) - - // Convert to base64 - encoded := base64.StdEncoding.EncodeToString(full) - - // Prepend some information - encoded = "vault:v0:" + encoded - // Generate the response resp := &logical.Response{ Data: map[string]interface{}{ - "ciphertext": encoded, + "ciphertext": ciphertext, }, } return resp, nil diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index 0b341b7f76..cb12d6677d 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -1,126 +1,12 @@ package transit import ( - "crypto/rand" - "encoding/json" "fmt" - "github.com/hashicorp/vault/helper/kdf" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) -const ( - // kdfMode is the only KDF mode currently supported - kdfMode = "hmac-sha256-counter" -) - -// Policy is the struct used to store metadata -type Policy struct { - Name string `json:"name"` - Key []byte `json:"key"` - CipherMode string `json:"cipher"` - - // Derived keys MUST provide a context and the - // master underlying key is never used. - Derived bool `json:"derived"` - KDFMode string `json:"kdf_mode"` -} - -func (p *Policy) Serialize() ([]byte, error) { - return json.Marshal(p) -} - -// DeriveKey is used to derive the encryption key that should -// be used depending on the policy. If derivation is disabled the -// raw key is used and no context is required, otherwise the KDF -// mode is used with the context to derive the proper key. -func (p *Policy) DeriveKey(context []byte) ([]byte, error) { - // Fast-path non-derived keys - if !p.Derived { - return p.Key, nil - } - - // Ensure a context is provided - if len(context) == 0 { - return nil, fmt.Errorf("missing 'context' for key deriviation. The key was created using a derived key, which means additional, per-request information must be included in order to encrypt or decrypt information.") - } - - switch p.KDFMode { - case kdfMode: - prf := kdf.HMACSHA256PRF - prfLen := kdf.HMACSHA256PRFLen - return kdf.CounterMode(prf, prfLen, p.Key, context, 256) - default: - return nil, fmt.Errorf("unsupported key derivation mode") - } -} - -func DeserializePolicy(buf []byte) (*Policy, error) { - p := new(Policy) - if err := json.Unmarshal(buf, p); err != nil { - return nil, err - } - return p, nil -} - -func getPolicy(req *logical.Request, name string) (*Policy, error) { - // Check if the policy already exists - raw, err := req.Storage.Get("policy/" + name) - if err != nil { - return nil, err - } - if raw == nil { - return nil, nil - } - - // Decode the policy - p, err := DeserializePolicy(raw.Value) - if err != nil { - return nil, err - } - return p, nil -} - -// generatePolicy is used to create a new named policy with -// a randomly generated key -func generatePolicy(storage logical.Storage, name string, derived bool) (*Policy, error) { - // Create the policy object - p := &Policy{ - Name: name, - CipherMode: "aes-gcm", - Derived: derived, - } - if derived { - p.KDFMode = kdfMode - } - - // Generate a 256bit key - p.Key = make([]byte, 32) - _, err := rand.Read(p.Key) - if err != nil { - return nil, err - } - - // Encode the policy - buf, err := p.Serialize() - if err != nil { - return nil, err - } - - // Write the policy into storage - err = storage.Put(&logical.StorageEntry{ - Key: "policy/" + name, - Value: buf, - }) - if err != nil { - return nil, err - } - - // Return the policy - return p, nil -} - func pathKeys() *framework.Path { return &framework.Path{ Pattern: "keys/" + framework.GenericNameRegex("name"), @@ -169,6 +55,7 @@ func pathPolicyWrite( func pathPolicyRead( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) + p, err := getPolicy(req, name) if err != nil { return nil, err @@ -183,6 +70,7 @@ func pathPolicyRead( "name": p.Name, "cipher_mode": p.CipherMode, "derived": p.Derived, + "disabled": p.Disabled, }, } if p.Derived { @@ -195,14 +83,26 @@ func pathPolicyDelete( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) - err := req.Storage.Delete("policy/" + name) + p, err := getPolicy(req, name) + if err != nil { + return nil, err + } + if p == nil { + return logical.ErrorResponse(fmt.Sprintf("no such key %s", name)), logical.ErrInvalidRequest + } + + if !p.Disabled { + return logical.ErrorResponse(fmt.Sprintf("key must be disabled before deletion")), logical.ErrInvalidRequest + } + + err = req.Storage.Delete("policy/" + name) if err != nil { return nil, err } return nil, nil } -const pathPolicyHelpSyn = `Managed named encrption keys` +const pathPolicyHelpSyn = `Managed named encryption keys` const pathPolicyHelpDesc = ` This path is used to manage the named keys that are available. diff --git a/builtin/logical/transit/path_raw.go b/builtin/logical/transit/path_raw.go deleted file mode 100644 index 6c349a20dd..0000000000 --- a/builtin/logical/transit/path_raw.go +++ /dev/null @@ -1,58 +0,0 @@ -package transit - -import ( - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/framework" -) - -func pathRaw() *framework.Path { - return &framework.Path{ - Pattern: "raw/" + framework.GenericNameRegex("name"), - Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Name of the key", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: pathRawRead, - }, - - HelpSynopsis: pathPolicyHelpSyn, - HelpDescription: pathPolicyHelpDesc, - } -} - -func pathRawRead( - req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - p, err := getPolicy(req, name) - if err != nil { - return nil, err - } - if p == nil { - return nil, nil - } - - // Return the response - resp := &logical.Response{ - Data: map[string]interface{}{ - "name": p.Name, - "key": p.Key, - "cipher_mode": p.CipherMode, - "derived": p.Derived, - }, - } - if p.Derived { - resp.Data["kdf_mode"] = p.KDFMode - } - return resp, nil -} - -const pathRawHelpSyn = `Fetch raw keys for named encrption keys` - -const pathRawHelpDesc = ` -This path is used to get the underlying encryption keys used for the -named keys that are available. -` diff --git a/builtin/logical/transit/path_rewrap.go b/builtin/logical/transit/path_rewrap.go new file mode 100644 index 0000000000..d6b48d0356 --- /dev/null +++ b/builtin/logical/transit/path_rewrap.go @@ -0,0 +1,120 @@ +package transit + +import ( + "encoding/base64" + "fmt" + + "github.com/hashicorp/vault/helper/certutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathRewrap() *framework.Path { + return &framework.Path{ + Pattern: "rewrap/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the key", + }, + + "ciphertext": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Ciphertext value to rewrap", + }, + + "context": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Context for key derivation. Required for derived keys.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathRewrapWrite, + }, + + HelpSynopsis: pathRewrapHelpSyn, + HelpDescription: pathRewrapHelpDesc, + } +} + +func pathRewrapWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + value := d.Get("ciphertext").(string) + if len(value) == 0 { + return logical.ErrorResponse("missing ciphertext to decrypt"), logical.ErrInvalidRequest + } + + // 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 + } + + plaintext, err := p.Decrypt(context, value) + 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 plaintext == "" { + return nil, fmt.Errorf("empty plaintext returned during rewrap") + } + + ciphertext, err := p.Encrypt(context, plaintext) + 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, + }, + } + return resp, nil +} + +const pathRewrapHelpSyn = `Rewrap ciphertext` + +const pathRewrapHelpDesc = ` +After key rotation, this function can be used to rewrap the +given ciphertext with the latest version of the named key. +If the given ciphertext is already using the latest version +of the key, this function is a no-op. +` diff --git a/builtin/logical/transit/path_rotate.go b/builtin/logical/transit/path_rotate.go new file mode 100644 index 0000000000..c557dc9278 --- /dev/null +++ b/builtin/logical/transit/path_rotate.go @@ -0,0 +1,56 @@ +package transit + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathRotate() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/rotate", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the key", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathRotateWrite, + }, + + HelpSynopsis: pathRotateHelpSyn, + HelpDescription: pathRotateHelpDesc, + } +} + +func pathRotateWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + // Check if the policy already exists + policy, err := getPolicy(req, name) + if err != nil { + return nil, err + } + if policy == nil { + return logical.ErrorResponse( + fmt.Sprintf("no existing role named %s could be found", name)), + logical.ErrInvalidRequest + } + + // Generate the policy + err = policy.rotate(req.Storage) + + return nil, err +} + +const pathRotateHelpSyn = `Rotate named encryption key` + +const pathRotateHelpDesc = ` +This path is used to rotate the named key. After rotation, +new encryption requests using this name will use the new key, +but decryption will still be supported for older versions. +` diff --git a/builtin/logical/transit/policy.go b/builtin/logical/transit/policy.go new file mode 100644 index 0000000000..2587fbaec3 --- /dev/null +++ b/builtin/logical/transit/policy.go @@ -0,0 +1,360 @@ +package transit + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/json" + "strconv" + "strings" + "time" + + "github.com/hashicorp/vault/helper/certutil" + "github.com/hashicorp/vault/helper/kdf" + "github.com/hashicorp/vault/logical" +) + +const ( + // kdfMode is the only KDF mode currently supported + kdfMode = "hmac-sha256-counter" +) + +// KeyEntry stores the key and metadata +type KeyEntry struct { + Key []byte `json:"key"` + CreationTime int64 `json:"creation_time"` +} + +// KeyEntryMap is used to allow JSON marshal/unmarshal +type KeyEntryMap map[int]KeyEntry + +// MarshalJSON implements JSON marshaling +func (kem KeyEntryMap) MarshalJSON() ([]byte, error) { + intermediate := map[string]KeyEntry{} + for k, v := range kem { + intermediate[strconv.Itoa(k)] = v + } + return json.Marshal(&intermediate) +} + +// MarshalJSON implements JSON unmarshaling +func (kem KeyEntryMap) UnmarshalJSON(data []byte) error { + intermediate := map[string]KeyEntry{} + err := json.Unmarshal(data, &intermediate) + if err != nil { + return err + } + for k, v := range intermediate { + keyval, err := strconv.Atoi(k) + if err != nil { + return err + } + kem[keyval] = v + } + + return nil +} + +// Policy is the struct used to store metadata +type Policy struct { + Name string `json:"name"` + Key []byte `json:"key,omitempty"` //DEPRECATED + Keys KeyEntryMap `json:"keys"` + CipherMode string `json:"cipher"` + + // Derived keys MUST provide a context and the + // master underlying key is never used. + Derived bool `json:"derived"` + KDFMode string `json:"kdf_mode"` + + // Whether the key can be used for encryption + Disabled bool `json:"disabled"` + + // The minimum version of the key allowed to be used + // for decryption + MinDecryptionVersion int `json:"min_decryption_version"` +} + +func (p *Policy) Persist(storage logical.Storage, name string) error { + // Encode the policy + buf, err := p.Serialize() + if err != nil { + return err + } + + // Write the policy into storage + err = storage.Put(&logical.StorageEntry{ + Key: "policy/" + name, + Value: buf, + }) + if err != nil { + return err + } + + return nil +} + +func (p *Policy) Serialize() ([]byte, error) { + return json.Marshal(p) +} + +// DeriveKey is used to derive the encryption key that should +// be used depending on the policy. If derivation is disabled the +// raw key is used and no context is required, otherwise the KDF +// mode is used with the context to derive the proper key. +func (p *Policy) DeriveKey(context []byte, ver int) ([]byte, error) { + if p.Keys == nil || len(p.Keys) == 0 { + if p.Key == nil || len(p.Key) == 0 { + return nil, certutil.InternalError{Err: "unable to access the key; no key versions found"} + } + p.migrateKeyToKeysMap() + } + + if len(p.Keys) == 0 { + return nil, certutil.InternalError{Err: "unable to access the key; no key versions found"} + } + + if ver <= 0 || ver > len(p.Keys) { + return nil, certutil.UserError{Err: "invalid key version"} + } + + // Fast-path non-derived keys + if !p.Derived { + return p.Keys[ver].Key, nil + } + + // Ensure a context is provided + if len(context) == 0 { + return nil, certutil.UserError{Err: "missing 'context' for key deriviation. The key was created using a derived key, which means additional, per-request information must be included in order to encrypt or decrypt information"} + } + + switch p.KDFMode { + case kdfMode: + prf := kdf.HMACSHA256PRF + prfLen := kdf.HMACSHA256PRFLen + return kdf.CounterMode(prf, prfLen, p.Keys[ver].Key, context, 256) + default: + return nil, certutil.InternalError{Err: "unsupported key derivation mode"} + } +} + +func (p *Policy) Encrypt(context []byte, value string) (string, error) { + // Decode the plaintext value + plaintext, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", certutil.UserError{Err: "failed to decode plaintext as base64"} + } + + // Derive the key that should be used + key, err := p.DeriveKey(context, len(p.Keys)) + if err != nil { + return "", certutil.InternalError{Err: err.Error()} + } + + // Guard against a potentially invalid cipher-mode + switch p.CipherMode { + case "aes-gcm": + default: + return "", certutil.InternalError{Err: "unsupported cipher mode"} + } + + // Setup the cipher + aesCipher, err := aes.NewCipher(key) + if err != nil { + return "", certutil.InternalError{Err: err.Error()} + } + + // Setup the GCM AEAD + gcm, err := cipher.NewGCM(aesCipher) + if err != nil { + return "", certutil.InternalError{Err: err.Error()} + } + + // Compute random nonce + nonce := make([]byte, gcm.NonceSize()) + _, err = rand.Read(nonce) + if err != nil { + return "", certutil.InternalError{Err: err.Error()} + } + + // Encrypt and tag with GCM + out := gcm.Seal(nil, nonce, plaintext, nil) + + // Place the encrypted data after the nonce + full := append(nonce, out...) + + // Convert to base64 + encoded := base64.StdEncoding.EncodeToString(full) + + // Prepend some information + encoded = "vault:v" + strconv.Itoa(len(p.Keys)) + ":" + encoded + + return encoded, nil +} + +func (p *Policy) Decrypt(context []byte, value string) (string, error) { + // Verify the prefix + if !strings.HasPrefix(value, "vault:v") { + return "", certutil.UserError{Err: "invalid ciphertext"} + } + + splitVerCiphertext := strings.SplitN(strings.TrimPrefix(value, "vault:v"), ":", 2) + if len(splitVerCiphertext) != 2 { + return "", certutil.UserError{Err: "invalid ciphertext"} + } + + ver, err := strconv.Atoi(splitVerCiphertext[0]) + if err != nil { + return "", certutil.UserError{Err: "invalid ciphertext"} + } + + if ver == 0 { + // Compatibility mode with initial implementation, where keys start at zero + ver = 1 + } + + if p.MinDecryptionVersion > 0 && ver < p.MinDecryptionVersion { + return "", certutil.UserError{Err: "ciphertext version is disallowed by policy (too old)"} + } + + // Derive the key that should be used + key, err := p.DeriveKey(context, ver) + if err != nil { + return "", err + } + + // Guard against a potentially invalid cipher-mode + switch p.CipherMode { + case "aes-gcm": + default: + return "", certutil.InternalError{Err: "unsupported cipher mode"} + } + + // Decode the base64 + decoded, err := base64.StdEncoding.DecodeString(splitVerCiphertext[1]) + if err != nil { + return "", certutil.UserError{Err: "invalid ciphertext"} + } + + // Setup the cipher + aesCipher, err := aes.NewCipher(key) + if err != nil { + return "", certutil.InternalError{Err: err.Error()} + } + + // Setup the GCM AEAD + gcm, err := cipher.NewGCM(aesCipher) + if err != nil { + return "", certutil.InternalError{Err: err.Error()} + } + + // Extract the nonce and ciphertext + nonce := decoded[:gcm.NonceSize()] + ciphertext := decoded[gcm.NonceSize():] + + // Verify and Decrypt + plain, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", certutil.UserError{Err: "invalid ciphertext"} + } + + return base64.StdEncoding.EncodeToString(plain), nil +} + +func (p *Policy) rotate(storage logical.Storage) error { + if p.Keys == nil { + p.migrateKeyToKeysMap() + } + + // Generate a 256bit key + newKey := make([]byte, 32) + _, err := rand.Read(newKey) + if err != nil { + return err + } + p.Keys[len(p.Keys)+1] = KeyEntry{ + Key: newKey, + CreationTime: time.Now().Unix(), + } + + return p.Persist(storage, p.Name) +} + +func (p *Policy) migrateKeyToKeysMap() { + if p.Key == nil || len(p.Key) == 0 { + p.Key = nil + p.Keys = KeyEntryMap{} + return + } + + p.Keys = KeyEntryMap{ + 1: KeyEntry{ + Key: p.Key, + CreationTime: time.Now().Unix(), + }, + } + p.Key = nil +} + +func deserializePolicy(buf []byte) (*Policy, error) { + p := &Policy{ + Keys: KeyEntryMap{}, + } + if err := json.Unmarshal(buf, p); err != nil { + return nil, err + } + + return p, nil +} + +func getPolicy(req *logical.Request, name string) (*Policy, error) { + // Check if the policy already exists + raw, err := req.Storage.Get("policy/" + name) + if err != nil { + return nil, err + } + if raw == nil { + return nil, nil + } + + // Decode the policy + p, err := deserializePolicy(raw.Value) + if err != nil { + return nil, err + } + + // Ensure we've moved from Key -> Keys + if p.Key != nil && len(p.Key) > 0 { + p.migrateKeyToKeysMap() + + err = p.Persist(req.Storage, name) + if err != nil { + return nil, err + } + } + + return p, nil +} + +// generatePolicy is used to create a new named policy with +// a randomly generated key +func generatePolicy(storage logical.Storage, name string, derived bool) (*Policy, error) { + // Create the policy object + p := &Policy{ + Name: name, + CipherMode: "aes-gcm", + Derived: derived, + } + if derived { + p.KDFMode = kdfMode + } + + err := p.rotate(storage) + if err != nil { + return nil, err + } + + // Return the policy + return p, nil +} diff --git a/logical/testing/testing.go b/logical/testing/testing.go index e829f003a3..117ee17a47 100644 --- a/logical/testing/testing.go +++ b/logical/testing/testing.go @@ -209,7 +209,18 @@ func Test(t TestT, c TestCase) { Path: "sys/revoke/" + resp.Secret.LeaseID, }) } - if err == nil && resp.IsError() && !s.ErrorOk { + // If it's an error, but an error is expected, and one is also + // returned as a logical.ErrorResponse, let it go to the check + if err != nil { + if !resp.IsError() || (resp.IsError() && !s.ErrorOk) { + t.Error(fmt.Sprintf("Failed step %d: %s", i+1, err)) + break + } + // Set it to nil here as we're catching on the + // logical.ErrorResponse instead + err = nil + } + if resp.IsError() && !s.ErrorOk { err = fmt.Errorf("Erroneous response:\n\n%#v", resp) } if err == nil && s.Check != nil { diff --git a/website/source/docs/secrets/transit/index.html.md b/website/source/docs/secrets/transit/index.html.md index f177ff1b96..b002a7667a 100644 --- a/website/source/docs/secrets/transit/index.html.md +++ b/website/source/docs/secrets/transit/index.html.md @@ -62,21 +62,6 @@ cipher_mode aes-gcm derived false ```` -We can read from the `raw/` endpoint to see the encryption key itself: - -``` -$ vault read transit/raw/foo -Key Value -name foo -cipher_mode aes-gcm -key PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8= -derived false -```` - -Here we can see that the randomly generated encryption key being used, as -well as the AES-GCM cipher mode. We don't need to know any of this to use -the key however. - Now, if we wanted to encrypt a piece of plain text, we use the encrypt endpoint using our named key: @@ -299,44 +284,3 @@ only encrypt or decrypt using the named keys they need access to. - -### /transit/raw/ -#### GET - -
-
Description
-
- Returns raw information about a named encryption key, - Including the underlying encryption key. This is a root protected endpoint. -
- -
Method
-
GET
- -
URL
-
`/transit/raw/`
- -
Parameters
-
- None -
- -
Returns
-
- - ```javascript - { - "data": { - "name": "foo", - "cipher_mode": "aes-gcm", - "key": "PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8=" - "derived": "true", - "kdf_mode": "hmac-sha256-counter", - } - } - ``` - -
-
- - From 82d1f28fb6588454f099efe356b9880b8b279ed5 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 17 Sep 2015 18:49:50 -0400 Subject: [PATCH 2/4] Remove enable/disable and make deletion_allowed a configurable property. On read, return the version and creation time of each key --- builtin/logical/transit/backend.go | 4 +- builtin/logical/transit/backend_test.go | 59 ++++++---- builtin/logical/transit/path_config.go | 27 ++++- .../logical/transit/path_enable_disable.go | 111 ------------------ builtin/logical/transit/path_encrypt.go | 5 - builtin/logical/transit/path_keys.go | 24 ++-- builtin/logical/transit/policy.go | 6 +- 7 files changed, 77 insertions(+), 159 deletions(-) delete mode 100644 builtin/logical/transit/path_enable_disable.go diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 68a805d66b..1510fbaffa 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -19,10 +19,8 @@ func Backend() *framework.Backend { }, Paths: []*framework.Path{ - // Rotate/Enable/Disable needs to come before Keys + // Rotate/Config needs to come before Keys // as the handler is greedy - pathEnable(), - pathDisable(), pathConfig(), pathRotate(), pathRewrap(), diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index f95c7847fe..ada1fc61f3 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -26,13 +26,13 @@ func TestBackend_basic(t *testing.T) { testAccStepEncrypt(t, "test", testPlaintext, decryptData), testAccStepDecrypt(t, "test", testPlaintext, decryptData), testAccStepDeleteNotDisabledPolicy(t, "test"), - testAccStepDisablePolicy(t, "test"), + testAccStepEnableDeletion(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepWritePolicy(t, "test", false), - testAccStepDisablePolicy(t, "test"), - testAccStepEnablePolicy(t, "test"), + testAccStepEnableDeletion(t, "test"), + testAccStepDisableDeletion(t, "test"), testAccStepDeleteNotDisabledPolicy(t, "test"), - testAccStepDisablePolicy(t, "test"), + testAccStepEnableDeletion(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepReadPolicy(t, "test", true, false), }, @@ -90,7 +90,7 @@ func TestBackend_rotation(t *testing.T) { testAccStepDecrypt(t, "test", testPlaintext, decryptData), testAccStepRewrap(t, "test", decryptData, 4), testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepDisablePolicy(t, "test"), + testAccStepEnableDeletion(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepReadPolicy(t, "test", true, false), }, @@ -106,7 +106,7 @@ func TestBackend_upsert(t *testing.T) { testAccStepEncrypt(t, "test", testPlaintext, decryptData), testAccStepReadPolicy(t, "test", false, false), testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepDisablePolicy(t, "test"), + testAccStepEnableDeletion(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepReadPolicy(t, "test", true, false), }, @@ -122,7 +122,7 @@ func TestBackend_basic_derived(t *testing.T) { testAccStepReadPolicy(t, "test", false, true), testAccStepEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData), testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepDisablePolicy(t, "test"), + testAccStepEnableDeletion(t, "test"), testAccStepDeletePolicy(t, "test"), testAccStepReadPolicy(t, "test", true, true), }, @@ -139,13 +139,6 @@ func testAccStepWritePolicy(t *testing.T, name string, derived bool) logicaltest } } -func testAccStepEnablePolicy(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.WriteOperation, - Path: "keys/" + name + "/enable", - } -} - func testAccStepAdjustPolicy(t *testing.T, name string, minVer int) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.WriteOperation, @@ -156,10 +149,23 @@ func testAccStepAdjustPolicy(t *testing.T, name string, minVer int) logicaltest. } } -func testAccStepDisablePolicy(t *testing.T, name string) logicaltest.TestStep { +func testAccStepDisableDeletion(t *testing.T, name string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "keys/" + name + "/disable", + Path: "keys/" + name + "/config", + Data: map[string]interface{}{ + "deletion_allowed": false, + }, + } +} + +func testAccStepEnableDeletion(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "keys/" + name + "/config", + Data: map[string]interface{}{ + "deletion_allowed": true, + }, } } @@ -176,6 +182,9 @@ func testAccStepDeleteNotDisabledPolicy(t *testing.T, name string) logicaltest.T Path: "keys/" + name, ErrorOk: true, Check: func(resp *logical.Response) error { + if resp == nil { + return fmt.Errorf("Got nil response instead of error") + } if resp.IsError() { return nil } @@ -198,13 +207,13 @@ func testAccStepReadPolicy(t *testing.T, name string, expectNone, derived bool) return nil } var d struct { - Name string `mapstructure:"name"` - Key []byte `mapstructure:"key"` - Keys [][]byte `mapstructure:"keys"` - CipherMode string `mapstructure:"cipher_mode"` - Derived bool `mapstructure:"derived"` - KDFMode string `mapstructure:"kdf_mode"` - Disabled bool `mapstructure:"disabled"` + Name string `mapstructure:"name"` + Key []byte `mapstructure:"key"` + Keys map[string]int64 `mapstructure:"keys"` + CipherMode string `mapstructure:"cipher_mode"` + Derived bool `mapstructure:"derived"` + KDFMode string `mapstructure:"kdf_mode"` + DeletionAllowed bool `mapstructure:"deletion_allowed"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err @@ -220,10 +229,10 @@ func testAccStepReadPolicy(t *testing.T, name string, expectNone, derived bool) if d.Key != nil { return fmt.Errorf("bad: %#v", d) } - if d.Keys != nil { + if d.Keys == nil { return fmt.Errorf("bad: %#v", d) } - if d.Disabled == true { + if d.DeletionAllowed == true { return fmt.Errorf("bad: %#v", d) } if d.Derived != derived { diff --git a/builtin/logical/transit/path_config.go b/builtin/logical/transit/path_config.go index 9e64530b05..d368c64b9a 100644 --- a/builtin/logical/transit/path_config.go +++ b/builtin/logical/transit/path_config.go @@ -21,6 +21,11 @@ func pathConfig() *framework.Path { Description: `If set, the minimum version of the key allowed to be decrypted.`, }, + + "deletion_allowed": &framework.FieldSchema{ + Type: framework.TypeBool, + Description: "Whether to allow deletion of the key", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -47,13 +52,27 @@ func pathConfigWrite( logical.ErrInvalidRequest } + persistNeeded := false + minDecryptionVersion := d.Get("min_decryption_version").(int) - if minDecryptionVersion == 0 || - minDecryptionVersion == policy.MinDecryptionVersion { - return nil, nil + if minDecryptionVersion != 0 && + minDecryptionVersion != policy.MinDecryptionVersion { + policy.MinDecryptionVersion = minDecryptionVersion + persistNeeded = true } - policy.MinDecryptionVersion = minDecryptionVersion + allowDeletionInt, ok := d.GetOk("deletion_allowed") + if ok { + allowDeletion := allowDeletionInt.(bool) + if allowDeletion != policy.DeletionAllowed { + policy.DeletionAllowed = allowDeletion + persistNeeded = true + } + } + + if !persistNeeded { + return nil, nil + } return nil, policy.Persist(req.Storage, name) } diff --git a/builtin/logical/transit/path_enable_disable.go b/builtin/logical/transit/path_enable_disable.go deleted file mode 100644 index d936821945..0000000000 --- a/builtin/logical/transit/path_enable_disable.go +++ /dev/null @@ -1,111 +0,0 @@ -package transit - -import ( - "fmt" - - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/framework" -) - -func pathEnable() *framework.Path { - return &framework.Path{ - Pattern: "keys/" + framework.GenericNameRegex("name") + "/enable", - Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Name of the key", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.WriteOperation: pathEnableWrite, - }, - - HelpSynopsis: pathEnableHelpSyn, - HelpDescription: pathEnableHelpDesc, - } -} - -func pathDisable() *framework.Path { - return &framework.Path{ - Pattern: "keys/" + framework.GenericNameRegex("name") + "/disable", - Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Name of the key", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.WriteOperation: pathDisableWrite, - }, - - HelpSynopsis: pathDisableHelpSyn, - HelpDescription: pathDisableHelpDesc, - } -} - -func pathEnableWrite( - req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - - // Check if the policy already exists - policy, err := getPolicy(req, name) - if err != nil { - return nil, err - } - if policy == nil { - return logical.ErrorResponse( - fmt.Sprintf("no existing role named %s could be found", name)), - logical.ErrInvalidRequest - } - - if !policy.Disabled { - return nil, nil - } - - policy.Disabled = false - - return nil, policy.Persist(req.Storage, name) -} - -func pathDisableWrite( - req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - - // Check if the policy already exists - policy, err := getPolicy(req, name) - if err != nil { - return nil, err - } - if policy == nil { - return logical.ErrorResponse( - fmt.Sprintf("no existing role named %s could be found", name)), - logical.ErrInvalidRequest - } - - if policy.Disabled { - return nil, nil - } - - policy.Disabled = true - - return nil, policy.Persist(req.Storage, name) -} - -const pathEnableHelpSyn = `Enable a named encryption key` - -const pathEnableHelpDesc = ` -This path is used to enable the named key. After enabling, -the key will be available for use for encryption. -` - -const pathDisableHelpSyn = `Disable a named encryption key` - -const pathDisableHelpDesc = ` -This path is used to disable the named key. After disabling, -the key cannot be used to encrypt values. This is useful when -when switching to a new named key, but wanting to be able to -decrypt against old keys while guarding against additional -data being encrypted with the old key. -` diff --git a/builtin/logical/transit/path_encrypt.go b/builtin/logical/transit/path_encrypt.go index 822cf63578..7a402292dd 100644 --- a/builtin/logical/transit/path_encrypt.go +++ b/builtin/logical/transit/path_encrypt.go @@ -52,11 +52,6 @@ func pathEncryptWrite( return nil, err } - // Fast track disable checking - if p != nil && p.Disabled { - return logical.ErrorResponse("key is disabled and cannot be used for encryption"), logical.ErrInvalidRequest - } - // Decode the context if any contextRaw := d.Get("context").(string) var context []byte diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index cb12d6677d..7c6d1d1573 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -2,6 +2,7 @@ package transit import ( "fmt" + "strconv" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -67,15 +68,22 @@ func pathPolicyRead( // Return the response resp := &logical.Response{ Data: map[string]interface{}{ - "name": p.Name, - "cipher_mode": p.CipherMode, - "derived": p.Derived, - "disabled": p.Disabled, + "name": p.Name, + "cipher_mode": p.CipherMode, + "derived": p.Derived, + "deletion_allowed": p.DeletionAllowed, }, } if p.Derived { resp.Data["kdf_mode"] = p.KDFMode } + + retKeys := map[string]int64{} + for k, v := range p.Keys { + retKeys[strconv.Itoa(k)] = v.CreationTime + } + resp.Data["keys"] = retKeys + return resp, nil } @@ -85,19 +93,19 @@ func pathPolicyDelete( p, err := getPolicy(req, name) if err != nil { - return nil, err + return logical.ErrorResponse(fmt.Sprintf("error looking up policy %s, error is %s", name, err)), err } if p == nil { return logical.ErrorResponse(fmt.Sprintf("no such key %s", name)), logical.ErrInvalidRequest } - if !p.Disabled { - return logical.ErrorResponse(fmt.Sprintf("key must be disabled before deletion")), logical.ErrInvalidRequest + if !p.DeletionAllowed { + return logical.ErrorResponse(fmt.Sprintf("'allow_deletion' config value is not set")), logical.ErrInvalidRequest } err = req.Storage.Delete("policy/" + name) if err != nil { - return nil, err + return logical.ErrorResponse(fmt.Sprintf("error deleting policy %s: %s", name, err)), err } return nil, nil } diff --git a/builtin/logical/transit/policy.go b/builtin/logical/transit/policy.go index 2587fbaec3..fbda93111e 100644 --- a/builtin/logical/transit/policy.go +++ b/builtin/logical/transit/policy.go @@ -68,12 +68,12 @@ type Policy struct { Derived bool `json:"derived"` KDFMode string `json:"kdf_mode"` - // Whether the key can be used for encryption - Disabled bool `json:"disabled"` - // The minimum version of the key allowed to be used // for decryption MinDecryptionVersion int `json:"min_decryption_version"` + + // Whether the key is allowed to be deleted + DeletionAllowed bool `json:"deletion_allowed"` } func (p *Policy) Persist(storage logical.Storage, name string) error { From b8fe46017024ed9c5985f909973a9ed2c88087e9 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 18 Sep 2015 09:50:53 -0400 Subject: [PATCH 3/4] 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. --- builtin/logical/transit/backend.go | 1 + builtin/logical/transit/backend_test.go | 80 +++++++++++++++ builtin/logical/transit/path_datakey.go | 129 ++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 builtin/logical/transit/path_datakey.go diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 1510fbaffa..9ff64896c7 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -27,6 +27,7 @@ func Backend() *framework.Backend { pathKeys(), pathEncrypt(), pathDecrypt(), + pathDatakey(), }, Secrets: []*framework.Secret{}, diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index ada1fc61f3..c43eac3875 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -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", diff --git a/builtin/logical/transit/path_datakey.go b/builtin/logical/transit/path_datakey.go new file mode 100644 index 0000000000..28715415c7 --- /dev/null +++ b/builtin/logical/transit/path_datakey.go @@ -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. +` From fa6cbba286f2cac7502d541cf3ec50e190ee485b Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 18 Sep 2015 14:40:06 -0400 Subject: [PATCH 4/4] Move no_plaintext to two separate paths for datakey. --- builtin/logical/transit/backend_test.go | 5 ++-- builtin/logical/transit/path_datakey.go | 37 +++++++++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index c43eac3875..5081644a24 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -441,15 +441,16 @@ func testAccStepWriteDatakey(t *testing.T, name string, noPlaintext bool, bits int, dataKeyInfo map[string]interface{}) logicaltest.TestStep { data := map[string]interface{}{} + subPath := "plaintext" if noPlaintext { - data["no_plaintext"] = true + subPath = "wrapped" } if bits != 256 { data["bits"] = bits } return logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "datakey/" + name, + Path: "datakey/" + subPath + "/" + name, Data: data, Check: func(resp *logical.Response) error { var d struct { diff --git a/builtin/logical/transit/path_datakey.go b/builtin/logical/transit/path_datakey.go index 28715415c7..43652733a3 100644 --- a/builtin/logical/transit/path_datakey.go +++ b/builtin/logical/transit/path_datakey.go @@ -12,13 +12,19 @@ import ( func pathDatakey() *framework.Path { return &framework.Path{ - Pattern: "datakey/" + framework.GenericNameRegex("name"), + Pattern: "datakey/" + framework.GenericNameRegex("plaintext") + "/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ "name": &framework.FieldSchema{ Type: framework.TypeString, Description: "The backend key used for encrypting the data key", }, + "plaintext": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `"plaintext" will return the key in both plaintext and +ciphertext; "wrapped" will return the ciphertext only.`, + }, + "context": &framework.FieldSchema{ Type: framework.TypeString, Description: "Context for key derivation. Required for derived keys.", @@ -30,11 +36,6 @@ func pathDatakey() *framework.Path { 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{ @@ -50,6 +51,16 @@ func pathDatakeyWrite( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) + plaintext := d.Get("plaintext").(string) + plaintextAllowed := false + switch plaintext { + case "plaintext": + plaintextAllowed = true + case "wrapped": + default: + return logical.ErrorResponse("Invalid path, must be 'plaintext' or 'wrapped'"), logical.ErrInvalidRequest + } + // Decode the context if any contextRaw := d.Get("context").(string) var context []byte @@ -75,6 +86,8 @@ func pathDatakeyWrite( newKey := make([]byte, 32) bits := d.Get("bits").(int) switch bits { + case 512: + newKey = make([]byte, 64) case 256: case 128: newKey = make([]byte, 16) @@ -109,7 +122,7 @@ func pathDatakeyWrite( }, } - if !d.Get("no_plaintext").(bool) { + if plaintextAllowed { resp.Data["plaintext"] = base64.StdEncoding.EncodeToString(newKey) } @@ -121,9 +134,9 @@ 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. +and decryption, protected by the named backend key. 128, 256, +or 512 bits can be specified; if not specified, the default +is 256 bits. Call with the the "wrapped" path to prevent the +(base64-encoded) plaintext key from being returned along with +the encrypted key, the "plaintext" path returns both. `