--- /dev/null
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package x509
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "net"
+ "net/url"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+)
+
+const (
+ // testNameConstraintsAgainstOpenSSL can be set to true to run tests
+ // against the system OpenSSL. This is disabled by default because Go
+ // cannot depend on having OpenSSL installed at testing time.
+ testNameConstraintsAgainstOpenSSL = false
+
+ // debugOpenSSLFailure can be set to true, when
+ // testNameConstraintsAgainstOpenSSL is also true, to cause
+ // intermediate files to be preserved for debugging.
+ debugOpenSSLFailure = false
+)
+
+type nameConstraintsTest struct {
+ roots []constraintsSpec
+ intermediates [][]constraintsSpec
+ leaf []string
+ expectedError string
+ noOpenSSL bool
+}
+
+type constraintsSpec struct {
+ ok []string
+ bad []string
+}
+
+var nameConstraintsTests = []nameConstraintsTest{
+ // #0: dummy test for the certificate generation process itself.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #1: dummy test for the certificate generation process itself: single
+ // level of intermediate.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #2: dummy test for the certificate generation process itself: two
+ // levels of intermediates.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #3: matching DNS constraint in root
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #4: matching DNS constraint in intermediate.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:example.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #5: .example.com only matches subdomains.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ expectedError: "\"example.com\" is not permitted",
+ },
+
+ // #6: .example.com matches subdomains.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.example.com"},
+ },
+
+ // #7: .example.com matches multiple levels of subdomains
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:foo.bar.example.com"},
+ },
+
+ // #8: specifying a permitted list of names does not exclude other name
+ // types
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:10.1.1.1"},
+ },
+
+ // #9: specifying a permitted list of names does not exclude other name
+ // types
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #10: intermediates can try to permit other names, which isn't
+ // forbidden if the leaf doesn't mention them. I.e. name constraints
+ // apply to names, not constraints themselves.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:example.com", "dns:foo.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #11: intermediates cannot add permitted names that the root doesn't
+ // grant them.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:example.com", "dns:foo.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.com"},
+ expectedError: "\"foo.com\" is not permitted",
+ },
+
+ // #12: intermediates can further limit their scope if they wish.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.bar.example.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.bar.example.com"},
+ },
+
+ // #13: intermediates can further limit their scope and that limitation
+ // is effective
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.bar.example.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.notbar.example.com"},
+ expectedError: "\"foo.notbar.example.com\" is not permitted",
+ },
+
+ // #14: roots can exclude subtrees and that doesn't affect other names.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:foo.com"},
+ },
+
+ // #15: roots exclusions are effective.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:foo.example.com"},
+ expectedError: "\"foo.example.com\" is excluded",
+ },
+
+ // #16: intermediates can also exclude names and that doesn't affect
+ // other names.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.com"},
+ },
+
+ // #17: intermediate exclusions are effective.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.example.com"},
+ expectedError: "\"foo.example.com\" is excluded",
+ },
+
+ // #18: having an exclusion doesn't prohibit other types of names.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:foo.com", "ip:10.1.1.1"},
+ },
+
+ // #19: IP-based exclusions are permitted and don't affect unrelated IP
+ // addresses.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:192.168.1.1"},
+ },
+
+ // #20: IP-based exclusions are effective
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:10.0.0.1"},
+ expectedError: "\"10.0.0.1\" is excluded",
+ },
+
+ // #21: intermediates can further constrain IP ranges.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"ip:0.0.0.0/1"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"ip:11.0.0.0/8"},
+ },
+ },
+ },
+ leaf: []string{"ip:11.0.0.1"},
+ expectedError: "\"11.0.0.1\" is excluded",
+ },
+
+ // #22: when multiple intermediates are present, chain building can
+ // avoid intermediates with incompatible constraints.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.foo.com"},
+ },
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.example.com"},
+ noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
+ },
+
+ // #23: (same as the previous test, but in the other order in ensure
+ // that we don't pass it by luck.)
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:.example.com"},
+ },
+ constraintsSpec{
+ ok: []string{"dns:.foo.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:foo.example.com"},
+ noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
+ },
+
+ // #24: when multiple roots are valid, chain building can avoid roots
+ // with incompatible constraints.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
+ },
+
+ // #25: (same as the previous test, but in the other order in ensure
+ // that we don't pass it by luck.)
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
+ },
+
+ // #26: chain building can find a valid path even with multiple levels
+ // of alternative intermediates and alternative roots.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ constraintsSpec{
+ ok: []string{"dns:example.com"},
+ },
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ []constraintsSpec{
+ constraintsSpec{},
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:bar.com"},
+ noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
+ },
+
+ // #27: chain building doesn't get stuck when there is no valid path.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ constraintsSpec{
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:bar.com"},
+ },
+ constraintsSpec{
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ },
+ leaf: []string{"dns:bar.com"},
+ expectedError: "\"bar.com\" is not permitted",
+ },
+
+ // #28: unknown name types don't cause a problem without constraints.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{},
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"unknown:"},
+ },
+
+ // #29: unknown name types are allowed even in constrained chains.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"unknown:"},
+ },
+
+ // #30: without SANs, a certificate is rejected in a constrained chain.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{},
+ 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
+ // expected.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:2000:abcd:1234::"},
+ },
+
+ // #32: IPv6 addresses work in constraints: root restrictions are
+ // effective.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:2000:1234:abcd::"},
+ expectedError: "\"2000:1234:abcd::\" is not permitted",
+ },
+
+ // #33: An IPv6 permitted subtree doesn't affect DNS names.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:2000:abcd::", "dns:foo.com"},
+ },
+
+ // #34: IPv6 exclusions don't affect unrelated addresses.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:2000:1234::"},
+ },
+
+ // #35: IPv6 exclusions are effective.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:2000:abcd::"},
+ expectedError: "\"2000:abcd::\" is excluded",
+ },
+
+ // #36: IPv6 constraints do not permit IPv4 addresses.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:10.0.0.1"},
+ expectedError: "\"10.0.0.1\" is not permitted",
+ },
+
+ // #37: IPv4 constraints do not permit IPv6 addresses.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:2000:abcd::"},
+ expectedError: "\"2000:abcd::\" is not permitted",
+ },
+
+ // #38: an exclusion of an unknown type doesn't affect other names.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"unknown:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #39: a permitted subtree of an unknown type doesn't affect other
+ // name types.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"unknown:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #40: exact email constraints work
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:foo@example.com"},
+ },
+
+ // #41: exact email constraints are effective
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:bar@example.com"},
+ expectedError: "\"bar@example.com\" is not permitted",
+ },
+
+ // #42: email canonicalisation works.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:\"\\f\\o\\o\"@example.com"},
+ noOpenSSL: true, // OpenSSL doesn't canonicalise email addresses before matching
+ },
+
+ // #43: limiting email addresses to a host works.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:foo@example.com"},
+ },
+
+ // #44: a leading dot matches hosts one level deep
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:foo@sub.example.com"},
+ },
+
+ // #45: a leading dot does not match the host itself
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:foo@example.com"},
+ expectedError: "\"foo@example.com\" is not permitted",
+ },
+
+ // #46: a leading dot also matches two (or more) levels deep.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:foo@sub.sub.example.com"},
+ },
+
+ // #47: the local part of an email is case-sensitive
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:Foo@example.com"},
+ expectedError: "\"Foo@example.com\" is not permitted",
+ },
+
+ // #48: the domain part of an email is not case-sensitive
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"email:foo@EXAMPLE.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"email:foo@example.com"},
+ },
+
+ // #49: the domain part of a DNS constraint is also not case-sensitive.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"dns:EXAMPLE.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"dns:example.com"},
+ },
+
+ // #50: URI constraints only cover the host part of the URI
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{
+ "uri:http://example.com/bar",
+ "uri:http://example.com:8080/",
+ "uri:https://example.com/wibble#bar",
+ },
+ },
+
+ // #51: URIs with IPs are rejected
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:http://1.2.3.4/"},
+ expectedError: "URI with IP",
+ },
+
+ // #52: URIs with IPs and ports are rejected
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:http://1.2.3.4:43/"},
+ expectedError: "URI with IP",
+ },
+
+ // #53: URIs with IPv6 addresses are also rejected
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:http://[2006:abcd::1]/"},
+ expectedError: "URI with IP",
+ },
+
+ // #54: URIs with IPv6 addresses with ports are also rejected
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:http://[2006:abcd::1]:16/"},
+ expectedError: "URI with IP",
+ },
+
+ // #55: URI constraints are effective
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:http://bar.com/"},
+ expectedError: "\"http://bar.com/\" is not permitted",
+ },
+
+ // #56: URI constraints are effective
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"uri:foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:http://foo.com/"},
+ expectedError: "\"http://foo.com/\" is excluded",
+ },
+
+ // #57: URI constraints can allow subdomains
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:http://www.foo.com/"},
+ },
+
+ // #58: excluding an IPv4-mapped-IPv6 address doesn't affect the IPv4
+ // version of that address.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ bad: []string{"ip:::ffff:1.2.3.4/128"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:1.2.3.4"},
+ },
+
+ // #59: a URI constraint isn't matched by a URN.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"uri:urn:example"},
+ expectedError: "URI with empty host",
+ },
+
+ // #60: excluding all IPv6 addresses doesn't exclude all IPv4 addresses
+ // too, even though IPv4 is mapped into the IPv6 range.
+ nameConstraintsTest{
+ roots: []constraintsSpec{
+ constraintsSpec{
+ ok: []string{"ip:1.2.3.0/24"},
+ bad: []string{"ip:::0/0"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ []constraintsSpec{
+ constraintsSpec{},
+ },
+ },
+ leaf: []string{"ip:1.2.3.4"},
+ },
+
+ // TODO(agl): handle empty name constraints. Currently this doesn't
+ // work because empty values are treated as missing.
+}
+
+func makeConstraintsCACert(constraints constraintsSpec, name string, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {
+ var serialBytes [16]byte
+ rand.Read(serialBytes[:])
+
+ template := &Certificate{
+ SerialNumber: new(big.Int).SetBytes(serialBytes[:]),
+ Subject: pkix.Name{
+ CommonName: name,
+ },
+ NotBefore: time.Unix(1000, 0),
+ NotAfter: time.Unix(2000, 0),
+ KeyUsage: KeyUsageCertSign,
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+
+ if err := addConstraintsToTemplate(constraints, template); err != nil {
+ return nil, err
+ }
+
+ if parent == nil {
+ parent = template
+ }
+ derBytes, err := CreateCertificate(rand.Reader, template, parent, &key.PublicKey, parentKey)
+ if err != nil {
+ return nil, err
+ }
+
+ caCert, err := ParseCertificate(derBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ return caCert, nil
+}
+
+func makeConstraintsLeafCert(sans []string, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {
+ var serialBytes [16]byte
+ rand.Read(serialBytes[:])
+
+ 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"},
+ },
+ NotBefore: time.Unix(1000, 0),
+ NotAfter: time.Unix(2000, 0),
+ KeyUsage: KeyUsageDigitalSignature,
+ BasicConstraintsValid: true,
+ IsCA: false,
+ }
+
+ for _, name := range sans {
+ switch {
+ case strings.HasPrefix(name, "dns:"):
+ template.DNSNames = append(template.DNSNames, name[4:])
+
+ case strings.HasPrefix(name, "ip:"):
+ ip := net.ParseIP(name[3:])
+ if ip == nil {
+ return nil, fmt.Errorf("cannot parse IP %q", name[3:])
+ }
+ template.IPAddresses = append(template.IPAddresses, ip)
+
+ case strings.HasPrefix(name, "email:"):
+ template.EmailAddresses = append(template.EmailAddresses, name[6:])
+
+ case strings.HasPrefix(name, "uri:"):
+ uri, err := url.Parse(name[4:])
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse URI %q: %s", name[4:], err)
+ }
+ template.URIs = append(template.URIs, uri)
+
+ case strings.HasPrefix(name, "unknown:"):
+ // This is a special case for testing unknown
+ // name types. A custom SAN extension is
+ // injected into the certificate.
+ if len(sans) != 1 {
+ panic("when using unknown name types, it must be the sole name")
+ }
+
+ template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
+ Id: []int{2, 5, 29, 17},
+ Value: []byte{
+ 0x30, // SEQUENCE
+ 3, // three bytes
+ 9, // undefined GeneralName type 9
+ 1,
+ 1,
+ },
+ })
+
+ default:
+ return nil, fmt.Errorf("unknown name type %q", name)
+ }
+ }
+
+ if parent == nil {
+ parent = template
+ }
+
+ derBytes, err := CreateCertificate(rand.Reader, template, parent, &key.PublicKey, parentKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return ParseCertificate(derBytes)
+}
+
+func customConstraintsExtension(typeNum int, constraint []byte, isExcluded bool) pkix.Extension {
+ appendConstraint := func(contents []byte, tag uint8) []byte {
+ contents = append(contents, tag|32 /* constructed */ |0x80 /* context-specific */)
+ contents = append(contents, byte(4+len(constraint)) /* length */)
+ contents = append(contents, 0x30 /* SEQUENCE */)
+ contents = append(contents, byte(2+len(constraint)) /* length */)
+ contents = append(contents, byte(typeNum) /* GeneralName type */)
+ contents = append(contents, byte(len(constraint)))
+ return append(contents, constraint...)
+ }
+
+ var contents []byte
+ if !isExcluded {
+ contents = appendConstraint(contents, 0 /* tag 0 for permitted */)
+ } else {
+ contents = appendConstraint(contents, 1 /* tag 1 for excluded */)
+ }
+
+ var value []byte
+ value = append(value, 0x30 /* SEQUENCE */)
+ value = append(value, byte(len(contents)))
+ value = append(value, contents...)
+
+ return pkix.Extension{
+ Id: []int{2, 5, 29, 30},
+ Value: value,
+ }
+}
+
+func addConstraintsToTemplate(constraints constraintsSpec, template *Certificate) error {
+ parse := func(constraints []string) (dnsNames []string, ips []*net.IPNet, emailAddrs []string, uriDomains []string, err error) {
+ for _, constraint := range constraints {
+ switch {
+ case strings.HasPrefix(constraint, "dns:"):
+ dnsNames = append(dnsNames, constraint[4:])
+
+ case strings.HasPrefix(constraint, "ip:"):
+ _, ipNet, err := net.ParseCIDR(constraint[3:])
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ ips = append(ips, ipNet)
+
+ case strings.HasPrefix(constraint, "email:"):
+ emailAddrs = append(emailAddrs, constraint[6:])
+
+ case strings.HasPrefix(constraint, "uri:"):
+ uriDomains = append(uriDomains, constraint[4:])
+
+ default:
+ return nil, nil, nil, nil, fmt.Errorf("unknown constraint %q", constraint)
+ }
+ }
+
+ return dnsNames, ips, emailAddrs, uriDomains, err
+ }
+
+ handleSpecialConstraint := func(constraint string, isExcluded bool) bool {
+ switch {
+ case constraint == "unknown:":
+ template.ExtraExtensions = append(template.ExtraExtensions, customConstraintsExtension(9 /* undefined GeneralName type */, []byte{1}, isExcluded))
+
+ default:
+ return false
+ }
+
+ return true
+ }
+
+ if len(constraints.ok) == 1 && len(constraints.bad) == 0 {
+ if handleSpecialConstraint(constraints.ok[0], false) {
+ return nil
+ }
+ }
+
+ if len(constraints.bad) == 1 && len(constraints.ok) == 0 {
+ if handleSpecialConstraint(constraints.bad[0], true) {
+ return nil
+ }
+ }
+
+ var err error
+ template.PermittedDNSDomains, template.PermittedIPRanges, template.PermittedEmailAddresses, template.PermittedURIDomains, err = parse(constraints.ok)
+ if err != nil {
+ return err
+ }
+
+ template.ExcludedDNSDomains, template.ExcludedIPRanges, template.ExcludedEmailAddresses, template.ExcludedURIDomains, err = parse(constraints.bad)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func TestNameConstraintCases(t *testing.T) {
+ privateKeys := sync.Pool{
+ New: func() interface{} {
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+ return priv
+ },
+ }
+
+ for i, test := range nameConstraintsTests {
+ rootPool := NewCertPool()
+ rootKey := privateKeys.Get().(*ecdsa.PrivateKey)
+ rootName := "Root " + strconv.Itoa(i)
+
+ // keys keeps track of all the private keys used in a given
+ // test and puts them back in the privateKeys pool at the end.
+ keys := []*ecdsa.PrivateKey{rootKey}
+
+ // At each level (root, intermediate(s), leaf), parent points to
+ // an example parent certificate and parentKey the key for the
+ // parent level. Since all certificates at a given level have
+ // the same name and public key, any parent certificate is
+ // sufficient to get the correct issuer name and authority
+ // key ID.
+ var parent *Certificate
+ parentKey := rootKey
+
+ for _, root := range test.roots {
+ rootCert, err := makeConstraintsCACert(root, rootName, rootKey, nil, rootKey)
+ if err != nil {
+ t.Fatalf("#%d: failed to create root: %s", i, err)
+ }
+
+ parent = rootCert
+ rootPool.AddCert(rootCert)
+ }
+
+ intermediatePool := NewCertPool()
+
+ for level, intermediates := range test.intermediates {
+ levelKey := privateKeys.Get().(*ecdsa.PrivateKey)
+ keys = append(keys, levelKey)
+ levelName := "Intermediate level " + strconv.Itoa(level)
+ var last *Certificate
+
+ for _, intermediate := range intermediates {
+ caCert, err := makeConstraintsCACert(intermediate, levelName, levelKey, parent, parentKey)
+ if err != nil {
+ t.Fatalf("#%d: failed to create %q: %s", i, levelName, err)
+ }
+
+ last = caCert
+ intermediatePool.AddCert(caCert)
+ }
+
+ parent = last
+ parentKey = levelKey
+ }
+
+ leafKey := privateKeys.Get().(*ecdsa.PrivateKey)
+ keys = append(keys, leafKey)
+
+ leafCert, err := makeConstraintsLeafCert(test.leaf, leafKey, parent, parentKey)
+ if err != nil {
+ t.Fatalf("#%d: cannot create leaf: %s", i, err)
+ }
+
+ if !test.noOpenSSL && testNameConstraintsAgainstOpenSSL {
+ output, err := testChainAgainstOpenSSL(leafCert, intermediatePool, rootPool)
+ if err == nil && len(test.expectedError) > 0 {
+ t.Errorf("#%d: unexpectedly succeeded against OpenSSL", i)
+ if debugOpenSSLFailure {
+ return
+ }
+ }
+
+ if err != nil {
+ 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)
+ if debugOpenSSLFailure {
+ return
+ }
+ }
+ }
+ }
+
+ verifyOpts := VerifyOptions{
+ Roots: rootPool,
+ Intermediates: intermediatePool,
+ CurrentTime: time.Unix(1500, 0),
+ }
+ _, err = leafCert.Verify(verifyOpts)
+
+ logInfo := true
+ if len(test.expectedError) == 0 {
+ if err != nil {
+ t.Errorf("#%d: unexpected failure: %s", i, err)
+ } else {
+ logInfo = false
+ }
+ } else {
+ if err == nil {
+ t.Errorf("#%d: unexpected success", i)
+ } else if !strings.Contains(err.Error(), test.expectedError) {
+ t.Errorf("#%d: expected error containing %q, but got: %s", i, test.expectedError, err)
+ } else {
+ logInfo = false
+ }
+ }
+
+ if logInfo {
+ certAsPEM := func(cert *Certificate) string {
+ var buf bytes.Buffer
+ pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
+ return string(buf.Bytes())
+ }
+ t.Errorf("#%d: root:\n%s", i, certAsPEM(rootPool.certs[0]))
+ t.Errorf("#%d: leaf:\n%s", i, certAsPEM(leafCert))
+ }
+
+ for _, key := range keys {
+ privateKeys.Put(key)
+ }
+ keys = keys[:0]
+ }
+}
+
+func writePEMsToTempFile(certs []*Certificate) *os.File {
+ file, err := ioutil.TempFile("", "name_constraints_test")
+ if err != nil {
+ panic("cannot create tempfile")
+ }
+
+ pemBlock := &pem.Block{Type: "CERTIFICATE"}
+ for _, cert := range certs {
+ pemBlock.Bytes = cert.Raw
+ pem.Encode(file, pemBlock)
+ }
+
+ return file
+}
+
+func testChainAgainstOpenSSL(leaf *Certificate, intermediates, roots *CertPool) (string, error) {
+ args := []string{"verify", "-no_check_time"}
+
+ rootsFile := writePEMsToTempFile(roots.certs)
+ if debugOpenSSLFailure {
+ println("roots file:", rootsFile.Name())
+ } else {
+ defer os.Remove(rootsFile.Name())
+ }
+ args = append(args, "-CAfile", rootsFile.Name())
+
+ if len(intermediates.certs) > 0 {
+ intermediatesFile := writePEMsToTempFile(intermediates.certs)
+ if debugOpenSSLFailure {
+ println("intermediates file:", intermediatesFile.Name())
+ } else {
+ defer os.Remove(intermediatesFile.Name())
+ }
+ args = append(args, "-untrusted", intermediatesFile.Name())
+ }
+
+ leafFile := writePEMsToTempFile([]*Certificate{leaf})
+ if debugOpenSSLFailure {
+ println("leaf file:", leafFile.Name())
+ } else {
+ defer os.Remove(leafFile.Name())
+ }
+ args = append(args, leafFile.Name())
+
+ var output bytes.Buffer
+ cmd := exec.Command("openssl", args...)
+ cmd.Stdout = &output
+ cmd.Stderr = &output
+
+ err := cmd.Run()
+ return string(output.Bytes()), err
+}
+
+var rfc2821Tests = []struct {
+ in string
+ localPart, domain string
+}{
+ {"foo@example.com", "foo", "example.com"},
+ {"@example.com", "", ""},
+ {"\"@example.com", "", ""},
+ {"\"\"@example.com", "", "example.com"},
+ {"\"a\"@example.com", "a", "example.com"},
+ {"\"\\a\"@example.com", "a", "example.com"},
+ {"a\"@example.com", "", ""},
+ {"foo..bar@example.com", "", ""},
+ {".foo.bar@example.com", "", ""},
+ {"foo.bar.@example.com", "", ""},
+ {"|{}?'@example.com", "|{}?'", "example.com"},
+
+ // Examples from RFC 3696
+ {"Abc\\@def@example.com", "Abc@def", "example.com"},
+ {"Fred\\ Bloggs@example.com", "Fred Bloggs", "example.com"},
+ {"Joe.\\\\Blow@example.com", "Joe.\\Blow", "example.com"},
+ {"\"Abc@def\"@example.com", "Abc@def", "example.com"},
+ {"\"Fred Bloggs\"@example.com", "Fred Bloggs", "example.com"},
+ {"customer/department=shipping@example.com", "customer/department=shipping", "example.com"},
+ {"$A12345@example.com", "$A12345", "example.com"},
+ {"!def!xyz%abc@example.com", "!def!xyz%abc", "example.com"},
+ {"_somename@example.com", "_somename", "example.com"},
+}
+
+func TestRFC2821Parsing(t *testing.T) {
+ for i, test := range rfc2821Tests {
+ mailbox, ok := parseRFC2821Mailbox(test.in)
+ expectedFailure := len(test.localPart) == 0 && len(test.domain) == 0
+
+ if ok && expectedFailure {
+ t.Errorf("#%d: %q unexpectedly parsed as (%q, %q)", i, test.in, mailbox.local, mailbox.domain)
+ continue
+ }
+
+ if !ok && !expectedFailure {
+ t.Errorf("#%d: unexpected failure for %q", i, test.in)
+ continue
+ }
+
+ if !ok {
+ continue
+ }
+
+ if mailbox.local != test.localPart || mailbox.domain != test.domain {
+ t.Errorf("#%d: %q parsed as (%q, %q), but wanted (%q, %q)", i, test.in, mailbox.local, mailbox.domain, test.localPart, test.domain)
+ }
+ }
+}
+
+func TestBadNamesInConstraints(t *testing.T) {
+ // Bad names in constraints should not parse.
+ badNames := []string{
+ "dns:foo.com.",
+ "email:abc@foo.com.",
+ "email:foo.com.",
+ "uri:example.com.",
+ "uri:1.2.3.4",
+ "uri:ffff::1",
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, badName := range badNames {
+ _, err := makeConstraintsCACert(constraintsSpec{
+ ok: []string{badName},
+ }, "TestAbsoluteNamesInConstraints", priv, nil, priv)
+
+ if err == nil {
+ t.Errorf("bad name %q unexpectedly accepted in name constraint", badName)
+ continue
+ }
+
+ if err != nil {
+ if str := err.Error(); !strings.Contains(str, "failed to parse ") && !strings.Contains(str, "constraint") {
+ t.Errorf("bad name %q triggered unrecognised error: %s", badName, str)
+ }
+ }
+ }
+}
+
+func TestBadNamesInSANs(t *testing.T) {
+ // Bad names in SANs should not parse.
+ badNames := []string{
+ "dns:foo.com.",
+ "email:abc@foo.com.",
+ "email:foo.com.",
+ "uri:https://example.com./dsf",
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, badName := range badNames {
+ _, err := makeConstraintsLeafCert([]string{badName}, priv, nil, priv)
+
+ if err == nil {
+ t.Errorf("bad name %q unexpectedly accepted in SAN", badName)
+ continue
+ }
+
+ if err != nil {
+ if str := err.Error(); !strings.Contains(str, "cannot parse ") {
+ t.Errorf("bad name %q triggered unrecognised error: %s", badName, str)
+ }
+ }
+ }
+}
status := chainCtx.TrustStatus.ErrorStatus
switch status {
case syscall.CERT_TRUST_IS_NOT_TIME_VALID:
- return CertificateInvalidError{c, Expired}
+ return CertificateInvalidError{c, Expired, ""}
default:
return UnknownAuthorityError{c, nil, nil}
}
if status.Error != 0 {
switch status.Error {
case syscall.CERT_E_EXPIRED:
- return CertificateInvalidError{c, Expired}
+ return CertificateInvalidError{c, Expired, ""}
case syscall.CERT_E_CN_NO_MATCH:
return HostnameError{c, opts.DNSName}
case syscall.CERT_E_UNTRUSTEDROOT:
"errors"
"fmt"
"net"
+ "net/url"
+ "reflect"
"runtime"
"strings"
"time"
// given in the VerifyOptions.
Expired
// CANotAuthorizedForThisName results when an intermediate or root
- // certificate has a name constraint which doesn't include the name
- // being checked.
+ // certificate has a name constraint which doesn't permit a DNS or
+ // other name (including IP address) in the leaf certificate.
CANotAuthorizedForThisName
// TooManyIntermediates results when a path length constraint is
// violated.
// NameMismatch results when the subject name of a parent certificate
// does not match the issuer name in the child.
NameMismatch
+ // NameConstraintsWithoutSANs results when a leaf certificate doesn't
+ // contain a Subject Alternative Name extension, but a CA certificate
+ // contains name constraints.
+ NameConstraintsWithoutSANs
+ // UnconstrainedName results when a CA certificate contains permitted
+ // name constraints, but leaf certificate contains a name of an
+ // unsupported or unconstrained type.
+ UnconstrainedName
+ // TooManyConstraints results when the number of comparision operations
+ // needed to check a certificate exceeds the limit set by
+ // VerifyOptions.MaxConstraintComparisions. This limit exists to
+ // prevent pathological certificates can consuming excessive amounts of
+ // CPU time to verify.
+ TooManyConstraints
)
// CertificateInvalidError results when an odd error occurs. Users of this
type CertificateInvalidError struct {
Cert *Certificate
Reason InvalidReason
+ Detail string
}
func (e CertificateInvalidError) Error() string {
case Expired:
return "x509: certificate has expired or is not yet valid"
case CANotAuthorizedForThisName:
- return "x509: a root or intermediate certificate is not authorized to sign in this domain"
+ return "x509: a root or intermediate certificate is not authorized to sign for this name: " + e.Detail
case TooManyIntermediates:
return "x509: too many intermediates for path length constraint"
case IncompatibleUsage:
return "x509: certificate specifies an incompatible key usage"
case NameMismatch:
return "x509: issuer name does not match subject from issuing certificate"
+ case NameConstraintsWithoutSANs:
+ return "x509: issuer has name constraints but leaf doesn't have a SAN extension"
+ case UnconstrainedName:
+ return "x509: issuer has name constraints but leaf contains unknown or unconstrained name: " + e.Detail
}
return "x509: unknown error"
}
// constraint down the chain which mirrors Windows CryptoAPI behavior,
// but not the spec. To accept any key usage, include ExtKeyUsageAny.
KeyUsages []ExtKeyUsage
+ // MaxConstraintComparisions is the maximum number of comparisons to
+ // perform when checking a given certificate's name constraints. If
+ // zero, a sensible default is used. This limit prevents pathalogical
+ // certificates from consuming excessive amounts of CPU time when
+ // validating.
+ MaxConstraintComparisions int
}
const (
rootCertificate
)
-func matchNameConstraint(domain, constraint string) bool {
+// rfc2821Mailbox represents a “mailbox” (which is an email address to most
+// people) by breaking it into the “local” (i.e. before the '@') and “domain”
+// parts.
+type rfc2821Mailbox struct {
+ local, domain string
+}
+
+// parseRFC2821Mailbox parses an email address into local and domain parts,
+// based on the ABNF for a “Mailbox” from RFC 2821. According to
+// https://tools.ietf.org/html/rfc5280#section-4.2.1.6 that's correct for an
+// rfc822Name from a certificate: “The format of an rfc822Name is a "Mailbox"
+// as defined in https://tools.ietf.org/html/rfc2821#section-4.1.2”.
+func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
+ if len(in) == 0 {
+ return mailbox, false
+ }
+
+ localPartBytes := make([]byte, 0, len(in)/2)
+
+ if in[0] == '"' {
+ // Quoted-string = DQUOTE *qcontent DQUOTE
+ // non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127
+ // qcontent = qtext / quoted-pair
+ // qtext = non-whitespace-control /
+ // %d33 / %d35-91 / %d93-126
+ // quoted-pair = ("\" text) / obs-qp
+ // text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text
+ //
+ // (Names beginning with “obs-” are the obsolete syntax from
+ // https://tools.ietf.org/html/rfc2822#section-4. Since it has
+ // been 16 years, we no longer accept that.)
+ in = in[1:]
+ QuotedString:
+ for {
+ if len(in) == 0 {
+ return mailbox, false
+ }
+ c := in[0]
+ in = in[1:]
+
+ switch {
+ case c == '"':
+ break QuotedString
+
+ case c == '\\':
+ // quoted-pair
+ if len(in) == 0 {
+ return mailbox, false
+ }
+ if in[0] == 11 ||
+ in[0] == 12 ||
+ (1 <= in[0] && in[0] <= 9) ||
+ (14 <= in[0] && in[0] <= 127) {
+ localPartBytes = append(localPartBytes, in[0])
+ in = in[1:]
+ } else {
+ return mailbox, false
+ }
+
+ case c == 11 ||
+ c == 12 ||
+ // Space (char 32) is not allowed based on the
+ // BNF, but RFC 3696 gives an example that
+ // assumes that it is. Several “verified”
+ // errata continue to argue about this point.
+ // We choose to accept it.
+ c == 32 ||
+ c == 33 ||
+ c == 127 ||
+ (1 <= c && c <= 8) ||
+ (14 <= c && c <= 31) ||
+ (35 <= c && c <= 91) ||
+ (93 <= c && c <= 126):
+ // qtext
+ localPartBytes = append(localPartBytes, c)
+
+ default:
+ return mailbox, false
+ }
+ }
+ } else {
+ // Atom ("." Atom)*
+ NextChar:
+ for len(in) > 0 {
+ // atext from https://tools.ietf.org/html/rfc2822#section-3.2.4
+ c := in[0]
+
+ switch {
+ case c == '\\':
+ // Examples given in RFC 3696 suggest that
+ // escaped characters can appear outside of a
+ // quoted string. Several “verified” errata
+ // continue to argue the point. We choose to
+ // accept it.
+ in = in[1:]
+ if len(in) == 0 {
+ return mailbox, false
+ }
+ fallthrough
+
+ case ('0' <= c && c <= '9') ||
+ ('a' <= c && c <= 'z') ||
+ ('A' <= c && c <= 'Z') ||
+ c == '!' || c == '#' || c == '$' || c == '%' ||
+ c == '&' || c == '\'' || c == '*' || c == '+' ||
+ c == '-' || c == '/' || c == '=' || c == '?' ||
+ c == '^' || c == '_' || c == '`' || c == '{' ||
+ c == '|' || c == '}' || c == '~' || c == '.':
+ localPartBytes = append(localPartBytes, in[0])
+ in = in[1:]
+
+ default:
+ break NextChar
+ }
+ }
+
+ if len(localPartBytes) == 0 {
+ return mailbox, false
+ }
+
+ // https://tools.ietf.org/html/rfc3696#section-3
+ // “period (".") may also appear, but may not be used to start
+ // or end the local part, nor may two or more consecutive
+ // periods appear.”
+ twoDots := []byte{'.', '.'}
+ if localPartBytes[0] == '.' ||
+ localPartBytes[len(localPartBytes)-1] == '.' ||
+ bytes.Contains(localPartBytes, twoDots) {
+ return mailbox, false
+ }
+ }
+
+ if len(in) == 0 || in[0] != '@' {
+ return mailbox, false
+ }
+ in = in[1:]
+
+ // The RFC species a format for domains, but that's known to be
+ // violated in practice so we accept that anything after an '@' is the
+ // domain part.
+ if _, ok := domainToReverseLabels(in); !ok {
+ return mailbox, false
+ }
+
+ mailbox.local = string(localPartBytes)
+ mailbox.domain = in
+ return mailbox, true
+}
+
+// domainToReverseLabels converts a textual domain name like foo.example.com to
+// the list of labels in reverse order, e.g. ["com", "example", "foo"].
+func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
+ for len(domain) > 0 {
+ if i := strings.LastIndexByte(domain, '.'); i == -1 {
+ reverseLabels = append(reverseLabels, domain)
+ domain = ""
+ } else {
+ reverseLabels = append(reverseLabels, domain[i+1:len(domain)])
+ domain = domain[:i]
+ }
+ }
+
+ if len(reverseLabels) > 0 && len(reverseLabels[0]) == 0 {
+ // An empty label at the end indicates an absolute value.
+ return nil, false
+ }
+
+ for _, label := range reverseLabels {
+ if len(label) == 0 {
+ // Empty labels are otherwise invalid.
+ return nil, false
+ }
+
+ for _, c := range label {
+ if c < 33 || c > 126 {
+ // Invalid character.
+ return nil, false
+ }
+ }
+ }
+
+ return reverseLabels, true
+}
+
+func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
+ // If the constraint contains an @, then it specifies an exact mailbox
+ // name.
+ if strings.Contains(constraint, "@") {
+ constraintMailbox, ok := parseRFC2821Mailbox(constraint)
+ if !ok {
+ return false, fmt.Errorf("x509: internal error: cannot parse constraint %q", constraint)
+ }
+ return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil
+ }
+
+ // Otherwise the constraint is like a DNS constraint of the domain part
+ // of the mailbox.
+ return matchDomainConstraint(mailbox.domain, constraint)
+}
+
+func matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
+ // https://tools.ietf.org/html/rfc5280#section-4.2.1.10
+ // “a uniformResourceIdentifier that does not include an authority
+ // component with a host name specified as a fully qualified domain
+ // name (e.g., if the URI either does not include an authority
+ // component or includes an authority component in which the host name
+ // is specified as an IP address), then the application MUST reject the
+ // certificate.”
+
+ host := uri.Host
+ if len(host) == 0 {
+ return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String())
+ }
+
+ if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") {
+ var err error
+ host, _, err = net.SplitHostPort(uri.Host)
+ if err != nil {
+ return false, err
+ }
+ }
+
+ if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") ||
+ net.ParseIP(host) != nil {
+ return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String())
+ }
+
+ return matchDomainConstraint(host, constraint)
+}
+
+func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
+ if len(ip) != len(constraint.IP) {
+ return false, nil
+ }
+
+ for i := range ip {
+ if mask := constraint.Mask[i]; ip[i]&mask != constraint.IP[i]&mask {
+ return false, nil
+ }
+ }
+
+ return true, nil
+}
+
+func matchDomainConstraint(domain, constraint string) (bool, error) {
// The meaning of zero length constraints is not specified, but this
// code follows NSS and accepts them as matching everything.
if len(constraint) == 0 {
- return true
+ return true, nil
}
- if len(domain) < len(constraint) {
- return false
+ domainLabels, ok := domainToReverseLabels(domain)
+ if !ok {
+ return false, fmt.Errorf("x509: internal error: cannot parse domain %q", domain)
}
- prefixLen := len(domain) - len(constraint)
- if !strings.EqualFold(domain[prefixLen:], constraint) {
- return false
+ // RFC 5280 says that a leading period in a domain name means that at
+ // least one label must be prepended, but only for URI and email
+ // constraints, not DNS constraints. The code also supports that
+ // behaviour for DNS constraints.
+
+ mustHaveSubdomains := false
+ if constraint[0] == '.' {
+ mustHaveSubdomains = true
+ constraint = constraint[1:]
+ }
+
+ constraintLabels, ok := domainToReverseLabels(constraint)
+ if !ok {
+ return false, fmt.Errorf("x509: internal error: cannot parse domain %q", constraint)
}
- if prefixLen == 0 {
- return true
+ if len(domainLabels) < len(constraintLabels) ||
+ (mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) {
+ return false, nil
}
- isSubdomain := domain[prefixLen-1] == '.'
- constraintHasLeadingDot := constraint[0] == '.'
- return isSubdomain != constraintHasLeadingDot
+ for i, constraintLabel := range constraintLabels {
+ if !strings.EqualFold(constraintLabel, domainLabels[i]) {
+ return false, nil
+ }
+ }
+
+ return true, nil
}
-// isValid performs validity checks on the c.
+// checkNameConstraints checks that c permits a child certificate to claim the
+// given name, of type nameType. The argument parsedName contains the parsed
+// form of name, suitable for passing to the match function. The total number
+// of comparisons is tracked in the given count and should not exceed the given
+// limit.
+func (c *Certificate) checkNameConstraints(count *int,
+ maxConstraintComparisons int,
+ nameType string,
+ name string,
+ parsedName interface{},
+ match func(parsedName, constraint interface{}) (match bool, err error),
+ permitted, excluded interface{}) error {
+
+ excludedValue := reflect.ValueOf(excluded)
+
+ *count += excludedValue.Len()
+ if *count > maxConstraintComparisons {
+ return CertificateInvalidError{c, TooManyConstraints, ""}
+ }
+
+ for i := 0; i < excludedValue.Len(); i++ {
+ constraint := excludedValue.Index(i).Interface()
+ match, err := match(parsedName, constraint)
+ if err != nil {
+ return CertificateInvalidError{c, CANotAuthorizedForThisName, err.Error()}
+ }
+
+ if match {
+ return CertificateInvalidError{c, CANotAuthorizedForThisName, fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint)}
+ }
+ }
+
+ permittedValue := reflect.ValueOf(permitted)
+
+ *count += permittedValue.Len()
+ if *count > maxConstraintComparisons {
+ return CertificateInvalidError{c, TooManyConstraints, ""}
+ }
+
+ ok := true
+ for i := 0; i < permittedValue.Len(); i++ {
+ constraint := permittedValue.Index(i).Interface()
+
+ var err error
+ if ok, err = match(parsedName, constraint); err != nil {
+ return CertificateInvalidError{c, CANotAuthorizedForThisName, err.Error()}
+ }
+
+ if ok {
+ break
+ }
+ }
+
+ if !ok {
+ return CertificateInvalidError{c, CANotAuthorizedForThisName, fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name)}
+ }
+
+ return nil
+}
+
+// isValid performs validity checks on c given that it is a candidate to append
+// to the chain in currentChain.
func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *VerifyOptions) error {
if len(c.UnhandledCriticalExtensions) > 0 {
return UnhandledCriticalExtension{}
if len(currentChain) > 0 {
child := currentChain[len(currentChain)-1]
if !bytes.Equal(child.RawIssuer, c.RawSubject) {
- return CertificateInvalidError{c, NameMismatch}
+ return CertificateInvalidError{c, NameMismatch, ""}
}
}
now = time.Now()
}
if now.Before(c.NotBefore) || now.After(c.NotAfter) {
- return CertificateInvalidError{c, Expired}
+ return CertificateInvalidError{c, Expired, ""}
}
- if len(c.PermittedDNSDomains) > 0 {
- ok := false
- for _, constraint := range c.PermittedDNSDomains {
- ok = matchNameConstraint(opts.DNSName, constraint)
- if ok {
- break
- }
+ if (certType == intermediateCertificate || certType == rootCertificate) && c.hasNameConstraints() {
+ maxConstraintComparisons := opts.MaxConstraintComparisions
+ if maxConstraintComparisons == 0 {
+ maxConstraintComparisons = 250000
}
+ count := 0
+ if len(currentChain) == 0 {
+ return errors.New("x509: internal error: empty chain when appending CA cert")
+ }
+ leaf := currentChain[0]
+
+ sanExtension, ok := leaf.getSANExtension()
if !ok {
- return CertificateInvalidError{c, CANotAuthorizedForThisName}
+ // 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, ""}
}
- }
- for _, constraint := range c.ExcludedDNSDomains {
- if matchNameConstraint(opts.DNSName, constraint) {
- return CertificateInvalidError{c, CANotAuthorizedForThisName}
+ err := forEachSAN(sanExtension, func(tag int, data []byte) error {
+ switch tag {
+ case nameTypeEmail:
+ name := string(data)
+ mailbox, ok := parseRFC2821Mailbox(name)
+ if !ok {
+ // This certificate should not have parsed.
+ return errors.New("x509: internal error: rfc822Name SAN failed to parse")
+ }
+
+ if err := c.checkNameConstraints(&count, maxConstraintComparisons, "email address", name, mailbox,
+ func(parsedName, constraint interface{}) (bool, error) {
+ return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
+ }, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil {
+ return err
+ }
+
+ case nameTypeDNS:
+ name := string(data)
+ if err := c.checkNameConstraints(&count, maxConstraintComparisons, "DNS name", name, name,
+ func(parsedName, constraint interface{}) (bool, error) {
+ return matchDomainConstraint(parsedName.(string), constraint.(string))
+ }, c.PermittedDNSDomains, c.ExcludedDNSDomains); err != nil {
+ return err
+ }
+
+ case nameTypeURI:
+ name := string(data)
+ uri, err := url.Parse(name)
+ if err != nil {
+ return fmt.Errorf("x509: internal error: URI SAN %q failed to parse", name)
+ }
+
+ if err := c.checkNameConstraints(&count, maxConstraintComparisons, "URI", name, uri,
+ func(parsedName, constraint interface{}) (bool, error) {
+ return matchURIConstraint(parsedName.(*url.URL), constraint.(string))
+ }, c.PermittedURIDomains, c.ExcludedURIDomains); err != nil {
+ return err
+ }
+
+ case nameTypeIP:
+ ip := net.IP(data)
+ if l := len(ip); l != net.IPv4len && l != net.IPv6len {
+ return fmt.Errorf("x509: internal error: IP SAN %x failed to parse", data)
+ }
+
+ if err := c.checkNameConstraints(&count, maxConstraintComparisons, "IP address", ip.String(), ip,
+ func(parsedName, constraint interface{}) (bool, error) {
+ return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
+ }, c.PermittedIPRanges, c.ExcludedIPRanges); err != nil {
+ return err
+ }
+
+ default:
+ // Unknown SAN types are ignored.
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return err
}
}
// encryption key could only be used for Diffie-Hellman key agreement.
if certType == intermediateCertificate && (!c.BasicConstraintsValid || !c.IsCA) {
- return CertificateInvalidError{c, NotAuthorizedToSign}
+ return CertificateInvalidError{c, NotAuthorizedToSign, ""}
}
if c.BasicConstraintsValid && c.MaxPathLen >= 0 {
numIntermediates := len(currentChain) - 1
if numIntermediates > c.MaxPathLen {
- return CertificateInvalidError{c, TooManyIntermediates}
+ return CertificateInvalidError{c, TooManyIntermediates, ""}
}
}
}
if len(chains) == 0 {
- err = CertificateInvalidError{c, IncompatibleUsage}
+ err = CertificateInvalidError{c, IncompatibleUsage, ""}
}
return
var nameConstraintTests = []struct {
constraint, domain string
+ expectError bool
shouldMatch bool
}{
- {"", "anything.com", true},
- {"example.com", "example.com", true},
- {"example.com", "ExAmPle.coM", true},
- {"example.com", "exampl1.com", false},
- {"example.com", "www.ExAmPle.coM", true},
- {"example.com", "notexample.com", false},
- {".example.com", "example.com", false},
- {".example.com", "www.example.com", true},
- {".example.com", "www..example.com", false},
+ {"", "anything.com", false, true},
+ {"example.com", "example.com", false, true},
+ {"example.com.", "example.com", true, false},
+ {"example.com", "example.com.", true, false},
+ {"example.com", "ExAmPle.coM", false, true},
+ {"example.com", "exampl1.com", false, false},
+ {"example.com", "www.ExAmPle.coM", false, true},
+ {"example.com", "sub.www.ExAmPle.coM", false, true},
+ {"example.com", "notexample.com", false, false},
+ {".example.com", "example.com", false, false},
+ {".example.com", "www.example.com", false, true},
+ {".example.com", "www..example.com", true, false},
}
func TestNameConstraints(t *testing.T) {
for i, test := range nameConstraintTests {
- result := matchNameConstraint(test.domain, test.constraint)
+ result, err := matchDomainConstraint(test.domain, test.constraint)
+
+ if err != nil && !test.expectError {
+ t.Errorf("unexpected error for test #%d: domain=%s, constraint=%s, err=%s", i, test.domain, test.constraint, err)
+ continue
+ }
+
+ if err == nil && test.expectError {
+ t.Errorf("unexpected success for test #%d: domain=%s, constraint=%s", i, test.domain, test.constraint)
+ continue
+ }
+
if result != test.shouldMatch {
t.Errorf("unexpected result for test #%d: domain=%s, constraint=%s, result=%t", i, test.domain, test.constraint, result)
}
"io"
"math/big"
"net"
+ "net/url"
"strconv"
+ "strings"
"time"
)
DNSNames []string
EmailAddresses []string
IPAddresses []net.IP
+ URIs []*url.URL
// Name constraints
PermittedDNSDomainsCritical bool // if true then the name constraints are marked critical.
PermittedDNSDomains []string
ExcludedDNSDomains []string
+ PermittedIPRanges []*net.IPNet
+ ExcludedIPRanges []*net.IPNet
+ PermittedEmailAddresses []string
+ ExcludedEmailAddresses []string
+ PermittedURIDomains []string
+ ExcludedURIDomains []string
// CRL Distribution Points
CRLDistributionPoints []string
return checkSignature(algo, signed, signature, c.PublicKey)
}
+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
+}
+
+func (c *Certificate) getSANExtension() ([]byte, 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] == 17 {
+ return e.Value, true
+ }
+ }
+
+ return nil, false
+}
+
func signaturePublicKeyAlgoMismatchError(expectedPubKeyAlgo PublicKeyAlgorithm, pubKey interface{}) error {
return fmt.Errorf("x509: signature algorithm specifies an %s public key, but have public key of type %T", expectedPubKeyAlgo.String(), pubKey)
}
Excluded []generalSubtree `asn1:"optional,tag:1"`
}
+const (
+ nameTypeEmail = 1
+ nameTypeDNS = 2
+ nameTypeURI = 6
+ nameTypeIP = 7
+)
+
type generalSubtree struct {
- Name string `asn1:"tag:2,optional,ia5"`
+ Email string `asn1:"tag:1,optional,ia5"`
+ Name string `asn1:"tag:2,optional,ia5"`
+ URIDomain string `asn1:"tag:6,optional,ia5"`
+ IPAndMask []byte `asn1:"tag:7,optional"`
}
// RFC 5280, 4.2.2.1
return nil
}
-func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddresses []net.IP, err error) {
+func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, err error) {
err = forEachSAN(value, func(tag int, data []byte) error {
switch tag {
- case 1:
- emailAddresses = append(emailAddresses, string(data))
- case 2:
- dnsNames = append(dnsNames, string(data))
- case 7:
+ case nameTypeEmail:
+ mailbox := string(data)
+ if _, ok := parseRFC2821Mailbox(mailbox); !ok {
+ return fmt.Errorf("x509: cannot parse rfc822Name %q", mailbox)
+ }
+ emailAddresses = append(emailAddresses, mailbox)
+ case nameTypeDNS:
+ domain := string(data)
+ if _, ok := domainToReverseLabels(domain); !ok {
+ return fmt.Errorf("x509: cannot parse dnsName %q", string(data))
+ }
+ dnsNames = append(dnsNames, domain)
+ case nameTypeURI:
+ uri, err := url.Parse(string(data))
+ if err != nil {
+ return fmt.Errorf("x509: cannot parse URI %q: %s", string(data), err)
+ }
+ if len(uri.Host) > 0 {
+ if _, ok := domainToReverseLabels(uri.Host); !ok {
+ return fmt.Errorf("x509: cannot parse URI %q: invalid domain", string(data))
+ }
+ }
+ uris = append(uris, uri)
+ case nameTypeIP:
switch len(data) {
case net.IPv4len, net.IPv6len:
ipAddresses = append(ipAddresses, data)
return
}
+// isValidIPMask returns true iff mask consists of zero or more 1 bits, followed by zero bits.
+func isValidIPMask(mask []byte) bool {
+ seenZero := false
+
+ for _, b := range mask {
+ if seenZero {
+ if b != 0 {
+ return false
+ }
+
+ continue
+ }
+
+ switch b {
+ case 0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe:
+ seenZero = true
+ case 0xff:
+ default:
+ return false
+ }
+ }
+
+ return true
+}
+
+func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandled bool, err error) {
+ // RFC 5280, 4.2.1.10
+
+ // NameConstraints ::= SEQUENCE {
+ // permittedSubtrees [0] GeneralSubtrees OPTIONAL,
+ // excludedSubtrees [1] GeneralSubtrees OPTIONAL }
+ //
+ // GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
+ //
+ // GeneralSubtree ::= SEQUENCE {
+ // base GeneralName,
+ // minimum [0] BaseDistance DEFAULT 0,
+ // maximum [1] BaseDistance OPTIONAL }
+ //
+ // BaseDistance ::= INTEGER (0..MAX)
+
+ var constraints nameConstraints
+ if rest, err := asn1.Unmarshal(e.Value, &constraints); err != nil {
+ return false, err
+ } else if len(rest) != 0 {
+ return false, errors.New("x509: trailing data after X.509 NameConstraints")
+ }
+
+ if len(constraints.Permitted) == 0 && len(constraints.Excluded) == 0 {
+ // https://tools.ietf.org/html/rfc5280#section-4.2.1.10:
+ // “either the permittedSubtrees field
+ // or the excludedSubtrees MUST be
+ // present”
+ return false, errors.New("x509: empty name constraints extension")
+ }
+
+ getValues := func(subtrees []generalSubtree) (dnsNames []string, ips []*net.IPNet, emails, uriDomains []string, err error) {
+ for _, subtree := range subtrees {
+ switch {
+ case len(subtree.Name) != 0:
+ domain := subtree.Name
+ if len(domain) > 0 && domain[0] == '.' {
+ // constraints can have a leading
+ // period to exclude the domain
+ // itself, but that's not valid in a
+ // normal domain name.
+ domain = domain[1:]
+ }
+ if _, ok := domainToReverseLabels(domain); !ok {
+ return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse dnsName constraint %q", subtree.Name)
+ }
+ dnsNames = append(dnsNames, subtree.Name)
+
+ case len(subtree.IPAndMask) != 0:
+ l := len(subtree.IPAndMask)
+ var ip, mask []byte
+
+ switch l {
+ case 8:
+ ip = subtree.IPAndMask[:4]
+ mask = subtree.IPAndMask[4:]
+
+ case 32:
+ ip = subtree.IPAndMask[:16]
+ mask = subtree.IPAndMask[16:]
+
+ default:
+ return nil, nil, nil, nil, fmt.Errorf("x509: IP constraint contained value of length %d", l)
+ }
+
+ if !isValidIPMask(mask) {
+ return nil, nil, nil, nil, fmt.Errorf("x509: IP constraint contained invalid mask %x", mask)
+ }
+
+ ips = append(ips, &net.IPNet{IP: net.IP(ip), Mask: net.IPMask(mask)})
+
+ case len(subtree.Email) != 0:
+ constraint := subtree.Email
+ // If the constraint contains an @ then
+ // it specifies an exact mailbox name.
+ if strings.Contains(constraint, "@") {
+ if _, ok := parseRFC2821Mailbox(constraint); !ok {
+ return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
+ }
+ } else {
+ // Otherwise it's a domain name.
+ domain := constraint
+ if len(domain) > 0 && domain[0] == '.' {
+ domain = domain[1:]
+ }
+ if _, ok := domainToReverseLabels(domain); !ok {
+ return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
+ }
+ }
+ emails = append(emails, constraint)
+
+ case len(subtree.URIDomain) != 0:
+ domain := subtree.URIDomain
+
+ if net.ParseIP(domain) != nil {
+ return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q: cannot be IP address", subtree.URIDomain)
+ }
+
+ if len(domain) > 0 && domain[0] == '.' {
+ // constraints can have a leading
+ // period to exclude the domain
+ // itself, but that's not valid in a
+ // normal domain name.
+ domain = domain[1:]
+ }
+ if _, ok := domainToReverseLabels(domain); !ok {
+ return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q", subtree.URIDomain)
+ }
+ uriDomains = append(uriDomains, subtree.URIDomain)
+
+ default:
+ unhandled = true
+ }
+ }
+
+ return dnsNames, ips, emails, uriDomains, nil
+ }
+
+ if out.PermittedDNSDomains, out.PermittedIPRanges, out.PermittedEmailAddresses, out.PermittedURIDomains, err = getValues(constraints.Permitted); err != nil {
+ return false, err
+ }
+ if out.ExcludedDNSDomains, out.ExcludedIPRanges, out.ExcludedEmailAddresses, out.ExcludedURIDomains, err = getValues(constraints.Excluded); err != nil {
+ return false, err
+ }
+ out.PermittedDNSDomainsCritical = e.Critical
+
+ return unhandled, nil
+}
+
func parseCertificate(in *certificate) (*Certificate, error) {
out := new(Certificate)
out.Raw = in.Raw
out.MaxPathLenZero = out.MaxPathLen == 0
// TODO: map out.MaxPathLen to 0 if it has the -1 default value? (Issue 19285)
case 17:
- out.DNSNames, out.EmailAddresses, out.IPAddresses, err = parseSANExtension(e.Value)
+ out.DNSNames, out.EmailAddresses, out.IPAddresses, out.URIs, err = parseSANExtension(e.Value)
if err != nil {
return nil, err
}
- if len(out.DNSNames) == 0 && len(out.EmailAddresses) == 0 && len(out.IPAddresses) == 0 {
+ if len(out.DNSNames) == 0 && len(out.EmailAddresses) == 0 && len(out.IPAddresses) == 0 && len(out.URIs) == 0 {
// If we didn't parse anything then we do the critical check, below.
unhandled = true
}
case 30:
- // RFC 5280, 4.2.1.10
-
- // NameConstraints ::= SEQUENCE {
- // permittedSubtrees [0] GeneralSubtrees OPTIONAL,
- // excludedSubtrees [1] GeneralSubtrees OPTIONAL }
- //
- // GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
- //
- // GeneralSubtree ::= SEQUENCE {
- // base GeneralName,
- // minimum [0] BaseDistance DEFAULT 0,
- // maximum [1] BaseDistance OPTIONAL }
- //
- // BaseDistance ::= INTEGER (0..MAX)
-
- var constraints nameConstraints
- if rest, err := asn1.Unmarshal(e.Value, &constraints); err != nil {
+ unhandled, err = parseNameConstraintsExtension(out, e)
+ if err != nil {
return nil, err
- } else if len(rest) != 0 {
- return nil, errors.New("x509: trailing data after X.509 NameConstraints")
}
- if len(constraints.Permitted) == 0 && len(constraints.Excluded) == 0 {
- // https://tools.ietf.org/html/rfc5280#section-4.2.1.10:
- // “either the permittedSubtrees field
- // or the excludedSubtrees MUST be
- // present”
- return nil, errors.New("x509: empty name constraints extension")
- }
-
- getDNSNames := func(subtrees []generalSubtree, isCritical bool) (dnsNames []string, err error) {
- for _, subtree := range subtrees {
- if len(subtree.Name) == 0 {
- if isCritical {
- return nil, UnhandledCriticalExtension{}
- }
- continue
- }
- dnsNames = append(dnsNames, subtree.Name)
- }
-
- return dnsNames, nil
- }
-
- if out.PermittedDNSDomains, err = getDNSNames(constraints.Permitted, e.Critical); err != nil {
- return out, err
- }
- if out.ExcludedDNSDomains, err = getDNSNames(constraints.Excluded, e.Critical); err != nil {
- return out, err
- }
- out.PermittedDNSDomainsCritical = e.Critical
-
case 31:
// RFC 5280, 4.2.1.13
// marshalSANs marshals a list of addresses into a the contents of an X.509
// SubjectAlternativeName extension.
-func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP) (derBytes []byte, err error) {
+func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL) (derBytes []byte, err error) {
var rawValues []asn1.RawValue
for _, name := range dnsNames {
- rawValues = append(rawValues, asn1.RawValue{Tag: 2, Class: 2, Bytes: []byte(name)})
+ rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeDNS, Class: 2, Bytes: []byte(name)})
}
for _, email := range emailAddresses {
- rawValues = append(rawValues, asn1.RawValue{Tag: 1, Class: 2, Bytes: []byte(email)})
+ rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeEmail, Class: 2, Bytes: []byte(email)})
}
for _, rawIP := range ipAddresses {
// If possible, we always want to encode IPv4 addresses in 4 bytes.
if ip == nil {
ip = rawIP
}
- rawValues = append(rawValues, asn1.RawValue{Tag: 7, Class: 2, Bytes: ip})
+ rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeIP, Class: 2, Bytes: ip})
+ }
+ for _, uri := range uris {
+ rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeURI, Class: 2, Bytes: []byte(uri.String())})
}
return asn1.Marshal(rawValues)
}
n++
}
- if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0) &&
+ if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 || len(template.URIs) > 0) &&
!oidInExtensions(oidExtensionSubjectAltName, template.ExtraExtensions) {
ret[n].Id = oidExtensionSubjectAltName
- ret[n].Value, err = marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses)
+ ret[n].Value, err = marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses, template.URIs)
if err != nil {
return
}
n++
}
- if (len(template.PermittedDNSDomains) > 0 || len(template.ExcludedDNSDomains) > 0) &&
+ if (len(template.PermittedDNSDomains) > 0 || len(template.ExcludedDNSDomains) > 0 ||
+ len(template.PermittedIPRanges) > 0 || len(template.ExcludedIPRanges) > 0 ||
+ len(template.PermittedEmailAddresses) > 0 || len(template.ExcludedEmailAddresses) > 0 ||
+ len(template.PermittedURIDomains) > 0 || len(template.ExcludedURIDomains) > 0) &&
!oidInExtensions(oidExtensionNameConstraints, template.ExtraExtensions) {
ret[n].Id = oidExtensionNameConstraints
ret[n].Critical = template.PermittedDNSDomainsCritical
var out nameConstraints
- out.Permitted = make([]generalSubtree, len(template.PermittedDNSDomains))
- for i, permitted := range template.PermittedDNSDomains {
- out.Permitted[i] = generalSubtree{Name: permitted}
+ ipAndMask := func(ipNet *net.IPNet) []byte {
+ maskedIP := ipNet.IP.Mask(ipNet.Mask)
+ ipAndMask := make([]byte, 0, len(maskedIP)+len(ipNet.Mask))
+ ipAndMask = append(ipAndMask, maskedIP...)
+ ipAndMask = append(ipAndMask, ipNet.Mask...)
+ return ipAndMask
+ }
+
+ out.Permitted = make([]generalSubtree, 0, len(template.PermittedDNSDomains)+len(template.PermittedIPRanges))
+ for _, permitted := range template.PermittedDNSDomains {
+ out.Permitted = append(out.Permitted, generalSubtree{Name: permitted})
+ }
+ for _, permitted := range template.PermittedIPRanges {
+ out.Permitted = append(out.Permitted, generalSubtree{IPAndMask: ipAndMask(permitted)})
+ }
+ for _, permitted := range template.PermittedEmailAddresses {
+ out.Permitted = append(out.Permitted, generalSubtree{Email: permitted})
+ }
+ for _, permitted := range template.PermittedURIDomains {
+ out.Permitted = append(out.Permitted, generalSubtree{URIDomain: permitted})
+ }
+
+ out.Excluded = make([]generalSubtree, 0, len(template.ExcludedDNSDomains)+len(template.ExcludedIPRanges))
+ for _, excluded := range template.ExcludedDNSDomains {
+ out.Excluded = append(out.Excluded, generalSubtree{Name: excluded})
+ }
+ for _, excluded := range template.ExcludedIPRanges {
+ out.Excluded = append(out.Excluded, generalSubtree{IPAndMask: ipAndMask(excluded)})
+ }
+ for _, excluded := range template.ExcludedEmailAddresses {
+ out.Excluded = append(out.Excluded, generalSubtree{Email: excluded})
}
- out.Excluded = make([]generalSubtree, len(template.ExcludedDNSDomains))
- for i, excluded := range template.ExcludedDNSDomains {
- out.Excluded[i] = generalSubtree{Name: excluded}
+ for _, excluded := range template.ExcludedURIDomains {
+ out.Excluded = append(out.Excluded, generalSubtree{URIDomain: excluded})
}
ret[n].Value, err = asn1.Marshal(out)
DNSNames []string
EmailAddresses []string
IPAddresses []net.IP
+ URIs []*url.URL
}
// These structures reflect the ASN.1 structure of X.509 certificate
// CreateCertificateRequest creates a new certificate request based on a
// template. The following members of template are used: Attributes, DNSNames,
-// EmailAddresses, ExtraExtensions, IPAddresses, SignatureAlgorithm, and
+// EmailAddresses, ExtraExtensions, IPAddresses, URIs, SignatureAlgorithm, and
// Subject. The private key is the private key of the signer.
//
// The returned slice is the certificate request in DER encoding.
var extensions []pkix.Extension
- if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0) &&
+ if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 || len(template.URIs) > 0) &&
!oidInExtensions(oidExtensionSubjectAltName, template.ExtraExtensions) {
- sanBytes, err := marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses)
+ sanBytes, err := marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses, template.URIs)
if err != nil {
return nil, err
}
for _, extension := range out.Extensions {
if extension.Id.Equal(oidExtensionSubjectAltName) {
- out.DNSNames, out.EmailAddresses, out.IPAddresses, err = parseSANExtension(extension.Value)
+ out.DNSNames, out.EmailAddresses, out.IPAddresses, out.URIs, err = parseSANExtension(extension.Value)
if err != nil {
return nil, err
}
"internal/testenv"
"math/big"
"net"
+ "net/url"
"os/exec"
"reflect"
"runtime"
"9048084225c53e8acb7feb6f04d16dc574a2f7a27c7b603c77cd0ece48027f012fb69b37e02a2a" +
"36dcd585d6ace53f546f961e05af"
+func parseCIDR(s string) *net.IPNet {
+ _, net, err := net.ParseCIDR(s)
+ if err != nil {
+ panic(err)
+ }
+ return net
+}
+
+func parseURI(s string) *url.URL {
+ uri, err := url.Parse(s)
+ if err != nil {
+ panic(err)
+ }
+ return uri
+}
+
func TestCreateSelfSignedCertificate(t *testing.T) {
random := rand.Reader
DNSNames: []string{"test.example.com"},
EmailAddresses: []string{"gopher@golang.org"},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1).To4(), net.ParseIP("2001:4860:0:2001::68")},
-
- PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3}},
- PermittedDNSDomains: []string{".example.com", "example.com"},
- ExcludedDNSDomains: []string{"bar.example.com"},
+ URIs: []*url.URL{parseURI("https://foo.com/wibble#foo")},
+
+ PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3}},
+ PermittedDNSDomains: []string{".example.com", "example.com"},
+ ExcludedDNSDomains: []string{"bar.example.com"},
+ PermittedIPRanges: []*net.IPNet{parseCIDR("192.168.1.1/16"), parseCIDR("1.2.3.4/8")},
+ ExcludedIPRanges: []*net.IPNet{parseCIDR("2001:db8::/48")},
+ PermittedEmailAddresses: []string{"foo@example.com"},
+ ExcludedEmailAddresses: []string{".example.com", "example.com"},
+ PermittedURIDomains: []string{".bar.com", "bar.com"},
+ ExcludedURIDomains: []string{".bar2.com", "bar2.com"},
CRLDistributionPoints: []string{"http://crl1.example.com/ca1.crl", "http://crl2.example.com/ca1.crl"},
t.Errorf("%s: failed to parse name constraint exclusions: %#v", test.name, cert.ExcludedDNSDomains)
}
+ if len(cert.PermittedIPRanges) != 2 || cert.PermittedIPRanges[0].String() != "192.168.0.0/16" || cert.PermittedIPRanges[1].String() != "1.0.0.0/8" {
+ t.Errorf("%s: failed to parse IP constraints: %#v", test.name, cert.PermittedIPRanges)
+ }
+
+ if len(cert.ExcludedIPRanges) != 1 || cert.ExcludedIPRanges[0].String() != "2001:db8::/48" {
+ t.Errorf("%s: failed to parse IP constraint exclusions: %#v", test.name, cert.ExcludedIPRanges)
+ }
+
+ if len(cert.PermittedEmailAddresses) != 1 || cert.PermittedEmailAddresses[0] != "foo@example.com" {
+ t.Errorf("%s: failed to parse permitted email addreses: %#v", test.name, cert.PermittedEmailAddresses)
+ }
+
+ if len(cert.ExcludedEmailAddresses) != 2 || cert.ExcludedEmailAddresses[0] != ".example.com" || cert.ExcludedEmailAddresses[1] != "example.com" {
+ t.Errorf("%s: failed to parse excluded email addreses: %#v", test.name, cert.ExcludedEmailAddresses)
+ }
+
+ if len(cert.PermittedURIDomains) != 2 || cert.PermittedURIDomains[0] != ".bar.com" || cert.PermittedURIDomains[1] != "bar.com" {
+ t.Errorf("%s: failed to parse permitted URIs: %#v", test.name, cert.PermittedURIDomains)
+ }
+
+ if len(cert.ExcludedURIDomains) != 2 || cert.ExcludedURIDomains[0] != ".bar2.com" || cert.ExcludedURIDomains[1] != "bar2.com" {
+ t.Errorf("%s: failed to parse excluded URIs: %#v", test.name, cert.ExcludedURIDomains)
+ }
+
if cert.Subject.CommonName != commonName {
t.Errorf("%s: subject wasn't correctly copied from the template. Got %s, want %s", test.name, cert.Subject.CommonName, commonName)
}
t.Errorf("%s: SAN emails differ from template. Got %v, want %v", test.name, cert.EmailAddresses, template.EmailAddresses)
}
+ if len(cert.URIs) != 1 || cert.URIs[0].String() != "https://foo.com/wibble#foo" {
+ t.Errorf("%s: URIs differ from template. Got %v, want %v", test.name, cert.URIs, template.URIs)
+ }
+
if !reflect.DeepEqual(cert.IPAddresses, template.IPAddresses) {
t.Errorf("%s: SAN IPs differ from template. Got %v, want %v", test.name, cert.IPAddresses, template.IPAddresses)
}
}
func TestCertificateRequestOverrides(t *testing.T) {
- sanContents, err := marshalSANs([]string{"foo.example.com"}, nil, nil)
+ sanContents, err := marshalSANs([]string{"foo.example.com"}, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
t.Errorf("bad attributes: %#v\n", csr.Attributes)
}
- sanContents2, err := marshalSANs([]string{"foo2.example.com"}, nil, nil)
+ sanContents2, err := marshalSANs([]string{"foo2.example.com"}, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
}
}
}
+
+const criticalNameConstraintWithUnknownTypePEM = `
+-----BEGIN CERTIFICATE-----
+MIIC/TCCAeWgAwIBAgICEjQwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UEAxMdRW1w
+dHkgbmFtZSBjb25zdHJhaW50cyBpc3N1ZXIwHhcNMTMwMjAxMDAwMDAwWhcNMjAw
+NTMwMTA0ODM4WjAhMR8wHQYDVQQDExZFbXB0eSBuYW1lIGNvbnN0cmFpbnRzMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwriElUIt3LCqmJObs+yDoWPD
+F5IqgWk6moIobYjPfextZiYU6I3EfvAwoNxPDkN2WowcocUZMJbEeEq5ebBksFnx
+f12gBxlIViIYwZAzu7aFvhDMyPKQI3C8CG0ZSC9ABZ1E3umdA3CEueNOmP/TChNq
+Cl23+BG1Qb/PJkpAO+GfpWSVhTcV53Mf/cKvFHcjGNrxzdSoq9fyW7a6gfcGEQY0
+LVkmwFWUfJ0wT8kaeLr0E0tozkIfo01KNWNzv6NcYP80QOBRDlApWu9ODmEVJHPD
+blx4jzTQ3JLa+4DvBNOjVUOp+mgRmjiW0rLdrxwOxIqIOwNjweMCp/hgxX/hTQID
+AQABozgwNjA0BgNVHR4BAf8EKjAooCQwIokgIACrzQAAAAAAAAAAAAAAAP////8A
+AAAAAAAAAAAAAAChADANBgkqhkiG9w0BAQsFAAOCAQEAWG+/zUMHQhP8uNCtgSHy
+im/vh7wminwAvWgMKxlkLBFns6nZeQqsOV1lABY7U0Zuoqa1Z5nb6L+iJa4ElREJ
+Oi/erLc9uLwBdDCAR0hUTKD7a6i4ooS39DTle87cUnj0MW1CUa6Hv5SsvpYW+1Xl
+eYJk/axQOOTcy4Es53dvnZsjXH0EA/QHnn7UV+JmlE3rtVxcYp6MLYPmRhTioROA
+/drghicRkiu9hxdPyxkYS16M5g3Zj30jdm+k/6C6PeNtN9YmOOganCOSyFYfGhqO
+ANYzpmuV+oIedAsPpIbfIzN8njYUs1zio+1IoI4o8ddM9sCbtPU8o+WoY6IsCKXV
+/g==
+-----END CERTIFICATE-----`
+
+func TestCriticalNameConstraintWithUnknownType(t *testing.T) {
+ block, _ := pem.Decode([]byte(criticalNameConstraintWithUnknownTypePEM))
+ cert, err := ParseCertificate(block.Bytes)
+ if err != nil {
+ t.Fatalf("unexpected parsing failure: %s", err)
+ }
+
+ if l := len(cert.UnhandledCriticalExtensions); l != 1 {
+ t.Fatalf("expected one unhandled critical extension, but found %d", l)
+ }
+}
+
+const badIPMaskPEM = `
+-----BEGIN CERTIFICATE-----
+MIICzzCCAbegAwIBAgICEjQwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAxMSQmFk
+IElQIG1hc2sgaXNzdWVyMB4XDTEzMDIwMTAwMDAwMFoXDTIwMDUzMDEwNDgzOFow
+FjEUMBIGA1UEAxMLQmFkIElQIG1hc2swggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDCuISVQi3csKqYk5uz7IOhY8MXkiqBaTqagihtiM997G1mJhTojcR+
+8DCg3E8OQ3ZajByhxRkwlsR4Srl5sGSwWfF/XaAHGUhWIhjBkDO7toW+EMzI8pAj
+cLwIbRlIL0AFnUTe6Z0DcIS5406Y/9MKE2oKXbf4EbVBv88mSkA74Z+lZJWFNxXn
+cx/9wq8UdyMY2vHN1Kir1/JbtrqB9wYRBjQtWSbAVZR8nTBPyRp4uvQTS2jOQh+j
+TUo1Y3O/o1xg/zRA4FEOUCla704OYRUkc8NuXHiPNNDcktr7gO8E06NVQ6n6aBGa
+OJbSst2vHA7Eiog7A2PB4wKn+GDFf+FNAgMBAAGjIDAeMBwGA1UdHgEB/wQSMBCg
+DDAKhwgBAgME//8BAKEAMA0GCSqGSIb3DQEBCwUAA4IBAQBYb7/NQwdCE/y40K2B
+IfKKb++HvCaKfAC9aAwrGWQsEWezqdl5Cqw5XWUAFjtTRm6iprVnmdvov6IlrgSV
+EQk6L96stz24vAF0MIBHSFRMoPtrqLiihLf0NOV7ztxSePQxbUJRroe/lKy+lhb7
+VeV5gmT9rFA45NzLgSznd2+dmyNcfQQD9AeeftRX4maUTeu1XFxinowtg+ZGFOKh
+E4D92uCGJxGSK72HF0/LGRhLXozmDdmPfSN2b6T/oLo942031iY46BqcI5LIVh8a
+Go4A1jOma5X6gh50Cw+kht8jM3yeNhSzXOKj7Uigjijx10z2wJu09Tyj5ahjoiwI
+pdX+
+-----END CERTIFICATE-----`
+
+func TestBadIPMask(t *testing.T) {
+ block, _ := pem.Decode([]byte(badIPMaskPEM))
+ _, err := ParseCertificate(block.Bytes)
+ if err == nil {
+ t.Fatalf("unexpected success")
+ }
+
+ const expected = "contained invalid mask"
+ if !strings.Contains(err.Error(), expected) {
+ t.Fatalf("expected %q in error but got: %s", expected, err)
+ }
+}
},
"crypto/x509": {
"L4", "CRYPTO-MATH", "OS", "CGO",
- "crypto/x509/pkix", "encoding/pem", "encoding/hex", "net", "os/user", "syscall",
+ "crypto/x509/pkix", "encoding/pem", "encoding/hex", "net", "os/user", "syscall", "net/url",
},
"crypto/x509/pkix": {"L4", "CRYPTO-MATH", "encoding/hex"},