1 /** @file
2 
3   Validate hostname matches certificate according to RFC6125
4 
5   @section license License
6 
7   Licensed to the Apache Software Foundation (ASF) under one
8   or more contributor license agreements.  See the NOTICE file
9   distributed with this work for additional information
10   regarding copyright ownership.  The ASF licenses this file
11   to you under the Apache License, Version 2.0 (the
12   "License"); you may not use this file except in compliance
13   with the License.  You may obtain a copy of the License at
14 
15       http://www.apache.org/licenses/LICENSE-2.0
16 
17   Unless required by applicable law or agreed to in writing, software
18   distributed under the License is distributed on an "AS IS" BASIS,
19   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20   See the License for the specific language governing permissions and
21   limitations under the License.
22  */
23 
24 #include <memory.h>
25 #include <strings.h>
26 #include <openssl/crypto.h>
27 #include <openssl/x509.h>
28 #include <openssl/x509v3.h>
29 
30 #include "tscore/ink_memory.h"
31 
32 using equal_fn = bool (*)(const unsigned char *, size_t, const unsigned char *, size_t);
33 
34 /* Return a ptr to a valid wildcard or NULL if not found
35  *
36  * Using OpenSSL default flags:
37  *   X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS = False
38  *   X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS  = False
39  * At most one wildcard per pattern.
40  * No wildcards inside IDNA labels (a full label match is ok:
41  *                                  *.a.b matches xn--something-or-other.a.b .)
42  * No wildcards after the first label.
43  */
44 
45 static const unsigned char *
find_wildcard_in_hostname(const unsigned char * p,size_t len,bool idna_subject)46 find_wildcard_in_hostname(const unsigned char *p, size_t len, bool idna_subject)
47 {
48   size_t i = 0;
49   // Minimum wildcard length *.a.b
50   if (len < 5) {
51     return nullptr;
52   }
53 
54   int wildcard_pos = -1;
55   // Find last dot (can't be last) -- memrchr is GNU extension....
56   size_t final_dot_pos = 0;
57   for (i = len - 2; i > 1; i--) {
58     if (p[i] == '.') {
59       final_dot_pos = i;
60       break;
61     }
62   }
63   // Final dot minimal pos is a.b.xxxxxx
64   if (final_dot_pos < 3) {
65     return nullptr;
66   }
67 
68   for (i = 0; i < final_dot_pos; i++) {
69     /*
70      * Make sure there are at least two '.' in the string
71      */
72     if (p[i] == '*') {
73       if (wildcard_pos != -1) {
74         // Multiple wildcards in first label
75         break;
76       } else if (i == 0 ||                                         // First char is wildcard
77                  ((i < final_dot_pos - 1) && (p[i + 1] == '.'))) { // Found a trailing wildcard in the first label
78 
79         // IDNA hostnames must match a full label
80         if (idna_subject && (i != 0 || p[i + 1] != '.')) {
81           break;
82         }
83 
84         wildcard_pos = i;
85       } else {
86         // Either mid-label wildcard or not enough dots
87         break;
88       }
89     }
90     // String contains at least two dots.
91     if (p[i] == '.') {
92       if (wildcard_pos != -1) {
93         return &p[wildcard_pos];
94       }
95       // Only valid wildcard is in the first label
96       break;
97     }
98   }
99   return nullptr;
100 }
101 
102 /*
103  * Comparison functions
104  * @param pattern is the value from the certificate
105  * @param subject is the value from the client request
106  */
107 
108 /* Compare while ASCII ignoring case. */
109 static bool
equal_nocase(const unsigned char * pattern,size_t pattern_len,const unsigned char * subject,size_t subject_len)110 equal_nocase(const unsigned char *pattern, size_t pattern_len, const unsigned char *subject, size_t subject_len)
111 {
112   if (pattern_len != subject_len) {
113     return false;
114   }
115   return (strncasecmp((char *)pattern, (char *)subject, pattern_len) == 0);
116 }
117 
118 /* Compare using memcmp. */
119 static bool
equal_case(const unsigned char * pattern,size_t pattern_len,const unsigned char * subject,size_t subject_len)120 equal_case(const unsigned char *pattern, size_t pattern_len, const unsigned char *subject, size_t subject_len)
121 {
122   if (pattern_len != subject_len) {
123     return false;
124   }
125   return (memcmp(pattern, subject, pattern_len) == 0);
126 }
127 
128 /*
129  * Compare the prefix and suffix with the subject, and check that the
130  * characters in-between are valid.
131  */
132 static bool
wildcard_match(const unsigned char * prefix,size_t prefix_len,const unsigned char * suffix,size_t suffix_len,const unsigned char * subject,size_t subject_len)133 wildcard_match(const unsigned char *prefix, size_t prefix_len, const unsigned char *suffix, size_t suffix_len,
134                const unsigned char *subject, size_t subject_len)
135 {
136   const unsigned char *wildcard_start;
137   const unsigned char *wildcard_end;
138   const unsigned char *p;
139 
140   if (subject_len < prefix_len + suffix_len) {
141     return false;
142   }
143   if (!equal_nocase(prefix, prefix_len, subject, prefix_len)) {
144     return false;
145   }
146   wildcard_start = subject + prefix_len;
147   wildcard_end   = subject + (subject_len - suffix_len);
148   if (!equal_nocase(wildcard_end, suffix_len, suffix, suffix_len)) {
149     return false;
150   }
151   /*
152    * If the wildcard makes up the entire first label, it must match at
153    * least one character.
154    */
155   if (prefix_len == 0 && *suffix == '.') {
156     if (wildcard_start == wildcard_end) {
157       return false;
158     }
159   }
160   /* The wildcard may match a literal '*' */
161   if (wildcard_end == wildcard_start + 1 && *wildcard_start == '*') {
162     return true;
163   }
164   /*
165    * Check that the part matched by the wildcard contains only
166    * permitted characters and only matches a single label
167    */
168   for (p = wildcard_start; p != wildcard_end; ++p) {
169     if (!(('a' <= *p && *p <= 'z') || ('A' <= *p && *p <= 'Z') || ('0' <= *p && *p <= '9') || *p == '-' || *p == '_')) {
170       return false;
171     }
172   }
173   return true;
174 }
175 
176 /* Compare using wildcards. */
177 static bool
equal_wildcard(const unsigned char * pattern,size_t pattern_len,const unsigned char * subject,size_t subject_len)178 equal_wildcard(const unsigned char *pattern, size_t pattern_len, const unsigned char *subject, size_t subject_len)
179 {
180   const unsigned char *wildcard = nullptr;
181 
182   bool is_idna = (subject_len > 4 && strncasecmp(reinterpret_cast<const char *>(subject), "xn--", 4) == 0);
183   /*
184    * Subject names starting with '.' can only match a wildcard pattern
185    * via a subject sub-domain pattern suffix match (that we don't allow).
186    */
187   if (subject_len > 5 && subject[0] != '.') {
188     wildcard = find_wildcard_in_hostname(pattern, pattern_len, is_idna);
189   }
190 
191   if (wildcard == nullptr) {
192     return equal_nocase(pattern, pattern_len, subject, subject_len);
193   }
194   return wildcard_match(pattern, wildcard - pattern, wildcard + 1, (pattern + pattern_len) - wildcard - 1, subject, subject_len);
195 }
196 
197 /*
198  * Compare an ASN1_STRING to a supplied string. only compare if string matches the specified type
199  *
200  * Returns true if the strings match, false otherwise
201  */
202 
203 static bool
do_check_string(ASN1_STRING * a,int cmp_type,equal_fn equal,const unsigned char * b,size_t blen,char ** peername)204 do_check_string(ASN1_STRING *a, int cmp_type, equal_fn equal, const unsigned char *b, size_t blen, char **peername)
205 {
206   bool retval = false;
207 
208   if (!a->data || !a->length || cmp_type != a->type) {
209     return false;
210   }
211   retval = equal(a->data, a->length, b, blen);
212   if (retval && peername) {
213     *peername = ats_strndup((char *)a->data, a->length);
214   }
215   return retval;
216 }
217 
218 bool
validate_hostname(X509 * x,const unsigned char * hostname,bool is_ip,char ** peername)219 validate_hostname(X509 *x, const unsigned char *hostname, bool is_ip, char **peername)
220 {
221   GENERAL_NAMES *gens = nullptr;
222   X509_NAME *name     = nullptr;
223   int i;
224   int alt_type;
225   bool retval = false;
226   ;
227   equal_fn equal;
228   size_t hostname_len = strlen((char *)hostname);
229 
230   if (!is_ip) {
231     alt_type = V_ASN1_IA5STRING;
232     equal    = equal_wildcard;
233   } else {
234     alt_type = V_ASN1_OCTET_STRING;
235     equal    = equal_case;
236   }
237 
238   // Check SANs for a match.
239   gens = static_cast<GENERAL_NAMES *>(X509_get_ext_d2i(x, NID_subject_alt_name, nullptr, nullptr));
240   if (gens) {
241     // BoringSSL has sk_GENERAL_NAME_num() return size_t.
242     for (i = 0; i < static_cast<int>(sk_GENERAL_NAME_num(gens)); i++) {
243       GENERAL_NAME *gen;
244       ASN1_STRING *cstr;
245       gen = sk_GENERAL_NAME_value(gens, i);
246 
247       if (is_ip && gen->type == GEN_IPADD) {
248         cstr = gen->d.iPAddress;
249       } else if (!is_ip && gen->type == GEN_DNS) {
250         cstr = gen->d.dNSName;
251       } else {
252         continue;
253       }
254 
255       if ((retval = do_check_string(cstr, alt_type, equal, hostname, hostname_len, peername)) == true) {
256         // We got a match
257         break;
258       }
259     }
260     GENERAL_NAMES_free(gens);
261     if (retval) {
262       return retval;
263     }
264   }
265   // No SAN match -- check the subject
266   i    = -1;
267   name = X509_get_subject_name(x);
268 
269   while ((i = X509_NAME_get_index_by_NID(name, NID_commonName, i)) >= 0) {
270     ASN1_STRING *str;
271     int astrlen;
272     unsigned char *astr;
273     str = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(name, i));
274     // Convert to UTF-8
275     astrlen = ASN1_STRING_to_UTF8(&astr, str);
276 
277     if (astrlen < 0) {
278       return -1;
279     }
280     retval = equal(astr, astrlen, hostname, hostname_len);
281     if (retval && peername) {
282       *peername = ats_strndup((char *)astr, astrlen);
283     }
284     OPENSSL_free(astr);
285     return retval;
286   }
287   return false;
288 }
289