@marcel91200 / caddy Mirror · read-only / commits / 50961ecc
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

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.