Newer
Older
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). The first path to match in a list of ACLs will
// identify the ACL to be applied.
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) matchPath(req *http.Request) bool {
return p.pathRx.MatchString(req.URL.Path)
}
func (p *TLSAuthACL) matchCN(req *http.Request) bool {
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
}
if acl.matchPath(req) {
if acl.matchCN(req) {
return true
}
break
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)