/src/mozilla-central/security/pkix/lib/pkixnames.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
3 | | /* This code is made available to you under your choice of the following sets |
4 | | * of licensing terms: |
5 | | */ |
6 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
7 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
8 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
9 | | */ |
10 | | /* Copyright 2014 Mozilla Contributors |
11 | | * |
12 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
13 | | * you may not use this file except in compliance with the License. |
14 | | * You may obtain a copy of the License at |
15 | | * |
16 | | * http://www.apache.org/licenses/LICENSE-2.0 |
17 | | * |
18 | | * Unless required by applicable law or agreed to in writing, software |
19 | | * distributed under the License is distributed on an "AS IS" BASIS, |
20 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
21 | | * See the License for the specific language governing permissions and |
22 | | * limitations under the License. |
23 | | */ |
24 | | |
25 | | // This code implements RFC6125-ish name matching, RFC5280-ish name constraint |
26 | | // checking, and related things. |
27 | | // |
28 | | // In this code, identifiers are classified as either "presented" or |
29 | | // "reference" identifiers are defined in |
30 | | // http://tools.ietf.org/html/rfc6125#section-1.8. A "presented identifier" is |
31 | | // one in the subjectAltName of the certificate, or sometimes within a CN of |
32 | | // the certificate's subject. The "reference identifier" is the one we are |
33 | | // being asked to match the certificate against. When checking name |
34 | | // constraints, the reference identifier is the entire encoded name constraint |
35 | | // extension value. |
36 | | |
37 | | #include <algorithm> |
38 | | |
39 | | #include "pkixcheck.h" |
40 | | #include "pkixutil.h" |
41 | | |
42 | | namespace mozilla { namespace pkix { |
43 | | |
44 | | namespace { |
45 | | |
46 | | // GeneralName ::= CHOICE { |
47 | | // otherName [0] OtherName, |
48 | | // rfc822Name [1] IA5String, |
49 | | // dNSName [2] IA5String, |
50 | | // x400Address [3] ORAddress, |
51 | | // directoryName [4] Name, |
52 | | // ediPartyName [5] EDIPartyName, |
53 | | // uniformResourceIdentifier [6] IA5String, |
54 | | // iPAddress [7] OCTET STRING, |
55 | | // registeredID [8] OBJECT IDENTIFIER } |
56 | | enum class GeneralNameType : uint8_t |
57 | | { |
58 | | // Note that these values are NOT contiguous. Some values have the |
59 | | // der::CONSTRUCTED bit set while others do not. |
60 | | // (The der::CONSTRUCTED bit is for types where the value is a SEQUENCE.) |
61 | | otherName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 0, |
62 | | rfc822Name = der::CONTEXT_SPECIFIC | 1, |
63 | | dNSName = der::CONTEXT_SPECIFIC | 2, |
64 | | x400Address = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 3, |
65 | | directoryName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 4, |
66 | | ediPartyName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 5, |
67 | | uniformResourceIdentifier = der::CONTEXT_SPECIFIC | 6, |
68 | | iPAddress = der::CONTEXT_SPECIFIC | 7, |
69 | | registeredID = der::CONTEXT_SPECIFIC | 8, |
70 | | // nameConstraints is a pseudo-GeneralName used to signify that a |
71 | | // reference ID is actually the entire name constraint extension. |
72 | | nameConstraints = 0xff |
73 | | }; |
74 | | |
75 | | inline Result |
76 | | ReadGeneralName(Reader& reader, |
77 | | /*out*/ GeneralNameType& generalNameType, |
78 | | /*out*/ Input& value) |
79 | 0 | { |
80 | 0 | uint8_t tag; |
81 | 0 | Result rv = der::ReadTagAndGetValue(reader, tag, value); |
82 | 0 | if (rv != Success) { |
83 | 0 | return rv; |
84 | 0 | } |
85 | 0 | switch (tag) { |
86 | 0 | case static_cast<uint8_t>(GeneralNameType::otherName): |
87 | 0 | generalNameType = GeneralNameType::otherName; |
88 | 0 | break; |
89 | 0 | case static_cast<uint8_t>(GeneralNameType::rfc822Name): |
90 | 0 | generalNameType = GeneralNameType::rfc822Name; |
91 | 0 | break; |
92 | 0 | case static_cast<uint8_t>(GeneralNameType::dNSName): |
93 | 0 | generalNameType = GeneralNameType::dNSName; |
94 | 0 | break; |
95 | 0 | case static_cast<uint8_t>(GeneralNameType::x400Address): |
96 | 0 | generalNameType = GeneralNameType::x400Address; |
97 | 0 | break; |
98 | 0 | case static_cast<uint8_t>(GeneralNameType::directoryName): |
99 | 0 | generalNameType = GeneralNameType::directoryName; |
100 | 0 | break; |
101 | 0 | case static_cast<uint8_t>(GeneralNameType::ediPartyName): |
102 | 0 | generalNameType = GeneralNameType::ediPartyName; |
103 | 0 | break; |
104 | 0 | case static_cast<uint8_t>(GeneralNameType::uniformResourceIdentifier): |
105 | 0 | generalNameType = GeneralNameType::uniformResourceIdentifier; |
106 | 0 | break; |
107 | 0 | case static_cast<uint8_t>(GeneralNameType::iPAddress): |
108 | 0 | generalNameType = GeneralNameType::iPAddress; |
109 | 0 | break; |
110 | 0 | case static_cast<uint8_t>(GeneralNameType::registeredID): |
111 | 0 | generalNameType = GeneralNameType::registeredID; |
112 | 0 | break; |
113 | 0 | default: |
114 | 0 | return Result::ERROR_BAD_DER; |
115 | 0 | } |
116 | 0 | return Success; |
117 | 0 | } |
118 | | |
119 | | enum class MatchResult |
120 | | { |
121 | | NoNamesOfGivenType = 0, |
122 | | Mismatch = 1, |
123 | | Match = 2 |
124 | | }; |
125 | | |
126 | | Result SearchNames(const Input* subjectAltName, Input subject, |
127 | | GeneralNameType referenceIDType, |
128 | | Input referenceID, |
129 | | FallBackToSearchWithinSubject fallBackToCommonName, |
130 | | /*out*/ MatchResult& match); |
131 | | Result SearchWithinRDN(Reader& rdn, |
132 | | GeneralNameType referenceIDType, |
133 | | Input referenceID, |
134 | | FallBackToSearchWithinSubject fallBackToEmailAddress, |
135 | | FallBackToSearchWithinSubject fallBackToCommonName, |
136 | | /*in/out*/ MatchResult& match); |
137 | | Result MatchAVA(Input type, |
138 | | uint8_t valueEncodingTag, |
139 | | Input presentedID, |
140 | | GeneralNameType referenceIDType, |
141 | | Input referenceID, |
142 | | FallBackToSearchWithinSubject fallBackToEmailAddress, |
143 | | FallBackToSearchWithinSubject fallBackToCommonName, |
144 | | /*in/out*/ MatchResult& match); |
145 | | Result ReadAVA(Reader& rdn, |
146 | | /*out*/ Input& type, |
147 | | /*out*/ uint8_t& valueTag, |
148 | | /*out*/ Input& value); |
149 | | void MatchSubjectPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
150 | | Input presentedID, |
151 | | GeneralNameType referenceIDType, |
152 | | Input referenceID, |
153 | | /*in/out*/ MatchResult& match); |
154 | | |
155 | | Result MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
156 | | Input presentedID, |
157 | | GeneralNameType referenceIDType, |
158 | | Input referenceID, |
159 | | /*in/out*/ MatchResult& matchResult); |
160 | | Result CheckPresentedIDConformsToConstraints(GeneralNameType referenceIDType, |
161 | | Input presentedID, |
162 | | Input nameConstraints); |
163 | | |
164 | | uint8_t LocaleInsensitveToLower(uint8_t a); |
165 | | bool StartsWithIDNALabel(Input id); |
166 | | |
167 | | enum class IDRole |
168 | | { |
169 | | ReferenceID = 0, |
170 | | PresentedID = 1, |
171 | | NameConstraint = 2, |
172 | | }; |
173 | | |
174 | | enum class AllowWildcards { No = 0, Yes = 1 }; |
175 | | |
176 | | // DNSName constraints implicitly allow subdomain matching when there is no |
177 | | // leading dot ("foo.example.com" matches a constraint of "example.com"), but |
178 | | // RFC822Name constraints only allow subdomain matching when there is a leading |
179 | | // dot ("foo.example.com" does not match "example.com" but does match |
180 | | // ".example.com"). |
181 | | enum class AllowDotlessSubdomainMatches { No = 0, Yes = 1 }; |
182 | | |
183 | | bool IsValidDNSID(Input hostname, IDRole idRole, |
184 | | AllowWildcards allowWildcards); |
185 | | |
186 | | Result MatchPresentedDNSIDWithReferenceDNSID( |
187 | | Input presentedDNSID, |
188 | | AllowWildcards allowWildcards, |
189 | | AllowDotlessSubdomainMatches allowDotlessSubdomainMatches, |
190 | | IDRole referenceDNSIDRole, |
191 | | Input referenceDNSID, |
192 | | /*out*/ bool& matches); |
193 | | |
194 | | Result MatchPresentedRFC822NameWithReferenceRFC822Name( |
195 | | Input presentedRFC822Name, IDRole referenceRFC822NameRole, |
196 | | Input referenceRFC822Name, /*out*/ bool& matches); |
197 | | |
198 | | } // namespace |
199 | | |
200 | | bool IsValidReferenceDNSID(Input hostname); |
201 | | bool IsValidPresentedDNSID(Input hostname); |
202 | | bool ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]); |
203 | | bool ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]); |
204 | | |
205 | | // This is used by the pkixnames_tests.cpp tests. |
206 | | Result |
207 | | MatchPresentedDNSIDWithReferenceDNSID(Input presentedDNSID, |
208 | | Input referenceDNSID, |
209 | | /*out*/ bool& matches) |
210 | 0 | { |
211 | 0 | return MatchPresentedDNSIDWithReferenceDNSID( |
212 | 0 | presentedDNSID, AllowWildcards::Yes, |
213 | 0 | AllowDotlessSubdomainMatches::Yes, IDRole::ReferenceID, |
214 | 0 | referenceDNSID, matches); |
215 | 0 | } |
216 | | |
217 | | // Verify that the given end-entity cert, which is assumed to have been already |
218 | | // validated with BuildCertChain, is valid for the given hostname. hostname is |
219 | | // assumed to be a string representation of an IPv4 address, an IPv6 addresss, |
220 | | // or a normalized ASCII (possibly punycode) DNS name. |
221 | | Result |
222 | | CheckCertHostname(Input endEntityCertDER, Input hostname, |
223 | | NameMatchingPolicy& nameMatchingPolicy) |
224 | 0 | { |
225 | 0 | BackCert cert(endEntityCertDER, EndEntityOrCA::MustBeEndEntity, nullptr); |
226 | 0 | Result rv = cert.Init(); |
227 | 0 | if (rv != Success) { |
228 | 0 | return rv; |
229 | 0 | } |
230 | 0 | |
231 | 0 | Time notBefore(Time::uninitialized); |
232 | 0 | rv = ParseValidity(cert.GetValidity(), ¬Before); |
233 | 0 | if (rv != Success) { |
234 | 0 | return rv; |
235 | 0 | } |
236 | 0 | FallBackToSearchWithinSubject fallBackToSearchWithinSubject; |
237 | 0 | rv = nameMatchingPolicy.FallBackToCommonName(notBefore, |
238 | 0 | fallBackToSearchWithinSubject); |
239 | 0 | if (rv != Success) { |
240 | 0 | return rv; |
241 | 0 | } |
242 | 0 | |
243 | 0 | const Input* subjectAltName(cert.GetSubjectAltName()); |
244 | 0 | Input subject(cert.GetSubject()); |
245 | 0 |
|
246 | 0 | // For backward compatibility with legacy certificates, we may fall back to |
247 | 0 | // searching for a name match in the subject common name for DNS names and |
248 | 0 | // IPv4 addresses. We don't do so for IPv6 addresses because we do not think |
249 | 0 | // there are many certificates that would need such fallback, and because |
250 | 0 | // comparisons of string representations of IPv6 addresses are particularly |
251 | 0 | // error prone due to the syntactic flexibility that IPv6 addresses have. |
252 | 0 | // |
253 | 0 | // IPv4 and IPv6 addresses are represented using the same type of GeneralName |
254 | 0 | // (iPAddress); they are differentiated by the lengths of the values. |
255 | 0 | MatchResult match; |
256 | 0 | uint8_t ipv6[16]; |
257 | 0 | uint8_t ipv4[4]; |
258 | 0 | if (IsValidReferenceDNSID(hostname)) { |
259 | 0 | rv = SearchNames(subjectAltName, subject, GeneralNameType::dNSName, |
260 | 0 | hostname, fallBackToSearchWithinSubject, match); |
261 | 0 | } else if (ParseIPv6Address(hostname, ipv6)) { |
262 | 0 | rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress, |
263 | 0 | Input(ipv6), FallBackToSearchWithinSubject::No, match); |
264 | 0 | } else if (ParseIPv4Address(hostname, ipv4)) { |
265 | 0 | rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress, |
266 | 0 | Input(ipv4), fallBackToSearchWithinSubject, match); |
267 | 0 | } else { |
268 | 0 | return Result::ERROR_BAD_CERT_DOMAIN; |
269 | 0 | } |
270 | 0 | if (rv != Success) { |
271 | 0 | return rv; |
272 | 0 | } |
273 | 0 | switch (match) { |
274 | 0 | case MatchResult::NoNamesOfGivenType: // fall through |
275 | 0 | case MatchResult::Mismatch: |
276 | 0 | return Result::ERROR_BAD_CERT_DOMAIN; |
277 | 0 | case MatchResult::Match: |
278 | 0 | return Success; |
279 | 0 | MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM |
280 | 0 | } |
281 | 0 | } |
282 | | |
283 | | // 4.2.1.10. Name Constraints |
284 | | Result |
285 | | CheckNameConstraints(Input encodedNameConstraints, |
286 | | const BackCert& firstChild, |
287 | | KeyPurposeId requiredEKUIfPresent) |
288 | 0 | { |
289 | 0 | for (const BackCert* child = &firstChild; child; child = child->childCert) { |
290 | 0 | FallBackToSearchWithinSubject fallBackToCommonName |
291 | 0 | = (child->endEntityOrCA == EndEntityOrCA::MustBeEndEntity && |
292 | 0 | requiredEKUIfPresent == KeyPurposeId::id_kp_serverAuth) |
293 | 0 | ? FallBackToSearchWithinSubject::Yes |
294 | 0 | : FallBackToSearchWithinSubject::No; |
295 | 0 |
|
296 | 0 | MatchResult match; |
297 | 0 | Result rv = SearchNames(child->GetSubjectAltName(), child->GetSubject(), |
298 | 0 | GeneralNameType::nameConstraints, |
299 | 0 | encodedNameConstraints, fallBackToCommonName, |
300 | 0 | match); |
301 | 0 | if (rv != Success) { |
302 | 0 | return rv; |
303 | 0 | } |
304 | 0 | switch (match) { |
305 | 0 | case MatchResult::Match: // fall through |
306 | 0 | case MatchResult::NoNamesOfGivenType: |
307 | 0 | break; |
308 | 0 | case MatchResult::Mismatch: |
309 | 0 | return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
310 | 0 | } |
311 | 0 | } |
312 | 0 |
|
313 | 0 | return Success; |
314 | 0 | } |
315 | | |
316 | | namespace { |
317 | | |
318 | | // SearchNames is used by CheckCertHostname and CheckNameConstraints. |
319 | | // |
320 | | // When called during name constraint checking, referenceIDType is |
321 | | // GeneralNameType::nameConstraints and referenceID is the entire encoded name |
322 | | // constraints extension value. |
323 | | // |
324 | | // The main benefit of using the exact same code paths for both is that we |
325 | | // ensure consistency between name validation and name constraint enforcement |
326 | | // regarding thing like "Which CN attributes should be considered as potential |
327 | | // CN-IDs" and "Which character sets are acceptable for CN-IDs?" If the name |
328 | | // matching and the name constraint enforcement logic were out of sync on these |
329 | | // issues (e.g. if name matching were to consider all subject CN attributes, |
330 | | // but name constraints were only enforced on the most specific subject CN), |
331 | | // trivial name constraint bypasses could result. |
332 | | |
333 | | Result |
334 | | SearchNames(/*optional*/ const Input* subjectAltName, |
335 | | Input subject, |
336 | | GeneralNameType referenceIDType, |
337 | | Input referenceID, |
338 | | FallBackToSearchWithinSubject fallBackToCommonName, |
339 | | /*out*/ MatchResult& match) |
340 | 0 | { |
341 | 0 | Result rv; |
342 | 0 |
|
343 | 0 | match = MatchResult::NoNamesOfGivenType; |
344 | 0 |
|
345 | 0 | // RFC 6125 says "A client MUST NOT seek a match for a reference identifier |
346 | 0 | // of CN-ID if the presented identifiers include a DNS-ID, SRV-ID, URI-ID, or |
347 | 0 | // any application-specific identifier types supported by the client." |
348 | 0 | // Accordingly, we only consider CN-IDs if there are no DNS-IDs in the |
349 | 0 | // subjectAltName. |
350 | 0 | // |
351 | 0 | // RFC 6125 says that IP addresses are out of scope, but for backward |
352 | 0 | // compatibility we accept them, by considering IP addresses to be an |
353 | 0 | // "application-specific identifier type supported by the client." |
354 | 0 | // |
355 | 0 | // TODO(bug XXXXXXX): Consider strengthening this check to "A client MUST NOT |
356 | 0 | // seek a match for a reference identifier of CN-ID if the certificate |
357 | 0 | // contains a subjectAltName extension." |
358 | 0 | // |
359 | 0 | // TODO(bug XXXXXXX): Consider dropping support for IP addresses as |
360 | 0 | // identifiers completely. |
361 | 0 |
|
362 | 0 | if (subjectAltName) { |
363 | 0 | Reader altNames; |
364 | 0 | rv = der::ExpectTagAndGetValueAtEnd(*subjectAltName, der::SEQUENCE, |
365 | 0 | altNames); |
366 | 0 | if (rv != Success) { |
367 | 0 | return rv; |
368 | 0 | } |
369 | 0 | |
370 | 0 | // According to RFC 5280, "If the subjectAltName extension is present, the |
371 | 0 | // sequence MUST contain at least one entry." For compatibility reasons, we |
372 | 0 | // do not enforce this. See bug 1143085. |
373 | 0 | while (!altNames.AtEnd()) { |
374 | 0 | GeneralNameType presentedIDType; |
375 | 0 | Input presentedID; |
376 | 0 | rv = ReadGeneralName(altNames, presentedIDType, presentedID); |
377 | 0 | if (rv != Success) { |
378 | 0 | return rv; |
379 | 0 | } |
380 | 0 | |
381 | 0 | rv = MatchPresentedIDWithReferenceID(presentedIDType, presentedID, |
382 | 0 | referenceIDType, referenceID, |
383 | 0 | match); |
384 | 0 | if (rv != Success) { |
385 | 0 | return rv; |
386 | 0 | } |
387 | 0 | if (referenceIDType != GeneralNameType::nameConstraints && |
388 | 0 | match == MatchResult::Match) { |
389 | 0 | return Success; |
390 | 0 | } |
391 | 0 | if (presentedIDType == GeneralNameType::dNSName || |
392 | 0 | presentedIDType == GeneralNameType::iPAddress) { |
393 | 0 | fallBackToCommonName = FallBackToSearchWithinSubject::No; |
394 | 0 | } |
395 | 0 | } |
396 | 0 | } |
397 | 0 |
|
398 | 0 | if (referenceIDType == GeneralNameType::nameConstraints) { |
399 | 0 | rv = CheckPresentedIDConformsToConstraints(GeneralNameType::directoryName, |
400 | 0 | subject, referenceID); |
401 | 0 | if (rv != Success) { |
402 | 0 | return rv; |
403 | 0 | } |
404 | 0 | } |
405 | 0 | |
406 | 0 | FallBackToSearchWithinSubject fallBackToEmailAddress; |
407 | 0 | if (!subjectAltName && |
408 | 0 | (referenceIDType == GeneralNameType::rfc822Name || |
409 | 0 | referenceIDType == GeneralNameType::nameConstraints)) { |
410 | 0 | fallBackToEmailAddress = FallBackToSearchWithinSubject::Yes; |
411 | 0 | } else { |
412 | 0 | fallBackToEmailAddress = FallBackToSearchWithinSubject::No; |
413 | 0 | } |
414 | 0 |
|
415 | 0 | // Short-circuit the parsing of the subject name if we're not going to match |
416 | 0 | // any names in it |
417 | 0 | if (fallBackToEmailAddress == FallBackToSearchWithinSubject::No && |
418 | 0 | fallBackToCommonName == FallBackToSearchWithinSubject::No) { |
419 | 0 | return Success; |
420 | 0 | } |
421 | 0 | |
422 | 0 | // Attempt to match the reference ID against the CN-ID, which we consider to |
423 | 0 | // be the most-specific CN AVA in the subject field. |
424 | 0 | // |
425 | 0 | // https://tools.ietf.org/html/rfc6125#section-2.3.1 says: |
426 | 0 | // |
427 | 0 | // To reduce confusion, in this specification we avoid such terms and |
428 | 0 | // instead use the terms provided under Section 1.8; in particular, we |
429 | 0 | // do not use the term "(most specific) Common Name field in the subject |
430 | 0 | // field" from [HTTP-TLS] and instead state that a CN-ID is a Relative |
431 | 0 | // Distinguished Name (RDN) in the certificate subject containing one |
432 | 0 | // and only one attribute-type-and-value pair of type Common Name (thus |
433 | 0 | // removing the possibility that an RDN might contain multiple AVAs |
434 | 0 | // (Attribute Value Assertions) of type CN, one of which could be |
435 | 0 | // considered "most specific"). |
436 | 0 | // |
437 | 0 | // https://tools.ietf.org/html/rfc6125#section-7.4 says: |
438 | 0 | // |
439 | 0 | // [...] Although it would be preferable to |
440 | 0 | // forbid multiple CN-IDs entirely, there are several reasons at this |
441 | 0 | // time why this specification states that they SHOULD NOT (instead of |
442 | 0 | // MUST NOT) be included [...] |
443 | 0 | // |
444 | 0 | // Consequently, it is unclear what to do when there are multiple CNs in the |
445 | 0 | // subject, regardless of whether there "SHOULD NOT" be. |
446 | 0 | // |
447 | 0 | // NSS's CERT_VerifyCertName mostly follows RFC2818 in this instance, which |
448 | 0 | // says: |
449 | 0 | // |
450 | 0 | // If a subjectAltName extension of type dNSName is present, that MUST |
451 | 0 | // be used as the identity. Otherwise, the (most specific) Common Name |
452 | 0 | // field in the Subject field of the certificate MUST be used. |
453 | 0 | // |
454 | 0 | // [...] |
455 | 0 | // |
456 | 0 | // In some cases, the URI is specified as an IP address rather than a |
457 | 0 | // hostname. In this case, the iPAddress subjectAltName must be present |
458 | 0 | // in the certificate and must exactly match the IP in the URI. |
459 | 0 | // |
460 | 0 | // (The main difference from RFC2818 is that NSS's CERT_VerifyCertName also |
461 | 0 | // matches IP addresses in the most-specific CN.) |
462 | 0 | // |
463 | 0 | // NSS's CERT_VerifyCertName finds the most specific CN via |
464 | 0 | // CERT_GetCommoName, which uses CERT_GetLastNameElement. Note that many |
465 | 0 | // NSS-based applications, including Gecko, also use CERT_GetCommonName. It |
466 | 0 | // is likely that other, non-NSS-based, applications also expect only the |
467 | 0 | // most specific CN to be matched against the reference ID. |
468 | 0 | // |
469 | 0 | // "A Layman's Guide to a Subset of ASN.1, BER, and DER" and other sources |
470 | 0 | // agree that an RDNSequence is ordered from most significant (least |
471 | 0 | // specific) to least significant (most specific), as do other references. |
472 | 0 | // |
473 | 0 | // However, Chromium appears to use the least-specific (first) CN instead of |
474 | 0 | // the most-specific; see https://crbug.com/366957. Also, MSIE and some other |
475 | 0 | // popular implementations apparently attempt to match the reference ID |
476 | 0 | // against any/all CNs in the subject. Since we're trying to phase out the |
477 | 0 | // use of CN-IDs, we intentionally avoid trying to match MSIE's more liberal |
478 | 0 | // behavior. |
479 | 0 | |
480 | 0 | // Name ::= CHOICE { -- only one possibility for now -- |
481 | 0 | // rdnSequence RDNSequence } |
482 | 0 | // |
483 | 0 | // RDNSequence ::= SEQUENCE OF RelativeDistinguishedName |
484 | 0 | // |
485 | 0 | // RelativeDistinguishedName ::= |
486 | 0 | // SET SIZE (1..MAX) OF AttributeTypeAndValue |
487 | 0 | Reader subjectReader(subject); |
488 | 0 | return der::NestedOf(subjectReader, der::SEQUENCE, der::SET, |
489 | 0 | der::EmptyAllowed::Yes, [&](Reader& r) { |
490 | 0 | return SearchWithinRDN(r, referenceIDType, referenceID, |
491 | 0 | fallBackToEmailAddress, fallBackToCommonName, match); |
492 | 0 | }); |
493 | 0 | } |
494 | | |
495 | | // RelativeDistinguishedName ::= |
496 | | // SET SIZE (1..MAX) OF AttributeTypeAndValue |
497 | | // |
498 | | // AttributeTypeAndValue ::= SEQUENCE { |
499 | | // type AttributeType, |
500 | | // value AttributeValue } |
501 | | Result |
502 | | SearchWithinRDN(Reader& rdn, |
503 | | GeneralNameType referenceIDType, |
504 | | Input referenceID, |
505 | | FallBackToSearchWithinSubject fallBackToEmailAddress, |
506 | | FallBackToSearchWithinSubject fallBackToCommonName, |
507 | | /*in/out*/ MatchResult& match) |
508 | 0 | { |
509 | 0 | do { |
510 | 0 | Input type; |
511 | 0 | uint8_t valueTag; |
512 | 0 | Input value; |
513 | 0 | Result rv = ReadAVA(rdn, type, valueTag, value); |
514 | 0 | if (rv != Success) { |
515 | 0 | return rv; |
516 | 0 | } |
517 | 0 | rv = MatchAVA(type, valueTag, value, referenceIDType, referenceID, |
518 | 0 | fallBackToEmailAddress, fallBackToCommonName, match); |
519 | 0 | if (rv != Success) { |
520 | 0 | return rv; |
521 | 0 | } |
522 | 0 | } while (!rdn.AtEnd()); |
523 | 0 |
|
524 | 0 | return Success; |
525 | 0 | } |
526 | | |
527 | | // AttributeTypeAndValue ::= SEQUENCE { |
528 | | // type AttributeType, |
529 | | // value AttributeValue } |
530 | | // |
531 | | // AttributeType ::= OBJECT IDENTIFIER |
532 | | // |
533 | | // AttributeValue ::= ANY -- DEFINED BY AttributeType |
534 | | // |
535 | | // DirectoryString ::= CHOICE { |
536 | | // teletexString TeletexString (SIZE (1..MAX)), |
537 | | // printableString PrintableString (SIZE (1..MAX)), |
538 | | // universalString UniversalString (SIZE (1..MAX)), |
539 | | // utf8String UTF8String (SIZE (1..MAX)), |
540 | | // bmpString BMPString (SIZE (1..MAX)) } |
541 | | Result |
542 | | MatchAVA(Input type, uint8_t valueEncodingTag, Input presentedID, |
543 | | GeneralNameType referenceIDType, |
544 | | Input referenceID, |
545 | | FallBackToSearchWithinSubject fallBackToEmailAddress, |
546 | | FallBackToSearchWithinSubject fallBackToCommonName, |
547 | | /*in/out*/ MatchResult& match) |
548 | 0 | { |
549 | 0 | // Try to match the CN as a DNSName or an IPAddress. |
550 | 0 | // |
551 | 0 | // id-at-commonName AttributeType ::= { id-at 3 } |
552 | 0 | // |
553 | 0 | // -- Naming attributes of type X520CommonName: |
554 | 0 | // -- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name)) |
555 | 0 | // -- |
556 | 0 | // -- Expanded to avoid parameterized type: |
557 | 0 | // X520CommonName ::= CHOICE { |
558 | 0 | // teletexString TeletexString (SIZE (1..ub-common-name)), |
559 | 0 | // printableString PrintableString (SIZE (1..ub-common-name)), |
560 | 0 | // universalString UniversalString (SIZE (1..ub-common-name)), |
561 | 0 | // utf8String UTF8String (SIZE (1..ub-common-name)), |
562 | 0 | // bmpString BMPString (SIZE (1..ub-common-name)) } |
563 | 0 | // |
564 | 0 | // python DottedOIDToCode.py id-at-commonName 2.5.4.3 |
565 | 0 | static const uint8_t id_at_commonName[] = { |
566 | 0 | 0x55, 0x04, 0x03 |
567 | 0 | }; |
568 | 0 | if (fallBackToCommonName == FallBackToSearchWithinSubject::Yes && |
569 | 0 | InputsAreEqual(type, Input(id_at_commonName))) { |
570 | 0 | // We might have previously found a match. Now that we've found another CN, |
571 | 0 | // we no longer consider that previous match to be a match, so "forget" about |
572 | 0 | // it. |
573 | 0 | match = MatchResult::NoNamesOfGivenType; |
574 | 0 |
|
575 | 0 | // PrintableString is a subset of ASCII that contains all the characters |
576 | 0 | // allowed in CN-IDs except '*'. Although '*' is illegal, there are many |
577 | 0 | // real-world certificates that are encoded this way, so we accept it. |
578 | 0 | // |
579 | 0 | // In the case of UTF8String, we rely on the fact that in UTF-8 the octets in |
580 | 0 | // a multi-byte encoding of a code point are always distinct from ASCII. Any |
581 | 0 | // non-ASCII byte in a UTF-8 string causes us to fail to match. We make no |
582 | 0 | // attempt to detect or report malformed UTF-8 (e.g. incomplete or overlong |
583 | 0 | // encodings of code points, or encodings of invalid code points). |
584 | 0 | // |
585 | 0 | // TeletexString is supported as long as it does not contain any escape |
586 | 0 | // sequences, which are not supported. We'll reject escape sequences as |
587 | 0 | // invalid characters in names, which means we only accept strings that are |
588 | 0 | // in the default character set, which is a superset of ASCII. Note that NSS |
589 | 0 | // actually treats TeletexString as ISO-8859-1. Many certificates that have |
590 | 0 | // wildcard CN-IDs (e.g. "*.example.com") use TeletexString because |
591 | 0 | // PrintableString is defined to not allow '*' and because, at one point in |
592 | 0 | // history, UTF8String was too new to use for compatibility reasons. |
593 | 0 | // |
594 | 0 | // UniversalString and BMPString are also deprecated, and they are a little |
595 | 0 | // harder to support because they are not single-byte ASCII superset |
596 | 0 | // encodings, so we don't bother. |
597 | 0 | if (valueEncodingTag != der::PrintableString && |
598 | 0 | valueEncodingTag != der::UTF8String && |
599 | 0 | valueEncodingTag != der::TeletexString) { |
600 | 0 | return Success; |
601 | 0 | } |
602 | 0 | |
603 | 0 | if (IsValidPresentedDNSID(presentedID)) { |
604 | 0 | MatchSubjectPresentedIDWithReferenceID(GeneralNameType::dNSName, |
605 | 0 | presentedID, referenceIDType, |
606 | 0 | referenceID, match); |
607 | 0 | } else { |
608 | 0 | // We don't match CN-IDs for IPv6 addresses. |
609 | 0 | // MatchSubjectPresentedIDWithReferenceID ensures that it won't match an |
610 | 0 | // IPv4 address with an IPv6 address, so we don't need to check that |
611 | 0 | // referenceID is an IPv4 address here. |
612 | 0 | uint8_t ipv4[4]; |
613 | 0 | if (ParseIPv4Address(presentedID, ipv4)) { |
614 | 0 | MatchSubjectPresentedIDWithReferenceID(GeneralNameType::iPAddress, |
615 | 0 | Input(ipv4), referenceIDType, |
616 | 0 | referenceID, match); |
617 | 0 | } |
618 | 0 | } |
619 | 0 |
|
620 | 0 | // Regardless of whether there was a match, we keep going in case we find |
621 | 0 | // another CN later. If we do find another one, then this match/mismatch |
622 | 0 | // will be ignored, because we only care about the most specific CN. |
623 | 0 |
|
624 | 0 | return Success; |
625 | 0 | } |
626 | 0 |
|
627 | 0 | // Match an email address against an emailAddress attribute in the |
628 | 0 | // subject. |
629 | 0 | // |
630 | 0 | // id-emailAddress AttributeType ::= { pkcs-9 1 } |
631 | 0 | // |
632 | 0 | // EmailAddress ::= IA5String (SIZE (1..ub-emailaddress-length)) |
633 | 0 | // |
634 | 0 | // python DottedOIDToCode.py id-emailAddress 1.2.840.113549.1.9.1 |
635 | 0 | static const uint8_t id_emailAddress[] = { |
636 | 0 | 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01 |
637 | 0 | }; |
638 | 0 | if (fallBackToEmailAddress == FallBackToSearchWithinSubject::Yes && |
639 | 0 | InputsAreEqual(type, Input(id_emailAddress))) { |
640 | 0 | if (referenceIDType == GeneralNameType::rfc822Name && |
641 | 0 | match == MatchResult::Match) { |
642 | 0 | // We already found a match; we don't need to match another one |
643 | 0 | return Success; |
644 | 0 | } |
645 | 0 | if (valueEncodingTag != der::IA5String) { |
646 | 0 | return Result::ERROR_BAD_DER; |
647 | 0 | } |
648 | 0 | return MatchPresentedIDWithReferenceID(GeneralNameType::rfc822Name, |
649 | 0 | presentedID, referenceIDType, |
650 | 0 | referenceID, match); |
651 | 0 | } |
652 | 0 | |
653 | 0 | return Success; |
654 | 0 | } |
655 | | |
656 | | void |
657 | | MatchSubjectPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
658 | | Input presentedID, |
659 | | GeneralNameType referenceIDType, |
660 | | Input referenceID, |
661 | | /*in/out*/ MatchResult& match) |
662 | 0 | { |
663 | 0 | Result rv = MatchPresentedIDWithReferenceID(presentedIDType, presentedID, |
664 | 0 | referenceIDType, referenceID, |
665 | 0 | match); |
666 | 0 | if (rv != Success) { |
667 | 0 | match = MatchResult::Mismatch; |
668 | 0 | } |
669 | 0 | } |
670 | | |
671 | | Result |
672 | | MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType, |
673 | | Input presentedID, |
674 | | GeneralNameType referenceIDType, |
675 | | Input referenceID, |
676 | | /*out*/ MatchResult& matchResult) |
677 | 0 | { |
678 | 0 | if (referenceIDType == GeneralNameType::nameConstraints) { |
679 | 0 | // matchResult is irrelevant when checking name constraints; only the |
680 | 0 | // pass/fail result of CheckPresentedIDConformsToConstraints matters. |
681 | 0 | return CheckPresentedIDConformsToConstraints(presentedIDType, presentedID, |
682 | 0 | referenceID); |
683 | 0 | } |
684 | 0 | |
685 | 0 | if (presentedIDType != referenceIDType) { |
686 | 0 | matchResult = MatchResult::Mismatch; |
687 | 0 | return Success; |
688 | 0 | } |
689 | 0 | |
690 | 0 | Result rv; |
691 | 0 | bool foundMatch; |
692 | 0 |
|
693 | 0 | switch (referenceIDType) { |
694 | 0 | case GeneralNameType::dNSName: |
695 | 0 | rv = MatchPresentedDNSIDWithReferenceDNSID( |
696 | 0 | presentedID, AllowWildcards::Yes, |
697 | 0 | AllowDotlessSubdomainMatches::Yes, IDRole::ReferenceID, |
698 | 0 | referenceID, foundMatch); |
699 | 0 | break; |
700 | 0 |
|
701 | 0 | case GeneralNameType::iPAddress: |
702 | 0 | foundMatch = InputsAreEqual(presentedID, referenceID); |
703 | 0 | rv = Success; |
704 | 0 | break; |
705 | 0 |
|
706 | 0 | case GeneralNameType::rfc822Name: |
707 | 0 | rv = MatchPresentedRFC822NameWithReferenceRFC822Name( |
708 | 0 | presentedID, IDRole::ReferenceID, referenceID, foundMatch); |
709 | 0 | break; |
710 | 0 |
|
711 | 0 | case GeneralNameType::directoryName: |
712 | 0 | // TODO: At some point, we may add APIs for matching DirectoryNames. |
713 | 0 | // fall through |
714 | 0 |
|
715 | 0 | case GeneralNameType::otherName: // fall through |
716 | 0 | case GeneralNameType::x400Address: // fall through |
717 | 0 | case GeneralNameType::ediPartyName: // fall through |
718 | 0 | case GeneralNameType::uniformResourceIdentifier: // fall through |
719 | 0 | case GeneralNameType::registeredID: // fall through |
720 | 0 | case GeneralNameType::nameConstraints: |
721 | 0 | return NotReached("unexpected nameType for SearchType::Match", |
722 | 0 | Result::FATAL_ERROR_INVALID_ARGS); |
723 | 0 | |
724 | 0 | MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM |
725 | 0 | } |
726 | 0 | |
727 | 0 | if (rv != Success) { |
728 | 0 | return rv; |
729 | 0 | } |
730 | 0 | matchResult = foundMatch ? MatchResult::Match : MatchResult::Mismatch; |
731 | 0 | return Success; |
732 | 0 | } |
733 | | |
734 | | enum class NameConstraintsSubtrees : uint8_t |
735 | | { |
736 | | permittedSubtrees = der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 0, |
737 | | excludedSubtrees = der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 1 |
738 | | }; |
739 | | |
740 | | Result CheckPresentedIDConformsToNameConstraintsSubtrees( |
741 | | GeneralNameType presentedIDType, |
742 | | Input presentedID, |
743 | | Reader& nameConstraints, |
744 | | NameConstraintsSubtrees subtreesType); |
745 | | Result MatchPresentedIPAddressWithConstraint(Input presentedID, |
746 | | Input iPAddressConstraint, |
747 | | /*out*/ bool& foundMatch); |
748 | | Result MatchPresentedDirectoryNameWithConstraint( |
749 | | NameConstraintsSubtrees subtreesType, Input presentedID, |
750 | | Input directoryNameConstraint, /*out*/ bool& matches); |
751 | | |
752 | | Result |
753 | | CheckPresentedIDConformsToConstraints( |
754 | | GeneralNameType presentedIDType, |
755 | | Input presentedID, |
756 | | Input encodedNameConstraints) |
757 | 0 | { |
758 | 0 | // NameConstraints ::= SEQUENCE { |
759 | 0 | // permittedSubtrees [0] GeneralSubtrees OPTIONAL, |
760 | 0 | // excludedSubtrees [1] GeneralSubtrees OPTIONAL } |
761 | 0 | Reader nameConstraints; |
762 | 0 | Result rv = der::ExpectTagAndGetValueAtEnd(encodedNameConstraints, |
763 | 0 | der::SEQUENCE, nameConstraints); |
764 | 0 | if (rv != Success) { |
765 | 0 | return rv; |
766 | 0 | } |
767 | 0 | |
768 | 0 | // RFC 5280 says "Conforming CAs MUST NOT issue certificates where name |
769 | 0 | // constraints is an empty sequence. That is, either the permittedSubtrees |
770 | 0 | // field or the excludedSubtrees MUST be present." |
771 | 0 | if (nameConstraints.AtEnd()) { |
772 | 0 | return Result::ERROR_BAD_DER; |
773 | 0 | } |
774 | 0 | |
775 | 0 | rv = CheckPresentedIDConformsToNameConstraintsSubtrees( |
776 | 0 | presentedIDType, presentedID, nameConstraints, |
777 | 0 | NameConstraintsSubtrees::permittedSubtrees); |
778 | 0 | if (rv != Success) { |
779 | 0 | return rv; |
780 | 0 | } |
781 | 0 | |
782 | 0 | rv = CheckPresentedIDConformsToNameConstraintsSubtrees( |
783 | 0 | presentedIDType, presentedID, nameConstraints, |
784 | 0 | NameConstraintsSubtrees::excludedSubtrees); |
785 | 0 | if (rv != Success) { |
786 | 0 | return rv; |
787 | 0 | } |
788 | 0 | |
789 | 0 | return der::End(nameConstraints); |
790 | 0 | } |
791 | | |
792 | | Result |
793 | | CheckPresentedIDConformsToNameConstraintsSubtrees( |
794 | | GeneralNameType presentedIDType, |
795 | | Input presentedID, |
796 | | Reader& nameConstraints, |
797 | | NameConstraintsSubtrees subtreesType) |
798 | 0 | { |
799 | 0 | if (!nameConstraints.Peek(static_cast<uint8_t>(subtreesType))) { |
800 | 0 | return Success; |
801 | 0 | } |
802 | 0 | |
803 | 0 | Reader subtrees; |
804 | 0 | Result rv = der::ExpectTagAndGetValue(nameConstraints, |
805 | 0 | static_cast<uint8_t>(subtreesType), |
806 | 0 | subtrees); |
807 | 0 | if (rv != Success) { |
808 | 0 | return rv; |
809 | 0 | } |
810 | 0 | |
811 | 0 | bool hasPermittedSubtreesMatch = false; |
812 | 0 | bool hasPermittedSubtreesMismatch = false; |
813 | 0 |
|
814 | 0 | // GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree |
815 | 0 | // |
816 | 0 | // do { ... } while(...) because subtrees isn't allowed to be empty. |
817 | 0 | do { |
818 | 0 | // GeneralSubtree ::= SEQUENCE { |
819 | 0 | // base GeneralName, |
820 | 0 | // minimum [0] BaseDistance DEFAULT 0, |
821 | 0 | // maximum [1] BaseDistance OPTIONAL } |
822 | 0 | Reader subtree; |
823 | 0 | rv = ExpectTagAndGetValue(subtrees, der::SEQUENCE, subtree); |
824 | 0 | if (rv != Success) { |
825 | 0 | return rv; |
826 | 0 | } |
827 | 0 | GeneralNameType nameConstraintType; |
828 | 0 | Input base; |
829 | 0 | rv = ReadGeneralName(subtree, nameConstraintType, base); |
830 | 0 | if (rv != Success) { |
831 | 0 | return rv; |
832 | 0 | } |
833 | 0 | // http://tools.ietf.org/html/rfc5280#section-4.2.1.10: "Within this |
834 | 0 | // profile, the minimum and maximum fields are not used with any name |
835 | 0 | // forms, thus, the minimum MUST be zero, and maximum MUST be absent." |
836 | 0 | // |
837 | 0 | // Since the default value isn't allowed to be encoded according to the DER |
838 | 0 | // encoding rules for DEFAULT, this is equivalent to saying that neither |
839 | 0 | // minimum or maximum must be encoded. |
840 | 0 | rv = der::End(subtree); |
841 | 0 | if (rv != Success) { |
842 | 0 | return rv; |
843 | 0 | } |
844 | 0 | |
845 | 0 | if (presentedIDType == nameConstraintType) { |
846 | 0 | bool matches; |
847 | 0 |
|
848 | 0 | switch (presentedIDType) { |
849 | 0 | case GeneralNameType::dNSName: |
850 | 0 | rv = MatchPresentedDNSIDWithReferenceDNSID( |
851 | 0 | presentedID, AllowWildcards::Yes, |
852 | 0 | AllowDotlessSubdomainMatches::Yes, IDRole::NameConstraint, |
853 | 0 | base, matches); |
854 | 0 | if (rv != Success) { |
855 | 0 | return rv; |
856 | 0 | } |
857 | 0 | break; |
858 | 0 |
|
859 | 0 | case GeneralNameType::iPAddress: |
860 | 0 | rv = MatchPresentedIPAddressWithConstraint(presentedID, base, |
861 | 0 | matches); |
862 | 0 | if (rv != Success) { |
863 | 0 | return rv; |
864 | 0 | } |
865 | 0 | break; |
866 | 0 |
|
867 | 0 | case GeneralNameType::directoryName: |
868 | 0 | rv = MatchPresentedDirectoryNameWithConstraint(subtreesType, |
869 | 0 | presentedID, base, |
870 | 0 | matches); |
871 | 0 | if (rv != Success) { |
872 | 0 | return rv; |
873 | 0 | } |
874 | 0 | break; |
875 | 0 |
|
876 | 0 | case GeneralNameType::rfc822Name: |
877 | 0 | rv = MatchPresentedRFC822NameWithReferenceRFC822Name( |
878 | 0 | presentedID, IDRole::NameConstraint, base, matches); |
879 | 0 | if (rv != Success) { |
880 | 0 | return rv; |
881 | 0 | } |
882 | 0 | break; |
883 | 0 |
|
884 | 0 | // RFC 5280 says "Conforming CAs [...] SHOULD NOT impose name |
885 | 0 | // constraints on the x400Address, ediPartyName, or registeredID |
886 | 0 | // name forms. It also says "Applications conforming to this profile |
887 | 0 | // [...] SHOULD be able to process name constraints that are imposed |
888 | 0 | // on [...] uniformResourceIdentifier [...]", but we don't bother. |
889 | 0 | // |
890 | 0 | // TODO: Ask to have spec updated to say ""Conforming CAs [...] SHOULD |
891 | 0 | // NOT impose name constraints on the otherName, x400Address, |
892 | 0 | // ediPartyName, uniformResourceIdentifier, or registeredID name |
893 | 0 | // forms." |
894 | 0 | case GeneralNameType::otherName: // fall through |
895 | 0 | case GeneralNameType::x400Address: // fall through |
896 | 0 | case GeneralNameType::ediPartyName: // fall through |
897 | 0 | case GeneralNameType::uniformResourceIdentifier: // fall through |
898 | 0 | case GeneralNameType::registeredID: // fall through |
899 | 0 | return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
900 | 0 |
|
901 | 0 | case GeneralNameType::nameConstraints: |
902 | 0 | return NotReached("invalid presentedIDType", |
903 | 0 | Result::FATAL_ERROR_LIBRARY_FAILURE); |
904 | 0 | |
905 | 0 | MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM |
906 | 0 | } |
907 | 0 | |
908 | 0 | switch (subtreesType) { |
909 | 0 | case NameConstraintsSubtrees::permittedSubtrees: |
910 | 0 | if (matches) { |
911 | 0 | hasPermittedSubtreesMatch = true; |
912 | 0 | } else { |
913 | 0 | hasPermittedSubtreesMismatch = true; |
914 | 0 | } |
915 | 0 | break; |
916 | 0 | case NameConstraintsSubtrees::excludedSubtrees: |
917 | 0 | if (matches) { |
918 | 0 | return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
919 | 0 | } |
920 | 0 | break; |
921 | 0 | } |
922 | 0 | } |
923 | 0 | } while (!subtrees.AtEnd()); |
924 | 0 |
|
925 | 0 | if (hasPermittedSubtreesMismatch && !hasPermittedSubtreesMatch) { |
926 | 0 | // If there was any entry of the given type in permittedSubtrees, then it |
927 | 0 | // required that at least one of them must match. Since none of them did, |
928 | 0 | // we have a failure. |
929 | 0 | return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
930 | 0 | } |
931 | 0 | |
932 | 0 | return Success; |
933 | 0 | } |
934 | | |
935 | | // We do not distinguish between a syntactically-invalid presentedDNSID and one |
936 | | // that is syntactically valid but does not match referenceDNSID; in both |
937 | | // cases, the result is false. |
938 | | // |
939 | | // We assume that both presentedDNSID and referenceDNSID are encoded in such a |
940 | | // way that US-ASCII (7-bit) characters are encoded in one byte and no encoding |
941 | | // of a non-US-ASCII character contains a code point in the range 0-127. For |
942 | | // example, UTF-8 is OK but UTF-16 is not. |
943 | | // |
944 | | // RFC6125 says that a wildcard label may be of the form <x>*<y>.<DNSID>, where |
945 | | // <x> and/or <y> may be empty. However, NSS requires <y> to be empty, and we |
946 | | // follow NSS's stricter policy by accepting wildcards only of the form |
947 | | // <x>*.<DNSID>, where <x> may be empty. |
948 | | // |
949 | | // An relative presented DNS ID matches both an absolute reference ID and a |
950 | | // relative reference ID. Absolute presented DNS IDs are not supported: |
951 | | // |
952 | | // Presented ID Reference ID Result |
953 | | // ------------------------------------- |
954 | | // example.com example.com Match |
955 | | // example.com. example.com Mismatch |
956 | | // example.com example.com. Match |
957 | | // example.com. example.com. Mismatch |
958 | | // |
959 | | // There are more subtleties documented inline in the code. |
960 | | // |
961 | | // Name constraints /////////////////////////////////////////////////////////// |
962 | | // |
963 | | // This is all RFC 5280 has to say about DNSName constraints: |
964 | | // |
965 | | // DNS name restrictions are expressed as host.example.com. Any DNS |
966 | | // name that can be constructed by simply adding zero or more labels to |
967 | | // the left-hand side of the name satisfies the name constraint. For |
968 | | // example, www.host.example.com would satisfy the constraint but |
969 | | // host1.example.com would not. |
970 | | // |
971 | | // This lack of specificity has lead to a lot of uncertainty regarding |
972 | | // subdomain matching. In particular, the following questions have been |
973 | | // raised and answered: |
974 | | // |
975 | | // Q: Does a presented identifier equal (case insensitive) to the name |
976 | | // constraint match the constraint? For example, does the presented |
977 | | // ID "host.example.com" match a "host.example.com" constraint? |
978 | | // A: Yes. RFC5280 says "by simply adding zero or more labels" and this |
979 | | // is the case of adding zero labels. |
980 | | // |
981 | | // Q: When the name constraint does not start with ".", do subdomain |
982 | | // presented identifiers match it? For example, does the presented |
983 | | // ID "www.host.example.com" match a "host.example.com" constraint? |
984 | | // A: Yes. RFC5280 says "by simply adding zero or more labels" and this |
985 | | // is the case of adding more than zero labels. The example is the |
986 | | // one from RFC 5280. |
987 | | // |
988 | | // Q: When the name constraint does not start with ".", does a |
989 | | // non-subdomain prefix match it? For example, does "bigfoo.bar.com" |
990 | | // match "foo.bar.com"? [4] |
991 | | // A: No. We interpret RFC 5280's language of "adding zero or more labels" |
992 | | // to mean that whole labels must be prefixed. |
993 | | // |
994 | | // (Note that the above three scenarios are the same as the RFC 6265 |
995 | | // domain matching rules [0].) |
996 | | // |
997 | | // Q: Is a name constraint that starts with "." valid, and if so, what |
998 | | // semantics does it have? For example, does a presented ID of |
999 | | // "www.example.com" match a constraint of ".example.com"? Does a |
1000 | | // presented ID of "example.com" match a constraint of ".example.com"? |
1001 | | // A: This implementation, NSS[1], and SChannel[2] all support a |
1002 | | // leading ".", but OpenSSL[3] does not yet. Amongst the |
1003 | | // implementations that support it, a leading "." is legal and means |
1004 | | // the same thing as when the "." is omitted, EXCEPT that a |
1005 | | // presented identifier equal (case insensitive) to the name |
1006 | | // constraint is not matched; i.e. presented DNSName identifiers |
1007 | | // must be subdomains. Some CAs in Mozilla's CA program (e.g. HARICA) |
1008 | | // have name constraints with the leading "." in their root |
1009 | | // certificates. The name constraints imposed on DCISS by Mozilla also |
1010 | | // have the it, so supporting this is a requirement for backward |
1011 | | // compatibility, even if it is not yet standardized. So, for example, a |
1012 | | // presented ID of "www.example.com" matches a constraint of |
1013 | | // ".example.com" but a presented ID of "example.com" does not. |
1014 | | // |
1015 | | // Q: Is there a way to prevent subdomain matches? |
1016 | | // A: Yes. |
1017 | | // |
1018 | | // Some people have proposed that dNSName constraints that do not |
1019 | | // start with a "." should be restricted to exact (case insensitive) |
1020 | | // matches. However, such a change of semantics from what RFC5280 |
1021 | | // specifies would be a non-backward-compatible change in the case of |
1022 | | // permittedSubtrees constraints, and it would be a security issue for |
1023 | | // excludedSubtrees constraints. |
1024 | | // |
1025 | | // However, it can be done with a combination of permittedSubtrees and |
1026 | | // excludedSubtrees, e.g. "example.com" in permittedSubtrees and |
1027 | | // ".example.com" in excudedSubtrees. |
1028 | | // |
1029 | | // Q: Are name constraints allowed to be specified as absolute names? |
1030 | | // For example, does a presented ID of "example.com" match a name |
1031 | | // constraint of "example.com." and vice versa. |
1032 | | // A: Absolute names are not supported as presented IDs or name |
1033 | | // constraints. Only reference IDs may be absolute. |
1034 | | // |
1035 | | // Q: Is "" a valid DNSName constraints? If so, what does it mean? |
1036 | | // A: Yes. Any valid presented DNSName can be formed "by simply adding zero |
1037 | | // or more labels to the left-hand side" of "". In particular, an |
1038 | | // excludedSubtrees DNSName constraint of "" forbids all DNSNames. |
1039 | | // |
1040 | | // Q: Is "." a valid DNSName constraints? If so, what does it mean? |
1041 | | // A: No, because absolute names are not allowed (see above). |
1042 | | // |
1043 | | // [0] RFC 6265 (Cookies) Domain Matching rules: |
1044 | | // http://tools.ietf.org/html/rfc6265#section-5.1.3 |
1045 | | // [1] NSS source code: |
1046 | | // https://mxr.mozilla.org/nss/source/lib/certdb/genname.c?rev=2a7348f013cb#1209 |
1047 | | // [2] Description of SChannel's behavior from Microsoft: |
1048 | | // http://www.imc.org/ietf-pkix/mail-archive/msg04668.html |
1049 | | // [3] Proposal to add such support to OpenSSL: |
1050 | | // http://www.mail-archive.com/openssl-dev%40openssl.org/msg36204.html |
1051 | | // https://rt.openssl.org/Ticket/Display.html?id=3562 |
1052 | | // [4] Feedback on the lack of clarify in the definition that never got |
1053 | | // incorporated into the spec: |
1054 | | // https://www.ietf.org/mail-archive/web/pkix/current/msg21192.html |
1055 | | Result |
1056 | | MatchPresentedDNSIDWithReferenceDNSID( |
1057 | | Input presentedDNSID, |
1058 | | AllowWildcards allowWildcards, |
1059 | | AllowDotlessSubdomainMatches allowDotlessSubdomainMatches, |
1060 | | IDRole referenceDNSIDRole, |
1061 | | Input referenceDNSID, |
1062 | | /*out*/ bool& matches) |
1063 | 0 | { |
1064 | 0 | if (!IsValidDNSID(presentedDNSID, IDRole::PresentedID, allowWildcards)) { |
1065 | 0 | return Result::ERROR_BAD_DER; |
1066 | 0 | } |
1067 | 0 | |
1068 | 0 | if (!IsValidDNSID(referenceDNSID, referenceDNSIDRole, AllowWildcards::No)) { |
1069 | 0 | return Result::ERROR_BAD_DER; |
1070 | 0 | } |
1071 | 0 | |
1072 | 0 | Reader presented(presentedDNSID); |
1073 | 0 | Reader reference(referenceDNSID); |
1074 | 0 |
|
1075 | 0 | switch (referenceDNSIDRole) |
1076 | 0 | { |
1077 | 0 | case IDRole::ReferenceID: |
1078 | 0 | break; |
1079 | 0 |
|
1080 | 0 | case IDRole::NameConstraint: |
1081 | 0 | { |
1082 | 0 | if (presentedDNSID.GetLength() > referenceDNSID.GetLength()) { |
1083 | 0 | if (referenceDNSID.GetLength() == 0) { |
1084 | 0 | // An empty constraint matches everything. |
1085 | 0 | matches = true; |
1086 | 0 | return Success; |
1087 | 0 | } |
1088 | 0 | // If the reference ID starts with a dot then skip the prefix of |
1089 | 0 | // of the presented ID and start the comparison at the position of that |
1090 | 0 | // dot. Examples: |
1091 | 0 | // |
1092 | 0 | // Matches Doesn't Match |
1093 | 0 | // ----------------------------------------------------------- |
1094 | 0 | // original presented ID: www.example.com badexample.com |
1095 | 0 | // skipped: www ba |
1096 | 0 | // presented ID w/o prefix: .example.com dexample.com |
1097 | 0 | // reference ID: .example.com .example.com |
1098 | 0 | // |
1099 | 0 | // If the reference ID does not start with a dot then we skip the |
1100 | 0 | // prefix of the presented ID but also verify that the prefix ends with |
1101 | 0 | // a dot. Examples: |
1102 | 0 | // |
1103 | 0 | // Matches Doesn't Match |
1104 | 0 | // ----------------------------------------------------------- |
1105 | 0 | // original presented ID: www.example.com badexample.com |
1106 | 0 | // skipped: www ba |
1107 | 0 | // must be '.': . d |
1108 | 0 | // presented ID w/o prefix: example.com example.com |
1109 | 0 | // reference ID: example.com example.com |
1110 | 0 | // |
1111 | 0 | if (reference.Peek('.')) { |
1112 | 0 | if (presented.Skip(static_cast<Input::size_type>( |
1113 | 0 | presentedDNSID.GetLength() - |
1114 | 0 | referenceDNSID.GetLength())) != Success) { |
1115 | 0 | return NotReached("skipping subdomain failed", |
1116 | 0 | Result::FATAL_ERROR_LIBRARY_FAILURE); |
1117 | 0 | } |
1118 | 0 | } else if (allowDotlessSubdomainMatches == |
1119 | 0 | AllowDotlessSubdomainMatches::Yes) { |
1120 | 0 | if (presented.Skip(static_cast<Input::size_type>( |
1121 | 0 | presentedDNSID.GetLength() - |
1122 | 0 | referenceDNSID.GetLength() - 1)) != Success) { |
1123 | 0 | return NotReached("skipping subdomains failed", |
1124 | 0 | Result::FATAL_ERROR_LIBRARY_FAILURE); |
1125 | 0 | } |
1126 | 0 | uint8_t b; |
1127 | 0 | if (presented.Read(b) != Success) { |
1128 | 0 | return NotReached("reading from presentedDNSID failed", |
1129 | 0 | Result::FATAL_ERROR_LIBRARY_FAILURE); |
1130 | 0 | } |
1131 | 0 | if (b != '.') { |
1132 | 0 | matches = false; |
1133 | 0 | return Success; |
1134 | 0 | } |
1135 | 0 | } |
1136 | 0 | } |
1137 | 0 | break; |
1138 | 0 | } |
1139 | 0 |
|
1140 | 0 | case IDRole::PresentedID: // fall through |
1141 | 0 | return NotReached("IDRole::PresentedID is not a valid referenceDNSIDRole", |
1142 | 0 | Result::FATAL_ERROR_INVALID_ARGS); |
1143 | 0 | } |
1144 | 0 | |
1145 | 0 | // We only allow wildcard labels that consist only of '*'. |
1146 | 0 | if (presented.Peek('*')) { |
1147 | 0 | if (presented.Skip(1) != Success) { |
1148 | 0 | return NotReached("Skipping '*' failed", |
1149 | 0 | Result::FATAL_ERROR_LIBRARY_FAILURE); |
1150 | 0 | } |
1151 | 0 | do { |
1152 | 0 | // This will happen if reference is a single, relative label |
1153 | 0 | if (reference.AtEnd()) { |
1154 | 0 | matches = false; |
1155 | 0 | return Success; |
1156 | 0 | } |
1157 | 0 | uint8_t referenceByte; |
1158 | 0 | if (reference.Read(referenceByte) != Success) { |
1159 | 0 | return NotReached("invalid reference ID", |
1160 | 0 | Result::FATAL_ERROR_INVALID_ARGS); |
1161 | 0 | } |
1162 | 0 | } while (!reference.Peek('.')); |
1163 | 0 | } |
1164 | 0 |
|
1165 | 0 | for (;;) { |
1166 | 0 | uint8_t presentedByte; |
1167 | 0 | if (presented.Read(presentedByte) != Success) { |
1168 | 0 | matches = false; |
1169 | 0 | return Success; |
1170 | 0 | } |
1171 | 0 | uint8_t referenceByte; |
1172 | 0 | if (reference.Read(referenceByte) != Success) { |
1173 | 0 | matches = false; |
1174 | 0 | return Success; |
1175 | 0 | } |
1176 | 0 | if (LocaleInsensitveToLower(presentedByte) != |
1177 | 0 | LocaleInsensitveToLower(referenceByte)) { |
1178 | 0 | matches = false; |
1179 | 0 | return Success; |
1180 | 0 | } |
1181 | 0 | if (presented.AtEnd()) { |
1182 | 0 | // Don't allow presented IDs to be absolute. |
1183 | 0 | if (presentedByte == '.') { |
1184 | 0 | return Result::ERROR_BAD_DER; |
1185 | 0 | } |
1186 | 0 | break; |
1187 | 0 | } |
1188 | 0 | } |
1189 | 0 |
|
1190 | 0 | // Allow a relative presented DNS ID to match an absolute reference DNS ID, |
1191 | 0 | // unless we're matching a name constraint. |
1192 | 0 | if (!reference.AtEnd()) { |
1193 | 0 | if (referenceDNSIDRole != IDRole::NameConstraint) { |
1194 | 0 | uint8_t referenceByte; |
1195 | 0 | if (reference.Read(referenceByte) != Success) { |
1196 | 0 | return NotReached("read failed but not at end", |
1197 | 0 | Result::FATAL_ERROR_LIBRARY_FAILURE); |
1198 | 0 | } |
1199 | 0 | if (referenceByte != '.') { |
1200 | 0 | matches = false; |
1201 | 0 | return Success; |
1202 | 0 | } |
1203 | 0 | } |
1204 | 0 | if (!reference.AtEnd()) { |
1205 | 0 | matches = false; |
1206 | 0 | return Success; |
1207 | 0 | } |
1208 | 0 | } |
1209 | 0 | |
1210 | 0 | matches = true; |
1211 | 0 | return Success; |
1212 | 0 | } |
1213 | | |
1214 | | // https://tools.ietf.org/html/rfc5280#section-4.2.1.10 says: |
1215 | | // |
1216 | | // For IPv4 addresses, the iPAddress field of GeneralName MUST contain |
1217 | | // eight (8) octets, encoded in the style of RFC 4632 (CIDR) to represent |
1218 | | // an address range [RFC4632]. For IPv6 addresses, the iPAddress field |
1219 | | // MUST contain 32 octets similarly encoded. For example, a name |
1220 | | // constraint for "class C" subnet 192.0.2.0 is represented as the |
1221 | | // octets C0 00 02 00 FF FF FF 00, representing the CIDR notation |
1222 | | // 192.0.2.0/24 (mask 255.255.255.0). |
1223 | | Result |
1224 | | MatchPresentedIPAddressWithConstraint(Input presentedID, |
1225 | | Input iPAddressConstraint, |
1226 | | /*out*/ bool& foundMatch) |
1227 | 0 | { |
1228 | 0 | if (presentedID.GetLength() != 4 && presentedID.GetLength() != 16) { |
1229 | 0 | return Result::ERROR_BAD_DER; |
1230 | 0 | } |
1231 | 0 | if (iPAddressConstraint.GetLength() != 8 && |
1232 | 0 | iPAddressConstraint.GetLength() != 32) { |
1233 | 0 | return Result::ERROR_BAD_DER; |
1234 | 0 | } |
1235 | 0 | |
1236 | 0 | // an IPv4 address never matches an IPv6 constraint, and vice versa. |
1237 | 0 | if (presentedID.GetLength() * 2 != iPAddressConstraint.GetLength()) { |
1238 | 0 | foundMatch = false; |
1239 | 0 | return Success; |
1240 | 0 | } |
1241 | 0 | |
1242 | 0 | Reader constraint(iPAddressConstraint); |
1243 | 0 | Reader constraintAddress; |
1244 | 0 | Result rv = constraint.Skip(iPAddressConstraint.GetLength() / 2u, |
1245 | 0 | constraintAddress); |
1246 | 0 | if (rv != Success) { |
1247 | 0 | return rv; |
1248 | 0 | } |
1249 | 0 | Reader constraintMask; |
1250 | 0 | rv = constraint.Skip(iPAddressConstraint.GetLength() / 2u, constraintMask); |
1251 | 0 | if (rv != Success) { |
1252 | 0 | return rv; |
1253 | 0 | } |
1254 | 0 | rv = der::End(constraint); |
1255 | 0 | if (rv != Success) { |
1256 | 0 | return rv; |
1257 | 0 | } |
1258 | 0 | |
1259 | 0 | Reader presented(presentedID); |
1260 | 0 | do { |
1261 | 0 | uint8_t presentedByte; |
1262 | 0 | rv = presented.Read(presentedByte); |
1263 | 0 | if (rv != Success) { |
1264 | 0 | return rv; |
1265 | 0 | } |
1266 | 0 | uint8_t constraintAddressByte; |
1267 | 0 | rv = constraintAddress.Read(constraintAddressByte); |
1268 | 0 | if (rv != Success) { |
1269 | 0 | return rv; |
1270 | 0 | } |
1271 | 0 | uint8_t constraintMaskByte; |
1272 | 0 | rv = constraintMask.Read(constraintMaskByte); |
1273 | 0 | if (rv != Success) { |
1274 | 0 | return rv; |
1275 | 0 | } |
1276 | 0 | foundMatch = |
1277 | 0 | ((presentedByte ^ constraintAddressByte) & constraintMaskByte) == 0; |
1278 | 0 | } while (foundMatch && !presented.AtEnd()); |
1279 | 0 |
|
1280 | 0 | return Success; |
1281 | 0 | } |
1282 | | |
1283 | | // AttributeTypeAndValue ::= SEQUENCE { |
1284 | | // type AttributeType, |
1285 | | // value AttributeValue } |
1286 | | // |
1287 | | // AttributeType ::= OBJECT IDENTIFIER |
1288 | | // |
1289 | | // AttributeValue ::= ANY -- DEFINED BY AttributeType |
1290 | | Result |
1291 | | ReadAVA(Reader& rdn, |
1292 | | /*out*/ Input& type, |
1293 | | /*out*/ uint8_t& valueTag, |
1294 | | /*out*/ Input& value) |
1295 | 0 | { |
1296 | 0 | return der::Nested(rdn, der::SEQUENCE, [&](Reader& ava) -> Result { |
1297 | 0 | Result rv = der::ExpectTagAndGetValue(ava, der::OIDTag, type); |
1298 | 0 | if (rv != Success) { |
1299 | 0 | return rv; |
1300 | 0 | } |
1301 | 0 | rv = der::ReadTagAndGetValue(ava, valueTag, value); |
1302 | 0 | if (rv != Success) { |
1303 | 0 | return rv; |
1304 | 0 | } |
1305 | 0 | return Success; |
1306 | 0 | }); |
1307 | 0 | } |
1308 | | |
1309 | | // Names are sequences of RDNs. RDNS are sets of AVAs. That means that RDNs are |
1310 | | // unordered, so in theory we should match RDNs with equivalent AVAs that are |
1311 | | // in different orders. Within the AVAs are DirectoryNames that are supposed to |
1312 | | // be compared according to LDAP stringprep normalization rules (e.g. |
1313 | | // normalizing whitespace), consideration of different character encodings, |
1314 | | // etc. Indeed, RFC 5280 says we MUST deal with all of that. |
1315 | | // |
1316 | | // In practice, many implementations, including NSS, only match Names in a way |
1317 | | // that only meets a subset of the requirements of RFC 5280. Those |
1318 | | // normalization and character encoding conversion steps appear to be |
1319 | | // unnecessary for processing real-world certificates, based on experience from |
1320 | | // having used NSS in Firefox for many years. |
1321 | | // |
1322 | | // RFC 5280 also says "CAs issuing certificates with a restriction of the form |
1323 | | // directoryName SHOULD NOT rely on implementation of the full |
1324 | | // ISO DN name comparison algorithm. This implies name restrictions MUST |
1325 | | // be stated identically to the encoding used in the subject field or |
1326 | | // subjectAltName extension." It goes on to say, in the security |
1327 | | // considerations: |
1328 | | // |
1329 | | // In addition, name constraints for distinguished names MUST be stated |
1330 | | // identically to the encoding used in the subject field or |
1331 | | // subjectAltName extension. If not, then name constraints stated as |
1332 | | // excludedSubtrees will not match and invalid paths will be accepted |
1333 | | // and name constraints expressed as permittedSubtrees will not match |
1334 | | // and valid paths will be rejected. To avoid acceptance of invalid |
1335 | | // paths, CAs SHOULD state name constraints for distinguished names as |
1336 | | // permittedSubtrees wherever possible. |
1337 | | // |
1338 | | // For permittedSubtrees, the MUST-level requirement is relaxed for |
1339 | | // compatibility in the case of PrintableString and UTF8String. That is, if a |
1340 | | // name constraint has been encoded using UTF8String and the presented ID has |
1341 | | // been encoded with a PrintableString (or vice-versa), they are considered to |
1342 | | // match if they are equal everywhere except for the tag identifying the |
1343 | | // encoding. See bug 1150114. |
1344 | | // |
1345 | | // For excludedSubtrees, we simply prohibit any non-empty directoryName |
1346 | | // constraint to ensure we are not being too lenient. We support empty |
1347 | | // DirectoryName constraints in excludedSubtrees so that a CA can say "Do not |
1348 | | // allow any DirectoryNames in issued certificates." |
1349 | | Result |
1350 | | MatchPresentedDirectoryNameWithConstraint(NameConstraintsSubtrees subtreesType, |
1351 | | Input presentedID, |
1352 | | Input directoryNameConstraint, |
1353 | | /*out*/ bool& matches) |
1354 | 0 | { |
1355 | 0 | Reader constraintRDNs; |
1356 | 0 | Result rv = der::ExpectTagAndGetValueAtEnd(directoryNameConstraint, |
1357 | 0 | der::SEQUENCE, constraintRDNs); |
1358 | 0 | if (rv != Success) { |
1359 | 0 | return rv; |
1360 | 0 | } |
1361 | 0 | Reader presentedRDNs; |
1362 | 0 | rv = der::ExpectTagAndGetValueAtEnd(presentedID, der::SEQUENCE, |
1363 | 0 | presentedRDNs); |
1364 | 0 | if (rv != Success) { |
1365 | 0 | return rv; |
1366 | 0 | } |
1367 | 0 | |
1368 | 0 | switch (subtreesType) { |
1369 | 0 | case NameConstraintsSubtrees::permittedSubtrees: |
1370 | 0 | break; // dealt with below |
1371 | 0 | case NameConstraintsSubtrees::excludedSubtrees: |
1372 | 0 | if (!constraintRDNs.AtEnd() || !presentedRDNs.AtEnd()) { |
1373 | 0 | return Result::ERROR_CERT_NOT_IN_NAME_SPACE; |
1374 | 0 | } |
1375 | 0 | matches = true; |
1376 | 0 | return Success; |
1377 | 0 | } |
1378 | 0 | |
1379 | 0 | for (;;) { |
1380 | 0 | // The AVAs have to be fully equal, but the constraint RDNs just need to be |
1381 | 0 | // a prefix of the presented RDNs. |
1382 | 0 | if (constraintRDNs.AtEnd()) { |
1383 | 0 | matches = true; |
1384 | 0 | return Success; |
1385 | 0 | } |
1386 | 0 | if (presentedRDNs.AtEnd()) { |
1387 | 0 | matches = false; |
1388 | 0 | return Success; |
1389 | 0 | } |
1390 | 0 | Reader constraintRDN; |
1391 | 0 | rv = der::ExpectTagAndGetValue(constraintRDNs, der::SET, constraintRDN); |
1392 | 0 | if (rv != Success) { |
1393 | 0 | return rv; |
1394 | 0 | } |
1395 | 0 | Reader presentedRDN; |
1396 | 0 | rv = der::ExpectTagAndGetValue(presentedRDNs, der::SET, presentedRDN); |
1397 | 0 | if (rv != Success) { |
1398 | 0 | return rv; |
1399 | 0 | } |
1400 | 0 | while (!constraintRDN.AtEnd() && !presentedRDN.AtEnd()) { |
1401 | 0 | Input constraintType; |
1402 | 0 | uint8_t constraintValueTag; |
1403 | 0 | Input constraintValue; |
1404 | 0 | rv = ReadAVA(constraintRDN, constraintType, constraintValueTag, |
1405 | 0 | constraintValue); |
1406 | 0 | if (rv != Success) { |
1407 | 0 | return rv; |
1408 | 0 | } |
1409 | 0 | Input presentedType; |
1410 | 0 | uint8_t presentedValueTag; |
1411 | 0 | Input presentedValue; |
1412 | 0 | rv = ReadAVA(presentedRDN, presentedType, presentedValueTag, |
1413 | 0 | presentedValue); |
1414 | 0 | if (rv != Success) { |
1415 | 0 | return rv; |
1416 | 0 | } |
1417 | 0 | // TODO (bug 1155767): verify that if an AVA is a PrintableString it |
1418 | 0 | // consists only of characters valid for PrintableStrings. |
1419 | 0 | bool avasMatch = |
1420 | 0 | InputsAreEqual(constraintType, presentedType) && |
1421 | 0 | InputsAreEqual(constraintValue, presentedValue) && |
1422 | 0 | (constraintValueTag == presentedValueTag || |
1423 | 0 | (constraintValueTag == der::Tag::UTF8String && |
1424 | 0 | presentedValueTag == der::Tag::PrintableString) || |
1425 | 0 | (constraintValueTag == der::Tag::PrintableString && |
1426 | 0 | presentedValueTag == der::Tag::UTF8String)); |
1427 | 0 | if (!avasMatch) { |
1428 | 0 | matches = false; |
1429 | 0 | return Success; |
1430 | 0 | } |
1431 | 0 | } |
1432 | 0 | if (!constraintRDN.AtEnd() || !presentedRDN.AtEnd()) { |
1433 | 0 | matches = false; |
1434 | 0 | return Success; |
1435 | 0 | } |
1436 | 0 | } |
1437 | 0 | } |
1438 | | |
1439 | | // RFC 5280 says: |
1440 | | // |
1441 | | // The format of an rfc822Name is a "Mailbox" as defined in Section 4.1.2 |
1442 | | // of [RFC2821]. A Mailbox has the form "Local-part@Domain". Note that a |
1443 | | // Mailbox has no phrase (such as a common name) before it, has no comment |
1444 | | // (text surrounded in parentheses) after it, and is not surrounded by "<" |
1445 | | // and ">". Rules for encoding Internet mail addresses that include |
1446 | | // internationalized domain names are specified in Section 7.5. |
1447 | | // |
1448 | | // and: |
1449 | | // |
1450 | | // A name constraint for Internet mail addresses MAY specify a |
1451 | | // particular mailbox, all addresses at a particular host, or all |
1452 | | // mailboxes in a domain. To indicate a particular mailbox, the |
1453 | | // constraint is the complete mail address. For example, |
1454 | | // "root@example.com" indicates the root mailbox on the host |
1455 | | // "example.com". To indicate all Internet mail addresses on a |
1456 | | // particular host, the constraint is specified as the host name. For |
1457 | | // example, the constraint "example.com" is satisfied by any mail |
1458 | | // address at the host "example.com". To specify any address within a |
1459 | | // domain, the constraint is specified with a leading period (as with |
1460 | | // URIs). For example, ".example.com" indicates all the Internet mail |
1461 | | // addresses in the domain "example.com", but not Internet mail |
1462 | | // addresses on the host "example.com". |
1463 | | |
1464 | | bool |
1465 | | IsValidRFC822Name(Input input) |
1466 | 0 | { |
1467 | 0 | Reader reader(input); |
1468 | 0 |
|
1469 | 0 | // Local-part@. |
1470 | 0 | bool startOfAtom = true; |
1471 | 0 | for (;;) { |
1472 | 0 | uint8_t presentedByte; |
1473 | 0 | if (reader.Read(presentedByte) != Success) { |
1474 | 0 | return false; |
1475 | 0 | } |
1476 | 0 | switch (presentedByte) { |
1477 | 0 | // atext is defined in https://tools.ietf.org/html/rfc2822#section-3.2.4 |
1478 | 0 | case 'A': case 'a': case 'N': case 'n': case '0': case '!': case '#': |
1479 | 0 | case 'B': case 'b': case 'O': case 'o': case '1': case '$': case '%': |
1480 | 0 | case 'C': case 'c': case 'P': case 'p': case '2': case '&': case '\'': |
1481 | 0 | case 'D': case 'd': case 'Q': case 'q': case '3': case '*': case '+': |
1482 | 0 | case 'E': case 'e': case 'R': case 'r': case '4': case '-': case '/': |
1483 | 0 | case 'F': case 'f': case 'S': case 's': case '5': case '=': case '?': |
1484 | 0 | case 'G': case 'g': case 'T': case 't': case '6': case '^': case '_': |
1485 | 0 | case 'H': case 'h': case 'U': case 'u': case '7': case '`': case '{': |
1486 | 0 | case 'I': case 'i': case 'V': case 'v': case '8': case '|': case '}': |
1487 | 0 | case 'J': case 'j': case 'W': case 'w': case '9': case '~': |
1488 | 0 | case 'K': case 'k': case 'X': case 'x': |
1489 | 0 | case 'L': case 'l': case 'Y': case 'y': |
1490 | 0 | case 'M': case 'm': case 'Z': case 'z': |
1491 | 0 | startOfAtom = false; |
1492 | 0 | break; |
1493 | 0 |
|
1494 | 0 | case '.': |
1495 | 0 | if (startOfAtom) { |
1496 | 0 | return false; |
1497 | 0 | } |
1498 | 0 | startOfAtom = true; |
1499 | 0 | break; |
1500 | 0 |
|
1501 | 0 | case '@': |
1502 | 0 | { |
1503 | 0 | if (startOfAtom) { |
1504 | 0 | return false; |
1505 | 0 | } |
1506 | 0 | Input domain; |
1507 | 0 | if (reader.SkipToEnd(domain) != Success) { |
1508 | 0 | return false; |
1509 | 0 | } |
1510 | 0 | return IsValidDNSID(domain, IDRole::PresentedID, AllowWildcards::No); |
1511 | 0 | } |
1512 | 0 |
|
1513 | 0 | default: |
1514 | 0 | return false; |
1515 | 0 | } |
1516 | 0 | } |
1517 | 0 | } |
1518 | | |
1519 | | Result |
1520 | | MatchPresentedRFC822NameWithReferenceRFC822Name(Input presentedRFC822Name, |
1521 | | IDRole referenceRFC822NameRole, |
1522 | | Input referenceRFC822Name, |
1523 | | /*out*/ bool& matches) |
1524 | 0 | { |
1525 | 0 | if (!IsValidRFC822Name(presentedRFC822Name)) { |
1526 | 0 | return Result::ERROR_BAD_DER; |
1527 | 0 | } |
1528 | 0 | Reader presented(presentedRFC822Name); |
1529 | 0 |
|
1530 | 0 | switch (referenceRFC822NameRole) |
1531 | 0 | { |
1532 | 0 | case IDRole::PresentedID: |
1533 | 0 | return Result::FATAL_ERROR_INVALID_ARGS; |
1534 | 0 |
|
1535 | 0 | case IDRole::ReferenceID: |
1536 | 0 | break; |
1537 | 0 |
|
1538 | 0 | case IDRole::NameConstraint: |
1539 | 0 | { |
1540 | 0 | if (InputContains(referenceRFC822Name, '@')) { |
1541 | 0 | // The constraint is of the form "Local-part@Domain". |
1542 | 0 | break; |
1543 | 0 | } |
1544 | 0 | |
1545 | 0 | // The constraint is of the form "example.com" or ".example.com". |
1546 | 0 | |
1547 | 0 | // Skip past the '@' in the presented ID. |
1548 | 0 | for (;;) { |
1549 | 0 | uint8_t presentedByte; |
1550 | 0 | if (presented.Read(presentedByte) != Success) { |
1551 | 0 | return Result::FATAL_ERROR_LIBRARY_FAILURE; |
1552 | 0 | } |
1553 | 0 | if (presentedByte == '@') { |
1554 | 0 | break; |
1555 | 0 | } |
1556 | 0 | } |
1557 | 0 |
|
1558 | 0 | Input presentedDNSID; |
1559 | 0 | if (presented.SkipToEnd(presentedDNSID) != Success) { |
1560 | 0 | return Result::FATAL_ERROR_LIBRARY_FAILURE; |
1561 | 0 | } |
1562 | 0 | |
1563 | 0 | return MatchPresentedDNSIDWithReferenceDNSID( |
1564 | 0 | presentedDNSID, AllowWildcards::No, |
1565 | 0 | AllowDotlessSubdomainMatches::No, IDRole::NameConstraint, |
1566 | 0 | referenceRFC822Name, matches); |
1567 | 0 | } |
1568 | 0 | } |
1569 | 0 | |
1570 | 0 | if (!IsValidRFC822Name(referenceRFC822Name)) { |
1571 | 0 | return Result::ERROR_BAD_DER; |
1572 | 0 | } |
1573 | 0 | |
1574 | 0 | Reader reference(referenceRFC822Name); |
1575 | 0 |
|
1576 | 0 | for (;;) { |
1577 | 0 | uint8_t presentedByte; |
1578 | 0 | if (presented.Read(presentedByte) != Success) { |
1579 | 0 | matches = reference.AtEnd(); |
1580 | 0 | return Success; |
1581 | 0 | } |
1582 | 0 | uint8_t referenceByte; |
1583 | 0 | if (reference.Read(referenceByte) != Success) { |
1584 | 0 | matches = false; |
1585 | 0 | return Success; |
1586 | 0 | } |
1587 | 0 | if (LocaleInsensitveToLower(presentedByte) != |
1588 | 0 | LocaleInsensitveToLower(referenceByte)) { |
1589 | 0 | matches = false; |
1590 | 0 | return Success; |
1591 | 0 | } |
1592 | 0 | } |
1593 | 0 | } |
1594 | | |
1595 | | // We avoid isdigit because it is locale-sensitive. See |
1596 | | // http://pubs.opengroup.org/onlinepubs/009695399/functions/tolower.html. |
1597 | | inline uint8_t |
1598 | | LocaleInsensitveToLower(uint8_t a) |
1599 | 0 | { |
1600 | 0 | if (a >= 'A' && a <= 'Z') { // unlikely |
1601 | 0 | return static_cast<uint8_t>( |
1602 | 0 | static_cast<uint8_t>(a - static_cast<uint8_t>('A')) + |
1603 | 0 | static_cast<uint8_t>('a')); |
1604 | 0 | } |
1605 | 0 | return a; |
1606 | 0 | } |
1607 | | |
1608 | | bool |
1609 | | StartsWithIDNALabel(Input id) |
1610 | 0 | { |
1611 | 0 | static const uint8_t IDN_ALABEL_PREFIX[4] = { 'x', 'n', '-', '-' }; |
1612 | 0 | Reader input(id); |
1613 | 0 | for (const uint8_t prefixByte : IDN_ALABEL_PREFIX) { |
1614 | 0 | uint8_t b; |
1615 | 0 | if (input.Read(b) != Success) { |
1616 | 0 | return false; |
1617 | 0 | } |
1618 | 0 | if (b != prefixByte) { |
1619 | 0 | return false; |
1620 | 0 | } |
1621 | 0 | } |
1622 | 0 | return true; |
1623 | 0 | } |
1624 | | |
1625 | | bool |
1626 | | ReadIPv4AddressComponent(Reader& input, bool lastComponent, |
1627 | | /*out*/ uint8_t& valueOut) |
1628 | 0 | { |
1629 | 0 | size_t length = 0; |
1630 | 0 | unsigned int value = 0; // Must be larger than uint8_t. |
1631 | 0 |
|
1632 | 0 | for (;;) { |
1633 | 0 | if (input.AtEnd() && lastComponent) { |
1634 | 0 | break; |
1635 | 0 | } |
1636 | 0 | |
1637 | 0 | uint8_t b; |
1638 | 0 | if (input.Read(b) != Success) { |
1639 | 0 | return false; |
1640 | 0 | } |
1641 | 0 | |
1642 | 0 | if (b >= '0' && b <= '9') { |
1643 | 0 | if (value == 0 && length > 0) { |
1644 | 0 | return false; // Leading zeros are not allowed. |
1645 | 0 | } |
1646 | 0 | value = (value * 10) + (b - '0'); |
1647 | 0 | if (value > 255) { |
1648 | 0 | return false; // Component's value is too large. |
1649 | 0 | } |
1650 | 0 | ++length; |
1651 | 0 | } else if (!lastComponent && b == '.') { |
1652 | 0 | break; |
1653 | 0 | } else { |
1654 | 0 | return false; // Invalid character. |
1655 | 0 | } |
1656 | 0 | } |
1657 | 0 |
|
1658 | 0 | if (length == 0) { |
1659 | 0 | return false; // empty components not allowed |
1660 | 0 | } |
1661 | 0 | |
1662 | 0 | valueOut = static_cast<uint8_t>(value); |
1663 | 0 | return true; |
1664 | 0 | } |
1665 | | |
1666 | | } // namespace |
1667 | | |
1668 | | // On Windows and maybe other platforms, OS-provided IP address parsing |
1669 | | // functions might fail if the protocol (IPv4 or IPv6) has been disabled, so we |
1670 | | // can't rely on them. |
1671 | | bool |
1672 | | ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]) |
1673 | 0 | { |
1674 | 0 | Reader input(hostname); |
1675 | 0 | return ReadIPv4AddressComponent(input, false, out[0]) && |
1676 | 0 | ReadIPv4AddressComponent(input, false, out[1]) && |
1677 | 0 | ReadIPv4AddressComponent(input, false, out[2]) && |
1678 | 0 | ReadIPv4AddressComponent(input, true, out[3]); |
1679 | 0 | } |
1680 | | |
1681 | | namespace { |
1682 | | |
1683 | | bool |
1684 | | FinishIPv6Address(/*in/out*/ uint8_t (&address)[16], int numComponents, |
1685 | | int contractionIndex) |
1686 | 0 | { |
1687 | 0 | assert(numComponents >= 0); |
1688 | 0 | assert(numComponents <= 8); |
1689 | 0 | assert(contractionIndex >= -1); |
1690 | 0 | assert(contractionIndex <= 8); |
1691 | 0 | assert(contractionIndex <= numComponents); |
1692 | 0 | if (!(numComponents >= 0 && |
1693 | 0 | numComponents <= 8 && |
1694 | 0 | contractionIndex >= -1 && |
1695 | 0 | contractionIndex <= 8 && |
1696 | 0 | contractionIndex <= numComponents)) { |
1697 | 0 | return false; |
1698 | 0 | } |
1699 | 0 | |
1700 | 0 | if (contractionIndex == -1) { |
1701 | 0 | // no contraction |
1702 | 0 | return numComponents == 8; |
1703 | 0 | } |
1704 | 0 | |
1705 | 0 | if (numComponents >= 8) { |
1706 | 0 | return false; // no room left to expand the contraction. |
1707 | 0 | } |
1708 | 0 | |
1709 | 0 | // Shift components that occur after the contraction over. |
1710 | 0 | std::copy_backward(address + (2u * static_cast<size_t>(contractionIndex)), |
1711 | 0 | address + (2u * static_cast<size_t>(numComponents)), |
1712 | 0 | address + (2u * 8u)); |
1713 | 0 | // Fill in the contracted area with zeros. |
1714 | 0 | std::fill_n(address + 2u * static_cast<size_t>(contractionIndex), |
1715 | 0 | (8u - static_cast<size_t>(numComponents)) * 2u, static_cast<uint8_t>(0u)); |
1716 | 0 |
|
1717 | 0 | return true; |
1718 | 0 | } |
1719 | | |
1720 | | } // namespace |
1721 | | |
1722 | | // On Windows and maybe other platforms, OS-provided IP address parsing |
1723 | | // functions might fail if the protocol (IPv4 or IPv6) has been disabled, so we |
1724 | | // can't rely on them. |
1725 | | bool |
1726 | | ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]) |
1727 | 0 | { |
1728 | 0 | Reader input(hostname); |
1729 | 0 |
|
1730 | 0 | int currentComponentIndex = 0; |
1731 | 0 | int contractionIndex = -1; |
1732 | 0 |
|
1733 | 0 | if (input.Peek(':')) { |
1734 | 0 | // A valid input can only start with ':' if there is a contraction at the |
1735 | 0 | // beginning. |
1736 | 0 | uint8_t b; |
1737 | 0 | if (input.Read(b) != Success || b != ':') { |
1738 | 0 | assert(false); |
1739 | 0 | return false; |
1740 | 0 | } |
1741 | 0 | if (input.Read(b) != Success) { |
1742 | 0 | return false; |
1743 | 0 | } |
1744 | 0 | if (b != ':') { |
1745 | 0 | return false; |
1746 | 0 | } |
1747 | 0 | contractionIndex = 0; |
1748 | 0 | } |
1749 | 0 |
|
1750 | 0 | for (;;) { |
1751 | 0 | // If we encounter a '.' then we'll have to backtrack to parse the input |
1752 | 0 | // from startOfComponent to the end of the input as an IPv4 address. |
1753 | 0 | Reader::Mark startOfComponent(input.GetMark()); |
1754 | 0 | uint16_t componentValue = 0; |
1755 | 0 | size_t componentLength = 0; |
1756 | 0 | while (!input.AtEnd() && !input.Peek(':')) { |
1757 | 0 | uint8_t value; |
1758 | 0 | uint8_t b; |
1759 | 0 | if (input.Read(b) != Success) { |
1760 | 0 | assert(false); |
1761 | 0 | return false; |
1762 | 0 | } |
1763 | 0 | switch (b) { |
1764 | 0 | case '0': case '1': case '2': case '3': case '4': |
1765 | 0 | case '5': case '6': case '7': case '8': case '9': |
1766 | 0 | value = static_cast<uint8_t>(b - static_cast<uint8_t>('0')); |
1767 | 0 | break; |
1768 | 0 | case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': |
1769 | 0 | value = static_cast<uint8_t>(b - static_cast<uint8_t>('a') + |
1770 | 0 | UINT8_C(10)); |
1771 | 0 | break; |
1772 | 0 | case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': |
1773 | 0 | value = static_cast<uint8_t>(b - static_cast<uint8_t>('A') + |
1774 | 0 | UINT8_C(10)); |
1775 | 0 | break; |
1776 | 0 | case '.': |
1777 | 0 | { |
1778 | 0 | // A dot indicates we hit a IPv4-syntax component. Backtrack, parsing |
1779 | 0 | // the input from startOfComponent to the end of the input as an IPv4 |
1780 | 0 | // address, and then combine it with the other components. |
1781 | 0 |
|
1782 | 0 | if (currentComponentIndex > 6) { |
1783 | 0 | return false; // Too many components before the IPv4 component |
1784 | 0 | } |
1785 | 0 | |
1786 | 0 | input.SkipToEnd(); |
1787 | 0 | Input ipv4Component; |
1788 | 0 | if (input.GetInput(startOfComponent, ipv4Component) != Success) { |
1789 | 0 | return false; |
1790 | 0 | } |
1791 | 0 | uint8_t (*ipv4)[4] = |
1792 | 0 | reinterpret_cast<uint8_t(*)[4]>(&out[2 * currentComponentIndex]); |
1793 | 0 | if (!ParseIPv4Address(ipv4Component, *ipv4)) { |
1794 | 0 | return false; |
1795 | 0 | } |
1796 | 0 | assert(input.AtEnd()); |
1797 | 0 | currentComponentIndex += 2; |
1798 | 0 |
|
1799 | 0 | return FinishIPv6Address(out, currentComponentIndex, |
1800 | 0 | contractionIndex); |
1801 | 0 | } |
1802 | 0 | default: |
1803 | 0 | return false; |
1804 | 0 | } |
1805 | 0 | if (componentLength >= 4) { |
1806 | 0 | // component too long |
1807 | 0 | return false; |
1808 | 0 | } |
1809 | 0 | ++componentLength; |
1810 | 0 | componentValue = (componentValue * 0x10u) + value; |
1811 | 0 | } |
1812 | 0 |
|
1813 | 0 | if (currentComponentIndex >= 8) { |
1814 | 0 | return false; // too many components |
1815 | 0 | } |
1816 | 0 | |
1817 | 0 | if (componentLength == 0) { |
1818 | 0 | if (input.AtEnd() && currentComponentIndex == contractionIndex) { |
1819 | 0 | if (contractionIndex == 0) { |
1820 | 0 | // don't accept "::" |
1821 | 0 | return false; |
1822 | 0 | } |
1823 | 0 | return FinishIPv6Address(out, currentComponentIndex, |
1824 | 0 | contractionIndex); |
1825 | 0 | } |
1826 | 0 | return false; |
1827 | 0 | } |
1828 | 0 | |
1829 | 0 | out[2 * currentComponentIndex] = |
1830 | 0 | static_cast<uint8_t>(componentValue / 0x100); |
1831 | 0 | out[(2 * currentComponentIndex) + 1] = |
1832 | 0 | static_cast<uint8_t>(componentValue % 0x100); |
1833 | 0 |
|
1834 | 0 | ++currentComponentIndex; |
1835 | 0 |
|
1836 | 0 | if (input.AtEnd()) { |
1837 | 0 | return FinishIPv6Address(out, currentComponentIndex, |
1838 | 0 | contractionIndex); |
1839 | 0 | } |
1840 | 0 | |
1841 | 0 | uint8_t b; |
1842 | 0 | if (input.Read(b) != Success || b != ':') { |
1843 | 0 | assert(false); |
1844 | 0 | return false; |
1845 | 0 | } |
1846 | 0 | |
1847 | 0 | if (input.Peek(':')) { |
1848 | 0 | // Contraction |
1849 | 0 | if (contractionIndex != -1) { |
1850 | 0 | return false; // multiple contractions are not allowed. |
1851 | 0 | } |
1852 | 0 | if (input.Read(b) != Success || b != ':') { |
1853 | 0 | assert(false); |
1854 | 0 | return false; |
1855 | 0 | } |
1856 | 0 | contractionIndex = currentComponentIndex; |
1857 | 0 | if (input.AtEnd()) { |
1858 | 0 | // "::" at the end of the input. |
1859 | 0 | return FinishIPv6Address(out, currentComponentIndex, |
1860 | 0 | contractionIndex); |
1861 | 0 | } |
1862 | 0 | } |
1863 | 0 | } |
1864 | 0 | } |
1865 | | |
1866 | | bool |
1867 | | IsValidReferenceDNSID(Input hostname) |
1868 | 0 | { |
1869 | 0 | return IsValidDNSID(hostname, IDRole::ReferenceID, AllowWildcards::No); |
1870 | 0 | } |
1871 | | |
1872 | | bool |
1873 | | IsValidPresentedDNSID(Input hostname) |
1874 | 0 | { |
1875 | 0 | return IsValidDNSID(hostname, IDRole::PresentedID, AllowWildcards::Yes); |
1876 | 0 | } |
1877 | | |
1878 | | namespace { |
1879 | | |
1880 | | // RFC 5280 Section 4.2.1.6 says that a dNSName "MUST be in the 'preferred name |
1881 | | // syntax', as specified by Section 3.5 of [RFC1034] and as modified by Section |
1882 | | // 2.1 of [RFC1123]" except "a dNSName of ' ' MUST NOT be used." Additionally, |
1883 | | // we allow underscores for compatibility with existing practice. |
1884 | | bool |
1885 | | IsValidDNSID(Input hostname, IDRole idRole, AllowWildcards allowWildcards) |
1886 | 0 | { |
1887 | 0 | if (hostname.GetLength() > 253) { |
1888 | 0 | return false; |
1889 | 0 | } |
1890 | 0 | |
1891 | 0 | Reader input(hostname); |
1892 | 0 |
|
1893 | 0 | if (idRole == IDRole::NameConstraint && input.AtEnd()) { |
1894 | 0 | return true; |
1895 | 0 | } |
1896 | 0 | |
1897 | 0 | size_t dotCount = 0; |
1898 | 0 | size_t labelLength = 0; |
1899 | 0 | bool labelIsAllNumeric = false; |
1900 | 0 | bool labelEndsWithHyphen = false; |
1901 | 0 |
|
1902 | 0 | // Only presented IDs are allowed to have wildcard labels. And, like |
1903 | 0 | // Chromium, be stricter than RFC 6125 requires by insisting that a |
1904 | 0 | // wildcard label consist only of '*'. |
1905 | 0 | bool isWildcard = allowWildcards == AllowWildcards::Yes && input.Peek('*'); |
1906 | 0 | bool isFirstByte = !isWildcard; |
1907 | 0 | if (isWildcard) { |
1908 | 0 | Result rv = input.Skip(1); |
1909 | 0 | if (rv != Success) { |
1910 | 0 | assert(false); |
1911 | 0 | return false; |
1912 | 0 | } |
1913 | 0 | |
1914 | 0 | uint8_t b; |
1915 | 0 | rv = input.Read(b); |
1916 | 0 | if (rv != Success) { |
1917 | 0 | return false; |
1918 | 0 | } |
1919 | 0 | if (b != '.') { |
1920 | 0 | return false; |
1921 | 0 | } |
1922 | 0 | ++dotCount; |
1923 | 0 | } |
1924 | 0 |
|
1925 | 0 | do { |
1926 | 0 | static const size_t MAX_LABEL_LENGTH = 63; |
1927 | 0 |
|
1928 | 0 | uint8_t b; |
1929 | 0 | if (input.Read(b) != Success) { |
1930 | 0 | return false; |
1931 | 0 | } |
1932 | 0 | switch (b) { |
1933 | 0 | case '-': |
1934 | 0 | if (labelLength == 0) { |
1935 | 0 | return false; // Labels must not start with a hyphen. |
1936 | 0 | } |
1937 | 0 | labelIsAllNumeric = false; |
1938 | 0 | labelEndsWithHyphen = true; |
1939 | 0 | ++labelLength; |
1940 | 0 | if (labelLength > MAX_LABEL_LENGTH) { |
1941 | 0 | return false; |
1942 | 0 | } |
1943 | 0 | break; |
1944 | 0 |
|
1945 | 0 | // We avoid isdigit because it is locale-sensitive. See |
1946 | 0 | // http://pubs.opengroup.org/onlinepubs/009695399/functions/isdigit.html |
1947 | 0 | case '0': case '5': |
1948 | 0 | case '1': case '6': |
1949 | 0 | case '2': case '7': |
1950 | 0 | case '3': case '8': |
1951 | 0 | case '4': case '9': |
1952 | 0 | if (labelLength == 0) { |
1953 | 0 | labelIsAllNumeric = true; |
1954 | 0 | } |
1955 | 0 | labelEndsWithHyphen = false; |
1956 | 0 | ++labelLength; |
1957 | 0 | if (labelLength > MAX_LABEL_LENGTH) { |
1958 | 0 | return false; |
1959 | 0 | } |
1960 | 0 | break; |
1961 | 0 |
|
1962 | 0 | // We avoid using islower/isupper/tolower/toupper or similar things, to |
1963 | 0 | // avoid any possibility of this code being locale-sensitive. See |
1964 | 0 | // http://pubs.opengroup.org/onlinepubs/009695399/functions/isupper.html |
1965 | 0 | case 'a': case 'A': case 'n': case 'N': |
1966 | 0 | case 'b': case 'B': case 'o': case 'O': |
1967 | 0 | case 'c': case 'C': case 'p': case 'P': |
1968 | 0 | case 'd': case 'D': case 'q': case 'Q': |
1969 | 0 | case 'e': case 'E': case 'r': case 'R': |
1970 | 0 | case 'f': case 'F': case 's': case 'S': |
1971 | 0 | case 'g': case 'G': case 't': case 'T': |
1972 | 0 | case 'h': case 'H': case 'u': case 'U': |
1973 | 0 | case 'i': case 'I': case 'v': case 'V': |
1974 | 0 | case 'j': case 'J': case 'w': case 'W': |
1975 | 0 | case 'k': case 'K': case 'x': case 'X': |
1976 | 0 | case 'l': case 'L': case 'y': case 'Y': |
1977 | 0 | case 'm': case 'M': case 'z': case 'Z': |
1978 | 0 | // We allow underscores for compatibility with existing practices. |
1979 | 0 | // See bug 1136616. |
1980 | 0 | case '_': |
1981 | 0 | labelIsAllNumeric = false; |
1982 | 0 | labelEndsWithHyphen = false; |
1983 | 0 | ++labelLength; |
1984 | 0 | if (labelLength > MAX_LABEL_LENGTH) { |
1985 | 0 | return false; |
1986 | 0 | } |
1987 | 0 | break; |
1988 | 0 |
|
1989 | 0 | case '.': |
1990 | 0 | ++dotCount; |
1991 | 0 | if (labelLength == 0 && |
1992 | 0 | (idRole != IDRole::NameConstraint || !isFirstByte)) { |
1993 | 0 | return false; |
1994 | 0 | } |
1995 | 0 | if (labelEndsWithHyphen) { |
1996 | 0 | return false; // Labels must not end with a hyphen. |
1997 | 0 | } |
1998 | 0 | labelLength = 0; |
1999 | 0 | break; |
2000 | 0 |
|
2001 | 0 | default: |
2002 | 0 | return false; // Invalid character. |
2003 | 0 | } |
2004 | 0 | isFirstByte = false; |
2005 | 0 | } while (!input.AtEnd()); |
2006 | 0 |
|
2007 | 0 | // Only reference IDs, not presented IDs or name constraints, may be |
2008 | 0 | // absolute. |
2009 | 0 | if (labelLength == 0 && idRole != IDRole::ReferenceID) { |
2010 | 0 | return false; |
2011 | 0 | } |
2012 | 0 | |
2013 | 0 | if (labelEndsWithHyphen) { |
2014 | 0 | return false; // Labels must not end with a hyphen. |
2015 | 0 | } |
2016 | 0 | |
2017 | 0 | if (labelIsAllNumeric) { |
2018 | 0 | return false; // Last label must not be all numeric. |
2019 | 0 | } |
2020 | 0 | |
2021 | 0 | if (isWildcard) { |
2022 | 0 | // If the DNS ID ends with a dot, the last dot signifies an absolute ID. |
2023 | 0 | size_t labelCount = (labelLength == 0) ? dotCount : (dotCount + 1); |
2024 | 0 |
|
2025 | 0 | // Like NSS, require at least two labels to follow the wildcard label. |
2026 | 0 | // |
2027 | 0 | // TODO(bug XXXXXXX): Allow the TrustDomain to control this on a |
2028 | 0 | // per-eTLD+1 basis, similar to Chromium. Even then, it might be better to |
2029 | 0 | // still enforce that there are at least two labels after the wildcard. |
2030 | 0 | if (labelCount < 3) { |
2031 | 0 | return false; |
2032 | 0 | } |
2033 | 0 | // XXX: RFC6125 says that we shouldn't accept wildcards within an IDN |
2034 | 0 | // A-Label. The consequence of this is that we effectively discriminate |
2035 | 0 | // against users of languages that cannot be encoded with ASCII. |
2036 | 0 | if (StartsWithIDNALabel(hostname)) { |
2037 | 0 | return false; |
2038 | 0 | } |
2039 | 0 | |
2040 | 0 | // TODO(bug XXXXXXX): Wildcards are not allowed for EV certificates. |
2041 | 0 | // Provide an option to indicate whether wildcards should be matched, for |
2042 | 0 | // the purpose of helping the application enforce this. |
2043 | 0 | } |
2044 | 0 | |
2045 | 0 | return true; |
2046 | 0 | } |
2047 | | |
2048 | | } // namespace |
2049 | | |
2050 | | } } // namespace mozilla::pkix |