blob: 68194e702efec8ef1db849e6d6857ea2877a8e41 [file] [log] [blame]
// 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/verify_name_match.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "net/cert/internal/cert_error_params.h"
#include "net/cert/internal/cert_errors.h"
#include "net/cert/internal/parse_name.h"
#include "net/der/input.h"
#include "net/der/parser.h"
#include "net/der/tag.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
namespace net {
DEFINE_CERT_ERROR_ID(kFailedConvertingAttributeValue,
"Failed converting AttributeValue to string");
DEFINE_CERT_ERROR_ID(kFailedNormalizingString, "Failed normalizing string");
namespace {
// RFC 5280 section A.1:
//
// pkcs-9 OBJECT IDENTIFIER ::=
// { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 9 }
//
// id-emailAddress AttributeType ::= { pkcs-9 1 }
//
// In dotted form: 1.2.840.113549.1.9.1
const uint8_t kOidEmailAddress[] = {0x2A, 0x86, 0x48, 0x86, 0xF7,
0x0D, 0x01, 0x09, 0x01};
// Types of character set checking that NormalizeDirectoryString can perform.
enum CharsetEnforcement {
NO_ENFORCEMENT,
ENFORCE_PRINTABLE_STRING,
ENFORCE_ASCII,
};
// Normalizes |output|, a UTF-8 encoded string, as if it contained
// only ASCII characters.
//
// This could be considered a partial subset of RFC 5280 rules, and
// is compatible with RFC 2459/3280.
//
// In particular, RFC 5280, Section 7.1 describes how UTF8String
// and PrintableString should be compared - using the LDAP StringPrep
// profile of RFC 4518, with case folding and whitespace compression.
// However, because it is optional for 2459/3280 implementations and because
// it's desirable to avoid the size cost of the StringPrep tables,
// this function treats |output| as if it was composed of ASCII.
//
// That is, rather than folding all whitespace characters, it only
// folds ' '. Rather than case folding using locale-aware handling,
// it only folds A-Z to a-z.
//
// This gives better results than outright rejecting (due to mismatched
// encodings), or from doing a strict binary comparison (the minimum
// required by RFC 3280), and is sufficient for those certificates
// publicly deployed.
//
// If |charset_enforcement| is not NO_ENFORCEMENT and |output| contains any
// characters not allowed in the specified charset, returns false.
//
// NOTE: |output| will be modified regardless of the return.
WARN_UNUSED_RESULT bool NormalizeDirectoryString(
CharsetEnforcement charset_enforcement,
std::string* output) {
// Normalized version will always be equal or shorter than input.
// Normalize in place and then truncate the output if necessary.
std::string::const_iterator read_iter = output->begin();
std::string::iterator write_iter = output->begin();
for (; read_iter != output->end() && *read_iter == ' '; ++read_iter) {
// Ignore leading whitespace.
}
for (; read_iter != output->end(); ++read_iter) {
const unsigned char c = *read_iter;
if (c == ' ') {
// If there are non-whitespace characters remaining in input, compress
// multiple whitespace chars to a single space, otherwise ignore trailing
// whitespace.
std::string::const_iterator next_iter = read_iter + 1;
if (next_iter != output->end() && *next_iter != ' ')
*(write_iter++) = ' ';
} else if (base::IsAsciiUpper(c)) {
// Fold case.
*(write_iter++) = c + ('a' - 'A');
} else {
// Note that these checks depend on the characters allowed by earlier
// conditions also being valid for the enforced charset.
switch (charset_enforcement) {
case ENFORCE_PRINTABLE_STRING:
// See NormalizePrintableStringValue comment for the acceptable list
// of characters.
if (!(base::IsAsciiLower(c) || (c >= '\'' && c <= ':') || c == '=' ||
c == '?'))
return false;
break;
case ENFORCE_ASCII:
if (c > 0x7F)
return false;
break;
case NO_ENFORCEMENT:
break;
}
*(write_iter++) = c;
}
}
if (write_iter != output->end())
output->erase(write_iter, output->end());
return true;
}
// Converts the value of X509NameAttribute |attribute| to UTF-8, normalizes it,
// and stores in |output|. The type of |attribute| must be one of the types for
// which IsNormalizableDirectoryString is true.
//
// If the value of |attribute| can be normalized, returns true and sets
// |output| to the case folded, normalized value. If the value of |attribute|
// is invalid, returns false.
// NOTE: |output| will be modified regardless of the return.
WARN_UNUSED_RESULT bool NormalizeValue(X509NameAttribute attribute,
std::string* output,
CertErrors* errors) {
DCHECK(errors);
if (!attribute.ValueAsStringUnsafe(output)) {
errors->AddError(kFailedConvertingAttributeValue,
CreateCertErrorParams1SizeT("tag", attribute.value_tag));
return false;
}
bool success = false;
switch (attribute.value_tag) {
case der::kPrintableString:
success = NormalizeDirectoryString(ENFORCE_PRINTABLE_STRING, output);
break;
case der::kBmpString:
case der::kUniversalString:
case der::kUtf8String:
success = NormalizeDirectoryString(NO_ENFORCEMENT, output);
break;
case der::kIA5String:
success = NormalizeDirectoryString(ENFORCE_ASCII, output);
break;
default:
NOTREACHED();
success = false;
break;
}
if (!success) {
errors->AddError(kFailedNormalizingString,
CreateCertErrorParams1SizeT("tag", attribute.value_tag));
}
return success;
}
// Returns true if |tag| is a string type that NormalizeValue can handle.
bool IsNormalizableDirectoryString(der::Tag tag) {
switch (tag) {
case der::kPrintableString:
case der::kUtf8String:
// RFC 5280 only requires handling IA5String for comparing domainComponent
// values, but handling it here avoids the need to special case anything.
case der::kIA5String:
case der::kUniversalString:
case der::kBmpString:
return true;
// TeletexString isn't normalized. Section 8 of RFC 5280 briefly
// describes the historical confusion between treating TeletexString
// as Latin1String vs T.61, and there are even incompatibilities within
// T.61 implementations. As this time is virtually unused, simply
// treat it with a binary comparison, as permitted by RFC 3280/5280.
default:
return false;
}
}
// Returns true if the value of X509NameAttribute |a| matches |b|.
bool VerifyValueMatch(X509NameAttribute a, X509NameAttribute b) {
if (IsNormalizableDirectoryString(a.value_tag) &&
IsNormalizableDirectoryString(b.value_tag)) {
std::string a_normalized, b_normalized;
// TODO(eroman): Plumb this down.
CertErrors unused_errors;
if (!NormalizeValue(a, &a_normalized, &unused_errors) ||
!NormalizeValue(b, &b_normalized, &unused_errors))
return false;
return a_normalized == b_normalized;
}
// Attributes encoded with different types may be assumed to be unequal.
if (a.value_tag != b.value_tag)
return false;
// All other types use binary comparison.
return a.value == b.value;
}
// Verifies that |a_parser| and |b_parser| are the same length and that every
// AttributeTypeAndValue in |a_parser| has a matching AttributeTypeAndValue in
// |b_parser|.
bool VerifyRdnMatch(der::Parser* a_parser, der::Parser* b_parser) {
RelativeDistinguishedName a_type_and_values, b_type_and_values;
if (!ReadRdn(a_parser, &a_type_and_values) ||
!ReadRdn(b_parser, &b_type_and_values))
return false;
// RFC 5280 section 7.1:
// Two relative distinguished names RDN1 and RDN2 match if they have the same
// number of naming attributes and for each naming attribute in RDN1 there is
// a matching naming attribute in RDN2.
if (a_type_and_values.size() != b_type_and_values.size())
return false;
// The ordering of elements may differ due to denormalized values sorting
// differently in the DER encoding. Since the number of elements should be
// small, a naive linear search for each element should be fine. (Hostile
// certificates already have ways to provoke pathological behavior.)
for (const auto& a : a_type_and_values) {
auto b_iter = b_type_and_values.begin();
for (; b_iter != b_type_and_values.end(); ++b_iter) {
const auto& b = *b_iter;
if (a.type == b.type && VerifyValueMatch(a, b)) {
break;
}
}
if (b_iter == b_type_and_values.end())
return false;
// Remove the matched element from b_type_and_values to ensure duplicate
// elements in a_type_and_values can't match the same element in
// b_type_and_values multiple times.
b_type_and_values.erase(b_iter);
}
// Every element in |a_type_and_values| had a matching element in
// |b_type_and_values|.
return true;
}
enum NameMatchType {
EXACT_MATCH,
SUBTREE_MATCH,
};
// Verify that |a| matches |b|. If |match_type| is EXACT_MATCH, returns true if
// they are an exact match as defined by RFC 5280 7.1. If |match_type| is
// SUBTREE_MATCH, returns true if |a| is within the subtree defined by |b| as
// defined by RFC 5280 7.1.
//
// |a| and |b| are ASN.1 RDNSequence values (not including the Sequence tag),
// defined in RFC 5280 section 4.1.2.4:
//
// Name ::= CHOICE { -- only one possibility for now --
// rdnSequence RDNSequence }
//
// RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
//
// RelativeDistinguishedName ::=
// SET SIZE (1..MAX) OF AttributeTypeAndValue
bool VerifyNameMatchInternal(const der::Input& a,
const der::Input& b,
NameMatchType match_type) {
// Empty Names are allowed. RFC 5280 section 4.1.2.4 requires "The issuer
// field MUST contain a non-empty distinguished name (DN)", while section
// 4.1.2.6 allows for the Subject to be empty in certain cases. The caller is
// assumed to have verified those conditions.
// RFC 5280 section 7.1:
// Two distinguished names DN1 and DN2 match if they have the same number of
// RDNs, for each RDN in DN1 there is a matching RDN in DN2, and the matching
// RDNs appear in the same order in both DNs.
// As an optimization, first just compare the number of RDNs:
der::Parser a_rdn_sequence_counter(a);
der::Parser b_rdn_sequence_counter(b);
while (a_rdn_sequence_counter.HasMore() && b_rdn_sequence_counter.HasMore()) {
if (!a_rdn_sequence_counter.SkipTag(der::kSet) ||
!b_rdn_sequence_counter.SkipTag(der::kSet)) {
return false;
}
}
// If doing exact match and either of the sequences has more elements than the
// other, not a match. If doing a subtree match, the first Name may have more
// RDNs than the second.
if (b_rdn_sequence_counter.HasMore())
return false;
if (match_type == EXACT_MATCH && a_rdn_sequence_counter.HasMore())
return false;
// Verify that RDNs in |a| and |b| match.
der::Parser a_rdn_sequence(a);
der::Parser b_rdn_sequence(b);
while (a_rdn_sequence.HasMore() && b_rdn_sequence.HasMore()) {
der::Parser a_rdn, b_rdn;
if (!a_rdn_sequence.ReadConstructed(der::kSet, &a_rdn) ||
!b_rdn_sequence.ReadConstructed(der::kSet, &b_rdn)) {
return false;
}
if (!VerifyRdnMatch(&a_rdn, &b_rdn))
return false;
}
return true;
}
} // namespace
bool NormalizeName(const der::Input& name_rdn_sequence,
std::string* normalized_rdn_sequence,
CertErrors* errors) {
DCHECK(errors);
// RFC 5280 section 4.1.2.4
// RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
der::Parser rdn_sequence_parser(name_rdn_sequence);
bssl::ScopedCBB cbb;
if (!CBB_init(cbb.get(), 0))
return false;
while (rdn_sequence_parser.HasMore()) {
// RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
der::Parser rdn_parser;
if (!rdn_sequence_parser.ReadConstructed(der::kSet, &rdn_parser))
return false;
RelativeDistinguishedName type_and_values;
if (!ReadRdn(&rdn_parser, &type_and_values))
return false;
CBB rdn_cbb;
if (!CBB_add_asn1(cbb.get(), &rdn_cbb, CBS_ASN1_SET))
return false;
for (const auto& type_and_value : type_and_values) {
// AttributeTypeAndValue ::= SEQUENCE {
// type AttributeType,
// value AttributeValue }
CBB attribute_type_and_value_cbb, type_cbb, value_cbb;
if (!CBB_add_asn1(&rdn_cbb, &attribute_type_and_value_cbb,
CBS_ASN1_SEQUENCE)) {
return false;
}
// AttributeType ::= OBJECT IDENTIFIER
if (!CBB_add_asn1(&attribute_type_and_value_cbb, &type_cbb,
CBS_ASN1_OBJECT) ||
!CBB_add_bytes(&type_cbb, type_and_value.type.UnsafeData(),
type_and_value.type.Length())) {
return false;
}
// AttributeValue ::= ANY -- DEFINED BY AttributeType
if (IsNormalizableDirectoryString(type_and_value.value_tag)) {
std::string normalized_value;
if (!NormalizeValue(type_and_value, &normalized_value, errors))
return false;
if (!CBB_add_asn1(&attribute_type_and_value_cbb, &value_cbb,
CBS_ASN1_UTF8STRING) ||
!CBB_add_bytes(&value_cbb, reinterpret_cast<const uint8_t*>(
normalized_value.data()),
normalized_value.size()))
return false;
} else {
if (!CBB_add_asn1(&attribute_type_and_value_cbb, &value_cbb,
type_and_value.value_tag) ||
!CBB_add_bytes(&value_cbb, type_and_value.value.UnsafeData(),
type_and_value.value.Length()))
return false;
}
if (!CBB_flush(&rdn_cbb))
return false;
}
// Ensure the encoded AttributeTypeAndValue values in the SET OF are sorted.
if (!CBB_flush_asn1_set_of(&rdn_cbb) || !CBB_flush(cbb.get()))
return false;
}
normalized_rdn_sequence->assign(CBB_data(cbb.get()),
CBB_data(cbb.get()) + CBB_len(cbb.get()));
return true;
}
bool VerifyNameMatch(const der::Input& a_rdn_sequence,
const der::Input& b_rdn_sequence) {
return VerifyNameMatchInternal(a_rdn_sequence, b_rdn_sequence, EXACT_MATCH);
}
bool VerifyNameInSubtree(const der::Input& name_rdn_sequence,
const der::Input& parent_rdn_sequence) {
return VerifyNameMatchInternal(name_rdn_sequence, parent_rdn_sequence,
SUBTREE_MATCH);
}
bool NameContainsEmailAddress(const der::Input& name_rdn_sequence,
bool* contained_email_address) {
der::Parser rdn_sequence_parser(name_rdn_sequence);
while (rdn_sequence_parser.HasMore()) {
der::Parser rdn_parser;
if (!rdn_sequence_parser.ReadConstructed(der::kSet, &rdn_parser))
return false;
RelativeDistinguishedName type_and_values;
if (!ReadRdn(&rdn_parser, &type_and_values))
return false;
for (const auto& type_and_value : type_and_values) {
if (type_and_value.type == der::Input(kOidEmailAddress)) {
*contained_email_address = true;
return true;
}
}
}
*contained_email_address = false;
return true;
}
} // namespace net