You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/target/target_certificate.go

508 lines
18 KiB

// 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"
"encoding/pem"
"math"
"math/big"
"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...)
randomSerialNumber, err := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64)))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating random serial number"))
}
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: randomSerialNumber,
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
}
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
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...)
notValidAfter := time.Now().AddDate(1, 0, 0) // 1 year from now
privKey, pubKey, cert, err := generateKeysAndCert(ctx, notValidAfter)
if err != nil {
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: 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"
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"
}
// 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
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")
}
notValidAfter := time.Now().AddDate(1, 0, 0) // 1 year from now
privKey, pubKey, cert, err := generateKeysAndCert(ctx, notValidAfter, WithAlias(alias))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating target proxy cert and keys"))
}
return &TargetAliasProxyCertificate{
TargetAliasProxyCertificate: &store.TargetAliasProxyCertificate{
TargetId: targetId,
PrivateKey: privKey,
PublicKey: pubKey,
AliasId: alias.PublicId,
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"
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"
}
// 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)
}