Files
threshold/internal/server/repository_workerauth_test.go
T
Jeff MitchellandIrena Rindos 01fb949d0b Add controller-led worker auth flow (#2413)
This adds an action to the API that allows pulling out an activation
token that can be given to a worker in its config (directly or via env
or file). The worker can pass this along with its normal request in
place of its normal nonce and the server will automatically accept it.

Co-authored-by: Irena Rindos <irenarindos@users.noreply.github.com>
2022-09-02 14:32:41 -04:00

470 lines
16 KiB
Go

package server
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"strings"
"testing"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/server/store"
"github.com/hashicorp/boundary/internal/types/scope"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/nodeenrollment"
"github.com/hashicorp/nodeenrollment/registration"
"github.com/hashicorp/nodeenrollment/rotation"
"github.com/hashicorp/nodeenrollment/storage/file"
"github.com/hashicorp/nodeenrollment/types"
"github.com/mitchellh/mapstructure"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/curve25519"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
// Test RootCertificate storage
func TestStoreRootCertificates(t *testing.T) {
require, assert := require.New(t), assert.New(t)
ctx := context.Background()
wrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, wrapper)
// Ensures the global scope contains a valid root key
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
require.NoError(err)
wrapper, err = kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase)
require.NoError(err)
require.NotNil(wrapper)
rw := db.New(conn)
workerAuthRepo, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
require.NoError(err)
// Fail to find root certificates prior to rotation/ creation
noCertAuth := &types.RootCertificates{Id: ca_id}
err = workerAuthRepo.Load(ctx, noCertAuth)
require.Error(err)
rootIds, err := workerAuthRepo.List(ctx, (*types.RootCertificate)(nil))
require.NoError(err)
assert.Len(rootIds, 0)
// Rotate will generate and store next and current, as we have none
roots, err := rotation.RotateRootCertificates(ctx, workerAuthRepo)
require.NoError(err)
rootIds, err = workerAuthRepo.List(ctx, (*types.RootCertificate)(nil))
require.NoError(err)
assert.Len(rootIds, 2)
// Load stored roots
certAuthority := &types.RootCertificates{Id: ca_id}
err = workerAuthRepo.Load(ctx, certAuthority)
require.NoError(err)
require.NotNil(certAuthority.GetNext())
require.NotNil(certAuthority.GetCurrent())
// Read the next cert and validate the stored values are valid after encrypt and decrypt
assert.Equal(roots.Next.PrivateKeyPkcs8, certAuthority.Next.PrivateKeyPkcs8)
// Read the current cert and validate the stored values are valid after encrypt and decrypt
assert.Equal(roots.Current.PrivateKeyPkcs8, certAuthority.Current.PrivateKeyPkcs8)
// Fail to find root certificates under bogus id
bogusCertAuth := &types.RootCertificates{Id: "bogus"}
err = workerAuthRepo.Load(ctx, bogusCertAuth)
require.Error(err)
}
func TestStoreCertAuthorityVersioning(t *testing.T) {
require, assert := require.New(t), assert.New(t)
ctx := context.Background()
wrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, wrapper)
// Ensures the global scope contains a valid root key
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
require.NoError(err)
wrapper, err = kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase)
require.NoError(err)
require.NotNil(wrapper)
rw := db.New(conn)
workerAuthRepo, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
require.NoError(err)
// Store CA and check that initial version updates
roots, err := rotation.RotateRootCertificates(ctx, workerAuthRepo)
require.NoError(err)
cAuth2 := &types.RootCertificates{Id: ca_id}
err = workerAuthRepo.Load(ctx, cAuth2)
require.NoError(err)
var result2 rootCertificatesVersion
err = mapstructure.Decode(cAuth2.State.AsMap(), &result2)
require.NoError(err)
assert.Equal(uint32(2), result2.Version)
// Store CA with old version, expect error
badVersion := &rootCertificatesVersion{Version: uint32(1)}
s := structs.New(badVersion)
s.TagName = "mapstructure"
badState, err := structpb.NewStruct(s.Map())
require.NoError(err)
newRoots2 := &types.RootCertificates{
Id: ca_id,
Next: roots.Next,
Current: roots.Current,
}
newRoots2.State = badState
err = workerAuthRepo.Store(ctx, newRoots2)
require.Error(err)
// Remove CA, expect updated version
cAuthRemove := &types.RootCertificates{Id: ca_id}
removeVersion := &rootCertificatesVersion{Version: uint32(2)}
s = structs.New(removeVersion)
s.TagName = "mapstructure"
removeState, err := structpb.NewStruct(s.Map())
require.NoError(err)
cAuthRemove.State = removeState
err = workerAuthRepo.Remove(ctx, cAuthRemove)
require.NoError(err)
}
// Test WorkerAuth storage
func TestStoreWorkerAuth(t *testing.T) {
require, assert := require.New(t), assert.New(t)
ctx := context.Background()
rootWrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, rootWrapper)
// Ensures the global scope contains a valid root key
require.NoError(kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
worker := TestPkiWorker(t, conn, rootWrapper)
rw := db.New(conn)
rootStorage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
require.NoError(err)
_, err = rotation.RotateRootCertificates(ctx, rootStorage)
require.NoError(err)
// Create struct to pass in with workerId that will be passed along to storage
state, err := AttachWorkerIdToState(ctx, worker.PublicId)
require.NoError(err)
// This happens on the worker
fileStorage, err := file.New(ctx)
require.NoError(err)
nodeCreds, err := types.NewNodeCredentials(ctx, fileStorage)
require.NoError(err)
// Create request using worker id
fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(ctx)
require.NoError(err)
keyId, err := nodeenrollment.KeyIdFromPkix(nodeCreds.CertificatePublicKeyPkix)
require.NoError(err)
nodePubKey, err := curve25519.X25519(nodeCreds.EncryptionPrivateKeyBytes, curve25519.Basepoint)
require.NoError(err)
// Add in node information to storage so we have a key to use
nodeInfo := &types.NodeInformation{
Id: keyId,
CertificatePublicKeyPkix: nodeCreds.CertificatePublicKeyPkix,
CertificatePublicKeyType: nodeCreds.CertificatePrivateKeyType,
EncryptionPublicKeyBytes: nodePubKey,
EncryptionPublicKeyType: nodeCreds.EncryptionPrivateKeyType,
RegistrationNonce: nodeCreds.RegistrationNonce,
State: state,
}
// Create storage for authentication and pass it the worker
storage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
require.NoError(err)
// The AuthorizeNode request will result in a WorkerAuth record being stored
_, err = registration.AuthorizeNode(ctx, storage, fetchReq, nodeenrollment.WithState(state))
require.NoError(err)
// We should now look for a node information value in storage and validate that it's populated
nodeInfos, err := storage.List(ctx, (*types.NodeInformation)(nil))
require.NoError(err)
require.NotEmpty(nodeInfos)
assert.Len(nodeInfos, 1)
// Validate the stored fields match those from the worker
nodeLookup := &types.NodeInformation{
Id: keyId,
}
err = storage.Load(ctx, nodeLookup)
require.NoError(err)
assert.NotEmpty(nodeLookup)
assert.Equal(nodeInfo.EncryptionPublicKeyBytes, nodeLookup.EncryptionPublicKeyBytes)
assert.Equal(nodeInfo.RegistrationNonce, nodeLookup.RegistrationNonce)
assert.Equal(nodeInfo.CertificatePublicKeyPkix, nodeLookup.CertificatePublicKeyPkix)
assert.Equal(nodeInfo.State.AsMap(), nodeLookup.State.AsMap())
// Remove node
err = storage.Remove(ctx, nodeLookup)
require.NoError(err)
err = storage.Load(ctx, nodeLookup)
require.Error(err)
}
func TestStoreServerLedActivationToken(t *testing.T) {
require, assert := require.New(t), assert.New(t)
ctx := context.Background()
rootWrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, rootWrapper)
// Ensure the global scope contains a valid root key
require.NoError(kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
rw := db.New(conn)
rootStorage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
require.NoError(err)
_, err = rotation.RotateRootCertificates(ctx, rootStorage)
require.NoError(err)
repo, err := NewRepository(rw, rw, kmsCache)
require.NoError(err)
worker, err := repo.CreateWorker(ctx, &Worker{Worker: &store.Worker{ScopeId: scope.Global.String()}}, WithCreateControllerLedActivationToken(true))
require.NoError(err)
require.NotEmpty(worker.ControllerGeneratedActivationToken)
// We should now look for an activation token value in storage using the
// lookup function and validate that it's populated
tokenNonce := new(types.ServerLedActivationTokenNonce)
marshaledNonce, err := base58.FastBase58Decoding(strings.TrimPrefix(worker.ControllerGeneratedActivationToken, nodeenrollment.ServerLedActivationTokenPrefix))
require.NoError(err)
require.NoError(proto.Unmarshal(marshaledNonce, tokenNonce))
hm := hmac.New(sha256.New, tokenNonce.HmacKeyBytes)
idBytes := hm.Sum(tokenNonce.Nonce)
actToken := &types.ServerLedActivationToken{
Id: base58.FastBase58Encoding(idBytes),
}
require.NoError(rootStorage.Load(ctx, actToken))
require.NotNil(actToken.CreationTime)
assert.False(actToken.CreationTime.AsTime().IsZero())
assert.Less(time.Until(actToken.CreationTime.AsTime()), time.Duration(0))
require.NoError(rootStorage.Remove(ctx, actToken))
require.Error(rootStorage.Load(ctx, actToken))
}
func TestUnsupportedMessages(t *testing.T) {
require := require.New(t)
ctx := context.Background()
wrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, wrapper)
rw := db.New(conn)
storage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
require.NoError(err)
err = storage.Store(ctx, &types.NodeCredentials{})
require.Error(err)
err = storage.Load(ctx, &types.NodeCredentials{Id: "bogus"})
require.Error(err)
_, err = storage.List(ctx, (*types.NodeCredentials)(nil))
require.Error(err)
err = storage.Remove(ctx, &types.NodeCredentials{Id: "bogus"})
require.Error(err)
}
func TestStoreNodeInformationTx(t *testing.T) {
t.Parallel()
testCtx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
testWrapper := db.TestWrapper(t)
testKeyId, err := testWrapper.KeyId(testCtx)
require.NoError(t, err)
kmsCache := kms.TestKms(t, conn, testWrapper)
// Ensures the global scope contains a valid root key
require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase)
require.NoError(t, err)
testRootStorage, err := NewRepositoryStorage(testCtx, rw, rw, kmsCache)
require.NoError(t, err)
_, err = rotation.RotateRootCertificates(testCtx, testRootStorage)
require.NoError(t, err)
// Create struct to pass in with workerId that will be passed along to
// storage
testWorker := TestPkiWorker(t, conn, testWrapper)
testState, err := AttachWorkerIdToState(testCtx, testWorker.PublicId)
require.NoError(t, err)
testNodeInfoFn := func() *types.NodeInformation {
// This happens on the worker
fileStorage, err := file.New(testCtx)
require.NoError(t, err)
nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage)
require.NoError(t, err)
nodePubKey, err := curve25519.X25519(nodeCreds.EncryptionPrivateKeyBytes, curve25519.Basepoint)
require.NoError(t, err)
// Add in node information to storage so we have a key to use
nodeInfo := &types.NodeInformation{
Id: testKeyId,
CertificatePublicKeyPkix: nodeCreds.CertificatePublicKeyPkix,
CertificatePublicKeyType: nodeCreds.CertificatePrivateKeyType,
EncryptionPublicKeyBytes: nodePubKey,
EncryptionPublicKeyType: nodeCreds.EncryptionPrivateKeyType,
ServerEncryptionPrivateKeyBytes: []byte("whatever"),
RegistrationNonce: nodeCreds.RegistrationNonce,
State: testState,
}
return nodeInfo
}
tests := []struct {
name string
writer db.Writer
databaseWrapper wrapping.Wrapper
node *types.NodeInformation
wantErr bool
wantErrIs errors.Code
wantErrContains string
}{
{
name: "missing-writer",
databaseWrapper: databaseWrapper,
node: testNodeInfoFn(),
wantErr: true,
wantErrIs: errors.InvalidParameter,
wantErrContains: "missing writer",
},
{
name: "missing-wrapper",
writer: rw,
node: testNodeInfoFn(),
wantErr: true,
wantErrIs: errors.InvalidParameter,
wantErrContains: "missing database wrapper",
},
{
name: "missing-node",
writer: rw,
databaseWrapper: databaseWrapper,
wantErr: true,
wantErrIs: errors.InvalidParameter,
wantErrContains: "missing NodeInformation",
},
{
name: "key-id-error",
writer: rw,
databaseWrapper: &mockTestWrapper{err: errors.New(testCtx, errors.Internal, "testing", "key-id-error")},
node: testNodeInfoFn(),
wantErr: true,
wantErrIs: errors.Internal,
wantErrContains: "key-id-error",
},
{
name: "encrypt-error",
writer: rw,
databaseWrapper: &mockTestWrapper{
encryptError: true,
err: errors.New(testCtx, errors.Encrypt, "testing", "encrypt-error"),
},
node: testNodeInfoFn(),
wantErr: true,
wantErrIs: errors.Encrypt,
wantErrContains: "encrypt-error",
},
{
name: "create-error-no-db",
writer: &db.Db{},
databaseWrapper: databaseWrapper,
node: testNodeInfoFn(),
wantErr: true,
wantErrContains: "db.Create",
},
{
name: "create-error-validation",
writer: rw,
databaseWrapper: databaseWrapper,
node: func() *types.NodeInformation {
ni := testNodeInfoFn()
ni.State = nil
return ni
}(),
wantErr: true,
wantErrContains: "missing WorkerId",
},
{
name: "success",
writer: rw,
databaseWrapper: databaseWrapper,
node: testNodeInfoFn(),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
err := StoreNodeInformationTx(testCtx, tc.writer, tc.databaseWrapper, tc.node)
if tc.wantErr {
require.Error(err)
if tc.wantErrIs != errors.Unknown {
assert.True(errors.Match(errors.T(tc.wantErrIs), err))
}
if tc.wantErrContains != "" {
assert.Contains(err.Error(), tc.wantErrContains)
}
return
}
require.NoError(err)
})
}
}
type mockTestWrapper struct {
wrapping.Wrapper
decryptError bool
encryptError bool
err error
keyId string
}
func (m *mockTestWrapper) KeyId(context.Context) (string, error) {
if m.err != nil {
return "", m.err
}
return m.keyId, nil
}
func (m *mockTestWrapper) Encrypt(ctx context.Context, plaintext []byte, options ...wrapping.Option) (*wrapping.BlobInfo, error) {
if m.err != nil && m.encryptError {
return nil, m.err
}
panic("todo")
}
func (m *mockTestWrapper) Decrypt(ctx context.Context, ciphertext *wrapping.BlobInfo, options ...wrapping.Option) ([]byte, error) {
if m.err != nil && m.decryptError {
return nil, m.err
}
return []byte("decrypted"), nil
}