rdp(repo): persist target proxy certificates

This commit is contained in:
irenarindos committed 2025-09-02 15:13:45 -04:00
1 parent 6afb42e334
commit 92e6a6de26
13 files changed
+1237 -59

No files matched your search

+3
View File
@@ -135,6 +135,9 @@ const (
// TargetAliasPrefix is the prefix for target aliases
TargetAliasPrefix = "alt"
// ProxyServerCertificatePrefix is the prefix for proxy server certificates
ProxyServerCertificatePrefix = "psc"
)
type ResourceInfo struct {
@@ -734,18 +734,20 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession
return nil, err
}
var targetAlias *talias.Alias
var err error
if ctxAlias := alias.FromContext(ctx); ctxAlias != nil {
a, err := s.resolveAlias(ctx, ctxAlias.PublicId)
targetAlias, err = s.resolveAlias(ctx, ctxAlias.PublicId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if a.HostId != "" {
if req.GetHostId() != "" && req.GetHostId() != a.HostId {
if targetAlias.HostId != "" {
if req.GetHostId() != "" && req.GetHostId() != targetAlias.HostId {
return nil, handlers.InvalidArgumentErrorf("Errors in provided fields.", map[string]string{
"host_id": "The host id specified in the request does not match the one provided by the alias. Consider omitting the host id in the request.",
})
}
req.HostId = a.HostId
req.HostId = targetAlias.HostId
}
}
@@ -803,7 +805,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession
if err != nil {
return nil, err
}
t, err := repo.LookupTarget(ctx, roundTripTarget.GetPublicId())
t, err := repo.LookupTargetForSessionAuthorization(ctx, roundTripTarget.GetPublicId(), target.WithAlias(targetAlias))
if err != nil {
if errors.IsNotFoundError(err) {
return nil, handlers.NotFoundErrorf("Target %q not found.", roundTripTarget.GetPublicId())
+8 -2
View File
@@ -5,6 +5,12 @@ package target
// Expose functions for use in tests.
var (
AllocTargetView = allocTargetView
TargetsViewDefaultTable = targetsViewDefaultTable
AllocTargetView = allocTargetView
TargetsViewDefaultTable = targetsViewDefaultTable
ProxyCertRewrapFn = proxyCertRewrapFn
ProxyAliasCertRewrapFn = proxyAliasCertRewrapFn
AllocTargetProxyCertificate = allocTargetProxyCertificate
AllocTargetAliasProxyCertificate = allocTargetAliasProxyCertificate
FetchTargetProxyServerCertificate = fetchTargetProxyServerCertificate
FetchTargetAliasProxyServerCertificate = fetchTargetAliasProxyServerCertificate
)
+2 -2
View File
@@ -55,8 +55,8 @@ type options struct {
WithEnableSessionRecording bool
WithNetResolver intglobals.NetIpResolver
WithStartPageAfterItem pagination.Item
WithAlias *talias.Alias
withAliases []*talias.Alias
withAlias *talias.Alias
withTargetId string
}
@@ -291,7 +291,7 @@ func WithAliases(in []*talias.Alias) Option {
// WithAlias provides an option to provide a single alias.
func WithAlias(in *talias.Alias) Option {
return func(o *options) {
o.withAlias = in
o.WithAlias = in
}
}
+1 -1
View File
@@ -273,7 +273,7 @@ func Test_GetOpts(t *testing.T) {
al, err := talias.NewAlias(context.Background(), "global", "test")
require.NoError(err)
opts := GetOpts(WithAlias(al))
assert.Equal(al, opts.withAlias)
assert.Equal(al, opts.WithAlias)
})
t.Run("WithTargetId", func(t *testing.T) {
assert := assert.New(t)
+117 -5
View File
@@ -89,9 +89,90 @@ func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms,
}, nil
}
// LookupTargetForSessionAuthorization will look up a target in the repository and return the target
// with its host source ids, credential source ids, and server certificate, if applicable. If the target is not
// found, it will return nil, nil.
// Supported option: WithAlias if the session authorization uses a target alias
func (r *Repository) LookupTargetForSessionAuthorization(ctx context.Context, publicId string, opt ...Option) (Target, error) {
const op = "target.(Repository).LookupTargetForSessionAuthorization"
opts := GetOpts(opt...)
if publicId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
target := allocTargetView()
target.PublicId = publicId
var address string
var hostSources []HostSource
var credSources []CredentialSource
var cert *ServerCertificate
_, err := r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(read db.Reader, w db.Writer) error {
lookupErr := read.LookupById(ctx, &target)
if lookupErr != nil {
return errors.Wrap(ctx, lookupErr, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId)))
}
var err error
if hostSources, err = fetchHostSources(ctx, read, target.PublicId); err != nil {
return errors.Wrap(ctx, err, op)
}
if credSources, err = fetchCredentialSources(ctx, read, target.PublicId); err != nil {
return errors.Wrap(ctx, err, op)
}
targetAddress, err := fetchAddress(ctx, read, target.PublicId)
if err != nil && !errors.IsNotFoundError(err) {
return errors.Wrap(ctx, err, op)
}
if targetAddress != nil {
address = targetAddress.GetAddress()
}
databaseWrapper, err := r.kms.GetWrapper(ctx, target.GetProjectId(), kms.KeyPurposeDatabase)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
if opts.WithAlias != nil {
cert, err = fetchTargetAliasProxyServerCertificate(ctx, read, w, target.PublicId, target.ProjectId, opts.WithAlias, databaseWrapper, target.GetSessionMaxSeconds())
if err != nil && !errors.IsNotFoundError(err) {
return errors.Wrap(ctx, err, op)
}
} else {
cert, err = fetchTargetProxyServerCertificate(ctx, read, w, target.PublicId, target.ProjectId, databaseWrapper, target.GetSessionMaxSeconds())
if err != nil && !errors.IsNotFoundError(err) {
return errors.Wrap(ctx, err, op)
}
}
return nil
},
)
if err != nil {
if errors.IsNotFoundError(err) {
return nil, nil
}
return nil, errors.Wrap(ctx, err, op)
}
subtype, err := target.targetSubtype(ctx, address)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
subtype.SetHostSources(hostSources)
subtype.SetCredentialSources(credSources)
subtype.SetProxyServerCertificate(cert)
return subtype, nil
}
// LookupTarget will look up a target in the repository and return the target
// with its host source ids and credential source ids. If the target is not
// found, it will return nil, nil, nil, nil. No options are currently supported.
// found, it will return nil, nil.
// Supported options: WithName, WithProjectId, and WithProjectName
func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, opt ...Option) (Target, error) {
const op = "target.(Repository).LookupTarget"
opts := GetOpts(opt...)
@@ -513,9 +594,10 @@ func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Opt
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target")
}
vet, ok := subtypeRegistry.vetFunc(target.GetType())
targetType := target.GetType()
vet, ok := subtypeRegistry.vetFunc(targetType)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType()))
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", targetType))
}
if err := vet(ctx, target); err != nil {
return nil, err
@@ -537,9 +619,9 @@ func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Opt
return nil, err
}
} else {
prefix, ok := subtypeRegistry.idPrefix(target.GetType())
prefix, ok := subtypeRegistry.idPrefix(targetType)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType()))
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", targetType))
}
id, err := db.NewPublicId(ctx, prefix)
if err != nil {
@@ -562,11 +644,18 @@ func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Opt
}
}
serverCert := t.GetProxyServerCertificate()
oplogWrapper, err := r.kms.GetWrapper(ctx, target.GetProjectId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
databaseWrapper, err := r.kms.GetWrapper(ctx, target.GetProjectId(), kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
metadata := t.Oplog(oplog.OpType_OP_TYPE_CREATE)
var createdAliases []*talias.Alias
var returnedTarget Target
@@ -613,6 +702,29 @@ func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Opt
msgs = append(msgs, &targetAliasOplogMsg)
}
if serverCert != nil {
proxyCert := allocTargetProxyCertificate()
err = proxyCert.fromServerCertificate(ctx, serverCert)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert server certificate to target proxy certificate"))
}
proxyCert.TargetId = t.GetPublicId()
id, err := db.NewPublicId(ctx, globals.ProxyServerCertificatePrefix)
if err != nil {
return errors.Wrap(ctx, err, op)
}
proxyCert.PublicId = id
if err := proxyCert.Encrypt(ctx, databaseWrapper); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error encrypting target proxy certificate"))
}
var targetProxyCertOplogMsg oplog.Message
if err := w.Create(ctx, proxyCert, db.NewOplogMsg(&targetProxyCertOplogMsg)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target proxy certificate"))
}
msgs = append(msgs, &targetProxyCertOplogMsg)
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
@@ -0,0 +1,167 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package target
import (
"context"
"time"
"github.com/hashicorp/boundary/globals"
talias "github.com/hashicorp/boundary/internal/alias/target"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/util"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
)
type proxyServerCertificate interface {
Decrypt(ctx context.Context, wrapper wrapping.Wrapper) error
Encrypt(ctx context.Context, wrapper wrapping.Wrapper) error
GetNotValidAfter() *timestamp.Timestamp
SetPrivateKey(privKeyBytes []byte)
SetPublicKey(pubKeyBytes []byte)
SetCertificate(certBytes []byte)
SetNotValidAfter(timestamp *timestamp.Timestamp)
ToServerCertificate(ctx context.Context) (*ServerCertificate, error)
}
// Check if the certificate is close to expiration and regenerate/ update it if necessary.
func maybeRegenerateCert(ctx context.Context, t proxyServerCertificate, w db.Writer, wrapper wrapping.Wrapper, sessionMaxSeconds uint32, opt ...Option) (*ServerCertificate, error) {
const op = "target.maybeRegenerateCert"
switch {
case wrapper == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "wrapper is nil")
case t == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "target is nil")
case util.IsNil(w):
return nil, errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
case sessionMaxSeconds == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "session max seconds is 0")
}
// Check if the certificate is close to expiration. We consider it close to expiration if it is less than 1 day + sessionMaxSeconds away from now.
if t.GetNotValidAfter().AsTime().Before(time.Now().Add((time.Hour * 24) + (time.Second * time.Duration(sessionMaxSeconds)))) {
notValidAfter := time.Now().AddDate(1, 0, 0) // 1 year from now
privKey, pubKey, cert, err := generateKeysAndCert(ctx, notValidAfter, opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating keys and certificate for proxy server certificate"))
}
t.SetPrivateKey(privKey)
t.SetPublicKey(pubKey)
t.SetCertificate(cert)
t.SetNotValidAfter(timestamp.New(notValidAfter))
if err := t.Encrypt(ctx, wrapper); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error encrypting proxy certificate"))
}
var rowsUpdated int
rowsUpdated, err = w.Update(ctx, t, []string{"PublicKey", "PrivateKey", "Certificate", "NotValidAfter"}, nil)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error updating proxy server certificate"))
}
if rowsUpdated != 1 {
return nil, errors.New(ctx, errors.MultipleRecords, op, "more than 1 proxy server certificate would have been updated")
}
}
if err := t.Decrypt(ctx, wrapper); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to decrypt proxy server certificate private key"))
}
serverCert, err := t.ToServerCertificate(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error converting proxy server certificate to PEM"))
}
return serverCert, nil
}
func fetchTargetProxyServerCertificate(ctx context.Context, r db.Reader, w db.Writer, targetId, scopeId string, wrapper wrapping.Wrapper, sessionMaxSeconds uint32) (*ServerCertificate, error) {
const op = "target.fetchTargetProxyServerCert"
switch {
case wrapper == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "wrapper is nil")
case util.IsNil(r):
return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil")
case util.IsNil(w):
return nil, errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
case targetId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "target id is empty")
case scopeId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "scope id is empty")
}
targetCert := allocTargetProxyCertificate()
if err := r.SearchWhere(ctx, &targetCert, "target_id = ?", []any{targetId}); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
// If a cert is not found, this target type does not support proxy server certificates/ it was not generated during target creation.
if targetCert.Certificate == nil {
return nil, errors.New(ctx, errors.RecordNotFound, op, "target proxy server certificate not found")
}
return maybeRegenerateCert(ctx, targetCert, w, wrapper, sessionMaxSeconds)
}
func fetchTargetAliasProxyServerCertificate(ctx context.Context, r db.Reader, w db.Writer, targetId, scopeId string, alias *talias.Alias, wrapper wrapping.Wrapper, sessionMaxSeconds uint32) (*ServerCertificate, error) {
const op = "target.fetchTargetProxyServerCert"
switch {
case wrapper == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "wrapper is nil")
case util.IsNil(r):
return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil")
case util.IsNil(w):
return nil, errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
case alias == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "alias is nil")
case alias.Value == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "alias value is empty")
case targetId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "target id is empty")
case scopeId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "scope id is empty")
}
// We first check if a localhost cert exists for this target; if it does not, one was not generated during
// target creation because it is not applicable and we don't need an alias proxy cert.
targetCert := allocTargetProxyCertificate()
if err := r.SearchWhere(ctx, &targetCert, "target_id = ?", []any{targetId}); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if targetCert.Certificate == nil {
return nil, errors.New(ctx, errors.RecordNotFound, op, "target proxy server certificate not found")
}
aliasCert := allocTargetAliasProxyCertificate()
if err := r.SearchWhere(ctx, &aliasCert, "target_id = ? and alias_id = ?", []any{targetId, alias.PublicId}); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
// Create the cert, if not found- alias certs are not created as part of target creation.
var err error
if aliasCert.Certificate == nil {
aliasCert, err = NewTargetAliasProxyCertificate(ctx, targetId, alias)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating new target alias proxy certificate"))
}
if err = aliasCert.Encrypt(ctx, wrapper); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error encrypting target alias proxy certificate"))
}
id, err := db.NewPublicId(ctx, globals.ProxyServerCertificatePrefix)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
aliasCert.PublicId = id
if err = w.Create(ctx, aliasCert); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target alias proxy certificate"))
}
}
return maybeRegenerateCert(ctx, aliasCert, w, wrapper, sessionMaxSeconds, WithAlias(alias))
}
@@ -0,0 +1,385 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package target_test
import (
"context"
"testing"
"github.com/hashicorp/boundary/globals"
talias "github.com/hashicorp/boundary/internal/alias/target"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/target/targettest"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFetchTargetProxyServerCertificate(t *testing.T) {
t.Parallel()
ctx := context.Background()
wrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, wrapper)
_, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
rw := db.New(conn)
databaseWrapper, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
tar := targettest.TestNewTestTarget(ctx, t, conn, proj.PublicId, "test-target")
// Create our default localhost target cert
cer, err := target.NewTargetProxyCertificate(ctx, target.WithTargetId(tar.GetPublicId()))
require.NoError(t, err)
require.NotNil(t, cer)
id, err := db.NewPublicId(ctx, globals.ProxyServerCertificatePrefix)
require.NoError(t, err)
cer.PublicId = id
err = cer.Encrypt(ctx, databaseWrapper)
require.NoError(t, err)
err = rw.Create(ctx, cer)
require.NoError(t, err)
tests := []struct {
name string
reader db.Reader
writer db.Writer
targetId string
scopeId string
wrapper wrapping.Wrapper
sessionMaxSecs uint32
wantErr bool
wantErrContains string
}{
{
name: "successful-lookup",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
},
{
name: "successful-max-secs-causes-cert-regen",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
wrapper: databaseWrapper,
sessionMaxSecs: 60 * 60 * 24 * 600, // well over a year
},
{
name: "missing-reader",
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "reader is nil",
},
{
name: "missing-writer",
reader: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "writer is nil",
},
{
name: "missing-target-id",
reader: rw,
writer: rw,
scopeId: proj.PublicId,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "target id is empty",
},
{
name: "missing-scope-id",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "scope id is empty",
},
{
name: "missing-wrapper",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "wrapper is nil",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := target.FetchTargetProxyServerCertificate(ctx, tt.reader, tt.writer, tt.targetId, tt.scopeId, tt.wrapper, tt.sessionMaxSecs)
if tt.wantErr {
require.Error(err)
require.Contains(err.Error(), tt.wantErrContains)
return
}
require.NoError(err)
assert.NotNil(got)
})
}
}
func TestFetchTargetAliasProxyServerCertificate(t *testing.T) {
t.Parallel()
ctx := context.Background()
wrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, wrapper)
_, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
rw := db.New(conn)
databaseWrapper, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
tar := targettest.TestNewTestTarget(ctx, t, conn, proj.PublicId, "test-target")
// Create our default localhost target cert
cer, err := target.NewTargetProxyCertificate(ctx, target.WithTargetId(tar.GetPublicId()))
require.NoError(t, err)
require.NotNil(t, cer)
id, err := db.NewPublicId(ctx, globals.ProxyServerCertificatePrefix)
require.NoError(t, err)
cer.PublicId = id
err = cer.Encrypt(ctx, databaseWrapper)
require.NoError(t, err)
err = rw.Create(ctx, cer)
require.NoError(t, err)
// Create an alias
aliasValue := "test-alias"
alias := talias.TestAlias(t, rw, aliasValue, talias.WithDestinationId(tar.GetPublicId()))
require.NoError(t, err)
require.NotNil(t, alias)
tests := []struct {
name string
reader db.Reader
writer db.Writer
targetId string
scopeId string
alias *talias.Alias
wrapper wrapping.Wrapper
sessionMaxSecs uint32
wantErr bool
wantErrContains string
}{
{
name: "successful-lookup",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
alias: alias,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
},
{
name: "successful-max-secs-causes-cert-regen",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
alias: alias,
wrapper: databaseWrapper,
sessionMaxSecs: 60 * 60 * 24 * 600, // well over a year
},
{
name: "missing-reader",
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
alias: alias,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "reader is nil",
},
{
name: "missing-writer",
reader: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
alias: alias,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "writer is nil",
},
{
name: "missing-target-id",
reader: rw,
writer: rw,
scopeId: proj.PublicId,
alias: alias,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "target id is empty",
},
{
name: "missing-scope-id",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
alias: alias,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "scope id is empty",
},
{
name: "missing-wrapper",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
alias: alias,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "wrapper is nil",
},
{
name: "missing-alias",
reader: rw,
writer: rw,
targetId: tar.GetPublicId(),
scopeId: proj.PublicId,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "alias is nil",
},
{
name: "no-existing-localhost-cert-for-target",
reader: rw,
writer: rw,
targetId: "fake-id",
alias: alias,
scopeId: proj.PublicId,
wrapper: databaseWrapper,
sessionMaxSecs: 28800,
wantErr: true,
wantErrContains: "target proxy server certificate not found",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := target.FetchTargetAliasProxyServerCertificate(ctx, tt.reader, tt.writer, tt.targetId, tt.scopeId, tt.alias, tt.wrapper, tt.sessionMaxSecs)
if tt.wantErr {
require.Error(err)
require.Contains(err.Error(), tt.wantErrContains)
return
}
require.NoError(err)
assert.NotNil(got)
})
}
}
func Test_FetchCertsWithinLookupTargetForSessionAuthorization(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, conn, wrapper)
_, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
repo, err := target.NewRepository(context.Background(), rw, rw, testKms)
require.NoError(t, err)
databaseWrapper, err := testKms.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
tar := targettest.TestNewTestTarget(ctx, t, conn, proj.PublicId, "test-target")
tar2 := targettest.TestNewTestTarget(ctx, t, conn, proj.PublicId, "test-target2")
// Create an alias
aliasValue := "test-alias"
alias := talias.TestAlias(t, rw, aliasValue, talias.WithDestinationId(tar.GetPublicId()))
require.NoError(t, err)
require.NotNil(t, alias)
// Create our default localhost target cert
cer, err := target.NewTargetProxyCertificate(ctx, target.WithTargetId(tar.GetPublicId()))
require.NoError(t, err)
require.NotNil(t, cer)
id, err := db.NewPublicId(ctx, globals.ProxyServerCertificatePrefix)
require.NoError(t, err)
cer.PublicId = id
err = cer.Encrypt(ctx, databaseWrapper)
require.NoError(t, err)
err = rw.Create(ctx, cer)
require.NoError(t, err)
tests := []struct {
name string
publicId string
opt []target.Option
wantCert bool
}{
{
name: "success-get-target-with-certificate",
publicId: tar.GetPublicId(),
wantCert: true,
},
{
name: "success-get-target-with-alias-certificate",
publicId: tar.GetPublicId(),
opt: []target.Option{
target.WithAlias(alias),
},
wantCert: true,
},
{
name: "success-get-target-no-cert",
publicId: tar2.GetPublicId(),
wantCert: false,
},
{
name: "success-get-target-no-cert-with-alias",
publicId: tar2.GetPublicId(),
opt: []target.Option{
target.WithAlias(alias),
},
wantCert: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := repo.LookupTargetForSessionAuthorization(ctx, tt.publicId, tt.opt...)
require.NoError(err)
assert.NotNil(got)
if tt.wantCert {
assert.NotNil(got.GetProxyServerCertificate())
} else {
assert.Nil(got.GetProxyServerCertificate())
}
})
}
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package target
import (
"context"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/util"
)
func init() {
kms.RegisterTableRewrapFn("target_proxy_certificate", proxyCertRewrapFn)
kms.RegisterTableRewrapFn("target_alias_proxy_certificate", proxyAliasCertRewrapFn)
}
func proxyCertRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error {
const op = "target.proxyCertRewrapFn"
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 []*TargetProxyCertificate
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.KeyPurposeDatabase)
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
}
func proxyAliasCertRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error {
const op = "target.proxyAliasCertRewrapFn"
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 []*TargetAliasProxyCertificate
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.KeyPurposeDatabase)
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 alias 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 alias certificate"))
}
}
return nil
}
+171
View File
@@ -0,0 +1,171 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package target_test
import (
"context"
"crypto/x509"
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/hashicorp/boundary/globals"
talias "github.com/hashicorp/boundary/internal/alias/target"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/target/targettest"
"github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRewrap_CertRewrapFn(t *testing.T) {
ctx := context.Background()
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 "target_proxy_certificate" WHERE key_id=\$1`,
).WillReturnError(errors.New("Query error"))
err := target.ProxyCertRewrapFn(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
_, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
kmsWrapper, err := kmsCache.GetWrapper(context.Background(), proj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
tar := targettest.TestNewTestTarget(ctx, t, conn, proj.PublicId, "test-target")
certKey, err := target.NewTargetProxyCertificate(ctx)
require.NoError(t, err)
certKey.TargetId = tar.GetPublicId()
id, err := db.NewPublicId(ctx, globals.ProxyServerCertificatePrefix)
require.NoError(t, err)
certKey.PublicId = id
err = certKey.Encrypt(ctx, kmsWrapper)
require.NoError(t, err)
err = rw.Create(ctx, certKey)
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, target.ProxyCertRewrapFn(ctx, certKey.KeyId, proj.PublicId, rw, rw, kmsCache))
// now we pull it from the db, decrypt it with the new key, and ensure things match
got := target.AllocTargetProxyCertificate()
got.PublicId = certKey.GetPublicId()
assert.NoError(t, rw.LookupById(ctx, got))
kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), proj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId()))
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.GetKeyId())
assert.NotEqual(t, certKey.KeyId, got.GetKeyId())
assert.Equal(t, newKeyVersionId, got.GetKeyId())
assert.NotEqual(t, certKey.UpdateTime, got.UpdateTime)
certPrivKeyParsed, err := x509.ParseECPrivateKey(certKey.PrivateKey)
require.NoError(t, err)
retrievedCertKeyParsed, err := x509.ParseECPrivateKey(got.PrivateKey)
require.NoError(t, err)
keysAreEqual := certPrivKeyParsed.Equal(retrievedCertKeyParsed)
assert.True(t, keysAreEqual, "expected the ECDSA keys to be equal")
})
}
func TestRewrap_AliasCertRewrapFn(t *testing.T) {
ctx := context.Background()
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 "target_alias_proxy_certificate" WHERE key_id=\$1`,
).WillReturnError(errors.New("Query error"))
err := target.ProxyCertRewrapFn(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
_, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
kmsWrapper, err := kmsCache.GetWrapper(context.Background(), proj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
tar := targettest.TestNewTestTarget(ctx, t, conn, proj.PublicId, "test-target")
aliasValue := "test-alias"
alias := talias.TestAlias(t, rw, aliasValue, talias.WithDestinationId(tar.GetPublicId()))
require.NoError(t, err)
require.NotNil(t, alias)
certKey, err := target.NewTargetAliasProxyCertificate(ctx, tar.GetPublicId(), alias)
require.NoError(t, err)
id, err := db.NewPublicId(ctx, globals.ProxyServerCertificatePrefix)
require.NoError(t, err)
certKey.PublicId = id
err = certKey.Encrypt(ctx, kmsWrapper)
require.NoError(t, err)
err = rw.Create(ctx, certKey)
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, target.ProxyAliasCertRewrapFn(ctx, certKey.KeyId, proj.PublicId, rw, rw, kmsCache))
// now we pull it from the db, decrypt it with the new key, and ensure things match
got := target.AllocTargetAliasProxyCertificate()
got.PublicId = certKey.GetPublicId()
assert.NoError(t, rw.LookupById(ctx, got))
kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), proj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId()))
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.GetKeyId())
assert.NotEqual(t, certKey.KeyId, got.GetKeyId())
assert.Equal(t, newKeyVersionId, got.GetKeyId())
assert.NotEqual(t, certKey.UpdateTime, got.UpdateTime)
certPrivKeyParsed, err := x509.ParseECPrivateKey(certKey.PrivateKey)
require.NoError(t, err)
retrievedCertKeyParsed, err := x509.ParseECPrivateKey(got.PrivateKey)
require.NoError(t, err)
keysAreEqual := certPrivKeyParsed.Equal(retrievedCertKeyParsed)
assert.True(t, keysAreEqual, "expected the ECDSA keys to be equal")
})
}
+228 -37
View File
@@ -10,6 +10,7 @@ import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
mathrand "math/rand"
"net"
@@ -47,7 +48,7 @@ func generatePrivAndPubKeys(ctx context.Context) (privKeyBytes []byte, pubKeyByt
}
// generateTargetCert generates a self-signed certificate for the target for localhost with the localhost addresses for ipv4 and ipv6.
// Supports the option withAlias to pass an alias for use in the cert DNS names field
// Supports the option WithAlias to pass an alias for use in the cert DNS names field
func generateTargetCert(ctx context.Context, privKey *ecdsa.PrivateKey, exp time.Time, opt ...Option) ([]byte, error) {
const op = "target.generateTargetCert"
switch {
@@ -77,8 +78,8 @@ func generateTargetCert(ctx context.Context, privKey *ecdsa.PrivateKey, exp time
DNSNames: []string{"localhost"},
}
if opts.withAlias != nil {
template.DNSNames = append(template.DNSNames, opts.withAlias.Value)
if opts.WithAlias != nil {
template.DNSNames = append(template.DNSNames, opts.WithAlias.Value)
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey)
@@ -88,6 +89,25 @@ func generateTargetCert(ctx context.Context, privKey *ecdsa.PrivateKey, exp time
return certBytes, nil
}
func generateKeysAndCert(ctx context.Context, notValidAfter time.Time, opt ...Option) (privKey []byte, pubKey []byte, cert []byte, err error) {
const op = "target.generateKeysAndCert"
privKey, pubKey, err = generatePrivAndPubKeys(ctx)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op)
}
parsedKey, err := x509.ParseECPrivateKey(privKey)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op)
}
cert, err = generateTargetCert(ctx, parsedKey, notValidAfter, opt...)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op)
}
return privKey, pubKey, cert, nil
}
// TargetProxyCertificate represents a proxy certificate for a target
type TargetProxyCertificate struct {
*store.TargetProxyCertificate
@@ -102,35 +122,26 @@ func NewTargetProxyCertificate(ctx context.Context, opt ...Option) (*TargetProxy
opts := GetOpts(opt...)
privKey, pubKey, err := generatePrivAndPubKeys(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating target proxy certificate key"))
}
notValidAfter := time.Now().AddDate(1, 0, 0) // 1 year from now
parsedKey, err := x509.ParseECPrivateKey(privKey)
privKey, pubKey, cert, err := generateKeysAndCert(ctx, notValidAfter)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing target proxy certificate key"))
}
certBytes, err := generateTargetCert(ctx, parsedKey, notValidAfter)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating target certificate"))
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating target proxy cert and keys"))
}
return &TargetProxyCertificate{
TargetProxyCertificate: &store.TargetProxyCertificate{
PrivateKey: privKey,
PublicKey: pubKey,
Certificate: certBytes,
Certificate: cert,
NotValidAfter: timestamp.New(notValidAfter),
TargetId: opts.withTargetId,
},
}, nil
}
// encrypt the target cert key before writing it to the db
func (t *TargetProxyCertificate) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetProxyCertificate).encrypt"
// Encrypt the target cert key before writing it to the db
func (t *TargetProxyCertificate) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetProxyCertificate).Encrypt"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
@@ -145,9 +156,9 @@ func (t *TargetProxyCertificate) encrypt(ctx context.Context, cipher wrapping.Wr
return nil
}
// decrypt the target cert key after reading it from the db
func (t *TargetProxyCertificate) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetProxyCertificate).decrypt"
// Decrypt the target cert key after reading it from the db
func (t *TargetProxyCertificate) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetProxyCertificate).Decrypt"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
@@ -197,6 +208,31 @@ func (t *TargetProxyCertificate) TableName() string {
return "target_proxy_certificate"
}
// SetTableName sets the table name
func (t *TargetProxyCertificate) SetTableName(name string) {
t.tableName = name
}
func (t *TargetProxyCertificate) GetNotValidAfter() *timestamp.Timestamp {
return t.NotValidAfter
}
func (t *TargetProxyCertificate) SetPrivateKey(privKeyBytes []byte) {
t.PrivateKey = privKeyBytes
}
func (t *TargetProxyCertificate) SetPublicKey(pubKeyBytes []byte) {
t.PublicKey = pubKeyBytes
}
func (t *TargetProxyCertificate) SetCertificate(certBytes []byte) {
t.Certificate = certBytes
}
func (t *TargetProxyCertificate) SetNotValidAfter(timestamp *timestamp.Timestamp) {
t.NotValidAfter = timestamp
}
// TargetAliasProxyCertificate represents a certificate for a target accessed with an alias
type TargetAliasProxyCertificate struct {
*store.TargetAliasProxyCertificate
@@ -213,18 +249,10 @@ func NewTargetAliasProxyCertificate(ctx context.Context, targetId string, alias
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target alias")
}
privKey, pubKey, err := generatePrivAndPubKeys(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating target proxy certificate key"))
}
notValidAfter := time.Now().AddDate(1, 0, 0) // 1 year from now
parsedKey, err := x509.ParseECPrivateKey(privKey)
privKey, pubKey, cert, err := generateKeysAndCert(ctx, notValidAfter, WithAlias(alias))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing target proxy certificate key"))
}
certBytes, err := generateTargetCert(ctx, parsedKey, notValidAfter, WithAlias(alias))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating target certificate"))
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating target proxy cert and keys"))
}
return &TargetAliasProxyCertificate{
@@ -233,15 +261,15 @@ func NewTargetAliasProxyCertificate(ctx context.Context, targetId string, alias
PrivateKey: privKey,
PublicKey: pubKey,
AliasId: alias.PublicId,
Certificate: certBytes,
Certificate: cert,
NotValidAfter: timestamp.New(notValidAfter),
},
}, nil
}
// encrypt the target cert key before writing it to the db
func (t *TargetAliasProxyCertificate) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetAliasProxyCertificate).encrypt"
// Encrypt the target cert key before writing it to the db
func (t *TargetAliasProxyCertificate) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetAliasProxyCertificate).Encrypt"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
@@ -257,8 +285,8 @@ func (t *TargetAliasProxyCertificate) encrypt(ctx context.Context, cipher wrappi
}
// decrypt the target cert key after reading it from the db
func (t *TargetAliasProxyCertificate) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetAliasProxyCertificate).decrypt"
func (t *TargetAliasProxyCertificate) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "target.(TargetAliasProxyCertificate).Decrypt"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
@@ -309,3 +337,166 @@ func (t *TargetAliasProxyCertificate) VetForWrite(ctx context.Context, _ db.Read
func (t *TargetAliasProxyCertificate) TableName() string {
return "target_alias_proxy_certificate"
}
// SetTableName sets the table name
func (t *TargetAliasProxyCertificate) SetTableName(name string) {
t.tableName = name
}
func (t *TargetAliasProxyCertificate) GetNotValidAfter() *timestamp.Timestamp {
return t.NotValidAfter
}
func (t *TargetAliasProxyCertificate) SetPrivateKey(privKeyBytes []byte) {
t.PrivateKey = privKeyBytes
}
func (t *TargetAliasProxyCertificate) SetPublicKey(pubKeyBytes []byte) {
t.PublicKey = pubKeyBytes
}
func (t *TargetAliasProxyCertificate) SetCertificate(certBytes []byte) {
t.Certificate = certBytes
}
func (t *TargetAliasProxyCertificate) SetNotValidAfter(timestamp *timestamp.Timestamp) {
t.NotValidAfter = timestamp
}
func pemsToServerCertificate(ctx context.Context, certPem, keyPem []byte) (*ServerCertificate, error) {
const op = "target.pemsToServerCertificate"
switch {
case certPem == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing certificate PEM")
case keyPem == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing private key PEM")
}
cPem := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certPem,
})
if certPem == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "error encoding certificate to PEM")
}
kPem := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyPem,
})
if keyPem == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "error encoding private key to PEM")
}
return &ServerCertificate{
CertificatePem: cPem,
PrivateKeyPem: kPem,
}, nil
}
func (t *TargetProxyCertificate) fromServerCertificate(ctx context.Context, serverCert *ServerCertificate) error {
const op = "target.(TargetProxyCertificate).fromServerCertificate"
switch {
case serverCert == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing server certificate")
case len(serverCert.CertificatePem) == 0:
return errors.New(ctx, errors.InvalidParameter, op, "missing certificate PEM data")
case len(serverCert.PrivateKeyPem) == 0:
return errors.New(ctx, errors.InvalidParameter, op, "missing private key PEM data")
}
decodedBlock, _ := pem.Decode(serverCert.CertificatePem)
if decodedBlock == nil || decodedBlock.Type != "CERTIFICATE" {
return errors.New(ctx, errors.InvalidParameter, op, "invalid PEM data for certificate")
}
cert, err := x509.ParseCertificate(decodedBlock.Bytes)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error parsing PEM data"))
}
t.Certificate = cert.Raw
t.NotValidAfter = timestamp.New(cert.NotAfter)
decodedKeyBlock, _ := pem.Decode(serverCert.PrivateKeyPem)
if decodedKeyBlock == nil || decodedKeyBlock.Type != "EC PRIVATE KEY" {
return errors.New(ctx, errors.InvalidParameter, op, "invalid PEM data for EC private key")
}
t.PrivateKey = decodedKeyBlock.Bytes
privKey, err := x509.ParseECPrivateKey(t.PrivateKey)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error parsing decoded private key"))
}
t.PublicKey, err = x509.MarshalPKIXPublicKey(&privKey.PublicKey)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error marshalling public key"))
}
return nil
}
// ToServerCertificate converts the target proxy certificate to a server certificate
func (t *TargetProxyCertificate) ToServerCertificate(ctx context.Context) (*ServerCertificate, error) {
const op = "target.(TargetProxyCertificate).ToServerCertificate"
switch {
case len(t.PrivateKey) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing private key")
case len(t.Certificate) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing certificate")
}
return pemsToServerCertificate(ctx, t.Certificate, t.PrivateKey)
}
func (t *TargetAliasProxyCertificate) fromServerCertificate(ctx context.Context, serverCert *ServerCertificate) error {
const op = "target.(TargetAliasProxyCertificate).fromServerCertificate"
switch {
case serverCert == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing server certificate")
case len(serverCert.CertificatePem) == 0:
return errors.New(ctx, errors.InvalidParameter, op, "missing certificate PEM data")
case len(serverCert.PrivateKeyPem) == 0:
return errors.New(ctx, errors.InvalidParameter, op, "missing private keyPEM data")
}
decodedBlock, _ := pem.Decode(serverCert.CertificatePem)
if decodedBlock == nil || decodedBlock.Type != "CERTIFICATE" {
return errors.New(ctx, errors.InvalidParameter, op, "invalid PEM data for certificate")
}
cert, err := x509.ParseCertificate(decodedBlock.Bytes)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error parsing PEM data"))
}
t.Certificate = cert.Raw
t.NotValidAfter = timestamp.New(cert.NotAfter)
decodedKeyBlock, _ := pem.Decode(serverCert.PrivateKeyPem)
if decodedKeyBlock == nil || decodedKeyBlock.Type != "EC PRIVATE KEY" {
return errors.New(ctx, errors.InvalidParameter, op, "invalid PEM data for EC private key")
}
t.PrivateKey = decodedKeyBlock.Bytes
privKey, err := x509.ParseECPrivateKey(t.PrivateKey)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error parsing decoded private key"))
}
t.PublicKey, err = x509.MarshalPKIXPublicKey(&privKey.PublicKey)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error marshalling public key"))
}
return nil
}
// ToServerCertificate converts the target alias proxy certificate to a server certificate
func (t *TargetAliasProxyCertificate) ToServerCertificate(ctx context.Context) (*ServerCertificate, error) {
const op = "target.(TargetAliasProxyCertificate).ToServerCertificate"
switch {
case len(t.PrivateKey) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing private key")
case len(t.Certificate) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing certificate")
}
return pemsToServerCertificate(ctx, t.Certificate, t.PrivateKey)
}
+51 -5
View File
@@ -105,7 +105,7 @@ func TestGenerateTargetCert(t *testing.T) {
require.Contains(pCert.DNSNames, "localhost")
if tt.opt != nil {
opts := GetOpts(tt.opt...)
require.Contains(pCert.DNSNames, opts.withAlias.Value)
require.Contains(pCert.DNSNames, opts.WithAlias.Value)
}
})
}
@@ -220,7 +220,7 @@ func Test_encrypt_decrypt_TargetCert(t *testing.T) {
assert, require := assert.New(t), require.New(t)
encryptedCert := tt.certKey.Clone()
err = encryptedCert.encrypt(ctx, tt.encryptWrapper)
err = encryptedCert.Encrypt(ctx, tt.encryptWrapper)
if tt.wantEncryptErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantEncryptErrMatch, err), "expected %q and got err: %+v", tt.wantEncryptErrMatch.Code, err)
@@ -231,7 +231,7 @@ func Test_encrypt_decrypt_TargetCert(t *testing.T) {
decryptedCert := encryptedCert.Clone()
decryptedCert.PrivateKey = []byte("")
err = decryptedCert.decrypt(ctx, tt.decryptWrapper)
err = decryptedCert.Decrypt(ctx, tt.decryptWrapper)
if tt.wantDecryptErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantDecryptErrMatch, err), "expected %q and got err: %+v", tt.wantDecryptErrMatch.Code, err)
@@ -378,7 +378,7 @@ func Test_encrypt_decrypt_TargetAliasCert(t *testing.T) {
assert, require := assert.New(t), require.New(t)
encryptedCert := tt.certKey.Clone()
err = encryptedCert.encrypt(ctx, tt.encryptWrapper)
err = encryptedCert.Encrypt(ctx, tt.encryptWrapper)
if tt.wantEncryptErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantEncryptErrMatch, err), "expected %q and got err: %+v", tt.wantEncryptErrMatch.Code, err)
@@ -389,7 +389,7 @@ func Test_encrypt_decrypt_TargetAliasCert(t *testing.T) {
decryptedCert := encryptedCert.Clone()
decryptedCert.PrivateKey = []byte("")
err = decryptedCert.decrypt(ctx, tt.decryptWrapper)
err = decryptedCert.Decrypt(ctx, tt.decryptWrapper)
if tt.wantDecryptErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantDecryptErrMatch, err), "expected %q and got err: %+v", tt.wantDecryptErrMatch.Code, err)
@@ -400,3 +400,49 @@ func Test_encrypt_decrypt_TargetAliasCert(t *testing.T) {
})
}
}
func TestTargetProxyCertToAndFromServerCert(t *testing.T) {
t.Parallel()
ctx := context.Background()
cert, err := NewTargetProxyCertificate(ctx)
require.NoError(t, err)
require.NotNil(t, cert)
serverCert, err := cert.ToServerCertificate(ctx)
require.NoError(t, err)
require.NotEmpty(t, serverCert.CertificatePem)
require.NotEmpty(t, serverCert.PrivateKeyPem)
newCert := allocTargetProxyCertificate()
err = newCert.fromServerCertificate(ctx, serverCert)
require.NoError(t, err)
require.Equal(t, cert.Certificate, newCert.Certificate)
}
func TestTargetAliasProxyCertToAndFromServerCert(t *testing.T) {
t.Parallel()
ctx := context.Background()
tId := "test-target-id"
tAId := "test-alias-id"
aliasValue := "test-alias-value"
alias := &talias.Alias{
Alias: &astore.Alias{
PublicId: tAId,
Value: aliasValue,
},
}
cert, err := NewTargetAliasProxyCertificate(ctx, tId, alias)
require.NoError(t, err)
require.NotNil(t, cert)
serverCert, err := cert.ToServerCertificate(ctx)
require.NoError(t, err)
require.NotEmpty(t, serverCert.CertificatePem)
require.NotEmpty(t, serverCert.PrivateKeyPem)
newCert := allocTargetAliasProxyCertificate()
err = newCert.fromServerCertificate(ctx, serverCert)
require.NoError(t, err)
require.Equal(t, cert.Certificate, newCert.Certificate)
}
+5 -2
View File
@@ -36,6 +36,7 @@ type Target struct {
HostSource []target.HostSource `gorm:"-"`
CredentialSources []target.CredentialSource `gorm:"-"`
Aliases []*talias.Alias `gorm:"-"`
ServerCert *target.ServerCertificate `gorm:"-"`
}
var (
@@ -162,7 +163,7 @@ func (t *Target) GetStorageBucketId() string {
}
func (t *Target) GetProxyServerCertificate() *target.ServerCertificate {
return nil
return t.ServerCert
}
func (t *Target) Clone() target.Target {
@@ -257,7 +258,9 @@ func (t *Target) SetEnableSessionRecording(_ bool) {}
func (t *Target) SetStorageBucketId(_ string) {}
func (t *Target) SetProxyServerCertificate(*target.ServerCertificate) {}
func (t *Target) SetProxyServerCertificate(sc *target.ServerCertificate) {
t.ServerCert = sc
}
func (t *Target) Oplog(op oplog.OpType) oplog.Metadata {
return oplog.Metadata{