@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 1,178 lines 40.4 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddytls
16
17 import (
18 "context"
19 "crypto/tls"
20 "crypto/x509"
21 "encoding/base64"
22 "encoding/json"
23 "encoding/pem"
24 "fmt"
25 "io"
26 "os"
27 "reflect"
28 "slices"
29 "strings"
30
31 "github.com/mholt/acmez/v3"
32 "go.uber.org/zap"
33 "go.uber.org/zap/zapcore"
34
35 "github.com/caddyserver/caddy/v2"
36 "github.com/caddyserver/caddy/v2/caddyconfig"
37 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
38 )
39
40 func init() {
41 caddy.RegisterModule(LeafCertClientAuth{})
42 }
43
44 // ConnectionPolicies govern the establishment of TLS connections. It is
45 // an ordered group of connection policies; the first matching policy will
46 // be used to configure TLS connections at handshake-time.
47 type ConnectionPolicies []*ConnectionPolicy
48
49 // Provision sets up each connection policy. It should be called
50 // during the Validate() phase, after the TLS app (if any) is
51 // already set up.
52 func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
53 for i, pol := range cp {
54 // matchers
55 mods, err := ctx.LoadModule(pol, "MatchersRaw")
56 if err != nil {
57 return fmt.Errorf("loading handshake matchers: %v", err)
58 }
59 for _, modIface := range mods.(map[string]any) {
60 cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher))
61 }
62
63 // enable HTTP/2 by default
64 if pol.ALPN == nil {
65 pol.ALPN = append(pol.ALPN, defaultALPN...)
66 }
67
68 // pre-build standard TLS config so we don't have to at handshake-time
69 err = pol.buildStandardTLSConfig(ctx)
70 if err != nil {
71 return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err)
72 }
73
74 if pol.ClientAuthentication != nil && len(pol.ClientAuthentication.VerifiersRaw) > 0 {
75 clientCertValidations, err := ctx.LoadModule(pol.ClientAuthentication, "VerifiersRaw")
76 if err != nil {
77 return fmt.Errorf("loading client cert verifiers: %v", err)
78 }
79 for _, validator := range clientCertValidations.([]any) {
80 cp[i].ClientAuthentication.verifiers = append(cp[i].ClientAuthentication.verifiers, validator.(ClientCertificateVerifier))
81 }
82 }
83
84 if len(pol.HandshakeContextRaw) > 0 {
85 modIface, err := ctx.LoadModule(pol, "HandshakeContextRaw")
86 if err != nil {
87 return fmt.Errorf("loading handshake context module: %v", err)
88 }
89 cp[i].handshakeContext = modIface.(HandshakeContext)
90 }
91 }
92
93 return nil
94 }
95
96 // TLSConfig returns a standard-lib-compatible TLS configuration which
97 // selects the first matching policy based on the ClientHello.
98 func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
99 // using ServerName to match policies is extremely common, especially in configs
100 // with lots and lots of different policies; we can fast-track those by indexing
101 // them by SNI, so we don't have to iterate potentially thousands of policies
102 // (TODO: this map does not account for wildcards, see if this is a problem in practice? look for reports of high connection latency with wildcard certs but low latency for non-wildcards in multi-thousand-cert deployments)
103 indexedBySNI := make(map[string]ConnectionPolicies)
104 if len(cp) > 30 {
105 for _, p := range cp {
106 for _, m := range p.matchers {
107 if sni, ok := m.(MatchServerName); ok {
108 for _, sniName := range sni {
109 // index for fast lookups during handshakes
110 indexName := asciiServerNameForMatch(sniName)
111 indexedBySNI[indexName] = append(indexedBySNI[indexName], p)
112 }
113 }
114 }
115 }
116 }
117
118 getConfigForClient := func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
119 // filter policies by SNI first, if possible, to speed things up
120 // when there may be lots of policies
121 possiblePolicies := cp
122 if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok {
123 possiblePolicies = indexedPolicies
124 }
125
126 policyLoop:
127 for _, pol := range possiblePolicies {
128 for _, matcher := range pol.matchers {
129 if !matcher.Match(hello) {
130 continue policyLoop
131 }
132 }
133 if pol.Drop {
134 return nil, fmt.Errorf("dropping connection")
135 }
136 return pol.TLSConfig, nil
137 }
138
139 return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello)
140 }
141
142 tlsCfg := &tls.Config{
143 MinVersion: tls.VersionTLS12,
144 GetConfigForClient: getConfigForClient,
145 }
146
147 // enable ECH, if configured
148 if tlsAppIface, err := ctx.AppIfConfigured("tls"); err == nil {
149 tlsApp := tlsAppIface.(*TLS)
150
151 if tlsApp.EncryptedClientHello != nil && len(tlsApp.EncryptedClientHello.configs) > 0 {
152 // if no publication was configured, we apply ECH to all server names by default,
153 // but the TLS app needs to know what they are in this case, since they don't appear
154 // in its config (remember, TLS connection policies are used by *other* apps to
155 // run TLS servers) -- we skip names with placeholders
156 if tlsApp.EncryptedClientHello.Publication == nil {
157 repl := caddy.NewReplacer()
158 for _, p := range cp {
159 var echNames []string
160 for _, m := range p.matchers {
161 if sni, ok := m.(MatchServerName); ok {
162 for _, name := range sni {
163 finalName := strings.ToLower(repl.ReplaceAll(name, ""))
164 echNames = append(echNames, finalName)
165 }
166 }
167 }
168 tlsApp.RegisterServerNames(echNames, p.ALPN)
169 }
170 }
171
172 getECHKeys := func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
173 tlsApp.EncryptedClientHello.configsMu.RLock()
174 defer tlsApp.EncryptedClientHello.configsMu.RUnlock()
175 return tlsApp.EncryptedClientHello.stdlibReady, nil
176 }
177
178 // crypto/tls decrypts the ClientHello with the keys from this config,
179 // but builds the retry configs it sends when ECH is rejected from the
180 // config returned by GetConfigForClient, so both need the keys.
181 tlsCfg.GetEncryptedClientHelloKeys = getECHKeys
182 for _, p := range cp {
183 if p.TLSConfig != nil && p.TLSConfig.GetEncryptedClientHelloKeys == nil {
184 p.TLSConfig.GetEncryptedClientHelloKeys = getECHKeys
185 }
186 }
187 }
188 }
189
190 return tlsCfg
191 }
192
193 // ConnectionPolicy specifies the logic for handling a TLS handshake.
194 // An empty policy is valid; safe and sensible defaults will be used.
195 type ConnectionPolicy struct {
196 // How to match this policy with a TLS ClientHello. If
197 // this policy is the first to match, it will be used.
198 MatchersRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=tls.handshake_match"`
199 matchers []ConnectionMatcher
200
201 // How to choose a certificate if more than one matched
202 // the given ServerName (SNI) value.
203 CertSelection *CustomCertSelectionPolicy `json:"certificate_selection,omitempty"`
204
205 // The list of cipher suites to support. Caddy's
206 // defaults are modern and secure.
207 CipherSuites []string `json:"cipher_suites,omitempty"`
208
209 // The list of elliptic curves to support. Caddy's
210 // defaults are modern and secure.
211 Curves []string `json:"curves,omitempty"`
212
213 // Protocols to use for Application-Layer Protocol
214 // Negotiation (ALPN) during the handshake.
215 ALPN []string `json:"alpn,omitempty"`
216
217 // Minimum TLS protocol version to allow. Default: `tls1.2`
218 ProtocolMin string `json:"protocol_min,omitempty"`
219
220 // Maximum TLS protocol version to allow. Default: `tls1.3`
221 ProtocolMax string `json:"protocol_max,omitempty"`
222
223 // Reject TLS connections. EXPERIMENTAL: May change.
224 Drop bool `json:"drop,omitempty"`
225
226 // Enables and configures TLS client authentication.
227 ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"`
228
229 // DefaultSNI becomes the ServerName in a ClientHello if there
230 // is no policy configured for the empty SNI value.
231 DefaultSNI string `json:"default_sni,omitempty"`
232
233 // FallbackSNI becomes the ServerName in a ClientHello if
234 // the original ServerName doesn't match any certificates
235 // in the cache. The use cases for this are very niche;
236 // typically if a client is a CDN and passes through the
237 // ServerName of the downstream handshake but can accept
238 // a certificate with the origin's hostname instead, then
239 // you would set this to your origin's hostname. Note that
240 // Caddy must be managing a certificate for this name.
241 //
242 // This feature is EXPERIMENTAL and subject to change or removal.
243 FallbackSNI string `json:"fallback_sni,omitempty"`
244
245 // Also known as "SSLKEYLOGFILE", TLS secrets will be written to
246 // this file in NSS key log format which can then be parsed by
247 // Wireshark and other tools. This is INSECURE as it allows other
248 // programs or tools to decrypt TLS connections. However, this
249 // capability can be useful for debugging and troubleshooting.
250 // **ENABLING THIS LOG COMPROMISES SECURITY!**
251 //
252 // This feature is EXPERIMENTAL and subject to change or removal.
253 InsecureSecretsLog string `json:"insecure_secrets_log,omitempty"`
254
255 // A module that can manipulate the context passed into CertMagic's
256 // certificate management functions during TLS handshakes.
257 // EXPERIMENTAL - subject to change or removal.
258 HandshakeContextRaw json.RawMessage `json:"handshake_context,omitempty" caddy:"namespace=tls.context inline_key=module"`
259 handshakeContext HandshakeContext
260
261 // TLSConfig is the fully-formed, standard lib TLS config
262 // used to serve TLS connections. Provision all
263 // ConnectionPolicies to populate this. It is exported only
264 // so it can be minimally adjusted after provisioning
265 // if necessary (like to adjust NextProtos to disable HTTP/2),
266 // and may be unexported in the future.
267 TLSConfig *tls.Config `json:"-"`
268 }
269
270 type HandshakeContext interface {
271 // HandshakeContext returns a context to pass into CertMagic's
272 // GetCertificate function used to serve, load, and manage certs
273 // during TLS handshakes. Generally you'll start with the context
274 // from the ClientHelloInfo, but you may use other information
275 // from it as well. Return an error to abort the handshake.
276 HandshakeContext(*tls.ClientHelloInfo) (context.Context, error)
277 }
278
279 func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
280 tlsAppIface, err := ctx.App("tls")
281 if err != nil {
282 return fmt.Errorf("getting tls app: %v", err)
283 }
284 tlsApp := tlsAppIface.(*TLS)
285
286 // fill in some "easy" default values, but for other values
287 // (such as slices), we should ensure that they start empty
288 // so the user-provided config can fill them in; then we will
289 // fill in a default config at the end if they are still unset
290 cfg := &tls.Config{
291 NextProtos: p.ALPN,
292 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
293 // TODO: I don't love how this works: we pre-build certmagic configs
294 // so that handshakes are faster. Unfortunately, certmagic configs are
295 // comprised of settings from both a TLS connection policy and a TLS
296 // automation policy. The only two fields (as of March 2020; v2 beta 17)
297 // of a certmagic config that come from the TLS connection policy are
298 // CertSelection and DefaultServerName, so an automation policy is what
299 // builds the base certmagic config. Since the pre-built config is
300 // shared, I don't think we can change any of its fields per-handshake,
301 // hence the awkward shallow copy (dereference) here and the subsequent
302 // changing of some of its fields. I'm worried this dereference allocates
303 // more at handshake-time, but I don't know how to practically pre-build
304 // a certmagic config for each combination of conn policy + automation policy...
305 cfg := *tlsApp.getConfigForName(hello.ServerName)
306 if p.CertSelection != nil {
307 // you would think we could just set this whether or not
308 // p.CertSelection is nil, but that leads to panics if
309 // it is, because cfg.CertSelection is an interface,
310 // so it will have a non-nil value even if the actual
311 // value underlying it is nil (sigh)
312 cfg.CertSelection = p.CertSelection
313 }
314 cfg.DefaultServerName = p.DefaultSNI
315 cfg.FallbackServerName = p.FallbackSNI
316
317 // TODO: experimental: if a handshake context module is configured, allow it
318 // to modify the context before passing it into CertMagic's GetCertificate
319 ctx := hello.Context()
320 if p.handshakeContext != nil {
321 ctx, err = p.handshakeContext.HandshakeContext(hello)
322 if err != nil {
323 return nil, fmt.Errorf("handshake context: %v", err)
324 }
325 }
326
327 return cfg.GetCertificateWithContext(ctx, hello)
328 },
329 MinVersion: tls.VersionTLS12,
330 MaxVersion: tls.VersionTLS13,
331 }
332
333 // session tickets support
334 if tlsApp.SessionTickets != nil {
335 cfg.SessionTicketsDisabled = tlsApp.SessionTickets.Disabled
336
337 // session ticket key rotation
338 tlsApp.SessionTickets.register(cfg)
339 ctx.OnCancel(func() {
340 // do cleanup when the context is canceled because,
341 // though unlikely, it is possible that a context
342 // needing a TLS server config could exist for less
343 // than the lifetime of the whole app
344 tlsApp.SessionTickets.unregister(cfg)
345 })
346 }
347
348 // TODO: Clean up session ticket active locks in storage if app (or process) is being closed!
349
350 // add all the cipher suites in order, without duplicates
351 cipherSuitesAdded := make(map[uint16]struct{})
352 for _, csName := range p.CipherSuites {
353 csID := CipherSuiteID(csName)
354 if csID == 0 {
355 return fmt.Errorf("unsupported cipher suite: %s", csName)
356 }
357 if _, ok := cipherSuitesAdded[csID]; !ok {
358 cipherSuitesAdded[csID] = struct{}{}
359 cfg.CipherSuites = append(cfg.CipherSuites, csID)
360 }
361 }
362
363 // add all the curve preferences in order, without duplicates
364 curvesAdded := make(map[tls.CurveID]struct{})
365 for _, curveName := range p.Curves {
366 curveID, ok := SupportedCurves[curveName]
367 if !ok {
368 return fmt.Errorf("unsupported curve: %s", curveName)
369 }
370 if _, ok := curvesAdded[curveID]; !ok {
371 curvesAdded[curveID] = struct{}{}
372 cfg.CurvePreferences = append(cfg.CurvePreferences, curveID)
373 }
374 }
375
376 // ensure ALPN includes the ACME TLS-ALPN protocol
377 alpnFound := slices.Contains(p.ALPN, acmez.ACMETLS1Protocol)
378 if !alpnFound && (cfg.NextProtos == nil || len(cfg.NextProtos) > 0) {
379 cfg.NextProtos = append(cfg.NextProtos, acmez.ACMETLS1Protocol)
380 }
381
382 // min and max protocol versions
383 if p.ProtocolMin != "" {
384 minVersion, ok := SupportedProtocols[p.ProtocolMin]
385 if !ok {
386 return fmt.Errorf("unsupported protocol_min: %s", p.ProtocolMin)
387 }
388 cfg.MinVersion = minVersion
389 }
390 if p.ProtocolMax != "" {
391 maxVersion, ok := SupportedProtocols[p.ProtocolMax]
392 if !ok {
393 return fmt.Errorf("unsupported protocol_max: %s", p.ProtocolMax)
394 }
395 cfg.MaxVersion = maxVersion
396 }
397 if cfg.MinVersion > cfg.MaxVersion {
398 return fmt.Errorf("protocol min (%s) cannot be greater than protocol max (%s)",
399 ProtocolName(cfg.MinVersion), ProtocolName(cfg.MaxVersion))
400 }
401
402 // client authentication
403 if p.ClientAuthentication != nil {
404 if err := p.ClientAuthentication.provision(ctx); err != nil {
405 return fmt.Errorf("provisioning client CA: %v", err)
406 }
407 if err := p.ClientAuthentication.ConfigureTLSConfig(cfg); err != nil {
408 return fmt.Errorf("configuring TLS client authentication: %v", err)
409 }
410
411 // Prevent privilege escalation in case multiple vhosts are configured for
412 // this TLS server; we could potentially figure out if that's the case, but
413 // that might be complex to get right every time. Actually, two proper
414 // solutions could leave tickets enabled, but I am not sure how to do them
415 // properly without significant time investment; there may be new Go
416 // APIs that alloaw this (Wrap/UnwrapSession?) but I do not know how to use
417 // them at this time. TODO: one of these is a possible future enhancement:
418 // A) Prevent resumptions across server identities (certificates): binding the ticket to the
419 // certificate we would serve in a full handshake, or even bind a ticket to the exact SNI
420 // it was issued under (though there are proposals for session resumption across hostnames).
421 // B) Prevent resumptions falsely authenticating a client: include the realm in the ticket,
422 // so that it can be validated upon resumption.
423 cfg.SessionTicketsDisabled = true
424 }
425
426 if p.InsecureSecretsLog != "" {
427 filename, err := caddy.NewReplacer().ReplaceOrErr(p.InsecureSecretsLog, true, true)
428 if err != nil {
429 return err
430 }
431 filename, err = caddy.FastAbs(filename)
432 if err != nil {
433 return err
434 }
435 logFile, _, err := secretsLogPool.LoadOrNew(filename, func() (caddy.Destructor, error) {
436 w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)
437 return destructableWriter{w}, err
438 })
439 if err != nil {
440 return err
441 }
442 ctx.OnCancel(func() { _, _ = secretsLogPool.Delete(filename) })
443
444 cfg.KeyLogWriter = logFile.(io.Writer)
445
446 if c := tlsApp.logger.Check(zapcore.WarnLevel, "TLS SECURITY COMPROMISED: secrets logging is enabled!"); c != nil {
447 c.Write(zap.String("log_filename", filename))
448 }
449 }
450
451 setDefaultTLSParams(cfg)
452
453 p.TLSConfig = cfg
454
455 return nil
456 }
457
458 // SettingsEmpty returns true if p's settings (fields
459 // except the matchers) are all empty/unset.
460 func (p ConnectionPolicy) SettingsEmpty() bool {
461 return p.CertSelection == nil &&
462 p.CipherSuites == nil &&
463 p.Curves == nil &&
464 p.ALPN == nil &&
465 p.ProtocolMin == "" &&
466 p.ProtocolMax == "" &&
467 p.ClientAuthentication == nil &&
468 p.DefaultSNI == "" &&
469 p.FallbackSNI == "" &&
470 p.InsecureSecretsLog == ""
471 }
472
473 // SettingsEqual returns true if p's settings (fields
474 // except the matchers) are the same as q.
475 func (p ConnectionPolicy) SettingsEqual(q ConnectionPolicy) bool {
476 p.MatchersRaw = nil
477 q.MatchersRaw = nil
478 return reflect.DeepEqual(p, q)
479 }
480
481 // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
482 //
483 // connection_policy {
484 // alpn <values...>
485 // cert_selection {
486 // ...
487 // }
488 // ciphers <cipher_suites...>
489 // client_auth {
490 // ...
491 // }
492 // curves <curves...>
493 // default_sni <server_name>
494 // match {
495 // ...
496 // }
497 // protocols <min> [<max>]
498 // # EXPERIMENTAL:
499 // drop
500 // fallback_sni <server_name>
501 // insecure_secrets_log <log_file>
502 // }
503 func (cp *ConnectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
504 _, wrapper := d.Next(), d.Val()
505
506 // No same-line options are supported
507 if d.CountRemainingArgs() > 0 {
508 return d.ArgErr()
509 }
510
511 var hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop,
512 hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool
513 for nesting := d.Nesting(); d.NextBlock(nesting); {
514 optionName := d.Val()
515 switch optionName {
516 case "alpn":
517 if d.CountRemainingArgs() == 0 {
518 return d.ArgErr()
519 }
520 cp.ALPN = append(cp.ALPN, d.RemainingArgs()...)
521 case "cert_selection":
522 if hasCertSelection {
523 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
524 }
525 p := &CustomCertSelectionPolicy{}
526 if err := p.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
527 return err
528 }
529 cp.CertSelection, hasCertSelection = p, true
530 case "client_auth":
531 if hasClientAuth {
532 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
533 }
534 ca := &ClientAuthentication{}
535 if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
536 return err
537 }
538 cp.ClientAuthentication, hasClientAuth = ca, true
539 case "ciphers":
540 if d.CountRemainingArgs() == 0 {
541 return d.ArgErr()
542 }
543 cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...)
544 case "curves":
545 if d.CountRemainingArgs() == 0 {
546 return d.ArgErr()
547 }
548 cp.Curves = append(cp.Curves, d.RemainingArgs()...)
549 case "default_sni":
550 if hasDefaultSNI {
551 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
552 }
553 if d.CountRemainingArgs() != 1 {
554 return d.ArgErr()
555 }
556 _, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true
557 case "drop": // EXPERIMENTAL
558 if hasDrop {
559 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
560 }
561 cp.Drop, hasDrop = true, true
562 case "fallback_sni": // EXPERIMENTAL
563 if hasFallbackSNI {
564 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
565 }
566 if d.CountRemainingArgs() != 1 {
567 return d.ArgErr()
568 }
569 _, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true
570 case "insecure_secrets_log": // EXPERIMENTAL
571 if hasInsecureSecretsLog {
572 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
573 }
574 if d.CountRemainingArgs() != 1 {
575 return d.ArgErr()
576 }
577 _, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true
578 case "match":
579 if hasMatch {
580 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
581 }
582 matcherSet, err := ParseCaddyfileNestedMatcherSet(d)
583 if err != nil {
584 return err
585 }
586 cp.MatchersRaw, hasMatch = matcherSet, true
587 case "protocols":
588 if hasProtocols {
589 return d.Errf("duplicate %s option '%s'", wrapper, optionName)
590 }
591 if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 {
592 return d.ArgErr()
593 }
594 _, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true
595 if d.NextArg() {
596 cp.ProtocolMax = d.Val()
597 }
598 default:
599 return d.ArgErr()
600 }
601
602 // No nested blocks are supported
603 if d.NextBlock(nesting + 1) {
604 return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
605 }
606 }
607
608 return nil
609 }
610
611 // ClientAuthentication configures TLS client auth.
612 type ClientAuthentication struct {
613 // Certificate authority module which provides the certificate pool of trusted certificates
614 CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
615 ca CA
616
617 // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
618 // A list of base64 DER-encoded CA certificates
619 // against which to validate client certificates.
620 // Client certs which are not signed by any of
621 // these CAs will be rejected.
622 TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
623
624 // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
625 // TrustedCACertPEMFiles is a list of PEM file names
626 // from which to load certificates of trusted CAs.
627 // Client certificates which are not signed by any of
628 // these CA certificates will be rejected.
629 TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"`
630
631 // Deprecated: This field is deprecated and will be removed in
632 // a future version. Please use the `validators` field instead
633 // with the tls.client_auth.verifier.leaf module instead.
634 //
635 // A list of base64 DER-encoded client leaf certs
636 // to accept. If this list is not empty, client certs
637 // which are not in this list will be rejected.
638 TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"`
639
640 // Client certificate verification modules. These can perform
641 // custom client authentication checks, such as ensuring the
642 // certificate is not revoked.
643 VerifiersRaw []json.RawMessage `json:"verifiers,omitempty" caddy:"namespace=tls.client_auth.verifier inline_key=verifier"`
644
645 verifiers []ClientCertificateVerifier
646
647 // The mode for authenticating the client. Allowed values are:
648 //
649 // Mode | Description
650 // -----|---------------
651 // `request` | Ask clients for a certificate, but allow even if there isn't one; do not verify it
652 // `require` | Require clients to present a certificate, but do not verify it
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 `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
665 }
666
667 // UnmarshalCaddyfile parses the Caddyfile segment to set up the client authentication. Syntax:
668 //
669 // client_auth {
670 // mode [request|require|verify_if_given|require_and_verify]
671 // trust_pool <module> {
672 // ...
673 // }
674 // verifier <module>
675 // }
676 //
677 // If `mode` is not provided, it defaults to `require_and_verify` if `trust_pool` is provided.
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.
683 }
684 for nesting := d.Nesting(); d.NextBlock(nesting); {
685 subdir := d.Val()
686 switch subdir {
687 case "mode":
688 if d.CountRemainingArgs() > 1 {
689 return d.ArgErr()
690 }
691 if !d.Args(&ca.Mode) {
692 return d.ArgErr()
693 }
694 case "trusted_ca_cert":
695 caddy.Log().Warn("The 'trusted_ca_cert' field is deprecated. Use the 'trust_pool' field instead.")
696 if len(ca.CARaw) != 0 {
697 return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'")
698 }
699 if !d.NextArg() {
700 return d.ArgErr()
701 }
702 ca.TrustedCACerts = append(ca.TrustedCACerts, d.Val())
703 case "trusted_leaf_cert":
704 if !d.NextArg() {
705 return d.ArgErr()
706 }
707 ca.TrustedLeafCerts = append(ca.TrustedLeafCerts, d.Val())
708 case "trusted_ca_cert_file":
709 caddy.Log().Warn("The 'trusted_ca_cert_file' field is deprecated. Use the 'trust_pool' field instead.")
710 if len(ca.CARaw) != 0 {
711 return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'")
712 }
713 if !d.NextArg() {
714 return d.ArgErr()
715 }
716 filename := d.Val()
717 ders, err := convertPEMFilesToDER(filename)
718 if err != nil {
719 return d.WrapErr(err)
720 }
721 ca.TrustedCACerts = append(ca.TrustedCACerts, ders...)
722 case "trusted_leaf_cert_file":
723 if !d.NextArg() {
724 return d.ArgErr()
725 }
726 filename := d.Val()
727 ders, err := convertPEMFilesToDER(filename)
728 if err != nil {
729 return d.WrapErr(err)
730 }
731 ca.TrustedLeafCerts = append(ca.TrustedLeafCerts, ders...)
732 case "trust_pool":
733 if len(ca.TrustedCACerts) != 0 {
734 return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'")
735 }
736 if !d.NextArg() {
737 return d.ArgErr()
738 }
739 modName := d.Val()
740 mod, err := caddyfile.UnmarshalModule(d, "tls.ca_pool.source."+modName)
741 if err != nil {
742 return d.WrapErr(err)
743 }
744 caMod, ok := mod.(CA)
745 if !ok {
746 return fmt.Errorf("trust_pool module '%s' is not a certificate pool provider", caMod)
747 }
748 ca.CARaw = caddyconfig.JSONModuleObject(caMod, "provider", modName, nil)
749 case "verifier":
750 if !d.NextArg() {
751 return d.ArgErr()
752 }
753
754 vType := d.Val()
755 modID := "tls.client_auth.verifier." + vType
756 unm, err := caddyfile.UnmarshalModule(d, modID)
757 if err != nil {
758 return err
759 }
760
761 _, ok := unm.(ClientCertificateVerifier)
762 if !ok {
763 return d.Errf("module '%s' is not a caddytls.ClientCertificateVerifier", modID)
764 }
765 ca.VerifiersRaw = append(ca.VerifiersRaw, caddyconfig.JSONModuleObject(unm, "verifier", vType, nil))
766 default:
767 return d.Errf("unknown subdirective for client_auth: %s", subdir)
768 }
769 }
770
771 // only trust_ca_cert or trust_ca_cert_file was specified
772 if len(ca.TrustedCACerts) > 0 {
773 fileMod := &InlineCAPool{}
774 fileMod.TrustedCACerts = append(fileMod.TrustedCACerts, ca.TrustedCACerts...)
775 ca.CARaw = caddyconfig.JSONModuleObject(fileMod, "provider", "inline", nil)
776 ca.TrustedCACertPEMFiles, ca.TrustedCACerts = nil, nil
777 }
778 return nil
779 }
780
781 func convertPEMFilesToDER(filename string) ([]string, error) {
782 certDataPEM, err := os.ReadFile(filename)
783 if err != nil {
784 return nil, err
785 }
786 var ders []string
787 // while block is not nil, we have more certificates in the file
788 for block, rest := pem.Decode(certDataPEM); block != nil; block, rest = pem.Decode(rest) {
789 if block.Type != "CERTIFICATE" {
790 return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename)
791 }
792 ders = append(
793 ders,
794 base64.StdEncoding.EncodeToString(block.Bytes),
795 )
796 }
797 // if we decoded nothing, return an error
798 if len(ders) == 0 {
799 return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename)
800 }
801 return ders, nil
802 }
803
804 func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error {
805 if len(clientauth.CARaw) > 0 && (len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0) {
806 return fmt.Errorf("conflicting config for client authentication trust CA")
807 }
808
809 // convert all named file paths to inline
810 if len(clientauth.TrustedCACertPEMFiles) > 0 {
811 for _, fpath := range clientauth.TrustedCACertPEMFiles {
812 ders, err := convertPEMFilesToDER(fpath)
813 if err != nil {
814 return err
815 }
816 clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...)
817 }
818 }
819
820 // if we have TrustedCACerts explicitly set, create an 'inline' CA and return
821 if len(clientauth.TrustedCACerts) > 0 {
822 caPool := InlineCAPool{
823 TrustedCACerts: clientauth.TrustedCACerts,
824 }
825 err := caPool.Provision(ctx)
826 if err != nil {
827 return err
828 }
829 clientauth.ca = caPool
830 }
831
832 // if we don't have any CARaw set, there's not much work to do
833 if clientauth.CARaw == nil {
834 return nil
835 }
836 caRaw, err := ctx.LoadModule(clientauth, "CARaw")
837 if err != nil {
838 return err
839 }
840 ca, ok := caRaw.(CA)
841 if !ok {
842 return fmt.Errorf("'ca' module '%s' is not a certificate pool provider", ca)
843 }
844 clientauth.ca = ca
845
846 return nil
847 }
848
849 // Active returns true if clientauth has an actionable configuration.
850 func (clientauth ClientAuthentication) Active() bool {
851 return len(clientauth.TrustedCACerts) > 0 ||
852 len(clientauth.TrustedCACertPEMFiles) > 0 ||
853 len(clientauth.TrustedLeafCerts) > 0 || // TODO: DEPRECATED
854 len(clientauth.VerifiersRaw) > 0 ||
855 len(clientauth.Mode) > 0 ||
856 clientauth.CARaw != nil || clientauth.ca != nil
857 }
858
859 // ConfigureTLSConfig sets up cfg to enforce clientauth's configuration.
860 func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) error {
861 // if there's no actionable client auth, simply disable it
862 if !clientauth.Active() {
863 cfg.ClientAuth = tls.NoClientCert
864 return nil
865 }
866
867 // enforce desired mode of client authentication
868 if len(clientauth.Mode) > 0 {
869 switch clientauth.Mode {
870 case "request":
871 cfg.ClientAuth = tls.RequestClientCert
872 case "require":
873 cfg.ClientAuth = tls.RequireAnyClientCert
874 case "verify_if_given":
875 cfg.ClientAuth = tls.VerifyClientCertIfGiven
876 case "require_and_verify":
877 cfg.ClientAuth = tls.RequireAndVerifyClientCert
878 default:
879 return fmt.Errorf("client auth mode not recognized: %s", clientauth.Mode)
880 }
881 } else {
882 // otherwise, set a safe default mode
883 if len(clientauth.TrustedCACerts) > 0 ||
884 len(clientauth.TrustedCACertPEMFiles) > 0 ||
885 clientauth.CARaw != nil || clientauth.ca != nil {
886 cfg.ClientAuth = tls.RequireAndVerifyClientCert
887 } else {
888 cfg.ClientAuth = tls.RequireAnyClientCert
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()
901 }
902
903 // TODO: DEPRECATED: Only here for backwards compatibility.
904 // If leaf cert is specified, enforce by adding a client auth module
905 if len(clientauth.TrustedLeafCerts) > 0 {
906 caddy.Log().Named("tls.connection_policy").Warn("trusted_leaf_certs is deprecated; use leaf verifier module instead")
907 var trustedLeafCerts []*x509.Certificate
908 for _, clientCertString := range clientauth.TrustedLeafCerts {
909 clientCert, err := decodeBase64DERCert(clientCertString)
910 if err != nil {
911 return fmt.Errorf("parsing certificate: %v", err)
912 }
913 trustedLeafCerts = append(trustedLeafCerts, clientCert)
914 }
915 clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{trustedLeafCerts: trustedLeafCerts})
916 }
917
918 // if a custom verification function already exists, wrap it
919 clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
920 cfg.VerifyConnection = clientauth.verifyConnection
921 return nil
922 }
923
924 // verifyConnection is for use as a tls.Config.VerifyConnection callback
925 // to do custom client certificate verification. It is intended for
926 // installation only by clientauth.ConfigureTLSConfig().
927 //
928 // Unlike VerifyPeerCertificate, VerifyConnection is called on every
929 // connection including resumed sessions, preventing session-resumption bypass.
930 func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error {
931 rawCerts := make([][]byte, len(cs.PeerCertificates))
932 for i, cert := range cs.PeerCertificates {
933 rawCerts[i] = cert.Raw
934 }
935
936 // first use any pre-existing custom verification function
937 if clientauth.existingVerifyPeerCert != nil {
938 if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil {
939 return err
940 }
941 }
942
943 // crypto/tls has already enforced whether the mode requires a
944 // certificate, so the verifiers only check one that was presented
945 if len(rawCerts) == 0 {
946 return nil
947 }
948 for _, verifier := range clientauth.verifiers {
949 if err := verifier.VerifyClientCertificate(rawCerts, cs.VerifiedChains); err != nil {
950 return err
951 }
952 }
953 return nil
954 }
955
956 // decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
957 func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
958 derBytes, err := base64.StdEncoding.DecodeString(certStr)
959 if err != nil {
960 return nil, err
961 }
962 return x509.ParseCertificate(derBytes)
963 }
964
965 // setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
966 // and server preferences of cfg if they are not already set; it does not
967 // overwrite values, only fills in missing values.
968 func setDefaultTLSParams(cfg *tls.Config) {
969 if len(cfg.CipherSuites) == 0 {
970 cfg.CipherSuites = getOptimalDefaultCipherSuites()
971 }
972
973 // Not a cipher suite, but still important for mitigating protocol downgrade attacks
974 // (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
975 cfg.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, cfg.CipherSuites...)
976
977 if len(cfg.CurvePreferences) == 0 {
978 cfg.CurvePreferences = defaultCurves
979 }
980
981 // crypto/tls docs:
982 // "If EncryptedClientHelloKeys is set, MinVersion, if set, must be VersionTLS13."
983 if cfg.EncryptedClientHelloKeys != nil && cfg.MinVersion != 0 && cfg.MinVersion < tls.VersionTLS13 {
984 cfg.MinVersion = tls.VersionTLS13
985 }
986 }
987
988 // LeafCertClientAuth verifies the client's leaf certificate.
989 type LeafCertClientAuth struct {
990 LeafCertificateLoadersRaw []json.RawMessage `json:"leaf_certs_loaders,omitempty" caddy:"namespace=tls.leaf_cert_loader inline_key=loader"`
991 trustedLeafCerts []*x509.Certificate
992 }
993
994 // CaddyModule returns the Caddy module information.
995 func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo {
996 return caddy.ModuleInfo{
997 ID: "tls.client_auth.verifier.leaf",
998 New: func() caddy.Module { return new(LeafCertClientAuth) },
999 }
1000 }
1001
1002 func (l *LeafCertClientAuth) Provision(ctx caddy.Context) error {
1003 if l.LeafCertificateLoadersRaw == nil {
1004 return nil
1005 }
1006 val, err := ctx.LoadModule(l, "LeafCertificateLoadersRaw")
1007 if err != nil {
1008 return fmt.Errorf("could not parse leaf certificates loaders: %s", err.Error())
1009 }
1010 trustedLeafCertloaders := []LeafCertificateLoader{}
1011 for _, loader := range val.([]any) {
1012 trustedLeafCertloaders = append(trustedLeafCertloaders, loader.(LeafCertificateLoader))
1013 }
1014 trustedLeafCertificates := []*x509.Certificate{}
1015 for _, loader := range trustedLeafCertloaders {
1016 certs, err := loader.LoadLeafCertificates()
1017 if err != nil {
1018 return fmt.Errorf("could not load leaf certificates: %s", err.Error())
1019 }
1020 trustedLeafCertificates = append(trustedLeafCertificates, certs...)
1021 }
1022 l.trustedLeafCerts = trustedLeafCertificates
1023 return nil
1024 }
1025
1026 // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
1027 func (l *LeafCertClientAuth) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1028 d.NextArg()
1029
1030 // accommodate the use of one-liners
1031 if d.CountRemainingArgs() > 1 {
1032 d.NextArg()
1033 modName := d.Val()
1034 mod, err := caddyfile.UnmarshalModule(d, "tls.leaf_cert_loader."+modName)
1035 if err != nil {
1036 return d.WrapErr(err)
1037 }
1038 vMod, ok := mod.(LeafCertificateLoader)
1039 if !ok {
1040 return fmt.Errorf("leaf module '%s' is not a leaf certificate loader", vMod)
1041 }
1042 l.LeafCertificateLoadersRaw = append(
1043 l.LeafCertificateLoadersRaw,
1044 caddyconfig.JSONModuleObject(vMod, "loader", modName, nil),
1045 )
1046 return nil
1047 }
1048
1049 // accommodate the use of nested blocks
1050 for nesting := d.Nesting(); d.NextBlock(nesting); {
1051 modName := d.Val()
1052 mod, err := caddyfile.UnmarshalModule(d, "tls.leaf_cert_loader."+modName)
1053 if err != nil {
1054 return d.WrapErr(err)
1055 }
1056 vMod, ok := mod.(LeafCertificateLoader)
1057 if !ok {
1058 return fmt.Errorf("leaf module '%s' is not a leaf certificate loader", vMod)
1059 }
1060 l.LeafCertificateLoadersRaw = append(
1061 l.LeafCertificateLoadersRaw,
1062 caddyconfig.JSONModuleObject(vMod, "loader", modName, nil),
1063 )
1064 }
1065 return nil
1066 }
1067
1068 func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
1069 if len(rawCerts) == 0 {
1070 return fmt.Errorf("no client certificate provided")
1071 }
1072
1073 remoteLeafCert, err := x509.ParseCertificate(rawCerts[0])
1074 if err != nil {
1075 return fmt.Errorf("can't parse the given certificate: %s", err.Error())
1076 }
1077
1078 if slices.ContainsFunc(l.trustedLeafCerts, remoteLeafCert.Equal) {
1079 return nil
1080 }
1081
1082 return fmt.Errorf("client leaf certificate failed validation")
1083 }
1084
1085 // PublicKeyAlgorithm is a JSON-unmarshalable wrapper type.
1086 type PublicKeyAlgorithm x509.PublicKeyAlgorithm
1087
1088 // UnmarshalJSON satisfies json.Unmarshaler.
1089 func (a *PublicKeyAlgorithm) UnmarshalJSON(b []byte) error {
1090 algoStr := strings.ToLower(strings.Trim(string(b), `"`))
1091 algo, ok := publicKeyAlgorithms[algoStr]
1092 if !ok {
1093 return fmt.Errorf("unrecognized public key algorithm: %s (expected one of %v)",
1094 algoStr, publicKeyAlgorithms)
1095 }
1096 *a = PublicKeyAlgorithm(algo)
1097 return nil
1098 }
1099
1100 // ConnectionMatcher is a type which matches TLS handshakes.
1101 type ConnectionMatcher interface {
1102 Match(*tls.ClientHelloInfo) bool
1103 }
1104
1105 // LeafCertificateLoader is a type that loads the trusted leaf certificates
1106 // for the tls.leaf_cert_loader modules
1107 type LeafCertificateLoader interface {
1108 LoadLeafCertificates() ([]*x509.Certificate, error)
1109 }
1110
1111 // ClientCertificateVerifier is a type which verifies client certificates.
1112 // It is called during verifyConnection in the TLS handshake, only when
1113 // the client presented a certificate.
1114 type ClientCertificateVerifier interface {
1115 VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
1116 }
1117
1118 var defaultALPN = []string{"h2", "http/1.1"}
1119
1120 type destructableWriter struct{ *os.File }
1121
1122 func (d destructableWriter) Destruct() error { return d.Close() }
1123
1124 var secretsLogPool = caddy.NewUsagePool()
1125
1126 // Interface guards
1127 var (
1128 _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
1129 _ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil)
1130 _ caddyfile.Unmarshaler = (*LeafCertClientAuth)(nil)
1131 )
1132
1133 // ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested
1134 // matcher set, and returns its raw module map value.
1135 func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) {
1136 matcherMap := make(map[string]ConnectionMatcher)
1137
1138 // in case there are multiple instances of the same matcher, concatenate
1139 // their tokens (we expect that UnmarshalCaddyfile should be able to
1140 // handle more than one segment); otherwise, we'd overwrite other
1141 // instances of the matcher in this set
1142 tokensByMatcherName := make(map[string][]caddyfile.Token)
1143 for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
1144 matcherName := d.Val()
1145 tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
1146 }
1147
1148 for matcherName, tokens := range tokensByMatcherName {
1149 mod, err := caddy.GetModule("tls.handshake_match." + matcherName)
1150 if err != nil {
1151 return nil, d.Errf("getting matcher module '%s': %v", matcherName, err)
1152 }
1153 unm, ok := mod.New().(caddyfile.Unmarshaler)
1154 if !ok {
1155 return nil, d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
1156 }
1157 err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
1158 if err != nil {
1159 return nil, err
1160 }
1161 cm, ok := unm.(ConnectionMatcher)
1162 if !ok {
1163 return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName)
1164 }
1165 matcherMap[matcherName] = cm
1166 }
1167
1168 matcherSet := make(caddy.ModuleMap)
1169 for name, matcher := range matcherMap {
1170 jsonBytes, err := json.Marshal(matcher)
1171 if err != nil {
1172 return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err)
1173 }
1174 matcherSet[name] = jsonBytes
1175 }
1176
1177 return matcherSet, nil
1178 }