5a85d96322
After internal design review, we decided to remove exposing algorithm choice to the end-user for the initial release. We'll solve nonce rotation by forcing rotations automatically on key GC (in a core job, not included in this changeset). Default to AES-256 GCM for the following criteria: * faster implementation when hardware acceleration is available * FIPS compliant * implementation in pure go * post-quantum resistance Also fixed a bug in the decoding from keystore and switched to a harder-to-misuse encoding method.
286 lines
8.4 KiB
Go
286 lines
8.4 KiB
Go
package nomad
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/hashicorp/nomad/testutil"
|
|
)
|
|
|
|
// TestKeyringEndpoint_CRUD exercises the basic keyring operations
|
|
func TestKeyringEndpoint_CRUD(t *testing.T) {
|
|
|
|
ci.Parallel(t)
|
|
srv, rootToken, shutdown := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0 // Prevent automatic dequeue
|
|
})
|
|
defer shutdown()
|
|
testutil.WaitForLeader(t, srv.RPC)
|
|
codec := rpcClient(t, srv)
|
|
|
|
// Upsert a new key
|
|
|
|
key, err := structs.NewRootKey(structs.EncryptionAlgorithmAES256GCM)
|
|
require.NoError(t, err)
|
|
id := key.Meta.KeyID
|
|
key.Meta.Active = true
|
|
|
|
updateReq := &structs.KeyringUpdateRootKeyRequest{
|
|
RootKey: key,
|
|
WriteRequest: structs.WriteRequest{Region: "global"},
|
|
}
|
|
var updateResp structs.KeyringUpdateRootKeyResponse
|
|
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
|
|
updateReq.AuthToken = rootToken.SecretID
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, uint64(0), updateResp.Index)
|
|
|
|
// Get and List don't need a token here because they rely on mTLS role verification
|
|
getReq := &structs.KeyringGetRootKeyRequest{
|
|
KeyID: id,
|
|
QueryOptions: structs.QueryOptions{Region: "global"},
|
|
}
|
|
var getResp structs.KeyringGetRootKeyResponse
|
|
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Get", getReq, &getResp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, updateResp.Index, getResp.Index)
|
|
require.Equal(t, structs.EncryptionAlgorithmAES256GCM, getResp.Key.Meta.Algorithm)
|
|
|
|
// Make a blocking query for List and wait for an Update. Note
|
|
// that List/Get queries don't need ACL tokens in the test server
|
|
// because they always pass the mTLS check
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
var listResp structs.KeyringListRootKeyMetaResponse
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
codec := rpcClient(t, srv) // not safe to share across goroutines
|
|
listReq := &structs.KeyringListRootKeyMetaRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: getResp.Index,
|
|
},
|
|
}
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.List", listReq, &listResp)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
updateReq.RootKey.Meta.CreateTime = time.Now()
|
|
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, uint64(0), updateResp.Index)
|
|
|
|
// wait for the blocking query to complete and check the response
|
|
wg.Wait()
|
|
require.Greater(t, listResp.Index, getResp.Index)
|
|
require.Len(t, listResp.Keys, 2) // bootstrap + new one
|
|
|
|
// Delete the key and verify that it's gone
|
|
|
|
delReq := &structs.KeyringDeleteRootKeyRequest{
|
|
KeyID: id,
|
|
WriteRequest: structs.WriteRequest{Region: "global"},
|
|
}
|
|
var delResp structs.KeyringDeleteRootKeyResponse
|
|
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
|
|
delReq.AuthToken = rootToken.SecretID
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
|
|
require.EqualError(t, err, "active root key cannot be deleted - call rotate first")
|
|
|
|
// set inactive
|
|
updateReq.RootKey.Meta.Active = false
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
|
|
require.NoError(t, err)
|
|
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
|
|
require.NoError(t, err)
|
|
require.Greater(t, delResp.Index, getResp.Index)
|
|
|
|
listReq := &structs.KeyringListRootKeyMetaRequest{
|
|
QueryOptions: structs.QueryOptions{Region: "global"},
|
|
}
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.List", listReq, &listResp)
|
|
require.NoError(t, err)
|
|
require.Greater(t, listResp.Index, getResp.Index)
|
|
require.Len(t, listResp.Keys, 1) // just the bootstrap key
|
|
}
|
|
|
|
// TestKeyringEndpoint_validateUpdate exercises all the various
|
|
// validations we make for the update RPC
|
|
func TestKeyringEndpoint_InvalidUpdates(t *testing.T) {
|
|
|
|
ci.Parallel(t)
|
|
srv, rootToken, shutdown := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0 // Prevent automatic dequeue
|
|
})
|
|
defer shutdown()
|
|
testutil.WaitForLeader(t, srv.RPC)
|
|
codec := rpcClient(t, srv)
|
|
|
|
// Setup an existing key
|
|
key, err := structs.NewRootKey(structs.EncryptionAlgorithmAES256GCM)
|
|
require.NoError(t, err)
|
|
id := key.Meta.KeyID
|
|
key.Meta.Active = true
|
|
|
|
updateReq := &structs.KeyringUpdateRootKeyRequest{
|
|
RootKey: key,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: rootToken.SecretID,
|
|
},
|
|
}
|
|
var updateResp structs.KeyringUpdateRootKeyResponse
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
key *structs.RootKey
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
key: &structs.RootKey{},
|
|
expectedErrMsg: "root key metadata is required",
|
|
},
|
|
{
|
|
key: &structs.RootKey{Meta: &structs.RootKeyMeta{}},
|
|
expectedErrMsg: "root key UUID is required",
|
|
},
|
|
{
|
|
key: &structs.RootKey{Meta: &structs.RootKeyMeta{KeyID: "invalid"}},
|
|
expectedErrMsg: "root key UUID is required",
|
|
},
|
|
{
|
|
key: &structs.RootKey{Meta: &structs.RootKeyMeta{
|
|
KeyID: id,
|
|
Algorithm: structs.EncryptionAlgorithmAES256GCM,
|
|
}},
|
|
expectedErrMsg: "root key material is required",
|
|
},
|
|
{
|
|
key: &structs.RootKey{
|
|
Key: []byte{0x01},
|
|
Meta: &structs.RootKeyMeta{
|
|
KeyID: id,
|
|
Algorithm: "whatever",
|
|
}},
|
|
expectedErrMsg: "root key algorithm cannot be changed after a key is created",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.expectedErrMsg, func(t *testing.T) {
|
|
updateReq := &structs.KeyringUpdateRootKeyRequest{
|
|
RootKey: tc.key,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: rootToken.SecretID,
|
|
},
|
|
}
|
|
var updateResp structs.KeyringUpdateRootKeyResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
|
|
require.EqualError(t, err, tc.expectedErrMsg)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
// TestKeyringEndpoint_Rotate exercises the key rotation logic
|
|
func TestKeyringEndpoint_Rotate(t *testing.T) {
|
|
|
|
ci.Parallel(t)
|
|
srv, rootToken, shutdown := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0 // Prevent automatic dequeue
|
|
})
|
|
defer shutdown()
|
|
testutil.WaitForLeader(t, srv.RPC)
|
|
codec := rpcClient(t, srv)
|
|
|
|
// Setup an existing key
|
|
key, err := structs.NewRootKey(structs.EncryptionAlgorithmAES256GCM)
|
|
require.NoError(t, err)
|
|
key.Meta.Active = true
|
|
|
|
updateReq := &structs.KeyringUpdateRootKeyRequest{
|
|
RootKey: key,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: rootToken.SecretID,
|
|
},
|
|
}
|
|
var updateResp structs.KeyringUpdateRootKeyResponse
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
|
|
require.NoError(t, err)
|
|
|
|
// Rotate the key
|
|
|
|
rotateReq := &structs.KeyringRotateRootKeyRequest{
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var rotateResp structs.KeyringRotateRootKeyResponse
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Rotate", rotateReq, &rotateResp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
|
|
rotateReq.AuthToken = rootToken.SecretID
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Rotate", rotateReq, &rotateResp)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, updateResp.Index, rotateResp.Index)
|
|
|
|
newID := rotateResp.Key.KeyID
|
|
|
|
// Verify we have a new key and the old one is inactive
|
|
|
|
listReq := &structs.KeyringListRootKeyMetaRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var listResp structs.KeyringListRootKeyMetaResponse
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.List", listReq, &listResp)
|
|
require.NoError(t, err)
|
|
|
|
require.Greater(t, listResp.Index, updateResp.Index)
|
|
require.Len(t, listResp.Keys, 3) // bootstrap + old + new
|
|
|
|
for _, keyMeta := range listResp.Keys {
|
|
if keyMeta.KeyID != newID {
|
|
require.False(t, keyMeta.Active, "expected old keys to be inactive")
|
|
} else {
|
|
require.True(t, keyMeta.Active, "expected new key to be inactive")
|
|
}
|
|
}
|
|
|
|
getReq := &structs.KeyringGetRootKeyRequest{
|
|
KeyID: newID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var getResp structs.KeyringGetRootKeyResponse
|
|
err = msgpackrpc.CallWithCodec(codec, "Keyring.Get", getReq, &getResp)
|
|
require.NoError(t, err)
|
|
|
|
gotKey := getResp.Key
|
|
require.Len(t, gotKey.Key, 32)
|
|
}
|