From 2d056fbe66849f041a233a0d961639fae3835cbb Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 25 Apr 2019 13:54:48 -0600 Subject: Initial commit of Storage, TLS, and automatic HTTPS implementations --- modules/caddytls/connpolicy.go | 149 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 modules/caddytls/connpolicy.go (limited to 'modules/caddytls/connpolicy.go') diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go new file mode 100644 index 0000000..9400034 --- /dev/null +++ b/modules/caddytls/connpolicy.go @@ -0,0 +1,149 @@ +package caddytls + +import ( + "crypto/tls" + "encoding/json" + "fmt" + + "bitbucket.org/lightcodelabs/caddy2" + "github.com/go-acme/lego/challenge/tlsalpn01" + "github.com/mholt/certmagic" +) + +// ConnectionPolicies is an ordered group of connection policies; +// the first matching policy will be used to configure TLS +// connections at handshake-time. +type ConnectionPolicies []*ConnectionPolicy + +// TLSConfig converts the group of policies to a standard-lib-compatible +// TLS configuration which selects the first matching policy based on +// the ClientHello. +func (cp ConnectionPolicies) TLSConfig(handle caddy2.Handle) (*tls.Config, error) { + // connection policy matchers + for i, pol := range cp { + for modName, rawMsg := range pol.MatchersRaw { + val, err := caddy2.LoadModule("tls.handshake_match."+modName, rawMsg) + if err != nil { + return nil, fmt.Errorf("loading handshake matcher module '%s': %s", modName, err) + } + cp[i].Matchers = append(cp[i].Matchers, val.(ConnectionMatcher)) + } + cp[i].MatchersRaw = nil // allow GC to deallocate - TODO: Does this help? + } + + // pre-build standard TLS configs so we don't have to at handshake-time + for i := range cp { + err := cp[i].buildStandardTLSConfig(handle) + if err != nil { + return nil, fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) + } + } + + return &tls.Config{ + GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { + policyLoop: + for _, pol := range cp { + for _, matcher := range pol.Matchers { + if !matcher.Match(hello) { + continue policyLoop + } + } + return pol.stdTLSConfig, nil + } + return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) + }, + }, nil +} + +// ConnectionPolicy specifies the logic for handling a TLS handshake. +type ConnectionPolicy struct { + MatchersRaw map[string]json.RawMessage `json:"match,omitempty"` + + CipherSuites []string `json:"cipher_suites,omitempty"` + Curves []string `json:"curves,omitempty"` + ALPN []string `json:"alpn,omitempty"` + ProtocolMin string `json:"protocol_min,omitempty"` + ProtocolMax string `json:"protocol_max,omitempty"` + + // TODO: Client auth + + // TODO: see if starlark could be useful here - enterprise only + StarlarkHandshake string `json:"starlark_handshake,omitempty"` + + Matchers []ConnectionMatcher + stdTLSConfig *tls.Config +} + +func (cp *ConnectionPolicy) buildStandardTLSConfig(handle caddy2.Handle) error { + tlsApp := handle.App("tls").(*TLS) + + cfg := &tls.Config{ + NextProtos: cp.ALPN, + PreferServerCipherSuites: true, + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + cfgTpl, err := tlsApp.getConfigForName(hello.ServerName) + if err != nil { + return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err) + } + newCfg := certmagic.New(tlsApp.certCache, cfgTpl) + return newCfg.GetCertificate(hello) + }, + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + // TODO: Session ticket key rotation (use Storage) + } + + // add all the cipher suites in order, without duplicates + cipherSuitesAdded := make(map[uint16]struct{}) + for _, csName := range cp.CipherSuites { + csID := supportedCipherSuites[csName] + if _, ok := cipherSuitesAdded[csID]; !ok { + cipherSuitesAdded[csID] = struct{}{} + cfg.CipherSuites = append(cfg.CipherSuites, csID) + } + } + + // add all the curve preferences in order, without duplicates + curvesAdded := make(map[tls.CurveID]struct{}) + for _, curveName := range cp.Curves { + curveID := supportedCurves[curveName] + if _, ok := curvesAdded[curveID]; !ok { + curvesAdded[curveID] = struct{}{} + cfg.CurvePreferences = append(cfg.CurvePreferences, curveID) + } + } + + // ensure ALPN includes the ACME TLS-ALPN protocol + var alpnFound bool + for _, a := range cp.ALPN { + if a == tlsalpn01.ACMETLS1Protocol { + alpnFound = true + break + } + } + if !alpnFound { + cfg.NextProtos = append(cfg.NextProtos, tlsalpn01.ACMETLS1Protocol) + } + + // min and max protocol versions + if cp.ProtocolMin != "" { + cfg.MinVersion = supportedProtocols[cp.ProtocolMin] + } + if cp.ProtocolMax != "" { + cfg.MaxVersion = supportedProtocols[cp.ProtocolMax] + } + if cp.ProtocolMin > cp.ProtocolMax { + return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", cp.ProtocolMin, cp.ProtocolMax) + } + + // TODO: client auth, and other fields + + cp.stdTLSConfig = cfg + + return nil +} + +// ConnectionMatcher is a type which matches TLS handshakes. +type ConnectionMatcher interface { + Match(*tls.ClientHelloInfo) bool +} -- cgit v1.2.3