feat(target): add proxy certificate repos and protos (#1577)

pull/6010/head
Irena Rindos 10 months ago committed by irenarindos
parent bcd2a62890
commit 6afb42e334

@ -0,0 +1,10 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
-- Updates 85/04_alias_target.up.sql to add a unique constraint on the public id and destination id
alter table alias_target
add constraint alias_target_destination_uq unique (public_id, destination_id);
commit;

@ -0,0 +1,132 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create table target_proxy_certificate(
public_id wt_public_id primary key,
public_key bytea,
constraint public_key_must_not_be_empty
check(length(public_key) > 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,
target_id wt_public_id not null
constraint target_proxy_fkey
references target (public_id)
on delete cascade
on update cascade,
certificate bytea not null
constraint certificate_must_not_be_empty
check(length(certificate) > 0),
not_valid_after wt_timestamp not null,
version wt_version,
create_time wt_timestamp,
update_time wt_timestamp
);
create trigger immutable_columns before update on target_proxy_certificate
for each row execute procedure immutable_columns('public_id', 'target_id', 'create_time');
create trigger update_version_column after update on target_proxy_certificate
for each row execute procedure update_version_column();
create trigger update_time_column before update on target_proxy_certificate
for each row execute procedure update_time_column();
create trigger default_create_time_column before insert on target_proxy_certificate
for each row execute procedure default_create_time();
comment on table target_proxy_certificate is
'target_proxy_certificate is a table where each row represents a proxy certificate for a target.';
create table target_alias_proxy_certificate(
public_id wt_public_id primary key,
public_key bytea,
constraint public_key_must_not_be_empty
check(length(public_key) > 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,
target_id wt_public_id not null
constraint target_proxy_fkey
references target (public_id)
on delete cascade
on update cascade,
alias_id wt_public_id not null,
certificate bytea not null
constraint certificate_must_not_be_empty
check(length(certificate) > 0),
not_valid_after wt_timestamp not null,
version wt_version,
create_time wt_timestamp,
update_time wt_timestamp,
constraint alias_target_fkey
foreign key (alias_id, target_id)
references alias_target (public_id, destination_id)
on delete cascade
on update cascade
);
create trigger immutable_columns before update on target_alias_proxy_certificate
for each row execute procedure immutable_columns('public_id', 'target_id', 'create_time');
create trigger update_version_column after update on target_alias_proxy_certificate
for each row execute procedure update_version_column();
create trigger update_time_column before update on target_alias_proxy_certificate
for each row execute procedure update_time_column();
create trigger default_create_time_column before insert on target_alias_proxy_certificate
for each row execute procedure default_create_time();
comment on table target_alias_proxy_certificate is
'target_alias_proxy_certificate is a table where each row represents a proxy certificate for a target for use with an alias.';
-- To account for users updating target aliases to change either the target id, host id, or value of an alias,
-- on update to alias_target, entries in target_alias_certificate that
-- match the old target_id and alias_id will be deleted.
create function remove_target_alias_certificates_for_updated_alias() returns trigger
as $$
begin
-- If the destination_id, host_id, and value of the alias have not changed, do nothing.
if old.destination_id is distinct from new.destination_id or
old.host_id is distinct from new.host_id or
old.value is distinct from new.value then
delete
from target_alias_proxy_certificate
where target_id = old.destination_id
and alias_id = old.public_id;
end if;
return new;
end;
$$ language plpgsql;
create trigger remove_target_alias_certificates_for_updated_alias before update of destination_id, host_id, value on alias_target
for each row execute procedure remove_target_alias_certificates_for_updated_alias();
create table session_proxy_certificate(
session_id wt_public_id not null
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)
);
comment on table session_proxy_certificate is
'session_proxy_certificate is a table where each row maps a certificate to a session id.';
commit;

@ -175,3 +175,99 @@ message CredentialSourceView {
// @inject_tag: `gorm:"not_null"`
string type = 20;
}
message TargetProxyCertificate {
// public_id is used to identify the target proxy key
// @inject_tag: `gorm:"primary_key"`
string public_id = 10;
// target_id is used to access the proxy target this key is for
// @inject_tag: `gorm:"not_null"`
string target_id = 20;
// public_key is the public key associated with this certificate
// @inject_tag: `gorm:"not_null"`
bytes public_key = 30;
// private_key is the plaintext key. this is not stored in the db
// @inject_tag: `gorm:"-" wrapping:"pt,private_key"`
bytes private_key = 40;
// private_key_encrypted is the encrypted PEM encoded private key
// @inject_tag: `gorm:"not_null" wrapping:"ct,private_key"`
bytes private_key_encrypted = 50;
// key_id is the kms private id used to encrypt this entry's private key
// @inject_tag: `gorm:"not_null"`
string key_id = 60;
// certificate is the PEM encoded certificate
// @inject_tag: `gorm:"not_null"`
bytes certificate = 70;
// not_valid_after is the timestamp at which this certificate's validity period ends
// @inject_tag: `gorm:"not_null"`
timestamp.v1.Timestamp not_valid_after = 80;
// version allows optimistic locking during modification
// @inject_tag: `gorm:"default:null"`
uint32 version = 90;
// create_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp create_time = 100;
// update_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp update_time = 110;
}
message TargetAliasProxyCertificate {
// public_id is used to identify the target proxy key
// @inject_tag: `gorm:"primary_key"`
string public_id = 10;
// target_id is used to access the proxy target this key is for
// @inject_tag: `gorm:"not_null"`
string target_id = 20;
// public_key is the public key associated with this certificate
// @inject_tag: `gorm:"not_null"`
bytes public_key = 30;
// private_key is the plaintext key. this is not stored in the db
// @inject_tag: `gorm:"-" wrapping:"pt,private_key"`
bytes private_key = 40;
// private_key_encrypted is the encrypted PEM encoded private key
// @inject_tag: `gorm:"not_null" wrapping:"ct,private_key"`
bytes private_key_encrypted = 50;
// key_id is the kms private id used to encrypt this entry's private key
// @inject_tag: `gorm:"not_null"`
string key_id = 60;
// alias_id is the public id of the alias target
// @inject_tag: `gorm:"not_null"`
string alias_id = 70;
// certificate is the PEM encoded certificate
// @inject_tag: `gorm:"not_null"`
bytes certificate = 80;
// not_valid_after is the timestamp at which this certificate's validity period ends
// @inject_tag: `gorm:"not_null"`
timestamp.v1.Timestamp not_valid_after = 90;
// version allows optimistic locking during modification
// @inject_tag: `gorm:"default:null"`
uint32 version = 100;
// create_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp create_time = 110;
// update_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp update_time = 120;
}

@ -608,3 +608,62 @@ type deletedSession struct {
func (s *deletedSession) TableName() string {
return "session_deleted"
}
// ProxyCertificate represents a session id to proxy certificate mapping
// 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"`
}
// NewProxyCertificate creates a new in memory ProxyCertificate
func NewProxyCertificate(ctx context.Context, sessionId string, 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")
}
return &ProxyCertificate{
SessionId: sessionId,
Certificate: certificate,
}, nil
}
func alloProxyCertificate() *ProxyCertificate {
return &ProxyCertificate{}
}
// Clone creates a clone of the ProxyCertificate
func (t *ProxyCertificate) Clone() *ProxyCertificate {
spc := &ProxyCertificate{
SessionId: t.SessionId,
}
if t.Certificate != nil {
spc.Certificate = make([]byte, len(t.Certificate))
copy(spc.Certificate, t.Certificate)
}
return spc
}
// VetForWrite implements db.VetForWrite() interface and validates the session proxy certificate
func (t *ProxyCertificate) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error {
const op = "session.(ProxyCertificate).VetForWrite"
switch {
case t.SessionId == "":
return errors.New(ctx, errors.InvalidParameter, op, "missing session id")
case len(t.Certificate) == 0:
return errors.New(ctx, errors.InvalidParameter, op, "missing certificate")
}
return nil
}
// TableName returns the table name.
func (t *ProxyCertificate) TableName() string {
return "session_proxy_certificate"
}

@ -433,3 +433,63 @@ func Test_newCert(t *testing.T) {
assert.Equal(t, parsedCert.PublicKey.(crypto.PublicKey), ed25519.PrivateKey(key).Public())
})
}
func TestProxyCertificate(t *testing.T) {
t.Parallel()
ctx := context.Background()
sId := "test-session-id"
cert := make([]byte, 20)
if _, err := rand.Read(cert); err != nil {
require.NotNil(t, err)
}
tests := []struct {
name string
sessionId string
certificate []byte
expected *ProxyCertificate
wantErr bool
wantErrContains string
}{
{
name: "valid-target-cert",
sessionId: sId,
certificate: cert,
expected: &ProxyCertificate{
SessionId: sId,
Certificate: cert,
},
},
{
name: "missing-session-id",
certificate: cert,
wantErr: true,
wantErrContains: "missing session id",
},
{
name: "missing-cert",
sessionId: sId,
wantErr: true,
wantErrContains: "missing certificate",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
gotCert, err := NewProxyCertificate(ctx, tt.sessionId, tt.certificate)
if tt.wantErr {
assert.Error(err)
return
}
require.NoError(err)
require.NotNil(gotCert)
assert.Equal(tt.expected.SessionId, gotCert.SessionId)
assert.Equal(tt.expected.Certificate, gotCert.Certificate)
})
}
}

