|
|
@ -2,8 +2,11 @@ package caddytls |
|
|
|
|
|
|
|
import ( |
|
|
|
"crypto/tls" |
|
|
|
"crypto/x509" |
|
|
|
"encoding/json" |
|
|
|
"fmt" |
|
|
|
"math/big" |
|
|
|
"strings" |
|
|
|
|
|
|
|
"bitbucket.org/lightcodelabs/caddy2" |
|
|
|
"github.com/go-acme/lego/challenge/tlsalpn01" |
|
|
@ -26,7 +29,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy2.Context) (*tls.Config, error) |
|
|
|
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].matchers = append(cp[i].matchers, val.(ConnectionMatcher)) |
|
|
|
} |
|
|
|
cp[i].MatchersRaw = nil // allow GC to deallocate - TODO: Does this help?
|
|
|
|
} |
|
|
@ -39,11 +42,34 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy2.Context) (*tls.Config, error) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// using ServerName to match policies is extremely common, especially in configs
|
|
|
|
// with lots and lots of different policies; we can fast-track those by indexing
|
|
|
|
// them by SNI, so we don't have to iterate potentially thousands of policies
|
|
|
|
indexedBySNI := make(map[string]ConnectionPolicies) |
|
|
|
if len(cp) > 30 { |
|
|
|
for _, p := range cp { |
|
|
|
for _, m := range p.matchers { |
|
|
|
if sni, ok := m.(MatchServerName); ok { |
|
|
|
for _, sniName := range sni { |
|
|
|
indexedBySNI[sniName] = append(indexedBySNI[sniName], p) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return &tls.Config{ |
|
|
|
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { |
|
|
|
// filter policies by SNI first, if possible, to speed things up
|
|
|
|
// when there may be lots of policies
|
|
|
|
possiblePolicies := cp |
|
|
|
if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { |
|
|
|
possiblePolicies = indexedPolicies |
|
|
|
} |
|
|
|
|
|
|
|
policyLoop: |
|
|
|
for _, pol := range cp { |
|
|
|
for _, matcher := range pol.Matchers { |
|
|
|
for _, pol := range possiblePolicies { |
|
|
|
for _, matcher := range pol.matchers { |
|
|
|
if !matcher.Match(hello) { |
|
|
|
continue policyLoop |
|
|
|
} |
|
|
@ -65,16 +91,18 @@ type ConnectionPolicy struct { |
|
|
|
ProtocolMin string `json:"protocol_min,omitempty"` |
|
|
|
ProtocolMax string `json:"protocol_max,omitempty"` |
|
|
|
|
|
|
|
CertSelection *CertSelectionPolicy `json:"certificate_selection,omitempty"` |
|
|
|
|
|
|
|
// TODO: Client auth
|
|
|
|
|
|
|
|
// TODO: see if starlark could be useful here - enterprise only
|
|
|
|
StarlarkHandshake string `json:"starlark_handshake,omitempty"` |
|
|
|
|
|
|
|
Matchers []ConnectionMatcher |
|
|
|
matchers []ConnectionMatcher |
|
|
|
stdTLSConfig *tls.Config |
|
|
|
} |
|
|
|
|
|
|
|
func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { |
|
|
|
func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { |
|
|
|
tlsAppIface, err := ctx.App("tls") |
|
|
|
if err != nil { |
|
|
|
return fmt.Errorf("getting tls app: %v", err) |
|
|
@ -82,17 +110,17 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { |
|
|
|
tlsApp := tlsAppIface.(*TLS) |
|
|
|
|
|
|
|
cfg := &tls.Config{ |
|
|
|
NextProtos: cp.ALPN, |
|
|
|
NextProtos: p.ALPN, |
|
|
|
PreferServerCipherSuites: true, |
|
|
|
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { |
|
|
|
// TODO: Must fix https://github.com/mholt/caddy/issues/2588
|
|
|
|
// (allow customizing the selection of a very specific certificate
|
|
|
|
// based on the ClientHelloInfo)
|
|
|
|
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) |
|
|
|
if p.CertSelection != nil { |
|
|
|
newCfg.CertSelector = makeCertSelector(p) |
|
|
|
} |
|
|
|
return newCfg.GetCertificate(hello) |
|
|
|
}, |
|
|
|
MinVersion: tls.VersionTLS12, |
|
|
@ -102,7 +130,7 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { |
|
|
|
|
|
|
|
// add all the cipher suites in order, without duplicates
|
|
|
|
cipherSuitesAdded := make(map[uint16]struct{}) |
|
|
|
for _, csName := range cp.CipherSuites { |
|
|
|
for _, csName := range p.CipherSuites { |
|
|
|
csID := supportedCipherSuites[csName] |
|
|
|
if _, ok := cipherSuitesAdded[csID]; !ok { |
|
|
|
cipherSuitesAdded[csID] = struct{}{} |
|
|
@ -112,7 +140,7 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { |
|
|
|
|
|
|
|
// add all the curve preferences in order, without duplicates
|
|
|
|
curvesAdded := make(map[tls.CurveID]struct{}) |
|
|
|
for _, curveName := range cp.Curves { |
|
|
|
for _, curveName := range p.Curves { |
|
|
|
curveID := supportedCurves[curveName] |
|
|
|
if _, ok := curvesAdded[curveID]; !ok { |
|
|
|
curvesAdded[curveID] = struct{}{} |
|
|
@ -122,7 +150,7 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { |
|
|
|
|
|
|
|
// ensure ALPN includes the ACME TLS-ALPN protocol
|
|
|
|
var alpnFound bool |
|
|
|
for _, a := range cp.ALPN { |
|
|
|
for _, a := range p.ALPN { |
|
|
|
if a == tlsalpn01.ACMETLS1Protocol { |
|
|
|
alpnFound = true |
|
|
|
break |
|
|
@ -133,23 +161,76 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { |
|
|
|
} |
|
|
|
|
|
|
|
// min and max protocol versions
|
|
|
|
if cp.ProtocolMin != "" { |
|
|
|
cfg.MinVersion = supportedProtocols[cp.ProtocolMin] |
|
|
|
if p.ProtocolMin != "" { |
|
|
|
cfg.MinVersion = supportedProtocols[p.ProtocolMin] |
|
|
|
} |
|
|
|
if cp.ProtocolMax != "" { |
|
|
|
cfg.MaxVersion = supportedProtocols[cp.ProtocolMax] |
|
|
|
if p.ProtocolMax != "" { |
|
|
|
cfg.MaxVersion = supportedProtocols[p.ProtocolMax] |
|
|
|
} |
|
|
|
if cp.ProtocolMin > cp.ProtocolMax { |
|
|
|
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", cp.ProtocolMin, cp.ProtocolMax) |
|
|
|
if p.ProtocolMin > p.ProtocolMax { |
|
|
|
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax) |
|
|
|
} |
|
|
|
|
|
|
|
// TODO: client auth, and other fields
|
|
|
|
|
|
|
|
cp.stdTLSConfig = cfg |
|
|
|
p.stdTLSConfig = cfg |
|
|
|
|
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
// CertSelectionPolicy represents a policy for selecting the certificate
|
|
|
|
// used to complete a handshake when there may be multiple options. All
|
|
|
|
// fields specified must match the candidate certificate for it to be chosen.
|
|
|
|
// This was needed to solve https://github.com/mholt/caddy/issues/2588.
|
|
|
|
type CertSelectionPolicy struct { |
|
|
|
SerialNumber *big.Int `json:"serial_number,omitempty"` |
|
|
|
SubjectOrganization string `json:"subject.organization,omitempty"` |
|
|
|
PublicKeyAlgorithm pkAlgorithm `json:"public_key_algorithm,omitempty"` |
|
|
|
} |
|
|
|
|
|
|
|
func makeCertSelector(p *ConnectionPolicy) func(*tls.ClientHelloInfo, []certmagic.Certificate) (certmagic.Certificate, error) { |
|
|
|
return func(hello *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) { |
|
|
|
for _, cert := range choices { |
|
|
|
var matchOrg bool |
|
|
|
if p.CertSelection.SubjectOrganization != "" { |
|
|
|
for _, org := range cert.Subject.Organization { |
|
|
|
if p.CertSelection.SubjectOrganization == org { |
|
|
|
matchOrg = true |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if !matchOrg { |
|
|
|
continue |
|
|
|
} |
|
|
|
if p.CertSelection.PublicKeyAlgorithm != pkAlgorithm(x509.UnknownPublicKeyAlgorithm) && |
|
|
|
pkAlgorithm(cert.PublicKeyAlgorithm) != p.CertSelection.PublicKeyAlgorithm { |
|
|
|
continue |
|
|
|
} |
|
|
|
if p.CertSelection.SerialNumber != nil && |
|
|
|
cert.SerialNumber.Cmp(p.CertSelection.SerialNumber) != 0 { |
|
|
|
continue |
|
|
|
} |
|
|
|
return cert, nil |
|
|
|
} |
|
|
|
return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
type pkAlgorithm x509.PublicKeyAlgorithm |
|
|
|
|
|
|
|
// UnmarshalJSON satisfies json.Unmarshaler.
|
|
|
|
func (a *pkAlgorithm) UnmarshalJSON(b []byte) error { |
|
|
|
algoStr := strings.ToLower(strings.Trim(string(b), `"`)) |
|
|
|
algo, ok := publicKeyAlgorithms[algoStr] |
|
|
|
if !ok { |
|
|
|
return fmt.Errorf("unrecognized public key algorithm: %s (expected one of %v)", |
|
|
|
algoStr, publicKeyAlgorithms) |
|
|
|
} |
|
|
|
a = &algo |
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
// ConnectionMatcher is a type which matches TLS handshakes.
|
|
|
|
type ConnectionMatcher interface { |
|
|
|
Match(*tls.ClientHelloInfo) bool |
|
|
|