mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 21:53:48 +03:00
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
This commit is contained in:
parent
8e821b5039
commit
50961ecc77
3 changed files with 155 additions and 17 deletions
|
@ -75,6 +75,15 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||
srv.AutoHTTPS = new(AutoHTTPSConfig)
|
||||
}
|
||||
|
||||
// disallow TLS client auth bypass which could
|
||||
// otherwise be exploited by sending an unprotected
|
||||
// SNI value during TLS handshake, then a protected
|
||||
// Host header during HTTP request later on that
|
||||
// connection
|
||||
if srv.hasTLSClientAuth() {
|
||||
srv.StrictSNIHost = true
|
||||
}
|
||||
|
||||
// TODO: Test this function to ensure these replacements are performed
|
||||
for i := range srv.Listen {
|
||||
srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
|
||||
|
@ -159,8 +168,7 @@ func (app *App) Start() error {
|
|||
return fmt.Errorf("%s: listening on %s: %v", network, addr, err)
|
||||
}
|
||||
|
||||
// enable HTTP/2 (and support for solving the
|
||||
// TLS-ALPN ACME challenge) by default
|
||||
// enable HTTP/2 by default
|
||||
for _, pol := range srv.TLSConnPolicies {
|
||||
if len(pol.ALPN) == 0 {
|
||||
pol.ALPN = append(pol.ALPN, defaultALPN...)
|
||||
|
@ -294,11 +302,11 @@ func (app *App) automaticHTTPS() error {
|
|||
return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
|
||||
}
|
||||
|
||||
// tell the server to use TLS by specifying a TLS
|
||||
// connection policy (which supports HTTP/2 and the
|
||||
// TLS-ALPN ACME challenge as well)
|
||||
srv.TLSConnPolicies = caddytls.ConnectionPolicies{
|
||||
{ALPN: defaultALPN},
|
||||
// tell the server to use TLS if it is not already doing so
|
||||
if srv.TLSConnPolicies == nil {
|
||||
srv.TLSConnPolicies = caddytls.ConnectionPolicies{
|
||||
&caddytls.ConnectionPolicy{ALPN: defaultALPN},
|
||||
}
|
||||
}
|
||||
|
||||
if srv.AutoHTTPS.DisableRedir {
|
||||
|
|
|
@ -40,7 +40,7 @@ type Server struct {
|
|||
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"`
|
||||
AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
|
||||
MaxRehandles *int `json:"max_rehandles,omitempty"`
|
||||
StrictSNIHost bool `json:"strict_sni_host,omitempty"` // TODO: see if we can turn this on by default when clientauth is configured
|
||||
StrictSNIHost bool `json:"strict_sni_host,omitempty"`
|
||||
|
||||
tlsApp *caddytls.TLS
|
||||
}
|
||||
|
@ -181,6 +181,15 @@ func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (s *Server) hasTLSClientAuth() bool {
|
||||
for _, cp := range s.TLSConnPolicies {
|
||||
if cp.Active() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AutoHTTPSConfig is used to disable automatic HTTPS
|
||||
// or certain aspects of it for a specific server.
|
||||
type AutoHTTPSConfig struct {
|
||||
|
|
|
@ -17,6 +17,7 @@ package caddytls
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
@ -111,13 +112,12 @@ type ConnectionPolicy struct {
|
|||
Matchers map[string]json.RawMessage `json:"match,omitempty"`
|
||||
CertSelection json.RawMessage `json:"certificate_selection,omitempty"`
|
||||
|
||||
CipherSuites []string `json:"cipher_suites,omitempty"`
|
||||
Curves []string `json:"curves,omitempty"`
|
||||
ALPN []string `json:"alpn,omitempty"`
|
||||
ProtocolMin string `json:"protocol_min,omitempty"`
|
||||
ProtocolMax string `json:"protocol_max,omitempty"`
|
||||
|
||||
// TODO: Client auth
|
||||
CipherSuites []string `json:"cipher_suites,omitempty"`
|
||||
Curves []string `json:"curves,omitempty"`
|
||||
ALPN []string `json:"alpn,omitempty"`
|
||||
ProtocolMin string `json:"protocol_min,omitempty"`
|
||||
ProtocolMax string `json:"protocol_max,omitempty"`
|
||||
ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"`
|
||||
|
||||
matchers []ConnectionMatcher
|
||||
certSelector certmagic.CertificateSelector
|
||||
|
@ -167,7 +167,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
|||
tlsApp.SessionTickets.unregister(cfg)
|
||||
})
|
||||
|
||||
// TODO: Clean up active locks if app (or process) is being closed!
|
||||
// TODO: Clean up session ticket active locks in storage if app (or process) is being closed!
|
||||
|
||||
// add all the cipher suites in order, without duplicates
|
||||
cipherSuitesAdded := make(map[uint16]struct{})
|
||||
|
@ -212,7 +212,15 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
|||
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
|
||||
}
|
||||
|
||||
// TODO: client auth, and other fields
|
||||
// client authentication
|
||||
if p.ClientAuthentication != nil {
|
||||
err := p.ClientAuthentication.ConfigureTLSConfig(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuring TLS client authentication: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: other fields
|
||||
|
||||
setDefaultTLSParams(cfg)
|
||||
|
||||
|
@ -221,6 +229,119 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ClientAuthentication configures TLS client auth.
|
||||
type ClientAuthentication struct {
|
||||
// A list of base64 DER-encoded CA certificates
|
||||
// against which to validate client certificates.
|
||||
// Client certs which are not signed by any of
|
||||
// these CAs will be rejected.
|
||||
TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
|
||||
|
||||
// A list of base64 DER-encoded client leaf certs
|
||||
// to accept. If this list is not empty, client certs
|
||||
// which are not in this list will be rejected.
|
||||
TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"`
|
||||
|
||||
// state established with the last call to ConfigureTLSConfig
|
||||
trustedLeafCerts []*x509.Certificate
|
||||
existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error
|
||||
}
|
||||
|
||||
// Active returns true if clientauth has an actionable configuration.
|
||||
func (clientauth ClientAuthentication) Active() bool {
|
||||
return len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedLeafCerts) > 0
|
||||
}
|
||||
|
||||
// ConfigureTLSConfig sets up cfg to enforce clientauth's configuration.
|
||||
func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) error {
|
||||
// if there's no actionable client auth, simply disable it
|
||||
if !clientauth.Active() {
|
||||
cfg.ClientAuth = tls.NoClientCert
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, at least require any client certificate
|
||||
cfg.ClientAuth = tls.RequireAnyClientCert
|
||||
|
||||
// enforce CA verification by adding CA certs to the ClientCAs pool
|
||||
if len(clientauth.TrustedCACerts) > 0 {
|
||||
caPool := x509.NewCertPool()
|
||||
for _, clientCAString := range clientauth.TrustedCACerts {
|
||||
clientCA, err := decodeBase64DERCert(clientCAString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing certificate: %v", err)
|
||||
}
|
||||
caPool.AddCert(clientCA)
|
||||
}
|
||||
cfg.ClientCAs = caPool
|
||||
|
||||
// now ensure the standard lib will verify client certificates
|
||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
// enforce leaf verification by writing our own verify function
|
||||
if len(clientauth.TrustedLeafCerts) > 0 {
|
||||
clientauth.trustedLeafCerts = []*x509.Certificate{}
|
||||
|
||||
for _, clientCertString := range clientauth.TrustedLeafCerts {
|
||||
clientCert, err := decodeBase64DERCert(clientCertString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing certificate: %v", err)
|
||||
}
|
||||
clientauth.trustedLeafCerts = append(clientauth.trustedLeafCerts, clientCert)
|
||||
}
|
||||
|
||||
// if a custom verification function already exists, wrap it
|
||||
clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
|
||||
|
||||
cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate
|
||||
// callback to do custom client certificate verification. It is intended
|
||||
// for installation only by clientauth.ConfigureTLSConfig().
|
||||
func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
// first use any pre-existing custom verification function
|
||||
if clientauth.existingVerifyPeerCert != nil {
|
||||
err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(rawCerts) == 0 {
|
||||
return fmt.Errorf("no client certificate provided")
|
||||
}
|
||||
|
||||
remoteLeafCert, err := x509.ParseCertificate(rawCerts[len(rawCerts)-1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't parse the given certificate: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, trustedLeafCert := range clientauth.trustedLeafCerts {
|
||||
if remoteLeafCert.Equal(trustedLeafCert) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("client leaf certificate failed validation")
|
||||
}
|
||||
|
||||
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
|
||||
func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
|
||||
// decode base64
|
||||
derBytes, err := base64.StdEncoding.DecodeString(certStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse the DER-encoded certificate
|
||||
return x509.ParseCertificate(derBytes)
|
||||
}
|
||||
|
||||
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
|
||||
// and server preferences of cfg if they are not already set; it does not
|
||||
// overwrite values, only fills in missing values.
|
||||
|
|
Loading…
Reference in a new issue