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"