@ -56,6 +56,8 @@ type options struct {
WithNetResolver intglobals.NetIpResolver
WithStartPageAfterItem pagination.Item
withAliases []*talias.Alias
withAlias *talias.Alias
withTargetId string
}
func getDefaultOptions() options {
@ -83,6 +85,7 @@ func getDefaultOptions() options {
WithIngressWorkerFilter: "",
WithAddress: "",
WithNetResolver: net.DefaultResolver,
withTargetId: "",
}
}
@ -284,3 +287,17 @@ func WithAliases(in []*talias.Alias) Option {
o.withAliases = in
}
}
// WithAlias provides an option to provide a single alias.
func WithAlias(in *talias.Alias) Option {
return func(o *options) {
o.withAlias = in
}
}
// WithTargetId provides an option to provide a target ID.
func WithTargetId(in string) Option {
return func(o *options) {
o.withTargetId = in
}
}

@ -268,4 +268,18 @@ func Test_GetOpts(t *testing.T) {
opts := GetOpts(WithAliases(input))
assert.Equal(input, opts.withAliases)
})
t.Run("WithAlias", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
al, err := talias.NewAlias(context.Background(), "global", "test")
require.NoError(err)
opts := GetOpts(WithAlias(al))
assert.Equal(al, opts.withAlias)
})
t.Run("WithTargetId", func(t *testing.T) {
assert := assert.New(t)
opts := GetOpts(WithTargetId("testId"))
testOpts := getDefaultOptions()
testOpts.withTargetId = "testId"
assert.Equal(opts, testOpts)
})
}

