2022-11-16 17:17:28 -06:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2023-11-28 16:25:26 -05:00
package base_plus_test
2022-11-16 17:17:28 -06:00
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/testing/internal/e2e"
"github.com/hashicorp/boundary/testing/internal/e2e/boundary"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
2022-12-05 18:25:12 -05:00
// TestCliKeyDestruction uses the boundary CLI to test key destruction.
func TestCliKeyDestruction ( t * testing . T ) {
2022-11-16 17:17:28 -06:00
e2e . MaybeSkipTest ( t )
ctx := context . Background ()
boundary . AuthenticateAdminCli ( t , ctx )
2022-12-05 18:25:12 -05:00
t . Log ( "Creating scope..." )
2022-11-16 17:17:28 -06:00
output := e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "create" ,
"-name" , "testscope" ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
var scopeReply scopes . ScopeCreateResult
require . NoError ( t , json . Unmarshal ( output . Stdout , & scopeReply ))
t . Cleanup ( func () {
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "delete" ,
"-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
})
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "list-keys" ,
"-scope-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
var keys scopes . KeyListResult
err := json . Unmarshal ( output . Stdout , & keys )
require . NoError ( t , err )
// We expect to see all DEKs and the root key, so DEKs+1 in total.
assert . Len ( t , keys . Items , len ( kms . ValidDekPurposes ()) + 1 , "Expected %d keys to be listed, but found %d. Keys: %v" , len ( kms . ValidDekPurposes ()) + 1 , len ( keys . Items ), keys . Items )
for _ , key := range keys . Items {
assert . Len ( t , key . Versions , 1 )
}
// Create OIDC auth method to create some encrypted data in the scope
2022-12-05 18:25:12 -05:00
t . Log ( "Creating auth method..." )
2022-11-16 17:17:28 -06:00
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"auth-methods" , "create" , "oidc" ,
"-scope-id" , scopeReply . Item . Id ,
"-api-url-prefix" , "http://example.com" ,
"-client-id" , "something" ,
"-client-secret" , "somethingelse" ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
var amReply authmethods . AuthMethodCreateResult
require . NoError ( t , json . Unmarshal ( output . Stdout , & amReply ))
t . Cleanup ( func () {
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"auth-methods" , "delete" ,
"-id" , amReply . Item . Id ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
})
2022-12-05 18:25:12 -05:00
t . Log ( "Rotating keys..." )
2022-11-16 17:17:28 -06:00
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "rotate-keys" ,
"-scope-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "list-keys" ,
"-scope-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
err = json . Unmarshal ( output . Stdout , & keys )
require . NoError ( t , err )
// We expect to see all DEKs and the root key, so DEKs+1 in total.
assert . Len ( t , keys . Items , len ( kms . ValidDekPurposes ()) + 1 , "Expected %d keys to be listed, but found %d. Keys: %v" , len ( kms . ValidDekPurposes ()) + 1 , len ( keys . Items ), keys . Items )
var rootKeyVersionToDestroy * scopes . KeyVersion
var databaseKeyVersionToDestroy * scopes . KeyVersion
for _ , key := range keys . Items {
// Each key should have a new version after rotation
assert . Len ( t , key . Versions , 2 )
if key . Purpose == "rootKey" {
rootKeyVersionToDestroy = key . Versions [ 1 ] // Key versions are ordered by version, descending
}
if key . Purpose == "database" {
databaseKeyVersionToDestroy = key . Versions [ 1 ]
}
}
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "list-key-version-destruction-jobs" ,
"-scope-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
var jobs scopes . KeyVersionDestructionJobListResult
err = json . Unmarshal ( output . Stdout , & jobs )
require . NoError ( t , err )
assert . Len ( t , jobs . Items , 0 )
2022-12-05 18:25:12 -05:00
t . Log ( "Destroying root key..." )
2022-11-16 17:17:28 -06:00
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "destroy-key-version" ,
"-scope-id" , scopeReply . Item . Id ,
"-key-version-id" , rootKeyVersionToDestroy . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
var destruction struct {
Item * scopes . KeyVersionDestructionResult
}
err = json . Unmarshal ( output . Stdout , & destruction )
require . NoError ( t , err )
// Root key versions should be destroyed synchronously, as they don't require any re-encryption
assert . Equal ( t , "completed" , destruction . Item . State )
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "list-key-version-destruction-jobs" ,
"-scope-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
err = json . Unmarshal ( output . Stdout , & jobs )
require . NoError ( t , err )
assert . Len ( t , jobs . Items , 0 )
2022-12-05 18:25:12 -05:00
t . Log ( "Destroying database key..." )
2022-11-16 17:17:28 -06:00
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "destroy-key-version" ,
"-scope-id" , scopeReply . Item . Id ,
"-key-version-id" , databaseKeyVersionToDestroy . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
err = json . Unmarshal ( output . Stdout , & destruction )
require . NoError ( t , err )
// Most data key versions should be destroyed asynchronously, as they require re-encryption
// of existing data.
assert . Equal ( t , "pending" , destruction . Item . State )
// The default scheduler job run interval is 1 minute. The runs become available 1
// second after the last successful run. We need to re-encrypt data in 1 table,
// and then remove the key. This job should take between 1 and 2 minutes to run,
// depending on the timing of the first started run.
cancelCtx , cancel := context . WithTimeout ( ctx , 3 * time . Minute )
t . Cleanup ( cancel )
for {
output = e2e . RunCommand ( cancelCtx , "boundary" ,
e2e . WithArgs (
"scopes" , "list-key-version-destruction-jobs" ,
"-scope-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
err = json . Unmarshal ( output . Stdout , & jobs )
require . NoError ( t , err )
if len ( jobs . Items ) == 0 {
break
}
require . Len ( t , jobs . Items , 1 )
assert . Equal ( t , databaseKeyVersionToDestroy . Id , jobs . Items [ 0 ]. KeyVersionId )
select {
case <- time . After ( 5 * time . Second ):
case <- cancelCtx . Done ():
t . Fatal ( "Test timed out waiting for destruction to finish" )
}
}
output = e2e . RunCommand ( ctx , "boundary" ,
e2e . WithArgs (
"scopes" , "list-keys" ,
"-scope-id" , scopeReply . Item . Id ,
"-format" , "json" ,
),
)
require . NoError ( t , output . Err , string ( output . Stderr ))
err = json . Unmarshal ( output . Stdout , & keys )
require . NoError ( t , err )
// We expect to see all DEKs and the root key, so DEKs+1 in total.
assert . Len ( t , keys . Items , len ( kms . ValidDekPurposes ()) + 1 , "Expected %d keys to be listed, but found %d. Keys: %v" , len ( kms . ValidDekPurposes ()) + 1 , len ( keys . Items ), keys . Items )
for _ , key := range keys . Items {
switch key . Purpose {
case "rootKey" , "database" :
assert . Len ( t , key . Versions , 1 )
default :
assert . Len ( t , key . Versions , 2 )
}
}
2024-03-27 09:49:06 -04:00
t . Log ( "Successfully destroyed a root key and data key" )
2022-11-16 17:17:28 -06:00
}
2022-12-05 18:25:12 -05:00
// TestApiKeyDestruction uses the boundary Go api to test key destruction.
func TestApiKeyDestruction ( t * testing . T ) {
2022-11-16 17:17:28 -06:00
e2e . MaybeSkipTest ( t )
client , err := boundary . NewApiClient ()
require . NoError ( t , err )
ctx := context . Background ()
sc := scopes . NewClient ( client )
2022-12-05 18:25:12 -05:00
t . Log ( "Creating scope..." )
2022-11-16 17:17:28 -06:00
scope , err := sc . Create ( ctx , "global" , scopes . WithName ( "testscope" ))
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err := sc . Delete ( context . Background (), scope . Item . Id )
require . NoError ( t , err )
})
keys , err := sc . ListKeys ( ctx , scope . Item . Id )
require . NoError ( t , err )
// We expect to see all DEKs and the root key, so DEKs+1 in total.
assert . Len ( t , keys . Items , len ( kms . ValidDekPurposes ()) + 1 , "Expected %d keys to be listed, but found %d. Keys: %v" , len ( kms . ValidDekPurposes ()) + 1 , len ( keys . Items ), keys . Items )
// Assert that all keys have the same number of versions
for _ , key := range keys . Items {
assert . Len ( t , key . Versions , 1 )
}
// Create OIDC auth method to create some encrypted data in the scope
2022-12-05 18:25:12 -05:00
t . Log ( "Creating auth method..." )
2022-11-16 17:17:28 -06:00
amc := authmethods . NewClient ( client )
am , err := amc . Create ( ctx , "oidc" , scope . Item . Id ,
authmethods . WithOidcAuthMethodApiUrlPrefix ( "http://example.com" ),
authmethods . WithOidcAuthMethodClientId ( "something" ),
authmethods . WithOidcAuthMethodClientSecret ( "somethingelse" ),
)
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err := amc . Delete ( context . Background (), am . Item . Id )
require . NoError ( t , err )
})
2022-12-05 18:25:12 -05:00
t . Log ( "Rotating keys..." )
2022-11-16 17:17:28 -06:00
_ , err = sc . RotateKeys ( ctx , scope . Item . Id , false )
require . NoError ( t , err )
keys , err = sc . ListKeys ( ctx , scope . Item . Id )
require . NoError ( t , err )
// We expect to see all DEKs and the root key, so DEKs+1 in total.
assert . Len ( t , keys . Items , len ( kms . ValidDekPurposes ()) + 1 , "Expected %d keys to be listed, but found %d. Keys: %v" , len ( kms . ValidDekPurposes ()) + 1 , len ( keys . Items ), keys . Items )
// Assert that all keys have the same number of versions,
// one higher than before
var rootKeyVersionToDestroy * scopes . KeyVersion
var databaseKeyVersionToDestroy * scopes . KeyVersion
for _ , key := range keys . Items {
// Each key should have a new version after rotation
assert . Len ( t , key . Versions , 2 )
if key . Purpose == "rootKey" {
// Key versions are ordered by version, descending.
// Pick the second newest version.
rootKeyVersionToDestroy = key . Versions [ 1 ]
}
if key . Purpose == "database" {
databaseKeyVersionToDestroy = key . Versions [ 1 ]
}
}
jobs , err := sc . ListKeyVersionDestructionJobs ( ctx , scope . Item . Id )
require . NoError ( t , err )
assert . Len ( t , jobs . Items , 0 )
2022-12-05 18:25:12 -05:00
t . Log ( "Destroying root key..." )
2022-11-16 17:17:28 -06:00
result , err := sc . DestroyKeyVersion ( ctx , scope . Item . Id , rootKeyVersionToDestroy . Id )
require . NoError ( t , err )
assert . Equal ( t , "completed" , result . State )
jobs , err = sc . ListKeyVersionDestructionJobs ( ctx , scope . Item . Id )
require . NoError ( t , err )
assert . Len ( t , jobs . Items , 0 )
2022-12-05 18:25:12 -05:00
t . Log ( "Destroying database key..." )
2022-11-16 17:17:28 -06:00
result , err = sc . DestroyKeyVersion ( ctx , scope . Item . Id , databaseKeyVersionToDestroy . Id )
require . NoError ( t , err )
assert . Equal ( t , "pending" , result . State )
jobs , err = sc . ListKeyVersionDestructionJobs ( ctx , scope . Item . Id )
require . NoError ( t , err )
assert . Len ( t , jobs . Items , 1 )
// The default scheduler job run interval is 1 minute. The runs become available 1
// second after the last successful run. We need to re-encrypt data in 1 table,
// and then remove the key. This job should take between 1 and 2 minutes to run,
// depending on the timing of the first started run.
ctx , cancel := context . WithTimeout ( context . Background (), 3 * time . Minute )
t . Cleanup ( cancel )
for {
jobs , err = sc . ListKeyVersionDestructionJobs ( ctx , scope . Item . Id )
require . NoError ( t , err )
if len ( jobs . Items ) == 0 {
break
}
require . Len ( t , jobs . Items , 1 )
assert . Equal ( t , databaseKeyVersionToDestroy . Id , jobs . Items [ 0 ]. KeyVersionId )
select {
case <- time . After ( 5 * time . Second ):
case <- ctx . Done ():
t . Fatal ( "Test timed out waiting for destruction to finish" )
}
}
keys , err = sc . ListKeys ( ctx , scope . Item . Id )
require . NoError ( t , err )
// We expect to see all DEKs and the root key, so DEKs+1 in total.
assert . Len ( t , keys . Items , len ( kms . ValidDekPurposes ()) + 1 , "Expected %d keys to be listed, but found %d. Keys: %v" , len ( kms . ValidDekPurposes ()) + 1 , len ( keys . Items ), keys . Items )
for _ , key := range keys . Items {
switch key . Purpose {
case "rootKey" , "database" :
assert . Len ( t , key . Versions , 1 )
default :
assert . Len ( t , key . Versions , 2 )
}
}
2024-03-27 09:49:06 -04:00
t . Log ( "Successfully destroyed a root key and data key" )
2022-11-16 17:17:28 -06:00
}