From ccd992355df7192993c666236047820244914598 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 21:19:13 +0200 Subject: Adding upstream version 1.21.8. Signed-off-by: Daniel Baumann --- src/crypto/x509/name_constraints_test.go | 2171 ++++++++++++++++++++++++++++++ 1 file changed, 2171 insertions(+) create mode 100644 src/crypto/x509/name_constraints_test.go (limited to 'src/crypto/x509/name_constraints_test.go') 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) + } + } +} -- cgit v1.2.3