@ -649,6 +649,308 @@ func (x *CredentialSourceView) GetType() string {
return ""
}
type TargetProxyCertificate struct {
state protoimpl.MessageState `protogen:"open.v1"`
// public_id is used to identify the target proxy key
// @inject_tag: `gorm:"primary_key"`
PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"`
// target_id is used to access the proxy target this key is for
// @inject_tag: `gorm:"not_null"`
TargetId string `protobuf:"bytes,20,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty" gorm:"not_null"`
// public_key is the public key associated with this certificate
// @inject_tag: `gorm:"not_null"`
PublicKey []byte `protobuf:"bytes,30,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty" gorm:"not_null"`
// private_key is the plaintext key. this is not stored in the db
// @inject_tag: `gorm:"-" wrapping:"pt,private_key"`
PrivateKey []byte `protobuf:"bytes,40,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" gorm:"-" wrapping:"pt,private_key"`
// private_key_encrypted is the encrypted PEM encoded private key
// @inject_tag: `gorm:"not_null" wrapping:"ct,private_key"`
PrivateKeyEncrypted []byte `protobuf:"bytes,50,opt,name=private_key_encrypted,json=privateKeyEncrypted,proto3" json:"private_key_encrypted,omitempty" gorm:"not_null" wrapping:"ct,private_key"`
// key_id is the kms private id used to encrypt this entry's private key
// @inject_tag: `gorm:"not_null"`
KeyId string `protobuf:"bytes,60,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"`
// certificate is the PEM encoded certificate
// @inject_tag: `gorm:"not_null"`
Certificate []byte `protobuf:"bytes,70,opt,name=certificate,proto3" json:"certificate,omitempty" gorm:"not_null"`
// not_valid_after is the timestamp at which this certificate's validity period ends
// @inject_tag: `gorm:"not_null"`
NotValidAfter *timestamp.Timestamp `protobuf:"bytes,80,opt,name=not_valid_after,json=notValidAfter,proto3" json:"not_valid_after,omitempty" gorm:"not_null"`
// version allows optimistic locking during modification
// @inject_tag: `gorm:"default:null"`
Version uint32 `protobuf:"varint,90,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"`
// create_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
CreateTime *timestamp.Timestamp `protobuf:"bytes,100,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
// update_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
UpdateTime *timestamp.Timestamp `protobuf:"bytes,110,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TargetProxyCertificate) Reset() {
*x = TargetProxyCertificate{}
mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TargetProxyCertificate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TargetProxyCertificate) ProtoMessage() {}
func (x *TargetProxyCertificate) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TargetProxyCertificate.ProtoReflect.Descriptor instead.
func (*TargetProxyCertificate) Descriptor() ([]byte, []int) {
return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{7}
}
func (x *TargetProxyCertificate) GetPublicId() string {
if x != nil {
return x.PublicId
}
return ""
}
func (x *TargetProxyCertificate) GetTargetId() string {
if x != nil {
return x.TargetId
}
return ""
}
func (x *TargetProxyCertificate) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
}
return nil
}
func (x *TargetProxyCertificate) GetPrivateKey() []byte {
if x != nil {
return x.PrivateKey
}
return nil
}
func (x *TargetProxyCertificate) GetPrivateKeyEncrypted() []byte {
if x != nil {
return x.PrivateKeyEncrypted
}
return nil
}
func (x *TargetProxyCertificate) GetKeyId() string {
if x != nil {
return x.KeyId
}
return ""
}
func (x *TargetProxyCertificate) GetCertificate() []byte {
if x != nil {
return x.Certificate
}
return nil
}
func (x *TargetProxyCertificate) GetNotValidAfter() *timestamp.Timestamp {
if x != nil {
return x.NotValidAfter
}
return nil
}
func (x *TargetProxyCertificate) GetVersion() uint32 {
if x != nil {
return x.Version
}
return 0
}
func (x *TargetProxyCertificate) GetCreateTime() *timestamp.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *TargetProxyCertificate) GetUpdateTime() *timestamp.Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
type TargetAliasProxyCertificate struct {
state protoimpl.MessageState `protogen:"open.v1"`
// public_id is used to identify the target proxy key
// @inject_tag: `gorm:"primary_key"`
PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"`
// target_id is used to access the proxy target this key is for
// @inject_tag: `gorm:"not_null"`
TargetId string `protobuf:"bytes,20,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty" gorm:"not_null"`
// public_key is the public key associated with this certificate
// @inject_tag: `gorm:"not_null"`
PublicKey []byte `protobuf:"bytes,30,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty" gorm:"not_null"`
// private_key is the plaintext key. this is not stored in the db
// @inject_tag: `gorm:"-" wrapping:"pt,private_key"`
PrivateKey []byte `protobuf:"bytes,40,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" gorm:"-" wrapping:"pt,private_key"`
// private_key_encrypted is the encrypted PEM encoded private key
// @inject_tag: `gorm:"not_null" wrapping:"ct,private_key"`
PrivateKeyEncrypted []byte `protobuf:"bytes,50,opt,name=private_key_encrypted,json=privateKeyEncrypted,proto3" json:"private_key_encrypted,omitempty" gorm:"not_null" wrapping:"ct,private_key"`
// key_id is the kms private id used to encrypt this entry's private key
// @inject_tag: `gorm:"not_null"`
KeyId string `protobuf:"bytes,60,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"`
// alias_id is the public id of the alias target
// @inject_tag: `gorm:"not_null"`
AliasId string `protobuf:"bytes,70,opt,name=alias_id,json=aliasId,proto3" json:"alias_id,omitempty" gorm:"not_null"`
// certificate is the PEM encoded certificate
// @inject_tag: `gorm:"not_null"`
Certificate []byte `protobuf:"bytes,80,opt,name=certificate,proto3" json:"certificate,omitempty" gorm:"not_null"`
// not_valid_after is the timestamp at which this certificate's validity period ends
// @inject_tag: `gorm:"not_null"`
NotValidAfter *timestamp.Timestamp `protobuf:"bytes,90,opt,name=not_valid_after,json=notValidAfter,proto3" json:"not_valid_after,omitempty" gorm:"not_null"`
// version allows optimistic locking during modification
// @inject_tag: `gorm:"default:null"`
Version uint32 `protobuf:"varint,100,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"`
// create_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
CreateTime *timestamp.Timestamp `protobuf:"bytes,110,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
// update_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
UpdateTime *timestamp.Timestamp `protobuf:"bytes,120,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TargetAliasProxyCertificate) Reset() {
*x = TargetAliasProxyCertificate{}
mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TargetAliasProxyCertificate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TargetAliasProxyCertificate) ProtoMessage() {}
func (x *TargetAliasProxyCertificate) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TargetAliasProxyCertificate.ProtoReflect.Descriptor instead.
func (*TargetAliasProxyCertificate) Descriptor() ([]byte, []int) {
return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{8}
}
func (x *TargetAliasProxyCertificate) GetPublicId() string {
if x != nil {
return x.PublicId
}
return ""
}
func (x *TargetAliasProxyCertificate) GetTargetId() string {
if x != nil {
return x.TargetId
}
return ""
}
func (x *TargetAliasProxyCertificate) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
}
return nil
}
func (x *TargetAliasProxyCertificate) GetPrivateKey() []byte {
if x != nil {
return x.PrivateKey
}
return nil
}
func (x *TargetAliasProxyCertificate) GetPrivateKeyEncrypted() []byte {
if x != nil {
return x.PrivateKeyEncrypted
}
return nil
}
func (x *TargetAliasProxyCertificate) GetKeyId() string {
if x != nil {
return x.KeyId
}
return ""
}
func (x *TargetAliasProxyCertificate) GetAliasId() string {
if x != nil {
return x.AliasId
}
return ""
}
func (x *TargetAliasProxyCertificate) GetCertificate() []byte {
if x != nil {
return x.Certificate
}
return nil
}
func (x *TargetAliasProxyCertificate) GetNotValidAfter() *timestamp.Timestamp {
if x != nil {
return x.NotValidAfter
}
return nil
}
func (x *TargetAliasProxyCertificate) GetVersion() uint32 {
if x != nil {
return x.Version
}
return 0
}
func (x *TargetAliasProxyCertificate) GetCreateTime() *timestamp.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *TargetAliasProxyCertificate) GetUpdateTime() *timestamp.Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
var File_controller_storage_target_store_v1_target_proto protoreflect.FileDescriptor
const file_controller_storage_target_store_v1_target_proto_rawDesc = "" +
@ -713,7 +1015,42 @@ const file_controller_storage_target_store_v1_target_proto_rawDesc = "" +
"\x14CredentialSourceView\x12\x1b\n" +
"\tpublic_id\x18\n" +
" \x01(\tR\bpublicId\x12\x12\n" +
"\x04type\x18\x14 \x01(\tR\x04typeB;Z9github.com/hashicorp/boundary/internal/target/store;storeb\x06proto3"
"\x04type\x18\x14 \x01(\tR\x04type\"\x87\x04\n" +
"\x16TargetProxyCertificate\x12\x1b\n" +
"\tpublic_id\x18\n" +
" \x01(\tR\bpublicId\x12\x1b\n" +
"\ttarget_id\x18\x14 \x01(\tR\btargetId\x12\x1d\n" +
"\n" +
"public_key\x18\x1e \x01(\fR\tpublicKey\x12\x1f\n" +
"\vprivate_key\x18( \x01(\fR\n" +
"privateKey\x122\n" +
"\x15private_key_encrypted\x182 \x01(\fR\x13privateKeyEncrypted\x12\x15\n" +
"\x06key_id\x18< \x01(\tR\x05keyId\x12 \n" +
"\vcertificate\x18F \x01(\fR\vcertificate\x12R\n" +
"\x0fnot_valid_after\x18P \x01(\v2*.controller.storage.timestamp.v1.TimestampR\rnotValidAfter\x12\x18\n" +
"\aversion\x18Z \x01(\rR\aversion\x12K\n" +
"\vcreate_time\x18d \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" +
"createTime\x12K\n" +
"\vupdate_time\x18n \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" +
"updateTime\"\xa7\x04\n" +
"\x1bTargetAliasProxyCertificate\x12\x1b\n" +
"\tpublic_id\x18\n" +
" \x01(\tR\bpublicId\x12\x1b\n" +
"\ttarget_id\x18\x14 \x01(\tR\btargetId\x12\x1d\n" +
"\n" +
"public_key\x18\x1e \x01(\fR\tpublicKey\x12\x1f\n" +
"\vprivate_key\x18( \x01(\fR\n" +
"privateKey\x122\n" +
"\x15private_key_encrypted\x182 \x01(\fR\x13privateKeyEncrypted\x12\x15\n" +
"\x06key_id\x18< \x01(\tR\x05keyId\x12\x19\n" +
"\balias_id\x18F \x01(\tR\aaliasId\x12 \n" +
"\vcertificate\x18P \x01(\fR\vcertificate\x12R\n" +
"\x0fnot_valid_after\x18Z \x01(\v2*.controller.storage.timestamp.v1.TimestampR\rnotValidAfter\x12\x18\n" +
"\aversion\x18d \x01(\rR\aversion\x12K\n" +
"\vcreate_time\x18n \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" +
"createTime\x12K\n" +
"\vupdate_time\x18x \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" +
"updateTimeB;Z9github.com/hashicorp/boundary/internal/target/store;storeb\x06proto3"
var (
file_controller_storage_target_store_v1_target_proto_rawDescOnce sync.Once
@ -727,29 +1064,37 @@ func file_controller_storage_target_store_v1_target_proto_rawDescGZIP() []byte {
return file_controller_storage_target_store_v1_target_proto_rawDescData
}
var file_controller_storage_target_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_controller_storage_target_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_controller_storage_target_store_v1_target_proto_goTypes = []any{
(*TargetView)(nil), // 0: controller.storage.target.store.v1.TargetView
(*TargetHostSet)(nil), // 1: controller.storage.target.store.v1.TargetHostSet
(*TargetAddress)(nil), // 2: controller.storage.target.store.v1.TargetAddress
(*CredentialLibrary)(nil), // 3: controller.storage.target.store.v1.CredentialLibrary
(*StaticCredential)(nil), // 4: controller.storage.target.store.v1.StaticCredential
(*CredentialSource)(nil), // 5: controller.storage.target.store.v1.CredentialSource
(*CredentialSourceView)(nil), // 6: controller.storage.target.store.v1.CredentialSourceView
(*timestamp.Timestamp)(nil), // 7: controller.storage.timestamp.v1.Timestamp
(*TargetView)(nil), // 0: controller.storage.target.store.v1.TargetView
(*TargetHostSet)(nil), // 1: controller.storage.target.store.v1.TargetHostSet
(*TargetAddress)(nil), // 2: controller.storage.target.store.v1.TargetAddress
(*CredentialLibrary)(nil), // 3: controller.storage.target.store.v1.CredentialLibrary
(*StaticCredential)(nil), // 4: controller.storage.target.store.v1.StaticCredential
(*CredentialSource)(nil), // 5: controller.storage.target.store.v1.CredentialSource
(*CredentialSourceView)(nil), // 6: controller.storage.target.store.v1.CredentialSourceView
(*TargetProxyCertificate)(nil), // 7: controller.storage.target.store.v1.TargetProxyCertificate
(*TargetAliasProxyCertificate)(nil), // 8: controller.storage.target.store.v1.TargetAliasProxyCertificate
(*timestamp.Timestamp)(nil), // 9: controller.storage.timestamp.v1.Timestamp
}
var file_controller_storage_target_store_v1_target_proto_depIdxs = []int32{
7, // 0: controller.storage.target.store.v1.TargetView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
7, // 1: controller.storage.target.store.v1.TargetView.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
7, // 2: controller.storage.target.store.v1.TargetHostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
7, // 3: controller.storage.target.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
7, // 4: controller.storage.target.store.v1.StaticCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
7, // 5: controller.storage.target.store.v1.CredentialSource.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
9, // 0: controller.storage.target.store.v1.TargetView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 1: controller.storage.target.store.v1.TargetView.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 2: controller.storage.target.store.v1.TargetHostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 3: controller.storage.target.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 4: controller.storage.target.store.v1.StaticCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 5: controller.storage.target.store.v1.CredentialSource.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 6: controller.storage.target.store.v1.TargetProxyCertificate.not_valid_after:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 7: controller.storage.target.store.v1.TargetProxyCertificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 8: controller.storage.target.store.v1.TargetProxyCertificate.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 9: controller.storage.target.store.v1.TargetAliasProxyCertificate.not_valid_after:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 10: controller.storage.target.store.v1.TargetAliasProxyCertificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
9, // 11: controller.storage.target.store.v1.TargetAliasProxyCertificate.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_controller_storage_target_store_v1_target_proto_init() }
@ -763,7 +1108,7 @@ func file_controller_storage_target_store_v1_target_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_storage_target_store_v1_target_proto_rawDesc), len(file_controller_storage_target_store_v1_target_proto_rawDesc)),
NumEnums: 0,
NumMessages: 7,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},

