Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(172)

Unified Diff: net/cert/internal/name_constraints.cc

Issue 1214933009: Class for parsing and evaluating RFC 5280 NameConstraints. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@compare_DN2
Patch Set: use test_helpers.h Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: net/cert/internal/name_constraints.cc
diff --git a/net/cert/internal/name_constraints.cc b/net/cert/internal/name_constraints.cc
new file mode 100644
index 0000000000000000000000000000000000000000..5112bd044b9dd2fecd4362e9b18c56fc4c961e1f
--- /dev/null
+++ b/net/cert/internal/name_constraints.cc
@@ -0,0 +1,495 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/cert/internal/name_constraints.h"
+
+#include "base/strings/string_util.h"
+#include "net/cert/internal/verify_name_match.h"
+#include "net/der/input.h"
+#include "net/der/parser.h"
+#include "net/der/tag.h"
+
+namespace net {
+
+namespace {
+
+// Return true if |name| falls in the subtree defined by |name_space|.
+// RFC 5280 section 4.2.1.10:
+// DNS name restrictions are expressed as host.example.com. Any DNS
+// name that can be constructed by simply adding zero or more labels
+// to the left-hand side of the name satisfies the name constraint. For
+// example, www.host.example.com would satisfy the constraint but
+// host1.example.com would not.
+//
+// Also handles wildcard names (|name| starts with "*.").
+// If |wildcard_matching| is WILDCARD_PARTIAL_MATCH "*.bar.com" is considered to
+// match the constraint "foo.bar.com". If it is WILDCARD_FULL_MATCH, "*.bar.com"
+// will match "bar.com" but not "foo.bar.com".
+// Wildcard handling is not specified by RFC 5280, but since certificate
+// verification allows it, name constraints must check it similarly.
+enum WildcardMatchType { WILDCARD_PARTIAL_MATCH, WILDCARD_FULL_MATCH };
eroman 2015/08/26 19:56:43 nit: newline after this
mattm 2015/08/29 01:37:18 Done.
+bool DNSNameMatches(const std::string& raw_name,
+ const std::string& raw_name_space,
+ WildcardMatchType wildcard_matching) {
+ base::StringPiece name(raw_name);
+ base::StringPiece name_space(raw_name_space);
+ // Normalize absolute DNS names by removing the trailing dot.
+ if (!name.empty() && *name.rbegin() == '.')
+ name.remove_suffix(1);
+ if (!name_space.empty() && *name_space.rbegin() == '.')
+ name_space.remove_suffix(1);
+
+ // Everything matches the empty name space.
+ if (name_space.empty())
+ return true;
+
+ // Wildcard partial-match handling ("*.bar.com" matching name space
+ // "foo.bar.com").
+ if (wildcard_matching == WILDCARD_PARTIAL_MATCH && name.size() > 2 &&
+ name[0] == '*' && name[1] == '.') {
+ size_t name_space_dot_pos = name_space.find('.');
+ if (name_space_dot_pos != std::string::npos) {
+ base::StringPiece name_space_domain(
+ name_space.begin() + name_space_dot_pos + 1,
+ name_space.size() - name_space_dot_pos - 1);
+ base::StringPiece wildcard_domain(name.begin() + 2, name.size() - 2);
+ if (base::EqualsCaseInsensitiveASCII(wildcard_domain, name_space_domain))
+ return true;
+ }
+ }
+
+ if (!base::EndsWith(name, name_space, base::CompareCase::INSENSITIVE_ASCII))
+ return false;
+ // Exact match.
+ if (name.size() == name_space.size())
+ return true;
+ // Subtree match.
+ if (name.size() >= name_space.size() + 2 &&
+ name[name.size() - name_space.size() - 1] == '.')
+ return true;
+ // Trailing text matches, but not in a subtree (e.g., "foobar.com" is not a
+ // match for "bar.com").
+ return false;
+}
+
+// Return true if |ip| matches the ip/netmask pair |ip_constraint|.
+// RFC 5280 section 4.2.1.10:
+// The syntax of iPAddress MUST be as described in Section 4.2.1.6 with
+// the following additions specifically for name constraints. For IPv4
+// addresses, the iPAddress field of GeneralName MUST contain eight (8)
+// octets, encoded in the style of RFC 4632 (CIDR) to represent an
+// address range [RFC4632]. For IPv6 addresses, the iPAddress field
+// MUST contain 32 octets similarly encoded. For example, a name
+// constraint for "class C" subnet 192.0.2.0 is represented as the
+// octets C0 00 02 00 FF FF FF 00, representing the CIDR notation
+// 192.0.2.0/24 (mask 255.255.255.0).
+bool VerifyIPMatchesConstraint(const IPAddressNumber& ip,
+ const std::vector<uint8_t>& ip_constraint) {
+ if (ip.size() != kIPv4AddressSize && ip.size() != kIPv6AddressSize)
+ return false;
+ if (ip_constraint.size() != ip.size() * 2)
+ return false;
+
+ std::vector<uint8_t>::const_iterator prefix_iter = ip_constraint.begin();
+ std::vector<uint8_t>::const_iterator netmask_iter =
+ ip_constraint.begin() + ip_constraint.size() / 2;
+ IPAddressNumber::const_iterator ip_iter = ip.begin();
+ for (; ip_iter != ip.end(); ++ip_iter, ++prefix_iter, ++netmask_iter) {
+ // This assumes that any non-masked bits of the prefix are 0, as required by
+ // RFC 4632 section 3.1.
+ if ((*ip_iter & *netmask_iter) != *prefix_iter)
+ return false;
+ }
+ return true;
+}
+
+enum ParseGeneralNameUnsupportedTypeBehavior {
+ RECORD_UNSUPPORTED,
+ IGNORE_UNSUPPORTED,
+};
eroman 2015/08/26 19:56:43 nit: newline
mattm 2015/08/29 01:37:17 Done.
+// Parse a GeneralName value and add it to |subtrees|.
eroman 2015/08/26 19:56:43 nit: Parse --> Parses ?
mattm 2015/08/29 01:37:18 Done.
+// The GeneralName values are not validated here, since failing on invalid names
+// here could cause an unnecessary failure if a name of that type does not
+// actually appear in the cert chain.
+bool ParseGeneralName(
+ const der::Input& input,
+ NameConstraints::GeneralNames* subtrees,
+ ParseGeneralNameUnsupportedTypeBehavior on_unsupported_types) {
+ der::Parser parser(input);
+ der::Tag tag;
+ der::Input value;
+ if (!parser.ReadTagAndValue(&tag, &value))
+ return false;
+ if ((tag & der::kTagClassMask) != der::kTagContextSpecific)
+ return false;
+ int klass = tag & ~der::kTagClassMask;
eroman 2015/08/26 19:56:43 klass ?
mattm 2015/08/29 01:37:18 Since class is a reserved word. But I've changed i
+ // GeneralName ::= CHOICE {
+ switch (klass) {
+ // otherName [0] OtherName,
+ case 0 + der::kTagConstructed:
+ if (on_unsupported_types != IGNORE_UNSUPPORTED)
eroman 2015/08/26 19:56:43 Rather than check this in every branch, can you ge
mattm 2015/08/29 01:37:18 It's not checked in every branch, only the branche
+ subtrees->has_other_names = true;
+ break;
+ // rfc822Name [1] IA5String,
+ case 1:
+ if (on_unsupported_types != IGNORE_UNSUPPORTED)
+ subtrees->has_rfc822_names = true;
+ break;
+ // dNSName [2] IA5String,
+ case 2:
+ subtrees->dns_names.push_back(value.AsString());
+ break;
+ // x400Address [3] ORAddress,
+ case 3 + der::kTagConstructed:
+ if (on_unsupported_types != IGNORE_UNSUPPORTED)
+ subtrees->has_x400_addresses = true;
+ break;
+ // directoryName [4] Name,
+ case 4 + der::kTagConstructed:
+ subtrees->directory_names.push_back(std::vector<uint8_t>(
+ value.UnsafeData(), value.UnsafeData() + value.Length()));
+ break;
+ // ediPartyName [5] EDIPartyName,
+ case 5 + der::kTagConstructed:
+ if (on_unsupported_types != IGNORE_UNSUPPORTED)
+ subtrees->has_edi_party_names = true;
+ break;
+ // uniformResourceIdentifier [6] IA5String,
+ case 6:
+ if (on_unsupported_types != IGNORE_UNSUPPORTED)
+ subtrees->has_uniform_resource_identifiers = true;
+ break;
+ // iPAddress [7] OCTET STRING,
+ case 7:
+ subtrees->ip_addresses.push_back(std::vector<uint8_t>(
eroman 2015/08/26 19:56:44 Should the ip be validated?
mattm 2015/08/29 01:37:18 Validating the ip would require a bit of extra wor
+ value.UnsafeData(), value.UnsafeData() + value.Length()));
eroman 2015/08/26 19:56:43 der::Input() has a ToString() method. Maybe this g
+ break;
+ // registeredID [8] OBJECT IDENTIFIER }
+ case 8:
+ if (on_unsupported_types != IGNORE_UNSUPPORTED)
+ subtrees->has_registered_ids = true;
+ break;
+ default:
+ return false;
+ }
+ return true;
+}
+
+// Parse a GeneralSubtrees |value| and store the contents in |subtrees|.
+// NOTE: |subtrees| will be modified regardless of the return.
+WARN_UNUSED_RESULT bool ParseGeneralSubtrees(
eroman 2015/08/26 19:56:43 nit: Why WARN_UNUSED_RESULT here but not for the o
mattm 2015/08/29 01:37:18 Done.
+ const der::Input& value,
+ NameConstraints::GeneralNames* subtrees,
eroman 2015/08/26 19:56:43 nit: Should the output parameter be last?
mattm 2015/08/29 01:37:18 Done.
+ bool is_critical) {
+ // GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
+ //
+ // GeneralSubtree ::= SEQUENCE {
+ // base GeneralName,
+ // minimum [0] BaseDistance DEFAULT 0,
+ // maximum [1] BaseDistance OPTIONAL }
+ //
+ // BaseDistance ::= INTEGER (0..MAX)
+ der::Parser sequence_parser(value);
+ while (sequence_parser.HasMore()) {
+ der::Parser subtree_sequence;
+ if (!sequence_parser.ReadSequence(&subtree_sequence))
+ return false;
+
+ der::Input raw_general_name;
+ if (!subtree_sequence.ReadRawTLV(&raw_general_name))
+ return false;
+
+ if (!ParseGeneralName(raw_general_name, subtrees, is_critical
+ ? RECORD_UNSUPPORTED
+ : IGNORE_UNSUPPORTED))
+ return false;
+
+ // RFC 5280 section 4.2.1.10:
+ // Within this profile, the minimum and maximum fields are not used with any
+ // name forms, thus, the minimum MUST be zero, and maximum MUST be absent.
+ // However, if an application encounters a critical name constraints
+ // extension that specifies other values for minimum or maximum for a name
+ // form that appears in a subsequent certificate, the application MUST
+ // either process these fields or reject the certificate.
+
+ // TODO(mattm): Technically we don't need to fail here: rather we only need
+ // to fail if a name of this type actually appears in a subsequent cert and
+ // this extension was marked critical.
+ // TODO(mattm): should this allow for the case that minimum is present but
+ // zero? (0 is the default, so it should not be present in DER encoding..)
+ if (subtree_sequence.HasMore())
+ return false;
+ }
+ return true;
+}
+
+} // namespace
+
+NameConstraints::GeneralNames::GeneralNames()
+ : has_other_names(false),
eroman 2015/08/26 19:56:44 nit: These might be easier to write as member init
mattm 2015/08/29 01:37:17 Done.
+ has_rfc822_names(false),
+ has_x400_addresses(false),
+ has_edi_party_names(false),
+ has_uniform_resource_identifiers(false),
+ has_registered_ids(false) {}
+
+NameConstraints::GeneralNames::~GeneralNames() {}
+
+NameConstraints::~NameConstraints() {}
+
+bool NameConstraints::Parse(const der::Input& extension_value,
+ bool is_critical) {
+ der::Parser extension_parser(extension_value);
+ der::Parser sequence_parser;
+
+ // NameConstraints ::= SEQUENCE {
+ // permittedSubtrees [0] GeneralSubtrees OPTIONAL,
+ // excludedSubtrees [1] GeneralSubtrees OPTIONAL }
+ if (!extension_parser.ReadSequence(&sequence_parser))
+ return false;
+ if (extension_parser.HasMore())
+ return false;
+
+ bool had_permitted_subtrees = false;
+ der::Input permitted_subtrees_value;
+ if (!sequence_parser.ReadOptionalTag(der::ContextSpecificConstructed(0),
+ &permitted_subtrees_value,
+ &had_permitted_subtrees))
+ return false;
+ if (had_permitted_subtrees) {
+ if (!ParseGeneralSubtrees(permitted_subtrees_value, &permitted_subtrees_,
+ is_critical))
+ return false;
+ }
+
+ bool had_excluded_subtrees = false;
+ der::Input excluded_subtrees_value;
+ if (!sequence_parser.ReadOptionalTag(der::ContextSpecificConstructed(1),
+ &excluded_subtrees_value,
+ &had_excluded_subtrees))
+ return false;
+ if (had_excluded_subtrees) {
+ if (!ParseGeneralSubtrees(excluded_subtrees_value, &excluded_subtrees_,
eroman 2015/08/26 19:56:43 note: The assumption is that Parse() is only calle
mattm 2015/08/29 01:37:18 noted in Parse method comment.
+ is_critical))
+ return false;
+ }
+
+ // RFC 5280 section 4.2.1.10:
+ // Conforming CAs MUST NOT issue certificates where name constraints is an
+ // empty sequence. That is, either the permittedSubtrees field or the
+ // excludedSubtrees MUST be present.
+ if (!had_permitted_subtrees && !had_excluded_subtrees)
+ return false;
+
+ if (sequence_parser.HasMore())
+ return false;
+
+ return true;
+}
+
+bool NameConstraints::IsPermittedCert(const der::Input& subject_rdn_sequence,
+ const der::Input& subject_alt_name,
+ bool is_leaf_cert) const {
+ // Subject Alternative Name handling:
+ //
+ // RFC 5280 section 4.2.1.6:
+ // id-ce-subjectAltName OBJECT IDENTIFIER ::= { id-ce 17 }
+ //
+ // SubjectAltName ::= GeneralNames
+ //
+ // GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
+
+ GeneralNames san_names;
+ if (subject_alt_name.Length()) {
+ der::Parser subject_alt_name_parser(subject_alt_name);
+ der::Parser san_sequence_parser;
+ if (!subject_alt_name_parser.ReadSequence(&san_sequence_parser))
+ return false;
+ if (subject_alt_name_parser.HasMore())
+ return false;
+
+ while (san_sequence_parser.HasMore()) {
+ der::Input raw_general_name;
+ if (!san_sequence_parser.ReadRawTLV(&raw_general_name))
+ return false;
+
+ if (!ParseGeneralName(raw_general_name, &san_names, RECORD_UNSUPPORTED))
+ return false;
+ }
+
+ if (san_names.has_other_names && !IsPermittedOtherName())
eroman 2015/08/26 19:56:43 What is IsPermittedOtherName() testing here, that
mattm 2015/08/29 01:37:18 This is testing that if a SubjectAlternativeName c
+ return false;
+ if (san_names.has_rfc822_names && !IsPermittedRFC822Name())
+ return false;
+ if (san_names.has_x400_addresses && !IsPermittedX400Address())
+ return false;
+ if (san_names.has_edi_party_names && !IsPermittedEdiPartyName())
+ return false;
+ if (san_names.has_uniform_resource_identifiers && !IsPermittedURI())
+ return false;
+ if (san_names.has_registered_ids && !IsPermittedRegisteredId())
+ return false;
+
+ for (const auto& dns_name : san_names.dns_names) {
+ if (!IsPermittedDNSName(dns_name))
+ return false;
+ }
+
+ for (const auto& directory_name : san_names.directory_names) {
+ if (!IsPermittedDirectoryName(
+ der::Input(directory_name.data(), directory_name.size())))
+ return false;
+ }
+
+ for (const auto& ip_address : san_names.ip_addresses) {
+ if (!IsPermittedIP(ip_address))
+ return false;
+ }
+ }
+
+ // Subject handling:
+
+ // RFC 5280 section 4.2.1.10:
+ // Legacy implementations exist where an electronic mail address is embedded
+ // in the subject distinguished name in an attribute of type emailAddress
+ // (Section 4.1.2.6). When constraints are imposed on the rfc822Name name
+ // form, but the certificate does not include a subject alternative name, the
+ // rfc822Name constraint MUST be applied to the attribute of type emailAddress
+ // in the subject distinguished name.
+ if (!subject_alt_name.Length() && !IsPermittedRFC822Name() &&
+ NameContainsEmailAddress(subject_rdn_sequence))
+ return false;
+
+ // RFC 5280 does not specify checking name constraints against subject
+ // CommonName, but since certificate verification allows it, name constraints
+ // must check it similarly.
+ if (is_leaf_cert &&
+ (san_names.dns_names.empty() && san_names.ip_addresses.empty()) &&
+ (!permitted_subtrees_.dns_names.empty() ||
+ !excluded_subtrees_.dns_names.empty() ||
+ !permitted_subtrees_.ip_addresses.empty() ||
+ !excluded_subtrees_.ip_addresses.empty())) {
+ // Note that while the commonName is transcoded to UTF-8, no special
+ // handling is done of internationalized domain names. (If an
+ // internationalized hostname is specified in commonName, it must be in
+ // punycode form.)
+ std::string common_name =
+ GetNormalizedCommonNameFromName(subject_rdn_sequence);
+ // If commonName is not present, or is an unsupported type, or contains
+ // invalid data, fail out.
+ if (common_name.empty())
+ return false;
+ IPAddressNumber ip_number;
+ bool was_ip = ParseIPLiteralToNumber(common_name, &ip_number);
+ // For IP addresses, Chrome only allows IPv4 in commonName (see comment in
+ // X509Certificate::VerifyHostname), otherwise interpret as a dNSName.
+ if (was_ip && ip_number.size() == kIPv4AddressSize) {
+ if (!IsPermittedIP(ip_number))
+ return false;
+ } else {
+ if (!IsPermittedDNSName(common_name))
+ return false;
+ }
+ }
+
+ // RFC 5280 4.1.2.6:
+ // 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.
+ if (subject_alt_name.Length() && subject_rdn_sequence.Length() == 0)
+ return true;
+
+ return IsPermittedDirectoryName(subject_rdn_sequence);
+}
+
+bool NameConstraints::IsPermittedDNSName(const std::string& name) const {
+ if (permitted_subtrees_.dns_names.empty() &&
+ excluded_subtrees_.dns_names.empty())
+ return true;
+
+ for (const std::string& excluded_name : excluded_subtrees_.dns_names) {
+ if (DNSNameMatches(name, excluded_name, WILDCARD_PARTIAL_MATCH))
+ return false;
+ }
+ for (const std::string& permitted_name : permitted_subtrees_.dns_names) {
+ if (DNSNameMatches(name, permitted_name, WILDCARD_FULL_MATCH))
+ return true;
+ }
+
+ return false;
+}
+
+bool NameConstraints::IsPermittedDirectoryName(
+ const der::Input& name_rdn_sequence) const {
+ if (permitted_subtrees_.directory_names.empty() &&
+ excluded_subtrees_.directory_names.empty())
+ return true;
+
+ for (const auto& excluded_name : excluded_subtrees_.directory_names) {
+ if (VerifyNameInSubtree(
+ name_rdn_sequence,
+ der::Input(excluded_name.data(), excluded_name.size()))) {
+ return false;
+ }
+ }
+ for (const auto& permitted_name : permitted_subtrees_.directory_names) {
+ if (VerifyNameInSubtree(
+ name_rdn_sequence,
+ der::Input(permitted_name.data(), permitted_name.size()))) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool NameConstraints::IsPermittedIP(const IPAddressNumber& ip) const {
+ if (permitted_subtrees_.ip_addresses.empty() &&
+ excluded_subtrees_.ip_addresses.empty())
+ return true;
+
+ for (const auto& excluded_ip : excluded_subtrees_.ip_addresses) {
+ if (VerifyIPMatchesConstraint(ip, excluded_ip))
+ return false;
+ }
+ for (const auto& permitted_ip : permitted_subtrees_.ip_addresses) {
+ if (VerifyIPMatchesConstraint(ip, permitted_ip))
+ return true;
+ }
+
+ return false;
+}
+
+bool NameConstraints::IsPermittedOtherName() const {
+ return (!permitted_subtrees_.has_other_names &&
+ !excluded_subtrees_.has_other_names);
+}
+
+bool NameConstraints::IsPermittedRFC822Name() const {
+ return (!permitted_subtrees_.has_rfc822_names &&
+ !excluded_subtrees_.has_rfc822_names);
+}
+
+bool NameConstraints::IsPermittedX400Address() const {
+ return (!permitted_subtrees_.has_x400_addresses &&
+ !excluded_subtrees_.has_x400_addresses);
+}
+
+bool NameConstraints::IsPermittedEdiPartyName() const {
+ return (!permitted_subtrees_.has_edi_party_names &&
+ !excluded_subtrees_.has_edi_party_names);
+}
+
+bool NameConstraints::IsPermittedURI() const {
+ return (!permitted_subtrees_.has_uniform_resource_identifiers &&
+ !excluded_subtrees_.has_uniform_resource_identifiers);
+}
+
+bool NameConstraints::IsPermittedRegisteredId() const {
+ return (!permitted_subtrees_.has_registered_ids &&
+ !excluded_subtrees_.has_registered_ids);
+}
+
+} // namespace net

Powered by Google App Engine
This is Rietveld 408576698