Files
threshold/internal/kms/kms.go
T
Jim 785241237e refactor (kms): adopt the go-kms-wrapping/extras/kms package (#2027)
* feature (db): add Db.UnderlyingDB()

This is needed to allow for integration with bits that need a *dbw.DB

* feature (kms): add new go-kms-wrapping/extras/kms dependency

* refactor (kms): schema changes required to adopt extras/kms pkg

* refactor (kms/iam): rename to DeprecateCreateKeysTx(...)

* refactor (kms): remove deprecated test

* refactor (kms): refactor Kms type

This refactor includes renaming the existing type to DeprecatedKms
and then refactoring the Kms type to become a wrapper of
go-kms-wrapping/extras/kms

* refactor (kms): delete all the unneeded bits

All this stuff is not needed now that we're using
go-kms-wrapping/extras/kms

* refactor (db): fixup tests after kms refactor

* refactor (kms/tests): use testing.TB for test helpers

Our test helper packages were not possible to use
with benchmarks. This refactor switches to testing.TB
to allow both tests and benchmarks to use the helpers.

* tests (session benchmarks): refactor to use new Kms type

* refactor (schema): rm test which relies on deprecated Kms code

* refactor (kms): post rebase on main changes required

* chore (kms refactor): re-add unit test from main

* refactor (kms): uses migrations that don't include KMS schema changes
2022-05-13 13:53:22 -04:00

283 lines
10 KiB
Go

package kms
import (
"context"
"fmt"
"io"
"reflect"
"sync"
"time"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-dbw"
wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
)
// Kms is a way to access wrappers for a given scope and purpose. Since keys can
// never change, only be added or (eventually) removed, it opportunistically
// caches, going to the database as needed.
type Kms struct {
underlying *wrappingKms.Kms
reader db.Reader
derivedPurposeCache sync.Map
}
// New creates a Kms using the provided reader and writer. No options are
// currently supported.
func New(ctx context.Context, reader *db.Db, writer *db.Db, _ ...Option) (*Kms, error) {
const op = "kms.(Kms).New"
if isNil(reader) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing reader")
}
if isNil(writer) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing writer")
}
purposes := stdNewKmsPurposes()
r := dbw.New(reader.UnderlyingDB())
w := dbw.New(writer.UnderlyingDB())
k, err := wrappingKms.New(r, w, purposes, wrappingKms.WithCache(true))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating new in-memory kms"))
}
return &Kms{
underlying: k,
reader: reader,
}, nil
}
// NewUsingReaderWriter creates a Kms using the provided reader and writer. No
// options are currently supported.
func NewUsingReaderWriter(ctx context.Context, reader db.Reader, writer db.Writer, _ ...Option) (*Kms, error) {
const op = "kms.(Kms).New"
if isNil(reader) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing reader")
}
if isNil(writer) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing writer")
}
purposes := stdNewKmsPurposes()
r, err := convertToRW(ctx, reader)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
w, err := convertToRW(ctx, writer)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
k, err := wrappingKms.New(r, w, purposes, wrappingKms.WithCache(true))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating new in-memory kms"))
}
return &Kms{
underlying: k,
reader: reader,
}, nil
}
func convertToRW(ctx context.Context, r interface{}) (*dbw.RW, error) {
const op = "kms.convertToDb"
d, ok := r.(*db.Db)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "unable to convert to db.DB")
}
return dbw.New(d.UnderlyingDB()), nil
}
// AddExternalWrappers allows setting the external keys.
func (k *Kms) AddExternalWrappers(ctx context.Context, opt ...Option) error {
const op = "kms.(Kms).AddExternalWrappers"
opts := getOpts(opt...)
if opts.withRootWrapper != nil {
if err := k.underlying.AddExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeRootKey.String()), opts.withRootWrapper); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add root wrapper"))
}
}
if opts.withWorkerAuthWrapper != nil {
if err := k.underlying.AddExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), opts.withWorkerAuthWrapper); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add worker auth wrapper"))
}
}
if opts.withRecoveryWrapper != nil {
if err := k.underlying.AddExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeRecovery.String()), opts.withRecoveryWrapper); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add recovery wrapper"))
}
}
return nil
}
// GetWrapper returns a wrapper for the given scope and purpose. When a keyId is
// passed, it will ensure that the returning wrapper has that key ID in the
// multiwrapper. This is not necessary for encryption but should be supplied for
// decryption.
func (k *Kms) GetWrapper(ctx context.Context, scopeId string, purpose KeyPurpose, opt ...Option) (wrapping.Wrapper, error) {
const op = "kms.(Kms).GetWrapper"
if scopeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
if purpose == KeyPurposeUnknown {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing purpose")
}
opts := getOpts(opt...)
w, err := k.underlying.GetWrapper(ctx, scopeId, wrappingKms.KeyPurpose(purpose.String()), wrappingKms.WithKeyId(opts.withKeyId))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get wrapper"))
}
return w, err
}
// GetExternalWrappers returns the Kms' ExternalWrappers
func (k *Kms) GetExternalWrappers(ctx context.Context) *ExternalWrappers {
const op = "kms.(Kms).GetExternalWrappers"
ret := &ExternalWrappers{}
if root, err := k.underlying.GetExternalRootWrapper(); err == nil {
ret.root = root
}
if workerAuth, err := k.underlying.GetExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String())); err == nil {
ret.workerAuth = workerAuth
}
if recovery, err := k.underlying.GetExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeRecovery.String())); err == nil {
ret.recovery = recovery
}
return ret
}
// GetDerivedPurposeCache returns the raw derived purpose cache
func (k *Kms) GetDerivedPurposeCache() *sync.Map {
return &k.derivedPurposeCache
}
// ReconcileKeys will reconcile the keys in the kms against known possible
// issues. This function reconciles the global scope unless the
// WithScopeIds(...) option is provided
func (k *Kms) ReconcileKeys(ctx context.Context, randomReader io.Reader, opt ...Option) error {
const op = "kms.ReconcileKeys"
if isNil(randomReader) {
return errors.New(ctx, errors.InvalidParameter, op, "missing rand reader")
}
// it's possible that the global audit key was created after this instance's
// database was initialized... so check if the audit wrapper is available
// for the global scope and if not, then add one to the global scope
if _, err := k.GetWrapper(ctx, scope.Global.String(), KeyPurposeAudit); err != nil {
if err := k.underlying.ReconcileKeys(
ctx,
[]string{scope.Global.String()},
[]wrappingKms.KeyPurpose{wrappingKms.KeyPurpose(KeyPurposeAudit.String())},
wrappingKms.WithRandomReader(randomReader),
); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error creating audit key in scope %s", scope.Global.String())))
}
}
opts := getOpts(opt...)
if len(opts.withScopeIds) > 0 {
if err := k.underlying.ReconcileKeys(
ctx,
opts.withScopeIds,
// just add additional purposes as needed going forward to reconcile
// new keys as they are added.
[]wrappingKms.KeyPurpose{wrappingKms.KeyPurpose(KeyPurposeOidc.String())},
wrappingKms.WithRandomReader(randomReader),
); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error creating keys in scopes %s", opts.withScopeIds)))
}
}
return nil
}
// CreateKeys creates the root key and DEKs returns a map of the new keys.
// Supports the WithRandomReader(...) and WithReaderWriter(...) options.
// When WithReaderWriter(...) is used the caller is responsible for managing the
// transaction which allows this capability to be shared with the iam repo when
// it's creating Scopes.
func (k *Kms) CreateKeys(ctx context.Context, scopeId string, opt ...Option) error {
const op = "kms.(Kms).CreateKeysTx"
if scopeId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
opts := getOpts(opt...)
kmsOpts := []wrappingKms.Option{wrappingKms.WithRandomReader(opts.withRandomReader)}
switch {
case !isNil(opts.withReader) && isNil(opts.withWriter):
return errors.New(ctx, errors.InvalidParameter, op, "missing writer")
case isNil(opts.withReader) && !isNil(opts.withWriter):
return errors.New(ctx, errors.InvalidParameter, op, "missing reader")
case !isNil(opts.withReader) && !isNil(opts.withWriter):
r, err := convertToRW(ctx, opts.withReader)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert reader"))
}
w, err := convertToRW(ctx, opts.withWriter)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert writer"))
}
kmsOpts = append(kmsOpts, wrappingKms.WithReaderWriter(r, w))
}
purposes := make([]wrappingKms.KeyPurpose, 0, len(ValidDekPurposes()))
for _, p := range ValidDekPurposes() {
purposes = append(purposes, wrappingKms.KeyPurpose(p.String()))
}
if err := k.underlying.CreateKeys(ctx, scopeId, purposes, kmsOpts...); err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
// ClearCache will clear the kms's cache which is useful after a scope has been
// deleted.
func (k *Kms) ClearCache(ctx context.Context) error {
const op = "kms.(Kms).ClearCache"
if err := k.underlying.ClearCache(ctx); err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
// VerifyGlobalRoot will verify that the global root wrapper is reasonable.
func (k *Kms) VerifyGlobalRoot(ctx context.Context) error {
const op = "kms.(Kms).VerifyGlobalRoot"
var keys []*rootKey
if err := k.reader.SearchWhere(ctx, &keys, "1=1", nil, db.WithLimit(1), db.WithOrder("create_time asc")); err != nil {
return errors.Wrap(ctx, err, op)
}
for _, rk := range keys {
if rk.ScopeId == scope.Global.String() {
return nil
}
}
return errors.New(ctx, errors.MigrationIntegrity, op, "can't find global scoped root key")
}
func stdNewKmsPurposes() []wrappingKms.KeyPurpose {
purposes := make([]wrappingKms.KeyPurpose, 0, len(ValidDekPurposes()))
for _, p := range ValidDekPurposes() {
purposes = append(purposes, wrappingKms.KeyPurpose(p.String()))
}
purposes = append(purposes, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), wrappingKms.KeyPurpose(KeyPurposeRecovery.String()))
return purposes
}
func isNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}
type rootKey struct {
PrivateId string `gorm:"primary_key"`
ScopeId string `gorm:"default:null"`
CreateTime time.Time `gorm:"default:current_timestamp"`
}
func (*rootKey) TableName() string { return "kms_root_key" }