Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Transit: Key Trim #5388

Merged
merged 14 commits into from
Oct 17, 2018
62 changes: 60 additions & 2 deletions builtin/logical/transit/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ to be used for signing. If set to zero, only
the latest version of the key is allowed.`,
},

"trimmed_min_version": &framework.FieldSchema{
Type: framework.TypeInt,
Description: `If set, permanently deletes all the key versions before the given version. This should always be greater than both 'min_decryption_version' and 'min_encryption_version'. This is not allowed to be set when either 'min_encryption_version' or 'min_decryption_version' is set to zero.`,
},

"deletion_allowed": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Whether to allow deletion of the key",
Expand All @@ -58,7 +63,7 @@ the latest version of the key is allowed.`,
}
}

func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (resp *logical.Response, retErr error) {
name := d.Get("name").(string)

// Check if the policy already exists before we lock everything
Expand All @@ -79,7 +84,25 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *
}
defer p.Unlock()

resp := &logical.Response{}
originalMinDecryptionVersion := p.MinDecryptionVersion
originalMinEncryptionVersion := p.MinEncryptionVersion
originalTrimmedMinVersion := p.TrimmedMinVersion
originalDeletionAllowed := p.DeletionAllowed
originalExportable := p.Exportable
originalAllowPlaintextBackup := p.AllowPlaintextBackup

defer func() {
Copy link
Contributor

@kalafut kalafut Sep 25, 2018

Choose a reason for hiding this comment

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

I guess the intent is that p.Persist in the return restores the old values. Does this defer approach work? Won't the updates to p take place after the Persist call runs? Or did you intend to call Persist in this function? Maybe one of the test case demonstrates this but it wasn't obvious which one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The policy is responded from the cache. The pointer to the policy is being modified in this function. Even if this function errors out, the policy in the cache gets modified accidentally. This is even before the p.Persist is being called. p.Persist however is already doing the right thing. So, this function is only trying to restore values in policy that it is touching.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, didn't notice that p is pointing into the cache. Makes sense then.

if retErr != nil || (resp != nil && resp.IsError()) {
p.MinDecryptionVersion = originalMinDecryptionVersion
p.MinEncryptionVersion = originalMinEncryptionVersion
p.TrimmedMinVersion = originalTrimmedMinVersion
p.DeletionAllowed = originalDeletionAllowed
p.Exportable = originalExportable
p.AllowPlaintextBackup = originalAllowPlaintextBackup
}
}()

resp = &logical.Response{}

persistNeeded := false

Expand Down Expand Up @@ -124,6 +147,31 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *
}
}

trimmedMinVersionRaw, ok := d.GetOk("trimmed_min_version")
if ok {
trimmedMinVersion := trimmedMinVersionRaw.(int)

switch {
case p.MinEncryptionVersion == 0:
return logical.ErrorResponse("trimmed min version cannot be set when min encryption version is not set"), nil
case p.MinDecryptionVersion == 0:
return logical.ErrorResponse("trimmed min version cannot be set when min decryption version is not set"), nil
case trimmedMinVersion > p.MinEncryptionVersion:
return logical.ErrorResponse("trimmed min version cannot be greater than min encryption version"), nil
case trimmedMinVersion > p.MinDecryptionVersion:
return logical.ErrorResponse("trimmed min version cannot be greater than min decryption version"), nil
case trimmedMinVersion < 0:
return logical.ErrorResponse("trimmed min version cannot be negative"), nil
case trimmedMinVersion == 0:
return logical.ErrorResponse("trimmed min version should be positive"), nil
case trimmedMinVersion < p.TrimmedMinVersion:
return logical.ErrorResponse(fmt.Sprintf("trimmed min version cannot be less than the already set value of %d", p.TrimmedMinVersion)), nil
}

p.TrimmedMinVersion = trimmedMinVersion
persistNeeded = true
}

// Check here to get the final picture after the logic on each
// individually. MinDecryptionVersion will always be 1 or above.
if p.MinEncryptionVersion > 0 &&
Expand Down Expand Up @@ -173,6 +221,16 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *
return nil, nil
}

