diff options
Diffstat (limited to 'security/pkix/test/gtest/pkixnames_tests.cpp')
-rw-r--r-- | security/pkix/test/gtest/pkixnames_tests.cpp | 2811 |
1 files changed, 2811 insertions, 0 deletions
diff --git a/security/pkix/test/gtest/pkixnames_tests.cpp b/security/pkix/test/gtest/pkixnames_tests.cpp new file mode 100644 index 000000000..9e2303ad0 --- /dev/null +++ b/security/pkix/test/gtest/pkixnames_tests.cpp @@ -0,0 +1,2811 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This code is made available to you under your choice of the following sets + * of licensing terms: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +/* Copyright 2014 Mozilla Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "pkixcheck.h" +#include "pkixder.h" +#include "pkixgtest.h" +#include "pkixutil.h" + +namespace mozilla { namespace pkix { + +Result MatchPresentedDNSIDWithReferenceDNSID(Input presentedDNSID, + Input referenceDNSID, + /*out*/ bool& matches); + +bool IsValidReferenceDNSID(Input hostname); +bool IsValidPresentedDNSID(Input hostname); +bool ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]); +bool ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]); + +} } // namespace mozilla::pkix + +using namespace mozilla::pkix; +using namespace mozilla::pkix::test; + +struct PresentedMatchesReference +{ + ByteString presentedDNSID; + ByteString referenceDNSID; + Result expectedResult; + bool expectedMatches; // only valid when expectedResult == Success +}; + +#define DNS_ID_MATCH(a, b) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(a), sizeof(a) - 1), \ + ByteString(reinterpret_cast<const uint8_t*>(b), sizeof(b) - 1), \ + Success, \ + true \ + } + +#define DNS_ID_MISMATCH(a, b) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(a), sizeof(a) - 1), \ + ByteString(reinterpret_cast<const uint8_t*>(b), sizeof(b) - 1), \ + Success, \ + false \ + } + +#define DNS_ID_BAD_DER(a, b) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(a), sizeof(a) - 1), \ + ByteString(reinterpret_cast<const uint8_t*>(b), sizeof(b) - 1), \ + Result::ERROR_BAD_DER, \ + false \ + } + +static const PresentedMatchesReference DNSID_MATCH_PARAMS[] = +{ + DNS_ID_BAD_DER("", "a"), + + DNS_ID_MATCH("a", "a"), + DNS_ID_MISMATCH("b", "a"), + + DNS_ID_MATCH("*.b.a", "c.b.a"), + DNS_ID_MISMATCH("*.b.a", "b.a"), + DNS_ID_MISMATCH("*.b.a", "b.a."), + + // We allow underscores for compatibility with existing practices. + DNS_ID_MATCH("a_b", "a_b"), + DNS_ID_MATCH("*.example.com", "uses_underscore.example.com"), + DNS_ID_MATCH("*.uses_underscore.example.com", "a.uses_underscore.example.com"), + + // See bug 1139039 + DNS_ID_MATCH("_.example.com", "_.example.com"), + DNS_ID_MATCH("*.example.com", "_.example.com"), + DNS_ID_MATCH("_", "_"), + DNS_ID_MATCH("___", "___"), + DNS_ID_MATCH("example_", "example_"), + DNS_ID_MATCH("_example", "_example"), + DNS_ID_MATCH("*._._", "x._._"), + + // See bug 1139039 + // A DNS-ID must not end in an all-numeric label. We don't consider + // underscores to be numeric. + DNS_ID_MATCH("_1", "_1"), + DNS_ID_MATCH("example._1", "example._1"), + DNS_ID_MATCH("example.1_", "example.1_"), + + // Wildcard not in leftmost label + DNS_ID_MATCH("d.c.b.a", "d.c.b.a"), + DNS_ID_BAD_DER("d.*.b.a", "d.c.b.a"), + DNS_ID_BAD_DER("d.c*.b.a", "d.c.b.a"), + DNS_ID_BAD_DER("d.c*.b.a", "d.cc.b.a"), + + // case sensitivity + DNS_ID_MATCH("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"), + DNS_ID_MATCH("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), + DNS_ID_MATCH("aBc", "Abc"), + + // digits + DNS_ID_MATCH("a1", "a1"), + + // A trailing dot indicates an absolute name. Absolute presented names are + // not allowed, but absolute reference names are allowed. + DNS_ID_MATCH("example", "example"), + DNS_ID_BAD_DER("example.", "example."), + DNS_ID_MATCH("example", "example."), + DNS_ID_BAD_DER("example.", "example"), + DNS_ID_MATCH("example.com", "example.com"), + DNS_ID_BAD_DER("example.com.", "example.com."), + DNS_ID_MATCH("example.com", "example.com."), + DNS_ID_BAD_DER("example.com.", "example.com"), + DNS_ID_BAD_DER("example.com..", "example.com."), + DNS_ID_BAD_DER("example.com..", "example.com"), + DNS_ID_BAD_DER("example.com...", "example.com."), + + // xn-- IDN prefix + DNS_ID_BAD_DER("x*.b.a", "xa.b.a"), + DNS_ID_BAD_DER("x*.b.a", "xna.b.a"), + DNS_ID_BAD_DER("x*.b.a", "xn-a.b.a"), + DNS_ID_BAD_DER("x*.b.a", "xn--a.b.a"), + DNS_ID_BAD_DER("xn*.b.a", "xn--a.b.a"), + DNS_ID_BAD_DER("xn-*.b.a", "xn--a.b.a"), + DNS_ID_BAD_DER("xn--*.b.a", "xn--a.b.a"), + DNS_ID_BAD_DER("xn*.b.a", "xn--a.b.a"), + DNS_ID_BAD_DER("xn-*.b.a", "xn--a.b.a"), + DNS_ID_BAD_DER("xn--*.b.a", "xn--a.b.a"), + DNS_ID_BAD_DER("xn---*.b.a", "xn--a.b.a"), + + // "*" cannot expand to nothing. + DNS_ID_BAD_DER("c*.b.a", "c.b.a"), + + ///////////////////////////////////////////////////////////////////////////// + // These are test cases adapted from Chromium's x509_certificate_unittest.cc. + // The parameter order is the opposite in Chromium's tests. Also, some tests + // were modified to fit into this framework or due to intentional differences + // between mozilla::pkix and Chromium. + + DNS_ID_MATCH("foo.com", "foo.com"), + DNS_ID_MATCH("f", "f"), + DNS_ID_MISMATCH("i", "h"), + DNS_ID_MATCH("*.foo.com", "bar.foo.com"), + DNS_ID_MATCH("*.test.fr", "www.test.fr"), + DNS_ID_MATCH("*.test.FR", "wwW.tESt.fr"), + DNS_ID_BAD_DER(".uk", "f.uk"), + DNS_ID_BAD_DER("?.bar.foo.com", "w.bar.foo.com"), + DNS_ID_BAD_DER("(www|ftp).foo.com", "www.foo.com"), // regex! + DNS_ID_BAD_DER("www.foo.com\0", "www.foo.com"), + DNS_ID_BAD_DER("www.foo.com\0*.foo.com", "www.foo.com"), + DNS_ID_MISMATCH("ww.house.example", "www.house.example"), + DNS_ID_MISMATCH("www.test.org", "test.org"), + DNS_ID_MISMATCH("*.test.org", "test.org"), + DNS_ID_BAD_DER("*.org", "test.org"), + DNS_ID_BAD_DER("w*.bar.foo.com", "w.bar.foo.com"), + DNS_ID_BAD_DER("ww*ww.bar.foo.com", "www.bar.foo.com"), + DNS_ID_BAD_DER("ww*ww.bar.foo.com", "wwww.bar.foo.com"), + + // Different than Chromium, matches NSS. + DNS_ID_BAD_DER("w*w.bar.foo.com", "wwww.bar.foo.com"), + + DNS_ID_BAD_DER("w*w.bar.foo.c0m", "wwww.bar.foo.com"), + + // '*' must be the only character in the wildcard label + DNS_ID_BAD_DER("wa*.bar.foo.com", "WALLY.bar.foo.com"), + + // We require "*" to be the last character in a wildcard label, but + // Chromium does not. + DNS_ID_BAD_DER("*Ly.bar.foo.com", "wally.bar.foo.com"), + + // Chromium does URL decoding of the reference ID, but we don't, and we also + // require that the reference ID is valid, so we can't test these two. + // DNS_ID_MATCH("www.foo.com", "ww%57.foo.com"), + // DNS_ID_MATCH("www&.foo.com", "www%26.foo.com"), + + DNS_ID_MISMATCH("*.test.de", "www.test.co.jp"), + DNS_ID_BAD_DER("*.jp", "www.test.co.jp"), + DNS_ID_MISMATCH("www.test.co.uk", "www.test.co.jp"), + DNS_ID_BAD_DER("www.*.co.jp", "www.test.co.jp"), + DNS_ID_MATCH("www.bar.foo.com", "www.bar.foo.com"), + DNS_ID_MISMATCH("*.foo.com", "www.bar.foo.com"), + DNS_ID_BAD_DER("*.*.foo.com", "www.bar.foo.com"), + DNS_ID_BAD_DER("*.*.foo.com", "www.bar.foo.com"), + + // Our matcher requires the reference ID to be a valid DNS name, so we cannot + // test this case. + //DNS_ID_BAD_DER("*.*.bar.foo.com", "*..bar.foo.com"), + + DNS_ID_MATCH("www.bath.org", "www.bath.org"), + + // Our matcher requires the reference ID to be a valid DNS name, so we cannot + // test these cases. + // DNS_ID_BAD_DER("www.bath.org", ""), + // DNS_ID_BAD_DER("www.bath.org", "20.30.40.50"), + // DNS_ID_BAD_DER("www.bath.org", "66.77.88.99"), + + // IDN tests + DNS_ID_MATCH("xn--poema-9qae5a.com.br", "xn--poema-9qae5a.com.br"), + DNS_ID_MATCH("*.xn--poema-9qae5a.com.br", "www.xn--poema-9qae5a.com.br"), + DNS_ID_MISMATCH("*.xn--poema-9qae5a.com.br", "xn--poema-9qae5a.com.br"), + DNS_ID_BAD_DER("xn--poema-*.com.br", "xn--poema-9qae5a.com.br"), + DNS_ID_BAD_DER("xn--*-9qae5a.com.br", "xn--poema-9qae5a.com.br"), + DNS_ID_BAD_DER("*--poema-9qae5a.com.br", "xn--poema-9qae5a.com.br"), + + // The following are adapted from the examples quoted from + // http://tools.ietf.org/html/rfc6125#section-6.4.3 + // (e.g., *.example.com would match foo.example.com but + // not bar.foo.example.com or example.com). + DNS_ID_MATCH("*.example.com", "foo.example.com"), + DNS_ID_MISMATCH("*.example.com", "bar.foo.example.com"), + DNS_ID_MISMATCH("*.example.com", "example.com"), + // (e.g., baz*.example.net and *baz.example.net and b*z.example.net would + // be taken to match baz1.example.net and foobaz.example.net and + // buzz.example.net, respectively. However, we don't allow any characters + // other than '*' in the wildcard label. + DNS_ID_BAD_DER("baz*.example.net", "baz1.example.net"), + + // Both of these are different from Chromium, but match NSS, becaues the + // wildcard character "*" is not the last character of the label. + DNS_ID_BAD_DER("*baz.example.net", "foobaz.example.net"), + DNS_ID_BAD_DER("b*z.example.net", "buzz.example.net"), + + // Wildcards should not be valid for public registry controlled domains, + // and unknown/unrecognized domains, at least three domain components must + // be present. For mozilla::pkix and NSS, there must always be at least two + // labels after the wildcard label. + DNS_ID_MATCH("*.test.example", "www.test.example"), + DNS_ID_MATCH("*.example.co.uk", "test.example.co.uk"), + DNS_ID_BAD_DER("*.exmaple", "test.example"), + + // The result is different than Chromium, because Chromium takes into account + // the additional knowledge it has that "co.uk" is a TLD. mozilla::pkix does + // not know that. + DNS_ID_MATCH("*.co.uk", "example.co.uk"), + + DNS_ID_BAD_DER("*.com", "foo.com"), + DNS_ID_BAD_DER("*.us", "foo.us"), + DNS_ID_BAD_DER("*", "foo"), + + // IDN variants of wildcards and registry controlled domains. + DNS_ID_MATCH("*.xn--poema-9qae5a.com.br", "www.xn--poema-9qae5a.com.br"), + DNS_ID_MATCH("*.example.xn--mgbaam7a8h", "test.example.xn--mgbaam7a8h"), + + // RFC6126 allows this, and NSS accepts it, but Chromium disallows it. + // TODO: File bug against Chromium. + DNS_ID_MATCH("*.com.br", "xn--poema-9qae5a.com.br"), + + DNS_ID_BAD_DER("*.xn--mgbaam7a8h", "example.xn--mgbaam7a8h"), + // Wildcards should be permissible for 'private' registry-controlled + // domains. (In mozilla::pkix, we do not know if it is a private registry- + // controlled domain or not.) + DNS_ID_MATCH("*.appspot.com", "www.appspot.com"), + DNS_ID_MATCH("*.s3.amazonaws.com", "foo.s3.amazonaws.com"), + + // Multiple wildcards are not valid. + DNS_ID_BAD_DER("*.*.com", "foo.example.com"), + DNS_ID_BAD_DER("*.bar.*.com", "foo.bar.example.com"), + + // Absolute vs relative DNS name tests. Although not explicitly specified + // in RFC 6125, absolute reference names (those ending in a .) should + // match either absolute or relative presented names. We don't allow + // absolute presented names. + // TODO: File errata against RFC 6125 about this. + DNS_ID_BAD_DER("foo.com.", "foo.com"), + DNS_ID_MATCH("foo.com", "foo.com."), + DNS_ID_BAD_DER("foo.com.", "foo.com."), + DNS_ID_BAD_DER("f.", "f"), + DNS_ID_MATCH("f", "f."), + DNS_ID_BAD_DER("f.", "f."), + DNS_ID_BAD_DER("*.bar.foo.com.", "www-3.bar.foo.com"), + DNS_ID_MATCH("*.bar.foo.com", "www-3.bar.foo.com."), + DNS_ID_BAD_DER("*.bar.foo.com.", "www-3.bar.foo.com."), + + // We require the reference ID to be a valid DNS name, so we cannot test this + // case. + // DNS_ID_MISMATCH(".", "."), + + DNS_ID_BAD_DER("*.com.", "example.com"), + DNS_ID_BAD_DER("*.com", "example.com."), + DNS_ID_BAD_DER("*.com.", "example.com."), + DNS_ID_BAD_DER("*.", "foo."), + DNS_ID_BAD_DER("*.", "foo"), + + // The result is different than Chromium because we don't know that co.uk is + // a TLD. + DNS_ID_MATCH("*.co.uk", "foo.co.uk"), + DNS_ID_MATCH("*.co.uk", "foo.co.uk."), + DNS_ID_BAD_DER("*.co.uk.", "foo.co.uk"), + DNS_ID_BAD_DER("*.co.uk.", "foo.co.uk."), + + DNS_ID_MISMATCH("*.example.com", "localhost"), + DNS_ID_MISMATCH("*.example.com", "localhost."), + // Note that we already have the testcase DNS_ID_BAD_DER("*", "foo") above +}; + +struct InputValidity +{ + ByteString input; + bool isValidReferenceID; + bool isValidPresentedID; +}; + +// str is null-terminated, which is why we subtract 1. str may contain embedded +// nulls (including at the end) preceding the null terminator though. +#define I(str, validReferenceID, validPresentedID) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(str), sizeof(str) - 1), \ + validReferenceID, \ + validPresentedID, \ + } + +static const InputValidity DNSNAMES_VALIDITY[] = +{ + I("a", true, true), + I("a.b", true, true), + I("a.b.c", true, true), + I("a.b.c.d", true, true), + + // empty labels + I("", false, false), + I(".", false, false), + I("a", true, true), + I(".a", false, false), + I(".a.b", false, false), + I("..a", false, false), + I("a..b", false, false), + I("a...b", false, false), + I("a..b.c", false, false), + I("a.b..c", false, false), + I(".a.b.c.", false, false), + + // absolute names (only allowed for reference names) + I("a.", true, false), + I("a.b.", true, false), + I("a.b.c.", true, false), + + // absolute names with empty label at end + I("a..", false, false), + I("a.b..", false, false), + I("a.b.c..", false, false), + I("a...", false, false), + + // Punycode + I("xn--", false, false), + I("xn--.", false, false), + I("xn--.a", false, false), + I("a.xn--", false, false), + I("a.xn--.", false, false), + I("a.xn--.b", false, false), + I("a.xn--.b", false, false), + I("a.xn--\0.b", false, false), + I("a.xn--a.b", true, true), + I("xn--a", true, true), + I("a.xn--a", true, true), + I("a.xn--a.a", true, true), + I("\xc4\x95.com", false, false), // UTF-8 ĕ + I("xn--jea.com", true, true), // punycode ĕ + I("xn--\xc4\x95.com", false, false), // UTF-8 ĕ, malformed punycode + UTF-8 mashup + + // Surprising punycode + I("xn--google.com", true, true), // 䕮䕵䕶䕱.com + I("xn--citibank.com", true, true), // 岍岊岊岅岉岎.com + I("xn--cnn.com", true, true), // 䁾.com + I("a.xn--cnn", true, true), // a.䁾 + I("a.xn--cnn.com", true, true), // a.䁾.com + + I("1.2.3.4", false, false), // IPv4 address + I("1::2", false, false), // IPV6 address + + // whitespace not allowed anywhere. + I(" ", false, false), + I(" a", false, false), + I("a ", false, false), + I("a b", false, false), + I("a.b 1", false, false), + I("a\t", false, false), + + // Nulls not allowed + I("\0", false, false), + I("a\0", false, false), + I("example.org\0.example.com", false, false), // Hi Moxie! + I("\0a", false, false), + I("xn--\0", false, false), + + // Allowed character set + I("a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z", true, true), + I("A.B.C.D.E.F.G.H.I.J.K.L.M.N.O.P.Q.R.S.T.U.V.W.X.Y.Z", true, true), + I("0.1.2.3.4.5.6.7.8.9.a", true, true), // "a" needed to avoid numeric last label + I("a-b", true, true), // hyphen (a label cannot start or end with a hyphen) + + // Underscores + I("a_b", true, true), + // See bug 1139039 + I("_", true, true), + I("a_", true, true), + I("_a", true, true), + I("_1", true, true), + I("1_", true, true), + I("___", true, true), + + // An invalid character in various positions + I("!", false, false), + I("!a", false, false), + I("a!", false, false), + I("a!b", false, false), + I("a.!", false, false), + I("a.a!", false, false), + I("a.!a", false, false), + I("a.a!a", false, false), + I("a.!a.a", false, false), + I("a.a!.a", false, false), + I("a.a!a.a", false, false), + + // Various other invalid characters + I("a!", false, false), + I("a@", false, false), + I("a#", false, false), + I("a$", false, false), + I("a%", false, false), + I("a^", false, false), + I("a&", false, false), + I("a*", false, false), + I("a(", false, false), + I("a)", false, false), + + // last label can't be fully numeric + I("1", false, false), + I("a.1", false, false), + + // other labels can be fully numeric + I("1.a", true, true), + I("1.2.a", true, true), + I("1.2.3.a", true, true), + + // last label can be *partly* numeric + I("1a", true, true), + I("1.1a", true, true), + I("1-1", true, true), + I("a.1-1", true, true), + I("a.1-a", true, true), + + // labels cannot start with a hyphen + I("-", false, false), + I("-1", false, false), + + // labels cannot end with a hyphen + I("1-", false, false), + I("1-.a", false, false), + I("a-", false, false), + I("a-.a", false, false), + I("a.1-.a", false, false), + I("a.a-.a", false, false), + + // labels can contain a hyphen in the middle + I("a-b", true, true), + I("1-2", true, true), + I("a.a-1", true, true), + + // multiple consecutive hyphens allowed + I("a--1", true, true), + I("1---a", true, true), + I("a-----------------b", true, true), + + // Wildcard specifications are not valid reference names, but are valid + // presented names if there are enough labels and if '*' is the only + // character in the wildcard label. + I("*.a", false, false), + I("a*", false, false), + I("a*.", false, false), + I("a*.a", false, false), + I("a*.a.", false, false), + I("*.a.b", false, true), + I("*.a.b.", false, false), + I("a*.b.c", false, false), + I("*.a.b.c", false, true), + I("a*.b.c.d", false, false), + + // Multiple wildcards are not allowed. + I("a**.b.c", false, false), + I("a*b*.c.d", false, false), + I("a*.b*.c", false, false), + + // Wildcards are only allowed in the first label. + I("a.*", false, false), + I("a.*.b", false, false), + I("a.b.*", false, false), + I("a.b*.c", false, false), + I("*.b*.c", false, false), + I(".*.a.b", false, false), + I(".a*.b.c", false, false), + + // Wildcards must be at the *end* of the first label. + I("*a.b.c", false, false), + I("a*b.c.d", false, false), + + // Wildcards not allowed with IDNA prefix + I("x*.a.b", false, false), + I("xn*.a.b", false, false), + I("xn-*.a.b", false, false), + I("xn--*.a.b", false, false), + I("xn--w*.a.b", false, false), + + // Redacted labels from RFC6962bis draft 4 + // https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-04#section-3.2.2 + I("(PRIVATE).foo", false, false), + + // maximum label length is 63 characters + I("1234567890" "1234567890" "1234567890" + "1234567890" "1234567890" "1234567890" "abc", true, true), + I("1234567890" "1234567890" "1234567890" + "1234567890" "1234567890" "1234567890" "abcd", false, false), + + // maximum total length is 253 characters + I("1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "12345678" "a", + true, true), + I("1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "1234567890" "." + "1234567890" "1234567890" "1234567890" "1234567890" "123456789" "a", + false, false), +}; + +static const InputValidity DNSNAMES_VALIDITY_TURKISH_I[] = +{ + // http://en.wikipedia.org/wiki/Dotted_and_dotless_I#In_computing + // IDN registration rules disallow "latin capital letter i with dot above," + // but our checks aren't intended to enforce those rules. + I("I", true, true), // ASCII capital I + I("i", true, true), // ASCII lowercase i + I("\xC4\xB0", false, false), // latin capital letter i with dot above + I("\xC4\xB1", false, false), // latin small letter dotless i + I("xn--i-9bb", true, true), // latin capital letter i with dot above, in punycode + I("xn--cfa", true, true), // latin small letter dotless i, in punycode + I("xn--\xC4\xB0", false, false), // latin capital letter i with dot above, mashup + I("xn--\xC4\xB1", false, false), // latin small letter dotless i, mashup +}; + +static const uint8_t LOWERCASE_I_VALUE[1] = { 'i' }; +static const uint8_t UPPERCASE_I_VALUE[1] = { 'I' }; +static const Input LOWERCASE_I(LOWERCASE_I_VALUE); +static const Input UPPERCASE_I(UPPERCASE_I_VALUE); + +template <unsigned int L> +struct IPAddressParams +{ + ByteString input; + bool isValid; + uint8_t expectedValueIfValid[L]; +}; + +#define IPV4_VALID(str, a, b, c, d) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(str), sizeof(str) - 1), \ + true, \ + { a, b, c, d } \ + } + +// The value of expectedValueIfValid must be ignored for invalid IP addresses. +// The value { 73, 73, 73, 73 } is used because it is unlikely to result in an +// accidental match, unlike { 0, 0, 0, 0 }, which is a value we actually test. +#define IPV4_INVALID(str) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(str), sizeof(str) - 1), \ + false, \ + { 73, 73, 73, 73 } \ + } + +static const IPAddressParams<4> IPV4_ADDRESSES[] = +{ + IPV4_INVALID(""), + IPV4_INVALID("1"), + IPV4_INVALID("1.2"), + IPV4_INVALID("1.2.3"), + IPV4_VALID("1.2.3.4", 1, 2, 3, 4), + IPV4_INVALID("1.2.3.4.5"), + + IPV4_INVALID("1.2.3.4a"), // a DNSName! + IPV4_INVALID("a.2.3.4"), // not even a DNSName! + IPV4_INVALID("1::2"), // IPv6 address + + // Whitespace not allowed + IPV4_INVALID(" 1.2.3.4"), + IPV4_INVALID("1.2.3.4 "), + IPV4_INVALID("1 .2.3.4"), + IPV4_INVALID("\n1.2.3.4"), + IPV4_INVALID("1.2.3.4\n"), + + // Nulls not allowed + IPV4_INVALID("\0"), + IPV4_INVALID("\0" "1.2.3.4"), + IPV4_INVALID("1.2.3.4\0"), + IPV4_INVALID("1.2.3.4\0.5"), + + // Range + IPV4_VALID("0.0.0.0", 0, 0, 0, 0), + IPV4_VALID("255.255.255.255", 255, 255, 255, 255), + IPV4_INVALID("256.0.0.0"), + IPV4_INVALID("0.256.0.0"), + IPV4_INVALID("0.0.256.0"), + IPV4_INVALID("0.0.0.256"), + IPV4_INVALID("999.0.0.0"), + IPV4_INVALID("9999999999999999999.0.0.0"), + + // All digits allowed + IPV4_VALID("0.1.2.3", 0, 1, 2, 3), + IPV4_VALID("4.5.6.7", 4, 5, 6, 7), + IPV4_VALID("8.9.0.1", 8, 9, 0, 1), + + // Leading zeros not allowed + IPV4_INVALID("01.2.3.4"), + IPV4_INVALID("001.2.3.4"), + IPV4_INVALID("00000000001.2.3.4"), + IPV4_INVALID("010.2.3.4"), + IPV4_INVALID("1.02.3.4"), + IPV4_INVALID("1.2.03.4"), + IPV4_INVALID("1.2.3.04"), + + // Empty components + IPV4_INVALID(".2.3.4"), + IPV4_INVALID("1..3.4"), + IPV4_INVALID("1.2..4"), + IPV4_INVALID("1.2.3."), + + // Too many components + IPV4_INVALID("1.2.3.4.5"), + IPV4_INVALID("1.2.3.4.5.6"), + IPV4_INVALID("0.1.2.3.4"), + IPV4_INVALID("1.2.3.4.0"), + + // Leading/trailing dot + IPV4_INVALID(".1.2.3.4"), + IPV4_INVALID("1.2.3.4."), + + // Other common forms of IPv4 address + // http://en.wikipedia.org/wiki/IPv4#Address_representations + IPV4_VALID("192.0.2.235", 192, 0, 2, 235), // dotted decimal (control value) + IPV4_INVALID("0xC0.0x00.0x02.0xEB"), // dotted hex + IPV4_INVALID("0301.0000.0002.0353"), // dotted octal + IPV4_INVALID("0xC00002EB"), // non-dotted hex + IPV4_INVALID("3221226219"), // non-dotted decimal + IPV4_INVALID("030000001353"), // non-dotted octal + IPV4_INVALID("192.0.0002.0xEB"), // mixed +}; + +#define IPV6_VALID(str, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(str), sizeof(str) - 1), \ + true, \ + { a, b, c, d, \ + e, f, g, h, \ + i, j, k, l, \ + m, n, o, p } \ + } + +#define IPV6_INVALID(str) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(str), sizeof(str) - 1), \ + false, \ + { 73, 73, 73, 73, \ + 73, 73, 73, 73, \ + 73, 73, 73, 73, \ + 73, 73, 73, 73 } \ + } + +static const IPAddressParams<16> IPV6_ADDRESSES[] = +{ + IPV6_INVALID(""), + IPV6_INVALID("1234"), + IPV6_INVALID("1234:5678"), + IPV6_INVALID("1234:5678:9abc"), + IPV6_INVALID("1234:5678:9abc:def0"), + IPV6_INVALID("1234:5678:9abc:def0:1234:"), + IPV6_INVALID("1234:5678:9abc:def0:1234:5678:"), + IPV6_INVALID("1234:5678:9abc:def0:1234:5678:9abc:"), + IPV6_VALID("1234:5678:9abc:def0:1234:5678:9abc:def0", + 0x12, 0x34, 0x56, 0x78, + 0x9a, 0xbc, 0xde, 0xf0, + 0x12, 0x34, 0x56, 0x78, + 0x9a, 0xbc, 0xde, 0xf0), + IPV6_INVALID("1234:5678:9abc:def0:1234:5678:9abc:def0:"), + IPV6_INVALID(":1234:5678:9abc:def0:1234:5678:9abc:def0"), + IPV6_INVALID("1234:5678:9abc:def0:1234:5678:9abc:def0:0000"), + + // Valid contractions + IPV6_VALID("::1", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01), + IPV6_VALID("::1234", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x12, 0x34), + IPV6_VALID("1234::", + 0x12, 0x34, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + IPV6_VALID("1234::5678", + 0x12, 0x34, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x56, 0x78), + IPV6_VALID("1234:5678::abcd", + 0x12, 0x34, 0x56, 0x78, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xab, 0xcd), + IPV6_VALID("1234:5678:9abc:def0:1234:5678:9abc::", + 0x12, 0x34, 0x56, 0x78, + 0x9a, 0xbc, 0xde, 0xf0, + 0x12, 0x34, 0x56, 0x78, + 0x9a, 0xbc, 0x00, 0x00), + + // Contraction in full IPv6 addresses not allowed + IPV6_INVALID("::1234:5678:9abc:def0:1234:5678:9abc:def0"), // start + IPV6_INVALID("1234:5678:9abc:def0:1234:5678:9abc:def0::"), // end + IPV6_INVALID("1234:5678::9abc:def0:1234:5678:9abc:def0"), // interior + + // Multiple contractions not allowed + IPV6_INVALID("::1::"), + IPV6_INVALID("::1::2"), + IPV6_INVALID("1::2::"), + + // Colon madness! + IPV6_INVALID(":"), + IPV6_INVALID("::"), + IPV6_INVALID(":::"), + IPV6_INVALID("::::"), + IPV6_INVALID(":::1"), + IPV6_INVALID("::::1"), + IPV6_INVALID("1:::2"), + IPV6_INVALID("1::::2"), + IPV6_INVALID("1:2:::"), + IPV6_INVALID("1:2::::"), + IPV6_INVALID("::1234:"), + IPV6_INVALID(":1234::"), + + IPV6_INVALID("01234::"), // too many digits, even if zero + IPV6_INVALID("12345678::"), // too many digits or missing colon + + // uppercase + IPV6_VALID("ABCD:EFAB::", + 0xab, 0xcd, 0xef, 0xab, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + // miXeD CAse + IPV6_VALID("aBcd:eFAb::", + 0xab, 0xcd, 0xef, 0xab, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + // IPv4-style + IPV6_VALID("::2.3.4.5", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x02, 0x03, 0x04, 0x05), + IPV6_VALID("1234::2.3.4.5", + 0x12, 0x34, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x02, 0x03, 0x04, 0x05), + IPV6_VALID("::abcd:2.3.4.5", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xab, 0xcd, + 0x02, 0x03, 0x04, 0x05), + IPV6_VALID("1234:5678:9abc:def0:1234:5678:252.253.254.255", + 0x12, 0x34, 0x56, 0x78, + 0x9a, 0xbc, 0xde, 0xf0, + 0x12, 0x34, 0x56, 0x78, + 252, 253, 254, 255), + IPV6_VALID("1234:5678:9abc:def0:1234::252.253.254.255", + 0x12, 0x34, 0x56, 0x78, + 0x9a, 0xbc, 0xde, 0xf0, + 0x12, 0x34, 0x00, 0x00, + 252, 253, 254, 255), + IPV6_INVALID("1234::252.253.254"), + IPV6_INVALID("::252.253.254"), + IPV6_INVALID("::252.253.254.300"), + IPV6_INVALID("1234::252.253.254.255:"), + IPV6_INVALID("1234::252.253.254.255:5678"), + + // Contractions that don't contract + IPV6_INVALID("::1234:5678:9abc:def0:1234:5678:9abc:def0"), + IPV6_INVALID("1234:5678:9abc:def0:1234:5678:9abc:def0::"), + IPV6_INVALID("1234:5678:9abc:def0::1234:5678:9abc:def0"), + IPV6_INVALID("1234:5678:9abc:def0:1234:5678::252.253.254.255"), + + // With and without leading zeros + IPV6_VALID("::123", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x23), + IPV6_VALID("::0123", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x23), + IPV6_VALID("::012", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x12), + IPV6_VALID("::0012", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x12), + IPV6_VALID("::01", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01), + IPV6_VALID("::001", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01), + IPV6_VALID("::0001", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01), + IPV6_VALID("::0", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + IPV6_VALID("::00", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + IPV6_VALID("::000", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + IPV6_VALID("::0000", + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + IPV6_INVALID("::01234"), + IPV6_INVALID("::00123"), + IPV6_INVALID("::000123"), + + // Trailing zero + IPV6_INVALID("::12340"), + + // Whitespace + IPV6_INVALID(" 1234:5678:9abc:def0:1234:5678:9abc:def0"), + IPV6_INVALID("\t1234:5678:9abc:def0:1234:5678:9abc:def0"), + IPV6_INVALID("\t1234:5678:9abc:def0:1234:5678:9abc:def0\n"), + IPV6_INVALID("1234 :5678:9abc:def0:1234:5678:9abc:def0"), + IPV6_INVALID("1234: 5678:9abc:def0:1234:5678:9abc:def0"), + IPV6_INVALID(":: 2.3.4.5"), + IPV6_INVALID("1234::252.253.254.255 "), + IPV6_INVALID("1234::252.253.254.255\n"), + IPV6_INVALID("1234::252.253. 254.255"), + + // Nulls + IPV6_INVALID("\0"), + IPV6_INVALID("::1\0:2"), + IPV6_INVALID("::1\0"), + IPV6_INVALID("::1.2.3.4\0"), + IPV6_INVALID("::1.2\02.3.4"), +}; + +class pkixnames_MatchPresentedDNSIDWithReferenceDNSID + : public ::testing::Test + , public ::testing::WithParamInterface<PresentedMatchesReference> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_MatchPresentedDNSIDWithReferenceDNSID, + MatchPresentedDNSIDWithReferenceDNSID) +{ + const PresentedMatchesReference& param(GetParam()); + SCOPED_TRACE(param.presentedDNSID.c_str()); + SCOPED_TRACE(param.referenceDNSID.c_str()); + Input presented; + ASSERT_EQ(Success, presented.Init(param.presentedDNSID.data(), + param.presentedDNSID.length())); + Input reference; + ASSERT_EQ(Success, reference.Init(param.referenceDNSID.data(), + param.referenceDNSID.length())); + + // sanity check that test makes sense + ASSERT_TRUE(IsValidReferenceDNSID(reference)); + + bool matches; + ASSERT_EQ(param.expectedResult, + MatchPresentedDNSIDWithReferenceDNSID(presented, reference, + matches)); + if (param.expectedResult == Success) { + ASSERT_EQ(param.expectedMatches, matches); + } +} + +INSTANTIATE_TEST_CASE_P(pkixnames_MatchPresentedDNSIDWithReferenceDNSID, + pkixnames_MatchPresentedDNSIDWithReferenceDNSID, + testing::ValuesIn(DNSID_MATCH_PARAMS)); + +class pkixnames_Turkish_I_Comparison + : public ::testing::Test + , public ::testing::WithParamInterface<InputValidity> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_Turkish_I_Comparison, MatchPresentedDNSIDWithReferenceDNSID) +{ + // Make sure we don't have the similar problems that strcasecmp and others + // have with the other kinds of "i" and "I" commonly used in Turkish locales. + + const InputValidity& inputValidity(GetParam()); + SCOPED_TRACE(inputValidity.input.c_str()); + Input input; + ASSERT_EQ(Success, input.Init(inputValidity.input.data(), + inputValidity.input.length())); + + bool isASCII = InputsAreEqual(LOWERCASE_I, input) || + InputsAreEqual(UPPERCASE_I, input); + { + bool matches; + ASSERT_EQ(inputValidity.isValidPresentedID ? Success + : Result::ERROR_BAD_DER, + MatchPresentedDNSIDWithReferenceDNSID(input, LOWERCASE_I, + matches)); + if (inputValidity.isValidPresentedID) { + ASSERT_EQ(isASCII, matches); + } + } + { + bool matches; + ASSERT_EQ(inputValidity.isValidPresentedID ? Success + : Result::ERROR_BAD_DER, + MatchPresentedDNSIDWithReferenceDNSID(input, UPPERCASE_I, + matches)); + if (inputValidity.isValidPresentedID) { + ASSERT_EQ(isASCII, matches); + } + } +} + +INSTANTIATE_TEST_CASE_P(pkixnames_Turkish_I_Comparison, + pkixnames_Turkish_I_Comparison, + testing::ValuesIn(DNSNAMES_VALIDITY_TURKISH_I)); + +class pkixnames_IsValidReferenceDNSID + : public ::testing::Test + , public ::testing::WithParamInterface<InputValidity> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_IsValidReferenceDNSID, IsValidReferenceDNSID) +{ + const InputValidity& inputValidity(GetParam()); + SCOPED_TRACE(inputValidity.input.c_str()); + Input input; + ASSERT_EQ(Success, input.Init(inputValidity.input.data(), + inputValidity.input.length())); + ASSERT_EQ(inputValidity.isValidReferenceID, IsValidReferenceDNSID(input)); + ASSERT_EQ(inputValidity.isValidPresentedID, IsValidPresentedDNSID(input)); +} + +INSTANTIATE_TEST_CASE_P(pkixnames_IsValidReferenceDNSID, + pkixnames_IsValidReferenceDNSID, + testing::ValuesIn(DNSNAMES_VALIDITY)); +INSTANTIATE_TEST_CASE_P(pkixnames_IsValidReferenceDNSID_Turkish_I, + pkixnames_IsValidReferenceDNSID, + testing::ValuesIn(DNSNAMES_VALIDITY_TURKISH_I)); + +class pkixnames_ParseIPv4Address + : public ::testing::Test + , public ::testing::WithParamInterface<IPAddressParams<4>> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_ParseIPv4Address, ParseIPv4Address) +{ + const IPAddressParams<4>& param(GetParam()); + SCOPED_TRACE(param.input.c_str()); + Input input; + ASSERT_EQ(Success, input.Init(param.input.data(), + param.input.length())); + uint8_t ipAddress[4]; + ASSERT_EQ(param.isValid, ParseIPv4Address(input, ipAddress)); + if (param.isValid) { + for (size_t i = 0; i < sizeof(ipAddress); ++i) { + ASSERT_EQ(param.expectedValueIfValid[i], ipAddress[i]); + } + } +} + +INSTANTIATE_TEST_CASE_P(pkixnames_ParseIPv4Address, + pkixnames_ParseIPv4Address, + testing::ValuesIn(IPV4_ADDRESSES)); + +class pkixnames_ParseIPv6Address + : public ::testing::Test + , public ::testing::WithParamInterface<IPAddressParams<16>> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_ParseIPv6Address, ParseIPv6Address) +{ + const IPAddressParams<16>& param(GetParam()); + SCOPED_TRACE(param.input.c_str()); + Input input; + ASSERT_EQ(Success, input.Init(param.input.data(), + param.input.length())); + uint8_t ipAddress[16]; + ASSERT_EQ(param.isValid, ParseIPv6Address(input, ipAddress)); + if (param.isValid) { + for (size_t i = 0; i < sizeof(ipAddress); ++i) { + ASSERT_EQ(param.expectedValueIfValid[i], ipAddress[i]); + } + } +} + +INSTANTIATE_TEST_CASE_P(pkixnames_ParseIPv6Address, + pkixnames_ParseIPv6Address, + testing::ValuesIn(IPV6_ADDRESSES)); + +// This is an arbitrary string that is used to indicate that no SAN extension +// should be put into the generated certificate. It needs to be different from +// "" or any other subjectAltName value that we actually want to test, but its +// actual value does not matter. Note that this isn't a correctly-encoded SAN +// extension value! +static const ByteString + NO_SAN(reinterpret_cast<const uint8_t*>("I'm a bad, bad, certificate")); + +struct CheckCertHostnameParams +{ + ByteString hostname; + ByteString subject; + ByteString subjectAltName; + Result result; +}; + +class pkixnames_CheckCertHostname + : public ::testing::Test + , public ::testing::WithParamInterface<CheckCertHostnameParams> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +#define WITH_SAN(r, ps, psan, result) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(r), sizeof(r) - 1), \ + ps, \ + psan, \ + result \ + } + +#define WITHOUT_SAN(r, ps, result) \ + { \ + ByteString(reinterpret_cast<const uint8_t*>(r), sizeof(r) - 1), \ + ps, \ + NO_SAN, \ + result \ + } + +static const uint8_t example_com[] = { + 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm' +}; + +// Note that We avoid zero-valued bytes in these IP addresses so that we don't +// get false negatives from anti-NULL-byte defenses in dNSName decoding. +static const uint8_t ipv4_addr_bytes[] = { + 1, 2, 3, 4 +}; +static const uint8_t ipv4_addr_bytes_as_str[] = "\x01\x02\x03\x04"; +static const uint8_t ipv4_addr_str[] = "1.2.3.4"; +static const uint8_t ipv4_addr_bytes_FFFFFFFF[8] = { + 1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff +}; + +static const uint8_t ipv4_compatible_ipv6_addr_bytes[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 1, 2, 3, 4 +}; +static const uint8_t ipv4_compatible_ipv6_addr_str[] = "::1.2.3.4"; + +static const uint8_t ipv4_mapped_ipv6_addr_bytes[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0xFF, 0xFF, + 1, 2, 3, 4 +}; +static const uint8_t ipv4_mapped_ipv6_addr_str[] = "::FFFF:1.2.3.4"; + +static const uint8_t ipv6_addr_bytes[] = { + 0x11, 0x22, 0x33, 0x44, + 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x11 +}; +static const uint8_t ipv6_addr_bytes_as_str[] = + "\x11\x22\x33\x44" + "\x55\x66\x77\x88" + "\x99\xaa\xbb\xcc" + "\xdd\xee\xff\x11"; + +static const uint8_t ipv6_addr_str[] = + "1122:3344:5566:7788:99aa:bbcc:ddee:ff11"; + +static const uint8_t ipv6_other_addr_bytes[] = { + 0xff, 0xee, 0xdd, 0xcc, + 0xbb, 0xaa, 0x99, 0x88, + 0x77, 0x66, 0x55, 0x44, + 0x33, 0x22, 0x11, 0x00, +}; + +static const uint8_t ipv4_other_addr_bytes[] = { + 5, 6, 7, 8 +}; +static const uint8_t ipv4_other_addr_bytes_FFFFFFFF[] = { + 5, 6, 7, 8, 0xff, 0xff, 0xff, 0xff +}; + +static const uint8_t ipv4_addr_00000000_bytes[] = { + 0, 0, 0, 0 +}; +static const uint8_t ipv4_addr_FFFFFFFF_bytes[] = { + 0, 0, 0, 0 +}; + +static const uint8_t ipv4_constraint_all_zeros_bytes[] = { + 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static const uint8_t ipv6_addr_all_zeros_bytes[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static const uint8_t ipv6_constraint_all_zeros_bytes[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static const uint8_t ipv4_constraint_CIDR_16_bytes[] = { + 1, 2, 0, 0, 0xff, 0xff, 0, 0 +}; +static const uint8_t ipv4_constraint_CIDR_17_bytes[] = { + 1, 2, 0, 0, 0xff, 0xff, 0x80, 0 +}; + +// The subnet is 1.2.0.0/16 but it is specified as 1.2.3.0/16 +static const uint8_t ipv4_constraint_CIDR_16_bad_addr_bytes[] = { + 1, 2, 3, 0, 0xff, 0xff, 0, 0 +}; + +// Masks are supposed to be of the form <ones><zeros>, but this one is of the +// form <ones><zeros><ones><zeros>. +static const uint8_t ipv4_constraint_bad_mask_bytes[] = { + 1, 2, 3, 0, 0xff, 0, 0xff, 0 +}; + +static const uint8_t ipv6_constraint_CIDR_16_bytes[] = { + 0x11, 0x22, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0xff, 0xff, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 +}; + +// The subnet is 1122::/16 but it is specified as 1122:3344::/16 +static const uint8_t ipv6_constraint_CIDR_16_bad_addr_bytes[] = { + 0x11, 0x22, 0x33, 0x44, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0xff, 0xff, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 +}; + +// Masks are supposed to be of the form <ones><zeros>, but this one is of the +// form <ones><zeros><ones><zeros>. +static const uint8_t ipv6_constraint_bad_mask_bytes[] = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static const uint8_t ipv4_addr_truncated_bytes[] = { + 1, 2, 3 +}; +static const uint8_t ipv4_addr_overlong_bytes[] = { + 1, 2, 3, 4, 5 +}; +static const uint8_t ipv4_constraint_truncated_bytes[] = { + 0, 0, 0, 0, + 0, 0, 0, +}; +static const uint8_t ipv4_constraint_overlong_bytes[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0 +}; + +static const uint8_t ipv6_addr_truncated_bytes[] = { + 0x11, 0x22, 0x33, 0x44, + 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff +}; +static const uint8_t ipv6_addr_overlong_bytes[] = { + 0x11, 0x22, 0x33, 0x44, + 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x11, 0x00 +}; +static const uint8_t ipv6_constraint_truncated_bytes[] = { + 0x11, 0x22, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0xff, 0xff, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0 +}; +static const uint8_t ipv6_constraint_overlong_bytes[] = { + 0x11, 0x22, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0xff, 0xff, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +// Note that, for DNSNames, these test cases in CHECK_CERT_HOSTNAME_PARAMS are +// mostly about testing different scenerios regarding the structure of entries +// in the subjectAltName and subject of the certificate, than about the how +// specific presented identifier values are matched against the reference +// identifier values. This is because we also use the test cases in +// DNSNAMES_VALIDITY to test CheckCertHostname. Consequently, tests about +// whether specific presented DNSNames (including wildcards, in particular) are +// matched against a reference DNSName only need to be added to +// DNSNAMES_VALIDITY, and not here. +static const CheckCertHostnameParams CHECK_CERT_HOSTNAME_PARAMS[] = +{ + // This is technically illegal. PrintableString is defined in such a way that + // '*' is not an allowed character, but there are many real-world certificates + // that are encoded this way. + WITHOUT_SAN("foo.example.com", RDN(CN("*.example.com", der::PrintableString)), + Success), + WITHOUT_SAN("foo.example.com", RDN(CN("*.example.com", der::UTF8String)), + Success), + + // Many certificates use TeletexString when encoding wildcards in CN-IDs + // because PrintableString is defined as not allowing '*' and UTF8String was, + // at one point in history, considered too new to depend on for compatibility. + // We accept TeletexString-encoded CN-IDs when they don't contain any escape + // sequences. The reference I used for the escape codes was + // https://tools.ietf.org/html/rfc1468. The escaping mechanism is actually + // pretty complex and these tests don't even come close to testing all the + // possibilities. + WITHOUT_SAN("foo.example.com", RDN(CN("*.example.com", der::TeletexString)), + Success), + // "ESC ( B" ({0x1B,0x50,0x42}) is the escape code to switch to ASCII, which + // is redundant because it already the default. + WITHOUT_SAN("foo.example.com", + RDN(CN("\x1B(B*.example.com", der::TeletexString)), + Result::ERROR_BAD_CERT_DOMAIN), + WITHOUT_SAN("foo.example.com", + RDN(CN("*.example\x1B(B.com", der::TeletexString)), + Result::ERROR_BAD_CERT_DOMAIN), + WITHOUT_SAN("foo.example.com", + RDN(CN("*.example.com\x1B(B", der::TeletexString)), + Result::ERROR_BAD_CERT_DOMAIN), + // "ESC $ B" ({0x1B,0x24,0x42}) is the escape code to switch to + // JIS X 0208-1983 (a Japanese character set). + WITHOUT_SAN("foo.example.com", + RDN(CN("\x1B$B*.example.com", der::TeletexString)), + Result::ERROR_BAD_CERT_DOMAIN), + WITHOUT_SAN("foo.example.com", + RDN(CN("*.example.com\x1B$B", der::TeletexString)), + Result::ERROR_BAD_CERT_DOMAIN), + + // Match a DNSName SAN entry with a redundant (ignored) matching CN-ID. + WITH_SAN("a", RDN(CN("a")), DNSName("a"), Success), + // Match a DNSName SAN entry when there is an CN-ID that doesn't match. + WITH_SAN("b", RDN(CN("a")), DNSName("b"), Success), + // Do not match a CN-ID when there is a valid DNSName SAN Entry. + WITH_SAN("a", RDN(CN("a")), DNSName("b"), Result::ERROR_BAD_CERT_DOMAIN), + // Do not match a CN-ID when there is a malformed DNSName SAN Entry. + WITH_SAN("a", RDN(CN("a")), DNSName("!"), Result::ERROR_BAD_DER), + // Do not match a matching CN-ID when there is a valid IPAddress SAN entry. + WITH_SAN("a", RDN(CN("a")), IPAddress(ipv4_addr_bytes), + Result::ERROR_BAD_CERT_DOMAIN), + // Do not match a matching CN-ID when there is a malformed IPAddress SAN entry. + WITH_SAN("a", RDN(CN("a")), IPAddress(example_com), + Result::ERROR_BAD_CERT_DOMAIN), + // Match a DNSName against a matching CN-ID when there is a SAN, but the SAN + // does not contain an DNSName or IPAddress entry. + WITH_SAN("a", RDN(CN("a")), RFC822Name("foo@example.com"), Success), + // Match a matching CN-ID when there is no SAN. + WITHOUT_SAN("a", RDN(CN("a")), Success), + // Do not match a mismatching CN-ID when there is no SAN. + WITHOUT_SAN("a", RDN(CN("b")), Result::ERROR_BAD_CERT_DOMAIN), + + // The first DNSName matches. + WITH_SAN("a", RDN(CN("foo")), DNSName("a") + DNSName("b"), Success), + // The last DNSName matches. + WITH_SAN("b", RDN(CN("foo")), DNSName("a") + DNSName("b"), Success), + // The middle DNSName matches. + WITH_SAN("b", RDN(CN("foo")), + DNSName("a") + DNSName("b") + DNSName("c"), Success), + // After an IP address. + WITH_SAN("b", RDN(CN("foo")), + IPAddress(ipv4_addr_bytes) + DNSName("b"), Success), + // Before an IP address. + WITH_SAN("a", RDN(CN("foo")), + DNSName("a") + IPAddress(ipv4_addr_bytes), Success), + // Between an RFC822Name and an IP address. + WITH_SAN("b", RDN(CN("foo")), + RFC822Name("foo@example.com") + DNSName("b") + + IPAddress(ipv4_addr_bytes), + Success), + // Duplicate DNSName. + WITH_SAN("a", RDN(CN("foo")), DNSName("a") + DNSName("a"), Success), + // After an invalid DNSName. + WITH_SAN("b", RDN(CN("foo")), DNSName("!") + DNSName("b"), + Result::ERROR_BAD_DER), + + // http://tools.ietf.org/html/rfc5280#section-4.2.1.6: "If the subjectAltName + // extension is present, the sequence MUST contain at least one entry." + // However, for compatibility reasons, this is not enforced. See bug 1143085. + // This case is treated as if the extension is not present (i.e. name + // matching falls back to the subject CN). + WITH_SAN("a", RDN(CN("a")), ByteString(), Success), + WITH_SAN("a", RDN(CN("b")), ByteString(), Result::ERROR_BAD_CERT_DOMAIN), + + // http://tools.ietf.org/html/rfc5280#section-4.1.2.6 says "If subject naming + // information is present only in the subjectAltName extension (e.g., a key + // bound only to an email address or URI), then the subject name MUST be an + // empty sequence and the subjectAltName extension MUST be critical." So, we + // have to support an empty subject. We don't enforce that the SAN must be + // critical or even that there is a SAN when the subject is empty, though. + WITH_SAN("a", ByteString(), DNSName("a"), Success), + // Make sure we return ERROR_BAD_CERT_DOMAIN and not ERROR_BAD_DER. + WITHOUT_SAN("a", ByteString(), Result::ERROR_BAD_CERT_DOMAIN), + + // Two CNs in the same RDN, both match. + WITHOUT_SAN("a", RDN(CN("a") + CN("a")), Success), + // Two CNs in the same RDN, both DNSNames, first one matches. + WITHOUT_SAN("a", RDN(CN("a") + CN("b")), + Result::ERROR_BAD_CERT_DOMAIN), + // Two CNs in the same RDN, both DNSNames, last one matches. + WITHOUT_SAN("b", RDN(CN("a") + CN("b")), Success), + // Two CNs in the same RDN, first one matches, second isn't a DNSName. + WITHOUT_SAN("a", RDN(CN("a") + CN("Not a DNSName")), + Result::ERROR_BAD_CERT_DOMAIN), + // Two CNs in the same RDN, first one not a DNSName, second matches. + WITHOUT_SAN("b", RDN(CN("Not a DNSName") + CN("b")), Success), + + // Two CNs in separate RDNs, both match. + WITHOUT_SAN("a", RDN(CN("a")) + RDN(CN("a")), Success), + // Two CNs in separate RDNs, both DNSNames, first one matches. + WITHOUT_SAN("a", RDN(CN("a")) + RDN(CN("b")), + Result::ERROR_BAD_CERT_DOMAIN), + // Two CNs in separate RDNs, both DNSNames, last one matches. + WITHOUT_SAN("b", RDN(CN("a")) + RDN(CN("b")), Success), + // Two CNs in separate RDNs, first one matches, second isn't a DNSName. + WITHOUT_SAN("a", RDN(CN("a")) + RDN(CN("Not a DNSName")), + Result::ERROR_BAD_CERT_DOMAIN), + // Two CNs in separate RDNs, first one not a DNSName, second matches. + WITHOUT_SAN("b", RDN(CN("Not a DNSName")) + RDN(CN("b")), Success), + + // One CN, one RDN, CN is the first AVA in the RDN, CN matches. + WITHOUT_SAN("a", RDN(CN("a") + OU("b")), Success), + // One CN, one RDN, CN is the first AVA in the RDN, CN does not match. + WITHOUT_SAN("b", RDN(CN("a") + OU("b")), + Result::ERROR_BAD_CERT_DOMAIN), + // One CN, one RDN, CN is not the first AVA in the RDN, CN matches. + WITHOUT_SAN("b", RDN(OU("a") + CN("b")), Success), + // One CN, one RDN, CN is not the first AVA in the RDN, CN does not match. + WITHOUT_SAN("a", RDN(OU("a") + CN("b")), + Result::ERROR_BAD_CERT_DOMAIN), + + // One CN, multiple RDNs, CN is in the first RDN, CN matches. + WITHOUT_SAN("a", RDN(CN("a")) + RDN(OU("b")), Success), + // One CN, multiple RDNs, CN is in the first RDN, CN does not match. + WITHOUT_SAN("b", RDN(CN("a")) + RDN(OU("b")), Result::ERROR_BAD_CERT_DOMAIN), + // One CN, multiple RDNs, CN is not in the first RDN, CN matches. + WITHOUT_SAN("b", RDN(OU("a")) + RDN(CN("b")), Success), + // One CN, multiple RDNs, CN is not in the first RDN, CN does not match. + WITHOUT_SAN("a", RDN(OU("a")) + RDN(CN("b")), Result::ERROR_BAD_CERT_DOMAIN), + + // One CN, one RDN, CN is not in the first or last AVA, CN matches. + WITHOUT_SAN("b", RDN(OU("a") + CN("b") + OU("c")), Success), + // One CN, multiple RDNs, CN is not in the first or last RDN, CN matches. + WITHOUT_SAN("b", RDN(OU("a")) + RDN(CN("b")) + RDN(OU("c")), Success), + + // Empty CN does not match. + WITHOUT_SAN("example.com", RDN(CN("")), Result::ERROR_BAD_CERT_DOMAIN), + + WITHOUT_SAN("uses_underscore.example.com", RDN(CN("*.example.com")), Success), + WITHOUT_SAN("a.uses_underscore.example.com", + RDN(CN("*.uses_underscore.example.com")), Success), + WITH_SAN("uses_underscore.example.com", RDN(CN("foo")), + DNSName("*.example.com"), Success), + WITH_SAN("a.uses_underscore.example.com", RDN(CN("foo")), + DNSName("*.uses_underscore.example.com"), Success), + + // Do not match a DNSName that is encoded in a malformed IPAddress. + WITH_SAN("example.com", RDN(CN("foo")), IPAddress(example_com), + Result::ERROR_BAD_CERT_DOMAIN), + + // We skip over the malformed IPAddress and match the DNSName entry because + // we've heard reports of real-world certificates that have malformed + // IPAddress SANs. + WITH_SAN("example.org", RDN(CN("foo")), + IPAddress(example_com) + DNSName("example.org"), Success), + + WITH_SAN("example.com", RDN(CN("foo")), + DNSName("!") + DNSName("example.com"), Result::ERROR_BAD_DER), + + // Match a matching IPv4 address SAN entry. + WITH_SAN(ipv4_addr_str, RDN(CN("foo")), IPAddress(ipv4_addr_bytes), + Success), + // Match a matching IPv4 addresses in the CN when there is no SAN + WITHOUT_SAN(ipv4_addr_str, RDN(CN(ipv4_addr_str)), Success), + // Do not match a matching IPv4 address in the CN when there is a SAN with + // a DNSName entry. + WITH_SAN(ipv4_addr_str, RDN(CN(ipv4_addr_str)), + DNSName("example.com"), Result::ERROR_BAD_CERT_DOMAIN), + // Do not match a matching IPv4 address in the CN when there is a SAN with + // a non-matching IPAddress entry. + WITH_SAN(ipv4_addr_str, RDN(CN(ipv4_addr_str)), + IPAddress(ipv6_addr_bytes), Result::ERROR_BAD_CERT_DOMAIN), + // Match a matching IPv4 address in the CN when there is a SAN with a + // non-IPAddress, non-DNSName entry. + WITH_SAN(ipv4_addr_str, RDN(CN(ipv4_addr_str)), + RFC822Name("foo@example.com"), Success), + // Do not match a matching IPv4 address in the CN when there is a SAN with a + // malformed IPAddress entry. + WITH_SAN(ipv4_addr_str, RDN(CN(ipv4_addr_str)), + IPAddress(example_com), Result::ERROR_BAD_CERT_DOMAIN), + // Do not match a matching IPv4 address in the CN when there is a SAN with a + // malformed DNSName entry. + WITH_SAN(ipv4_addr_str, RDN(CN(ipv4_addr_str)), + DNSName("!"), Result::ERROR_BAD_CERT_DOMAIN), + + // We don't match IPv6 addresses in the CN, regardless of whether there is + // a SAN. + WITHOUT_SAN(ipv6_addr_str, RDN(CN(ipv6_addr_str)), + Result::ERROR_BAD_CERT_DOMAIN), + WITH_SAN(ipv6_addr_str, RDN(CN(ipv6_addr_str)), + DNSName("example.com"), Result::ERROR_BAD_CERT_DOMAIN), + WITH_SAN(ipv6_addr_str, RDN(CN(ipv6_addr_str)), + IPAddress(ipv6_addr_bytes), Success), + WITH_SAN(ipv6_addr_str, RDN(CN("foo")), IPAddress(ipv6_addr_bytes), + Success), + + // We don't match the binary encoding of the bytes of IP addresses in the + // CN. + WITHOUT_SAN(ipv4_addr_str, RDN(CN(ipv4_addr_bytes_as_str)), + Result::ERROR_BAD_CERT_DOMAIN), + WITHOUT_SAN(ipv6_addr_str, RDN(CN(ipv6_addr_bytes_as_str)), + Result::ERROR_BAD_CERT_DOMAIN), + + // We don't match IP addresses with DNSName SANs. + WITH_SAN(ipv4_addr_str, RDN(CN("foo")), + DNSName(ipv4_addr_bytes_as_str), Result::ERROR_BAD_CERT_DOMAIN), + WITH_SAN(ipv4_addr_str, RDN(CN("foo")), DNSName(ipv4_addr_str), + Result::ERROR_BAD_CERT_DOMAIN), + WITH_SAN(ipv6_addr_str, RDN(CN("foo")), + DNSName(ipv6_addr_bytes_as_str), Result::ERROR_BAD_CERT_DOMAIN), + WITH_SAN(ipv6_addr_str, RDN(CN("foo")), DNSName(ipv6_addr_str), + Result::ERROR_BAD_CERT_DOMAIN), + + // Do not match an IPv4 reference ID against the equivalent IPv4-compatible + // IPv6 SAN entry. + WITH_SAN(ipv4_addr_str, RDN(CN("foo")), + IPAddress(ipv4_compatible_ipv6_addr_bytes), + Result::ERROR_BAD_CERT_DOMAIN), + // Do not match an IPv4 reference ID against the equivalent IPv4-mapped IPv6 + // SAN entry. + WITH_SAN(ipv4_addr_str, RDN(CN("foo")), + IPAddress(ipv4_mapped_ipv6_addr_bytes), + Result::ERROR_BAD_CERT_DOMAIN), + // Do not match an IPv4-compatible IPv6 reference ID against the equivalent + // IPv4 SAN entry. + WITH_SAN(ipv4_compatible_ipv6_addr_str, RDN(CN("foo")), + IPAddress(ipv4_addr_bytes), Result::ERROR_BAD_CERT_DOMAIN), + // Do not match an IPv4 reference ID against the equivalent IPv4-mapped IPv6 + // SAN entry. + WITH_SAN(ipv4_mapped_ipv6_addr_str, RDN(CN("foo")), + IPAddress(ipv4_addr_bytes), + Result::ERROR_BAD_CERT_DOMAIN), + + // Test that the presence of an otherName entry is handled appropriately. + // (The actual value of the otherName entry isn't important - that's not what + // we're testing here.) + WITH_SAN("example.com", ByteString(), + // The tag for otherName is CONTEXT_SPECIFIC | CONSTRUCTED | 0 + TLV((2 << 6) | (1 << 5) | 0, ByteString()) + DNSName("example.com"), + Success), + WITH_SAN("example.com", ByteString(), + TLV((2 << 6) | (1 << 5) | 0, ByteString()), + Result::ERROR_BAD_CERT_DOMAIN), +}; + +ByteString +CreateCert(const ByteString& subject, const ByteString& subjectAltName, + EndEntityOrCA endEntityOrCA = EndEntityOrCA::MustBeEndEntity) +{ + ByteString serialNumber(CreateEncodedSerialNumber(1)); + EXPECT_FALSE(ENCODING_FAILED(serialNumber)); + + ByteString issuerDER(Name(RDN(CN("issuer")))); + EXPECT_FALSE(ENCODING_FAILED(issuerDER)); + + ByteString extensions[2]; + if (subjectAltName != NO_SAN) { + extensions[0] = CreateEncodedSubjectAltName(subjectAltName); + EXPECT_FALSE(ENCODING_FAILED(extensions[0])); + } + if (endEntityOrCA == EndEntityOrCA::MustBeCA) { + // Currently, these tests assume that if we're creating a CA certificate, it + // will not have a subjectAlternativeName extension. If that assumption + // changes, this code will have to be updated. Ideally this would be + // ASSERT_EQ, but that inserts a 'return;', which doesn't match this + // function's return type. + EXPECT_EQ(subjectAltName, NO_SAN); + extensions[0] = CreateEncodedBasicConstraints(true, nullptr, + Critical::Yes); + EXPECT_FALSE(ENCODING_FAILED(extensions[0])); + } + + ScopedTestKeyPair keyPair(CloneReusedKeyPair()); + return CreateEncodedCertificate( + v3, sha256WithRSAEncryption(), serialNumber, issuerDER, + oneDayBeforeNow, oneDayAfterNow, Name(subject), *keyPair, + extensions, *keyPair, sha256WithRSAEncryption()); +} + +TEST_P(pkixnames_CheckCertHostname, CheckCertHostname) +{ + const CheckCertHostnameParams& param(GetParam()); + + ByteString cert(CreateCert(param.subject, param.subjectAltName)); + ASSERT_FALSE(ENCODING_FAILED(cert)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(cert.data(), cert.length())); + + Input hostnameInput; + ASSERT_EQ(Success, hostnameInput.Init(param.hostname.data(), + param.hostname.length())); + + ASSERT_EQ(param.result, CheckCertHostname(certInput, hostnameInput, + mNameMatchingPolicy)); +} + +INSTANTIATE_TEST_CASE_P(pkixnames_CheckCertHostname, + pkixnames_CheckCertHostname, + testing::ValuesIn(CHECK_CERT_HOSTNAME_PARAMS)); + +TEST_F(pkixnames_CheckCertHostname, SANWithoutSequence) +{ + // A certificate with a truly empty SAN extension (one that doesn't even + // contain a SEQUENCE at all) is malformed. If we didn't treat this as + // malformed then we'd have to treat it like the CN_EmptySAN cases. + + ByteString serialNumber(CreateEncodedSerialNumber(1)); + EXPECT_FALSE(ENCODING_FAILED(serialNumber)); + + ByteString extensions[2]; + extensions[0] = CreateEncodedEmptySubjectAltName(); + ASSERT_FALSE(ENCODING_FAILED(extensions[0])); + + ScopedTestKeyPair keyPair(CloneReusedKeyPair()); + ByteString certDER(CreateEncodedCertificate( + v3, sha256WithRSAEncryption(), serialNumber, + Name(RDN(CN("issuer"))), oneDayBeforeNow, oneDayAfterNow, + Name(RDN(CN("a"))), *keyPair, extensions, + *keyPair, sha256WithRSAEncryption())); + ASSERT_FALSE(ENCODING_FAILED(certDER)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(certDER.data(), certDER.length())); + + static const uint8_t a[] = { 'a' }; + ASSERT_EQ(Result::ERROR_EXTENSION_VALUE_INVALID, + CheckCertHostname(certInput, Input(a), mNameMatchingPolicy)); +} + +class pkixnames_CheckCertHostname_PresentedMatchesReference + : public ::testing::Test + , public ::testing::WithParamInterface<PresentedMatchesReference> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_CheckCertHostname_PresentedMatchesReference, CN_NoSAN) +{ + // Since there is no SAN, a valid presented DNS ID in the subject CN field + // should result in a match. + + const PresentedMatchesReference& param(GetParam()); + + ByteString cert(CreateCert(RDN(CN(param.presentedDNSID)), NO_SAN)); + ASSERT_FALSE(ENCODING_FAILED(cert)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(cert.data(), cert.length())); + + Input hostnameInput; + ASSERT_EQ(Success, hostnameInput.Init(param.referenceDNSID.data(), + param.referenceDNSID.length())); + + ASSERT_EQ(param.expectedMatches ? Success : Result::ERROR_BAD_CERT_DOMAIN, + CheckCertHostname(certInput, hostnameInput, mNameMatchingPolicy)); +} + +TEST_P(pkixnames_CheckCertHostname_PresentedMatchesReference, + SubjectAltName_CNNotDNSName) +{ + // A DNSName SAN entry should match, regardless of the contents of the + // subject CN. + + const PresentedMatchesReference& param(GetParam()); + + ByteString cert(CreateCert(RDN(CN("Common Name")), + DNSName(param.presentedDNSID))); + ASSERT_FALSE(ENCODING_FAILED(cert)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(cert.data(), cert.length())); + + Input hostnameInput; + ASSERT_EQ(Success, hostnameInput.Init(param.referenceDNSID.data(), + param.referenceDNSID.length())); + Result expectedResult + = param.expectedResult != Success ? param.expectedResult + : param.expectedMatches ? Success + : Result::ERROR_BAD_CERT_DOMAIN; + ASSERT_EQ(expectedResult, CheckCertHostname(certInput, hostnameInput, + mNameMatchingPolicy)); +} + +INSTANTIATE_TEST_CASE_P(pkixnames_CheckCertHostname_DNSID_MATCH_PARAMS, + pkixnames_CheckCertHostname_PresentedMatchesReference, + testing::ValuesIn(DNSID_MATCH_PARAMS)); + +TEST_P(pkixnames_Turkish_I_Comparison, CheckCertHostname_CN_NoSAN) +{ + // Make sure we don't have the similar problems that strcasecmp and others + // have with the other kinds of "i" and "I" commonly used in Turkish locales, + // when we're matching a CN due to lack of subjectAltName. + + const InputValidity& param(GetParam()); + SCOPED_TRACE(param.input.c_str()); + + Input input; + ASSERT_EQ(Success, input.Init(param.input.data(), param.input.length())); + + ByteString cert(CreateCert(RDN(CN(param.input)), NO_SAN)); + ASSERT_FALSE(ENCODING_FAILED(cert)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(cert.data(), cert.length())); + + Result expectedResult = (InputsAreEqual(LOWERCASE_I, input) || + InputsAreEqual(UPPERCASE_I, input)) + ? Success + : Result::ERROR_BAD_CERT_DOMAIN; + + ASSERT_EQ(expectedResult, CheckCertHostname(certInput, UPPERCASE_I, + mNameMatchingPolicy)); + ASSERT_EQ(expectedResult, CheckCertHostname(certInput, LOWERCASE_I, + mNameMatchingPolicy)); +} + +TEST_P(pkixnames_Turkish_I_Comparison, CheckCertHostname_SAN) +{ + // Make sure we don't have the similar problems that strcasecmp and others + // have with the other kinds of "i" and "I" commonly used in Turkish locales, + // when we're matching a dNSName in the SAN. + + const InputValidity& param(GetParam()); + SCOPED_TRACE(param.input.c_str()); + + Input input; + ASSERT_EQ(Success, input.Init(param.input.data(), param.input.length())); + + ByteString cert(CreateCert(RDN(CN("Common Name")), DNSName(param.input))); + ASSERT_FALSE(ENCODING_FAILED(cert)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(cert.data(), cert.length())); + + Result expectedResult + = (!param.isValidPresentedID) ? Result::ERROR_BAD_DER + : (InputsAreEqual(LOWERCASE_I, input) || + InputsAreEqual(UPPERCASE_I, input)) ? Success + : Result::ERROR_BAD_CERT_DOMAIN; + + ASSERT_EQ(expectedResult, CheckCertHostname(certInput, UPPERCASE_I, + mNameMatchingPolicy)); + ASSERT_EQ(expectedResult, CheckCertHostname(certInput, LOWERCASE_I, + mNameMatchingPolicy)); +} + +class pkixnames_CheckCertHostname_IPV4_Addresses + : public ::testing::Test + , public ::testing::WithParamInterface<IPAddressParams<4>> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_CheckCertHostname_IPV4_Addresses, + ValidIPv4AddressInIPAddressSAN) +{ + // When the reference hostname is a valid IPv4 address, a correctly-formed + // IPv4 Address SAN matches it. + + const IPAddressParams<4>& param(GetParam()); + + ByteString cert(CreateCert(RDN(CN("Common Name")), + IPAddress(param.expectedValueIfValid))); + ASSERT_FALSE(ENCODING_FAILED(cert)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(cert.data(), cert.length())); + + Input hostnameInput; + ASSERT_EQ(Success, hostnameInput.Init(param.input.data(), + param.input.length())); + + ASSERT_EQ(param.isValid ? Success : Result::ERROR_BAD_CERT_DOMAIN, + CheckCertHostname(certInput, hostnameInput, mNameMatchingPolicy)); +} + +TEST_P(pkixnames_CheckCertHostname_IPV4_Addresses, + ValidIPv4AddressInCN_NoSAN) +{ + // When the reference hostname is a valid IPv4 address, a correctly-formed + // IPv4 Address in the CN matches it when there is no SAN. + + const IPAddressParams<4>& param(GetParam()); + + SCOPED_TRACE(param.input.c_str()); + + ByteString cert(CreateCert(RDN(CN(param.input)), NO_SAN)); + ASSERT_FALSE(ENCODING_FAILED(cert)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(cert.data(), cert.length())); + + Input hostnameInput; + ASSERT_EQ(Success, hostnameInput.Init(param.input.data(), + param.input.length())); + + // Some of the invalid IPv4 addresses are valid DNS names! + Result expectedResult = (param.isValid || IsValidReferenceDNSID(hostnameInput)) + ? Success + : Result::ERROR_BAD_CERT_DOMAIN; + + ASSERT_EQ(expectedResult, CheckCertHostname(certInput, hostnameInput, + mNameMatchingPolicy)); +} + +INSTANTIATE_TEST_CASE_P(pkixnames_CheckCertHostname_IPV4_ADDRESSES, + pkixnames_CheckCertHostname_IPV4_Addresses, + testing::ValuesIn(IPV4_ADDRESSES)); + +struct NameConstraintParams +{ + ByteString subject; + ByteString subjectAltName; + ByteString subtrees; + Result expectedPermittedSubtreesResult; + Result expectedExcludedSubtreesResult; +}; + +static ByteString +PermittedSubtrees(const ByteString& generalSubtrees) +{ + return TLV(der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 0, + generalSubtrees); +} + +static ByteString +ExcludedSubtrees(const ByteString& generalSubtrees) +{ + return TLV(der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1, + generalSubtrees); +} + +// Does not encode min or max. +static ByteString +GeneralSubtree(const ByteString& base) +{ + return TLV(der::SEQUENCE, base); +} + +static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] = +{ + ///////////////////////////////////////////////////////////////////////////// + // XXX: Malformed name constraints for supported types of names are ignored + // when there are no names of that type to constrain. + { ByteString(), NO_SAN, + GeneralSubtree(DNSName("!")), + Success, Success + }, + { // DirectoryName constraints are an exception, because *every* certificate + // has at least one DirectoryName (tbsCertificate.subject). + ByteString(), NO_SAN, + GeneralSubtree(Name(ByteString(reinterpret_cast<const uint8_t*>("!"), 1))), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { ByteString(), NO_SAN, + GeneralSubtree(IPAddress(ipv4_constraint_truncated_bytes)), + Success, Success + }, + { ByteString(), NO_SAN, + GeneralSubtree(IPAddress(ipv4_constraint_overlong_bytes)), + Success, Success + }, + { ByteString(), NO_SAN, + GeneralSubtree(IPAddress(ipv6_constraint_truncated_bytes)), + Success, Success + }, + { ByteString(), NO_SAN, + GeneralSubtree(IPAddress(ipv6_constraint_overlong_bytes)), + Success, Success + }, + { ByteString(), NO_SAN, + GeneralSubtree(RFC822Name("!")), + Success, Success + }, + + ///////////////////////////////////////////////////////////////////////////// + // Edge cases of name constraint absolute vs. relative and subdomain matching + // that are not clearly explained in RFC 5280. (See the long comment above + // MatchPresentedDNSIDWithReferenceDNSID.) + + // Q: Does a presented identifier equal (case insensitive) to the name + // constraint match the constraint? For example, does the presented + // ID "host.example.com" match a "host.example.com" constraint? + { ByteString(), DNSName("host.example.com"), + GeneralSubtree(DNSName("host.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // This test case is an example from RFC 5280. + ByteString(), DNSName("host1.example.com"), + GeneralSubtree(DNSName("host.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { ByteString(), RFC822Name("a@host.example.com"), + GeneralSubtree(RFC822Name("host.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // This test case is an example from RFC 5280. + ByteString(), RFC822Name("a@host1.example.com"), + GeneralSubtree(RFC822Name("host.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + + // Q: When the name constraint does not start with ".", do subdomain + // presented identifiers match it? For example, does the presented + // ID "www.host.example.com" match a "host.example.com" constraint? + { // This test case is an example from RFC 5280. + ByteString(), DNSName("www.host.example.com"), + GeneralSubtree(DNSName( "host.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // The subdomain matching rule for host names that do not start with "." is + // different for RFC822Names than for DNSNames! + ByteString(), RFC822Name("a@www.host.example.com"), + GeneralSubtree(RFC822Name( "host.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, + Success + }, + + // Q: When the name constraint does not start with ".", does a + // non-subdomain prefix match it? For example, does "bigfoo.bar.com" + // match "foo.bar.com"? + { ByteString(), DNSName("bigfoo.bar.com"), + GeneralSubtree(DNSName( "foo.bar.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { ByteString(), RFC822Name("a@bigfoo.bar.com"), + GeneralSubtree(RFC822Name( "foo.bar.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + + // Q: Is a name constraint that starts with "." valid, and if so, what + // semantics does it have? For example, does a presented ID of + // "www.example.com" match a constraint of ".example.com"? Does a + // presented ID of "example.com" match a constraint of ".example.com"? + { ByteString(), DNSName("www.example.com"), + GeneralSubtree(DNSName( ".example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // When there is no Local-part, an RFC822 name constraint's domain may + // start with '.', and the semantics are the same as for DNSNames. + ByteString(), RFC822Name("a@www.example.com"), + GeneralSubtree(RFC822Name( ".example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // When there is a Local-part, an RFC822 name constraint's domain must not + // start with '.'. + ByteString(), RFC822Name("a@www.example.com"), + GeneralSubtree(RFC822Name( "a@.example.com")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // Check that we only allow subdomains to match. + ByteString(), DNSName( "example.com"), + GeneralSubtree(DNSName(".example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // Check that we only allow subdomains to match. + ByteString(), RFC822Name("a@example.com"), + GeneralSubtree(RFC822Name(".example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // Check that we don't get confused and consider "b" == "." + ByteString(), DNSName("bexample.com"), + GeneralSubtree(DNSName(".example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // Check that we don't get confused and consider "b" == "." + ByteString(), RFC822Name("a@bexample.com"), + GeneralSubtree(RFC822Name( ".example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + + // Q: Is there a way to prevent subdomain matches? + // (This is tested in a different set of tests because it requires a + // combination of permittedSubtrees and excludedSubtrees.) + + // Q: Are name constraints allowed to be specified as absolute names? + // For example, does a presented ID of "example.com" match a name + // constraint of "example.com." and vice versa? + // + { // The DNSName in the constraint is not valid because constraint DNS IDs + // are not allowed to be absolute. + ByteString(), DNSName("example.com"), + GeneralSubtree(DNSName("example.com.")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { ByteString(), RFC822Name("a@example.com"), + GeneralSubtree(RFC822Name( "example.com.")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { // The DNSName in the SAN is not valid because presented DNS IDs are not + // allowed to be absolute. + ByteString(), DNSName("example.com."), + GeneralSubtree(DNSName("example.com")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { ByteString(), RFC822Name("a@example.com."), + GeneralSubtree(RFC822Name( "example.com")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { // The presented DNSName is the same length as the constraint, because the + // subdomain is only one character long and because the constraint both + // begins and ends with ".". But, it doesn't matter because absolute names + // are not allowed for DNSName constraints. + ByteString(), DNSName("p.example.com"), + GeneralSubtree(DNSName(".example.com.")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { // The presented DNSName is the same length as the constraint, because the + // subdomain is only one character long and because the constraint both + // begins and ends with ".". + ByteString(), RFC822Name("a@p.example.com"), + GeneralSubtree(RFC822Name( ".example.com.")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { // Same as previous test case, but using a wildcard presented ID. + ByteString(), DNSName("*.example.com"), + GeneralSubtree(DNSName(".example.com.")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // Same as previous test case, but using a wildcard presented ID, which is + // invalid in an RFC822Name. + ByteString(), RFC822Name("a@*.example.com"), + GeneralSubtree(RFC822Name( ".example.com.")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + + // Q: Are "" and "." valid DNSName constraints? If so, what do they mean? + { ByteString(), DNSName("example.com"), + GeneralSubtree(DNSName("")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), RFC822Name("a@example.com"), + GeneralSubtree(RFC822Name("")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // The malformed (absolute) presented ID does not match. + ByteString(), DNSName("example.com."), + GeneralSubtree(DNSName("")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("a@example.com."), + GeneralSubtree(RFC822Name("")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // Invalid syntax in name constraint + ByteString(), DNSName("example.com"), + GeneralSubtree(DNSName(".")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { // Invalid syntax in name constraint + ByteString(), RFC822Name("a@example.com"), + GeneralSubtree(RFC822Name(".")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER, + }, + { ByteString(), DNSName("example.com."), + GeneralSubtree(DNSName(".")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("a@example.com."), + GeneralSubtree(RFC822Name(".")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + + ///////////////////////////////////////////////////////////////////////////// + // Basic IP Address constraints (non-CN-ID) + + // The Mozilla CA Policy says this means "no IPv4 addresses allowed." + { ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_all_zeros_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), IPAddress(ipv4_addr_00000000_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_all_zeros_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), IPAddress(ipv4_addr_FFFFFFFF_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_all_zeros_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + + // The Mozilla CA Policy says this means "no IPv6 addresses allowed." + { ByteString(), IPAddress(ipv6_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_all_zeros_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), IPAddress(ipv6_addr_all_zeros_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_all_zeros_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + + // RFC 5280 doesn't partition IP address constraints into separate IPv4 and + // IPv6 categories, so a IPv4 permittedSubtrees constraint excludes all IPv6 + // addresses, and vice versa. + { ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_all_zeros_bytes)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { ByteString(), IPAddress(ipv6_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_all_zeros_bytes)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + + // IPv4 Subnets + { ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_CIDR_16_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_CIDR_17_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), IPAddress(ipv4_other_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_CIDR_16_bytes)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // XXX(bug 1089430): We don't reject this even though it is weird. + ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_CIDR_16_bad_addr_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // XXX(bug 1089430): We don't reject this even though it is weird. + ByteString(), IPAddress(ipv4_other_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_bad_mask_bytes)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + + // IPv6 Subnets + { ByteString(), IPAddress(ipv6_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_CIDR_16_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), IPAddress(ipv6_other_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_CIDR_16_bytes)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // XXX(bug 1089430): We don't reject this even though it is weird. + ByteString(), IPAddress(ipv6_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_CIDR_16_bad_addr_bytes)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // XXX(bug 1089430): We don't reject this even though it is weird. + ByteString(), IPAddress(ipv6_other_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_bad_mask_bytes)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + + // Malformed presented IP addresses and constraints + + { // The presented IPv4 address is empty + ByteString(), IPAddress(), + GeneralSubtree(IPAddress(ipv4_constraint_all_zeros_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv4 address is truncated + ByteString(), IPAddress(ipv4_addr_truncated_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_all_zeros_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv4 address is too long + ByteString(), IPAddress(ipv4_addr_overlong_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_all_zeros_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv4 constraint is empty + ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress()), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv4 constraint is truncated + ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_truncated_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv4 constraint is too long + ByteString(), IPAddress(ipv4_addr_bytes), + GeneralSubtree(IPAddress(ipv4_constraint_overlong_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv6 address is empty + ByteString(), IPAddress(), + GeneralSubtree(IPAddress(ipv6_constraint_all_zeros_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv6 address is truncated + ByteString(), IPAddress(ipv6_addr_truncated_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_all_zeros_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv6 address is too long + ByteString(), IPAddress(ipv6_addr_overlong_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_all_zeros_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv6 constraint is empty + ByteString(), IPAddress(ipv6_addr_bytes), + GeneralSubtree(IPAddress()), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv6 constraint is truncated + ByteString(), IPAddress(ipv6_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_truncated_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + { // The presented IPv6 constraint is too long + ByteString(), IPAddress(ipv6_addr_bytes), + GeneralSubtree(IPAddress(ipv6_constraint_overlong_bytes)), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + + ///////////////////////////////////////////////////////////////////////////// + // XXX: We don't reject malformed name constraints when there are no names of + // that type. + { ByteString(), NO_SAN, GeneralSubtree(DNSName("!")), + Success, Success + }, + { ByteString(), NO_SAN, GeneralSubtree(IPAddress(ipv4_addr_overlong_bytes)), + Success, Success + }, + { ByteString(), NO_SAN, GeneralSubtree(IPAddress(ipv6_addr_overlong_bytes)), + Success, Success + }, + { ByteString(), NO_SAN, GeneralSubtree(RFC822Name("\0")), + Success, Success + }, + + ///////////////////////////////////////////////////////////////////////////// + // Basic CN-ID DNSName constraint tests. + + { // Empty Name is ignored for DNSName constraints. + ByteString(), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { // Empty CN is ignored for DNSName constraints because it isn't a + // syntactically-valid DNSName. + // + // NSS gives different results. + RDN(CN("")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { // IP Address is ignored for DNSName constraints. + // + // NSS gives different results. + RDN(CN("1.2.3.4")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { // OU has something that looks like a dNSName that matches. + RDN(OU("a.example.com")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { // OU has something that looks like a dNSName that does not match. + RDN(OU("b.example.com")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { // NSS gives different results. + RDN(CN("Not a DNSName")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { RDN(CN("a.example.com")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { RDN(CN("b.example.com")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // DNSName CN-ID match is detected when there is a SAN w/o any DNSName or + // IPAddress + RDN(CN("a.example.com")), RFC822Name("foo@example.com"), + GeneralSubtree(DNSName("a.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // DNSName CN-ID mismatch is detected when there is a SAN w/o any DNSName + // or IPAddress + RDN(CN("a.example.com")), RFC822Name("foo@example.com"), + GeneralSubtree(DNSName("b.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // DNSName CN-ID match not reported when there is a DNSName SAN + RDN(CN("a.example.com")), DNSName("b.example.com"), + GeneralSubtree(DNSName("a.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // DNSName CN-ID mismatch not reported when there is a DNSName SAN + RDN(CN("a.example.com")), DNSName("b.example.com"), + GeneralSubtree(DNSName("b.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE, + }, + { // DNSName CN-ID match not reported when there is an IPAddress SAN + RDN(CN("a.example.com")), IPAddress(ipv4_addr_bytes), + GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { // DNSName CN-ID mismatch not reported when there is an IPAddress SAN + RDN(CN("a.example.com")), IPAddress(ipv4_addr_bytes), + GeneralSubtree(DNSName("b.example.com")), + Success, Success + }, + + { // IPAddress CN-ID match is detected when there is a SAN w/o any DNSName or + // IPAddress + RDN(CN(ipv4_addr_str)), RFC822Name("foo@example.com"), + GeneralSubtree(IPAddress(ipv4_addr_bytes_FFFFFFFF)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // IPAddress CN-ID mismatch is detected when there is a SAN w/o any DNSName + // or IPAddress + RDN(CN(ipv4_addr_str)), RFC822Name("foo@example.com"), + GeneralSubtree(IPAddress(ipv4_other_addr_bytes_FFFFFFFF)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // IPAddress CN-ID match not reported when there is a DNSName SAN + RDN(CN(ipv4_addr_str)), DNSName("b.example.com"), + GeneralSubtree(IPAddress(ipv4_addr_bytes_FFFFFFFF)), + Success, Success + }, + { // IPAddress CN-ID mismatch not reported when there is a DNSName SAN + RDN(CN(ipv4_addr_str)), DNSName("b.example.com"), + GeneralSubtree(IPAddress(ipv4_addr_bytes_FFFFFFFF)), + Success, Success + }, + { // IPAddress CN-ID match not reported when there is an IPAddress SAN + RDN(CN(ipv4_addr_str)), IPAddress(ipv4_other_addr_bytes), + GeneralSubtree(IPAddress(ipv4_addr_bytes_FFFFFFFF)), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // IPAddress CN-ID mismatch not reported when there is an IPAddress SAN + RDN(CN(ipv4_addr_str)), IPAddress(ipv4_other_addr_bytes), + GeneralSubtree(IPAddress(ipv4_other_addr_bytes_FFFFFFFF)), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + + ///////////////////////////////////////////////////////////////////////////// + // Test that constraints are applied to the most specific (last) CN, and only + // that CN-ID. + + { // Name constraint only matches a.example.com, but the most specific CN + // (i.e. the CN-ID) is b.example.com. (Two CNs in one RDN.) + RDN(CN("a.example.com") + CN("b.example.com")), NO_SAN, + GeneralSubtree(DNSName("a.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // Name constraint only matches a.example.com, but the most specific CN + // (i.e. the CN-ID) is b.example.com. (Two CNs in separate RDNs.) + RDN(CN("a.example.com")) + RDN(CN("b.example.com")), NO_SAN, + GeneralSubtree(DNSName("a.example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success + }, + { // Name constraint only permits b.example.com, and the most specific CN + // (i.e. the CN-ID) is b.example.com. (Two CNs in one RDN.) + RDN(CN("a.example.com") + CN("b.example.com")), NO_SAN, + GeneralSubtree(DNSName("b.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // Name constraint only permits b.example.com, and the most specific CN + // (i.e. the CN-ID) is b.example.com. (Two CNs in separate RDNs.) + RDN(CN("a.example.com")) + RDN(CN("b.example.com")), NO_SAN, + GeneralSubtree(DNSName("b.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + + ///////////////////////////////////////////////////////////////////////////// + // Additional RFC822 name constraint tests. There are more tests regarding + // the DNSName part of the constraint mixed into the DNSName constraint + // tests. + + { ByteString(), RFC822Name("a@example.com"), + GeneralSubtree(RFC822Name("a@example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + + // Bug 1056773: name constraints that omit Local-part but include '@' are + // invalid. + { ByteString(), RFC822Name("a@example.com"), + GeneralSubtree(RFC822Name("@example.com")), + Result::ERROR_BAD_DER, + Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("@example.com"), + GeneralSubtree(RFC822Name("@example.com")), + Result::ERROR_BAD_DER, + Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("example.com"), + GeneralSubtree(RFC822Name("@example.com")), + Result::ERROR_BAD_DER, + Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("a@mail.example.com"), + GeneralSubtree(RFC822Name("a@*.example.com")), + Result::ERROR_BAD_DER, + Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("a@*.example.com"), + GeneralSubtree(RFC822Name(".example.com")), + Result::ERROR_BAD_DER, + Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("@example.com"), + GeneralSubtree(RFC822Name(".example.com")), + Result::ERROR_BAD_DER, + Result::ERROR_BAD_DER + }, + { ByteString(), RFC822Name("@a.example.com"), + GeneralSubtree(RFC822Name(".example.com")), + Result::ERROR_BAD_DER, + Result::ERROR_BAD_DER + }, + + ///////////////////////////////////////////////////////////////////////////// + // Test name constraints with underscores. + // + { ByteString(), DNSName("uses_underscore.example.com"), + GeneralSubtree(DNSName("uses_underscore.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), DNSName("uses_underscore.example.com"), + GeneralSubtree(DNSName("example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), DNSName("a.uses_underscore.example.com"), + GeneralSubtree(DNSName("uses_underscore.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), RFC822Name("a@uses_underscore.example.com"), + GeneralSubtree(RFC822Name("uses_underscore.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), RFC822Name("uses_underscore@example.com"), + GeneralSubtree(RFC822Name("example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), RFC822Name("a@a.uses_underscore.example.com"), + GeneralSubtree(RFC822Name(".uses_underscore.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + + ///////////////////////////////////////////////////////////////////////////// + // Name constraint tests that relate to having an empty SAN. According to RFC + // 5280 this isn't valid, but we allow it for compatibility reasons (see bug + // 1143085). + { // For DNSNames, we fall back to the subject CN. + RDN(CN("a.example.com")), ByteString(), + GeneralSubtree(DNSName("a.example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // For RFC822Names, we do not fall back to the subject emailAddress. + // This new implementation seems to conform better to the standards for + // RFC822 name constraints, by only applying the name constraints to + // emailAddress names in the certificate subject if there is no + // subjectAltName extension in the cert. + // In this case, the presence of the (empty) SAN extension means that RFC822 + // name constraints are not enforced on the emailAddress attributes of the + // subject. + RDN(emailAddress("a@example.com")), ByteString(), + GeneralSubtree(RFC822Name("a@example.com")), + Success, Success + }, + { // Compare this to the case where there is no SAN (i.e. the name + // constraints are enforced, because the extension is not present at all). + RDN(emailAddress("a@example.com")), NO_SAN, + GeneralSubtree(RFC822Name("a@example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + + ///////////////////////////////////////////////////////////////////////////// + // DirectoryName name constraint tests + + { // One AVA per RDN + RDN(OU("Example Organization")) + RDN(CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization")) + + RDN(CN("example.com"))))), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // RDNs can have multiple AVAs. + RDN(OU("Example Organization") + CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization") + + CN("example.com"))))), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // The constraint is a prefix of the subject DN. + RDN(OU("Example Organization")) + RDN(CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization"))))), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // The name constraint is not a prefix of the subject DN. + // Note that for excludedSubtrees, we simply prohibit any non-empty + // directoryName constraint to ensure we are not being too lenient. + RDN(OU("Other Example Organization")) + RDN(CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization")) + + RDN(CN("example.com"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // Same as the previous one, but one RDN with multiple AVAs. + RDN(OU("Other Example Organization") + CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization") + + CN("example.com"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // With multiple AVAs per RDN in the subject DN, the constraint is not a + // prefix of the subject DN. + RDN(OU("Example Organization") + CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // The subject DN RDN has multiple AVAs, but the name constraint has only + // one AVA per RDN. + RDN(OU("Example Organization") + CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization")) + + RDN(CN("example.com"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // The name constraint RDN has multiple AVAs, but the subject DN has only + // one AVA per RDN. + RDN(OU("Example Organization")) + RDN(CN("example.com")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization") + + CN("example.com"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // In this case, the constraint uses a different encoding from the subject. + // We consider them to match because we allow UTF8String and + // PrintableString to compare equal when their contents are equal. + RDN(OU("Example Organization", der::UTF8String)) + RDN(CN("example.com")), + NO_SAN, GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization", + der::PrintableString)) + + RDN(CN("example.com"))))), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // Same as above, but with UTF8String/PrintableString switched. + RDN(OU("Example Organization", der::PrintableString)) + RDN(CN("example.com")), + NO_SAN, GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization", + der::UTF8String)) + + RDN(CN("example.com"))))), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // If the contents aren't the same, then they shouldn't match. + RDN(OU("Other Example Organization", der::UTF8String)) + RDN(CN("example.com")), + NO_SAN, GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization", + der::PrintableString)) + + RDN(CN("example.com"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { // Only UTF8String and PrintableString are considered equivalent. + RDN(OU("Example Organization", der::PrintableString)) + RDN(CN("example.com")), + NO_SAN, GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization", + der::TeletexString)) + + RDN(CN("example.com"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + // Some additional tests for completeness: + // Ensure that wildcards are handled: + { RDN(CN("*.example.com")), NO_SAN, GeneralSubtree(DNSName("example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), DNSName("*.example.com"), + GeneralSubtree(DNSName("example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), DNSName("www.example.com"), + GeneralSubtree(DNSName("*.example.com")), + Result::ERROR_BAD_DER, Result::ERROR_BAD_DER + }, + // Handle multiple name constraint entries: + { RDN(CN("example.com")), NO_SAN, + GeneralSubtree(DNSName("example.org")) + + GeneralSubtree(DNSName("example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { ByteString(), DNSName("example.com"), + GeneralSubtree(DNSName("example.org")) + + GeneralSubtree(DNSName("example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + // Handle multiple names in subject alternative name extension: + { ByteString(), DNSName("example.com") + DNSName("example.org"), + GeneralSubtree(DNSName("example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + // Handle a mix of DNSName and DirectoryName: + { RDN(OU("Example Organization")), DNSName("example.com"), + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization"))))) + + GeneralSubtree(DNSName("example.com")), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { RDN(OU("Other Example Organization")), DNSName("example.com"), + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization"))))) + + GeneralSubtree(DNSName("example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + { RDN(OU("Example Organization")), DNSName("example.org"), + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization"))))) + + GeneralSubtree(DNSName("example.com")), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + // Handle a certificate with no DirectoryName: + { ByteString(), DNSName("example.com"), + GeneralSubtree(DirectoryName(Name(RDN(OU("Example Organization"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, +}; + +class pkixnames_CheckNameConstraints + : public ::testing::Test + , public ::testing::WithParamInterface<NameConstraintParams> +{ +public: + DefaultNameMatchingPolicy mNameMatchingPolicy; +}; + +TEST_P(pkixnames_CheckNameConstraints, + NameConstraintsEnforcedForDirectlyIssuedEndEntity) +{ + // Test that name constraints are enforced on a certificate directly issued by + // a certificate with the given name constraints. + + const NameConstraintParams& param(GetParam()); + + ByteString certDER(CreateCert(param.subject, param.subjectAltName)); + ASSERT_FALSE(ENCODING_FAILED(certDER)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(certDER.data(), certDER.length())); + BackCert cert(certInput, EndEntityOrCA::MustBeEndEntity, nullptr); + ASSERT_EQ(Success, cert.Init()); + + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + PermittedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedPermittedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_serverAuth)); + } + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + ExcludedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedExcludedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_serverAuth)); + } + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + PermittedSubtrees(param.subtrees) + + ExcludedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ((param.expectedPermittedSubtreesResult == + param.expectedExcludedSubtreesResult) + ? param.expectedExcludedSubtreesResult + : Result::ERROR_CERT_NOT_IN_NAME_SPACE, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_serverAuth)); + } +} + +INSTANTIATE_TEST_CASE_P(pkixnames_CheckNameConstraints, + pkixnames_CheckNameConstraints, + testing::ValuesIn(NAME_CONSTRAINT_PARAMS)); + +// The |subjectAltName| param is not used for these test cases (hence the use of +// "NO_SAN"). +static const NameConstraintParams NO_FALLBACK_NAME_CONSTRAINT_PARAMS[] = +{ + // The only difference between end-entities being verified for serverAuth and + // intermediates or end-entities being verified for other uses is that for + // the latter cases, there is no fallback matching of DNSName entries to the + // subject common name. + { RDN(CN("Not a DNSName")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { RDN(CN("a.example.com")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + { RDN(CN("b.example.com")), NO_SAN, GeneralSubtree(DNSName("a.example.com")), + Success, Success + }, + // Sanity-check that name constraints are in fact enforced in these cases. + { RDN(CN("Example Name")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(CN("Example Name"))))), + Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, + // (In this implementation, if a DirectoryName is in excludedSubtrees, nothing + // is considered to be in the name space.) + { RDN(CN("Other Example Name")), NO_SAN, + GeneralSubtree(DirectoryName(Name(RDN(CN("Example Name"))))), + Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE + }, +}; + +class pkixnames_CheckNameConstraintsOnIntermediate + : public ::testing::Test + , public ::testing::WithParamInterface<NameConstraintParams> +{ +}; + +TEST_P(pkixnames_CheckNameConstraintsOnIntermediate, + NameConstraintsEnforcedOnIntermediate) +{ + // Test that name constraints are enforced on an intermediate certificate + // directly issued by a certificate with the given name constraints. + + const NameConstraintParams& param(GetParam()); + + ByteString certDER(CreateCert(param.subject, NO_SAN, + EndEntityOrCA::MustBeCA)); + ASSERT_FALSE(ENCODING_FAILED(certDER)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(certDER.data(), certDER.length())); + BackCert cert(certInput, EndEntityOrCA::MustBeCA, nullptr); + ASSERT_EQ(Success, cert.Init()); + + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + PermittedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedPermittedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_serverAuth)); + } + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + ExcludedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedExcludedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_serverAuth)); + } + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + PermittedSubtrees(param.subtrees) + + ExcludedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedExcludedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_serverAuth)); + } +} + +INSTANTIATE_TEST_CASE_P(pkixnames_CheckNameConstraintsOnIntermediate, + pkixnames_CheckNameConstraintsOnIntermediate, + testing::ValuesIn(NO_FALLBACK_NAME_CONSTRAINT_PARAMS)); + +class pkixnames_CheckNameConstraintsForNonServerAuthUsage + : public ::testing::Test + , public ::testing::WithParamInterface<NameConstraintParams> +{ +}; + +TEST_P(pkixnames_CheckNameConstraintsForNonServerAuthUsage, + NameConstraintsEnforcedForNonServerAuthUsage) +{ + // Test that for key purposes other than serverAuth, fallback to the subject + // common name does not occur. + + const NameConstraintParams& param(GetParam()); + + ByteString certDER(CreateCert(param.subject, NO_SAN)); + ASSERT_FALSE(ENCODING_FAILED(certDER)); + Input certInput; + ASSERT_EQ(Success, certInput.Init(certDER.data(), certDER.length())); + BackCert cert(certInput, EndEntityOrCA::MustBeEndEntity, nullptr); + ASSERT_EQ(Success, cert.Init()); + + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + PermittedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedPermittedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_clientAuth)); + } + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + ExcludedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedExcludedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_clientAuth)); + } + { + ByteString nameConstraintsDER(TLV(der::SEQUENCE, + PermittedSubtrees(param.subtrees) + + ExcludedSubtrees(param.subtrees))); + Input nameConstraints; + ASSERT_EQ(Success, + nameConstraints.Init(nameConstraintsDER.data(), + nameConstraintsDER.length())); + ASSERT_EQ(param.expectedExcludedSubtreesResult, + CheckNameConstraints(nameConstraints, cert, + KeyPurposeId::id_kp_clientAuth)); + } +} + +INSTANTIATE_TEST_CASE_P(pkixnames_CheckNameConstraintsForNonServerAuthUsage, + pkixnames_CheckNameConstraintsForNonServerAuthUsage, + testing::ValuesIn(NO_FALLBACK_NAME_CONSTRAINT_PARAMS)); |