open-consul/agent/structs/config_entry_inline_certificate.go
hc-github-team-consul-core 54ace0e072
Backport of Ensure RSA keys are at least 2048 bits in length into release/1.16.x (#17935)
* backport of commit 93ccfe4c1195ba0ab2d12443f25d9cf29e9e4f0c

* Ensure RSA keys are at least 2048 bits in length (#17911)

* Ensure RSA keys are at least 2048 bits in length

* Add changelog

* update key length check for FIPS compliance

* Fix no new variables error and failing to return when error exists from
validating

* clean up code for better readability

* actually return value

---------

Co-authored-by: jm96441n <john.maguire@hashicorp.com>
2023-06-28 17:36:38 +00:00

168 lines
4.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
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"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex
}
func (e *InlineCertificateConfigEntry) GetKind() string { return InlineCertificate }
func (e *InlineCertificateConfigEntry) GetName() string { return e.Name }
func (e *InlineCertificateConfigEntry) Normalize() error { 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)
}