Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 72 additions & 8 deletions factory/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"encoding/pem"
"fmt"
"net"
"regexp"
"sort"
"strings"
"time"
Expand All @@ -28,10 +27,6 @@ const (
fingerprint = "listener.cattle.io/fingerprint"
)

var (
cnRegexp = regexp.MustCompile("^([A-Za-z0-9:][-A-Za-z0-9_.:]*)?[A-Za-z0-9:]$")
)

type TLS struct {
CACert []*x509.Certificate
CAKey crypto.Signer
Expand Down Expand Up @@ -244,7 +239,7 @@ func populateCN(secret *v1.Secret, cn ...string) *v1.Secret {
secret.Annotations = map[string]string{}
}
for _, cn := range cn {
if cnRegexp.MatchString(cn) {
if validHostnamePattern(cn) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is correct, as populateCN is also called to add CNs extracted from TLS handshakes and HTTP host headers. Since validHostnamePattern calls validHostname(host, true), I believe this would allow unauthenticated callers to make dynamiclistener add wildcard SANs, if they spoof the correct value. I think wildcards should only be added if requested by the administrator.

secret.Annotations[getAnnotationKey(cn)] = cn
} else {
logrus.Errorf("dropping invalid CN: %s", cn)
Expand Down Expand Up @@ -272,7 +267,7 @@ func NeedsUpdate(maxSANs int, secret *v1.Secret, cn ...string) bool {
}

for _, cn := range cn {
if secret.Annotations[getAnnotationKey(cn)] == "" {
if secret.Annotations[getAnnotationKey(cn)] == "" && secret.Annotations[getAnnotationKey(getWildcardSAN(cn))] == "" {
if maxSANs > 0 && len(cns(secret)) >= maxSANs {
return false
}
Expand Down Expand Up @@ -344,13 +339,82 @@ func NewPrivateKey() (crypto.Signer, error) {
func getAnnotationKey(cn string) string {
cn = cnPrefix + cn
cnLen := len(cn)
if cnLen < 64 && !strings.ContainsRune(cn, ':') {
if cnLen < 64 && !strings.ContainsRune(cn, ':') && !strings.ContainsRune(cn, '*') {
return cn
}
digest := sha256.Sum256([]byte(cn))
// : only resides in IPv6 addresses
cn = strings.ReplaceAll(cn, ":", "_")
// * only resides in domain name wildcards, so it cannot coexit with : in annotation keys
cn = strings.ReplaceAll(cn, "*", "_")
if cnLen > 56 {
cnLen = 56
}
return cn[0:cnLen] + "-" + hex.EncodeToString(digest[0:])[0:6]
}

// get wildcard SAN for a given CN
// e.g. *.example.com for k3s.example.com
func getWildcardSAN(cn string) string {
if strings.Contains(cn, ".") {
return "*." + strings.SplitN(cn, ".", 2)[1]
}
return cn
}

// from https://github.com/golang/go/blob/master/src/crypto/x509/verify.go
func validHostnamePattern(host string) bool { return validHostname(host, true) }

// adapted from https://github.com/golang/go/blob/master/src/crypto/x509/verify.go
// DIFFERENT WITH Go codebase: added support of IPv6 address (:)
//
// validHostname reports whether host is a valid hostname that can be matched or
// matched against according to RFC 6125 2.2, with some leniency to accommodate
// legacy values.
func validHostname(host string, isPattern bool) bool {
if !isPattern {
host = strings.TrimSuffix(host, ".")
}
if len(host) == 0 {
return false
}

for i, part := range strings.Split(host, ".") {
if part == "" {
// Empty label.
return false
}
if isPattern && i == 0 && part == "*" {
// Only allow full left-most wildcards, as those are the only ones
// we match, and matching literal '*' characters is probably never
// the expected behavior.
continue
}
for j, c := range part {
if 'a' <= c && c <= 'z' {
continue
}
if '0' <= c && c <= '9' {
continue
}
if 'A' <= c && c <= 'Z' {
continue
}
if c == '-' && j != 0 {
continue
}
if c == '_' {
// Not a valid character in hostnames, but commonly
// found in deployments outside the WebPKI.
continue
}
if c == ':' {
// IPv6 support added in dynamiclistener
continue
}
return false
}
}

return true
}