// Do these checks after 'min_encryption_version', 'min_decryption_version'
// and 'trim_before_version' fields are processed. These are applicable
// even if one of them is being modified.
switch {
case p.TrimmedMinVersion > p.MinEncryptionVersion:
return logical.ErrorResponse("min encryption version should not be less than trimmed min version"), nil
case p.TrimmedMinVersion > p.MinDecryptionVersion:
return logical.ErrorResponse("min decryption version should not be less then trimmed min version"), nil
}

if len(resp.Warnings) == 0 {
return nil, p.Persist(ctx, req.Storage)
}
Expand Down
251 changes: 250 additions & 1 deletion builtin/logical/transit/path_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,264 @@ import (
"strings"
"testing"

"github.com/hashicorp/vault/helper/keysutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/logical"
)

func TestTransit_ConfigTrimmedMinVersion(t *testing.T) {
b, storage := createBackendWithSysView(t)

doReq := func(t *testing.T, req *logical.Request) *logical.Response {
t.Helper()
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("got err:\n%#v\nresp:\n%#v\n", err, resp)
}
return resp
}
doErrReq := func(t *testing.T, req *logical.Request) {
t.Helper()
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err == nil && (resp == nil || !resp.IsError()) {
t.Fatalf("expected error; resp:\n%#v\n", resp)
}
}

// Create a key
req := &logical.Request{
Path: "keys/aes",
Storage: storage,
Operation: logical.UpdateOperation,
}
doReq(t, req)

// Get the policy and check that the archive has correct number of keys
p, _, err := b.lm.GetPolicy(namespace.RootContext(nil), keysutil.PolicyRequest{
Storage: storage,
Name: "aes",
})
if err != nil {
t.Fatal(err)
}

// Archive: 0, 1
archive, err := p.LoadArchive(namespace.RootContext(nil), storage)
if err != nil {
t.Fatal(err)
}
// Index "0" in the archive is unused. Hence the length of the archived
// keys will always be 1 more than the actual number of keys.
if len(archive.Keys) != 2 {
t.Fatalf("bad: len of archived keys; expected: 2, actual: %d", len(archive.Keys))
}

// Ensure that there are 5 key versions, by rotating the key 4 times
for i := 0; i < 4; i++ {
req.Path = "keys/aes/rotate"
req.Data = nil
doReq(t, req)
}

// Archive: 0, 1, 2, 3, 4, 5
archive, err = p.LoadArchive(namespace.RootContext(nil), storage)
if err != nil {
t.Fatal(err)
}
if len(archive.Keys) != 6 {
t.Fatalf("bad: len of archived keys; expected: 6, actual: %d", len(archive.Keys))
}

// Trimmed min version should not be set when min_encryption_version is not
// set
req.Path = "keys/aes/config"
req.Data = map[string]interface{}{
"trimmed_min_version": 1,
}
doErrReq(t, req)

// Set min_encryption_version to 4
req.Data = map[string]interface{}{
"min_encryption_version": 4,
}
doReq(t, req)

// Set min_decryption_version to 3
req.Data = map[string]interface{}{
"min_decryption_version": 3,
}
doReq(t, req)

// Trimmed min version cannot be greater than min encryption version
req.Data = map[string]interface{}{
"trimmed_min_version": 5,
}
doErrReq(t, req)

// Trimmed min version cannot be greater than min decryption version
req.Data["trimmed_min_version"] = 4
doErrReq(t, req)

// Trimmed min version cannot be negative
req.Data["trimmed_min_version"] = -1
req.Data = map[string]interface{}{
"trimmed_min_version": -1,
}
doErrReq(t, req)

// Trimmed min version should be positive
req.Data["trimmed_min_version"] = 0
doErrReq(t, req)

// Trim all keys before version 3. Index 0 and index 1 will be deleted from
// archived keys.
req.Data["trimmed_min_version"] = 3
doReq(t, req)

// Archive: 3, 4, 5
archive, err = p.LoadArchive(namespace.RootContext(nil), storage)
if err != nil {
t.Fatal(err)
}
if len(archive.Keys) != 3 {
t.Fatalf("bad: len of archived keys; expected: 3, actual: %d", len(archive.Keys))
}

// Min decryption version should not be less than trimmed min version
req.Data = map[string]interface{}{
"min_decryption_version": 1,
}
doErrReq(t, req)

// Min encryption version should not be less than trimmed min version
req.Data = map[string]interface{}{
"min_encryption_version": 2,
}
doErrReq(t, req)

