Skip to content
Snippets Groups Projects
tls.go 2.97 KiB
Newer Older
ale's avatar
ale committed
package serverutil

import (
	"crypto/tls"
	"net/http"
	"regexp"

	common "git.autistici.org/ai3/go-common"
)

// TLSAuthACL describes a single access control entry. Path and
// CommonName are anchored regular expressions (they must match the
// entire string).
type TLSAuthACL struct {
	Path       string `yaml:"path"`
	CommonName string `yaml:"cn"`

	pathRx, cnRx *regexp.Regexp
}

func (p *TLSAuthACL) compile() error {
	var err error
	p.pathRx, err = regexp.Compile("^" + p.Path + "$")
	if err != nil {
		return err
	}
	p.cnRx, err = regexp.Compile("^" + p.CommonName + "$")
	return err
}

func (p *TLSAuthACL) match(req *http.Request) bool {
	if !p.pathRx.MatchString(req.URL.Path) {
		return false
	}
	for _, cert := range req.TLS.PeerCertificates {
		if p.cnRx.MatchString(cert.Subject.CommonName) {
			return true
		}
	}
	return false
}

// TLSAuthConfig stores access control lists for TLS authentication. Access
// control lists are matched against the request path and the
// CommonName component of the peer certificate subject.
type TLSAuthConfig struct {
	Allow []*TLSAuthACL `yaml:"allow"`
}

func (c *TLSAuthConfig) match(req *http.Request) bool {
	// Fail *OPEN* if unconfigured.
	if c == nil || len(c.Allow) == 0 {
		return true
	}
	for _, acl := range c.Allow {
		if acl.match(req) {
			return true
		}
	}
	return false
}

// TLSServerConfig configures a TLS server with client authentication
// and authorization based on the client X509 certificate.
type TLSServerConfig struct {
	Cert string         `yaml:"cert"`
	Key  string         `yaml:"key"`
	CA   string         `yaml:"ca"`
	Auth *TLSAuthConfig `yaml:"acl"`
}

// TLSConfig returns a tls.Config created with the current configuration.
func (c *TLSServerConfig) TLSConfig() (*tls.Config, error) {
	cert, err := tls.LoadX509KeyPair(c.Cert, c.Key)
	if err != nil {
		return nil, err
	}

	cas, err := common.LoadCA(c.CA)
	if err != nil {
		return nil, err
	}

	// Set some TLS-level parameters (cipher-related), assuming
	// we're using EC keys.
	tlsConf := &tls.Config{
		Certificates:             []tls.Certificate{cert},
		ClientAuth:               tls.RequireAndVerifyClientCert,
		ClientCAs:                cas,
		CipherSuites:             []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384},
		MinVersion:               tls.VersionTLS12,
		PreferServerCipherSuites: true,
	}
	tlsConf.BuildNameToCertificate()

	return tlsConf, nil
}

// TLSAuthWrapper protects a root HTTP handler with TLS authentication.
func (c *TLSServerConfig) TLSAuthWrapper(h http.Handler) (http.Handler, error) {
	// Compile regexps.
	if c.Auth != nil {
		for _, acl := range c.Auth.Allow {
			if err := acl.compile(); err != nil {
				return nil, err
			}
		}
	}

	// Build the wrapper function to check client certificates
	// identities (looking at the CN part of the X509 subject).
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if c.Auth.match(r) {
			h.ServeHTTP(w, r)
			return
		}
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
	}), nil
}