@ -18,6 +18,12 @@ import (
"github.com/hashicorp/boundary/internal/types/resource"
)
// ServerCertificate holds the PEM encoded certificate and key for a target
type ServerCertificate struct {
CertificatePem []byte
PrivateKeyPem []byte
}
// Target is a commmon interface for all target subtypes
type Target interface {
GetPublicId() string
@ -42,6 +48,7 @@ type Target interface {
GetCredentialSources() []CredentialSource
GetStorageBucketId() string
GetEnableSessionRecording() bool
GetProxyServerCertificate() *ServerCertificate
Clone() Target
SetPublicId(context.Context, string) error
SetProjectId(string)
@ -64,6 +71,7 @@ type Target interface {
SetStorageBucketId(string)
SetEnableSessionRecording(bool)
Oplog(op oplog.OpType) oplog.Metadata
SetProxyServerCertificate(*ServerCertificate)
}
const (

@ -0,0 +1,311 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package target
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
mathrand "math/rand"
"net"
"time"
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/target/store"
"github.com/hashicorp/boundary/internal/util"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
"google.golang.org/protobuf/proto"
)
func generatePrivAndPubKeys(ctx context.Context) (privKeyBytes []byte, pubKeyBytes []byte, err error) {
const op = "target.generatePrivAndPubKeys"
// Generate a private key using the P521 curve
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "failed to generate ECDSA key")
}
privKeyBytes, err = x509.MarshalECPrivateKey(key)
if err != nil {
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("error marshalling private key"))
}
pubKeyBytes, err = x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("error marshalling public key"))
}
return privKeyBytes, pubKeyBytes, nil
}
// 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
func generateTargetCert(ctx context.Context, privKey *ecdsa.PrivateKey, exp time.Time, opt ...Option) ([]byte, error) {
const op = "target.generateTargetCert"
switch {
case util.IsNil(privKey):
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing private key")
case exp.IsZero():
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing expiry")
case exp.Before(time.Now()):
return nil, errors.New(ctx, errors.InvalidParameter, op, "expiration time must be in the future")
}
opts := GetOpts(opt...)
template := &x509.Certificate{
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
},
Subject: pkix.Name{
CommonName: "localhost",
},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
SerialNumber: big.NewInt(mathrand.Int63()),
NotBefore: time.Now().Add(-1 * time.Minute),
NotAfter: exp,
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
}
if opts.withAlias != nil {
template.DNSNames = append(template.DNSNames, opts.withAlias.Value)
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.GenCert))
}
return certBytes, nil
}
// TargetProxyCertificate represents a proxy certificate for a target
type TargetProxyCertificate struct {
*store.TargetProxyCertificate
tableName string `gorm:"-"`
}
// newTargetProxyCertificate creates a new in memory TargetProxyCertificate
// Supports the options withTargetId to set the target ID
// If this is not provided, the TargetId will need to be set before storing the certificate
func NewTargetProxyCertificate(ctx context.Context, opt ...Option) (*TargetProxyCertificate, error) {
const op = "target.NewTargetProxyCertificate"
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)
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 &TargetProxyCertificate{
TargetProxyCertificate: &store.TargetProxyCertificate{
PrivateKey: privKey,
PublicKey: pubKey,
Certificate: certBytes,
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"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.WrapStruct(ctx, cipher, t.TargetProxyCertificate, 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 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")
}
if err := structwrapping.UnwrapStruct(ctx, cipher, t.TargetProxyCertificate, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
return nil
}
func allocTargetProxyCertificate() *TargetProxyCertificate {
return &TargetProxyCertificate{
TargetProxyCertificate: &store.TargetProxyCertificate{},
}
}
// Clone creates a clone of the TargetProxyCertificate
func (t *TargetProxyCertificate) Clone() *TargetProxyCertificate {
cp := proto.Clone(t.TargetProxyCertificate)
return &TargetProxyCertificate{
TargetProxyCertificate: cp.(*store.TargetProxyCertificate),
}
}
// VetForWrite implements db.VetForWrite() interface and validates a target certificate
func (t *TargetProxyCertificate) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error {
const op = "target.(TargetProxyCertificate).VetForWrite"
switch {
case t.PrivateKeyEncrypted == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing private key")
case t.PublicKey == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing public key")
case t.KeyId == "":
return errors.New(ctx, errors.InvalidParameter, op, "missing key id")
case t.TargetId == "":
return errors.New(ctx, errors.InvalidParameter, op, "missing target id")
case len(t.Certificate) == 0:
return errors.New(ctx, errors.InvalidParameter, op, "missing certificate")
case t.NotValidAfter == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing not valid after")
}
return nil
}
// TableName returns the table name.
func (t *TargetProxyCertificate) TableName() string {
return "target_proxy_certificate"
}
// TargetAliasProxyCertificate represents a certificate for a target accessed with an alias
type TargetAliasProxyCertificate struct {
*store.TargetAliasProxyCertificate
tableName string `gorm:"-"`
}
// NewTargetAliasProxyCertificate creates a new in memory TargetAliasProxyCertificate
func NewTargetAliasProxyCertificate(ctx context.Context, targetId string, alias *talias.Alias) (*TargetAliasProxyCertificate, error) {
const op = "target.NewTargetAliasProxyCertificate"
switch {
case targetId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target id")
case alias == nil:
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)
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 &TargetAliasProxyCertificate{
TargetAliasProxyCertificate: &store.TargetAliasProxyCertificate{
TargetId: targetId,
PrivateKey: privKey,
PublicKey: pubKey,
AliasId: alias.PublicId,
Certificate: certBytes,
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"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.WrapStruct(ctx, cipher, t.TargetAliasProxyCertificate, 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 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"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.UnwrapStruct(ctx, cipher, t.TargetAliasProxyCertificate, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
return nil
}
func allocTargetAliasProxyCertificate() *TargetAliasProxyCertificate {
return &TargetAliasProxyCertificate{
TargetAliasProxyCertificate: &store.TargetAliasProxyCertificate{},
}
}
// Clone creates a clone of the TargetAliasProxyCertificate
func (t *TargetAliasProxyCertificate) Clone() *TargetAliasProxyCertificate {
cp := proto.Clone(t.TargetAliasProxyCertificate)
return &TargetAliasProxyCertificate{
TargetAliasProxyCertificate: cp.(*store.TargetAliasProxyCertificate),
}
}
// VetForWrite implements db.VetForWrite() interface and validates the target alias certificate
func (t *TargetAliasProxyCertificate) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error {
const op = "target.(TargetAliasProxyCertificate).VetForWrite"
switch {
case t.PrivateKeyEncrypted == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing private key")
case t.PublicKey == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing public key")
case t.KeyId == "":
return errors.New(ctx, errors.InvalidParameter, op, "missing key id")
case t.TargetId == "":
return errors.New(ctx, errors.InvalidParameter, op, "missing target id")
case t.AliasId == "":
return errors.New(ctx, errors.InvalidParameter, op, "missing alias id")
case len(t.Certificate) == 0:
return errors.New(ctx, errors.InvalidParameter, op, "missing certificate")
case t.NotValidAfter == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing not valid after")
}
return nil
}
// TableName returns the table name.
func (t *TargetAliasProxyCertificate) TableName() string {
return "target_alias_proxy_certificate"
}

@ -0,0 +1,402 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package target
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"testing"
"time"
talias "github.com/hashicorp/boundary/internal/alias/target"
astore "github.com/hashicorp/boundary/internal/alias/target/store"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/aead"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateTargetCert(t *testing.T) {
t.Parallel()
ctx := context.Background()
privKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
require.NoError(t, err)
aliasValue := "test-alias-value"
alias := &talias.Alias{
Alias: &astore.Alias{
Value: aliasValue,
},
}
tests := []struct {
name string
privKey *ecdsa.PrivateKey
exp time.Time
opt []Option
wantErr bool
wantErrContains string
}{
{
name: "valid-target-proxy-cert",
privKey: privKey,
exp: time.Now().Add(1 * time.Hour),
},
{
name: "valid-with-alias",
privKey: privKey,
exp: time.Now().Add(1 * time.Hour),
opt: []Option{
WithAlias(alias),
},
},
{
name: "invalid-expiration",
privKey: privKey,
exp: time.Now().Add(-1 * time.Hour),
wantErr: true,
wantErrContains: "expiration time must be in the future",
},
{
name: "missing-key",
exp: time.Now().Add(1 * time.Hour),
wantErr: true,
wantErrContains: "missing private key",
},
{
name: "missing-expiry",
privKey: privKey,
wantErr: true,
wantErrContains: "missing expiry",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := generateTargetCert(ctx, tt.privKey, tt.exp, tt.opt...)
if tt.wantErr {
assert.Error(err)
return
}
require.NoError(err)
require.NotNil(got)
pCert, err := x509.ParseCertificate(got)
require.NoError(err)
require.NotNil(pCert)
require.Equal("localhost", pCert.Subject.CommonName)
// Cert timestamps do not have ms resolution
tt.exp = tt.exp.Truncate(time.Second)
require.True(tt.exp.Equal(pCert.NotAfter))
require.Contains(pCert.DNSNames, "localhost")
if tt.opt != nil {
opts := GetOpts(tt.opt...)
require.Contains(pCert.DNSNames, opts.withAlias.Value)
}
})
}
}
func TestTargetProxyCertificate(t *testing.T) {
t.Parallel()
ctx := context.Background()
tId := "test-target-id"
tests := []struct {
name string
opt []Option
wantErr bool
wantErrContains string
}{
{
name: "valid-target-proxy-cert",
},
{
name: "valid-with-options",
opt: []Option{
WithTargetId(tId),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
gotCert, err := NewTargetProxyCertificate(ctx, tt.opt...)
if tt.wantErr {
assert.Error(err)
return
}
require.NoError(err)
require.NotNil(gotCert)
if tt.opt != nil {
assert.Equal(tId, gotCert.TargetId)
}
})
}
}
func Test_encrypt_decrypt_TargetCert(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rootWrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, rootWrapper)
org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper))
databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
projDatabaseWrapper, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
proxyCert, err := NewTargetProxyCertificate(ctx)
require.NoError(t, err)
tests := []struct {
name string
certKey *TargetProxyCertificate
encryptWrapper wrapping.Wrapper
wantEncryptErrMatch *errors.Template
decryptWrapper wrapping.Wrapper
wantDecryptErrMatch *errors.Template
}{
{
name: "success",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
decryptWrapper: databaseWrapper,
},
{
name: "encrypt-missing-wrapper",
certKey: proxyCert,
wantEncryptErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "encrypt-bad-wrapper",
certKey: proxyCert,
encryptWrapper: &aead.Wrapper{},
wantEncryptErrMatch: errors.T(errors.Encrypt),
},
{
name: "encrypt-missing-wrapper",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
wantDecryptErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "decrypt-bad-wrapper",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
decryptWrapper: &aead.Wrapper{},
wantDecryptErrMatch: errors.T(errors.Decrypt),
},
{
name: "decrypt-wrong-wrapper",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
decryptWrapper: projDatabaseWrapper,
wantDecryptErrMatch: errors.T(errors.Decrypt),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
encryptedCert := tt.certKey.Clone()
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)
return
}
require.NoError(err)
assert.NotEmpty(encryptedCert.PrivateKeyEncrypted)
decryptedCert := encryptedCert.Clone()
decryptedCert.PrivateKey = []byte("")
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)
return
}
require.NoError(err)
assert.Equal(tt.certKey.PrivateKey, decryptedCert.PrivateKey)
})
}
}
func TestTargetAliasProxyCertificate(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,
},
}
tests := []struct {
name string
targetId string
alias *talias.Alias
wantErr bool
wantErrContains string
}{
{
name: "valid-target-cert",
targetId: tId,
alias: alias,
},
{
name: "missing-target-id",
alias: alias,
wantErr: true,
wantErrContains: "missing target id",
},
{
name: "missing-alias",
targetId: tId,
wantErr: true,
wantErrContains: "missing alias",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
gotCert, err := NewTargetAliasProxyCertificate(ctx, tt.targetId, tt.alias)
if tt.wantErr {
assert.Error(err)
return
}
require.NoError(err)
require.NotNil(gotCert)
assert.Equal(tId, gotCert.TargetId)
assert.Equal(tAId, gotCert.AliasId)
assert.NotNil(gotCert.Certificate)
})
}
}
func Test_encrypt_decrypt_TargetAliasCert(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rootWrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, rootWrapper)
org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper))
databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
projDatabaseWrapper, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
tId := "test-target-id"
tAId := "test-alias-id"
aliasValue := "test-alias-value"
alias := &talias.Alias{
Alias: &astore.Alias{
PublicId: tAId,
Value: aliasValue,
},
}
proxyCert, err := NewTargetAliasProxyCertificate(ctx, tId, alias)
require.NoError(t, err)
tests := []struct {
name string
certKey *TargetAliasProxyCertificate
encryptWrapper wrapping.Wrapper
wantEncryptErrMatch *errors.Template
decryptWrapper wrapping.Wrapper
wantDecryptErrMatch *errors.Template
}{
{
name: "success",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
decryptWrapper: databaseWrapper,
},
{
name: "encrypt-missing-wrapper",
certKey: proxyCert,
wantEncryptErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "encrypt-bad-wrapper",
certKey: proxyCert,
encryptWrapper: &aead.Wrapper{},
wantEncryptErrMatch: errors.T(errors.Encrypt),
},
{
name: "encrypt-missing-wrapper",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
wantDecryptErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "decrypt-bad-wrapper",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
decryptWrapper: &aead.Wrapper{},
wantDecryptErrMatch: errors.T(errors.Decrypt),
},
{
name: "decrypt-wrong-wrapper",
certKey: proxyCert,
encryptWrapper: databaseWrapper,
decryptWrapper: projDatabaseWrapper,
wantDecryptErrMatch: errors.T(errors.Decrypt),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
encryptedCert := tt.certKey.Clone()
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)
return
}
require.NoError(err)
assert.NotEmpty(encryptedCert.PrivateKeyEncrypted)
decryptedCert := encryptedCert.Clone()
decryptedCert.PrivateKey = []byte("")
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)
return
}
require.NoError(err)
assert.Equal(tt.certKey.PrivateKey, decryptedCert.PrivateKey)
})
}
}

