diff --git a/internal/db/schema/migrations/oss/postgres/99/03_worker_authentication.up.sql b/internal/db/schema/migrations/oss/postgres/99/03_worker_authentication.up.sql index b2aca4b53..33d71f06e 100644 --- a/internal/db/schema/migrations/oss/postgres/99/03_worker_authentication.up.sql +++ b/internal/db/schema/migrations/oss/postgres/99/03_worker_authentication.up.sql @@ -44,7 +44,7 @@ create trigger worker_auth_ca_update_time_column insert into worker_auth_ca(private_id) values('roots'); create trigger update_version_column after update on worker_auth_ca - for each row execute procedure update_version_column(); + for each row execute procedure update_version_column(private_id); comment on table worker_auth_ca is 'worker_auth_ca is a one-row versioned table used for locking for certificate rotation on the worker_auth_ca_certificate table.'; @@ -110,7 +110,7 @@ create table worker_auth_authorized( references kms_data_key_version (private_id) on delete restrict on update cascade, - nonce bytea + nonce bytea -- encrypted value ); create trigger @@ -124,7 +124,7 @@ comment on table worker_auth_authorized is 'worker_auth_authorized is a table where each row represents key and cert data associated with an authorized worker.'; create table worker_auth_certificate_bundle( - certificate_public_key bytea not null + root_certificate_public_key bytea not null constraint worker_auth_ca_certificate_fkey references worker_auth_ca_certificate(public_key) on delete cascade @@ -137,7 +137,7 @@ create table worker_auth_certificate_bundle( cert_bundle bytea constraint current_cert_bundle_must_not_be_empty check(length(cert_bundle) > 0), - primary key(certificate_public_key, worker_key_identifier) + primary key(root_certificate_public_key, worker_key_identifier) ); comment on table worker_auth_certificate_bundle is @@ -147,6 +147,6 @@ create trigger immutable_columns before update on worker_auth_certificate_bundle - for each row execute procedure immutable_columns('certificate_public_key', 'worker_key_identifier', 'cert_bundle'); + for each row execute procedure immutable_columns('root_certificate_public_key', 'worker_key_identifier', 'cert_bundle'); commit; \ No newline at end of file diff --git a/internal/proto/controller/storage/servers/store/v1/worker_auth.proto b/internal/proto/controller/storage/servers/store/v1/worker_auth.proto index 197ea4005..fd7d91d87 100644 --- a/internal/proto/controller/storage/servers/store/v1/worker_auth.proto +++ b/internal/proto/controller/storage/servers/store/v1/worker_auth.proto @@ -40,11 +40,11 @@ message WorkerAuth { // WorkerCertBundle contains all fields related to a WorkerCertBundle resource message WorkerCertBundle { // The public key of the issuing root certificate - // @inject_tag: `gorm:"not_null"` - bytes certificate_public_key = 10; + // @inject_tag: `gorm:"primary_key"` + bytes root_certificate_public_key = 10; // The WorkerAuth worker_key_identifier this cert bundle record is for - // @inject_tag: `gorm:"not_null"` + // @inject_tag: `gorm:"primary_key"` string worker_key_identifier = 20; // CertBundle is the marshaled protobuf certificate bundle for a WorkerAuth diff --git a/internal/servers/query.go b/internal/servers/query.go index 3d51150ee..d83f5428f 100644 --- a/internal/servers/query.go +++ b/internal/servers/query.go @@ -2,11 +2,25 @@ package servers const ( deleteWhereCreateTimeSql = `create_time < ?` - deleteTagsByWorkerIdSql = ` + + deleteTagsByWorkerIdSql = ` delete from server_worker_tag where source = ? and worker_id = ?` + + deleteWorkerAuthQuery = ` + delete from worker_auth_authorized + where worker_key_identifier = @worker_key_identifier; + ` + deleteWorkerCertBundlesQuery = ` + delete from worker_auth_certificate_bundle + where worker_key_identifier = @worker_key_identifier; + ` + deleteRootCertificateQuery = ` + delete from worker_auth_ca_certificate + where state = @state; + ` ) diff --git a/internal/servers/repository_workerauth.go b/internal/servers/repository_workerauth.go new file mode 100644 index 000000000..d266e16b5 --- /dev/null +++ b/internal/servers/repository_workerauth.go @@ -0,0 +1,755 @@ +package servers + +import ( + "context" + "crypto/x509" + "database/sql" + "fmt" + "strconv" + + "github.com/fatih/structs" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/go-dbw" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + nodee "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/types" + "github.com/mitchellh/mapstructure" + "google.golang.org/protobuf/proto" +) + +// Ensure we implement the Storage interfaces +var ( + _ nodee.Storage = (*WorkerAuthRepositoryStorage)(nil) +) + +type rootCertificatesVersion struct { + Version uint32 +} + +// WorkerAuthRepositoryStorage is the Worker Auth database repository +type WorkerAuthRepositoryStorage struct { + reader db.Reader + writer db.Writer + kms *kms.Kms +} + +// NewRepositoryStorage creates a new WorkerAuthRepositoryStorage that implements the Storage interface +func NewRepositoryStorage(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms) (*WorkerAuthRepositoryStorage, error) { + const op = "servers.(WorkerAuthRepositoryStorage).NewRepositoryStorage" + switch { + case isNil(r): + return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil") + case isNil(w): + return nil, errors.New(ctx, errors.InvalidParameter, op, "writer is nil") + case isNil(kms): + return nil, errors.New(ctx, errors.InvalidParameter, op, "kms is nil") + } + + workerAuthRepoStorage := &WorkerAuthRepositoryStorage{ + reader: r, + writer: w, + kms: kms, + } + + return workerAuthRepoStorage, nil +} + +// Store implements the Storage interface +func (r *WorkerAuthRepositoryStorage) Store(ctx context.Context, msg nodee.MessageWithId) error { + const op = "servers.(WorkerAuthRepositoryStorage).Store" + if err := types.ValidateMessage(msg); err != nil { + return errors.Wrap(ctx, err, op) + } + if msg.GetId() == "" { + return errors.New(ctx, errors.InvalidParameter, op, "given message cannot be stored as it has no ID") + } + + // Determine type of message to store + switch t := msg.(type) { + case *types.NodeInformation: + err := r.storeNodeInformation(ctx, t) + if err != nil { + return errors.Wrap(ctx, err, op) + } + case *types.RootCertificates: + err := r.storeRootCertificates(ctx, t) + if err != nil { + return errors.Wrap(ctx, err, op) + } + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for Store", msg)) + } + + return nil +} + +// Node information is stored in two parts: +// * the workerAuth record is stored with a reference to a worker +// * certificate bundles are stored with a reference to the workerAuth record and issuing root certificate +func (r *WorkerAuthRepositoryStorage) storeNodeInformation(ctx context.Context, node *types.NodeInformation) error { + const op = "servers.(WorkerAuthRepositoryStorage).storeNodeInformation" + if node == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing NodeInformation") + } + + nodeAuth := allocWorkerAuth() + nodeAuth.WorkerKeyIdentifier = node.Id + nodeAuth.WorkerEncryptionPubKey = node.EncryptionPublicKeyBytes + nodeAuth.WorkerSigningPubKey = node.CertificatePublicKeyPkix + + // Encrypt the private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op) + } + nodeAuth.KeyId, err = databaseWrapper.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op) + } + nodeAuth.ControllerEncryptionPrivKey, err = encrypt(ctx, node.ServerEncryptionPrivateKeyBytes, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + nodeAuth.Nonce, err = encrypt(ctx, node.RegistrationNonce, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + // Get workerId from state passed in + var result workerAuthWorkerId + err = mapstructure.Decode(node.State.AsMap(), &result) + if err != nil { + return errors.Wrap(ctx, err, op) + } + nodeAuth.WorkerId = result.WorkerId + + if err := nodeAuth.ValidateNewWorkerAuth(ctx); err != nil { + return errors.Wrap(ctx, err, op) + } + + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(read db.Reader, w db.Writer) error { + // Store WorkerAuth + storeNodeAuth := nodeAuth.clone() + if err := w.Create(ctx, &storeNodeAuth); err != nil { + return errors.Wrap(ctx, err, op) + } + // Then store cert bundles associated with this WorkerAuth + for _, c := range node.CertificateBundles { + if err := r.storeWorkerCertBundle(ctx, c, nodeAuth.WorkerKeyIdentifier, w); err != nil { + return errors.Wrap(ctx, err, op) + } + } + return nil + }, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +func (r *WorkerAuthRepositoryStorage) storeWorkerCertBundle(ctx context.Context, bundle *types.CertificateBundle, + workerKeyIdentifier string, writer db.Writer, +) error { + const op = "servers.(WorkerAuthRepositoryStorage).storeWorkerCertBundle" + if bundle == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing CertificateBundle") + } + if workerKeyIdentifier == "" { + return errors.New(ctx, errors.InvalidParameter, op, "empty workerKeyIdentifier") + } + if writer == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + } + + workerCertBundle := allocWorkerCertBundle() + bundleBytes, err := proto.Marshal(bundle) + if err != nil { + return errors.New(ctx, errors.Encode, op, "error marshaling nodetypes.CertificateBundle", errors.WithWrap(err)) + } + + // Extract serial number from CA cert + caCert := bundle.CaCertificateDer + parsedCert, err := x509.ParseCertificate(caCert) + if err != nil { + return errors.Wrap(ctx, err, op) + } + workerCertBundle.RootCertificatePublicKey = parsedCert.AuthorityKeyId + workerCertBundle.CertBundle = bundleBytes + workerCertBundle.WorkerKeyIdentifier = workerKeyIdentifier + + err = workerCertBundle.ValidateNewWorkerCertBundle(ctx) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + err = writer.Create(ctx, &workerCertBundle) + + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func (r *WorkerAuthRepositoryStorage) convertRootCertificate(ctx context.Context, cert *types.RootCertificate) (*RootCertificate, error) { + const op = "servers.(WorkerAuthRepositoryStorage).convertRootCertificate" + if cert == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing RootCertificate") + } + + rootCert := allocRootCertificate() + + parsedCert, err := x509.ParseCertificate(cert.CertificateDer) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rootCert.SerialNumber = parsedCert.SerialNumber.Uint64() + rootCert.Certificate = cert.CertificateDer + rootCert.NotValidAfter = timestamp.New(cert.NotAfter.AsTime()) + rootCert.NotValidBefore = timestamp.New(cert.NotBefore.AsTime()) + rootCert.PublicKey = cert.PublicKeyPkix + rootCert.State = cert.Id + rootCert.IssuingCa = ca_id + + // Encrypt the private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rootCert.KeyId, err = databaseWrapper.KeyId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rootCert.PrivateKey, err = encrypt(ctx, cert.PrivateKeyPkcs8, databaseWrapper) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + err = rootCert.ValidateNewRootCertificate(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + return rootCert, nil +} + +func (r *WorkerAuthRepositoryStorage) storeRootCertificates(ctx context.Context, cert *types.RootCertificates) error { + const op = "servers.(WorkerAuthRepositoryStorage).storeRootCertificates" + if cert == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing RootCertificate") + } + + nextCert, err := r.convertRootCertificate(ctx, cert.Next) + currentCert, err := r.convertRootCertificate(ctx, cert.Current) + + // Use passed version + var result rootCertificatesVersion + err = mapstructure.Decode(cert.State.AsMap(), &result) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + version := uint32(1) + if result.Version != 0 { + version = result.Version + } + certAuthority := &CertificateAuthority{ + CertificateAuthority: &store.CertificateAuthority{ + PrivateId: ca_id, + Version: version + 1, + }, + } + + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + // Delete the old certs + if err = r.removeRootCertificateWithWriter(ctx, string(CurrentState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + if err = r.removeRootCertificateWithWriter(ctx, string(NextState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + + // Update the certAuthority's version column + rowsUpdated, err := w.Update(ctx, certAuthority, []string{"Version"}, nil, db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated != 1 { + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated CertificateAuthority and %d rows updated", rowsUpdated)) + } + + // Then insert the new certs + if nextCert != nil { + if err = w.Create(ctx, &nextCert); err != nil { + return errors.Wrap(ctx, err, op) + } + } + if currentCert != nil { + if err = w.Create(ctx, ¤tCert); err != nil { + return errors.Wrap(ctx, err, op) + } + } + return nil + }, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +// Load implements the Storage interface. +// Load loads values into the given message. The message must be populated +// with the ID value. If not found, the returned error should be ErrNotFound. +func (r *WorkerAuthRepositoryStorage) Load(ctx context.Context, msg nodee.MessageWithId) error { + const op = "servers.(WorkerAuthRepositoryStorage).Load" + if err := types.ValidateMessage(msg); err != nil { + return errors.Wrap(ctx, err, op) + } + if msg.GetId() == "" { + return errors.New(ctx, errors.InvalidParameter, op, "given message cannot be loaded as it has no ID") + } + + switch t := msg.(type) { + case *types.NodeInformation: + err := r.loadNodeInformation(ctx, t) + if err != nil { + return errors.Wrap(ctx, err, op) + } + case *types.RootCertificates: + err := r.loadRootCertificates(ctx, t) + if err != nil { + return errors.Wrap(ctx, err, op) + } + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for Load", t)) + } + + return nil +} + +// Node information is loaded in two parts: +// * the workerAuth record +// * its certificate bundles +func (r *WorkerAuthRepositoryStorage) loadNodeInformation(ctx context.Context, node *types.NodeInformation) error { + const op = "servers.(WorkerAuthRepositoryStorage).loadNodeInformation" + if node == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing NodeInformation") + } + + authorizedWorker, err := r.findWorkerAuth(ctx, node) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if authorizedWorker == nil { + return nodee.ErrNotFound + } + + node.EncryptionPublicKeyBytes = authorizedWorker.WorkerEncryptionPubKey + node.CertificatePublicKeyPkix = authorizedWorker.WorkerSigningPubKey + + // Default values are used for key types + node.EncryptionPublicKeyType = types.KEYTYPE_X25519 + node.CertificatePublicKeyType = types.KEYTYPE_ED25519 + node.ServerEncryptionPrivateKeyType = types.KEYTYPE_X25519 + + // Decrypt private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase, kms.WithKeyId(authorizedWorker.KeyId)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + node.ServerEncryptionPrivateKeyBytes, err = decrypt(ctx, authorizedWorker.ControllerEncryptionPrivKey, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + node.RegistrationNonce, err = decrypt(ctx, authorizedWorker.Nonce, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + // Get cert bundles from the other table + certBundles, err := r.findCertBundles(ctx, node.Id) + if err != nil { + return errors.Wrap(ctx, err, op) + } + node.CertificateBundles = certBundles + + return nil +} + +func (r *WorkerAuthRepositoryStorage) findCertBundles(ctx context.Context, workerKeyId string) ([]*types.CertificateBundle, error) { + const op = "servers.(WorkerAuthRepositoryStorage).findCertBundles" + if workerKeyId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty workerKeyId") + } + + var bundles []*WorkerCertBundle + err := r.reader.SearchWhere(ctx, &bundles, "worker_key_identifier = ?", []interface{}{workerKeyId}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + certBundle := []*types.CertificateBundle{} + for _, bundle := range bundles { + thisBundle := &types.CertificateBundle{} + if err := proto.Unmarshal(bundle.WorkerCertBundle.CertBundle, thisBundle); err != nil { + return nil, errors.New(ctx, errors.Decode, op, "error unmarshaling message", errors.WithWrap(err)) + } + certBundle = append(certBundle, thisBundle) + } + + return certBundle, nil +} + +func (r *WorkerAuthRepositoryStorage) findWorkerAuth(ctx context.Context, node *types.NodeInformation) (*WorkerAuth, error) { + const op = "servers.(WorkerAuthRepositoryStorage).findWorkerAuth" + if node == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "node is nil") + } + + worker := allocWorkerAuth() + worker.WorkerKeyIdentifier = node.Id + + err := r.reader.LookupById(ctx, worker) + if err != nil { + if errors.Is(err, dbw.ErrRecordNotFound) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op) + } + + return worker, nil +} + +func (r *WorkerAuthRepositoryStorage) loadRootCertificates(ctx context.Context, cert *types.RootCertificates) error { + const op = "servers.(WorkerAuthRepositoryStorage).loadRootCertificates" + if cert == nil { + return errors.New(ctx, errors.InvalidParameter, op, "cert is nil") + } + + // See if we have the CertificateAuthority first + certAuthority := &CertificateAuthority{ + CertificateAuthority: &store.CertificateAuthority{ + PrivateId: cert.Id, + }, + } + err := r.reader.LookupById(ctx, certAuthority) + if err != nil { + return nodee.ErrNotFound + } + + // Add version to state field of cert + versionMap := &rootCertificatesVersion{Version: certAuthority.Version} + stateOpt := structs.Map(versionMap) + state, err := structpb.NewStruct(stateOpt) + if err != nil { + return errors.Wrap(ctx, err, op) + } + cert.State = state + + // Load current and next certificates + certsToFind := []string{string(NextState), string(CurrentState)} + for _, c := range certsToFind { + + rootCertificate := allocRootCertificate() + rootCert := &types.RootCertificate{} + + if err := r.reader.SearchWhere(ctx, &rootCertificate, "state = ?", []interface{}{c}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op) + } + + if rootCertificate.Certificate == nil { + return nodee.ErrNotFound + } + + rootCert.CertificateDer = rootCertificate.Certificate + rootCert.NotAfter = rootCertificate.NotValidAfter.Timestamp + rootCert.NotBefore = rootCertificate.NotValidBefore.Timestamp + rootCert.PublicKeyPkix = rootCertificate.PublicKey + rootCert.PrivateKeyType = types.KEYTYPE_ED25519 + + // decrypt private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase, kms.WithKeyId(rootCertificate.KeyId)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + rootCert.PrivateKeyPkcs8, err = decrypt(ctx, rootCertificate.PrivateKey, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + if c == string(NextState) { + cert.Next = rootCert + } + if c == string(CurrentState) { + cert.Current = rootCert + } + } + + return nil +} + +// Remove implements the Storage interface. +// Remove removes the given message. Only the ID field of the message is considered. +func (r *WorkerAuthRepositoryStorage) Remove(ctx context.Context, msg nodee.MessageWithId) error { + const op = "servers.(WorkerAuthRepositoryStorage).Remove" + if err := types.ValidateMessage(msg); err != nil { + return errors.New(ctx, errors.InvalidParameter, op, "given message cannot be removed as it has no ID") + } + + // Determine type of message to remove + switch msg.(type) { + case *types.NodeInformation: + err := r.removeNodeInformation(ctx, msg.GetId()) + if err != nil { + return errors.Wrap(ctx, err, op) + } + case *types.RootCertificates: + err := r.removeCertificateAuthority(ctx, msg.(*types.RootCertificates)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for Remove", msg)) + } + + return nil +} + +func (r *WorkerAuthRepositoryStorage) removeNodeInformation(ctx context.Context, id string) error { + const op = "servers.(WorkerAuthRepositoryStorage).removeNodeInformation" + if id == "" { + return errors.New(ctx, errors.InvalidParameter, op, "empty id") + } + + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + var err error + _, err = w.Exec(ctx, deleteWorkerAuthQuery, []interface{}{sql.Named("worker_key_identifier", id)}) + if err != nil { + return errors.Wrap(ctx, err, op) + } + _, err = w.Exec(ctx, deleteWorkerCertBundlesQuery, []interface{}{sql.Named("worker_key_identifier", id)}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete workerAuth")) + } + return nil + }, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +func (r *WorkerAuthRepositoryStorage) removeCertificateAuthority(ctx context.Context, cert *types.RootCertificates) error { + const op = "servers.(WorkerAuthRepositoryStorage).removeCertificateAuthority" + + var result rootCertificatesVersion + err := mapstructure.Decode(cert.State.AsMap(), &result) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + version := uint32(1) + if result.Version != 0 { + version = result.Version + } + certAuthority := &CertificateAuthority{ + CertificateAuthority: &store.CertificateAuthority{ + PrivateId: ca_id, + Version: version + 1, + }, + } + + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + // Update the certAuthority's version column first + rowsUpdated, err := w.Update(ctx, certAuthority, []string{"Version"}, nil, db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated != 1 { + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated CertificateAuthority and %d rows updated", rowsUpdated)) + } + + if err := r.removeRootCertificateWithWriter(ctx, string(CurrentState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + if err := r.removeRootCertificateWithWriter(ctx, string(NextState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil + }, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +func (r *WorkerAuthRepositoryStorage) removeRootCertificateWithWriter(ctx context.Context, id string, writer db.Writer) error { + const op = "servers.(WorkerAuthRepositoryStorage).removeRootCertificateWithWriter" + if id == "" { + return errors.New(ctx, errors.InvalidParameter, op, "empty id") + } + if writer == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + } + + rows, err := writer.Exec(ctx, deleteRootCertificateQuery, []interface{}{ + sql.Named("state", id), + }) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete root certificate")) + } + if rows > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 root certificate would have been deleted") + } + return nil +} + +// List implements the Storage interface. +// List returns a list of IDs; the type of the message is used to disambiguate what to list. +func (r *WorkerAuthRepositoryStorage) List(ctx context.Context, msg proto.Message) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).List" + + var err error + var ids []string + // Determine type of message to store + switch msg.(type) { + case *types.NodeInformation: + ids, err = r.listNodeInformation(ctx) + case *types.RootCertificate: + ids, err = r.listRootCertificates(ctx) + case *types.RootCertificates: + ids, err = r.listCertificateAuthority(ctx) + default: + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for List", msg)) + } + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return ids, nil +} + +// Returns a list of node auth IDs +func (r *WorkerAuthRepositoryStorage) listNodeInformation(ctx context.Context) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).listNodeCertificates" + + var where string + var nodeAuths []*WorkerAuth + err := r.reader.SearchWhere(ctx, &nodeAuths, where, []interface{}{}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var nodeIds []string + for _, auth := range nodeAuths { + nodeIds = append(nodeIds, auth.WorkerKeyIdentifier) + } + return nodeIds, nil +} + +// Returns a list of root certificates +func (r *WorkerAuthRepositoryStorage) listRootCertificates(ctx context.Context) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).listRootCertificates" + + var where string + var rootCertificates []*RootCertificate + err := r.reader.SearchWhere(ctx, &rootCertificates, where, []interface{}{}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var certIds []string + for _, cert := range rootCertificates { + certIds = append(certIds, strconv.FormatUint(cert.SerialNumber, 10)) + } + + return certIds, nil +} + +// Returns a list of certificate authorities +func (r *WorkerAuthRepositoryStorage) listCertificateAuthority(ctx context.Context) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).listCertificateAuthority" + + var where string + var rootCertificates []*CertificateAuthority + err := r.reader.SearchWhere(ctx, &rootCertificates, where, []interface{}{}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var certIds []string + for _, cert := range rootCertificates { + certIds = append(certIds, cert.PrivateId) + } + + return certIds, nil +} + +// encrypt value before writing it to the db +func encrypt(ctx context.Context, value []byte, wrapper wrapping.Wrapper) ([]byte, error) { + const op = "servers.(WorkerAuthRepositoryStorage).encrypt" + if value == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing value") + } + if wrapper == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapper") + } + + blobInfo, err := wrapper.Encrypt(ctx, value) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error encrypting recovery info", errors.WithWrap(err)) + } + marshaledBlob, err := proto.Marshal(blobInfo) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error marshaling encrypted blob", errors.WithWrap(err)) + } + return marshaledBlob, nil +} + +func decrypt(ctx context.Context, value []byte, wrapper wrapping.Wrapper) ([]byte, error) { + const op = "servers.(WorkerAuthRepositoryStorage).decrypt" + if value == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing value") + } + if wrapper == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapper") + } + + blobInfo := new(wrapping.BlobInfo) + if err := proto.Unmarshal(value, blobInfo); err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error decoding encrypted blob", errors.WithWrap(err)) + } + + marshaledInfo, err := wrapper.Decrypt(ctx, blobInfo) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error decrypting recovery info", errors.WithWrap(err)) + } + + return marshaledInfo, nil +} diff --git a/internal/servers/repository_workerauth_test.go b/internal/servers/repository_workerauth_test.go new file mode 100644 index 000000000..3e752bf86 --- /dev/null +++ b/internal/servers/repository_workerauth_test.go @@ -0,0 +1,236 @@ +package servers + +import ( + "context" + "crypto/rand" + "testing" + + "github.com/fatih/structs" + "github.com/mitchellh/mapstructure" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/types/scope" + "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/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/curve25519" +) + +// 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)} + badStateOpt := structs.Map(badVersion) + badState, err := structpb.NewStruct(badStateOpt) + 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)} + removeOpt := structs.Map(removeVersion) + removeState, err := structpb.NewStruct(removeOpt) + 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() + 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) + + worker := TestWorker(t, conn, wrapper) + + 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.NewFileStorage(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 + require.NoError(registration.AuthorizeNode(ctx, storage, fetchReq, nodeenrollment.WithState(state))) + + // 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) + + // Remove node + err = storage.Remove(ctx, nodeLookup) + require.NoError(err) + err = storage.Load(ctx, nodeLookup) + require.Error(err) +} + +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) +} diff --git a/internal/servers/store/worker_auth.pb.go b/internal/servers/store/worker_auth.pb.go index 119db8fee..88c48b18b 100644 --- a/internal/servers/store/worker_auth.pb.go +++ b/internal/servers/store/worker_auth.pb.go @@ -140,11 +140,11 @@ type WorkerCertBundle struct { unknownFields protoimpl.UnknownFields // The public key of the issuing root certificate - // @inject_tag: `gorm:"not_null"` - CertificatePublicKey []byte `protobuf:"bytes,10,opt,name=certificate_public_key,json=certificatePublicKey,proto3" json:"certificate_public_key,omitempty" gorm:"not_null"` + // @inject_tag: `gorm:"primary_key"` + RootCertificatePublicKey []byte `protobuf:"bytes,10,opt,name=root_certificate_public_key,json=rootCertificatePublicKey,proto3" json:"root_certificate_public_key,omitempty" gorm:"primary_key"` // The WorkerAuth worker_key_identifier this cert bundle record is for - // @inject_tag: `gorm:"not_null"` - WorkerKeyIdentifier string `protobuf:"bytes,20,opt,name=worker_key_identifier,json=workerKeyIdentifier,proto3" json:"worker_key_identifier,omitempty" gorm:"not_null"` + // @inject_tag: `gorm:"primary_key"` + WorkerKeyIdentifier string `protobuf:"bytes,20,opt,name=worker_key_identifier,json=workerKeyIdentifier,proto3" json:"worker_key_identifier,omitempty" gorm:"primary_key"` // CertBundle is the marshaled protobuf certificate bundle for a WorkerAuth // @inject_tag: `gorm:"not_null"` CertBundle []byte `protobuf:"bytes,30,opt,name=cert_bundle,json=certBundle,proto3" json:"cert_bundle,omitempty" gorm:"not_null"` @@ -182,9 +182,9 @@ func (*WorkerCertBundle) Descriptor() ([]byte, []int) { return file_controller_storage_servers_store_v1_worker_auth_proto_rawDescGZIP(), []int{1} } -func (x *WorkerCertBundle) GetCertificatePublicKey() []byte { +func (x *WorkerCertBundle) GetRootCertificatePublicKey() []byte { if x != nil { - return x.CertificatePublicKey + return x.RootCertificatePublicKey } return nil } @@ -231,22 +231,22 @@ var file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc = []byte{ 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x69, 0x76, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, - 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x9d, + 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0xa6, 0x01, 0x0a, 0x10, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x42, 0x75, 0x6e, - 0x64, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, - 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, - 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, - 0x0b, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x1e, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x65, 0x72, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x42, 0x3c, - 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, - 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x6c, 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x18, 0x72, 0x6f, 0x6f, 0x74, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x62, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x65, 0x72, + 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/servers/testing.go b/internal/servers/testing.go index b027d7876..fee3a0333 100644 --- a/internal/servers/testing.go +++ b/internal/servers/testing.go @@ -26,7 +26,7 @@ func populateBytes(length int) []byte { return fieldBytes } -func TestKkmsKey(ctx context.Context, t *testing.T, conn *db.DB, wrapper wrapping.Wrapper) string { +func TestKmsKey(ctx context.Context, t *testing.T, conn *db.DB, wrapper wrapping.Wrapper) string { t.Helper() org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) kmsCache := kms.TestKms(t, conn, wrapper) diff --git a/internal/servers/worker.go b/internal/servers/worker.go index f7978fb0a..4789f810f 100644 --- a/internal/servers/worker.go +++ b/internal/servers/worker.go @@ -4,12 +4,32 @@ import ( "context" "strings" + "github.com/fatih/structs" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/servers/store" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" ) +type workerAuthWorkerId struct { + WorkerId string +} + +// AttachWorkerIdToState accepts a workerId and creates a struct for use with the Nodeenrollment lib +// This is intended for use in worker authorization; AuthorizeNode in the lib accepts the option WithState +// so that the workerId is passed through to storage and associated with a WorkerAuth record +func AttachWorkerIdToState(ctx context.Context, workerId string) (*structpb.Struct, error) { + const op = "servers.(Worker).AttachWorkerIdToState" + if workerId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing workerId") + } + + workerMap := &workerAuthWorkerId{WorkerId: workerId} + stateOpt := structs.Map(workerMap) + return structpb.NewStruct(stateOpt) +} + // A Worker is a server that provides an address which can be used to proxy // session connections. It can be tagged with custom tags and is used when // authorizing and establishing a session. It is owned by a scope. diff --git a/internal/servers/worker_auth.go b/internal/servers/worker_auth.go index d3bd84907..3c7c11b56 100644 --- a/internal/servers/worker_auth.go +++ b/internal/servers/worker_auth.go @@ -56,7 +56,7 @@ func newWorkerAuth(ctx context.Context, workerKeyIdentifier, workerId string, op return l, nil } -func AllocWorkerAuth() *WorkerAuth { +func allocWorkerAuth() *WorkerAuth { return &WorkerAuth{ WorkerAuth: &store.WorkerAuth{}, } @@ -132,15 +132,15 @@ func newWorkerCertBundle(ctx context.Context, certificatePublicKey []byte, worke l := &WorkerCertBundle{ WorkerCertBundle: &store.WorkerCertBundle{ - CertificatePublicKey: certificatePublicKey, - WorkerKeyIdentifier: workerKeyIdentifier, - CertBundle: certBundle, + RootCertificatePublicKey: certificatePublicKey, + WorkerKeyIdentifier: workerKeyIdentifier, + CertBundle: certBundle, }, } return l, nil } -func AllocWorkerCertBundle() *WorkerCertBundle { +func allocWorkerCertBundle() *WorkerCertBundle { return &WorkerCertBundle{ WorkerCertBundle: &store.WorkerCertBundle{}, } @@ -156,7 +156,7 @@ func (w *WorkerCertBundle) clone() *WorkerCertBundle { // Validate is called before storing a WorkerCertBundle in the db func (w *WorkerCertBundle) ValidateNewWorkerCertBundle(ctx context.Context) error { const op = "servers.(WorkerAuth).validateNewWorkerCertBundle" - if w.CertificatePublicKey == nil { + if w.RootCertificatePublicKey == nil { return errors.New(ctx, errors.InvalidParameter, op, "missing CertificatePublicKey") } if w.WorkerKeyIdentifier == "" { diff --git a/internal/servers/workerauth_store_test.go b/internal/servers/workerauth_store_test.go index 488f5fe6a..e66d6d67d 100644 --- a/internal/servers/workerauth_store_test.go +++ b/internal/servers/workerauth_store_test.go @@ -427,7 +427,7 @@ func TestWorkerCertBundle(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - testKey := TestKkmsKey(ctx, t, conn, wrapper) + testKey := TestKmsKey(ctx, t, conn, wrapper) worker := TestWorker(t, conn, wrapper) workerAuth := TestWorkerAuth(ctx, t, conn, worker, testKey) @@ -454,9 +454,9 @@ func TestWorkerCertBundle(t *testing.T) { certificateBundle: certBundle, }, expected: &store.WorkerCertBundle{ - CertificatePublicKey: rootCA.PublicKey, - WorkerKeyIdentifier: workerAuth.WorkerKeyIdentifier, - CertBundle: certBundle, + RootCertificatePublicKey: rootCA.PublicKey, + WorkerKeyIdentifier: workerAuth.WorkerKeyIdentifier, + CertBundle: certBundle, }, wantErr: false, },