// Copyright 2015 The Chromium Authors // // 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 // // https://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 "verify_name_match.h" #include #include #include "cert_error_params.h" #include "cert_errors.h" #include "input.h" #include "parse_name.h" #include "parser.h" BSSL_NAMESPACE_BEGIN DEFINE_CERT_ERROR_ID(kFailedConvertingAttributeValue, "Failed converting AttributeValue to string"); DEFINE_CERT_ERROR_ID(kFailedNormalizingString, "Failed normalizing string"); namespace { // 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. [[nodiscard]] 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 (c >= 'A' && c <= 'Z') { // 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 (!((c >= 'a' && c <= 'z') || (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. [[nodiscard]] bool NormalizeValue(X509NameAttribute attribute, std::string *output, CertErrors *errors) { BSSL_CHECK(errors); if (!attribute.ValueAsStringUnsafe(output)) { errors->AddError(kFailedConvertingAttributeValue, CreateCertErrorParams1SizeT("tag", attribute.value_tag)); return false; } bool success = false; switch (attribute.value_tag) { case CBS_ASN1_PRINTABLESTRING: success = NormalizeDirectoryString(ENFORCE_PRINTABLE_STRING, output); break; case CBS_ASN1_BMPSTRING: case CBS_ASN1_UNIVERSALSTRING: case CBS_ASN1_UTF8STRING: success = NormalizeDirectoryString(NO_ENFORCEMENT, output); break; case CBS_ASN1_IA5STRING: 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(CBS_ASN1_TAG tag) { switch (tag) { case CBS_ASN1_PRINTABLESTRING: case CBS_ASN1_UTF8STRING: // RFC 5280 only requires handling IA5String for comparing domainComponent // values, but handling it here avoids the need to special case anything. case CBS_ASN1_IA5STRING: case CBS_ASN1_UNIVERSALSTRING: case CBS_ASN1_BMPSTRING: 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(der::Input a, 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(CBS_ASN1_SET) || !b_rdn_sequence_counter.SkipTag(CBS_ASN1_SET)) { 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(CBS_ASN1_SET, &a_rdn) || !b_rdn_sequence.ReadConstructed(CBS_ASN1_SET, &b_rdn)) { return false; } if (!VerifyRdnMatch(&a_rdn, &b_rdn)) { return false; } } return true; } } // namespace bool NormalizeName(der::Input name_rdn_sequence, std::string *normalized_rdn_sequence, CertErrors *errors) { BSSL_CHECK(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(CBS_ASN1_SET, &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.data(), type_and_value.type.size())) { 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(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.data(), type_and_value.value.size())) { 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(der::Input a_rdn_sequence, der::Input b_rdn_sequence) { return VerifyNameMatchInternal(a_rdn_sequence, b_rdn_sequence, EXACT_MATCH); } bool VerifyNameInSubtree(der::Input name_rdn_sequence, der::Input parent_rdn_sequence) { return VerifyNameMatchInternal(name_rdn_sequence, parent_rdn_sequence, SUBTREE_MATCH); } bool FindEmailAddressesInName( der::Input name_rdn_sequence, std::vector *contained_email_addresses) { contained_email_addresses->clear(); der::Parser rdn_sequence_parser(name_rdn_sequence); while (rdn_sequence_parser.HasMore()) { der::Parser rdn_parser; if (!rdn_sequence_parser.ReadConstructed(CBS_ASN1_SET, &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(kTypeEmailAddressOid)) { std::string email_address; if (!type_and_value.ValueAsString(&email_address)) { return false; } contained_email_addresses->push_back(std::move(email_address)); } } } return true; } BSSL_NAMESPACE_END