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
Initial implementation of TLS client authentication (#2731)
* Add support for client TLS authentication Signed-off-by: Alexandre Stein <alexandre_stein@interlab-net.com> * make and use client authentication struct * force StrictSNIHost if TLSConnPolicies is not empty * Implement leafs verification * Fixes issue when using multiple verification * applies the comments from maintainers * Apply comment * Refactor/cleanup initial TLS client auth implementation
Alexandre Stein committed
Sep 3, 2019 at 17:35 UTC
50961ecc77eb7e6a33f823da1d9eea6554a14b43
3 files changed
+155
-17
modules/caddyhttp/caddyhttp.go
+15
-7
@@ -75,6 +75,15 @@ func (app *App) Provision(ctx caddy.Context) error {
75
srv.AutoHTTPS = new(AutoHTTPSConfig)
76
}
77
78
+ // disallow TLS client auth bypass which could
79
+ // otherwise be exploited by sending an unprotected
80
+ // SNI value during TLS handshake, then a protected
81
+ // Host header during HTTP request later on that
82
+ // connection
83
+ if srv.hasTLSClientAuth() {
84
+ srv.StrictSNIHost = true
85
+ }
86
+
87
// TODO: Test this function to ensure these replacements are performed
88
for i := range srv.Listen {
89
srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
@@ -159,8 +168,7 @@ func (app *App) Start() error {
168
return fmt.Errorf("%s: listening on %s: %v", network, addr, err)
169
}
170
162
- // enable HTTP/2 (and support for solving the
163
- // TLS-ALPN ACME challenge) by default
171
+ // enable HTTP/2 by default
172
for _, pol := range srv.TLSConnPolicies {
173
if len(pol.ALPN) == 0 {
174
pol.ALPN = append(pol.ALPN, defaultALPN...)
@@ -294,11 +302,11 @@ func (app *App) automaticHTTPS() error {
302
return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
303
}
304
297
- // tell the server to use TLS by specifying a TLS
298
- // connection policy (which supports HTTP/2 and the
299
- // TLS-ALPN ACME challenge as well)
300
- srv.TLSConnPolicies = caddytls.ConnectionPolicies{
301
- {ALPN: defaultALPN},
305
+ // tell the server to use TLS if it is not already doing so
306
+ if srv.TLSConnPolicies == nil {
307
+ srv.TLSConnPolicies = caddytls.ConnectionPolicies{
308
+ &caddytls.ConnectionPolicy{ALPN: defaultALPN},
309
+ }
310
}
311
312
if srv.AutoHTTPS.DisableRedir {
modules/caddyhttp/server.go
+10
-1
@@ -40,7 +40,7 @@ type Server struct {
40
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"`
41
AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
42
MaxRehandles *int `json:"max_rehandles,omitempty"`
43
- StrictSNIHost bool `json:"strict_sni_host,omitempty"` // TODO: see if we can turn this on by default when clientauth is configured
43
+ StrictSNIHost bool `json:"strict_sni_host,omitempty"`
44
45
tlsApp *caddytls.TLS
46
}
@@ -181,6 +181,15 @@ func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
181
return false
182
}
183
184
+func (s *Server) hasTLSClientAuth() bool {
185
+ for _, cp := range s.TLSConnPolicies {
186
+ if cp.Active() {
187
+ return true
188
+ }
189
+ }
190
+ return false
191
+}
192
+
193
// AutoHTTPSConfig is used to disable automatic HTTPS
194
// or certain aspects of it for a specific server.
195
type AutoHTTPSConfig struct {
modules/caddytls/connpolicy.go
+130
-9
@@ -17,6 +17,7 @@ package caddytls
17
import (
18
"crypto/tls"
19
"crypto/x509"
20
+ "encoding/base64"
21
"encoding/json"
22
"fmt"
23
"strings"
@@ -111,13 +112,12 @@ type ConnectionPolicy struct {
112
Matchers map[string]json.RawMessage `json:"match,omitempty"`
113
CertSelection json.RawMessage `json:"certificate_selection,omitempty"`
114
114
- CipherSuites []string `json:"cipher_suites,omitempty"`
115
- Curves []string `json:"curves,omitempty"`
116
- ALPN []string `json:"alpn,omitempty"`
117
- ProtocolMin string `json:"protocol_min,omitempty"`
118
- ProtocolMax string `json:"protocol_max,omitempty"`
119
-
120
- // TODO: Client auth
115
+ CipherSuites []string `json:"cipher_suites,omitempty"`
116
+ Curves []string `json:"curves,omitempty"`
117
+ ALPN []string `json:"alpn,omitempty"`
118
+ ProtocolMin string `json:"protocol_min,omitempty"`
119
+ ProtocolMax string `json:"protocol_max,omitempty"`
120
+ ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"`
121
122
matchers []ConnectionMatcher
123
certSelector certmagic.CertificateSelector
@@ -167,7 +167,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
167
tlsApp.SessionTickets.unregister(cfg)
168
})
169
170
- // TODO: Clean up active locks if app (or process) is being closed!
170
+ // TODO: Clean up session ticket active locks in storage if app (or process) is being closed!
171
172
// add all the cipher suites in order, without duplicates
173
cipherSuitesAdded := make(map[uint16]struct{})
@@ -212,7 +212,15 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
212
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
213
}
214
215
- // TODO: client auth, and other fields
215
+ // client authentication
216
+ if p.ClientAuthentication != nil {
217
+ err := p.ClientAuthentication.ConfigureTLSConfig(cfg)
218
+ if err != nil {
219
+ return fmt.Errorf("configuring TLS client authentication: %v", err)
220
+ }
221
+ }
222
+
223
+ // TODO: other fields
224
225
setDefaultTLSParams(cfg)
226
@@ -221,6 +229,119 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
229
return nil
230
}
231
232
+// ClientAuthentication configures TLS client auth.
233
+type ClientAuthentication struct {
234
+ // A list of base64 DER-encoded CA certificates
235
+ // against which to validate client certificates.
236
+ // Client certs which are not signed by any of
237
+ // these CAs will be rejected.
238
+ TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
239
+
240
+ // A list of base64 DER-encoded client leaf certs
241
+ // to accept. If this list is not empty, client certs
242
+ // which are not in this list will be rejected.
243
+ TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"`
244
+
245
+ // state established with the last call to ConfigureTLSConfig
246
+ trustedLeafCerts []*x509.Certificate
247
+ existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error
248
+}
249
+
250
+// Active returns true if clientauth has an actionable configuration.
251
+func (clientauth ClientAuthentication) Active() bool {
252
+ return len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedLeafCerts) > 0
253
+}
254
+
255
+// ConfigureTLSConfig sets up cfg to enforce clientauth's configuration.
256
+func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) error {
257
+ // if there's no actionable client auth, simply disable it
258
+ if !clientauth.Active() {
259
+ cfg.ClientAuth = tls.NoClientCert
260
+ return nil
261
+ }
262
+
263
+ // otherwise, at least require any client certificate
264
+ cfg.ClientAuth = tls.RequireAnyClientCert
265
+
266
+ // enforce CA verification by adding CA certs to the ClientCAs pool
267
+ if len(clientauth.TrustedCACerts) > 0 {
268
+ caPool := x509.NewCertPool()
269
+ for _, clientCAString := range clientauth.TrustedCACerts {
270
+ clientCA, err := decodeBase64DERCert(clientCAString)
271
+ if err != nil {
272
+ return fmt.Errorf("parsing certificate: %v", err)
273
+ }
274
+ caPool.AddCert(clientCA)
275
+ }
276
+ cfg.ClientCAs = caPool
277
+
278
+ // now ensure the standard lib will verify client certificates
279
+ cfg.ClientAuth = tls.RequireAndVerifyClientCert
280
+ }
281
+
282
+ // enforce leaf verification by writing our own verify function
283
+ if len(clientauth.TrustedLeafCerts) > 0 {
284
+ clientauth.trustedLeafCerts = []*x509.Certificate{}
285
+
286
+ for _, clientCertString := range clientauth.TrustedLeafCerts {
287
+ clientCert, err := decodeBase64DERCert(clientCertString)
288
+ if err != nil {
289
+ return fmt.Errorf("parsing certificate: %v", err)
290
+ }
291
+ clientauth.trustedLeafCerts = append(clientauth.trustedLeafCerts, clientCert)
292
+ }
293
+
294
+ // if a custom verification function already exists, wrap it
295
+ clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
296
+
297
+ cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
298
+ }
299
+
300
+ return nil
301
+}
302
+
303
+// verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate
304
+// callback to do custom client certificate verification. It is intended
305
+// for installation only by clientauth.ConfigureTLSConfig().
306
+func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
307
+ // first use any pre-existing custom verification function
308
+ if clientauth.existingVerifyPeerCert != nil {
309
+ err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains)
310
+ if err != nil {
311
+ return err
312
+ }
313
+ }
314
+
315
+ if len(rawCerts) == 0 {
316
+ return fmt.Errorf("no client certificate provided")
317
+ }
318
+
319
+ remoteLeafCert, err := x509.ParseCertificate(rawCerts[len(rawCerts)-1])
320
+ if err != nil {
321
+ return fmt.Errorf("can't parse the given certificate: %s", err.Error())
322
+ }
323
+
324
+ for _, trustedLeafCert := range clientauth.trustedLeafCerts {
325
+ if remoteLeafCert.Equal(trustedLeafCert) {
326
+ return nil
327
+ }
328
+ }
329
+
330
+ return fmt.Errorf("client leaf certificate failed validation")
331
+}
332
+
333
+// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
334
+func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
335
+ // decode base64
336
+ derBytes, err := base64.StdEncoding.DecodeString(certStr)
337
+ if err != nil {
338
+ return nil, err
339
+ }
340
+
341
+ // parse the DER-encoded certificate
342
+ return x509.ParseCertificate(derBytes)
343
+}
344
+
345
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
346
// and server preferences of cfg if they are not already set; it does not
347
// overwrite values, only fills in missing values.