Bug 1063281, Part 5: Implement DNS ID matching, r=keeler

--HG--
extra : rebase_source : 5221245ce8da065d64a7ff17bdfde0e617562447
This commit is contained in:
Brian Smith 2014-09-30 19:40:15 -07:00
parent 01eb47bf7f
commit d9be7c3bb6
2 changed files with 489 additions and 6 deletions

View File

@ -22,10 +22,14 @@
* limitations under the License. * limitations under the License.
*/ */
// This code attempts to implement RFC6125 name matching. It also attempts to // This code attempts to implement RFC6125 name matching.
// give the same results as the Chromium implementation //
// (X509Certificate::VerifyHostname) when both are given clean input (no // In this code, identifiers are classified as either "presented" or
// leading whitespace, etc.) // "reference" identifiers are defined in
// http://tools.ietf.org/html/rfc6125#section-1.8. A "presented identifier" is
// one in the subjectAltName of the certificate, or sometimes within a CN of
// the certificate's subject. The "reference identifier" is the one we are
// being asked to match the certificate against.
// //
// On Windows and maybe other platforms, OS-provided IP address parsing // On Windows and maybe other platforms, OS-provided IP address parsing
// functions might fail if the protocol (IPv4 or IPv6) has been disabled, so we // functions might fail if the protocol (IPv4 or IPv6) has been disabled, so we
@ -38,6 +42,189 @@ namespace mozilla { namespace pkix {
namespace { namespace {
uint8_t LocaleInsensitveToLower(uint8_t a);
bool StartsWithIDNALabel(Input id);
} // unnamed namespace
// We do not distinguish between a syntactically-invalid presentedDNSID and one
// that is syntactically valid but does not match referenceDNSID; in both
// cases, the result is false.
//
// We assume that both presentedDNSID and referenceDNSID are encoded in such a
// way that US-ASCII (7-bit) characters are encoded in one byte and no encoding
// of a non-US-ASCII character contains a code point in the range 0-127. For
// example, UTF-8 is OK but UTF-16 is not.
//
// The referenceDNSIDWasVerifiedAsValid parameter must be the result of calling
// IsValidDNSName.
//
// RFC6125 says that a wildcard label may be of the form <x>*<y>.<DNSID>, where
// <x> and/or <y> may be empty. However, NSS requires <y> to be empty, and we
// follow NSS's stricter policy by accepting wildcards only of the form
// <x>*.<DNSID>, where <x> may be empty.
bool
PresentedDNSIDMatchesReferenceDNSID(Input presentedDNSID,
Input referenceDNSID,
bool referenceDNSIDWasVerifiedAsValid)
{
assert(referenceDNSIDWasVerifiedAsValid);
if (!referenceDNSIDWasVerifiedAsValid) {
return false;
}
Reader presented(presentedDNSID);
Reader reference(referenceDNSID);
size_t currentLabel = 0;
bool hasWildcardLabel = false;
bool lastPresentedByteWasDot = false;
bool firstPresentedByteIsWildcard = presented.Peek('*');
do {
uint8_t presentedByte;
if (presented.Read(presentedByte) != Success) {
return false; // Reject completely empty input.
}
if (presentedByte == '*' && currentLabel == 0) {
hasWildcardLabel = true;
// Like NSS, be stricter than RFC6125 requires by insisting that the
// "*" must be the last character in the label. This also prevents
// multiple "*" in the label.
if (!presented.Peek('.')) {
return false;
}
// RFC6125 says that we shouldn't accept wildcards within an IDN A-Label.
//
// We also don't allow a non-IDN presented ID label to match an IDN
// reference ID label, except when the entire presented ID label is "*".
// This avoids confusion when matching a presented ID like
// "xn-*.example.org" against "xn--www.example.org" (which attempts to
// abuse the punycode syntax) or "www-*.example.org" against
// "xn--www--ep4c4a2kpf" (which makes sense to match, semantically, but
// no implementations actually do).
//
// XXX: The consequence of this is that we effectively discriminate
// against users of languages that cannot be encoded with ASCII.
if (!firstPresentedByteIsWildcard) {
if (StartsWithIDNALabel(presentedDNSID)) {
return false;
}
if (StartsWithIDNALabel(referenceDNSID)) {
return false;
}
}
// RFC 6125 is unclear about whether "www*.example.org" matches
// "www.example.org". The Chromium test suite has this test:
//
// { false, "w.bar.foo.com", "w*.bar.foo.com" },
//
// We agree with Chromium by forbidding "*" from expanding to the empty
// string.
uint8_t referenceByte;
if (reference.Read(referenceByte) != Success) {
return false;
}
if (referenceByte == '.') {
return false;
}
while (!reference.Peek('.')) {
if (reference.Read(referenceByte) != Success) {
return false;
}
}
} else {
if (presentedByte == '.') {
// This check is needed to prevent ".." at the end of the presented ID
// from being accepted.
if (lastPresentedByteWasDot) {
return false;
}
lastPresentedByteWasDot = true;
if (!presented.AtEnd()) {
++currentLabel;
}
} else {
lastPresentedByteWasDot = false;
}
// The presented ID may have a terminating dot '.' to mark it as
// absolute, and it still matches a reference ID without that
// terminating dot.
if (presentedByte != '.' || !presented.AtEnd() || !reference.AtEnd()) {
uint8_t referenceByte;
if (reference.Read(referenceByte) != Success) {
return false;
}
if (LocaleInsensitveToLower(presentedByte) !=
LocaleInsensitveToLower(referenceByte)) {
return false;
}
}
}
} while (!presented.AtEnd());
// The reference ID may have a terminating dot '.' to mark it as absolute,
// and a presented ID without that terminating dot still matches it.
static const uint8_t DOT[1] = { '.' };
if (!reference.AtEnd() && !reference.MatchRest(DOT)) {
return false;
}
if (hasWildcardLabel) {
// Like NSS, we require at least two labels after the wildcard.
if (currentLabel < 2) {
return false;
}
// TODO(bug XXXXXXX): Allow the TrustDomain to control this on a
// per-eTLD+1 basis, similar to Chromium. Even then, it might be better to
// still enforce that there are at least two labels after the wildcard.
// TODO(bug XXXXXXX): Wildcards are not allowed for EV certificates.
// Provide an option to indicate whether wildcards should be matched, for
// the purpose of helping the application enforce this.
}
return true;
}
namespace {
// We avoid isdigit because it is locale-sensitive. See
// http://pubs.opengroup.org/onlinepubs/009695399/functions/tolower.html.
inline uint8_t
LocaleInsensitveToLower(uint8_t a)
{
if (a >= 'A' && a <= 'Z') { // unlikely
return static_cast<uint8_t>(
static_cast<uint8_t>(a - static_cast<uint8_t>('A')) +
static_cast<uint8_t>('a'));
}
return a;
}
bool
StartsWithIDNALabel(Input id)
{
static const uint8_t IDN_ALABEL_PREFIX[4] = { 'x', 'n', '-', '-' };
Reader input(id);
for (size_t i = 0; i < sizeof(IDN_ALABEL_PREFIX); ++i) {
uint8_t b;
if (input.Read(b) != Success) {
return false;
}
if (b != IDN_ALABEL_PREFIX[i]) {
return false;
}
}
return true;
}
bool bool
ReadIPv4AddressComponent(Reader& input, bool lastComponent, ReadIPv4AddressComponent(Reader& input, bool lastComponent,
/*out*/ uint8_t& valueOut) /*out*/ uint8_t& valueOut)

View File

@ -30,6 +30,10 @@
namespace mozilla { namespace pkix { namespace mozilla { namespace pkix {
bool PresentedDNSIDMatchesReferenceDNSID(Input presentedDNSID,
Input referenceDNSID,
bool referenceDNSIDWasVerifiedAsValid);
bool IsValidDNSName(Input hostname); bool IsValidDNSName(Input hostname);
bool ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]); bool ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]);
bool ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]); bool ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]);
@ -39,6 +43,236 @@ bool ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]);
using namespace mozilla::pkix; using namespace mozilla::pkix;
using namespace mozilla::pkix::test; using namespace mozilla::pkix::test;
struct PresentedMatchesReference
{
ByteString presentedDNSID;
ByteString referenceDNSID;
bool matches;
};
#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), \
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), \
false \
}
static const PresentedMatchesReference DNSID_MATCH_PARAMS[] =
{
DNS_ID_MISMATCH("", "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."),
// Wildcard not in leftmost label
DNS_ID_MATCH("d.c.b.a", "d.c.b.a"),
DNS_ID_MISMATCH("d.*.b.a", "d.c.b.a"),
DNS_ID_MISMATCH("d.c*.b.a", "d.c.b.a"),
DNS_ID_MISMATCH("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, and absolute names can match
// relative names, and vice-versa.
DNS_ID_MATCH("example", "example"),
DNS_ID_MATCH("example.", "example."),
DNS_ID_MATCH("example", "example."),
DNS_ID_MATCH("example.", "example"),
DNS_ID_MATCH("example.com", "example.com"),
DNS_ID_MATCH("example.com.", "example.com."),
DNS_ID_MATCH("example.com", "example.com."),
DNS_ID_MATCH("example.com.", "example.com"),
DNS_ID_MISMATCH("example.com..", "example.com."),
DNS_ID_MISMATCH("example.com..", "example.com"),
DNS_ID_MISMATCH("example.com...", "example.com."),
// xn-- IDN prefix
DNS_ID_MATCH("x*.b.a", "xa.b.a"),
DNS_ID_MATCH("x*.b.a", "xna.b.a"),
DNS_ID_MATCH("x*.b.a", "xn-a.b.a"),
DNS_ID_MISMATCH("x*.b.a", "xn--a.b.a"),
DNS_ID_MISMATCH("xn*.b.a", "xn--a.b.a"),
DNS_ID_MISMATCH("xn-*.b.a", "xn--a.b.a"),
DNS_ID_MISMATCH("xn--*.b.a", "xn--a.b.a"),
DNS_ID_MISMATCH("xn*.b.a", "xn--a.b.a"),
DNS_ID_MISMATCH("xn-*.b.a", "xn--a.b.a"),
DNS_ID_MISMATCH("xn--*.b.a", "xn--a.b.a"),
DNS_ID_MISMATCH("xn---*.b.a", "xn--a.b.a"),
// "*" cannot expand to nothing.
DNS_ID_MISMATCH("c*.b.a", "c.b.a"),
// --------------------------------------------------------------------------
// The rest of these are test cases adapted from Chromium's
// x509_certificate_unittest.cc. The parameter order is the opposite in
// Chromium's tests. Also, they 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_MISMATCH(".uk", "f.uk"),
DNS_ID_MISMATCH("?.bar.foo.com", "w.bar.foo.com"),
DNS_ID_MISMATCH("(www|ftp).foo.com", "www.foo.com"), // regex!
DNS_ID_MISMATCH("www.foo.com\0", "www.foo.com"),
DNS_ID_MISMATCH("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_MISMATCH("*.org", "test.org"),
DNS_ID_MISMATCH("w*.bar.foo.com", "w.bar.foo.com"),
DNS_ID_MISMATCH("ww*ww.bar.foo.com", "www.bar.foo.com"),
DNS_ID_MISMATCH("ww*ww.bar.foo.com", "wwww.bar.foo.com"),
// Different than Chromium, matches NSS.
DNS_ID_MISMATCH("w*w.bar.foo.com", "wwww.bar.foo.com"),
DNS_ID_MISMATCH("w*w.bar.foo.c0m", "wwww.bar.foo.com"),
DNS_ID_MATCH("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_MISMATCH("*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_MISMATCH("*.jp", "www.test.co.jp"),
DNS_ID_MISMATCH("www.test.co.uk", "www.test.co.jp"),
DNS_ID_MISMATCH("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_MISMATCH("*.*.foo.com", "www.bar.foo.com"),
DNS_ID_MISMATCH("*.*.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_MISMATCH("*.*.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_MISMATCH("www.bath.org", ""),
// DNS_ID_MISMATCH("www.bath.org", "20.30.40.50"),
// DNS_ID_MISMATCH("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_MISMATCH("xn--poema-*.com.br", "xn--poema-9qae5a.com.br"),
DNS_ID_MISMATCH("xn--*-9qae5a.com.br", "xn--poema-9qae5a.com.br"),
DNS_ID_MISMATCH("*--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
DNS_ID_MATCH("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_MISMATCH("*baz.example.net", "foobaz.example.net"),
DNS_ID_MISMATCH("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_MISMATCH("*.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_MISMATCH("*.com", "foo.com"),
DNS_ID_MISMATCH("*.us", "foo.us"),
DNS_ID_MISMATCH("*", "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_MISMATCH("*.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_MISMATCH("*.*.com", "foo.example.com"),
DNS_ID_MISMATCH("*.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.
// TODO: File errata against RFC 6125 about this.
DNS_ID_MATCH("foo.com.", "foo.com"),
DNS_ID_MATCH("foo.com", "foo.com."),
DNS_ID_MATCH("foo.com.", "foo.com."),
DNS_ID_MATCH("f.", "f"),
DNS_ID_MATCH("f", "f."),
DNS_ID_MATCH("f.", "f."),
DNS_ID_MATCH("*.bar.foo.com.", "www-3.bar.foo.com"),
DNS_ID_MATCH("*.bar.foo.com", "www-3.bar.foo.com."),
DNS_ID_MATCH("*.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_MISMATCH("*.com.", "example.com"),
DNS_ID_MISMATCH("*.com", "example.com."),
DNS_ID_MISMATCH("*.com.", "example.com."),
DNS_ID_MISMATCH("*.", "foo."),
DNS_ID_MISMATCH("*.", "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."),
};
struct InputValidity struct InputValidity
{ {
ByteString input; ByteString input;
@ -63,8 +297,8 @@ static const InputValidity DNSNAMES_VALIDITY[] =
I("", false), I("", false),
I(".", false), I(".", false),
I("a", true), I("a", true),
I(".a", false), I(".a", false), // PresentedDNSIDMatchesReferenceDNSID depends on this
I(".a.b", false), I(".a.b", false), // PresentedDNSIDMatchesReferenceDNSID depends on this
I("..a", false), I("..a", false),
I("a..b", false), I("a..b", false),
I("a...b", false), I("a...b", false),
@ -239,6 +473,11 @@ static const InputValidity DNSNAMES_VALIDITY_TURKISH_I[] =
I("xn--\0xC4\0xB1", false), // latin small letter dotless i, mashup I("xn--\0xC4\0xB1", 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> template <unsigned int L>
struct IPAddressParams struct IPAddressParams
{ {
@ -573,6 +812,63 @@ static const IPAddressParams<16> IPV6_ADDRESSES[] =
IPV6_INVALID("::1.2\02.3.4"), IPV6_INVALID("::1.2\02.3.4"),
}; };
class pkixnames_PresentedDNSIDMatchesReferenceDNSID
: public ::testing::Test
, public ::testing::WithParamInterface<PresentedMatchesReference>
{
};
TEST_P(pkixnames_PresentedDNSIDMatchesReferenceDNSID,
PresentedDNSIDMatchesReferenceDNSID)
{
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()));
bool referenceIsValidDNSName = IsValidDNSName(reference);
ASSERT_TRUE(referenceIsValidDNSName); // sanity check that test makes sense
ASSERT_EQ(param.matches,
PresentedDNSIDMatchesReferenceDNSID(presented, reference,
referenceIsValidDNSName));
}
INSTANTIATE_TEST_CASE_P(pkixnames_PresentedDNSIDMatchesReferenceDNSID,
pkixnames_PresentedDNSIDMatchesReferenceDNSID,
testing::ValuesIn(DNSID_MATCH_PARAMS));
class pkixnames_Turkish_I_Comparison
: public ::testing::Test
, public ::testing::WithParamInterface<InputValidity>
{
};
TEST_P(pkixnames_Turkish_I_Comparison, PresentedDNSIDMatchesReferenceDNSID)
{
// 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);
ASSERT_EQ(isASCII, PresentedDNSIDMatchesReferenceDNSID(input, LOWERCASE_I,
true));
ASSERT_EQ(isASCII, PresentedDNSIDMatchesReferenceDNSID(input, UPPERCASE_I,
true));
}
INSTANTIATE_TEST_CASE_P(pkixnames_Turkish_I_Comparison,
pkixnames_Turkish_I_Comparison,
testing::ValuesIn(DNSNAMES_VALIDITY_TURKISH_I));
class pkixnames_IsValidDNSName class pkixnames_IsValidDNSName
: public ::testing::Test : public ::testing::Test
, public ::testing::WithParamInterface<InputValidity> , public ::testing::WithParamInterface<InputValidity>