// Rotate 5 more times
for i := 0; i < 5; i++ {
doReq(t, &logical.Request{
Path: "keys/aes/rotate",
Storage: storage,
Operation: logical.UpdateOperation,
})
}

// Archive: 3, 4, 5, 6, 7, 8, 9, 10
archive, err = p.LoadArchive(namespace.RootContext(nil), storage)
if err != nil {
t.Fatal(err)
}
if len(archive.Keys) != 8 {
t.Fatalf("bad: len of archived keys; expected: 8, actual: %d", len(archive.Keys))
}

// Set min encryption version to 7
req.Path = "keys/aes/config"
req.Data = map[string]interface{}{
"min_encryption_version": 7,
}
doReq(t, req)

// Set min decryption version to 7
req.Data = map[string]interface{}{
"min_decryption_version": 7,
}
doReq(t, req)

// Trimmed all versions before 7
req.Data = map[string]interface{}{
"trimmed_min_version": 7,
}
doReq(t, req)

// Archive: 7, 8, 9, 10
archive, err = p.LoadArchive(namespace.RootContext(nil), storage)
if err != nil {
t.Fatal(err)
}
if len(archive.Keys) != 4 {
t.Fatalf("bad: len of archived keys; expected: 4, actual: %d", len(archive.Keys))
}

// Read the key
req.Path = "keys/aes"
req.Operation = logical.ReadOperation
resp := doReq(t, req)
keys := resp.Data["keys"].(map[string]int64)
if len(keys) != 4 {
t.Fatalf("bad: number of keys; expected: 4, actual: %d", len(keys))
}

// Test if moving the min_encryption_version and min_decryption_versions
// are working fine

// Set min encryption version to 10
req.Path = "keys/aes/config"
req.Operation = logical.UpdateOperation
req.Data = map[string]interface{}{
"min_encryption_version": 10,
}
doReq(t, req)
if p.MinEncryptionVersion != 10 {
t.Fatalf("failed to set min encryption version")
}

// Set min decryption version to 9
req.Data = map[string]interface{}{
"min_decryption_version": 9,
}
doReq(t, req)
if p.MinDecryptionVersion != 9 {
t.Fatalf("failed to set min encryption version")
}

// Reduce the min decryption version to 8
req.Data = map[string]interface{}{
"min_decryption_version": 8,
}
doReq(t, req)
if p.MinDecryptionVersion != 8 {
t.Fatalf("failed to set min encryption version")
}

// Reduce the min encryption version to 8
req.Data = map[string]interface{}{
"min_encryption_version": 8,
}
doReq(t, req)
if p.MinDecryptionVersion != 8 {
t.Fatalf("failed to set min decryption version")
}
catsby marked this conversation as resolved.
Show resolved Hide resolved

// Read the key to ensure that the keys are properly copied from the
// archive into the policy
req.Path = "keys/aes"
req.Operation = logical.ReadOperation
resp = doReq(t, req)
keys = resp.Data["keys"].(map[string]int64)
if len(keys) != 3 {
t.Fatalf("bad: number of keys; expected: 3, actual: %d", len(keys))
}

// Ensure that archive has remained unchanged
// Archive: 7, 8, 9, 10
archive, err = p.LoadArchive(namespace.RootContext(nil), storage)
if err != nil {
t.Fatal(err)
}
if len(archive.Keys) != 4 {
t.Fatalf("bad: len of archived keys; expected: 4, actual: %d", len(archive.Keys))
}
}

func TestTransit_ConfigSettings(t *testing.T) {
b, storage := createBackendWithSysView(t)

doReq := func(req *logical.Request) *logical.Response {
resp, err := b.HandleRequest(context.Background(), req)
if err != nil {
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("got err:\n%#v\nreq:\n%#v\n", err, *req)
}
return resp
Expand Down
1 change: 1 addition & 0 deletions builtin/logical/transit/path_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f
"deletion_allowed": p.DeletionAllowed,
"min_decryption_version": p.MinDecryptionVersion,
"min_encryption_version": p.MinEncryptionVersion,
"trimmed_min_version": p.TrimmedMinVersion,
"latest_version": p.LatestVersion,
"exportable": p.Exportable,
"allow_plaintext_backup": p.AllowPlaintextBackup,
Expand Down
Loading