]> Cypherpunks.ru repositories - gostls13.git/commitdiff
crypto/x509: ignore Common Name when it does not parse as a hostname
authorFilippo Valsorda <filippo@golang.org>
Wed, 11 Jul 2018 19:59:56 +0000 (15:59 -0400)
committerFilippo Valsorda <filippo@golang.org>
Mon, 16 Jul 2018 19:30:08 +0000 (19:30 +0000)
The Common Name is used as a hostname when there are no Subject
Alternative Names, but it is not restricted by name constraints. To
protect against a name constraints bypass, we used to require SANs for
constrained chains. See the NameConstraintsWithoutSANs error.

This change ignores the CN when it does not look like a hostname, so we
can avoid returning NameConstraintsWithoutSANs.

This makes it possible to validate certificates with non-hostname CN
against chains that use name constraints to disallow all names, like the
Estonian IDs.

Updates #24151

Change-Id: I798d797990720a01ad9b5a13336756cc472ebf44
Reviewed-on: https://go-review.googlesource.com/123355
Reviewed-by: Adam Langley <agl@golang.org>
src/crypto/x509/name_constraints_test.go
src/crypto/x509/verify.go
src/crypto/x509/verify_test.go
src/crypto/x509/x509.go

index 95d55fd7619b4f5ab3310b4fcfbd40e8e4179037..e356fc5cb9491df8f7a651b4adb88b6f35dd3817 100644 (file)
@@ -57,6 +57,7 @@ type constraintsSpec struct {
 type leafSpec struct {
        sans []string
        ekus []string
+       cn   string
 }
 
 var nameConstraintsTests = []nameConstraintsTest{
@@ -633,7 +634,7 @@ 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{
@@ -647,9 +648,9 @@ var nameConstraintsTests = []nameConstraintsTest{
                },
                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
@@ -1580,6 +1581,60 @@ var nameConstraintsTests = []nameConstraintsTest{
                        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) {
@@ -1625,9 +1680,8 @@ func makeConstraintsLeafCert(leaf leafSpec, key *ecdsa.PrivateKey, parent *Certi
        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),
@@ -1899,7 +1953,9 @@ func TestConstraintCases(t *testing.T) {
                        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)
@@ -1912,7 +1968,7 @@ func TestConstraintCases(t *testing.T) {
                                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
                                        }
@@ -1949,7 +2005,7 @@ func TestConstraintCases(t *testing.T) {
                        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))
@@ -2012,7 +2068,7 @@ func testChainAgainstOpenSSL(leaf *Certificate, intermediates, roots *CertPool)
        cmd.Stderr = &output
 
        err := cmd.Run()
-       return string(output.Bytes()), err
+       return output.String(), err
 }
 
 var rfc2821Tests = []struct {
index 1ef49c0f4a2b5f4992a09617f9ef795f7fcd78bc..4326e39f1c22b0d9ddbff496b528b782e992a75f 100644 (file)
@@ -6,14 +6,12 @@ package x509
 
 import (
        "bytes"
-       "encoding/asn1"
        "errors"
        "fmt"
        "net"
        "net/url"
        "reflect"
        "runtime"
-       "strconv"
        "strings"
        "time"
        "unicode/utf8"
@@ -43,7 +41,8 @@ const (
        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
@@ -102,6 +101,12 @@ type HostnameError struct {
 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
@@ -115,10 +120,10 @@ func (h HostnameError) Error() string {
                        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, ", ")
                }
        }
 
@@ -583,17 +588,16 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
                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)
@@ -692,18 +696,6 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
        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
@@ -860,6 +852,64 @@ nextIntermediate:
        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, ".")
@@ -940,15 +990,16 @@ func (c *Certificate) VerifyHostname(h string) error {
 
        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}
index 95034dbba33eb2a630ffbd58c87d6c054ba57db0..c677c03141365fd93cc3324c69dce2fe8187ac9d 100644 (file)
@@ -71,7 +71,16 @@ var verifyTests = []verifyTest{
                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,
@@ -248,7 +257,7 @@ var verifyTests = []verifyTest{
                dnsName:     "notfoo.example",
                systemSkip:  true,
 
-               errorCallback: expectHostnameError,
+               errorCallback: expectHostnameError("certificate is valid for"),
        },
        {
                // The issuer name in the leaf doesn't exactly match the
@@ -281,7 +290,7 @@ var verifyTests = []verifyTest{
                currentTime: 1486684488,
                systemSkip:  true,
 
-               errorCallback: expectHostnameError,
+               errorCallback: expectHostnameError("certificate is not valid for any names"),
        },
        {
                // Test that excluded names are respected.
@@ -318,19 +327,46 @@ var verifyTests = []verifyTest{
 
                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
@@ -338,7 +374,7 @@ func expectExpired(t *testing.T, i int, err error) (ok bool) {
 
 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
@@ -347,11 +383,11 @@ func expectUsageError(t *testing.T, i int, err error) (ok bool) {
 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
@@ -359,7 +395,7 @@ func expectAuthorityUnknown(t *testing.T, i int, err error) (ok bool) {
 
 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
@@ -371,7 +407,7 @@ func expectHashError(t *testing.T, i int, err error) bool {
                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
@@ -379,7 +415,7 @@ func expectHashError(t *testing.T, i int, err error) bool {
 
 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
@@ -387,7 +423,7 @@ func expectSubjectIssuerMismatcthError(t *testing.T, i int, err error) (ok bool)
 
 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
@@ -395,7 +431,7 @@ func expectNameConstraintsError(t *testing.T, i int, err error) (ok bool) {
 
 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
@@ -403,7 +439,7 @@ func expectNotAuthorizedError(t *testing.T, i int, err error) (ok bool) {
 
 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
@@ -454,7 +490,7 @@ func testVerify(t *testing.T, useSystemRoots bool) {
 
                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
                }
 
@@ -472,7 +508,7 @@ func testVerify(t *testing.T, useSystemRoots bool) {
                }
 
                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) {
@@ -1513,6 +1549,95 @@ yU1yRHUqUYpN0DWFpsPbBqgM6uUAVO2ayBFhPgWUaqkmSbZ/Nq7isGvknaTmcIwT
 +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
@@ -1530,7 +1655,7 @@ func TestUnknownAuthorityError(t *testing.T) {
                }
                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,
@@ -1702,3 +1827,28 @@ UNhY4JhezH9gQYqvDMWrWDAbBgNVHSMEFDASgBArF29S5Bnqw7de8GzGA1nfMAoG
 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)
+               }
+       }
+}
index 34eb8b0105ee40b40df4d25206d309bf17b64d64..2e72471de287885e478a8b9d24aeebda507e3fcb 100644 (file)
@@ -843,23 +843,16 @@ func (c *Certificate) CheckSignature(algo SignatureAlgorithm, signed, signature
 }
 
 func (c *Certificate) hasNameConstraints() bool {
-       for _, e := range c.Extensions {
-               if len(e.Id) == 4 && e.Id[0] == 2 && e.Id[1] == 5 && e.Id[2] == 29 && e.Id[3] == 30 {
-                       return true
-               }
-       }
-
-       return false
+       return oidInExtensions(oidExtensionNameConstraints, c.Extensions)
 }
 
-func (c *Certificate) getSANExtension() ([]byte, bool) {
+func (c *Certificate) getSANExtension() []byte {
        for _, e := range c.Extensions {
-               if len(e.Id) == 4 && e.Id[0] == 2 && e.Id[1] == 5 && e.Id[2] == 29 && e.Id[3] == 17 {
-                       return e.Value, true
+               if e.Id.Equal(oidExtensionSubjectAltName) {
+                       return e.Value
                }
        }
-
-       return nil, false
+       return nil
 }
 
 func signaturePublicKeyAlgoMismatchError(expectedPubKeyAlgo PublicKeyAlgorithm, pubKey interface{}) error {