2b1e5f69e2
* update go version to 1.18 for api and sdk, go mod tidy * removes ioutil usage everywhere which was deprecated in go1.16 in favour of io and os packages. Also introduces a lint rule which forbids use of ioutil going forward. Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com>
439 lines
14 KiB
Go
439 lines
14 KiB
Go
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 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
|
|
}
|