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 {