• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.http;
18 
19 import org.bouncycastle.asn1.x509.X509Name;
20 
21 import java.net.InetAddress;
22 import java.net.UnknownHostException;
23 import java.security.cert.X509Certificate;
24 import java.security.cert.CertificateParsingException;
25 import java.util.Collection;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import java.util.regex.PatternSyntaxException;
31 import java.util.Vector;
32 
33 /**
34  * Implements basic domain-name validation as specified by RFC2818.
35  *
36  * {@hide}
37  */
38 public class DomainNameChecker {
39     private static Pattern QUICK_IP_PATTERN;
40     static {
41         try {
42             QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$");
43         } catch (PatternSyntaxException e) {}
44     }
45 
46     private static final int ALT_DNS_NAME = 2;
47     private static final int ALT_IPA_NAME = 7;
48 
49     /**
50      * Checks the site certificate against the domain name of the site being visited
51      * @param certificate The certificate to check
52      * @param thisDomain The domain name of the site being visited
53      * @return True iff if there is a domain match as specified by RFC2818
54      */
match(X509Certificate certificate, String thisDomain)55     public static boolean match(X509Certificate certificate, String thisDomain) {
56         if (certificate == null || thisDomain == null || thisDomain.length() == 0) {
57             return false;
58         }
59 
60         thisDomain = thisDomain.toLowerCase();
61         if (!isIpAddress(thisDomain)) {
62             return matchDns(certificate, thisDomain);
63         } else {
64             return matchIpAddress(certificate, thisDomain);
65         }
66     }
67 
68     /**
69      * @return True iff the domain name is specified as an IP address
70      */
isIpAddress(String domain)71     private static boolean isIpAddress(String domain) {
72         boolean rval = (domain != null && domain.length() != 0);
73         if (rval) {
74             try {
75                 // do a quick-dirty IP match first to avoid DNS lookup
76                 rval = QUICK_IP_PATTERN.matcher(domain).matches();
77                 if (rval) {
78                     rval = domain.equals(
79                         InetAddress.getByName(domain).getHostAddress());
80                 }
81             } catch (UnknownHostException e) {
82                 String errorMessage = e.getMessage();
83                 if (errorMessage == null) {
84                   errorMessage = "unknown host exception";
85                 }
86 
87                 if (HttpLog.LOGV) {
88                     HttpLog.v("DomainNameChecker.isIpAddress(): " + errorMessage);
89                 }
90 
91                 rval = false;
92             }
93         }
94 
95         return rval;
96     }
97 
98     /**
99      * Checks the site certificate against the IP domain name of the site being visited
100      * @param certificate The certificate to check
101      * @param thisDomain The DNS domain name of the site being visited
102      * @return True iff if there is a domain match as specified by RFC2818
103      */
matchIpAddress(X509Certificate certificate, String thisDomain)104     private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) {
105         if (HttpLog.LOGV) {
106             HttpLog.v("DomainNameChecker.matchIpAddress(): this domain: " + thisDomain);
107         }
108 
109         try {
110             Collection subjectAltNames = certificate.getSubjectAlternativeNames();
111             if (subjectAltNames != null) {
112                 Iterator i = subjectAltNames.iterator();
113                 while (i.hasNext()) {
114                     List altNameEntry = (List)(i.next());
115                     if (altNameEntry != null && 2 <= altNameEntry.size()) {
116                         Integer altNameType = (Integer)(altNameEntry.get(0));
117                         if (altNameType != null) {
118                             if (altNameType.intValue() == ALT_IPA_NAME) {
119                                 String altName = (String)(altNameEntry.get(1));
120                                 if (altName != null) {
121                                     if (HttpLog.LOGV) {
122                                         HttpLog.v("alternative IP: " + altName);
123                                     }
124                                     if (thisDomain.equalsIgnoreCase(altName)) {
125                                         return true;
126                                     }
127                                 }
128                             }
129                         }
130                     }
131                 }
132             }
133         } catch (CertificateParsingException e) {}
134 
135         return false;
136     }
137 
138     /**
139      * Checks the site certificate against the DNS domain name of the site being visited
140      * @param certificate The certificate to check
141      * @param thisDomain The DNS domain name of the site being visited
142      * @return True iff if there is a domain match as specified by RFC2818
143      */
matchDns(X509Certificate certificate, String thisDomain)144     private static boolean matchDns(X509Certificate certificate, String thisDomain) {
145         boolean hasDns = false;
146         try {
147             Collection subjectAltNames = certificate.getSubjectAlternativeNames();
148             if (subjectAltNames != null) {
149                 Iterator i = subjectAltNames.iterator();
150                 while (i.hasNext()) {
151                     List altNameEntry = (List)(i.next());
152                     if (altNameEntry != null && 2 <= altNameEntry.size()) {
153                         Integer altNameType = (Integer)(altNameEntry.get(0));
154                         if (altNameType != null) {
155                             if (altNameType.intValue() == ALT_DNS_NAME) {
156                                 hasDns = true;
157                                 String altName = (String)(altNameEntry.get(1));
158                                 if (altName != null) {
159                                     if (matchDns(thisDomain, altName)) {
160                                         return true;
161                                     }
162                                 }
163                             }
164                         }
165                     }
166                 }
167             }
168         } catch (CertificateParsingException e) {
169             // one way we can get here is if an alternative name starts with
170             // '*' character, which is contrary to one interpretation of the
171             // spec (a valid DNS name must start with a letter); there is no
172             // good way around this, and in order to be compatible we proceed
173             // to check the common name (ie, ignore alternative names)
174             if (HttpLog.LOGV) {
175                 String errorMessage = e.getMessage();
176                 if (errorMessage == null) {
177                     errorMessage = "failed to parse certificate";
178                 }
179 
180                 if (HttpLog.LOGV) {
181                     HttpLog.v("DomainNameChecker.matchDns(): " + errorMessage);
182                 }
183             }
184         }
185 
186         if (!hasDns) {
187             X509Name xName = new X509Name(certificate.getSubjectDN().getName());
188             Vector val = xName.getValues();
189             Vector oid = xName.getOIDs();
190             for (int i = 0; i < oid.size(); i++) {
191                 if (oid.elementAt(i).equals(X509Name.CN)) {
192                     return matchDns(thisDomain, (String)(val.elementAt(i)));
193                 }
194             }
195         }
196 
197         return false;
198     }
199 
200     /**
201      * @param thisDomain The domain name of the site being visited
202      * @param thatDomain The domain name from the certificate
203      * @return True iff thisDomain matches thatDomain as specified by RFC2818
204      */
matchDns(String thisDomain, String thatDomain)205     private static boolean matchDns(String thisDomain, String thatDomain) {
206         if (HttpLog.LOGV) {
207             HttpLog.v("DomainNameChecker.matchDns():" +
208                       " this domain: " + thisDomain +
209                       " that domain: " + thatDomain);
210         }
211 
212         if (thisDomain == null || thisDomain.length() == 0 ||
213             thatDomain == null || thatDomain.length() == 0) {
214             return false;
215         }
216 
217         thatDomain = thatDomain.toLowerCase();
218 
219         // (a) domain name strings are equal, ignoring case: X matches X
220         boolean rval = thisDomain.equals(thatDomain);
221         if (!rval) {
222             String[] thisDomainTokens = thisDomain.split("\\.");
223             String[] thatDomainTokens = thatDomain.split("\\.");
224 
225             int thisDomainTokensNum = thisDomainTokens.length;
226             int thatDomainTokensNum = thatDomainTokens.length;
227 
228             // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X
229             if (thisDomainTokensNum >= thatDomainTokensNum) {
230                 for (int i = thatDomainTokensNum - 1; i >= 0; --i) {
231                     rval = thisDomainTokens[i].equals(thatDomainTokens[i]);
232                     if (!rval) {
233                         // (c) OR we have a special *-match:
234                         // Z.Y.X matches *.Y.X but does not match *.X
235                         rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum);
236                         if (rval) {
237                             rval = thatDomainTokens[0].equals("*");
238                             if (!rval) {
239                                 // (d) OR we have a *-component match:
240                                 // f*.com matches foo.com but not bar.com
241                                 rval = domainTokenMatch(
242                                     thisDomainTokens[0], thatDomainTokens[0]);
243                             }
244                         }
245 
246                         break;
247                     }
248                 }
249             }
250         }
251 
252         return rval;
253     }
254 
255     /**
256      * @param thisDomainToken The domain token from the current domain name
257      * @param thatDomainToken The domain token from the certificate
258      * @return True iff thisDomainToken matches thatDomainToken, using the
259      * wildcard match as specified by RFC2818-3.1. For example, f*.com must
260      * match foo.com but not bar.com
261      */
domainTokenMatch(String thisDomainToken, String thatDomainToken)262     private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) {
263         if (thisDomainToken != null && thatDomainToken != null) {
264             int starIndex = thatDomainToken.indexOf('*');
265             if (starIndex >= 0) {
266                 if (thatDomainToken.length() - 1 <= thisDomainToken.length()) {
267                     String prefix = thatDomainToken.substring(0,  starIndex);
268                     String suffix = thatDomainToken.substring(starIndex + 1);
269 
270                     return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix);
271                 }
272             }
273         }
274 
275         return false;
276     }
277 }
278