Newer
Older
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
}
// TLSAuthACLListFlag is a convenience type that allows callers to use
// the 'flag' package to specify a list of TLSAuthACL objects. It
// implements the flag.Value interface.
type TLSAuthACLListFlag []*TLSAuthACL
func (l TLSAuthACLListFlag) String() string {
var out []string
for _, acl := range l {
out = append(out, fmt.Sprintf("%s:%s", acl.Path, acl.CommonName))
}
return strings.Join(out, ",")
}
func (l *TLSAuthACLListFlag) Set(value string) error {
parts := strings.SplitN(value, ":", 2)
if len(parts) != 2 {
return errors.New("bad acl format")
}
*l = append(*l, &TLSAuthACL{
Path: parts[0],
CommonName: parts[1],
})
return nil
}
// 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
}
var serverCiphers = []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}
// 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
}
// Set some TLS-level parameters (cipher-related), assuming
// we're using EC keys.
tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
}
// Require client certificates if a CA is specified.
if c.CA != "" {
cas, err := common.LoadCA(c.CA)
if err != nil {
return nil, err
}
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
tlsConf.ClientCAs = cas
}
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
}
// Log the failed access, useful for debugging.
var tlsmsg string
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
tlsmsg = fmt.Sprintf(" TLS client '%s' at", r.TLS.PeerCertificates[0].Subject.CommonName)
}
log.Printf("unauthorized access to %s from %s%s", r.URL.Path, tlsmsg, r.RemoteAddr)
http.Error(w, "Forbidden", http.StatusForbidden)