transport/tlscommon/tls_config.go (255 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package tlscommon
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"net"
"time"
"github.com/elastic/elastic-agent-libs/logp"
)
// TLSConfig is the interface used to configure a tcp client or server from a `Config`
type TLSConfig struct {
// List of allowed SSL/TLS protocol versions. Connections might be dropped
// after handshake succeeded, if TLS version in use is not listed.
Versions []TLSVersion
// Configure SSL/TLS verification mode used during handshake. By default
// VerifyFull will be used.
Verification TLSVerificationMode
// List of certificate chains to present to the other side of the
// connection.
Certificates []tls.Certificate
// Set of root certificate authorities use to verify server certificates.
// If RootCAs is nil, TLS might use the system its root CA set (not supported
// on MS Windows).
RootCAs *x509.CertPool
// Set of root certificate authorities use to verify client certificates.
// If ClientCAs is nil, TLS might use the system its root CA set (not supported
// on MS Windows).
ClientCAs *x509.CertPool
// List of supported cipher suites. If nil, a default list provided by the
// implementation will be used.
CipherSuites []CipherSuite
// Types of elliptic curves that will be used in an ECDHE handshake. If empty,
// the implementation will choose a default.
CurvePreferences []tls.CurveID
// Renegotiation controls what types of renegotiation are supported.
// The default, never, is correct for the vast majority of applications.
Renegotiation tls.RenegotiationSupport
// ClientAuth controls how we want to verify certificate from a client, `none`, `optional` and
// `required`, default to required. Do not affect TCP client.
ClientAuth tls.ClientAuthType
// CASha256 is the CA certificate pin, this is used to validate the CA that will be used to trust
// the server certificate.
CASha256 []string
// CATrustedFingerprint is the HEX encoded fingerprint of a CA certificate. If present in the chain
// this certificate will be added to the list of trusted CAs (RootCAs) during the handshake.
CATrustedFingerprint string
// ServerName is the remote server we're connecting to. It can be a hostname or IP address.
ServerName string
// time returns the current time as the number of seconds since the epoch.
// If time is nil, TLS uses time.Now.
time func() time.Time
}
var (
ErrMissingPeerCertificate = errors.New("missing peer certificates")
)
// ToConfig generates a tls.Config object. Note, you must use BuildModuleClientConfig to generate a config with
// ServerName set, use that method for servers with SNI.
// By default VerifyConnection is set to client mode.
func (c *TLSConfig) ToConfig() *tls.Config {
if c == nil {
return &tls.Config{} //nolint:gosec // empty TLS config
}
minVersion, maxVersion := extractMinMaxVersion(c.Versions)
insecure := c.Verification != VerifyStrict
if c.Verification == VerifyNone {
logp.NewLogger("tls").Warn("SSL/TLS verifications disabled.")
}
return &tls.Config{
MinVersion: minVersion,
MaxVersion: maxVersion,
Certificates: c.Certificates,
RootCAs: c.RootCAs,
ClientCAs: c.ClientCAs,
InsecureSkipVerify: insecure, //nolint:gosec // we are using our own verification for now
CipherSuites: convCipherSuites(c.CipherSuites),
CurvePreferences: c.CurvePreferences,
Renegotiation: c.Renegotiation,
ClientAuth: c.ClientAuth,
Time: c.time,
VerifyConnection: makeVerifyConnection(c),
}
}
// BuildModuleClientConfig takes the TLSConfig and transform it into a `tls.Config`.
func (c *TLSConfig) BuildModuleClientConfig(host string) *tls.Config {
if c == nil {
// use default TLS settings, if config is empty.
return &tls.Config{
ServerName: host,
InsecureSkipVerify: true, //nolint:gosec // we are using our own verification for now
VerifyConnection: makeVerifyConnection(&TLSConfig{
Verification: VerifyFull,
ServerName: host,
}),
}
}
// Make a copy of c, because we're gonna mutate it after
// calling ToConfig. ToConfig calls a function that creates
// a closure that needs to access cc. A shallow copy is enough
// because all slice/pointer fields won't be modified.
cc := *c
// Keep a copy of the host (whether an IP or hostname)
// for later validation. It is used by makeVerifyConnection
cc.ServerName = host
config := cc.ToConfig()
// config.ServerName does not verify IP addresses
config.ServerName = host
return config
}
// BuildServerConfig takes the TLSConfig and transform it into a `tls.Config`
// for server side connections.
func (c *TLSConfig) BuildServerConfig(host string) *tls.Config {
if c == nil {
// use default TLS settings, if config is empty.
return &tls.Config{
ServerName: host,
InsecureSkipVerify: true, //nolint:gosec // we are using our own verification for now
VerifyConnection: makeVerifyServerConnection(&TLSConfig{
Verification: VerifyCertificate,
ServerName: host,
}),
}
}
config := c.ToConfig()
config.ServerName = host
config.VerifyConnection = makeVerifyServerConnection(c)
return config
}
func trustRootCA(cfg *TLSConfig, peerCerts []*x509.Certificate) error {
logger := logp.NewLogger("tls")
logger.Info("'ca_trusted_fingerprint' set, looking for matching fingerprints")
fingerprint, err := hex.DecodeString(cfg.CATrustedFingerprint)
if err != nil {
return fmt.Errorf("decode 'ca_trusted_fingerprint': %w", err)
}
foundCADigests := []string{}
for _, cert := range peerCerts {
// Compute digest for each certificate.
digest := sha256.Sum256(cert.Raw)
if cert.IsCA {
foundCADigests = append(foundCADigests, hex.EncodeToString(digest[:]))
}
if !bytes.Equal(digest[0:], fingerprint) {
continue
}
// Make sure the fingerprint matches a CA certificate
if !cert.IsCA {
logger.Warn("Certificate matching 'ca_trusted_fingerprint' found, but it is not a CA certificate. 'ca_trusted_fingerprint' can only be used to trust CA certificates.")
continue
}
logger.Info("CA certificate matching 'ca_trusted_fingerprint' found, adding it to 'certificate_authorities'")
if cfg.RootCAs == nil {
cfg.RootCAs = x509.NewCertPool()
}
cfg.RootCAs.AddCert(cert)
return nil
}
// if we are here, we didn't find any CA certificate matching the fingerprint
if len(foundCADigests) == 0 {
logger.Warn("The remote server's certificate is presented without its certificate chain. Using 'ca_trusted_fingerprint' requires that the server presents a certificate chain that includes the certificate's issuing certificate authority.")
} else {
logger.Warnf("The provided 'ca_trusted_fingerprint': '%s' does not match the fingerprint of any Certificate Authority present in the server's certificate chain. Found the following CA fingerprints instead: %v", cfg.CATrustedFingerprint, foundCADigests)
}
return nil
}
func makeVerifyConnection(cfg *TLSConfig) func(tls.ConnectionState) error {
serverName := cfg.ServerName
switch cfg.Verification {
case VerifyFull:
// Cert is trusted by CA
// Hostname or IP matches the certificate
// tls.Config.InsecureSkipVerify is set to true
return func(cs tls.ConnectionState) error {
if cfg.CATrustedFingerprint != "" {
if err := trustRootCA(cfg, cs.PeerCertificates); err != nil {
return err
}
}
// On the client side, PeerCertificates can't be empty.
if len(cs.PeerCertificates) == 0 {
return ErrMissingPeerCertificate
}
opts := x509.VerifyOptions{
Roots: cfg.RootCAs,
Intermediates: x509.NewCertPool(),
}
err := verifyCertsWithOpts(cs.PeerCertificates, cfg.CASha256, opts)
if err != nil {
return err
}
return verifyHostname(cs.PeerCertificates[0], serverName)
}
case VerifyCertificate:
// Cert is trusted by CA
// Does NOT validate hostname or IP addresses
// tls.Config.InsecureSkipVerify is set to true
return func(cs tls.ConnectionState) error {
if cfg.CATrustedFingerprint != "" {
if err := trustRootCA(cfg, cs.PeerCertificates); err != nil {
return err
}
}
// On the client side, PeerCertificates can't be empty.
if len(cs.PeerCertificates) == 0 {
return ErrMissingPeerCertificate
}
opts := x509.VerifyOptions{
Roots: cfg.RootCAs,
Intermediates: x509.NewCertPool(),
}
return verifyCertsWithOpts(cs.PeerCertificates, cfg.CASha256, opts)
}
case VerifyStrict:
// Cert is trusted by CA
// Hostname or IP matches the certificate
// Returns error if SNA is empty
// The whole validation is done by Go's standard library default
// SSL/TLS verification (tls.Config.InsecureSkipVerify is set to false)
// so we only need to check the pin
if len(cfg.CASha256) > 0 {
return func(cs tls.ConnectionState) error {
if cfg.CATrustedFingerprint != "" {
if err := trustRootCA(cfg, cs.PeerCertificates); err != nil {
return err
}
}
return verifyCAPin(cfg.CASha256, cs.VerifiedChains)
}
}
default:
}
return nil
}
func makeVerifyServerConnection(cfg *TLSConfig) func(tls.ConnectionState) error {
switch cfg.Verification {
// VerifyFull would attempt to match 'host' (c.ServerName) that is the host
// the client is trying to connect to with a DNS, IP or the CN from the
// client's certificate. Such validation, besides making no sense on the
// server side also causes errors as the client certificate usually does not
// contain a DNS, IP or CN matching the server's hostname.
case VerifyFull, VerifyCertificate:
return func(cs tls.ConnectionState) error {
if len(cs.PeerCertificates) == 0 {
if cfg.ClientAuth == tls.RequireAndVerifyClientCert {
return ErrMissingPeerCertificate
}
return nil
}
opts := x509.VerifyOptions{
Roots: cfg.ClientCAs,
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}
return verifyCertsWithOpts(cs.PeerCertificates, cfg.CASha256, opts)
}
case VerifyStrict:
if len(cfg.CASha256) > 0 {
return func(cs tls.ConnectionState) error {
return verifyCAPin(cfg.CASha256, cs.VerifiedChains)
}
}
default:
}
return nil
}
func verifyCertsWithOpts(certs []*x509.Certificate, casha256 []string, opts x509.VerifyOptions) error {
for _, cert := range certs[1:] {
opts.Intermediates.AddCert(cert)
}
verifiedChains, err := certs[0].Verify(opts)
if err != nil {
return err
}
if len(casha256) > 0 {
return verifyCAPin(casha256, verifiedChains)
}
return nil
}
// verifyHostname verifies if the provided hostnmae matches
// cert.DNSNames, cert.IPAddress (SNA)
// For hostnames, if SNA is empty, validate the hostname against cert.Subject.CommonName
func verifyHostname(cert *x509.Certificate, hostname string) error {
if hostname == "" {
return nil
}
// check if the server name is an IP
ip := hostname
if len(ip) >= 3 && ip[0] == '[' && ip[len(ip)-1] == ']' {
ip = ip[1 : len(ip)-1]
}
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
for _, certIP := range cert.IPAddresses {
if parsedIP.Equal(certIP) {
return nil
}
}
parsedCNIP := net.ParseIP(cert.Subject.CommonName)
if parsedCNIP != nil {
if parsedIP.Equal(parsedCNIP) {
return nil
}
}
return x509.HostnameError{Certificate: cert, Host: hostname}
}
dnsnames := cert.DNSNames
if len(dnsnames) == 0 || len(dnsnames) == 1 && dnsnames[0] == "" {
if cert.Subject.CommonName != "" {
dnsnames = []string{cert.Subject.CommonName}
}
}
for _, name := range dnsnames {
if matchHostnames(name, hostname) {
if !validHostname(name, true) {
return fmt.Errorf("invalid hostname in cert")
}
return nil
}
}
return x509.HostnameError{Certificate: cert, Host: hostname}
}