mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
Implement custom cert selection policies; optimize matching for SNI
This commit is contained in:
parent
5a4a1421de
commit
210d0cf7f1
3 changed files with 109 additions and 20 deletions
|
@ -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
|
||||
|
|
|
@ -11,7 +11,7 @@ type MatchServerName []string
|
|||
|
||||
func init() {
|
||||
caddy2.RegisterModule(caddy2.Module{
|
||||
Name: "tls.handshake_match.host",
|
||||
Name: "tls.handshake_match.sni",
|
||||
New: func() interface{} { return MatchServerName{} },
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package caddytls
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -316,4 +317,11 @@ var supportedProtocols = map[string]uint16{
|
|||
"tls1.3": tls.VersionTLS13,
|
||||
}
|
||||
|
||||
// publicKeyAlgorithms is the map of supported public key algorithms.
|
||||
var publicKeyAlgorithms = map[string]pkAlgorithm{
|
||||
"rsa": pkAlgorithm(x509.RSA),
|
||||
"dsa": pkAlgorithm(x509.DSA),
|
||||
"ecdsa": pkAlgorithm(x509.ECDSA),
|
||||
}
|
||||
|
||||
const automateKey = "automate"
|
||||
|
|
Loading…
Reference in a new issue