// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package connect
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/url"
"os"
"strings"
"sync"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/api"
)
// parseLeafX509Cert will parse an X509 certificate
// from the TLS certificate and store the parsed
// value in the TLS certificate as the Leaf field.
func parseLeafX509Cert ( leaf * tls . Certificate ) error {
if leaf == nil {
// nothing to parse for nil cert
return nil
}
if leaf . Leaf != nil {
// leaf cert was already parsed
return nil
}
cert , err := x509 . ParseCertificate ( leaf . Certificate [ 0 ] )
if err != nil {
return err
}
leaf . Leaf = cert
return nil
}
// verifierFunc is a function that can accept rawCertificate bytes from a peer
// and verify them against a given tls.Config. It's called from the
// tls.Config.VerifyPeerCertificate hook.
//
// We don't pass verifiedChains since that is always nil in our usage.
// Implementations can use the roots provided in the cfg to verify the certs.
//
// The passed *tls.Config may have a nil VerifyPeerCertificates function but
// will have correct roots, leaf and other fields.
type verifierFunc func ( cfg * tls . Config , rawCerts [ ] [ ] byte ) error
// defaultTLSConfig returns the standard config with no peer verifier. It is
// insecure to use it as-is.
func defaultTLSConfig ( ) * tls . Config {
cfg := & tls . Config {
MinVersion : tls . VersionTLS12 ,
ClientAuth : tls . RequireAndVerifyClientCert ,
// We don't have access to go internals that decide if AES hardware
// acceleration is available in order to prefer CHA CHA if not. So let's
// just always prefer AES for now. We can look into doing something uglier
// later like using an external lib for AES checking if it seems important.
// https://github.com/golang/go/blob/df91b8044dbe790c69c16058330f545be069cc1f/src/crypto/tls/common.go#L919:14
CipherSuites : [ ] uint16 {
tls . TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ,
tls . TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,
tls . TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ,
tls . TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 ,
tls . TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 ,
tls . TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 ,
} ,
// We have to set this since otherwise Go will attempt to verify DNS names
// match DNS SAN/CN which we don't want. We hook up VerifyPeerCertificate to
// do our own path validation as well as Connect AuthZ.
InsecureSkipVerify : true ,
// Include h2 to allow connect http servers to automatically support http2.
// See: https://github.com/golang/go/blob/917c33fe8672116b04848cf11545296789cafd3b/src/net/http/server.go#L2724-L2731
NextProtos : [ ] string { "h2" } ,
}
return cfg
}
// devTLSConfigFromFiles returns a default TLS Config but with certs and CAs
// based on local files for dev. No verification is setup.
func devTLSConfigFromFiles ( caFile , certFile ,
keyFile string ) ( * tls . Config , error ) {
roots := x509 . NewCertPool ( )
bs , err := os . ReadFile ( caFile )
if err != nil {
return nil , err
}
roots . AppendCertsFromPEM ( bs )
cert , err := tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
return nil , err
}
cfg := defaultTLSConfig ( )
cfg . Certificates = [ ] tls . Certificate { cert }
cfg . RootCAs = roots
cfg . ClientCAs = roots
return cfg , nil
}
// CertURIFromConn is a helper to extract the service identifier URI from a
// net.Conn. If the net.Conn is not a *tls.Conn then an error is always
// returned. If the *tls.Conn didn't present a valid connect certificate, or is
// not yet past the handshake, an error is returned.
func CertURIFromConn ( conn net . Conn ) ( connect . CertURI , error ) {
tc , ok := conn . ( * tls . Conn )
if ! ok {
return nil , fmt . Errorf ( "invalid non-TLS connect client" )
}
gotURI , err := extractCertURI ( tc . ConnectionState ( ) . PeerCertificates )
if err != nil {
return nil , err
}
return connect . ParseCertURI ( gotURI )
}
// extractCertURI returns the first URI SAN from the leaf certificate presented
// in the slice. The slice is expected to be the passed from
// tls.Conn.ConnectionState().PeerCertificates and requires that the leaf has at
// least one URI and the first URI is the correct one to use.
func extractCertURI ( certs [ ] * x509 . Certificate ) ( * url . URL , error ) {
if len ( certs ) < 1 {
return nil , errors . New ( "no peer certificate presented" )
}
// Only check the first cert assuming this is the only leaf. It's not clear if
// services might ever legitimately present multiple leaf certificates or if
// the slice is just to allow presenting the whole chain of intermediates.
cert := certs [ 0 ]
// Our certs will only ever have a single URI for now so only check that
if len ( cert . URIs ) < 1 {
return nil , errors . New ( "peer certificate invalid" )
}
return cert . URIs [ 0 ] , nil
}
// verifyServerCertMatchesURI is used on tls connections dialed to a connect
// server to ensure that the certificate it presented has the correct identity.
func verifyServerCertMatchesURI ( certs [ ] * x509 . Certificate , expected connect . CertURI ) error {
expectedStr := expected . URI ( ) . String ( )
gotURI , err := extractCertURI ( certs )
if err != nil {
return errors . New ( "peer certificate mismatch" )
}
// Override the hostname since we rely on x509 constraints to limit ability to
// spoof the trust domain if needed (i.e. because a root is shared with other
// PKI or Consul clusters). This allows for seamless migrations between trust
// domains.
expectURI := expected . URI ( )
expectURI . Host = gotURI . Host
if strings . EqualFold ( gotURI . String ( ) , expectURI . String ( ) ) {
return nil
}
return fmt . Errorf ( "peer certificate mismatch got %s, want %s" ,
gotURI . String ( ) , expectedStr )
}
// newServerSideVerifier returns a verifierFunc that wraps the provided
// api.Client to verify the TLS chain and perform AuthZ for the server end of
// the connection. The service name provided is used as the target service name
// for the Authorization.
func newServerSideVerifier ( logger hclog . Logger , client * api . Client , serviceName string ) verifierFunc {
return func ( tlsCfg * tls . Config , rawCerts [ ] [ ] byte ) error {
leaf , err := verifyChain ( tlsCfg , rawCerts , false )
if err != nil {
logger . Error ( "failed TLS verification" , "error" , err )
return err
}
// Check leaf is a cert we understand
if len ( leaf . URIs ) < 1 {
logger . Error ( "invalid leaf certificate: no URIs set" )
return errors . New ( "connect: invalid leaf certificate" )
}
certURI , err := connect . ParseCertURI ( leaf . URIs [ 0 ] )
if err != nil {
logger . Error ( "invalid leaf certificate URI" , "error" , err )
return errors . New ( "connect: invalid leaf certificate URI" )
}
// No AuthZ if there is no client.
if client == nil {
logger . Info ( "nil client provided" )
return nil
}
// Perform AuthZ
req := & api . AgentAuthorizeParams {
Target : serviceName ,
ClientCertURI : certURI . URI ( ) . String ( ) ,
ClientCertSerial : connect . EncodeSerialNumber ( leaf . SerialNumber ) ,
}
resp , err := client . Agent ( ) . ConnectAuthorize ( req )
if err != nil {
logger . Error ( "authz call failed" , "error" , err )
return errors . New ( "connect: authz call failed: " + err . Error ( ) )
}
if ! resp . Authorized {
logger . Error ( "authz call denied" , "reason" , resp . Reason )
return errors . New ( "connect: authz denied: " + resp . Reason )
}
return nil
}
}
// clientSideVerifier is a verifierFunc that performs verification of certificates
// on the client end of the connection. For now it is just basic TLS
// verification since the identity check needs additional state and becomes
// clunky to customize the callback for every outgoing request. That is done
// within Service.Dial for now.
func clientSideVerifier ( tlsCfg * tls . Config , rawCerts [ ] [ ] byte ) error {
_ , err := verifyChain ( tlsCfg , rawCerts , true )
return err
}
// verifyChain performs standard TLS verification without enforcing remote
// hostname matching.
func verifyChain ( tlsCfg * tls . Config , rawCerts [ ] [ ] byte , client bool ) ( * x509 . Certificate , error ) {
// Fetch leaf and intermediates. This is based on code form tls handshake.
if len ( rawCerts ) < 1 {
return nil , errors . New ( "tls: no certificates from peer" )
}
certs := make ( [ ] * x509 . Certificate , len ( rawCerts ) )
for i , asn1Data := range rawCerts {
cert , err := x509 . ParseCertificate ( asn1Data )
if err != nil {
return nil , errors . New ( "tls: failed to parse certificate from peer: " + err . Error ( ) )
}
certs [ i ] = cert
}
cas := tlsCfg . RootCAs
if client {
cas = tlsCfg . ClientCAs
}
opts := x509 . VerifyOptions {
Roots : cas ,
Intermediates : x509 . NewCertPool ( ) ,
}
if ! client {
// Server side only sets KeyUsages in tls. This defaults to ServerAuth in
// x509 lib. See
// https://github.com/golang/go/blob/ee7dd810f9ca4e63ecfc1d3044869591783b8b74/src/crypto/x509/verify.go#L866-L868
opts . KeyUsages = [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageClientAuth }
}
// All but the first cert are intermediates
for _ , cert := range certs [ 1 : ] {
opts . Intermediates . AddCert ( cert )
}
_ , err := certs [ 0 ] . Verify ( opts )
return certs [ 0 ] , err
}
// dynamicTLSConfig represents the state for returning a tls.Config that can
// have root and leaf certificates updated dynamically with all existing clients
// and servers automatically picking up the changes. It requires initializing
// with a valid base config from which all the non-certificate and verification
// params are used. The base config passed should not be modified externally as
// it is assumed to be serialized by the embedded mutex.
type dynamicTLSConfig struct {
base * tls . Config
sync . RWMutex
leaf * tls . Certificate
roots * x509 . CertPool
// readyCh is closed when the config first gets both leaf and roots set.
// Watchers can wait on this via ReadyWait.
readyCh chan struct { }
}
type tlsCfgUpdate struct {
ch chan struct { }
next * tlsCfgUpdate
}
// newDynamicTLSConfig returns a dynamicTLSConfig constructed from base.
// base.Certificates[0] is used as the initial leaf and base.RootCAs is used as
// the initial roots.
func newDynamicTLSConfig ( base * tls . Config , logger hclog . Logger ) * dynamicTLSConfig {
cfg := & dynamicTLSConfig {
base : base ,
}
if len ( base . Certificates ) > 0 {
cfg . leaf = & base . Certificates [ 0 ]
// If this does error then future calls to Ready will fail
// It is better to handle not-Ready rather than failing
if err := parseLeafX509Cert ( cfg . leaf ) ; err != nil && logger != nil {
logger . Error ( "error parsing configured leaf certificate" , "error" , err )
}
}
if base . RootCAs != nil {
cfg . roots = base . RootCAs
}
if ! cfg . Ready ( ) {
cfg . readyCh = make ( chan struct { } )
}
return cfg
}
// Get fetches the lastest tls.Config with all the hooks attached to keep it
// loading the most recent roots and certs even after future changes to cfg.
//
// The verifierFunc passed will be attached to the config returned such that it
// runs with the _latest_ config object returned passed to it. That means that a
// client can use this config for a long time and will still verify against the
// latest roots even though the roots in the struct is has can't change.
func ( cfg * dynamicTLSConfig ) Get ( v verifierFunc ) * tls . Config {
cfg . RLock ( )
defer cfg . RUnlock ( )
copy := cfg . base . Clone ( )
copy . RootCAs = cfg . roots
copy . ClientCAs = cfg . roots
if v != nil {
copy . VerifyPeerCertificate = func ( rawCerts [ ] [ ] byte , chains [ ] [ ] * x509 . Certificate ) error {
return v ( cfg . Get ( nil ) , rawCerts )
}
}
copy . GetCertificate = func ( _ * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
leaf := cfg . Leaf ( )
if leaf == nil {
return nil , errors . New ( "tls: no certificates configured" )
}
return leaf , nil
}
copy . GetClientCertificate = func ( _ * tls . CertificateRequestInfo ) ( * tls . Certificate , error ) {
leaf := cfg . Leaf ( )
if leaf == nil {
return nil , errors . New ( "tls: no certificates configured" )
}
return leaf , nil
}
copy . GetConfigForClient = func ( * tls . ClientHelloInfo ) ( * tls . Config , error ) {
return cfg . Get ( v ) , nil
}
return copy
}
// SetRoots sets new roots.
func ( cfg * dynamicTLSConfig ) SetRoots ( roots * x509 . CertPool ) error {
cfg . Lock ( )
defer cfg . Unlock ( )
cfg . roots = roots
cfg . notify ( )
return nil
}
// SetLeaf sets a new leaf.
func ( cfg * dynamicTLSConfig ) SetLeaf ( leaf * tls . Certificate ) error {
cfg . Lock ( )
defer cfg . Unlock ( )
if err := parseLeafX509Cert ( leaf ) ; err != nil {
return err
}
cfg . leaf = leaf
cfg . notify ( )
return nil
}
// notify is called under lock during an update to check if we are now ready.
func ( cfg * dynamicTLSConfig ) notify ( ) {
if cfg . readyCh != nil && cfg . leaf != nil && cfg . roots != nil && cfg . leaf . Leaf != nil {
close ( cfg . readyCh )
cfg . readyCh = nil
}
}
func ( cfg * dynamicTLSConfig ) VerifyLeafWithRoots ( ) error {
cfg . RLock ( )
defer cfg . RUnlock ( )
if cfg . roots == nil {
return fmt . Errorf ( "No roots are set" )
} else if cfg . leaf == nil {
return fmt . Errorf ( "No leaf certificate is set" )
} else if cfg . leaf . Leaf == nil {
return fmt . Errorf ( "Leaf certificate has not been parsed" )
}
_ , err := cfg . leaf . Leaf . Verify ( x509 . VerifyOptions { Roots : cfg . roots } )
return err
}
// Roots returns the current CA root CertPool.
func ( cfg * dynamicTLSConfig ) Roots ( ) * x509 . CertPool {
cfg . RLock ( )
defer cfg . RUnlock ( )
return cfg . roots
}
// Leaf returns the current Leaf certificate.
func ( cfg * dynamicTLSConfig ) Leaf ( ) * tls . Certificate {
cfg . RLock ( )
defer cfg . RUnlock ( )
return cfg . leaf
}
// Ready returns whether or not both roots and a leaf certificate are
// configured. If both are non-nil, they are assumed to be valid and usable.
func ( cfg * dynamicTLSConfig ) Ready ( ) bool {
// not locking because VerifyLeafWithRoots will do that
return cfg . VerifyLeafWithRoots ( ) == nil
}
// ReadyWait returns a chan that is closed when the Service becomes ready
// for use for the first time. Note that if the Service is ready when it is
// called it returns a nil chan. Ready means that it has root and leaf
// certificates configured but not that the combination is valid nor that
// the current time is within the validity window of the certificate. The
// service may subsequently stop being "ready" if it's certificates expire
// or are revoked and an error prevents new ones from being loaded but this
// method will not stop returning a nil chan in that case. It is only useful
// for initial startup. For ongoing health Ready() should be used.
func ( cfg * dynamicTLSConfig ) ReadyWait ( ) <- chan struct { } {
cfg . RLock ( )
defer cfg . RUnlock ( )
return cfg . readyCh
}