caddytls: Make peer certificate verification pluggable (#4389)

* caddytls: Adding ClientCertValidator for custom client cert validations

* caddytls: Cleanups for ClientCertValidator changes

caddytls: Cleanups for ClientCertValidator changes

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* Unexported field Validators, corrected renaming of LeafVerificationValidator to LeafCertClientAuth

* admin: Write proper status on invalid requests (#4569) (fix #4561)

* Apply suggestions from code review

* Register module; fix compilation

* Add log for deprecation notice

Co-authored-by: Roettges Florian <roettges.florian@scheidt-bachmann.de>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Alok Naushad <alokme123@gmail.com>
This commit is contained in:
Gr33nbl00d 2022-06-02 22:25:07 +02:00 committed by GitHub
parent 9864b138fb
commit 0a14f97e49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -18,6 +18,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -26,6 +27,10 @@ import (
"github.com/mholt/acmez" "github.com/mholt/acmez"
) )
func init() {
caddy.RegisterModule(LeafCertClientAuth{})
}
// ConnectionPolicies govern the establishment of TLS connections. It is // ConnectionPolicies govern the establishment of TLS connections. It is
// an ordered group of connection policies; the first matching policy will // an ordered group of connection policies; the first matching policy will
// be used to configure TLS connections at handshake-time. // be used to configure TLS connections at handshake-time.
@ -55,6 +60,16 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err)
} }
if pol.ClientAuthentication != nil && len(pol.ClientAuthentication.VerifiersRaw) > 0 {
clientCertValidations, err := ctx.LoadModule(pol.ClientAuthentication, "VerifiersRaw")
if err != nil {
return fmt.Errorf("loading client cert verifiers: %v", err)
}
for _, validator := range clientCertValidations.([]interface{}) {
cp[i].ClientAuthentication.verifiers = append(cp[i].ClientAuthentication.verifiers, validator.(ClientCertificateVerifier))
}
}
} }
return nil return nil
@ -62,7 +77,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
// TLSConfig returns a standard-lib-compatible TLS configuration which // TLSConfig returns a standard-lib-compatible TLS configuration which
// selects the first matching policy based on the ClientHello. // selects the first matching policy based on the ClientHello.
func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config {
// using ServerName to match policies is extremely common, especially in configs // using ServerName to match policies is extremely common, especially in configs
// with lots and lots of different policies; we can fast-track those by indexing // with lots and lots of different policies; we can fast-track those by indexing
// them by SNI, so we don't have to iterate potentially thousands of policies // them by SNI, so we don't have to iterate potentially thousands of policies
@ -293,11 +308,22 @@ type ClientAuthentication struct {
// these CA certificates will be rejected. // these CA certificates will be rejected.
TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"` TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"`
// DEPRECATED: This field is deprecated and will be removed in
// a future version. Please use the `validators` field instead
// with the tls.client_auth.leaf module instead.
//
// A list of base64 DER-encoded client leaf certs // A list of base64 DER-encoded client leaf certs
// to accept. If this list is not empty, client certs // to accept. If this list is not empty, client certs
// which are not in this list will be rejected. // which are not in this list will be rejected.
TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"` TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"`
// Client certificate verification modules. These can perform
// custom client authentication checks, such as ensuring the
// certificate is not revoked.
VerifiersRaw []json.RawMessage `json:"verifiers,omitempty" caddy:"namespace=tls.client_auth inline_key=verifier"`
verifiers []ClientCertificateVerifier
// The mode for authenticating the client. Allowed values are: // The mode for authenticating the client. Allowed values are:
// //
// Mode | Description // Mode | Description
@ -312,8 +338,6 @@ type ClientAuthentication struct {
// are provided; otherwise, the default mode is `require`. // are provided; otherwise, the default mode is `require`.
Mode string `json:"mode,omitempty"` Mode string `json:"mode,omitempty"`
// state established with the last call to ConfigureTLSConfig
trustedLeafCerts []*x509.Certificate
existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error
} }
@ -321,7 +345,8 @@ type ClientAuthentication struct {
func (clientauth ClientAuthentication) Active() bool { func (clientauth ClientAuthentication) Active() bool {
return len(clientauth.TrustedCACerts) > 0 || return len(clientauth.TrustedCACerts) > 0 ||
len(clientauth.TrustedCACertPEMFiles) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 ||
len(clientauth.TrustedLeafCerts) > 0 || len(clientauth.TrustedLeafCerts) > 0 || // TODO: DEPRECATED
len(clientauth.VerifiersRaw) > 0 ||
len(clientauth.Mode) > 0 len(clientauth.Mode) > 0
} }
@ -378,28 +403,31 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
cfg.ClientCAs = caPool cfg.ClientCAs = caPool
} }
// enforce leaf verification by writing our own verify function // TODO: DEPRECATED: Only here for backwards compatibility.
// If leaf cert is specified, enforce by adding a client auth module
if len(clientauth.TrustedLeafCerts) > 0 { if len(clientauth.TrustedLeafCerts) > 0 {
clientauth.trustedLeafCerts = []*x509.Certificate{} caddy.Log().Named("tls.connection_policy").Warn("trusted_leaf_certs is deprecated; use leaf verifier module instead")
var trustedLeafCerts []*x509.Certificate
for _, clientCertString := range clientauth.TrustedLeafCerts { for _, clientCertString := range clientauth.TrustedLeafCerts {
clientCert, err := decodeBase64DERCert(clientCertString) clientCert, err := decodeBase64DERCert(clientCertString)
if err != nil { if err != nil {
return fmt.Errorf("parsing certificate: %v", err) return fmt.Errorf("parsing certificate: %v", err)
} }
clientauth.trustedLeafCerts = append(clientauth.trustedLeafCerts, clientCert) trustedLeafCerts = append(trustedLeafCerts, clientCert)
} }
// if a custom verification function already exists, wrap it clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{TrustedLeafCerts: trustedLeafCerts})
clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
} }
// if a custom verification function already exists, wrap it
clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
return nil return nil
} }
// verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate // verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate
// callback to do custom client certificate verification. It is intended // callback to do custom client certificate verification. It is intended
// for installation only by clientauth.ConfigureTLSConfig(). // for installation only by clientauth.ConfigureTLSConfig().
func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { func (clientauth *ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// first use any pre-existing custom verification function // first use any pre-existing custom verification function
if clientauth.existingVerifyPeerCert != nil { if clientauth.existingVerifyPeerCert != nil {
err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains) err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains)
@ -407,23 +435,13 @@ func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte,
return err return err
} }
} }
for _, verifier := range clientauth.verifiers {
if len(rawCerts) == 0 { err := verifier.VerifyClientCertificate(rawCerts, verifiedChains)
return fmt.Errorf("no client certificate provided") if err != nil {
} return err
remoteLeafCert, err := x509.ParseCertificate(rawCerts[0])
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 nil
return fmt.Errorf("client leaf certificate failed validation")
} }
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr. // decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
@ -461,6 +479,38 @@ func setDefaultTLSParams(cfg *tls.Config) {
cfg.PreferServerCipherSuites = true cfg.PreferServerCipherSuites = true
} }
// LeafCertClientAuth verifies the client's leaf certificate.
type LeafCertClientAuth struct {
TrustedLeafCerts []*x509.Certificate
}
// CaddyModule returns the Caddy module information.
func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.client_auth.leaf",
New: func() caddy.Module { return new(LeafCertClientAuth) },
}
}
func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no client certificate provided")
}
remoteLeafCert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("can't parse the given certificate: %s", err.Error())
}
for _, trustedLeafCert := range l.TrustedLeafCerts {
if remoteLeafCert.Equal(trustedLeafCert) {
return nil
}
}
return fmt.Errorf("client leaf certificate failed validation")
}
// PublicKeyAlgorithm is a JSON-unmarshalable wrapper type. // PublicKeyAlgorithm is a JSON-unmarshalable wrapper type.
type PublicKeyAlgorithm x509.PublicKeyAlgorithm type PublicKeyAlgorithm x509.PublicKeyAlgorithm
@ -481,4 +531,10 @@ type ConnectionMatcher interface {
Match(*tls.ClientHelloInfo) bool Match(*tls.ClientHelloInfo) bool
} }
// ClientCertificateVerifier is a type which verifies client certificates.
// It is called during verifyPeerCertificate in the TLS handshake.
type ClientCertificateVerifier interface {
VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
}
var defaultALPN = []string{"h2", "http/1.1"} var defaultALPN = []string{"h2", "http/1.1"}