type leafSpec struct {
sans []string
ekus []string
+ cn string
}
var nameConstraintsTests = []nameConstraintsTest{
},
},
- // #30: without SANs, a certificate is rejected in a constrained chain.
+ // #30: without SANs, a certificate with a CN is rejected in a constrained chain.
nameConstraintsTest{
roots: []constraintsSpec{
constraintsSpec{
},
leaf: leafSpec{
sans: []string{},
+ cn: "foo.com",
},
expectedError: "leaf doesn't have a SAN extension",
- noOpenSSL: true, // OpenSSL doesn't require SANs in this case.
},
// #31: IPv6 addresses work in constraints: roots can permit them as
ekus: []string{"email", "serverAuth"},
},
},
+
+ // #82: a certificate without SANs and CN is accepted in a constrained chain.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{},
+ },
+ },
+
+ // #83: a certificate without SANs and with a CN that does not parse as a
+ // hostname is accepted in a constrained chain.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{},
+ cn: "foo,bar",
+ },
+ },
+
+ // #84: a certificate with SANs and CN is accepted in a constrained chain.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.com"},
+ cn: "foo.bar",
+ },
+ },
}
func makeConstraintsCACert(constraints constraintsSpec, name string, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {
template := &Certificate{
SerialNumber: new(big.Int).SetBytes(serialBytes[:]),
Subject: pkix.Name{
- // Don't set a CommonName because OpenSSL (at least) will try to
- // match it against name constraints.
OrganizationalUnit: []string{"Leaf"},
+ CommonName: leaf.cn,
},
NotBefore: time.Unix(1000, 0),
NotAfter: time.Unix(2000, 0),
t.Fatalf("#%d: cannot create leaf: %s", i, err)
}
- if !test.noOpenSSL && testNameConstraintsAgainstOpenSSL {
+ // Skip tests with CommonName set because OpenSSL will try to match it
+ // against name constraints, while we ignore it when it's not hostname-looking.
+ if !test.noOpenSSL && testNameConstraintsAgainstOpenSSL && test.leaf.cn == "" {
output, err := testChainAgainstOpenSSL(leafCert, intermediatePool, rootPool)
if err == nil && len(test.expectedError) > 0 {
t.Errorf("#%d: unexpectedly succeeded against OpenSSL", i)
if _, ok := err.(*exec.ExitError); !ok {
t.Errorf("#%d: OpenSSL failed to run: %s", i, err)
} else if len(test.expectedError) == 0 {
- t.Errorf("#%d: OpenSSL unexpectedly failed: %q", i, output)
+ t.Errorf("#%d: OpenSSL unexpectedly failed: %v", i, output)
if debugOpenSSLFailure {
return
}
certAsPEM := func(cert *Certificate) string {
var buf bytes.Buffer
pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
- return string(buf.Bytes())
+ return buf.String()
}
t.Errorf("#%d: root:\n%s", i, certAsPEM(rootPool.certs[0]))
t.Errorf("#%d: leaf:\n%s", i, certAsPEM(leafCert))
cmd.Stderr = &output
err := cmd.Run()
- return string(output.Bytes()), err
+ return output.String(), err
}
var rfc2821Tests = []struct {
import (
"bytes"
- "encoding/asn1"
"errors"
"fmt"
"net"
"net/url"
"reflect"
"runtime"
- "strconv"
"strings"
"time"
"unicode/utf8"
NameMismatch
// NameConstraintsWithoutSANs results when a leaf certificate doesn't
// contain a Subject Alternative Name extension, but a CA certificate
- // contains name constraints.
+ // contains name constraints, and the Common Name can be interpreted as
+ // a hostname.
NameConstraintsWithoutSANs
// UnconstrainedName results when a CA certificate contains permitted
// name constraints, but leaf certificate contains a name of an
func (h HostnameError) Error() string {
c := h.Certificate
+ if !c.hasSANExtension() && !validHostname(c.Subject.CommonName) &&
+ matchHostnames(toLowerCaseASCII(c.Subject.CommonName), toLowerCaseASCII(h.Host)) {
+ // This would have validated, if it weren't for the validHostname check on Common Name.
+ return "x509: Common Name is not a valid hostname: " + c.Subject.CommonName
+ }
+
var valid string
if ip := net.ParseIP(h.Host); ip != nil {
// Trying to validate an IP
valid += san.String()
}
} else {
- if c.hasSANExtension() {
- valid = strings.Join(c.DNSNames, ", ")
- } else {
+ if c.commonNameAsHostname() {
valid = c.Subject.CommonName
+ } else {
+ valid = strings.Join(c.DNSNames, ", ")
}
}
leaf = currentChain[0]
}
- if (certType == intermediateCertificate || certType == rootCertificate) && c.hasNameConstraints() {
- sanExtension, ok := leaf.getSANExtension()
- if !ok {
- // This is the deprecated, legacy case of depending on
- // the CN as a hostname. Chains modern enough to be
- // using name constraints should not be depending on
- // CNs.
- return CertificateInvalidError{c, NameConstraintsWithoutSANs, ""}
- }
-
- err := forEachSAN(sanExtension, func(tag int, data []byte) error {
+ checkNameConstraints := (certType == intermediateCertificate || certType == rootCertificate) && c.hasNameConstraints()
+ if checkNameConstraints && leaf.commonNameAsHostname() {
+ // This is the deprecated, legacy case of depending on the commonName as
+ // a hostname. We don't enforce name constraints against the CN, but
+ // VerifyHostname will look for hostnames in there if there are no SANs.
+ // In order to ensure VerifyHostname will not accept an unchecked name,
+ // return an error here.
+ return CertificateInvalidError{c, NameConstraintsWithoutSANs, ""}
+ } else if checkNameConstraints && leaf.hasSANExtension() {
+ err := forEachSAN(leaf.getSANExtension(), func(tag int, data []byte) error {
switch tag {
case nameTypeEmail:
name := string(data)
return nil
}
-// formatOID formats an ASN.1 OBJECT IDENTIFER in the common, dotted style.
-func formatOID(oid asn1.ObjectIdentifier) string {
- ret := ""
- for i, v := range oid {
- if i > 0 {
- ret += "."
- }
- ret += strconv.Itoa(v)
- }
- return ret
-}
-
// Verify attempts to verify c by building one or more chains from c to a
// certificate in opts.Roots, using certificates in opts.Intermediates if
// needed. If successful, it returns one or more chains where the first
return
}
+// validHostname returns whether host is a valid hostname that can be matched or
+// matched against according to RFC 6125 2.2, with some leniency to accomodate
+// legacy values.
+func validHostname(host string) bool {
+ host = strings.TrimSuffix(host, ".")
+
+ if len(host) == 0 {
+ return false
+ }
+
+ for i, part := range strings.Split(host, ".") {
+ if part == "" {
+ // Empty label.
+ return false
+ }
+ if 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 == '_' {
+ // _ is not a valid character in hostnames, but it's commonly
+ // found in deployments outside the WebPKI.
+ continue
+ }
+ return false
+ }
+ }
+
+ return true
+}
+
+// commonNameAsHostname reports whether the Common Name field should be
+// considered the hostname that the certificate is valid for. This is a legacy
+// behavior, disabled if the Subject Alt Name extension is present.
+//
+// It applies the strict validHostname check to the Common Name field, so that
+// certificates without SANs can still be validated against CAs with name
+// constraints if there is no risk the CN would be matched as a hostname.
+// See NameConstraintsWithoutSANs and issue 24151.
+func (c *Certificate) commonNameAsHostname() bool {
+ return !c.hasSANExtension() && validHostname(c.Subject.CommonName)
+}
+
func matchHostnames(pattern, host string) bool {
host = strings.TrimSuffix(host, ".")
pattern = strings.TrimSuffix(pattern, ".")
lowered := toLowerCaseASCII(h)
- if c.hasSANExtension() {
+ if c.commonNameAsHostname() {
+ if matchHostnames(toLowerCaseASCII(c.Subject.CommonName), lowered) {
+ return nil
+ }
+ } else {
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 HostnameError{c, h}
currentTime: 1395785200,
dnsName: "www.example.com",
- errorCallback: expectHostnameError,
+ errorCallback: expectHostnameError("certificate is valid for"),
+ },
+ {
+ leaf: googleLeaf,
+ intermediates: []string{giag2Intermediate},
+ roots: []string{geoTrustRoot},
+ currentTime: 1395785200,
+ dnsName: "1.2.3.4",
+
+ errorCallback: expectHostnameError("doesn't contain any IP SANs"),
},
{
leaf: googleLeaf,
dnsName: "notfoo.example",
systemSkip: true,
- errorCallback: expectHostnameError,
+ errorCallback: expectHostnameError("certificate is valid for"),
},
{
// The issuer name in the leaf doesn't exactly match the
currentTime: 1486684488,
systemSkip: true,
- errorCallback: expectHostnameError,
+ errorCallback: expectHostnameError("certificate is not valid for any names"),
},
{
// Test that excluded names are respected.
errorCallback: expectUnhandledCriticalExtension,
},
+ {
+ // Test that invalid CN are ignored.
+ leaf: invalidCNWithoutSAN,
+ dnsName: "foo,invalid",
+ roots: []string{invalidCNRoot},
+ currentTime: 1540000000,
+ systemSkip: true,
+
+ errorCallback: expectHostnameError("Common Name is not a valid hostname"),
+ },
+ {
+ // Test that valid CN are respected.
+ leaf: validCNWithoutSAN,
+ dnsName: "foo.example.com",
+ roots: []string{invalidCNRoot},
+ currentTime: 1540000000,
+ systemSkip: true,
+
+ expectedChains: [][]string{
+ {"foo.example.com", "Test root"},
+ },
+ },
}
-func expectHostnameError(t *testing.T, i int, err error) (ok bool) {
- if _, ok := err.(HostnameError); !ok {
- t.Errorf("#%d: error was not a HostnameError: %s", i, err)
- return false
+func expectHostnameError(msg string) func(*testing.T, int, error) bool {
+ return func(t *testing.T, i int, err error) (ok bool) {
+ if _, ok := err.(HostnameError); !ok {
+ t.Errorf("#%d: error was not a HostnameError: %v", i, err)
+ return false
+ }
+ if !strings.Contains(err.Error(), msg) {
+ t.Errorf("#%d: HostnameError did not contain %q: %v", i, msg, err)
+ }
+ return true
}
- return true
}
func expectExpired(t *testing.T, i int, err error) (ok bool) {
if inval, ok := err.(CertificateInvalidError); !ok || inval.Reason != Expired {
- t.Errorf("#%d: error was not Expired: %s", i, err)
+ t.Errorf("#%d: error was not Expired: %v", i, err)
return false
}
return true
func expectUsageError(t *testing.T, i int, err error) (ok bool) {
if inval, ok := err.(CertificateInvalidError); !ok || inval.Reason != IncompatibleUsage {
- t.Errorf("#%d: error was not IncompatibleUsage: %s", i, err)
+ t.Errorf("#%d: error was not IncompatibleUsage: %v", i, err)
return false
}
return true
func expectAuthorityUnknown(t *testing.T, i int, err error) (ok bool) {
e, ok := err.(UnknownAuthorityError)
if !ok {
- t.Errorf("#%d: error was not UnknownAuthorityError: %s", i, err)
+ t.Errorf("#%d: error was not UnknownAuthorityError: %v", i, err)
return false
}
if e.Cert == nil {
- t.Errorf("#%d: error was UnknownAuthorityError, but missing Cert: %s", i, err)
+ t.Errorf("#%d: error was UnknownAuthorityError, but missing Cert: %v", i, err)
return false
}
return true
func expectSystemRootsError(t *testing.T, i int, err error) bool {
if _, ok := err.(SystemRootsError); !ok {
- t.Errorf("#%d: error was not SystemRootsError: %s", i, err)
+ t.Errorf("#%d: error was not SystemRootsError: %v", i, err)
return false
}
return true
return false
}
if expected := "algorithm unimplemented"; !strings.Contains(err.Error(), expected) {
- t.Errorf("#%d: error resulting from invalid hash didn't contain '%s', rather it was: %s", i, expected, err)
+ t.Errorf("#%d: error resulting from invalid hash didn't contain '%s', rather it was: %v", i, expected, err)
return false
}
return true
func expectSubjectIssuerMismatcthError(t *testing.T, i int, err error) (ok bool) {
if inval, ok := err.(CertificateInvalidError); !ok || inval.Reason != NameMismatch {
- t.Errorf("#%d: error was not a NameMismatch: %s", i, err)
+ t.Errorf("#%d: error was not a NameMismatch: %v", i, err)
return false
}
return true
func expectNameConstraintsError(t *testing.T, i int, err error) (ok bool) {
if inval, ok := err.(CertificateInvalidError); !ok || inval.Reason != CANotAuthorizedForThisName {
- t.Errorf("#%d: error was not a CANotAuthorizedForThisName: %s", i, err)
+ t.Errorf("#%d: error was not a CANotAuthorizedForThisName: %v", i, err)
return false
}
return true
func expectNotAuthorizedError(t *testing.T, i int, err error) (ok bool) {
if inval, ok := err.(CertificateInvalidError); !ok || inval.Reason != NotAuthorizedToSign {
- t.Errorf("#%d: error was not a NotAuthorizedToSign: %s", i, err)
+ t.Errorf("#%d: error was not a NotAuthorizedToSign: %v", i, err)
return false
}
return true
func expectUnhandledCriticalExtension(t *testing.T, i int, err error) (ok bool) {
if _, ok := err.(UnhandledCriticalExtension); !ok {
- t.Errorf("#%d: error was not an UnhandledCriticalExtension: %s", i, err)
+ t.Errorf("#%d: error was not an UnhandledCriticalExtension: %v", i, err)
return false
}
return true
leaf, err := certificateFromPEM(test.leaf)
if err != nil {
- t.Errorf("#%d: failed to parse leaf: %s", i, err)
+ t.Errorf("#%d: failed to parse leaf: %v", i, err)
return
}
}
if test.errorCallback == nil && err != nil {
- t.Errorf("#%d: unexpected error: %s", i, err)
+ t.Errorf("#%d: unexpected error: %v", i, err)
}
if test.errorCallback != nil {
if !test.errorCallback(t, i, err) {
+NQCZDd5eFeU8PpNX7rgaYE4GPq+EEmLVCBYmdctr8QVdqJ//8Xu3+1phjDy
-----END CERTIFICATE-----`
+const invalidCNRoot = `
+-----BEGIN CERTIFICATE-----
+MIIBFjCBvgIJAIsu4r+jb70UMAoGCCqGSM49BAMCMBQxEjAQBgNVBAsMCVRlc3Qg
+cm9vdDAeFw0xODA3MTExODMyMzVaFw0yODA3MDgxODMyMzVaMBQxEjAQBgNVBAsM
+CVRlc3Qgcm9vdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABF6oDgMg0LV6YhPj
+QXaPXYCc2cIyCdqp0ROUksRz0pOLTc5iY2nraUheRUD1vRRneq7GeXOVNn7uXONg
+oCGMjNwwCgYIKoZIzj0EAwIDRwAwRAIgDSiwgIn8g1lpruYH0QD1GYeoWVunfmrI
+XzZZl0eW/ugCICgOfXeZ2GGy3wIC0352BaC3a8r5AAb2XSGNe+e9wNN6
+-----END CERTIFICATE-----
+`
+
+const invalidCNWithoutSAN = `
+Certificate:
+ Data:
+ Version: 1 (0x0)
+ Serial Number:
+ 07:ba:bc:b7:d9:ab:0c:02:fe:50:1d:4e:15:a3:0d:e4:11:16:14:a2
+ Signature Algorithm: ecdsa-with-SHA256
+ Issuer: OU = Test root
+ Validity
+ Not Before: Jul 11 18:35:21 2018 GMT
+ Not After : Jul 8 18:35:21 2028 GMT
+ Subject: CN = "foo,invalid"
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (256 bit)
+ pub:
+ 04:a7:a6:7c:22:33:a7:47:7f:08:93:2d:5f:61:35:
+ 2e:da:45:67:76:f2:97:73:18:b0:01:12:4a:1a:d5:
+ b7:6f:41:3c:bb:05:69:f4:06:5d:ff:eb:2b:a7:85:
+ 0b:4c:f7:45:4e:81:40:7a:a9:c6:1d:bb:ba:d9:b9:
+ 26:b3:ca:50:90
+ ASN1 OID: prime256v1
+ NIST CURVE: P-256
+ Signature Algorithm: ecdsa-with-SHA256
+ 30:45:02:21:00:85:96:75:b6:72:3c:67:12:a0:7f:86:04:81:
+ d2:dd:c8:67:50:d7:5f:85:c0:54:54:fc:e6:6b:45:08:93:d3:
+ 2a:02:20:60:86:3e:d6:28:a6:4e:da:dd:6e:95:89:cc:00:76:
+ 78:1c:03:80:85:a6:5a:0b:eb:c5:f3:9c:2e:df:ef:6e:fa
+-----BEGIN CERTIFICATE-----
+MIIBJDCBywIUB7q8t9mrDAL+UB1OFaMN5BEWFKIwCgYIKoZIzj0EAwIwFDESMBAG
+A1UECwwJVGVzdCByb290MB4XDTE4MDcxMTE4MzUyMVoXDTI4MDcwODE4MzUyMVow
+FjEUMBIGA1UEAwwLZm9vLGludmFsaWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AASnpnwiM6dHfwiTLV9hNS7aRWd28pdzGLABEkoa1bdvQTy7BWn0Bl3/6yunhQtM
+90VOgUB6qcYdu7rZuSazylCQMAoGCCqGSM49BAMCA0gAMEUCIQCFlnW2cjxnEqB/
+hgSB0t3IZ1DXX4XAVFT85mtFCJPTKgIgYIY+1iimTtrdbpWJzAB2eBwDgIWmWgvr
+xfOcLt/vbvo=
+-----END CERTIFICATE-----
+`
+
+const validCNWithoutSAN = `
+Certificate:
+ Data:
+ Version: 1 (0x0)
+ Serial Number:
+ 07:ba:bc:b7:d9:ab:0c:02:fe:50:1d:4e:15:a3:0d:e4:11:16:14:a4
+ Signature Algorithm: ecdsa-with-SHA256
+ Issuer: OU = Test root
+ Validity
+ Not Before: Jul 11 18:47:24 2018 GMT
+ Not After : Jul 8 18:47:24 2028 GMT
+ Subject: CN = foo.example.com
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (256 bit)
+ pub:
+ 04:a7:a6:7c:22:33:a7:47:7f:08:93:2d:5f:61:35:
+ 2e:da:45:67:76:f2:97:73:18:b0:01:12:4a:1a:d5:
+ b7:6f:41:3c:bb:05:69:f4:06:5d:ff:eb:2b:a7:85:
+ 0b:4c:f7:45:4e:81:40:7a:a9:c6:1d:bb:ba:d9:b9:
+ 26:b3:ca:50:90
+ ASN1 OID: prime256v1
+ NIST CURVE: P-256
+ Signature Algorithm: ecdsa-with-SHA256
+ 30:44:02:20:53:6c:d7:b7:59:61:51:72:a5:18:a3:4b:0d:52:
+ ea:15:fa:d0:93:30:32:54:4b:ed:0f:58:85:b8:a8:1a:82:3b:
+ 02:20:14:77:4b:0e:7e:4f:0a:4f:64:26:97:dc:d0:ed:aa:67:
+ 1d:37:85:da:b4:87:ba:25:1c:2a:58:f7:23:11:8b:3d
+-----BEGIN CERTIFICATE-----
+MIIBJzCBzwIUB7q8t9mrDAL+UB1OFaMN5BEWFKQwCgYIKoZIzj0EAwIwFDESMBAG
+A1UECwwJVGVzdCByb290MB4XDTE4MDcxMTE4NDcyNFoXDTI4MDcwODE4NDcyNFow
+GjEYMBYGA1UEAwwPZm9vLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D
+AQcDQgAEp6Z8IjOnR38Iky1fYTUu2kVndvKXcxiwARJKGtW3b0E8uwVp9AZd/+sr
+p4ULTPdFToFAeqnGHbu62bkms8pQkDAKBggqhkjOPQQDAgNHADBEAiBTbNe3WWFR
+cqUYo0sNUuoV+tCTMDJUS+0PWIW4qBqCOwIgFHdLDn5PCk9kJpfc0O2qZx03hdq0
+h7olHCpY9yMRiz0=
+-----END CERTIFICATE-----
+`
+
var unknownAuthorityErrorTests = []struct {
cert string
expected string
}
c, err := ParseCertificate(der.Bytes)
if err != nil {
- t.Errorf("#%d: Unable to parse certificate -> %s", i, err)
+ t.Errorf("#%d: Unable to parse certificate -> %v", i, err)
}
uae := &UnknownAuthorityError{
Cert: c,
CCqGSM49BAMCA0gAMEUCIQClA3d4tdrDu9Eb5ZBpgyC+fU1xTZB0dKQHz6M5fPZA
2AIgN96lM+CPGicwhN24uQI6flOsO3H0TJ5lNzBYLtnQtlc=
-----END CERTIFICATE-----`
+
+func TestValidHostname(t *testing.T) {
+ tests := []struct {
+ host string
+ want bool
+ }{
+ {"example.com", true},
+ {"eXample123-.com", true},
+ {"-eXample123-.com", false},
+ {"", false},
+ {".", false},
+ {"example..com", false},
+ {".example.com", false},
+ {"*.example.com", true},
+ {"*foo.example.com", false},
+ {"foo.*.example.com", false},
+ {"exa_mple.com", true},
+ {"foo,bar", false},
+ }
+ for _, tt := range tests {
+ if got := validHostname(tt.host); got != tt.want {
+ t.Errorf("validHostname(%q) = %v, want %v", tt.host, got, tt.want)
+ }
+ }
+}