From 0a14f97e4944a1aadc5fa9e6e8569759a9f0b5ec Mon Sep 17 00:00:00 2001 From: Gr33nbl00d Date: Thu, 2 Jun 2022 22:25:07 +0200 Subject: [PATCH] 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 * Update modules/caddytls/connpolicy.go Co-authored-by: Francis Lavoie * Update modules/caddytls/connpolicy.go Co-authored-by: Francis Lavoie * Update modules/caddytls/connpolicy.go Co-authored-by: Francis Lavoie * Update modules/caddytls/connpolicy.go Co-authored-by: Matt Holt * Update modules/caddytls/connpolicy.go Co-authored-by: Matt Holt * 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 Co-authored-by: Francis Lavoie Co-authored-by: Matt Holt Co-authored-by: Alok Naushad --- modules/caddytls/connpolicy.go | 108 +++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 65cda5fb873..d6304a999bf 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -18,6 +18,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/json" "fmt" "os" "strings" @@ -26,6 +27,10 @@ import ( "github.com/mholt/acmez" ) +func init() { + caddy.RegisterModule(LeafCertClientAuth{}) +} + // ConnectionPolicies govern the establishment of TLS connections. It is // an ordered group of connection policies; the first matching policy will // be used to configure TLS connections at handshake-time. @@ -55,6 +60,16 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { if err != nil { 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 @@ -62,7 +77,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { // TLSConfig returns a standard-lib-compatible TLS configuration which // 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 // 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 @@ -293,11 +308,22 @@ type ClientAuthentication struct { // these CA certificates will be rejected. 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 // 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"` + // 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: // // Mode | Description @@ -312,8 +338,6 @@ type ClientAuthentication struct { // are provided; otherwise, the default mode is `require`. Mode string `json:"mode,omitempty"` - // state established with the last call to ConfigureTLSConfig - trustedLeafCerts []*x509.Certificate existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error } @@ -321,7 +345,8 @@ type ClientAuthentication struct { func (clientauth ClientAuthentication) Active() bool { return len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 || - len(clientauth.TrustedLeafCerts) > 0 || + len(clientauth.TrustedLeafCerts) > 0 || // TODO: DEPRECATED + len(clientauth.VerifiersRaw) > 0 || len(clientauth.Mode) > 0 } @@ -378,28 +403,31 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro 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 { - 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 { clientCert, err := decodeBase64DERCert(clientCertString) if err != nil { 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.existingVerifyPeerCert = cfg.VerifyPeerCertificate - cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate + clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{TrustedLeafCerts: trustedLeafCerts}) } + // 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 { +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) @@ -407,23 +435,13 @@ func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, return err } } - - 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 clientauth.trustedLeafCerts { - if remoteLeafCert.Equal(trustedLeafCert) { - return nil + for _, verifier := range clientauth.verifiers { + err := verifier.VerifyClientCertificate(rawCerts, verifiedChains) + if err != nil { + return err } } - - return fmt.Errorf("client leaf certificate failed validation") + return nil } // decodeBase64DERCert base64-decodes, then DER-decodes, certStr. @@ -461,6 +479,38 @@ func setDefaultTLSParams(cfg *tls.Config) { 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. type PublicKeyAlgorithm x509.PublicKeyAlgorithm @@ -481,4 +531,10 @@ type ConnectionMatcher interface { 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"}