| /* -*- 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 implements RFC6125-ish name matching, RFC5280-ish name constraint |
| // checking, and related things. |
| // |
| // In this code, identifiers are classified as either "presented" or |
| // "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. When checking name |
| // constraints, the reference identifier is the entire encoded name constraint |
| // extension value. |
| |
| #include <algorithm> |
| |
| #include "mozpkix/pkixcheck.h" |
| #include "mozpkix/pkixutil.h" |
| |
| namespace mozilla { namespace pkix { |
| |
| namespace { |
| |
| // GeneralName ::= CHOICE { |
| // otherName [0] OtherName, |
| // rfc822Name [1] IA5String, |
| // dNSName [2] IA5String, |
| // x400Address [3] ORAddress, |
| // directoryName [4] Name, |
| // ediPartyName [5] EDIPartyName, |
| // uniformResourceIdentifier [6] IA5String, |
| // iPAddress [7] OCTET STRING, |
| // registeredID [8] OBJECT IDENTIFIER } |
| enum class GeneralNameType : uint8_t |
| { |
| // Note that these values are NOT contiguous. Some values have the |
| // der::CONSTRUCTED bit set while others do not. |
| // (The der::CONSTRUCTED bit is for types where the value is a SEQUENCE.) |
| otherName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 0, |
| rfc822Name = der::CONTEXT_SPECIFIC | 1, |
| dNSName = der::CONTEXT_SPECIFIC | 2, |
| x400Address = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 3, |
| directoryName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 4, |
| ediPartyName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 5, |
| uniformResourceIdentifier = der::CONTEXT_SPECIFIC | 6, |
| iPAddress = der::CONTEXT_SPECIFIC | 7, |
| registeredID = der::CONTEXT_SPECIFIC | 8, |
| // nameConstraints is a pseudo-GeneralName used to signify that a |
| // reference ID is actually the entire name constraint extension. |
| nameConstraints = 0xff |
| }; |
| |
| inline Result |
| ReadGeneralName(Reader& reader, |
| /*out*/ GeneralNameType& generalNameType, |
| /*out*/ Input& value) |
| { |
| uint8_t tag; |
| Result rv = der::ReadTagAndGetValue(reader, tag, value); |
| if (rv != Success) { |
| return rv; |
| } |
| switch (tag) { |
| case static_cast<uint8_t>(GeneralNameType::otherName): |
| generalNameType = GeneralNameType::otherName; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::rfc822Name): |
| generalNameType = GeneralNameType::rfc822Name; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::dNSName): |
| generalNameType = GeneralNameType::dNSName; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::x400Address): |
| generalNameType = GeneralNameType::x400Address; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::directoryName): |
| generalNameType = GeneralNameType::directoryName; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::ediPartyName): |
| generalNameType = GeneralNameType::ediPartyName; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::uniformResourceIdentifier): |
| generalNameType = GeneralNameType::uniformResourceIdentifier; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::iPAddress): |
| generalNameType = GeneralNameType::iPAddress; |
| break; |
| case static_cast<uint8_t>(GeneralNameType::registeredID): |
| generalNameType = GeneralNameType::registeredID; |
| break; |
| default: |
| return Result::ERROR_BAD_DER; |
| } |
| return Success; |
| } |
| |
| enum class MatchResult |
| { |
| NoNamesOfGivenType = 0, |
| Mismatch = 1, |
| Match = 2 |
| }; |
| |
| Result SearchNames(const Input* subjectAltName, Input subject, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| FallBackToSearchWithinSubject fallBackToCommonName, |
| /*out*/ MatchResult& match); |
| Result SearchWithinRDN(Reader& rdn, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| FallBackToSearchWithinSubject fallBackToEmailAddress, |
| FallBackToSearchWithinSubject fallBackToCommonName, |
| /*in/out*/ MatchResult& match); |
| Result MatchAVA(Input type, |
| uint8_t valueEncodingTag, |
| Input presentedID, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| FallBackToSearchWithinSubject fallBackToEmailAddress, |
| FallBackToSearchWithinSubject fallBackToCommonName, |
| /*in/out*/ MatchResult& match); |
| Result ReadAVA(Reader& rdn, |
| /*out*/ Input& type, |
| /*out*/ uint8_t& valueTag, |
| /*out*/ Input& value); |
| void MatchSubjectPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
| Input presentedID, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| /*in/out*/ MatchResult& match); |
| |
| Result MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
| Input presentedID, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| /*in/out*/ MatchResult& matchResult); |
| Result CheckPresentedIDConformsToConstraints(GeneralNameType referenceIDType, |
| Input presentedID, |
| Input nameConstraints); |
| |
| uint8_t LocaleInsensitveToLower(uint8_t a); |
| bool StartsWithIDNALabel(Input id); |
| |
| enum class IDRole |
| { |
| ReferenceID = 0, |
| PresentedID = 1, |
| NameConstraint = 2, |
| }; |
| |
| enum class AllowWildcards { No = 0, Yes = 1 }; |
| |
| // DNSName constraints implicitly allow subdomain matching when there is no |
| // leading dot ("foo.example.com" matches a constraint of "example.com"), but |
| // RFC822Name constraints only allow subdomain matching when there is a leading |
| // dot ("foo.example.com" does not match "example.com" but does match |
| // ".example.com"). |
| enum class AllowDotlessSubdomainMatches { No = 0, Yes = 1 }; |
| |
| bool IsValidDNSID(Input hostname, IDRole idRole, |
| AllowWildcards allowWildcards); |
| |
| Result MatchPresentedDNSIDWithReferenceDNSID( |
| Input presentedDNSID, |
| AllowWildcards allowWildcards, |
| AllowDotlessSubdomainMatches allowDotlessSubdomainMatches, |
| IDRole referenceDNSIDRole, |
| Input referenceDNSID, |
| /*out*/ bool& matches); |
| |
| Result MatchPresentedRFC822NameWithReferenceRFC822Name( |
| Input presentedRFC822Name, IDRole referenceRFC822NameRole, |
| Input referenceRFC822Name, /*out*/ bool& matches); |
| |
| } // namespace |
| |
| bool IsValidReferenceDNSID(Input hostname); |
| bool IsValidPresentedDNSID(Input hostname); |
| bool ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]); |
| bool ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]); |
| |
| // This is used by the pkixnames_tests.cpp tests. |
| Result |
| MatchPresentedDNSIDWithReferenceDNSID(Input presentedDNSID, |
| Input referenceDNSID, |
| /*out*/ bool& matches) |
| { |
| return MatchPresentedDNSIDWithReferenceDNSID( |
| presentedDNSID, AllowWildcards::Yes, |
| AllowDotlessSubdomainMatches::Yes, IDRole::ReferenceID, |
| referenceDNSID, matches); |
| } |
| |
| // Verify that the given end-entity cert, which is assumed to have been already |
| // validated with BuildCertChain, is valid for the given hostname. hostname is |
| // assumed to be a string representation of an IPv4 address, an IPv6 addresss, |
| // or a normalized ASCII (possibly punycode) DNS name. |
| Result |
| CheckCertHostname(Input endEntityCertDER, Input hostname, |
| NameMatchingPolicy& nameMatchingPolicy) |
| { |
| BackCert cert(endEntityCertDER, EndEntityOrCA::MustBeEndEntity, nullptr); |
| Result rv = cert.Init(); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| Time notBefore(Time::uninitialized); |
| rv = ParseValidity(cert.GetValidity(), ¬Before); |
| if (rv != Success) { |
| return rv; |
| } |
| FallBackToSearchWithinSubject fallBackToSearchWithinSubject; |
| rv = nameMatchingPolicy.FallBackToCommonName(notBefore, |
| fallBackToSearchWithinSubject); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| const Input* subjectAltName(cert.GetSubjectAltName()); |
| Input subject(cert.GetSubject()); |
| |
| // For backward compatibility with legacy certificates, we may fall back to |
| // searching for a name match in the subject common name for DNS names and |
| // IPv4 addresses. We don't do so for IPv6 addresses because we do not think |
| // there are many certificates that would need such fallback, and because |
| // comparisons of string representations of IPv6 addresses are particularly |
| // error prone due to the syntactic flexibility that IPv6 addresses have. |
| // |
| // IPv4 and IPv6 addresses are represented using the same type of GeneralName |
| // (iPAddress); they are differentiated by the lengths of the values. |
| MatchResult match; |
| uint8_t ipv6[16]; |
| uint8_t ipv4[4]; |
| if (IsValidReferenceDNSID(hostname)) { |
| rv = SearchNames(subjectAltName, subject, GeneralNameType::dNSName, |
| hostname, fallBackToSearchWithinSubject, match); |
| } else if (ParseIPv6Address(hostname, ipv6)) { |
| rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress, |
| Input(ipv6), FallBackToSearchWithinSubject::No, match); |
| } else if (ParseIPv4Address(hostname, ipv4)) { |
| rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress, |
| Input(ipv4), fallBackToSearchWithinSubject, match); |
| } else { |
| return Result::ERROR_BAD_CERT_DOMAIN; |
| } |
| if (rv != Success) { |
| return rv; |
| } |
| switch (match) { |
| case MatchResult::NoNamesOfGivenType: // fall through |
| case MatchResult::Mismatch: |
| return Result::ERROR_BAD_CERT_DOMAIN; |
| case MatchResult::Match: |
| return Success; |
| MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM |
| } |
| } |
| |
| // 4.2.1.10. Name Constraints |
| Result |
| CheckNameConstraints(Input encodedNameConstraints, |
| const BackCert& firstChild, |
| KeyPurposeId requiredEKUIfPresent) |
| { |
| for (const BackCert* child = &firstChild; child; child = child->childCert) { |
| FallBackToSearchWithinSubject fallBackToCommonName |
| = (child->endEntityOrCA == EndEntityOrCA::MustBeEndEntity && |
| requiredEKUIfPresent == KeyPurposeId::id_kp_serverAuth) |
| ? FallBackToSearchWithinSubject::Yes |
| : FallBackToSearchWithinSubject::No; |
| |
| MatchResult match; |
| Result rv = SearchNames(child->GetSubjectAltName(), child->GetSubject(), |
| GeneralNameType::nameConstraints, |
| encodedNameConstraints, fallBackToCommonName, |
| match); |
| if (rv != Success) { |
| return rv; |
| } |
| switch (match) { |
| case MatchResult::Match: // fall through |
| case MatchResult::NoNamesOfGivenType: |
| break; |
| case MatchResult::Mismatch: |
| return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
| } |
| } |
| |
| return Success; |
| } |
| |
| namespace { |
| |
| // SearchNames is used by CheckCertHostname and CheckNameConstraints. |
| // |
| // When called during name constraint checking, referenceIDType is |
| // GeneralNameType::nameConstraints and referenceID is the entire encoded name |
| // constraints extension value. |
| // |
| // The main benefit of using the exact same code paths for both is that we |
| // ensure consistency between name validation and name constraint enforcement |
| // regarding thing like "Which CN attributes should be considered as potential |
| // CN-IDs" and "Which character sets are acceptable for CN-IDs?" If the name |
| // matching and the name constraint enforcement logic were out of sync on these |
| // issues (e.g. if name matching were to consider all subject CN attributes, |
| // but name constraints were only enforced on the most specific subject CN), |
| // trivial name constraint bypasses could result. |
| |
| Result |
| SearchNames(/*optional*/ const Input* subjectAltName, |
| Input subject, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| FallBackToSearchWithinSubject fallBackToCommonName, |
| /*out*/ MatchResult& match) |
| { |
| Result rv; |
| |
| match = MatchResult::NoNamesOfGivenType; |
| |
| // RFC 6125 says "A client MUST NOT seek a match for a reference identifier |
| // of CN-ID if the presented identifiers include a DNS-ID, SRV-ID, URI-ID, or |
| // any application-specific identifier types supported by the client." |
| // Accordingly, we only consider CN-IDs if there are no DNS-IDs in the |
| // subjectAltName. |
| // |
| // RFC 6125 says that IP addresses are out of scope, but for backward |
| // compatibility we accept them, by considering IP addresses to be an |
| // "application-specific identifier type supported by the client." |
| // |
| // TODO(bug XXXXXXX): Consider strengthening this check to "A client MUST NOT |
| // seek a match for a reference identifier of CN-ID if the certificate |
| // contains a subjectAltName extension." |
| // |
| // TODO(bug XXXXXXX): Consider dropping support for IP addresses as |
| // identifiers completely. |
| |
| if (subjectAltName) { |
| Reader altNames; |
| rv = der::ExpectTagAndGetValueAtEnd(*subjectAltName, der::SEQUENCE, |
| altNames); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| // According to RFC 5280, "If the subjectAltName extension is present, the |
| // sequence MUST contain at least one entry." For compatibility reasons, we |
| // do not enforce this. See bug 1143085. |
| while (!altNames.AtEnd()) { |
| GeneralNameType presentedIDType; |
| Input presentedID; |
| rv = ReadGeneralName(altNames, presentedIDType, presentedID); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| rv = MatchPresentedIDWithReferenceID(presentedIDType, presentedID, |
| referenceIDType, referenceID, |
| match); |
| if (rv != Success) { |
| return rv; |
| } |
| if (referenceIDType != GeneralNameType::nameConstraints && |
| match == MatchResult::Match) { |
| return Success; |
| } |
| if (presentedIDType == GeneralNameType::dNSName || |
| presentedIDType == GeneralNameType::iPAddress) { |
| fallBackToCommonName = FallBackToSearchWithinSubject::No; |
| } |
| } |
| } |
| |
| if (referenceIDType == GeneralNameType::nameConstraints) { |
| rv = CheckPresentedIDConformsToConstraints(GeneralNameType::directoryName, |
| subject, referenceID); |
| if (rv != Success) { |
| return rv; |
| } |
| } |
| |
| FallBackToSearchWithinSubject fallBackToEmailAddress; |
| if (!subjectAltName && |
| (referenceIDType == GeneralNameType::rfc822Name || |
| referenceIDType == GeneralNameType::nameConstraints)) { |
| fallBackToEmailAddress = FallBackToSearchWithinSubject::Yes; |
| } else { |
| fallBackToEmailAddress = FallBackToSearchWithinSubject::No; |
| } |
| |
| // Short-circuit the parsing of the subject name if we're not going to match |
| // any names in it |
| if (fallBackToEmailAddress == FallBackToSearchWithinSubject::No && |
| fallBackToCommonName == FallBackToSearchWithinSubject::No) { |
| return Success; |
| } |
| |
| // Attempt to match the reference ID against the CN-ID, which we consider to |
| // be the most-specific CN AVA in the subject field. |
| // |
| // https://tools.ietf.org/html/rfc6125#section-2.3.1 says: |
| // |
| // To reduce confusion, in this specification we avoid such terms and |
| // instead use the terms provided under Section 1.8; in particular, we |
| // do not use the term "(most specific) Common Name field in the subject |
| // field" from [HTTP-TLS] and instead state that a CN-ID is a Relative |
| // Distinguished Name (RDN) in the certificate subject containing one |
| // and only one attribute-type-and-value pair of type Common Name (thus |
| // removing the possibility that an RDN might contain multiple AVAs |
| // (Attribute Value Assertions) of type CN, one of which could be |
| // considered "most specific"). |
| // |
| // https://tools.ietf.org/html/rfc6125#section-7.4 says: |
| // |
| // [...] Although it would be preferable to |
| // forbid multiple CN-IDs entirely, there are several reasons at this |
| // time why this specification states that they SHOULD NOT (instead of |
| // MUST NOT) be included [...] |
| // |
| // Consequently, it is unclear what to do when there are multiple CNs in the |
| // subject, regardless of whether there "SHOULD NOT" be. |
| // |
| // NSS's CERT_VerifyCertName mostly follows RFC2818 in this instance, which |
| // says: |
| // |
| // If a subjectAltName extension of type dNSName is present, that MUST |
| // be used as the identity. Otherwise, the (most specific) Common Name |
| // field in the Subject field of the certificate MUST be used. |
| // |
| // [...] |
| // |
| // In some cases, the URI is specified as an IP address rather than a |
| // hostname. In this case, the iPAddress subjectAltName must be present |
| // in the certificate and must exactly match the IP in the URI. |
| // |
| // (The main difference from RFC2818 is that NSS's CERT_VerifyCertName also |
| // matches IP addresses in the most-specific CN.) |
| // |
| // NSS's CERT_VerifyCertName finds the most specific CN via |
| // CERT_GetCommoName, which uses CERT_GetLastNameElement. Note that many |
| // NSS-based applications, including Gecko, also use CERT_GetCommonName. It |
| // is likely that other, non-NSS-based, applications also expect only the |
| // most specific CN to be matched against the reference ID. |
| // |
| // "A Layman's Guide to a Subset of ASN.1, BER, and DER" and other sources |
| // agree that an RDNSequence is ordered from most significant (least |
| // specific) to least significant (most specific), as do other references. |
| // |
| // However, Chromium appears to use the least-specific (first) CN instead of |
| // the most-specific; see https://crbug.com/366957. Also, MSIE and some other |
| // popular implementations apparently attempt to match the reference ID |
| // against any/all CNs in the subject. Since we're trying to phase out the |
| // use of CN-IDs, we intentionally avoid trying to match MSIE's more liberal |
| // behavior. |
| |
| // Name ::= CHOICE { -- only one possibility for now -- |
| // rdnSequence RDNSequence } |
| // |
| // RDNSequence ::= SEQUENCE OF RelativeDistinguishedName |
| // |
| // RelativeDistinguishedName ::= |
| // SET SIZE (1..MAX) OF AttributeTypeAndValue |
| Reader subjectReader(subject); |
| return der::NestedOf(subjectReader, der::SEQUENCE, der::SET, |
| der::EmptyAllowed::Yes, [&](Reader& r) { |
| return SearchWithinRDN(r, referenceIDType, referenceID, |
| fallBackToEmailAddress, fallBackToCommonName, match); |
| }); |
| } |
| |
| // RelativeDistinguishedName ::= |
| // SET SIZE (1..MAX) OF AttributeTypeAndValue |
| // |
| // AttributeTypeAndValue ::= SEQUENCE { |
| // type AttributeType, |
| // value AttributeValue } |
| Result |
| SearchWithinRDN(Reader& rdn, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| FallBackToSearchWithinSubject fallBackToEmailAddress, |
| FallBackToSearchWithinSubject fallBackToCommonName, |
| /*in/out*/ MatchResult& match) |
| { |
| do { |
| Input type; |
| uint8_t valueTag; |
| Input value; |
| Result rv = ReadAVA(rdn, type, valueTag, value); |
| if (rv != Success) { |
| return rv; |
| } |
| rv = MatchAVA(type, valueTag, value, referenceIDType, referenceID, |
| fallBackToEmailAddress, fallBackToCommonName, match); |
| if (rv != Success) { |
| return rv; |
| } |
| } while (!rdn.AtEnd()); |
| |
| return Success; |
| } |
| |
| // AttributeTypeAndValue ::= SEQUENCE { |
| // type AttributeType, |
| // value AttributeValue } |
| // |
| // AttributeType ::= OBJECT IDENTIFIER |
| // |
| // AttributeValue ::= ANY -- DEFINED BY AttributeType |
| // |
| // DirectoryString ::= CHOICE { |
| // teletexString TeletexString (SIZE (1..MAX)), |
| // printableString PrintableString (SIZE (1..MAX)), |
| // universalString UniversalString (SIZE (1..MAX)), |
| // utf8String UTF8String (SIZE (1..MAX)), |
| // bmpString BMPString (SIZE (1..MAX)) } |
| Result |
| MatchAVA(Input type, uint8_t valueEncodingTag, Input presentedID, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| FallBackToSearchWithinSubject fallBackToEmailAddress, |
| FallBackToSearchWithinSubject fallBackToCommonName, |
| /*in/out*/ MatchResult& match) |
| { |
| // Try to match the CN as a DNSName or an IPAddress. |
| // |
| // id-at-commonName AttributeType ::= { id-at 3 } |
| // |
| // -- Naming attributes of type X520CommonName: |
| // -- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name)) |
| // -- |
| // -- Expanded to avoid parameterized type: |
| // X520CommonName ::= CHOICE { |
| // teletexString TeletexString (SIZE (1..ub-common-name)), |
| // printableString PrintableString (SIZE (1..ub-common-name)), |
| // universalString UniversalString (SIZE (1..ub-common-name)), |
| // utf8String UTF8String (SIZE (1..ub-common-name)), |
| // bmpString BMPString (SIZE (1..ub-common-name)) } |
| // |
| // python DottedOIDToCode.py id-at-commonName 2.5.4.3 |
| static const uint8_t id_at_commonName[] = { |
| 0x55, 0x04, 0x03 |
| }; |
| if (fallBackToCommonName == FallBackToSearchWithinSubject::Yes && |
| InputsAreEqual(type, Input(id_at_commonName))) { |
| // We might have previously found a match. Now that we've found another CN, |
| // we no longer consider that previous match to be a match, so "forget" about |
| // it. |
| match = MatchResult::NoNamesOfGivenType; |
| |
| // PrintableString is a subset of ASCII that contains all the characters |
| // allowed in CN-IDs except '*'. Although '*' is illegal, there are many |
| // real-world certificates that are encoded this way, so we accept it. |
| // |
| // In the case of UTF8String, we rely on the fact that in UTF-8 the octets in |
| // a multi-byte encoding of a code point are always distinct from ASCII. Any |
| // non-ASCII byte in a UTF-8 string causes us to fail to match. We make no |
| // attempt to detect or report malformed UTF-8 (e.g. incomplete or overlong |
| // encodings of code points, or encodings of invalid code points). |
| // |
| // TeletexString is supported as long as it does not contain any escape |
| // sequences, which are not supported. We'll reject escape sequences as |
| // invalid characters in names, which means we only accept strings that are |
| // in the default character set, which is a superset of ASCII. Note that NSS |
| // actually treats TeletexString as ISO-8859-1. Many certificates that have |
| // wildcard CN-IDs (e.g. "*.example.com") use TeletexString because |
| // PrintableString is defined to not allow '*' and because, at one point in |
| // history, UTF8String was too new to use for compatibility reasons. |
| // |
| // UniversalString and BMPString are also deprecated, and they are a little |
| // harder to support because they are not single-byte ASCII superset |
| // encodings, so we don't bother. |
| if (valueEncodingTag != der::PrintableString && |
| valueEncodingTag != der::UTF8String && |
| valueEncodingTag != der::TeletexString) { |
| return Success; |
| } |
| |
| if (IsValidPresentedDNSID(presentedID)) { |
| MatchSubjectPresentedIDWithReferenceID(GeneralNameType::dNSName, |
| presentedID, referenceIDType, |
| referenceID, match); |
| } else { |
| // We don't match CN-IDs for IPv6 addresses. |
| // MatchSubjectPresentedIDWithReferenceID ensures that it won't match an |
| // IPv4 address with an IPv6 address, so we don't need to check that |
| // referenceID is an IPv4 address here. |
| uint8_t ipv4[4]; |
| if (ParseIPv4Address(presentedID, ipv4)) { |
| MatchSubjectPresentedIDWithReferenceID(GeneralNameType::iPAddress, |
| Input(ipv4), referenceIDType, |
| referenceID, match); |
| } |
| } |
| |
| // Regardless of whether there was a match, we keep going in case we find |
| // another CN later. If we do find another one, then this match/mismatch |
| // will be ignored, because we only care about the most specific CN. |
| |
| return Success; |
| } |
| |
| // Match an email address against an emailAddress attribute in the |
| // subject. |
| // |
| // id-emailAddress AttributeType ::= { pkcs-9 1 } |
| // |
| // EmailAddress ::= IA5String (SIZE (1..ub-emailaddress-length)) |
| // |
| // python DottedOIDToCode.py id-emailAddress 1.2.840.113549.1.9.1 |
| static const uint8_t id_emailAddress[] = { |
| 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01 |
| }; |
| if (fallBackToEmailAddress == FallBackToSearchWithinSubject::Yes && |
| InputsAreEqual(type, Input(id_emailAddress))) { |
| if (referenceIDType == GeneralNameType::rfc822Name && |
| match == MatchResult::Match) { |
| // We already found a match; we don't need to match another one |
| return Success; |
| } |
| if (valueEncodingTag != der::IA5String) { |
| return Result::ERROR_BAD_DER; |
| } |
| return MatchPresentedIDWithReferenceID(GeneralNameType::rfc822Name, |
| presentedID, referenceIDType, |
| referenceID, match); |
| } |
| |
| return Success; |
| } |
| |
| void |
| MatchSubjectPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
| Input presentedID, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| /*in/out*/ MatchResult& match) |
| { |
| Result rv = MatchPresentedIDWithReferenceID(presentedIDType, presentedID, |
| referenceIDType, referenceID, |
| match); |
| if (rv != Success) { |
| match = MatchResult::Mismatch; |
| } |
| } |
| |
| Result |
| MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
| Input presentedID, |
| GeneralNameType referenceIDType, |
| Input referenceID, |
| /*out*/ MatchResult& matchResult) |
| { |
| if (referenceIDType == GeneralNameType::nameConstraints) { |
| // matchResult is irrelevant when checking name constraints; only the |
| // pass/fail result of CheckPresentedIDConformsToConstraints matters. |
| return CheckPresentedIDConformsToConstraints(presentedIDType, presentedID, |
| referenceID); |
| } |
| |
| if (presentedIDType != referenceIDType) { |
| matchResult = MatchResult::Mismatch; |
| return Success; |
| } |
| |
| Result rv; |
| bool foundMatch; |
| |
| switch (referenceIDType) { |
| case GeneralNameType::dNSName: |
| rv = MatchPresentedDNSIDWithReferenceDNSID( |
| presentedID, AllowWildcards::Yes, |
| AllowDotlessSubdomainMatches::Yes, IDRole::ReferenceID, |
| referenceID, foundMatch); |
| break; |
| |
| case GeneralNameType::iPAddress: |
| foundMatch = InputsAreEqual(presentedID, referenceID); |
| rv = Success; |
| break; |
| |
| case GeneralNameType::rfc822Name: |
| rv = MatchPresentedRFC822NameWithReferenceRFC822Name( |
| presentedID, IDRole::ReferenceID, referenceID, foundMatch); |
| break; |
| |
| case GeneralNameType::directoryName: |
| // TODO: At some point, we may add APIs for matching DirectoryNames. |
| // fall through |
| |
| case GeneralNameType::otherName: // fall through |
| case GeneralNameType::x400Address: // fall through |
| case GeneralNameType::ediPartyName: // fall through |
| case GeneralNameType::uniformResourceIdentifier: // fall through |
| case GeneralNameType::registeredID: // fall through |
| case GeneralNameType::nameConstraints: |
| return NotReached("unexpected nameType for SearchType::Match", |
| Result::FATAL_ERROR_INVALID_ARGS); |
| |
| MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM |
| } |
| |
| if (rv != Success) { |
| return rv; |
| } |
| matchResult = foundMatch ? MatchResult::Match : MatchResult::Mismatch; |
| return Success; |
| } |
| |
| enum class NameConstraintsSubtrees : uint8_t |
| { |
| permittedSubtrees = der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 0, |
| excludedSubtrees = der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 1 |
| }; |
| |
| Result CheckPresentedIDConformsToNameConstraintsSubtrees( |
| GeneralNameType presentedIDType, |
| Input presentedID, |
| Reader& nameConstraints, |
| NameConstraintsSubtrees subtreesType); |
| Result MatchPresentedIPAddressWithConstraint(Input presentedID, |
| Input iPAddressConstraint, |
| /*out*/ bool& foundMatch); |
| Result MatchPresentedDirectoryNameWithConstraint( |
| NameConstraintsSubtrees subtreesType, Input presentedID, |
| Input directoryNameConstraint, /*out*/ bool& matches); |
| |
| Result |
| CheckPresentedIDConformsToConstraints( |
| GeneralNameType presentedIDType, |
| Input presentedID, |
| Input encodedNameConstraints) |
| { |
| // NameConstraints ::= SEQUENCE { |
| // permittedSubtrees [0] GeneralSubtrees OPTIONAL, |
| // excludedSubtrees [1] GeneralSubtrees OPTIONAL } |
| Reader nameConstraints; |
| Result rv = der::ExpectTagAndGetValueAtEnd(encodedNameConstraints, |
| der::SEQUENCE, nameConstraints); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| // RFC 5280 says "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 (nameConstraints.AtEnd()) { |
| return Result::ERROR_BAD_DER; |
| } |
| |
| rv = CheckPresentedIDConformsToNameConstraintsSubtrees( |
| presentedIDType, presentedID, nameConstraints, |
| NameConstraintsSubtrees::permittedSubtrees); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| rv = CheckPresentedIDConformsToNameConstraintsSubtrees( |
| presentedIDType, presentedID, nameConstraints, |
| NameConstraintsSubtrees::excludedSubtrees); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| return der::End(nameConstraints); |
| } |
| |
| Result |
| CheckPresentedIDConformsToNameConstraintsSubtrees( |
| GeneralNameType presentedIDType, |
| Input presentedID, |
| Reader& nameConstraints, |
| NameConstraintsSubtrees subtreesType) |
| { |
| if (!nameConstraints.Peek(static_cast<uint8_t>(subtreesType))) { |
| return Success; |
| } |
| |
| Reader subtrees; |
| Result rv = der::ExpectTagAndGetValue(nameConstraints, |
| static_cast<uint8_t>(subtreesType), |
| subtrees); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| bool hasPermittedSubtreesMatch = false; |
| bool hasPermittedSubtreesMismatch = false; |
| |
| // GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree |
| // |
| // do { ... } while(...) because subtrees isn't allowed to be empty. |
| do { |
| // GeneralSubtree ::= SEQUENCE { |
| // base GeneralName, |
| // minimum [0] BaseDistance DEFAULT 0, |
| // maximum [1] BaseDistance OPTIONAL } |
| Reader subtree; |
| rv = ExpectTagAndGetValue(subtrees, der::SEQUENCE, subtree); |
| if (rv != Success) { |
| return rv; |
| } |
| GeneralNameType nameConstraintType; |
| Input base; |
| rv = ReadGeneralName(subtree, nameConstraintType, base); |
| if (rv != Success) { |
| return rv; |
| } |
| // http://tools.ietf.org/html/rfc5280#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." |
| // |
| // Since the default value isn't allowed to be encoded according to the DER |
| // encoding rules for DEFAULT, this is equivalent to saying that neither |
| // minimum or maximum must be encoded. |
| rv = der::End(subtree); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| if (presentedIDType == nameConstraintType) { |
| bool matches; |
| |
| switch (presentedIDType) { |
| case GeneralNameType::dNSName: |
| rv = MatchPresentedDNSIDWithReferenceDNSID( |
| presentedID, AllowWildcards::Yes, |
| AllowDotlessSubdomainMatches::Yes, IDRole::NameConstraint, |
| base, matches); |
| if (rv != Success) { |
| return rv; |
| } |
| break; |
| |
| case GeneralNameType::iPAddress: |
| rv = MatchPresentedIPAddressWithConstraint(presentedID, base, |
| matches); |
| if (rv != Success) { |
| return rv; |
| } |
| break; |
| |
| case GeneralNameType::directoryName: |
| rv = MatchPresentedDirectoryNameWithConstraint(subtreesType, |
| presentedID, base, |
| matches); |
| if (rv != Success) { |
| return rv; |
| } |
| break; |
| |
| case GeneralNameType::rfc822Name: |
| rv = MatchPresentedRFC822NameWithReferenceRFC822Name( |
| presentedID, IDRole::NameConstraint, base, matches); |
| if (rv != Success) { |
| return rv; |
| } |
| break; |
| |
| // RFC 5280 says "Conforming CAs [...] SHOULD NOT impose name |
| // constraints on the x400Address, ediPartyName, or registeredID |
| // name forms. It also says "Applications conforming to this profile |
| // [...] SHOULD be able to process name constraints that are imposed |
| // on [...] uniformResourceIdentifier [...]", but we don't bother. |
| // |
| // TODO: Ask to have spec updated to say ""Conforming CAs [...] SHOULD |
| // NOT impose name constraints on the otherName, x400Address, |
| // ediPartyName, uniformResourceIdentifier, or registeredID name |
| // forms." |
| case GeneralNameType::otherName: // fall through |
| case GeneralNameType::x400Address: // fall through |
| case GeneralNameType::ediPartyName: // fall through |
| case GeneralNameType::uniformResourceIdentifier: // fall through |
| case GeneralNameType::registeredID: // fall through |
| return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
| |
| case GeneralNameType::nameConstraints: |
| return NotReached("invalid presentedIDType", |
| Result::FATAL_ERROR_LIBRARY_FAILURE); |
| |
| MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM |
| } |
| |
| switch (subtreesType) { |
| case NameConstraintsSubtrees::permittedSubtrees: |
| if (matches) { |
| hasPermittedSubtreesMatch = true; |
| } else { |
| hasPermittedSubtreesMismatch = true; |
| } |
| break; |
| case NameConstraintsSubtrees::excludedSubtrees: |
| if (matches) { |
| return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
| } |
| break; |
| } |
| } |
| } while (!subtrees.AtEnd()); |
| |
| if (hasPermittedSubtreesMismatch && !hasPermittedSubtreesMatch) { |
| // If there was any entry of the given type in permittedSubtrees, then it |
| // required that at least one of them must match. Since none of them did, |
| // we have a failure. |
| return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
| } |
| |
| return Success; |
| } |
| |
| // 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. |
| // |
| // 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. |
| // |
| // An relative presented DNS ID matches both an absolute reference ID and a |
| // relative reference ID. Absolute presented DNS IDs are not supported: |
| // |
| // Presented ID Reference ID Result |
| // ------------------------------------- |
| // example.com example.com Match |
| // example.com. example.com Mismatch |
| // example.com example.com. Match |
| // example.com. example.com. Mismatch |
| // |
| // There are more subtleties documented inline in the code. |
| // |
| // Name constraints /////////////////////////////////////////////////////////// |
| // |
| // This is all RFC 5280 has to say about DNSName constraints: |
| // |
| // 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. |
| // |
| // This lack of specificity has lead to a lot of uncertainty regarding |
| // subdomain matching. In particular, the following questions have been |
| // raised and answered: |
| // |
| // Q: Does a presented identifier equal (case insensitive) to the name |
| // constraint match the constraint? For example, does the presented |
| // ID "host.example.com" match a "host.example.com" constraint? |
| // A: Yes. RFC5280 says "by simply adding zero or more labels" and this |
| // is the case of adding zero labels. |
| // |
| // Q: When the name constraint does not start with ".", do subdomain |
| // presented identifiers match it? For example, does the presented |
| // ID "www.host.example.com" match a "host.example.com" constraint? |
| // A: Yes. RFC5280 says "by simply adding zero or more labels" and this |
| // is the case of adding more than zero labels. The example is the |
| // one from RFC 5280. |
| // |
| // Q: When the name constraint does not start with ".", does a |
| // non-subdomain prefix match it? For example, does "bigfoo.bar.com" |
| // match "foo.bar.com"? [4] |
| // A: No. We interpret RFC 5280's language of "adding zero or more labels" |
| // to mean that whole labels must be prefixed. |
| // |
| // (Note that the above three scenarios are the same as the RFC 6265 |
| // domain matching rules [0].) |
| // |
| // Q: Is a name constraint that starts with "." valid, and if so, what |
| // semantics does it have? For example, does a presented ID of |
| // "www.example.com" match a constraint of ".example.com"? Does a |
| // presented ID of "example.com" match a constraint of ".example.com"? |
| // A: This implementation, NSS[1], and SChannel[2] all support a |
| // leading ".", but OpenSSL[3] does not yet. Amongst the |
| // implementations that support it, a leading "." is legal and means |
| // the same thing as when the "." is omitted, EXCEPT that a |
| // presented identifier equal (case insensitive) to the name |
| // constraint is not matched; i.e. presented DNSName identifiers |
| // must be subdomains. Some CAs in Mozilla's CA program (e.g. HARICA) |
| // have name constraints with the leading "." in their root |
| // certificates. The name constraints imposed on DCISS by Mozilla also |
| // have the it, so supporting this is a requirement for backward |
| // compatibility, even if it is not yet standardized. So, for example, a |
| // presented ID of "www.example.com" matches a constraint of |
| // ".example.com" but a presented ID of "example.com" does not. |
| // |
| // Q: Is there a way to prevent subdomain matches? |
| // A: Yes. |
| // |
| // Some people have proposed that dNSName constraints that do not |
| // start with a "." should be restricted to exact (case insensitive) |
| // matches. However, such a change of semantics from what RFC5280 |
| // specifies would be a non-backward-compatible change in the case of |
| // permittedSubtrees constraints, and it would be a security issue for |
| // excludedSubtrees constraints. |
| // |
| // However, it can be done with a combination of permittedSubtrees and |
| // excludedSubtrees, e.g. "example.com" in permittedSubtrees and |
| // ".example.com" in excudedSubtrees. |
| // |
| // Q: Are name constraints allowed to be specified as absolute names? |
| // For example, does a presented ID of "example.com" match a name |
| // constraint of "example.com." and vice versa. |
| // A: Absolute names are not supported as presented IDs or name |
| // constraints. Only reference IDs may be absolute. |
| // |
| // Q: Is "" a valid DNSName constraints? If so, what does it mean? |
| // A: Yes. Any valid presented DNSName can be formed "by simply adding zero |
| // or more labels to the left-hand side" of "". In particular, an |
| // excludedSubtrees DNSName constraint of "" forbids all DNSNames. |
| // |
| // Q: Is "." a valid DNSName constraints? If so, what does it mean? |
| // A: No, because absolute names are not allowed (see above). |
| // |
| // [0] RFC 6265 (Cookies) Domain Matching rules: |
| // http://tools.ietf.org/html/rfc6265#section-5.1.3 |
| // [1] NSS source code: |
| // https://mxr.mozilla.org/nss/source/lib/certdb/genname.c?rev=2a7348f013cb#1209 |
| // [2] Description of SChannel's behavior from Microsoft: |
| // http://www.imc.org/ietf-pkix/mail-archive/msg04668.html |
| // [3] Proposal to add such support to OpenSSL: |
| // http://www.mail-archive.com/openssl-dev%40openssl.org/msg36204.html |
| // https://rt.openssl.org/Ticket/Display.html?id=3562 |
| // [4] Feedback on the lack of clarify in the definition that never got |
| // incorporated into the spec: |
| // https://www.ietf.org/mail-archive/web/pkix/current/msg21192.html |
| Result |
| MatchPresentedDNSIDWithReferenceDNSID( |
| Input presentedDNSID, |
| AllowWildcards allowWildcards, |
| AllowDotlessSubdomainMatches allowDotlessSubdomainMatches, |
| IDRole referenceDNSIDRole, |
| Input referenceDNSID, |
| /*out*/ bool& matches) |
| { |
| if (!IsValidDNSID(presentedDNSID, IDRole::PresentedID, allowWildcards)) { |
| return Result::ERROR_BAD_DER; |
| } |
| |
| if (!IsValidDNSID(referenceDNSID, referenceDNSIDRole, AllowWildcards::No)) { |
| return Result::ERROR_BAD_DER; |
| } |
| |
| Reader presented(presentedDNSID); |
| Reader reference(referenceDNSID); |
| |
| switch (referenceDNSIDRole) |
| { |
| case IDRole::ReferenceID: |
| break; |
| |
| case IDRole::NameConstraint: |
| { |
| if (presentedDNSID.GetLength() > referenceDNSID.GetLength()) { |
| if (referenceDNSID.GetLength() == 0) { |
| // An empty constraint matches everything. |
| matches = true; |
| return Success; |
| } |
| // If the reference ID starts with a dot then skip the prefix of |
| // of the presented ID and start the comparison at the position of that |
| // dot. Examples: |
| // |
| // Matches Doesn't Match |
| // ----------------------------------------------------------- |
| // original presented ID: www.example.com badexample.com |
| // skipped: www ba |
| // presented ID w/o prefix: .example.com dexample.com |
| // reference ID: .example.com .example.com |
| // |
| // If the reference ID does not start with a dot then we skip the |
| // prefix of the presented ID but also verify that the prefix ends with |
| // a dot. Examples: |
| // |
| // Matches Doesn't Match |
| // ----------------------------------------------------------- |
| // original presented ID: www.example.com badexample.com |
| // skipped: www ba |
| // must be '.': . d |
| // presented ID w/o prefix: example.com example.com |
| // reference ID: example.com example.com |
| // |
| if (reference.Peek('.')) { |
| if (presented.Skip(static_cast<Input::size_type>( |
| presentedDNSID.GetLength() - |
| referenceDNSID.GetLength())) != Success) { |
| return NotReached("skipping subdomain failed", |
| Result::FATAL_ERROR_LIBRARY_FAILURE); |
| } |
| } else if (allowDotlessSubdomainMatches == |
| AllowDotlessSubdomainMatches::Yes) { |
| if (presented.Skip(static_cast<Input::size_type>( |
| presentedDNSID.GetLength() - |
| referenceDNSID.GetLength() - 1)) != Success) { |
| return NotReached("skipping subdomains failed", |
| Result::FATAL_ERROR_LIBRARY_FAILURE); |
| } |
| uint8_t b; |
| if (presented.Read(b) != Success) { |
| return NotReached("reading from presentedDNSID failed", |
| Result::FATAL_ERROR_LIBRARY_FAILURE); |
| } |
| if (b != '.') { |
| matches = false; |
| return Success; |
| } |
| } |
| } |
| break; |
| } |
| |
| case IDRole::PresentedID: // fall through |
| return NotReached("IDRole::PresentedID is not a valid referenceDNSIDRole", |
| Result::FATAL_ERROR_INVALID_ARGS); |
| } |
| |
| // We only allow wildcard labels that consist only of '*'. |
| if (presented.Peek('*')) { |
| if (presented.Skip(1) != Success) { |
| return NotReached("Skipping '*' failed", |
| Result::FATAL_ERROR_LIBRARY_FAILURE); |
| } |
| do { |
| // This will happen if reference is a single, relative label |
| if (reference.AtEnd()) { |
| matches = false; |
| return Success; |
| } |
| uint8_t referenceByte; |
| if (reference.Read(referenceByte) != Success) { |
| return NotReached("invalid reference ID", |
| Result::FATAL_ERROR_INVALID_ARGS); |
| } |
| } while (!reference.Peek('.')); |
| } |
| |
| for (;;) { |
| uint8_t presentedByte; |
| if (presented.Read(presentedByte) != Success) { |
| matches = false; |
| return Success; |
| } |
| uint8_t referenceByte; |
| if (reference.Read(referenceByte) != Success) { |
| matches = false; |
| return Success; |
| } |
| if (LocaleInsensitveToLower(presentedByte) != |
| LocaleInsensitveToLower(referenceByte)) { |
| matches = false; |
| return Success; |
| } |
| if (presented.AtEnd()) { |
| // Don't allow presented IDs to be absolute. |
| if (presentedByte == '.') { |
| return Result::ERROR_BAD_DER; |
| } |
| break; |
| } |
| } |
| |
| // Allow a relative presented DNS ID to match an absolute reference DNS ID, |
| // unless we're matching a name constraint. |
| if (!reference.AtEnd()) { |
| if (referenceDNSIDRole != IDRole::NameConstraint) { |
| uint8_t referenceByte; |
| if (reference.Read(referenceByte) != Success) { |
| return NotReached("read failed but not at end", |
| Result::FATAL_ERROR_LIBRARY_FAILURE); |
| } |
| if (referenceByte != '.') { |
| matches = false; |
| return Success; |
| } |
| } |
| if (!reference.AtEnd()) { |
| matches = false; |
| return Success; |
| } |
| } |
| |
| matches = true; |
| return Success; |
| } |
| |
| // https://tools.ietf.org/html/rfc5280#section-4.2.1.10 says: |
| // |
| // 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). |
| Result |
| MatchPresentedIPAddressWithConstraint(Input presentedID, |
| Input iPAddressConstraint, |
| /*out*/ bool& foundMatch) |
| { |
| if (presentedID.GetLength() != 4 && presentedID.GetLength() != 16) { |
| return Result::ERROR_BAD_DER; |
| } |
| if (iPAddressConstraint.GetLength() != 8 && |
| iPAddressConstraint.GetLength() != 32) { |
| return Result::ERROR_BAD_DER; |
| } |
| |
| // an IPv4 address never matches an IPv6 constraint, and vice versa. |
| if (presentedID.GetLength() * 2 != iPAddressConstraint.GetLength()) { |
| foundMatch = false; |
| return Success; |
| } |
| |
| Reader constraint(iPAddressConstraint); |
| Reader constraintAddress; |
| Result rv = constraint.Skip(iPAddressConstraint.GetLength() / 2u, |
| constraintAddress); |
| if (rv != Success) { |
| return rv; |
| } |
| Reader constraintMask; |
| rv = constraint.Skip(iPAddressConstraint.GetLength() / 2u, constraintMask); |
| if (rv != Success) { |
| return rv; |
| } |
| rv = der::End(constraint); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| Reader presented(presentedID); |
| do { |
| uint8_t presentedByte; |
| rv = presented.Read(presentedByte); |
| if (rv != Success) { |
| return rv; |
| } |
| uint8_t constraintAddressByte; |
| rv = constraintAddress.Read(constraintAddressByte); |
| if (rv != Success) { |
| return rv; |
| } |
| uint8_t constraintMaskByte; |
| rv = constraintMask.Read(constraintMaskByte); |
| if (rv != Success) { |
| return rv; |
| } |
| foundMatch = |
| ((presentedByte ^ constraintAddressByte) & constraintMaskByte) == 0; |
| } while (foundMatch && !presented.AtEnd()); |
| |
| return Success; |
| } |
| |
| // AttributeTypeAndValue ::= SEQUENCE { |
| // type AttributeType, |
| // value AttributeValue } |
| // |
| // AttributeType ::= OBJECT IDENTIFIER |
| // |
| // AttributeValue ::= ANY -- DEFINED BY AttributeType |
| Result |
| ReadAVA(Reader& rdn, |
| /*out*/ Input& type, |
| /*out*/ uint8_t& valueTag, |
| /*out*/ Input& value) |
| { |
| return der::Nested(rdn, der::SEQUENCE, [&](Reader& ava) -> Result { |
| Result rv = der::ExpectTagAndGetValue(ava, der::OIDTag, type); |
| if (rv != Success) { |
| return rv; |
| } |
| rv = der::ReadTagAndGetValue(ava, valueTag, value); |
| if (rv != Success) { |
| return rv; |
| } |
| return Success; |
| }); |
| } |
| |
| // Names are sequences of RDNs. RDNS are sets of AVAs. That means that RDNs are |
| // unordered, so in theory we should match RDNs with equivalent AVAs that are |
| // in different orders. Within the AVAs are DirectoryNames that are supposed to |
| // be compared according to LDAP stringprep normalization rules (e.g. |
| // normalizing whitespace), consideration of different character encodings, |
| // etc. Indeed, RFC 5280 says we MUST deal with all of that. |
| // |
| // In practice, many implementations, including NSS, only match Names in a way |
| // that only meets a subset of the requirements of RFC 5280. Those |
| // normalization and character encoding conversion steps appear to be |
| // unnecessary for processing real-world certificates, based on experience from |
| // having used NSS in Firefox for many years. |
| // |
| // RFC 5280 also says "CAs issuing certificates with a restriction of the form |
| // directoryName SHOULD NOT rely on implementation of the full |
| // ISO DN name comparison algorithm. This implies name restrictions MUST |
| // be stated identically to the encoding used in the subject field or |
| // subjectAltName extension." It goes on to say, in the security |
| // considerations: |
| // |
| // In addition, name constraints for distinguished names MUST be stated |
| // identically to the encoding used in the subject field or |
| // subjectAltName extension. If not, then name constraints stated as |
| // excludedSubtrees will not match and invalid paths will be accepted |
| // and name constraints expressed as permittedSubtrees will not match |
| // and valid paths will be rejected. To avoid acceptance of invalid |
| // paths, CAs SHOULD state name constraints for distinguished names as |
| // permittedSubtrees wherever possible. |
| // |
| // For permittedSubtrees, the MUST-level requirement is relaxed for |
| // compatibility in the case of PrintableString and UTF8String. That is, if a |
| // name constraint has been encoded using UTF8String and the presented ID has |
| // been encoded with a PrintableString (or vice-versa), they are considered to |
| // match if they are equal everywhere except for the tag identifying the |
| // encoding. See bug 1150114. |
| // |
| // For excludedSubtrees, we simply prohibit any non-empty directoryName |
| // constraint to ensure we are not being too lenient. We support empty |
| // DirectoryName constraints in excludedSubtrees so that a CA can say "Do not |
| // allow any DirectoryNames in issued certificates." |
| Result |
| MatchPresentedDirectoryNameWithConstraint(NameConstraintsSubtrees subtreesType, |
| Input presentedID, |
| Input directoryNameConstraint, |
| /*out*/ bool& matches) |
| { |
| Reader constraintRDNs; |
| Result rv = der::ExpectTagAndGetValueAtEnd(directoryNameConstraint, |
| der::SEQUENCE, constraintRDNs); |
| if (rv != Success) { |
| return rv; |
| } |
| Reader presentedRDNs; |
| rv = der::ExpectTagAndGetValueAtEnd(presentedID, der::SEQUENCE, |
| presentedRDNs); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| switch (subtreesType) { |
| case NameConstraintsSubtrees::permittedSubtrees: |
| break; // dealt with below |
| case NameConstraintsSubtrees::excludedSubtrees: |
| if (!constraintRDNs.AtEnd() || !presentedRDNs.AtEnd()) { |
| return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
| } |
| matches = true; |
| return Success; |
| } |
| |
| for (;;) { |
| // The AVAs have to be fully equal, but the constraint RDNs just need to be |
| // a prefix of the presented RDNs. |
| if (constraintRDNs.AtEnd()) { |
| matches = true; |
| return Success; |
| } |
| if (presentedRDNs.AtEnd()) { |
| matches = false; |
| return Success; |
| } |
| Reader constraintRDN; |
| rv = der::ExpectTagAndGetValue(constraintRDNs, der::SET, constraintRDN); |
| if (rv != Success) { |
| return rv; |
| } |
| Reader presentedRDN; |
| rv = der::ExpectTagAndGetValue(presentedRDNs, der::SET, presentedRDN); |
| if (rv != Success) { |
| return rv; |
| } |
| while (!constraintRDN.AtEnd() && !presentedRDN.AtEnd()) { |
| Input constraintType; |
| uint8_t constraintValueTag; |
| Input constraintValue; |
| rv = ReadAVA(constraintRDN, constraintType, constraintValueTag, |
| constraintValue); |
| if (rv != Success) { |
| return rv; |
| } |
| Input presentedType; |
| uint8_t presentedValueTag; |
| Input presentedValue; |
| rv = ReadAVA(presentedRDN, presentedType, presentedValueTag, |
| presentedValue); |
| if (rv != Success) { |
| return rv; |
| } |
| // TODO (bug 1155767): verify that if an AVA is a PrintableString it |
| // consists only of characters valid for PrintableStrings. |
| bool avasMatch = |
| InputsAreEqual(constraintType, presentedType) && |
| InputsAreEqual(constraintValue, presentedValue) && |
| (constraintValueTag == presentedValueTag || |
| (constraintValueTag == der::Tag::UTF8String && |
| presentedValueTag == der::Tag::PrintableString) || |
| (constraintValueTag == der::Tag::PrintableString && |
| presentedValueTag == der::Tag::UTF8String)); |
| if (!avasMatch) { |
| matches = false; |
| return Success; |
| } |
| } |
| if (!constraintRDN.AtEnd() || !presentedRDN.AtEnd()) { |
| matches = false; |
| return Success; |
| } |
| } |
| } |
| |
| // RFC 5280 says: |
| // |
| // The format of an rfc822Name is a "Mailbox" as defined in Section 4.1.2 |
| // of [RFC2821]. A Mailbox has the form "Local-part@Domain". Note that a |
| // Mailbox has no phrase (such as a common name) before it, has no comment |
| // (text surrounded in parentheses) after it, and is not surrounded by "<" |
| // and ">". Rules for encoding Internet mail addresses that include |
| // internationalized domain names are specified in Section 7.5. |
| // |
| // and: |
| // |
| // A name constraint for Internet mail addresses MAY specify a |
| // particular mailbox, all addresses at a particular host, or all |
| // mailboxes in a domain. To indicate a particular mailbox, the |
| // constraint is the complete mail address. For example, |
| // "root@example.com" indicates the root mailbox on the host |
| // "example.com". To indicate all Internet mail addresses on a |
| // particular host, the constraint is specified as the host name. For |
| // example, the constraint "example.com" is satisfied by any mail |
| // address at the host "example.com". To specify any address within a |
| // domain, the constraint is specified with a leading period (as with |
| // URIs). For example, ".example.com" indicates all the Internet mail |
| // addresses in the domain "example.com", but not Internet mail |
| // addresses on the host "example.com". |
| |
| bool |
| IsValidRFC822Name(Input input) |
| { |
| Reader reader(input); |
| |
| // Local-part@. |
| bool startOfAtom = true; |
| for (;;) { |
| uint8_t presentedByte; |
| if (reader.Read(presentedByte) != Success) { |
| return false; |
| } |
| switch (presentedByte) { |
| // atext is defined in https://tools.ietf.org/html/rfc2822#section-3.2.4 |
| case 'A': case 'a': case 'N': case 'n': case '0': case '!': case '#': |
| case 'B': case 'b': case 'O': case 'o': case '1': case '$': case '%': |
| case 'C': case 'c': case 'P': case 'p': case '2': case '&': case '\'': |
| case 'D': case 'd': case 'Q': case 'q': case '3': case '*': case '+': |
| case 'E': case 'e': case 'R': case 'r': case '4': case '-': case '/': |
| case 'F': case 'f': case 'S': case 's': case '5': case '=': case '?': |
| case 'G': case 'g': case 'T': case 't': case '6': case '^': case '_': |
| case 'H': case 'h': case 'U': case 'u': case '7': case '`': case '{': |
| case 'I': case 'i': case 'V': case 'v': case '8': case '|': case '}': |
| case 'J': case 'j': case 'W': case 'w': case '9': case '~': |
| case 'K': case 'k': case 'X': case 'x': |
| case 'L': case 'l': case 'Y': case 'y': |
| case 'M': case 'm': case 'Z': case 'z': |
| startOfAtom = false; |
| break; |
| |
| case '.': |
| if (startOfAtom) { |
| return false; |
| } |
| startOfAtom = true; |
| break; |
| |
| case '@': |
| { |
| if (startOfAtom) { |
| return false; |
| } |
| Input domain; |
| if (reader.SkipToEnd(domain) != Success) { |
| return false; |
| } |
| return IsValidDNSID(domain, IDRole::PresentedID, AllowWildcards::No); |
| } |
| |
| default: |
| return false; |
| } |
| } |
| } |
| |
| Result |
| MatchPresentedRFC822NameWithReferenceRFC822Name(Input presentedRFC822Name, |
| IDRole referenceRFC822NameRole, |
| Input referenceRFC822Name, |
| /*out*/ bool& matches) |
| { |
| if (!IsValidRFC822Name(presentedRFC822Name)) { |
| return Result::ERROR_BAD_DER; |
| } |
| Reader presented(presentedRFC822Name); |
| |
| switch (referenceRFC822NameRole) |
| { |
| case IDRole::PresentedID: |
| return Result::FATAL_ERROR_INVALID_ARGS; |
| |
| case IDRole::ReferenceID: |
| break; |
| |
| case IDRole::NameConstraint: |
| { |
| if (InputContains(referenceRFC822Name, '@')) { |
| // The constraint is of the form "Local-part@Domain". |
| break; |
| } |
| |
| // The constraint is of the form "example.com" or ".example.com". |
| |
| // Skip past the '@' in the presented ID. |
| for (;;) { |
| uint8_t presentedByte; |
| if (presented.Read(presentedByte) != Success) { |
| return Result::FATAL_ERROR_LIBRARY_FAILURE; |
| } |
| if (presentedByte == '@') { |
| break; |
| } |
| } |
| |
| Input presentedDNSID; |
| if (presented.SkipToEnd(presentedDNSID) != Success) { |
| return Result::FATAL_ERROR_LIBRARY_FAILURE; |
| } |
| |
| return MatchPresentedDNSIDWithReferenceDNSID( |
| presentedDNSID, AllowWildcards::No, |
| AllowDotlessSubdomainMatches::No, IDRole::NameConstraint, |
| referenceRFC822Name, matches); |
| } |
| } |
| |
| if (!IsValidRFC822Name(referenceRFC822Name)) { |
| return Result::ERROR_BAD_DER; |
| } |
| |
| Reader reference(referenceRFC822Name); |
| |
| for (;;) { |
| uint8_t presentedByte; |
| if (presented.Read(presentedByte) != Success) { |
| matches = reference.AtEnd(); |
| return Success; |
| } |
| uint8_t referenceByte; |
| if (reference.Read(referenceByte) != Success) { |
| matches = false; |
| return Success; |
| } |
| if (LocaleInsensitveToLower(presentedByte) != |
| LocaleInsensitveToLower(referenceByte)) { |
| matches = false; |
| return Success; |
| } |
| } |
| } |
| |
| // 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 (const uint8_t prefixByte : IDN_ALABEL_PREFIX) { |
| uint8_t b; |
| if (input.Read(b) != Success) { |
| return false; |
| } |
| if (b != prefixByte) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool |
| ReadIPv4AddressComponent(Reader& input, bool lastComponent, |
| /*out*/ uint8_t& valueOut) |
| { |
| size_t length = 0; |
| unsigned int value = 0; // Must be larger than uint8_t. |
| |
| for (;;) { |
| if (input.AtEnd() && lastComponent) { |
| break; |
| } |
| |
| uint8_t b; |
| if (input.Read(b) != Success) { |
| return false; |
| } |
| |
| if (b >= '0' && b <= '9') { |
| if (value == 0 && length > 0) { |
| return false; // Leading zeros are not allowed. |
| } |
| value = (value * 10) + (b - '0'); |
| if (value > 255) { |
| return false; // Component's value is too large. |
| } |
| ++length; |
| } else if (!lastComponent && b == '.') { |
| break; |
| } else { |
| return false; // Invalid character. |
| } |
| } |
| |
| if (length == 0) { |
| return false; // empty components not allowed |
| } |
| |
| valueOut = static_cast<uint8_t>(value); |
| return true; |
| } |
| |
| } // namespace |
| |
| // 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. |
| bool |
| ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]) |
| { |
| Reader input(hostname); |
| return ReadIPv4AddressComponent(input, false, out[0]) && |
| ReadIPv4AddressComponent(input, false, out[1]) && |
| ReadIPv4AddressComponent(input, false, out[2]) && |
| ReadIPv4AddressComponent(input, true, out[3]); |
| } |
| |
| namespace { |
| |
| bool |
| FinishIPv6Address(/*in/out*/ uint8_t (&address)[16], int numComponents, |
| int contractionIndex) |
| { |
| assert(numComponents >= 0); |
| assert(numComponents <= 8); |
| assert(contractionIndex >= -1); |
| assert(contractionIndex <= 8); |
| assert(contractionIndex <= numComponents); |
| if (!(numComponents >= 0 && |
| numComponents <= 8 && |
| contractionIndex >= -1 && |
| contractionIndex <= 8 && |
| contractionIndex <= numComponents)) { |
| return false; |
| } |
| |
| if (contractionIndex == -1) { |
| // no contraction |
| return numComponents == 8; |
| } |
| |
| if (numComponents >= 8) { |
| return false; // no room left to expand the contraction. |
| } |
| |
| // Shift components that occur after the contraction over. |
| std::copy_backward(address + (2u * static_cast<size_t>(contractionIndex)), |
| address + (2u * static_cast<size_t>(numComponents)), |
| address + (2u * 8u)); |
| // Fill in the contracted area with zeros. |
| std::fill_n(address + 2u * static_cast<size_t>(contractionIndex), |
| (8u - static_cast<size_t>(numComponents)) * 2u, static_cast<uint8_t>(0u)); |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| // 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. |
| bool |
| ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]) |
| { |
| Reader input(hostname); |
| |
| int currentComponentIndex = 0; |
| int contractionIndex = -1; |
| |
| if (input.Peek(':')) { |
| // A valid input can only start with ':' if there is a contraction at the |
| // beginning. |
| uint8_t b; |
| if (input.Read(b) != Success || b != ':') { |
| assert(false); |
| return false; |
| } |
| if (input.Read(b) != Success) { |
| return false; |
| } |
| if (b != ':') { |
| return false; |
| } |
| contractionIndex = 0; |
| } |
| |
| for (;;) { |
| // If we encounter a '.' then we'll have to backtrack to parse the input |
| // from startOfComponent to the end of the input as an IPv4 address. |
| Reader::Mark startOfComponent(input.GetMark()); |
| uint16_t componentValue = 0; |
| size_t componentLength = 0; |
| while (!input.AtEnd() && !input.Peek(':')) { |
| uint8_t value; |
| uint8_t b; |
| if (input.Read(b) != Success) { |
| assert(false); |
| return false; |
| } |
| switch (b) { |
| case '0': case '1': case '2': case '3': case '4': |
| case '5': case '6': case '7': case '8': case '9': |
| value = static_cast<uint8_t>(b - static_cast<uint8_t>('0')); |
| break; |
| case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': |
| value = static_cast<uint8_t>(b - static_cast<uint8_t>('a') + |
| UINT8_C(10)); |
| break; |
| case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': |
| value = static_cast<uint8_t>(b - static_cast<uint8_t>('A') + |
| UINT8_C(10)); |
| break; |
| case '.': |
| { |
| // A dot indicates we hit a IPv4-syntax component. Backtrack, parsing |
| // the input from startOfComponent to the end of the input as an IPv4 |
| // address, and then combine it with the other components. |
| |
| if (currentComponentIndex > 6) { |
| return false; // Too many components before the IPv4 component |
| } |
| |
| input.SkipToEnd(); |
| Input ipv4Component; |
| if (input.GetInput(startOfComponent, ipv4Component) != Success) { |
| return false; |
| } |
| uint8_t (*ipv4)[4] = |
| reinterpret_cast<uint8_t(*)[4]>(&out[2 * currentComponentIndex]); |
| if (!ParseIPv4Address(ipv4Component, *ipv4)) { |
| return false; |
| } |
| assert(input.AtEnd()); |
| currentComponentIndex += 2; |
| |
| return FinishIPv6Address(out, currentComponentIndex, |
| contractionIndex); |
| } |
| default: |
| return false; |
| } |
| if (componentLength >= 4) { |
| // component too long |
| return false; |
| } |
| ++componentLength; |
| componentValue = (componentValue * 0x10u) + value; |
| } |
| |
| if (currentComponentIndex >= 8) { |
| return false; // too many components |
| } |
| |
| if (componentLength == 0) { |
| if (input.AtEnd() && currentComponentIndex == contractionIndex) { |
| if (contractionIndex == 0) { |
| // don't accept "::" |
| return false; |
| } |
| return FinishIPv6Address(out, currentComponentIndex, |
| contractionIndex); |
| } |
| return false; |
| } |
| |
| out[2 * currentComponentIndex] = |
| static_cast<uint8_t>(componentValue / 0x100); |
| out[(2 * currentComponentIndex) + 1] = |
| static_cast<uint8_t>(componentValue % 0x100); |
| |
| ++currentComponentIndex; |
| |
| if (input.AtEnd()) { |
| return FinishIPv6Address(out, currentComponentIndex, |
| contractionIndex); |
| } |
| |
| uint8_t b; |
| if (input.Read(b) != Success || b != ':') { |
| assert(false); |
| return false; |
| } |
| |
| if (input.Peek(':')) { |
| // Contraction |
| if (contractionIndex != -1) { |
| return false; // multiple contractions are not allowed. |
| } |
| if (input.Read(b) != Success || b != ':') { |
| assert(false); |
| return false; |
| } |
| contractionIndex = currentComponentIndex; |
| if (input.AtEnd()) { |
| // "::" at the end of the input. |
| return FinishIPv6Address(out, currentComponentIndex, |
| contractionIndex); |
| } |
| } |
| } |
| } |
| |
| bool |
| IsValidReferenceDNSID(Input hostname) |
| { |
| return IsValidDNSID(hostname, IDRole::ReferenceID, AllowWildcards::No); |
| } |
| |
| bool |
| IsValidPresentedDNSID(Input hostname) |
| { |
| return IsValidDNSID(hostname, IDRole::PresentedID, AllowWildcards::Yes); |
| } |
| |
| namespace { |
| |
| // RFC 5280 Section 4.2.1.6 says that a dNSName "MUST be in the 'preferred name |
| // syntax', as specified by Section 3.5 of [RFC1034] and as modified by Section |
| // 2.1 of [RFC1123]" except "a dNSName of ' ' MUST NOT be used." Additionally, |
| // we allow underscores for compatibility with existing practice. |
| bool |
| IsValidDNSID(Input hostname, IDRole idRole, AllowWildcards allowWildcards) |
| { |
| if (hostname.GetLength() > 253) { |
| return false; |
| } |
| |
| Reader input(hostname); |
| |
| if (idRole == IDRole::NameConstraint && input.AtEnd()) { |
| return true; |
| } |
| |
| size_t dotCount = 0; |
| size_t labelLength = 0; |
| bool labelIsAllNumeric = false; |
| bool labelEndsWithHyphen = false; |
| |
| // Only presented IDs are allowed to have wildcard labels. And, like |
| // Chromium, be stricter than RFC 6125 requires by insisting that a |
| // wildcard label consist only of '*'. |
| bool isWildcard = allowWildcards == AllowWildcards::Yes && input.Peek('*'); |
| bool isFirstByte = !isWildcard; |
| if (isWildcard) { |
| Result rv = input.Skip(1); |
| if (rv != Success) { |
| assert(false); |
| return false; |
| } |
| |
| uint8_t b; |
| rv = input.Read(b); |
| if (rv != Success) { |
| return false; |
| } |
| if (b != '.') { |
| return false; |
| } |
| ++dotCount; |
| } |
| |
| 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; |
| labelEndsWithHyphen = 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; |
| } |
| labelEndsWithHyphen = 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': |
| // We allow underscores for compatibility with existing practices. |
| // See bug 1136616. |
| case '_': |
| labelIsAllNumeric = false; |
| labelEndsWithHyphen = false; |
| ++labelLength; |
| if (labelLength > MAX_LABEL_LENGTH) { |
| return false; |
| } |
| break; |
| |
| case '.': |
| ++dotCount; |
| if (labelLength == 0 && |
| (idRole != IDRole::NameConstraint || !isFirstByte)) { |
| return false; |
| } |
| if (labelEndsWithHyphen) { |
| return false; // Labels must not end with a hyphen. |
| } |
| labelLength = 0; |
| break; |
| |
| default: |
| return false; // Invalid character. |
| } |
| isFirstByte = false; |
| } while (!input.AtEnd()); |
| |
| // Only reference IDs, not presented IDs or name constraints, may be |
| // absolute. |
| if (labelLength == 0 && idRole != IDRole::ReferenceID) { |
| return false; |
| } |
| |
| if (labelEndsWithHyphen) { |
| return false; // Labels must not end with a hyphen. |
| } |
| |
| if (labelIsAllNumeric) { |
| return false; // Last label must not be all numeric. |
| } |
| |
| if (isWildcard) { |
| // If the DNS ID ends with a dot, the last dot signifies an absolute ID. |
| size_t labelCount = (labelLength == 0) ? dotCount : (dotCount + 1); |
| |
| // Like NSS, require at least two labels to follow the wildcard label. |
| // |
| // 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. |
| if (labelCount < 3) { |
| return false; |
| } |
| // XXX: RFC6125 says that we shouldn't accept wildcards within an IDN |
| // A-Label. The consequence of this is that we effectively discriminate |
| // against users of languages that cannot be encoded with ASCII. |
| if (StartsWithIDNALabel(hostname)) { |
| return false; |
| } |
| |
| // 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 |
| |
| } } // namespace mozilla::pkix |