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
This commit is contained in:
vnxme 2024-07-24 20:01:06 +03:00 committed by GitHub
parent 61fe152c60
commit 3579815a6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 251 additions and 1 deletions

View File

@ -22,6 +22,8 @@ import (
"math/big"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
// CustomCertSelectionPolicy represents a policy for selecting the certificate
@ -122,6 +124,79 @@ nextChoice:
return certmagic.DefaultCertificateSelector(hello, viable)
}
// UnmarshalCaddyfile sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax:
//
// cert_selection {
// all_tags <values...>
// any_tag <values...>
// public_key_algorithm <dsa|ecdsa|rsa>
// serial_number <big_integers...>
// subject_organization <values...>
// }
func (p *CustomCertSelectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
_, wrapper := d.Next(), d.Val() // consume wrapper name
// No same-line options are supported
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
var hasPublicKeyAlgorithm bool
for nesting := d.Nesting(); d.NextBlock(nesting); {
optionName := d.Val()
switch optionName {
case "all_tags":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
p.AllTags = append(p.AllTags, d.RemainingArgs()...)
case "any_tag":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
p.AnyTag = append(p.AnyTag, d.RemainingArgs()...)
case "public_key_algorithm":
if hasPublicKeyAlgorithm {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
d.NextArg()
if err := p.PublicKeyAlgorithm.UnmarshalJSON([]byte(d.Val())); err != nil {
return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err)
}
hasPublicKeyAlgorithm = true
case "serial_number":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
for d.NextArg() {
val, bi := d.Val(), bigInt{}
_, ok := bi.SetString(val, 10)
if !ok {
return d.Errf("parsing %s option '%s': invalid big.int value %s", wrapper, optionName, val)
}
p.SerialNumber = append(p.SerialNumber, bi)
}
case "subject_organization":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
p.SubjectOrganization = append(p.SubjectOrganization, d.RemainingArgs()...)
default:
return d.ArgErr()
}
// No nested blocks are supported
if d.NextBlock(nesting + 1) {
return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
}
}
return nil
}
// bigInt is a big.Int type that interops with JSON encodings as a string.
type bigInt struct{ big.Int }
@ -144,3 +219,6 @@ func (bi *bigInt) UnmarshalJSON(p []byte) error {
}
return nil
}
// Interface guard
var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil)

View File

@ -363,6 +363,136 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
p.InsecureSecretsLog == ""
}
// UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
//
// connection_policy {
// alpn <values...>
// cert_selection {
// ...
// }
// ciphers <cipher_suites...>
// client_auth {
// ...
// }
// curves <curves...>
// default_sni <server_name>
// match {
// ...
// }
// protocols <min> [<max>]
// # EXPERIMENTAL:
// drop
// fallback_sni <server_name>
// insecure_secrets_log <log_file>
// }
func (cp *ConnectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
_, wrapper := d.Next(), d.Val()
// No same-line options are supported
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
var hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop,
hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool
for nesting := d.Nesting(); d.NextBlock(nesting); {
optionName := d.Val()
switch optionName {
case "alpn":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
cp.ALPN = append(cp.ALPN, d.RemainingArgs()...)
case "cert_selection":
if hasCertSelection {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
p := &CustomCertSelectionPolicy{}
if err := p.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
return err
}
cp.CertSelection, hasCertSelection = p, true
case "client_auth":
if hasClientAuth {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
ca := &ClientAuthentication{}
if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
return err
}
cp.ClientAuthentication, hasClientAuth = ca, true
case "ciphers":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...)
case "curves":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
cp.Curves = append(cp.Curves, d.RemainingArgs()...)
case "default_sni":
if hasDefaultSNI {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true
case "drop": // EXPERIMENTAL
if hasDrop {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
cp.Drop, hasDrop = true, true
case "fallback_sni": // EXPERIMENTAL
if hasFallbackSNI {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true
case "insecure_secrets_log": // EXPERIMENTAL
if hasInsecureSecretsLog {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true
case "match":
if hasMatch {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
matcherSet, err := ParseCaddyfileNestedMatcherSet(d)
if err != nil {
return err
}
cp.MatchersRaw, hasMatch = matcherSet, true
case "protocols":
if hasProtocols {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 {
return d.ArgErr()
}
_, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true
if d.NextArg() {
cp.ProtocolMax = d.Val()
}
default:
return d.ArgErr()
}
// No nested blocks are supported
if d.NextBlock(nesting + 1) {
return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
}
}
return nil
}
// ClientAuthentication configures TLS client auth.
type ClientAuthentication struct {
// Certificate authority module which provides the certificate pool of trusted certificates
@ -819,4 +949,46 @@ func (d destructableWriter) Destruct() error { return d.Close() }
var secretsLogPool = caddy.NewUsagePool()
var _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
// Interface guards
var (
_ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
_ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil)
)
// ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested
// matcher set, and returns its raw module map value.
func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) {
matcherMap := make(map[string]ConnectionMatcher)
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
dd := caddyfile.NewDispenser(tokens)
dd.Next() // consume wrapper name
unm, err := caddyfile.UnmarshalModule(dd, "tls.handshake_match."+matcherName)
if err != nil {
return nil, err
}
cm, ok := unm.(ConnectionMatcher)
if !ok {
return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName)
}
matcherMap[matcherName] = cm
}
matcherSet := make(caddy.ModuleMap)
for name, matcher := range matcherMap {
jsonBytes, err := json.Marshal(matcher)
if err != nil {
return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err)
}
matcherSet[name] = jsonBytes
}
return matcherSet, nil
}