diff --git a/internal/ingress/controller/certificate.go b/internal/ingress/controller/certificate.go new file mode 100644 index 000000000..cec420659 --- /dev/null +++ b/internal/ingress/controller/certificate.go @@ -0,0 +1,108 @@ +package controller + +import ( + "crypto/x509" + "net" + "strings" + "unicode/utf8" +) + +// Please check https://github.com/golang/go/issues/22922 +// +// Since Go 1.9 the common name field is not used anymore. +// We copy the code to not break existing clusters that doesn't have certificates with SAN yet +// TODO: Remove this helpers in the future. + +// verifyHostname returns nil if c is a valid certificate for the named host. +// Otherwise it returns an error describing the mismatch. +func verifyHostname(h string, c *x509.Certificate) error { + // IP addresses may be written in [ ]. + candidateIP := h + if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' { + candidateIP = h[1 : len(h)-1] + } + if ip := net.ParseIP(candidateIP); ip != nil { + // We only match IP addresses against IP SANs. + // https://tools.ietf.org/html/rfc6125#appendix-B.2 + for _, candidate := range c.IPAddresses { + if ip.Equal(candidate) { + return nil + } + } + return x509.HostnameError{Certificate: c, Host: candidateIP} + } + + lowered := toLowerCaseASCII(h) + + if len(c.DNSNames) > 0 { + for _, match := range c.DNSNames { + if matchHostnames(toLowerCaseASCII(match), lowered) { + return nil + } + } + // If Subject Alt Name is given, we ignore the common name. + } else if matchHostnames(toLowerCaseASCII(c.Subject.CommonName), lowered) { + return nil + } + + return x509.HostnameError{Certificate: c, Host: h} +} + +// toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use +// an explicitly ASCII function to avoid any sharp corners resulting from +// performing Unicode operations on DNS labels. +func toLowerCaseASCII(in string) string { + // If the string is already lower-case then there's nothing to do. + isAlreadyLowerCase := true + for _, c := range in { + if c == utf8.RuneError { + // If we get a UTF-8 error then there might be + // upper-case ASCII bytes in the invalid sequence. + isAlreadyLowerCase = false + break + } + if 'A' <= c && c <= 'Z' { + isAlreadyLowerCase = false + break + } + } + + if isAlreadyLowerCase { + return in + } + + out := []byte(in) + for i, c := range out { + if 'A' <= c && c <= 'Z' { + out[i] += 'a' - 'A' + } + } + return string(out) +} + +func matchHostnames(pattern, host string) bool { + host = strings.TrimSuffix(host, ".") + pattern = strings.TrimSuffix(pattern, ".") + + if len(pattern) == 0 || len(host) == 0 { + return false + } + + patternParts := strings.Split(pattern, ".") + hostParts := strings.Split(host, ".") + + if len(patternParts) != len(hostParts) { + return false + } + + for i, patternPart := range patternParts { + if i == 0 && patternPart == "*" { + continue + } + if patternPart != hostParts[i] { + return false + } + } + + return true +} diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index 9a8883c8c..7d46f1a88 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -1032,8 +1032,15 @@ func (n *NGINXController) createServers(data []*extensions.Ingress, cert := bc.(*ingress.SSLCert) err = cert.Certificate.VerifyHostname(host) if err != nil { - glog.Warningf("ssl certificate %v does not contain a Common Name or Subject Alternative Name for host %v", key, host) - continue + glog.Warningf("unexpected error validating SSL certificate %v for host %v. Reason: %v", key, host, err) + glog.Warningf("Validating certificate against DNS names. This will be deprecated in a future version.") + // check the common name field + // https://github.com/golang/go/issues/22922 + err := verifyHostname(host, cert.Certificate) + if err != nil { + glog.Warningf("ssl certificate %v does not contain a Common Name or Subject Alternative Name for host %v. Reason: %v", key, host, err) + continue + } } servers[host].SSLCertificate = cert.PemFileName