mirror of https://github.com/hashicorp/consul
185 lines
4.9 KiB
Go
185 lines
4.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package structs
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/version"
|
|
)
|
|
|
|
// InlineCertificateConfigEntry manages the configuration for an inline certificate
|
|
// with the given name.
|
|
type InlineCertificateConfigEntry struct {
|
|
// Kind of config entry. This will be set to structs.InlineCertificate.
|
|
Kind string
|
|
|
|
// Name is used to match the config entry with its associated inline certificate.
|
|
Name string
|
|
|
|
// Certificate is the public certificate component of an x509 key pair encoded in raw PEM format.
|
|
Certificate string
|
|
// PrivateKey is the private key component of an x509 key pair encoded in raw PEM format.
|
|
PrivateKey string
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
Hash uint64 `json:",omitempty" hash:"ignore"`
|
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
|
RaftIndex `hash:"ignore"`
|
|
}
|
|
|
|
func (e *InlineCertificateConfigEntry) SetHash(h uint64) {
|
|
e.Hash = h
|
|
}
|
|
|
|
func (e *InlineCertificateConfigEntry) GetHash() uint64 {
|
|
return e.Hash
|
|
}
|
|
|
|
func (e *InlineCertificateConfigEntry) GetKind() string { return InlineCertificate }
|
|
func (e *InlineCertificateConfigEntry) GetName() string { return e.Name }
|
|
func (e *InlineCertificateConfigEntry) Normalize() error {
|
|
h, err := HashConfigEntry(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Hash = h
|
|
return nil
|
|
}
|
|
func (e *InlineCertificateConfigEntry) GetMeta() map[string]string { return e.Meta }
|
|
func (e *InlineCertificateConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta {
|
|
return &e.EnterpriseMeta
|
|
}
|
|
func (e *InlineCertificateConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex }
|
|
|
|
// Envoy will silently reject any RSA keys that are less than 2048 bytes long
|
|
// https://github.com/envoyproxy/envoy/blob/main/source/extensions/transport_sockets/tls/context_impl.cc#L238
|
|
const MinKeyLength = 2048
|
|
|
|
func (e *InlineCertificateConfigEntry) Validate() error {
|
|
err := validateConfigEntryMeta(e.Meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
privateKeyBlock, _ := pem.Decode([]byte(e.PrivateKey))
|
|
if privateKeyBlock == nil {
|
|
return errors.New("failed to parse private key PEM")
|
|
}
|
|
|
|
err = validateKeyLength(privateKeyBlock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
certificateBlock, _ := pem.Decode([]byte(e.Certificate))
|
|
if certificateBlock == nil {
|
|
return errors.New("failed to parse certificate PEM")
|
|
}
|
|
|
|
// make sure we have a valid x509 certificate
|
|
_, err = x509.ParseCertificate(certificateBlock.Bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse certificate: %w", err)
|
|
}
|
|
|
|
// validate that the cert was generated with the given private key
|
|
_, err = tls.X509KeyPair([]byte(e.Certificate), []byte(e.PrivateKey))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate that each host referenced in the CN, DNSSans, and IPSans
|
|
// are valid hostnames
|
|
hosts, err := e.Hosts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, host := range hosts {
|
|
if _, ok := dns.IsDomainName(host); !ok {
|
|
return fmt.Errorf("host %q must be a valid DNS hostname", host)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateKeyLength(privateKeyBlock *pem.Block) error {
|
|
if privateKeyBlock.Type != "RSA PRIVATE KEY" {
|
|
return nil
|
|
}
|
|
|
|
key, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyBitLen := key.N.BitLen()
|
|
|
|
if version.IsFIPS() {
|
|
return fipsLenCheck(keyBitLen)
|
|
}
|
|
|
|
return nonFipsLenCheck(keyBitLen)
|
|
}
|
|
|
|
func nonFipsLenCheck(keyLen int) error {
|
|
// ensure private key is of the correct length
|
|
if keyLen < MinKeyLength {
|
|
return errors.New("key length must be at least 2048 bits")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func fipsLenCheck(keyLen int) error {
|
|
if keyLen != 2048 && keyLen != 3072 && keyLen != 4096 {
|
|
return errors.New("key length invalid: only RSA lengths of 2048, 3072, and 4096 are allowed in FIPS mode")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *InlineCertificateConfigEntry) Hosts() ([]string, error) {
|
|
certificateBlock, _ := pem.Decode([]byte(e.Certificate))
|
|
if certificateBlock == nil {
|
|
return nil, errors.New("failed to parse certificate PEM")
|
|
}
|
|
|
|
certificate, err := x509.ParseCertificate(certificateBlock.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
|
}
|
|
|
|
hosts := []string{certificate.Subject.CommonName}
|
|
|
|
for _, name := range certificate.DNSNames {
|
|
hosts = append(hosts, name)
|
|
}
|
|
|
|
for _, ip := range certificate.IPAddresses {
|
|
hosts = append(hosts, ip.String())
|
|
}
|
|
|
|
return hosts, nil
|
|
}
|
|
|
|
func (e *InlineCertificateConfigEntry) CanRead(authz acl.Authorizer) error {
|
|
var authzContext acl.AuthorizerContext
|
|
e.FillAuthzContext(&authzContext)
|
|
return authz.ToAllowAuthorizer().MeshReadAllowed(&authzContext)
|
|
}
|
|
|
|
func (e *InlineCertificateConfigEntry) CanWrite(authz acl.Authorizer) error {
|
|
var authzContext acl.AuthorizerContext
|
|
e.FillAuthzContext(&authzContext)
|
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
|
}
|