@marcel91200 / caddy Mirror · read-only / commits / 210d0cf7
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

Implement custom cert selection policies; optimize matching for SNI

Matthew Holt committed May 24, 2019 at 13:18 UTC 210d0cf7f1040c1372a79869b8b279a92a52baf5
3 files changed +109 -20
modules/caddytls/connpolicy.go
+100 -19
@@ -2,8 +2,11 @@ package caddytls
2
3 import (
4 "crypto/tls"
5 + "crypto/x509"
6 "encoding/json"
7 "fmt"
8 + "math/big"
9 + "strings"
10
11 "bitbucket.org/lightcodelabs/caddy2"
12 "github.com/go-acme/lego/challenge/tlsalpn01"
@@ -26,7 +29,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy2.Context) (*tls.Config, error)
29 if err != nil {
30 return nil, fmt.Errorf("loading handshake matcher module '%s': %s", modName, err)
31 }
29 - cp[i].Matchers = append(cp[i].Matchers, val.(ConnectionMatcher))
32 + cp[i].matchers = append(cp[i].matchers, val.(ConnectionMatcher))
33 }
34 cp[i].MatchersRaw = nil // allow GC to deallocate - TODO: Does this help?
35 }
@@ -39,11 +42,34 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy2.Context) (*tls.Config, error)
42 }
43 }
44
45 + // using ServerName to match policies is extremely common, especially in configs
46 + // with lots and lots of different policies; we can fast-track those by indexing
47 + // them by SNI, so we don't have to iterate potentially thousands of policies
48 + indexedBySNI := make(map[string]ConnectionPolicies)
49 + if len(cp) > 30 {
50 + for _, p := range cp {
51 + for _, m := range p.matchers {
52 + if sni, ok := m.(MatchServerName); ok {
53 + for _, sniName := range sni {
54 + indexedBySNI[sniName] = append(indexedBySNI[sniName], p)
55 + }
56 + }
57 + }
58 + }
59 + }
60 +
61 return &tls.Config{
62 GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
63 + // filter policies by SNI first, if possible, to speed things up
64 + // when there may be lots of policies
65 + possiblePolicies := cp
66 + if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok {
67 + possiblePolicies = indexedPolicies
68 + }
69 +
70 policyLoop:
45 - for _, pol := range cp {
46 - for _, matcher := range pol.Matchers {
71 + for _, pol := range possiblePolicies {
72 + for _, matcher := range pol.matchers {
73 if !matcher.Match(hello) {
74 continue policyLoop
75 }
@@ -65,16 +91,18 @@ type ConnectionPolicy struct {
91 ProtocolMin string `json:"protocol_min,omitempty"`
92 ProtocolMax string `json:"protocol_max,omitempty"`
93
94 + CertSelection *CertSelectionPolicy `json:"certificate_selection,omitempty"`
95 +
96 // TODO: Client auth
97
98 // TODO: see if starlark could be useful here - enterprise only
99 StarlarkHandshake string `json:"starlark_handshake,omitempty"`
100
73 - Matchers []ConnectionMatcher
101 + matchers []ConnectionMatcher
102 stdTLSConfig *tls.Config
103 }
104
77 -func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
105 +func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
106 tlsAppIface, err := ctx.App("tls")
107 if err != nil {
108 return fmt.Errorf("getting tls app: %v", err)
@@ -82,17 +110,17 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
110 tlsApp := tlsAppIface.(*TLS)
111
112 cfg := &tls.Config{
85 - NextProtos: cp.ALPN,
113 + NextProtos: p.ALPN,
114 PreferServerCipherSuites: true,
115 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
88 - // TODO: Must fix https://github.com/mholt/caddy/issues/2588
89 - // (allow customizing the selection of a very specific certificate
90 - // based on the ClientHelloInfo)
116 cfgTpl, err := tlsApp.getConfigForName(hello.ServerName)
117 if err != nil {
118 return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err)
119 }
120 newCfg := certmagic.New(tlsApp.certCache, cfgTpl)
121 + if p.CertSelection != nil {
122 + newCfg.CertSelector = makeCertSelector(p)
123 + }
124 return newCfg.GetCertificate(hello)
125 },
126 MinVersion: tls.VersionTLS12,
@@ -102,7 +130,7 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
130
131 // add all the cipher suites in order, without duplicates
132 cipherSuitesAdded := make(map[uint16]struct{})
105 - for _, csName := range cp.CipherSuites {
133 + for _, csName := range p.CipherSuites {
134 csID := supportedCipherSuites[csName]
135 if _, ok := cipherSuitesAdded[csID]; !ok {
136 cipherSuitesAdded[csID] = struct{}{}
@@ -112,7 +140,7 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
140
141 // add all the curve preferences in order, without duplicates
142 curvesAdded := make(map[tls.CurveID]struct{})
115 - for _, curveName := range cp.Curves {
143 + for _, curveName := range p.Curves {
144 curveID := supportedCurves[curveName]
145 if _, ok := curvesAdded[curveID]; !ok {
146 curvesAdded[curveID] = struct{}{}
@@ -122,7 +150,7 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
150
151 // ensure ALPN includes the ACME TLS-ALPN protocol
152 var alpnFound bool
125 - for _, a := range cp.ALPN {
153 + for _, a := range p.ALPN {
154 if a == tlsalpn01.ACMETLS1Protocol {
155 alpnFound = true
156 break
@@ -133,23 +161,76 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
161 }
162
163 // min and max protocol versions
136 - if cp.ProtocolMin != "" {
137 - cfg.MinVersion = supportedProtocols[cp.ProtocolMin]
164 + if p.ProtocolMin != "" {
165 + cfg.MinVersion = supportedProtocols[p.ProtocolMin]
166 }
139 - if cp.ProtocolMax != "" {
140 - cfg.MaxVersion = supportedProtocols[cp.ProtocolMax]
167 + if p.ProtocolMax != "" {
168 + cfg.MaxVersion = supportedProtocols[p.ProtocolMax]
169 }
142 - if cp.ProtocolMin > cp.ProtocolMax {
143 - return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", cp.ProtocolMin, cp.ProtocolMax)
170 + if p.ProtocolMin > p.ProtocolMax {
171 + return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
172 }
173
174 // TODO: client auth, and other fields
175
148 - cp.stdTLSConfig = cfg
176 + p.stdTLSConfig = cfg
177
178 return nil
179 }
180
181 +// CertSelectionPolicy represents a policy for selecting the certificate
182 +// used to complete a handshake when there may be multiple options. All
183 +// fields specified must match the candidate certificate for it to be chosen.
184 +// This was needed to solve https://github.com/mholt/caddy/issues/2588.
185 +type CertSelectionPolicy struct {
186 + SerialNumber *big.Int `json:"serial_number,omitempty"`
187 + SubjectOrganization string `json:"subject.organization,omitempty"`
188 + PublicKeyAlgorithm pkAlgorithm `json:"public_key_algorithm,omitempty"`
189 +}
190 +
191 +func makeCertSelector(p *ConnectionPolicy) func(*tls.ClientHelloInfo, []certmagic.Certificate) (certmagic.Certificate, error) {
192 + return func(hello *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) {
193 + for _, cert := range choices {
194 + var matchOrg bool
195 + if p.CertSelection.SubjectOrganization != "" {
196 + for _, org := range cert.Subject.Organization {
197 + if p.CertSelection.SubjectOrganization == org {
198 + matchOrg = true
199 + break
200 + }
201 + }
202 + }
203 + if !matchOrg {
204 + continue
205 + }
206 + if p.CertSelection.PublicKeyAlgorithm != pkAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
207 + pkAlgorithm(cert.PublicKeyAlgorithm) != p.CertSelection.PublicKeyAlgorithm {
208 + continue
209 + }
210 + if p.CertSelection.SerialNumber != nil &&
211 + cert.SerialNumber.Cmp(p.CertSelection.SerialNumber) != 0 {
212 + continue
213 + }
214 + return cert, nil
215 + }
216 + return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
217 + }
218 +}
219 +
220 +type pkAlgorithm x509.PublicKeyAlgorithm
221 +
222 +// UnmarshalJSON satisfies json.Unmarshaler.
223 +func (a *pkAlgorithm) UnmarshalJSON(b []byte) error {
224 + algoStr := strings.ToLower(strings.Trim(string(b), `"`))
225 + algo, ok := publicKeyAlgorithms[algoStr]
226 + if !ok {
227 + return fmt.Errorf("unrecognized public key algorithm: %s (expected one of %v)",
228 + algoStr, publicKeyAlgorithms)
229 + }
230 + a = &algo
231 + return nil
232 +}
233 +
234 // ConnectionMatcher is a type which matches TLS handshakes.
235 type ConnectionMatcher interface {
236 Match(*tls.ClientHelloInfo) bool
modules/caddytls/matchers.go
+1 -1
@@ -11,7 +11,7 @@ type MatchServerName []string
11
12 func init() {
13 caddy2.RegisterModule(caddy2.Module{
14 - Name: "tls.handshake_match.host",
14 + Name: "tls.handshake_match.sni",
15 New: func() interface{} { return MatchServerName{} },
16 })
17 }
modules/caddytls/tls.go
+8
@@ -2,6 +2,7 @@ package caddytls
2
3 import (
4 "crypto/tls"
5 + "crypto/x509"
6 "encoding/json"
7 "fmt"
8 "net/http"
@@ -316,4 +317,11 @@ var supportedProtocols = map[string]uint16{
317 "tls1.3": tls.VersionTLS13,
318 }
319
320 +// publicKeyAlgorithms is the map of supported public key algorithms.
321 +var publicKeyAlgorithms = map[string]pkAlgorithm{
322 + "rsa": pkAlgorithm(x509.RSA),
323 + "dsa": pkAlgorithm(x509.DSA),
324 + "ecdsa": pkAlgorithm(x509.ECDSA),
325 +}
326 +
327 const automateKey = "automate"