@ -161,6 +161,10 @@ func (t *Target) GetStorageBucketId() string {
return ""
}
func (t *Target) GetProxyServerCertificate() *target.ServerCertificate {
return nil
}
func (t *Target) Clone() target.Target {
cp := proto.Clone(t.Target)
return &Target{
@ -253,6 +257,8 @@ func (t *Target) SetEnableSessionRecording(_ bool) {}
func (t *Target) SetStorageBucketId(_ string) {}
func (t *Target) SetProxyServerCertificate(*target.ServerCertificate) {}
func (t *Target) Oplog(op oplog.OpType) oplog.Metadata {
return oplog.Metadata{
"resource-public-id": []string{t.PublicId},

@ -164,6 +164,10 @@ func (t *Target) GetStorageBucketId() string {
return ""
}
func (t *Target) GetProxyServerCertificate() *target.ServerCertificate {
return nil
}
func (t *Target) SetPublicId(ctx context.Context, publicId string) error {
const op = "tcp.(Target).SetPublicId"
if !strings.HasPrefix(publicId, TargetPrefix+"_") {
@ -247,5 +251,6 @@ func (t *Target) SetCredentialSources(sources []target.CredentialSource) {
t.CredentialSources = sources
}
func (t *Target) SetEnableSessionRecording(_ bool) {}
func (t *Target) SetStorageBucketId(_ string) {}
func (t *Target) SetEnableSessionRecording(_ bool) {}
func (t *Target) SetStorageBucketId(_ string) {}
func (t *Target) SetProxyServerCertificate(*target.ServerCertificate) {}

Loading…
Cancel
Save