mirror of https://github.com/hashicorp/boundary
feat(target): add proxy certificate repos and protos (#1577)
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;
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue