@marcel91200 / caddy Mirror · read-only / commits / 8a6ab2b7
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 10, 22:30 UTC

caddytls: require a trust pool for client cert verification modes (#8394)

* caddytls: require a trust pool for client cert verification modes Reject verify_if_given and require_and_verify without a trust pool so they stop falling back to system roots. * Apply suggestion from @francislavoie Signed-off-by: Francis Lavoie <lavofr@gmail.com> --------- Signed-off-by: Francis Lavoie <lavofr@gmail.com> Co-authored-by: Francis Lavoie <lavofr@gmail.com>

Faiyaz Rahman committed Nov 10, 2026 at 15:09 UTC 8a6ab2b7bb3c6b83ed8d8031d6457375629858d8
2 files changed +108 -5
modules/caddytls/connpolicy.go
+14 -5
@@ -653,9 +653,12 @@ type ClientAuthentication struct {
653 // `verify_if_given` | Ask clients for a certificate; allow even if there isn't one, but verify it if there is
654 // `require_and_verify` | Require clients to present a valid certificate that is verified
655 //
656 - // The default mode is `require_and_verify` if any
657 - // TrustedCACerts or TrustedCACertPEMFiles or TrustedLeafCerts
658 - // are provided; otherwise, the default mode is `require`.
656 + // The `verify_if_given` and `require_and_verify` modes need a
657 + // trust pool (`ca`). To verify against the system's root
658 + // certificates, use the `tls.ca_pool.source.system` module.
659 + //
660 + // The default mode is `require_and_verify` if a trust pool is
661 + // provided; otherwise, the default mode is `require`.
662 Mode string `json:"mode,omitempty"`
663
664 existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error
@@ -672,7 +675,8 @@ type ClientAuthentication struct {
675 // }
676 //
677 // If `mode` is not provided, it defaults to `require_and_verify` if `trust_pool` is provided.
675 -// Otherwise, it defaults to `require`.
678 +// Otherwise, it defaults to `require`. The `verify_if_given` and `require_and_verify` modes
679 +// need a `trust_pool`; use `trust_pool system` to verify against the system roots.
680 func (ca *ClientAuthentication) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
681 for d.NextArg() {
682 // consume any tokens on the same line, if any.
@@ -878,7 +882,6 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
882 // otherwise, set a safe default mode
883 if len(clientauth.TrustedCACerts) > 0 ||
884 len(clientauth.TrustedCACertPEMFiles) > 0 ||
881 - len(clientauth.TrustedLeafCerts) > 0 ||
885 clientauth.CARaw != nil || clientauth.ca != nil {
886 cfg.ClientAuth = tls.RequireAndVerifyClientCert
887 } else {
@@ -886,6 +889,12 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
889 }
890 }
891
892 + // without ClientCAs, Go verifies client certs against the system roots
893 + verifyMode := cfg.ClientAuth == tls.VerifyClientCertIfGiven || cfg.ClientAuth == tls.RequireAndVerifyClientCert
894 + if verifyMode && clientauth.ca == nil {
895 + return fmt.Errorf("client auth modes verify_if_given and require_and_verify need a trust pool; use the 'system' trust pool to verify against system roots")
896 + }
897 +
898 // enforce CA verification by adding CA certs to the ClientCAs pool
899 if clientauth.ca != nil {
900 cfg.ClientCAs = clientauth.ca.CertPool()
modules/caddytls/connpolicy_verifyconnection_test.go
+94
@@ -1,6 +1,7 @@
1 package caddytls
2
3 import (
4 + "context"
5 "crypto"
6 "crypto/ecdsa"
7 "crypto/elliptic"
@@ -8,12 +9,15 @@ import (
9 "crypto/tls"
10 "crypto/x509"
11 "crypto/x509/pkix"
12 + "encoding/json"
13 "errors"
14 "math/big"
15 "net"
16 "reflect"
17 "testing"
18 "time"
19 +
20 + "github.com/caddyserver/caddy/v2"
21 )
22
23 type testClientCertificateVerifier struct {
@@ -146,6 +150,96 @@ func TestClientAuthenticationModeWithLeafVerifier(t *testing.T) {
150 }
151 }
152
153 +func TestClientAuthenticationVerifyModesNeedTrustPool(t *testing.T) {
154 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
155 + defer cancel()
156 +
157 + // verifiers are loaded by the connection policy, so set both fields like a provisioned config
158 + verifiersRaw := []json.RawMessage{json.RawMessage(`{"verifier":"leaf"}`)}
159 + verifiers := []ClientCertificateVerifier{LeafCertClientAuth{trustedLeafCerts: []*x509.Certificate{issueTestCert(t, nil, 0).Leaf}}}
160 + customPool := json.RawMessage(`{"provider":"inline","trusted_ca_certs":["` + test_der_1 + `"]}`)
161 + systemPool := json.RawMessage(`{"provider":"system"}`)
162 +
163 + for i, tc := range []struct {
164 + clientauth ClientAuthentication
165 + want tls.ClientAuthType
166 + wantErr bool
167 + }{
168 + {clientauth: ClientAuthentication{Mode: "verify_if_given"}, wantErr: true},
169 + {clientauth: ClientAuthentication{Mode: "require_and_verify"}, wantErr: true},
170 + {clientauth: ClientAuthentication{Mode: "require_and_verify", VerifiersRaw: verifiersRaw, verifiers: verifiers}, wantErr: true},
171 + {clientauth: ClientAuthentication{Mode: "verify_if_given", CARaw: systemPool}, want: tls.VerifyClientCertIfGiven},
172 + {clientauth: ClientAuthentication{Mode: "require_and_verify", CARaw: systemPool}, want: tls.RequireAndVerifyClientCert},
173 + {clientauth: ClientAuthentication{Mode: "verify_if_given", CARaw: customPool}, want: tls.VerifyClientCertIfGiven},
174 + {clientauth: ClientAuthentication{Mode: "require_and_verify", CARaw: customPool}, want: tls.RequireAndVerifyClientCert},
175 + {clientauth: ClientAuthentication{CARaw: customPool}, want: tls.RequireAndVerifyClientCert},
176 + {clientauth: ClientAuthentication{Mode: "request"}, want: tls.RequestClientCert},
177 + {clientauth: ClientAuthentication{Mode: "require"}, want: tls.RequireAnyClientCert},
178 + {clientauth: ClientAuthentication{VerifiersRaw: verifiersRaw, verifiers: verifiers}, want: tls.RequireAnyClientCert},
179 + {clientauth: ClientAuthentication{TrustedLeafCerts: []string{test_der_1}}, want: tls.RequireAnyClientCert},
180 + } {
181 + if err := tc.clientauth.provision(ctx); err != nil {
182 + t.Fatalf("Test %d: provisioning: %v", i, err)
183 + }
184 + cfg := new(tls.Config)
185 + err := tc.clientauth.ConfigureTLSConfig(cfg)
186 + if tc.wantErr {
187 + if err == nil {
188 + t.Errorf("Test %d: expected error but got none", i)
189 + }
190 + continue
191 + }
192 + if err != nil {
193 + t.Errorf("Test %d: unexpected error: %v", i, err)
194 + continue
195 + }
196 + if cfg.ClientAuth != tc.want {
197 + t.Errorf("Test %d: expected %v, got %v", i, tc.want, cfg.ClientAuth)
198 + }
199 + if tc.clientauth.CARaw != nil && cfg.ClientCAs == nil {
200 + t.Errorf("Test %d: expected ClientCAs to be set", i)
201 + }
202 + }
203 +}
204 +
205 +func TestClientAuthenticationVerifierOnlyHandshake(t *testing.T) {
206 + ca := issueTestCert(t, nil, 0)
207 + serverCert := issueTestCert(t, &ca, x509.ExtKeyUsageServerAuth)
208 + trusted := issueTestCert(t, &ca, x509.ExtKeyUsageClientAuth)
209 + untrusted := issueTestCert(t, &ca, x509.ExtKeyUsageClientAuth)
210 +
211 + for _, tc := range []struct {
212 + cert *tls.Certificate
213 + wantError bool
214 + }{
215 + {cert: nil, wantError: true},
216 + {cert: &trusted, wantError: false},
217 + {cert: &untrusted, wantError: true},
218 + } {
219 + // no mode and no trust pool, so only the verifier checks the cert
220 + clientauth := &ClientAuthentication{
221 + VerifiersRaw: []json.RawMessage{json.RawMessage(`{"verifier":"leaf"}`)},
222 + verifiers: []ClientCertificateVerifier{LeafCertClientAuth{trustedLeafCerts: []*x509.Certificate{trusted.Leaf}}},
223 + }
224 + serverConfig := &tls.Config{Certificates: []tls.Certificate{serverCert}}
225 + if err := clientauth.ConfigureTLSConfig(serverConfig); err != nil {
226 + t.Fatalf("configuring TLS: %v", err)
227 + }
228 + clientConfig := &tls.Config{InsecureSkipVerify: true}
229 + if tc.cert != nil {
230 + clientConfig.Certificates = []tls.Certificate{*tc.cert}
231 + }
232 +
233 + err := serverHandshake(t, serverConfig, clientConfig)
234 + if tc.wantError && err == nil {
235 + t.Errorf("cert %v: expected handshake error but got none", tc.cert != nil)
236 + }
237 + if !tc.wantError && err != nil {
238 + t.Errorf("cert %v: unexpected handshake error: %v", tc.cert != nil, err)
239 + }
240 + }
241 +}
242 +
243 // serverHandshake runs one TLS handshake over loopback TCP and returns
244 // the server's handshake error.
245 func serverHandshake(t *testing.T, serverConfig, clientConfig *tls.Config) error {