From 4a2c8b52748fe3c81561a313e0789add48233b15 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 30 Sep 2014 14:41:39 -0700 Subject: [PATCH] Bug 1063281, Part 2: Implement IsValidDNSName, r=keeler --HG-- extra : rebase_source : 202898df26c7321f543ab7aeb222cdc6db67fe0d --- security/pkix/lib/pkixnames.cpp | 137 ++++++++++ security/pkix/moz.build | 1 + security/pkix/test/gtest/moz.build | 1 + security/pkix/test/gtest/pkixnames_tests.cpp | 261 +++++++++++++++++++ 4 files changed, 400 insertions(+) create mode 100644 security/pkix/lib/pkixnames.cpp create mode 100644 security/pkix/test/gtest/pkixnames_tests.cpp diff --git a/security/pkix/lib/pkixnames.cpp b/security/pkix/lib/pkixnames.cpp new file mode 100644 index 000000000000..934a60c45a90 --- /dev/null +++ b/security/pkix/lib/pkixnames.cpp @@ -0,0 +1,137 @@ +/* -*- 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. + */ + +// This code attempts to implement RFC6125 name matching. It also attempts to +// give the same results as the Chromium implementation +// (X509Certificate::VerifyHostname) when both are given clean input (no +// leading whitespace, etc.) +// +// 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 +// can't rely on them. + +#include "pkix/Input.h" + +namespace mozilla { namespace pkix { + +bool +IsValidDNSName(Input hostname) +{ + if (hostname.GetLength() > 253) { + return false; + } + + Reader input(hostname); + size_t labelLength = 0; + bool labelIsAllNumeric = false; + bool endsWithHyphen = false; + + do { + static const size_t MAX_LABEL_LENGTH = 63; + + uint8_t b; + if (input.Read(b) != Success) { + return false; + } + switch (b) { + case '-': + if (labelLength == 0) { + return false; // Labels must not start with a hyphen. + } + labelIsAllNumeric = false; + endsWithHyphen = true; + ++labelLength; + if (labelLength > MAX_LABEL_LENGTH) { + return false; + } + break; + + // We avoid isdigit because it is locale-sensitive. See + // http://pubs.opengroup.org/onlinepubs/009695399/functions/isdigit.html + case '0': case '5': + case '1': case '6': + case '2': case '7': + case '3': case '8': + case '4': case '9': + if (labelLength == 0) { + labelIsAllNumeric = true; + } + endsWithHyphen = false; + ++labelLength; + if (labelLength > MAX_LABEL_LENGTH) { + return false; + } + break; + + // We avoid using islower/isupper/tolower/toupper or similar things, to + // avoid any possibility of this code being locale-sensitive. See + // http://pubs.opengroup.org/onlinepubs/009695399/functions/isupper.html + case 'a': case 'A': case 'n': case 'N': + case 'b': case 'B': case 'o': case 'O': + case 'c': case 'C': case 'p': case 'P': + case 'd': case 'D': case 'q': case 'Q': + case 'e': case 'E': case 'r': case 'R': + case 'f': case 'F': case 's': case 'S': + case 'g': case 'G': case 't': case 'T': + case 'h': case 'H': case 'u': case 'U': + case 'i': case 'I': case 'v': case 'V': + case 'j': case 'J': case 'w': case 'W': + case 'k': case 'K': case 'x': case 'X': + case 'l': case 'L': case 'y': case 'Y': + case 'm': case 'M': case 'z': case 'Z': + labelIsAllNumeric = false; + endsWithHyphen = false; + ++labelLength; + if (labelLength > MAX_LABEL_LENGTH) { + return false; + } + break; + + case '.': + if (labelLength == 0) { + return false; + } + if (endsWithHyphen) { + return false; // Labels must not end with a hyphen. + } + labelLength = 0; + break; + + default: + return false; // Invalid character. + } + } while (!input.AtEnd()); + + if (endsWithHyphen) { + return false; // Labels must not end with a hyphen. + } + + if (labelIsAllNumeric) { + return false; // Last label must not be all numeric. + } + + return true; +} + +} } // namespace mozilla::pkix diff --git a/security/pkix/moz.build b/security/pkix/moz.build index 45aaa4be2a51..5f735624286b 100644 --- a/security/pkix/moz.build +++ b/security/pkix/moz.build @@ -10,6 +10,7 @@ SOURCES += [ 'lib/pkixcert.cpp', 'lib/pkixcheck.cpp', 'lib/pkixder.cpp', + 'lib/pkixnames.cpp', 'lib/pkixnss.cpp', 'lib/pkixocsp.cpp', 'lib/pkixresult.cpp', diff --git a/security/pkix/test/gtest/moz.build b/security/pkix/test/gtest/moz.build index b43287200a17..92ac437d7942 100644 --- a/security/pkix/test/gtest/moz.build +++ b/security/pkix/test/gtest/moz.build @@ -17,6 +17,7 @@ SOURCES += [ 'pkixder_pki_types_tests.cpp', 'pkixder_universal_types_tests.cpp', 'pkixgtest.cpp', + 'pkixnames_tests.cpp', 'pkixocsp_CreateEncodedOCSPRequest_tests.cpp', 'pkixocsp_VerifyEncodedOCSPResponse.cpp', ] diff --git a/security/pkix/test/gtest/pkixnames_tests.cpp b/security/pkix/test/gtest/pkixnames_tests.cpp new file mode 100644 index 000000000000..32c850e49091 --- /dev/null +++ b/security/pkix/test/gtest/pkixnames_tests.cpp @@ -0,0 +1,261 @@ +/* -*- 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 + +#include "pkix/Input.h" +#include "pkixgtest.h" +#include "pkixtestutil.h" + +namespace mozilla { namespace pkix { + +bool IsValidDNSName(Input hostname); + +} } // namespace mozilla::pkix + +using namespace mozilla::pkix; +using namespace mozilla::pkix::test; + +struct InputValidity +{ + ByteString input; + bool isValid; +}; + +// 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, valid) \ + { \ + ByteString(reinterpret_cast(str), sizeof(str) - 1), valid \ + } + +static const InputValidity DNSNAMES_VALIDITY[] = +{ + I("a", true), + I("a.b", true), + I("a.b.c", true), + I("a.b.c.d", true), + + // empty labels + I("", false), + I(".", false), + I("a", true), + I(".a", false), + I(".a.b", false), + I("..a", false), + I("a..b", false), + I("a...b", false), + I("a..b.c", false), + I("a.b..c", false), + I(".a.b.c.", false), + + // absolute names + I("a.", true), + I("a.b.", true), + I("a.b.c.", true), + + // absolute names with empty label at end + I("a..", false), + I("a.b..", false), + I("a.b.c..", false), + I("a...", false), + + // Punycode + I("xn--", false), + I("xn--.", false), + I("xn--.a", false), + I("a.xn--", false), + I("a.xn--.", false), + I("a.xn--.b", false), + I("a.xn--.b", false), + I("a.xn--\0.b", false), + I("a.xn--a.b", true), + I("xn--a", true), + I("a.xn--a", true), + I("a.xn--a.a", true), + I("\0xc4\0x95.com", false), // UTF-8 ĕ + I("xn--jea.com", true), // punycode ĕ + I("xn--\0xc4\0x95.com", false), // UTF-8 ĕ, malformed punycode + UTF-8 mashup + + // Surprising punycode + I("xn--google.com", true), // 䕮䕵䕶䕱.com + I("xn--citibank.com", true), // 岍岊岊岅岉岎.com + I("xn--cnn.com", true), // 䁾.com + I("a.xn--cnn", true), // a.䁾 + I("a.xn--cnn.com", true), // a.䁾.com + + I("1.2.3.4", false), // IPv4 address + I("1::2", false), // IPV6 address + + // whitespace not allowed anywhere. + I(" ", false), + I(" a", false), + I("a ", false), + I("a b", false), + I("a.b 1", false), + I("a\t", false), + + // Nulls not allowed + I("\0", false), + I("a\0", false), + I("example.org\0.example.com", false), // Hi Moxie! + I("\0a", false), + I("xn--\0", 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), + 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), + I("0.1.2.3.4.5.6.7.8.9.a", true), // "a" needed to avoid numeric last label + I("a-b", true), // hyphen (a label cannot start or end with a hyphen) + + // An invalid character in various positions + I("!", false), + I("!a", false), + I("a!", false), + I("a!b", false), + I("a.!", false), + I("a.a!", false), + I("a.!a", false), + I("a.a!a", false), + I("a.!a.a", false), + I("a.a!.a", false), + I("a.a!a.a", false), + + // Various other invalid characters + I("a!", false), + I("a@", false), + I("a#", false), + I("a$", false), + I("a%", false), + I("a^", false), + I("a&", false), + I("a*", false), + I("a(", false), + I("a)", false), + + // last label can't be fully numeric + I("1", false), + I("a.1", false), + + // other labels can be fully numeric + I("1.a", true), + I("1.2.a", true), + I("1.2.3.a", true), + + // last label can be *partly* numeric + I("1a", true), + I("1.1a", true), + I("1-1", true), + I("a.1-1", true), + I("a.1-a", true), + + // labels cannot start with a hyphen + I("-", false), + I("-1", false), + + // labels cannot end with a hyphen + I("1-", false), + I("1-.a", false), + I("a-", false), + I("a-.a", false), + I("a.1-.a", false), + I("a.a-.a", false), + + // labels can contain a hyphen in the middle + I("a-b", true), + I("1-2", true), + I("a.a-1", true), + + // multiple consecutive hyphens allowed + I("a--1", true), + I("1---a", true), + I("a-----------------b", true), + + // Wildcard specifications are not valid DNS names + I("*.a", false), + I("a*", false), + I("a*.a", 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), + + // maximum label length is 63 characters + I("1234567890" "1234567890" "1234567890" + "1234567890" "1234567890" "1234567890" "abc", true), + I("1234567890" "1234567890" "1234567890" + "1234567890" "1234567890" "1234567890" "abcd", 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), + 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), +}; + +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), // ASCII capital I + I("i", true), // ASCII lowercase i + I("\0xC4\0xB0", false), // latin capital letter i with dot above + I("\0xC4\0xB1", false), // latin small letter dotless i + I("xn--i-9bb", true), // latin capital letter i with dot above, in punycode + I("xn--cfa", true), // latin small letter dotless i, in punycode + I("xn--\0xC4\0xB0", false), // latin capital letter i with dot above, mashup + I("xn--\0xC4\0xB1", false), // latin small letter dotless i, mashup +}; + +class pkixnames_IsValidDNSName + : public ::testing::Test + , public ::testing::WithParamInterface +{ +}; + +TEST_P(pkixnames_IsValidDNSName, IsValidDNSName) +{ + 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.isValid, IsValidDNSName(input)); +} + +INSTANTIATE_TEST_CASE_P(pkixnames_IsValidDNSName, + pkixnames_IsValidDNSName, + testing::ValuesIn(DNSNAMES_VALIDITY)); +INSTANTIATE_TEST_CASE_P(pkixnames_IsValidDNSName_Turkish_I, + pkixnames_IsValidDNSName, + testing::ValuesIn(DNSNAMES_VALIDITY_TURKISH_I));