Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 12, 10:45 UTC
caddytls: Caddyfile support for TLS conn and cert sel policies (#6462)
* Caddyfile support for TLS custom certificate selection policy * Caddyfile support for TLS connection policy
vnxme committed
Jul 24, 2024 at 20:01 UTC
3579815a6c099bfbcff0143f7a251a92b4fa4198
2 files changed
+251
-1
modules/caddytls/certselection.go
+78
@@ -22,6 +22,8 @@ import (
22
"math/big"
23
24
"github.com/caddyserver/certmagic"
25
+
26
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
27
)
28
29
// CustomCertSelectionPolicy represents a policy for selecting the certificate
@@ -122,6 +124,79 @@ nextChoice:
124
return certmagic.DefaultCertificateSelector(hello, viable)
125
}
126
127
+// UnmarshalCaddyfile sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax:
128
+//
129
+// cert_selection {
130
+// all_tags <values...>
131
+// any_tag <values...>
132
+// public_key_algorithm <dsa|ecdsa|rsa>
133
+// serial_number <big_integers...>
134
+// subject_organization <values...>
135
+// }
136
+func (p *CustomCertSelectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
137
+ _, wrapper := d.Next(), d.Val() // consume wrapper name
138
+
139
+ // No same-line options are supported
140
+ if d.CountRemainingArgs() > 0 {
141
+ return d.ArgErr()
142
+ }
143
+
144
+ var hasPublicKeyAlgorithm bool
145
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
146
+ optionName := d.Val()
147
+ switch optionName {
148
+ case "all_tags":
149
+ if d.CountRemainingArgs() == 0 {
150
+ return d.ArgErr()
151
+ }
152
+ p.AllTags = append(p.AllTags, d.RemainingArgs()...)
153
+ case "any_tag":
154
+ if d.CountRemainingArgs() == 0 {
155
+ return d.ArgErr()
156
+ }
157
+ p.AnyTag = append(p.AnyTag, d.RemainingArgs()...)
158
+ case "public_key_algorithm":
159
+ if hasPublicKeyAlgorithm {
160
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
161
+ }
162
+ if d.CountRemainingArgs() != 1 {
163
+ return d.ArgErr()
164
+ }
165
+ d.NextArg()
166
+ if err := p.PublicKeyAlgorithm.UnmarshalJSON([]byte(d.Val())); err != nil {
167
+ return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err)
168
+ }
169
+ hasPublicKeyAlgorithm = true
170
+ case "serial_number":
171
+ if d.CountRemainingArgs() == 0 {
172
+ return d.ArgErr()
173
+ }
174
+ for d.NextArg() {
175
+ val, bi := d.Val(), bigInt{}
176
+ _, ok := bi.SetString(val, 10)
177
+ if !ok {
178
+ return d.Errf("parsing %s option '%s': invalid big.int value %s", wrapper, optionName, val)
179
+ }
180
+ p.SerialNumber = append(p.SerialNumber, bi)
181
+ }
182
+ case "subject_organization":
183
+ if d.CountRemainingArgs() == 0 {
184
+ return d.ArgErr()
185
+ }
186
+ p.SubjectOrganization = append(p.SubjectOrganization, d.RemainingArgs()...)
187
+ default:
188
+ return d.ArgErr()
189
+ }
190
+
191
+ // No nested blocks are supported
192
+ if d.NextBlock(nesting + 1) {
193
+ return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
194
+ }
195
+ }
196
+
197
+ return nil
198
+}
199
+
200
// bigInt is a big.Int type that interops with JSON encodings as a string.
201
type bigInt struct{ big.Int }
202
@@ -144,3 +219,6 @@ func (bi *bigInt) UnmarshalJSON(p []byte) error {
219
}
220
return nil
221
}
222
+
223
+// Interface guard
224
+var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil)
modules/caddytls/connpolicy.go
+173
-1
@@ -363,6 +363,136 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
363
p.InsecureSecretsLog == ""
364
}
365
366
+// UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
367
+//
368
+// connection_policy {
369
+// alpn <values...>
370
+// cert_selection {
371
+// ...
372
+// }
373
+// ciphers <cipher_suites...>
374
+// client_auth {
375
+// ...
376
+// }
377
+// curves <curves...>
378
+// default_sni <server_name>
379
+// match {
380
+// ...
381
+// }
382
+// protocols <min> [<max>]
383
+// # EXPERIMENTAL:
384
+// drop
385
+// fallback_sni <server_name>
386
+// insecure_secrets_log <log_file>
387
+// }
388
+func (cp *ConnectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
389
+ _, wrapper := d.Next(), d.Val()
390
+
391
+ // No same-line options are supported
392
+ if d.CountRemainingArgs() > 0 {
393
+ return d.ArgErr()
394
+ }
395
+
396
+ var hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop,
397
+ hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool
398
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
399
+ optionName := d.Val()
400
+ switch optionName {
401
+ case "alpn":
402
+ if d.CountRemainingArgs() == 0 {
403
+ return d.ArgErr()
404
+ }
405
+ cp.ALPN = append(cp.ALPN, d.RemainingArgs()...)
406
+ case "cert_selection":
407
+ if hasCertSelection {
408
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
409
+ }
410
+ p := &CustomCertSelectionPolicy{}
411
+ if err := p.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
412
+ return err
413
+ }
414
+ cp.CertSelection, hasCertSelection = p, true
415
+ case "client_auth":
416
+ if hasClientAuth {
417
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
418
+ }
419
+ ca := &ClientAuthentication{}
420
+ if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
421
+ return err
422
+ }
423
+ cp.ClientAuthentication, hasClientAuth = ca, true
424
+ case "ciphers":
425
+ if d.CountRemainingArgs() == 0 {
426
+ return d.ArgErr()
427
+ }
428
+ cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...)
429
+ case "curves":
430
+ if d.CountRemainingArgs() == 0 {
431
+ return d.ArgErr()
432
+ }
433
+ cp.Curves = append(cp.Curves, d.RemainingArgs()...)
434
+ case "default_sni":
435
+ if hasDefaultSNI {
436
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
437
+ }
438
+ if d.CountRemainingArgs() != 1 {
439
+ return d.ArgErr()
440
+ }
441
+ _, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true
442
+ case "drop": // EXPERIMENTAL
443
+ if hasDrop {
444
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
445
+ }
446
+ cp.Drop, hasDrop = true, true
447
+ case "fallback_sni": // EXPERIMENTAL
448
+ if hasFallbackSNI {
449
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
450
+ }
451
+ if d.CountRemainingArgs() != 1 {
452
+ return d.ArgErr()
453
+ }
454
+ _, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true
455
+ case "insecure_secrets_log": // EXPERIMENTAL
456
+ if hasInsecureSecretsLog {
457
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
458
+ }
459
+ if d.CountRemainingArgs() != 1 {
460
+ return d.ArgErr()
461
+ }
462
+ _, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true
463
+ case "match":
464
+ if hasMatch {
465
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
466
+ }
467
+ matcherSet, err := ParseCaddyfileNestedMatcherSet(d)
468
+ if err != nil {
469
+ return err
470
+ }
471
+ cp.MatchersRaw, hasMatch = matcherSet, true
472
+ case "protocols":
473
+ if hasProtocols {
474
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
475
+ }
476
+ if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 {
477
+ return d.ArgErr()
478
+ }
479
+ _, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true
480
+ if d.NextArg() {
481
+ cp.ProtocolMax = d.Val()
482
+ }
483
+ default:
484
+ return d.ArgErr()
485
+ }
486
+
487
+ // No nested blocks are supported
488
+ if d.NextBlock(nesting + 1) {
489
+ return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
490
+ }
491
+ }
492
+
493
+ return nil
494
+}
495
+
496
// ClientAuthentication configures TLS client auth.
497
type ClientAuthentication struct {
498
// Certificate authority module which provides the certificate pool of trusted certificates
@@ -819,4 +949,46 @@ func (d destructableWriter) Destruct() error { return d.Close() }
949
950
var secretsLogPool = caddy.NewUsagePool()
951
822
-var _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
952
+// Interface guards
953
+var (
954
+ _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
955
+ _ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil)
956
+)
957
+
958
+// ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested
959
+// matcher set, and returns its raw module map value.
960
+func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) {
961
+ matcherMap := make(map[string]ConnectionMatcher)
962
+
963
+ tokensByMatcherName := make(map[string][]caddyfile.Token)
964
+ for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
965
+ matcherName := d.Val()
966
+ tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
967
+ }
968
+
969
+ for matcherName, tokens := range tokensByMatcherName {
970
+ dd := caddyfile.NewDispenser(tokens)
971
+ dd.Next() // consume wrapper name
972
+
973
+ unm, err := caddyfile.UnmarshalModule(dd, "tls.handshake_match."+matcherName)
974
+ if err != nil {
975
+ return nil, err
976
+ }
977
+ cm, ok := unm.(ConnectionMatcher)
978
+ if !ok {
979
+ return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName)
980
+ }
981
+ matcherMap[matcherName] = cm
982
+ }
983
+
984
+ matcherSet := make(caddy.ModuleMap)
985
+ for name, matcher := range matcherMap {
986
+ jsonBytes, err := json.Marshal(matcher)
987
+ if err != nil {
988
+ return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err)
989
+ }
990
+ matcherSet[name] = jsonBytes
991
+ }
992
+
993
+ return matcherSet, nil
994
+}