From 4527802c7691a044186adcf0b7fa7f580317c635 Mon Sep 17 00:00:00 2001 From: Irena Rindos Date: Fri, 27 Jun 2025 09:01:27 -0400 Subject: [PATCH] session(proxy cert):associate session with proxy certificates (#1606) * ce: session(proxy cert):associate session with proxy certificates --- .../handlers/targets/target_service.go | 10 +- ...t_credential_injection_certificates.up.sql | 12 ++- internal/session/options.go | 8 ++ internal/session/options_test.go | 12 +++ internal/session/repository_session.go | 92 ++++++++++++------ internal/session/repository_session_test.go | 93 +++++++++++++++++++ internal/session/rewrapping.go | 37 ++++++++ internal/session/rewrapping_test.go | 79 ++++++++++++++++ internal/session/session.go | 56 ++++++++++- internal/session/session_test.go | 18 +++- 10 files changed, 382 insertions(+), 35 deletions(-) diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 7ac52ba13..df0184aca 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -999,7 +999,15 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession for _, sw := range selectedWorkers { workerAddresses = append(workerAddresses, sw.Address) } - sess, err = sessionRepo.CreateSession(ctx, wrapper, sess, workerAddresses) + var proxyCert *session.ProxyCertificate + if t.GetProxyServerCertificate() != nil { + pc := t.GetProxyServerCertificate() + proxyCert = &session.ProxyCertificate{ + PrivateKey: pc.PrivateKeyPem, + Certificate: pc.CertificatePem, + } + } + sess, err = sessionRepo.CreateSession(ctx, wrapper, sess, workerAddresses, session.WithProxyCertificate(proxyCert)) if err != nil { return nil, err } diff --git a/internal/db/schema/migrations/oss/postgres/99903/02_target_credential_injection_certificates.up.sql b/internal/db/schema/migrations/oss/postgres/99903/02_target_credential_injection_certificates.up.sql index 06aed125e..28f9bbe56 100644 --- a/internal/db/schema/migrations/oss/postgres/99903/02_target_credential_injection_certificates.up.sql +++ b/internal/db/schema/migrations/oss/postgres/99903/02_target_credential_injection_certificates.up.sql @@ -116,14 +116,22 @@ create trigger remove_target_alias_certificates_for_updated_alias before update for each row execute procedure remove_target_alias_certificates_for_updated_alias(); create table session_proxy_certificate( - session_id wt_public_id not null + session_id wt_public_id primary key constraint session_fkey references session (public_id) on delete cascade on update cascade, certificate bytea not null constraint certificate_must_not_be_empty - check(length(certificate) > 0) + check(length(certificate) > 0), + private_key_encrypted bytea not null -- encrypted PEM encoded priv key + constraint private_key_must_not_be_empty + check(length(private_key_encrypted) > 0), + key_id kms_private_id not null -- key used to encrypt entries + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade ); comment on table session_proxy_certificate is diff --git a/internal/session/options.go b/internal/session/options.go index 6ddc9f3a5..9d0070429 100644 --- a/internal/session/options.go +++ b/internal/session/options.go @@ -42,6 +42,7 @@ type options struct { withIgnoreDecryptionFailures bool withRandomReader io.Reader withStartPageAfterItem pagination.Item + withProxyCertificate *ProxyCertificate } func getDefaultOptions() options { @@ -159,3 +160,10 @@ func WithStartPageAfterItem(item pagination.Item) Option { o.withStartPageAfterItem = item } } + +// WithProxyCertificate is used to associate a ProxyCertificate with a session. +func WithProxyCertificate(pc *ProxyCertificate) Option { + return func(o *options) { + o.withProxyCertificate = pc + } +} diff --git a/internal/session/options_test.go b/internal/session/options_test.go index f2f127e0d..351bee0f5 100644 --- a/internal/session/options_test.go +++ b/internal/session/options_test.go @@ -94,4 +94,16 @@ func Test_GetOpts(t *testing.T) { testOpts.withRandomReader = reader assert.Equal(opts, testOpts) }) + t.Run("WithProxyCertificate", func(t *testing.T) { + assert := assert.New(t) + pc := &ProxyCertificate{ + Certificate: []byte("test-cert"), + PrivateKey: []byte("test-key"), + SessionId: "s_1234", + } + opts := getOpts(WithProxyCertificate(pc)) + testOpts := getDefaultOptions() + testOpts.withProxyCertificate = pc + assert.Equal(opts, testOpts) + }) } diff --git a/internal/session/repository_session.go b/internal/session/repository_session.go index 0b82821a6..f541d344a 100644 --- a/internal/session/repository_session.go +++ b/internal/session/repository_session.go @@ -22,46 +22,38 @@ import ( // CreateSession inserts into the repository and returns the new Session with // its State of "Pending". The following fields must be empty when creating a -// session: WorkerId, and PublicId. No options are -// currently supported. -func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping.Wrapper, newSession *Session, workerAddresses []string, _ ...Option) (*Session, error) { +// session: WorkerId, and PublicId. +// Supports the withProxyCertificate option +func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping.Wrapper, newSession *Session, workerAddresses []string, opt ...Option) (*Session, error) { const op = "session.(Repository).CreateSession" - if newSession == nil { + switch { + case newSession == nil: return nil, errors.New(ctx, errors.InvalidParameter, op, "missing session") - } - if newSession.PublicId != "" { + case newSession.PublicId != "": return nil, errors.New(ctx, errors.InvalidParameter, op, "public id is not empty") - } - if len(newSession.Certificate) != 0 { + case newSession.Certificate != nil: return nil, errors.New(ctx, errors.InvalidParameter, op, "certificate is not empty") - } - if newSession.TargetId == "" { + case newSession.TargetId == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target id") - } - if newSession.UserId == "" { + case newSession.UserId == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id") - } - if newSession.AuthTokenId == "" { + case newSession.AuthTokenId == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token id") - } - if newSession.ProjectId == "" { + case newSession.ProjectId == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") - } - if newSession.HostId == "" && newSession.HostSetId == "" && newSession.Endpoint == "" { + case newSession.HostId != "" && newSession.HostSetId != "" && newSession.Endpoint == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "missing host source and endpoint") - } - if newSession.CtTofuToken != nil { + case newSession.CtTofuToken != nil: return nil, errors.New(ctx, errors.InvalidParameter, op, "ct is not empty") - } - if newSession.TofuToken != nil { + case newSession.TofuToken != nil: return nil, errors.New(ctx, errors.InvalidParameter, op, "tofu token is not empty") - } - if newSession.ExpirationTime == nil || newSession.ExpirationTime.Timestamp.AsTime().IsZero() { + case newSession.ExpirationTime == nil || newSession.ExpirationTime.Timestamp.AsTime().IsZero(): return nil, errors.New(ctx, errors.InvalidParameter, op, "missing expiration time") + case len(workerAddresses) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing worker addresses") } - if len(workerAddresses) == 0 { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing addresses") - } + + opts := getOpts(opt...) id, err := newId(ctx) if err != nil { @@ -146,6 +138,22 @@ func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping. returnedSession.StaticCredentials = c } + if opts.withProxyCertificate != nil { + sessionProxyCertificate := opts.withProxyCertificate + sessionProxyCertificate.SessionId = newSession.PublicId + + if len(sessionProxyCertificate.PrivateKey) == 0 || len(sessionProxyCertificate.Certificate) == 0 { + return errors.New(ctx, errors.InvalidParameter, op, "proxy certificate private key or certificate is empty") + } + err := sessionProxyCertificate.Encrypt(ctx, sessionWrapper) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to encrypt proxy certificate")) + } + if err = w.Create(ctx, sessionProxyCertificate); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to create proxy certificate")) + } + } + // TODO: after upgrading to gorm v2 this batch insert can be replaced, since gorm v2 supports batch inserts q, batchInsertArgs, err := batchInsertSessionCredentialDynamic(newSession.DynamicCredentials) if err == nil { @@ -979,3 +987,33 @@ func decrypt(ctx context.Context, kmsRepo kms.GetWrapperer, session *Session) er } return nil } + +// LookupProxyCertificate will look up a proxy certificate in the repository by session ID. +// If not found, it returns nil, nil. +func (r *Repository) LookupProxyCertificate(ctx context.Context, projectId, sessionId string) (*ProxyCertificate, error) { + const op = "session.(Repository).LookupProxyCertificate" + switch { + case projectId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + case sessionId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing session id") + } + + proxyCert := allocProxyCertificate() + proxyCert.SessionId = sessionId + if err := r.reader.LookupById(ctx, proxyCert); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op) + } + wrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeSessions) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get session wrapper")) + } + err = proxyCert.Decrypt(ctx, wrapper) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to decrypt proxy certificate")) + } + return proxyCert, nil +} diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index cd5ec2748..ba3032fdd 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -1911,3 +1911,96 @@ func TestRepository_CheckIfNoLongerActive(t *testing.T) { } assert.ElementsMatch(t, gotIds, []string{unrecognizedSessionId, terminatedSession.PublicId, cancelingSess.PublicId}) } + +func TestRepository_LookupProxyCertificate(t *testing.T) { + ctx := t.Context() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, err) + iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) // despite not looking like it, this is necessary for some reason + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), proj.PublicId, kms.KeyPurposeSessions) + require.NoError(t, err) + + at := authtoken.TestAuthToken(t, conn, kmsCache, org.GetPublicId()) + uId := at.GetIamUserId() + hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + tar := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + session := TestSession(t, conn, wrapper, ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ProjectId: proj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + + privKeyValue := []byte("fake-private-key") + certValue := []byte("fake-cert-value") + cert, err := NewProxyCertificate(ctx, session.PublicId, privKeyValue, certValue) + require.NoError(t, err) + require.NotNil(t, cert) + + err = cert.Encrypt(ctx, kmsWrapper) + require.NoError(t, err) + err = rw.Create(ctx, cert) + require.NoError(t, err) + tests := []struct { + name string + projectId string + sessionId string + wantNotFound bool + wantErr bool + wantErrContains string + }{ + { + name: "success-lookup", + projectId: proj.GetPublicId(), + sessionId: session.PublicId, + }, + { + name: "not-found", + projectId: proj.GetPublicId(), + sessionId: "fake-session-not-found", + wantNotFound: true, + }, + { + name: "missing-public-id", + sessionId: session.PublicId, + wantErr: true, + wantErrContains: "missing project id", + }, + { + name: "missing-session-id", + projectId: proj.GetPublicId(), + wantErr: true, + wantErrContains: "missing session id", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + got, err := repo.LookupProxyCertificate(ctx, tt.projectId, tt.sessionId) + if tt.wantErr { + assert.Contains(err.Error(), tt.wantErrContains) + return + } + require.NoError(err) + if tt.wantNotFound { + require.Nil(got) + return + } + require.NotNil(got) + assert.Equal(got.PrivateKey, privKeyValue) + assert.Equal(got.Certificate, certValue) + }) + } +} diff --git a/internal/session/rewrapping.go b/internal/session/rewrapping.go index 947cf41bd..e5c263523 100644 --- a/internal/session/rewrapping.go +++ b/internal/session/rewrapping.go @@ -15,6 +15,7 @@ import ( func init() { kms.RegisterTableRewrapFn(defaultSessionTableName, sessionRewrapFn) kms.RegisterTableRewrapFn("session_credential", sessionCredentialRewrapFn) + kms.RegisterTableRewrapFn("session_proxy_certificate", sessionProxyCertificateRewrapFn) } func rewrapParameterChecks(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) string { @@ -125,3 +126,39 @@ func sessionRewrapFn(ctx context.Context, dataKeyVersionId string, scopeId strin } return nil } + +func sessionProxyCertificateRewrapFn(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error { + const op = "session.sessionProxyCertificateRewrapFn" + switch { + case dataKeyVersionId == "": + return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id") + case scopeId == "": + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + case util.IsNil(reader): + return errors.New(ctx, errors.InvalidParameter, op, "missing database reader") + case util.IsNil(writer): + return errors.New(ctx, errors.InvalidParameter, op, "missing database writer") + case kmsRepo == nil: + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository") + } + var certs []*ProxyCertificate + if err := reader.SearchWhere(ctx, &certs, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeSessions) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, ck := range certs { + if err := ck.Decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt proxy certificate")) + } + if err := ck.Encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt proxy certificate")) + } + if _, err := writer.Update(ctx, ck, []string{"PrivateKeyEncrypted", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to rewrap proxy certificate")) + } + } + return nil +} diff --git a/internal/session/rewrapping_test.go b/internal/session/rewrapping_test.go index 9b91c8611..94af2078e 100644 --- a/internal/session/rewrapping_test.go +++ b/internal/session/rewrapping_test.go @@ -235,3 +235,82 @@ func TestRewrap_sessionRewrapFn(t *testing.T) { assert.Empty(t, got.KeyId) }) } + +func TestRewrap_sessionProxyCertificateRewrapFn(t *testing.T) { + ctx := t.Context() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT \$1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + mock.ExpectQuery( + `SELECT \* FROM "kms_oplog_schema_version" WHERE 1=1 ORDER BY "kms_oplog_schema_version"."version" LIMIT \$1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "session_proxy_certificate" WHERE key_id=\$1`, + ).WillReturnError(errors.New("Query error")) + err := sessionProxyCertificateRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) // despite not looking like it, this is necessary for some reason + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), proj.PublicId, kms.KeyPurposeSessions) + require.NoError(t, err) + + at := authtoken.TestAuthToken(t, conn, kmsCache, org.GetPublicId()) + uId := at.GetIamUserId() + hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + tar := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + session := TestSession(t, conn, wrapper, ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ProjectId: proj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + + encryptedKeyValue := []byte("fake-encrypted-key") + cert, err := NewProxyCertificate(ctx, session.PublicId, encryptedKeyValue, []byte("fake-cert")) + require.NoError(t, err) + require.NotNil(t, cert) + + err = cert.Encrypt(ctx, kmsWrapper) + require.NoError(t, err) + err = rw.Create(ctx, cert) + require.NoError(t, err) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, proj.PublicId)) + assert.NoError(t, sessionProxyCertificateRewrapFn(ctx, cert.KeyId, proj.PublicId, rw, rw, kmsCache)) + + // now we pull it from the db, decrypt it with the new key, and ensure things match + got := allocProxyCertificate() + got.SessionId = cert.SessionId + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeSessions, kms.WithKeyId(got.KeyId)) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.Decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.KeyId) + assert.Equal(t, newKeyVersionId, got.KeyId) + assert.Equal(t, cert.PrivateKey, got.PrivateKey) + assert.Equal(t, cert.Certificate, got.Certificate) + }) +} diff --git a/internal/session/session.go b/internal/session/session.go index b1987daef..a3696a534 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -613,27 +613,33 @@ func (s *deletedSession) TableName() string { // It's stored during session authorization and used at connection authorization // time to lookup the proxy certificate, if applicable type ProxyCertificate struct { - SessionId string `gorm:"not_null"` - Certificate []byte `gorm:"not_null"` + SessionId string `gorm:"primary_key"` + Certificate []byte `gorm:"not_null"` + PrivateKey []byte `gorm:"-" wrapping:"pt,private_key"` + PrivateKeyEncrypted []byte `gorm:"not_null" wrapping:"ct,private_key"` + KeyId string `gorm:"not_null"` } // NewProxyCertificate creates a new in memory ProxyCertificate -func NewProxyCertificate(ctx context.Context, sessionId string, certificate []byte) (*ProxyCertificate, error) { +func NewProxyCertificate(ctx context.Context, sessionId string, privateKey, certificate []byte) (*ProxyCertificate, error) { const op = "session.NewProxyCertificate" switch { case sessionId == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "missing session id") case len(certificate) == 0: return nil, errors.New(ctx, errors.InvalidParameter, op, "missing certificate") + case len(privateKey) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing private key") } return &ProxyCertificate{ SessionId: sessionId, Certificate: certificate, + PrivateKey: privateKey, }, nil } -func alloProxyCertificate() *ProxyCertificate { +func allocProxyCertificate() *ProxyCertificate { return &ProxyCertificate{} } @@ -641,11 +647,20 @@ func alloProxyCertificate() *ProxyCertificate { func (t *ProxyCertificate) Clone() *ProxyCertificate { spc := &ProxyCertificate{ SessionId: t.SessionId, + KeyId: t.KeyId, } if t.Certificate != nil { spc.Certificate = make([]byte, len(t.Certificate)) copy(spc.Certificate, t.Certificate) } + if t.PrivateKey != nil { + spc.PrivateKey = make([]byte, len(t.PrivateKey)) + copy(spc.PrivateKey, t.PrivateKey) + } + if t.PrivateKeyEncrypted != nil { + spc.PrivateKeyEncrypted = make([]byte, len(t.PrivateKeyEncrypted)) + copy(spc.PrivateKeyEncrypted, t.PrivateKeyEncrypted) + } return spc } @@ -658,11 +673,44 @@ func (t *ProxyCertificate) VetForWrite(ctx context.Context, _ db.Reader, opType return errors.New(ctx, errors.InvalidParameter, op, "missing session id") case len(t.Certificate) == 0: return errors.New(ctx, errors.InvalidParameter, op, "missing certificate") + case len(t.PrivateKeyEncrypted) == 0: + return errors.New(ctx, errors.InvalidParameter, op, "missing encrypted private key") + case t.KeyId == "": + return errors.New(ctx, errors.InvalidParameter, op, "missing key id") } return nil } +// Encrypt the proxy cert before writing it to the db +func (t *ProxyCertificate) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "session.(ProxyCertificate).Encrypt" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.WrapStruct(ctx, cipher, t, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + keyId, err := cipher.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id")) + } + t.KeyId = keyId + return nil +} + +// Decrypt the proxy cert after reading it from the db +func (t *ProxyCertificate) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "session.(ProxyCertificate).Decrypt" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.UnwrapStruct(ctx, cipher, t, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} + // TableName returns the table name. func (t *ProxyCertificate) TableName() string { return "session_proxy_certificate" diff --git a/internal/session/session_test.go b/internal/session/session_test.go index e511fec36..4c7513f21 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -443,11 +443,16 @@ func TestProxyCertificate(t *testing.T) { if _, err := rand.Read(cert); err != nil { require.NotNil(t, err) } + privKey := make([]byte, 20) + if _, err := rand.Read(privKey); err != nil { + require.NotNil(t, err) + } tests := []struct { name string sessionId string certificate []byte + privKey []byte expected *ProxyCertificate wantErr bool wantErrContains string @@ -456,23 +461,34 @@ func TestProxyCertificate(t *testing.T) { name: "valid-target-cert", sessionId: sId, certificate: cert, + privKey: privKey, expected: &ProxyCertificate{ SessionId: sId, Certificate: cert, + PrivateKey: privKey, }, }, { name: "missing-session-id", certificate: cert, + privKey: privKey, wantErr: true, wantErrContains: "missing session id", }, { name: "missing-cert", sessionId: sId, + privKey: privKey, wantErr: true, wantErrContains: "missing certificate", }, + { + name: "missing-priv-key", + sessionId: sId, + certificate: cert, + wantErr: true, + wantErrContains: "missing private key", + }, } for _, tt := range tests { @@ -480,7 +496,7 @@ func TestProxyCertificate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - gotCert, err := NewProxyCertificate(ctx, tt.sessionId, tt.certificate) + gotCert, err := NewProxyCertificate(ctx, tt.sessionId, tt.privKey, tt.certificate) if tt.wantErr { assert.Error(err)