summaryrefslogtreecommitdiffstats
path: root/src/crypto/x509/name_constraints_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/crypto/x509/name_constraints_test.go')
-rw-r--r--src/crypto/x509/name_constraints_test.go2171
1 files changed, 2171 insertions, 0 deletions
diff --git a/src/crypto/x509/name_constraints_test.go b/src/crypto/x509/name_constraints_test.go
new file mode 100644
index 0000000..4c22c4c
--- /dev/null
+++ b/src/crypto/x509/name_constraints_test.go
@@ -0,0 +1,2171 @@
+// 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/asn1"
+ "encoding/hex"
+ "encoding/pem"
+ "fmt"
+ "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 leafSpec
+ requestedEKUs []ExtKeyUsage
+ expectedError string
+ noOpenSSL bool
+ ignoreCN bool
+}
+
+type constraintsSpec struct {
+ ok []string
+ bad []string
+ ekus []string
+}
+
+type leafSpec struct {
+ sans []string
+ ekus []string
+ cn string
+}
+
+var nameConstraintsTests = []nameConstraintsTest{
+ // #0: dummy test for the certificate generation process itself.
+ {
+ roots: make([]constraintsSpec, 1),
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #1: dummy test for the certificate generation process itself: single
+ // level of intermediate.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #2: dummy test for the certificate generation process itself: two
+ // levels of intermediates.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #3: matching DNS constraint in root
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #4: matching DNS constraint in intermediate.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:example.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #5: .example.com only matches subdomains.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ expectedError: "\"example.com\" is not permitted",
+ },
+
+ // #6: .example.com matches subdomains.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.example.com"},
+ },
+ },
+
+ // #7: .example.com matches multiple levels of subdomains
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.bar.example.com"},
+ },
+ },
+
+ // #8: specifying a permitted list of names does not exclude other name
+ // types
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:10.1.1.1"},
+ },
+ },
+
+ // #9: specifying a permitted list of names does not exclude other name
+ // types
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:example.com", "dns:foo.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #11: intermediates cannot add permitted names that the root doesn't
+ // grant them.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:example.com", "dns:foo.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.com"},
+ },
+ expectedError: "\"foo.com\" is not permitted",
+ },
+
+ // #12: intermediates can further limit their scope if they wish.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:.bar.example.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.bar.example.com"},
+ },
+ },
+
+ // #13: intermediates can further limit their scope and that limitation
+ // is effective
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:.bar.example.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.com"},
+ },
+ },
+
+ // #15: roots exclusions are effective.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.example.com"},
+ },
+ expectedError: "\"foo.example.com\" is excluded",
+ },
+
+ // #16: intermediates can also exclude names and that doesn't affect
+ // other names.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.com"},
+ },
+ },
+
+ // #17: intermediate exclusions are effective.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.example.com"},
+ },
+ expectedError: "\"foo.example.com\" is excluded",
+ },
+
+ // #18: having an exclusion doesn't prohibit other types of names.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"dns:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:foo.com", "ip:10.1.1.1"},
+ },
+ },
+
+ // #19: IP-based exclusions are permitted and don't affect unrelated IP
+ // addresses.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:192.168.1.1"},
+ },
+ },
+
+ // #20: IP-based exclusions are effective
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:10.0.0.1"},
+ },
+ expectedError: "\"10.0.0.1\" is excluded",
+ },
+
+ // #21: intermediates can further constrain IP ranges.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"ip:0.0.0.0/1"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ bad: []string{"ip:11.0.0.0/8"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:.foo.com"},
+ },
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.)
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ok: []string{"dns:.example.com"},
+ },
+ {
+ ok: []string{"dns:.foo.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: []constraintsSpec{
+ {},
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.)
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ {},
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ {
+ ok: []string{"dns:example.com"},
+ },
+ {},
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ {
+ {},
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ {
+ ok: []string{"dns:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ {
+ {
+ ok: []string{"dns:bar.com"},
+ },
+ {
+ ok: []string{"dns:foo.com"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:bar.com"},
+ },
+ expectedError: "\"bar.com\" is not permitted",
+ },
+
+ // #28: unknown name types don't cause a problem without constraints.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"unknown:"},
+ },
+ },
+
+ // #29: unknown name types are allowed even in constrained chains.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"unknown:"},
+ },
+ },
+
+ // #30: without SANs, a certificate with a CN is still accepted in a
+ // constrained chain, since we ignore the CN in VerifyHostname.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{},
+ cn: "foo.com",
+ },
+ },
+
+ // #31: IPv6 addresses work in constraints: roots can permit them as
+ // expected.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:2000:abcd:1234::"},
+ },
+ },
+
+ // #32: IPv6 addresses work in constraints: root restrictions are
+ // effective.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:2000:1234:abcd::"},
+ },
+ expectedError: "\"2000:1234:abcd::\" is not permitted",
+ },
+
+ // #33: An IPv6 permitted subtree doesn't affect DNS names.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:2000:abcd::", "dns:foo.com"},
+ },
+ },
+
+ // #34: IPv6 exclusions don't affect unrelated addresses.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:2000:1234::"},
+ },
+ },
+
+ // #35: IPv6 exclusions are effective.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:2000:abcd::"},
+ },
+ expectedError: "\"2000:abcd::\" is excluded",
+ },
+
+ // #36: IPv6 constraints do not permit IPv4 addresses.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"ip:2000:abcd::/32"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:10.0.0.1"},
+ },
+ expectedError: "\"10.0.0.1\" is not permitted",
+ },
+
+ // #37: IPv4 constraints do not permit IPv6 addresses.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"ip:10.0.0.0/8"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:2000:abcd::"},
+ },
+ expectedError: "\"2000:abcd::\" is not permitted",
+ },
+
+ // #38: an exclusion of an unknown type doesn't affect other names.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"unknown:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #39: a permitted subtree of an unknown type doesn't affect other
+ // name types.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"unknown:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #40: exact email constraints work
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@example.com"},
+ },
+ },
+
+ // #41: exact email constraints are effective
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:bar@example.com"},
+ },
+ expectedError: "\"bar@example.com\" is not permitted",
+ },
+
+ // #42: email canonicalisation works.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@example.com"},
+ },
+ },
+
+ // #44: a leading dot matches hosts one level deep
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@sub.example.com"},
+ },
+ },
+
+ // #45: a leading dot does not match the host itself
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@example.com"},
+ },
+ expectedError: "\"foo@example.com\" is not permitted",
+ },
+
+ // #46: a leading dot also matches two (or more) levels deep.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:.example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@sub.sub.example.com"},
+ },
+ },
+
+ // #47: the local part of an email is case-sensitive
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:foo@example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:Foo@example.com"},
+ },
+ expectedError: "\"Foo@example.com\" is not permitted",
+ },
+
+ // #48: the domain part of an email is not case-sensitive
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:foo@EXAMPLE.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@example.com"},
+ },
+ },
+
+ // #49: the domain part of a DNS constraint is also not case-sensitive.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:EXAMPLE.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #50: URI constraints only cover the host part of the URI
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{
+ "uri:http://example.com/bar",
+ "uri:http://example.com:8080/",
+ "uri:https://example.com/wibble#bar",
+ },
+ },
+ },
+
+ // #51: URIs with IPs are rejected
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:http://1.2.3.4/"},
+ },
+ expectedError: "URI with IP",
+ },
+
+ // #52: URIs with IPs and ports are rejected
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:http://1.2.3.4:43/"},
+ },
+ expectedError: "URI with IP",
+ },
+
+ // #53: URIs with IPv6 addresses are also rejected
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:http://[2006:abcd::1]/"},
+ },
+ expectedError: "URI with IP",
+ },
+
+ // #54: URIs with IPv6 addresses with ports are also rejected
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:http://[2006:abcd::1]:16/"},
+ },
+ expectedError: "URI with IP",
+ },
+
+ // #55: URI constraints are effective
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:http://bar.com/"},
+ },
+ expectedError: "\"http://bar.com/\" is not permitted",
+ },
+
+ // #56: URI constraints are effective
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"uri:foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:http://foo.com/"},
+ },
+ expectedError: "\"http://foo.com/\" is excluded",
+ },
+
+ // #57: URI constraints can allow subdomains
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:http://www.foo.com/"},
+ },
+ },
+
+ // #58: excluding an IPv4-mapped-IPv6 address doesn't affect the IPv4
+ // version of that address.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"ip:::ffff:1.2.3.4/128"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:1.2.3.4"},
+ },
+ },
+
+ // #59: a URI constraint isn't matched by a URN.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:example.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []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.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"ip:1.2.3.0/24"},
+ bad: []string{"ip:::0/0"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"ip:1.2.3.4"},
+ },
+ },
+
+ // #61: omitting extended key usage in a CA certificate implies that
+ // any usage is ok.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth", "other"},
+ },
+ },
+
+ // #62: The “any” EKU also means that any usage is ok.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"any"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth", "other"},
+ },
+ },
+
+ // #63: An intermediate with enumerated EKUs causes a failure if we
+ // test for an EKU not in that set. (ServerAuth is required by
+ // default.)
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"email"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth"},
+ },
+ expectedError: "incompatible key usage",
+ },
+
+ // #64: an unknown EKU in the leaf doesn't break anything, even if it's not
+ // correctly nested.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"email"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"other"},
+ },
+ requestedEKUs: []ExtKeyUsage{ExtKeyUsageAny},
+ },
+
+ // #65: trying to add extra permitted key usages in an intermediate
+ // (after a limitation in the root) is acceptable so long as the leaf
+ // certificate doesn't use them.
+ {
+ roots: []constraintsSpec{
+ {
+ ekus: []string{"serverAuth"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"serverAuth", "email"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth"},
+ },
+ },
+
+ // #66: EKUs in roots are not ignored.
+ {
+ roots: []constraintsSpec{
+ {
+ ekus: []string{"email"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"serverAuth"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth"},
+ },
+ expectedError: "incompatible key usage",
+ },
+
+ // #67: SGC key usages used to permit serverAuth and clientAuth,
+ // but don't anymore.
+ {
+ roots: []constraintsSpec{
+ {},
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"netscapeSGC"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth", "clientAuth"},
+ },
+ expectedError: "incompatible key usage",
+ },
+
+ // #68: SGC key usages used to permit serverAuth and clientAuth,
+ // but don't anymore.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"msSGC"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth", "clientAuth"},
+ },
+ expectedError: "incompatible key usage",
+ },
+
+ // #69: an empty DNS constraint should allow anything.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ },
+
+ // #70: an empty DNS constraint should also reject everything.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"dns:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ },
+ expectedError: "\"example.com\" is excluded",
+ },
+
+ // #71: an empty email constraint should allow anything
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"email:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@example.com"},
+ },
+ },
+
+ // #72: an empty email constraint should also reject everything.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"email:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:foo@example.com"},
+ },
+ expectedError: "\"foo@example.com\" is excluded",
+ },
+
+ // #73: an empty URI constraint should allow anything
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"uri:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:https://example.com/test"},
+ },
+ },
+
+ // #74: an empty URI constraint should also reject everything.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"uri:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"uri:https://example.com/test"},
+ },
+ expectedError: "\"https://example.com/test\" is excluded",
+ },
+
+ // #75: serverAuth in a leaf shouldn't permit clientAuth when requested in
+ // VerifyOptions.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"serverAuth"},
+ },
+ requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth},
+ expectedError: "incompatible key usage",
+ },
+
+ // #76: MSSGC in a leaf used to match a request for serverAuth, but doesn't
+ // anymore.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"msSGC"},
+ },
+ requestedEKUs: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ expectedError: "incompatible key usage",
+ },
+
+ // An invalid DNS SAN should be detected only at validation time so
+ // that we can process CA certificates in the wild that have invalid SANs.
+ // See https://github.com/golang/go/issues/23995
+
+ // #77: an invalid DNS or mail SAN will not be detected if name constraint
+ // checking is not triggered.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:this is invalid", "email:this @ is invalid"},
+ },
+ },
+
+ // #78: an invalid DNS SAN will be detected if any name constraint checking
+ // is triggered.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"uri:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:this is invalid"},
+ },
+ expectedError: "cannot parse dnsName",
+ },
+
+ // #79: an invalid email SAN will be detected if any name constraint
+ // checking is triggered.
+ {
+ roots: []constraintsSpec{
+ {
+ bad: []string{"uri:"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"email:this @ is invalid"},
+ },
+ expectedError: "cannot parse rfc822Name",
+ },
+
+ // #80: if several EKUs are requested, satisfying any of them is sufficient.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ ekus: []string{"email"},
+ },
+ requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth, ExtKeyUsageEmailProtection},
+ },
+
+ // #81: EKUs that are not asserted in VerifyOpts are not required to be
+ // nested.
+ {
+ roots: make([]constraintsSpec, 1),
+ intermediates: [][]constraintsSpec{
+ {
+ {
+ ekus: []string{"serverAuth"},
+ },
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{"dns:example.com"},
+ // There's no email EKU in the intermediate. This would be rejected if
+ // full nesting was required.
+ ekus: []string{"email", "serverAuth"},
+ },
+ },
+
+ // #82: a certificate without SANs and CN is accepted in a constrained chain.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]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.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]constraintsSpec{
+ {
+ {},
+ },
+ },
+ leaf: leafSpec{
+ sans: []string{},
+ cn: "foo,bar",
+ },
+ },
+
+ // #84: a certificate with SANs and CN is accepted in a constrained chain.
+ {
+ roots: []constraintsSpec{
+ {
+ ok: []string{"dns:foo.com", "dns:.foo.com"},
+ },
+ },
+ intermediates: [][]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) {
+ 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(leaf leafSpec, 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{
+ OrganizationalUnit: []string{"Leaf"},
+ CommonName: leaf.cn,
+ },
+ NotBefore: time.Unix(1000, 0),
+ NotAfter: time.Unix(2000, 0),
+ KeyUsage: KeyUsageDigitalSignature,
+ BasicConstraintsValid: true,
+ IsCA: false,
+ }
+
+ for _, name := range leaf.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, "invalidip:"):
+ ipBytes, err := hex.DecodeString(name[10:])
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse invalid IP: %s", err)
+ }
+ template.IPAddresses = append(template.IPAddresses, net.IP(ipBytes))
+
+ 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(leaf.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)
+ }
+ }
+
+ var err error
+ if template.ExtKeyUsage, template.UnknownExtKeyUsage, err = parseEKUs(leaf.ekus); 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
+ }
+
+ 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
+ }
+
+ if template.ExtKeyUsage, template.UnknownExtKeyUsage, err = parseEKUs(constraints.ekus); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func parseEKUs(ekuStrs []string) (ekus []ExtKeyUsage, unknowns []asn1.ObjectIdentifier, err error) {
+ for _, s := range ekuStrs {
+ switch s {
+ case "serverAuth":
+ ekus = append(ekus, ExtKeyUsageServerAuth)
+ case "clientAuth":
+ ekus = append(ekus, ExtKeyUsageClientAuth)
+ case "email":
+ ekus = append(ekus, ExtKeyUsageEmailProtection)
+ case "netscapeSGC":
+ ekus = append(ekus, ExtKeyUsageNetscapeServerGatedCrypto)
+ case "msSGC":
+ ekus = append(ekus, ExtKeyUsageMicrosoftServerGatedCrypto)
+ case "any":
+ ekus = append(ekus, ExtKeyUsageAny)
+ case "other":
+ unknowns = append(unknowns, asn1.ObjectIdentifier{2, 4, 1, 2, 3})
+ default:
+ return nil, nil, fmt.Errorf("unknown EKU %q", s)
+ }
+ }
+
+ return
+}
+
+func TestConstraintCases(t *testing.T) {
+ privateKeys := sync.Pool{
+ New: func() any {
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+ return priv
+ },
+ }
+
+ for i, test := range nameConstraintsTests {
+ t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
+ 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("failed to create root: %s", 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("failed to create %q: %s", 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("cannot create leaf: %s", err)
+ }
+
+ // 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(t, leafCert, intermediatePool, rootPool)
+ if err == nil && len(test.expectedError) > 0 {
+ t.Error("unexpectedly succeeded against OpenSSL")
+ if debugOpenSSLFailure {
+ return
+ }
+ }
+
+ if err != nil {
+ if _, ok := err.(*exec.ExitError); !ok {
+ t.Errorf("OpenSSL failed to run: %s", err)
+ } else if len(test.expectedError) == 0 {
+ t.Errorf("OpenSSL unexpectedly failed: %v", output)
+ if debugOpenSSLFailure {
+ return
+ }
+ }
+ }
+ }
+
+ verifyOpts := VerifyOptions{
+ Roots: rootPool,
+ Intermediates: intermediatePool,
+ CurrentTime: time.Unix(1500, 0),
+ KeyUsages: test.requestedEKUs,
+ }
+ _, err = leafCert.Verify(verifyOpts)
+
+ logInfo := true
+ if len(test.expectedError) == 0 {
+ if err != nil {
+ t.Errorf("unexpected failure: %s", err)
+ } else {
+ logInfo = false
+ }
+ } else {
+ if err == nil {
+ t.Error("unexpected success")
+ } else if !strings.Contains(err.Error(), test.expectedError) {
+ t.Errorf("expected error containing %q, but got: %s", 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 buf.String()
+ }
+ t.Errorf("root:\n%s", certAsPEM(rootPool.mustCert(t, 0)))
+ if intermediates := allCerts(t, intermediatePool); len(intermediates) > 0 {
+ for ii, intermediate := range intermediates {
+ t.Errorf("intermediate %d:\n%s", ii, certAsPEM(intermediate))
+ }
+ }
+ t.Errorf("leaf:\n%s", certAsPEM(leafCert))
+ }
+
+ for _, key := range keys {
+ privateKeys.Put(key)
+ }
+ })
+ }
+}
+
+func writePEMsToTempFile(certs []*Certificate) *os.File {
+ file, err := os.CreateTemp("", "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(t *testing.T, leaf *Certificate, intermediates, roots *CertPool) (string, error) {
+ args := []string{"verify", "-no_check_time"}
+
+ rootsFile := writePEMsToTempFile(allCerts(t, roots))
+ if debugOpenSSLFailure {
+ println("roots file:", rootsFile.Name())
+ } else {
+ defer os.Remove(rootsFile.Name())
+ }
+ args = append(args, "-CAfile", rootsFile.Name())
+
+ if intermediates.len() > 0 {
+ intermediatesFile := writePEMsToTempFile(allCerts(t, intermediates))
+ 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 output.String(), 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) {
+ constraintParseError := func(err error) bool {
+ str := err.Error()
+ return strings.Contains(str, "failed to parse ") && strings.Contains(str, "constraint")
+ }
+
+ encodingError := func(err error) bool {
+ return strings.Contains(err.Error(), "cannot be encoded as an IA5String")
+ }
+
+ // Bad names in constraints should not parse.
+ badNames := []struct {
+ name string
+ matcher func(error) bool
+ }{
+ {"dns:foo.com.", constraintParseError},
+ {"email:abc@foo.com.", constraintParseError},
+ {"email:foo.com.", constraintParseError},
+ {"uri:example.com.", constraintParseError},
+ {"uri:1.2.3.4", constraintParseError},
+ {"uri:ffff::1", constraintParseError},
+ {"dns:not–hyphen.com", encodingError},
+ {"email:foo@not–hyphen.com", encodingError},
+ {"uri:not–hyphen.com", encodingError},
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, test := range badNames {
+ _, err := makeConstraintsCACert(constraintsSpec{
+ ok: []string{test.name},
+ }, "TestAbsoluteNamesInConstraints", priv, nil, priv)
+
+ if err == nil {
+ t.Errorf("bad name %q unexpectedly accepted in name constraint", test.name)
+ continue
+ } else {
+ if !test.matcher(err) {
+ t.Errorf("bad name %q triggered unrecognised error: %s", test.name, err)
+ }
+ }
+ }
+}
+
+func TestBadNamesInSANs(t *testing.T) {
+ // Bad names in URI and IP SANs should not parse. Bad DNS and email SANs
+ // will parse and are tested in name constraint tests at the top of this
+ // file.
+ badNames := []string{
+ "uri:https://example.com./dsf",
+ "invalidip:0102",
+ "invalidip:0102030405",
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, badName := range badNames {
+ _, err := makeConstraintsLeafCert(leafSpec{sans: []string{badName}}, priv, nil, priv)
+
+ if err == nil {
+ t.Errorf("bad name %q unexpectedly accepted in SAN", badName)
+ continue
+ }
+
+ if str := err.Error(); !strings.Contains(str, "cannot parse ") {
+ t.Errorf("bad name %q triggered unrecognised error: %s", badName, str)
+ }
+ }
+}