mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
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:
parent
61fe152c60
commit
3579815a6c
2 changed files with 251 additions and 1 deletions
|
@ -22,6 +22,8 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CustomCertSelectionPolicy represents a policy for selecting the certificate
|
// CustomCertSelectionPolicy represents a policy for selecting the certificate
|
||||||
|
@ -122,6 +124,79 @@ nextChoice:
|
||||||
return certmagic.DefaultCertificateSelector(hello, viable)
|
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.
|
// bigInt is a big.Int type that interops with JSON encodings as a string.
|
||||||
type bigInt struct{ big.Int }
|
type bigInt struct{ big.Int }
|
||||||
|
|
||||||
|
@ -144,3 +219,6 @@ func (bi *bigInt) UnmarshalJSON(p []byte) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface guard
|
||||||
|
var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil)
|
||||||
|
|
|
@ -363,6 +363,136 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
|
||||||
p.InsecureSecretsLog == ""
|
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.
|
// ClientAuthentication configures TLS client auth.
|
||||||
type ClientAuthentication struct {
|
type ClientAuthentication struct {
|
||||||
// Certificate authority module which provides the certificate pool of trusted certificates
|
// 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 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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue