From 245211ebe9f881b575461958274bada3b8e20b7b Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Thu, 23 Nov 2017 18:34:19 +0000
Subject: [PATCH] Add server-side TLS helpers

Includes TLS authentication, to authorize peers
based on the subject of their X509 certificate.
---
 serverutil/http.go |  68 ++++++++++++++++++++++++++
 serverutil/tls.go  | 119 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 187 insertions(+)
 create mode 100644 serverutil/http.go
 create mode 100644 serverutil/tls.go

diff --git a/serverutil/http.go b/serverutil/http.go
new file mode 100644
index 0000000..079b933
--- /dev/null
+++ b/serverutil/http.go
@@ -0,0 +1,68 @@
+package serverutil
+
+import (
+	"context"
+	"crypto/tls"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+)
+
+// Serve HTTP(S) content on the specified address. If serverConfig is
+// not nil, enable HTTPS and TLS authentication.
+//
+// This function will return an error if there are problems creating
+// the listener, otherwise it will handle graceful termination on
+// SIGINT or SIGTERM and return nil.
+func Serve(h http.Handler, serverConfig *TLSServerConfig, addr string) (err error) {
+	var tlsConfig *tls.Config
+	if serverConfig != nil {
+		tlsConfig, err = serverConfig.TLSConfig()
+		if err != nil {
+			return err
+		}
+		h, err = serverConfig.TLSAuthWrapper(h)
+		if err != nil {
+			return err
+		}
+	}
+
+	srv := &http.Server{
+		Addr:         addr,
+		Handler:      h,
+		ReadTimeout:  30 * time.Second,
+		WriteTimeout: 30 * time.Second,
+		IdleTimeout:  60 * time.Second,
+		TLSConfig:    tlsConfig,
+	}
+
+	done := make(chan struct{})
+	sigCh := make(chan os.Signal, 1)
+	go func() {
+		<-sigCh
+		log.Printf("exiting")
+
+		// Gracefully terminate for 3 seconds max, then shut
+		// down remaining clients.
+		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+		defer cancel()
+		if err := srv.Shutdown(ctx); err == context.Canceled {
+			if err := srv.Close(); err != nil {
+				log.Printf("error terminating server: %v", err)
+			}
+		}
+
+		close(done)
+	}()
+	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
+		return err
+	}
+
+	<-done
+	return nil
+}
diff --git a/serverutil/tls.go b/serverutil/tls.go
new file mode 100644
index 0000000..926488f
--- /dev/null
+++ b/serverutil/tls.go
@@ -0,0 +1,119 @@
+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
+}
-- 
GitLab