blob: 779c46abe931d1e57452df60e82422268d6eb7f0 [file] [log] [blame]
The Android Open Source Projectadc854b2009-03-03 19:28:47 -08001/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18// BEGIN android-added
19// Copied and condensed code taken from the Apache HttpClient. Also slightly
20// modified, so it matches the package/class structure of the core libraries.
21// This HostnameVerifier does checking similar to what the RI and popular
22// browsers do.
23// END android-added
24
25package javax.net.ssl;
26
27import org.apache.harmony.luni.util.Inet6Util;
28
29import java.io.IOException;
30import java.io.InputStream;
31import java.security.cert.Certificate;
32import java.security.cert.CertificateParsingException;
33import java.security.cert.X509Certificate;
34import java.util.Arrays;
35import java.util.Collection;
36import java.util.Iterator;
37import java.util.LinkedList;
38import java.util.List;
39import java.util.Locale;
40import java.util.StringTokenizer;
41import java.util.logging.Level;
42import java.util.logging.Logger;
43
44import javax.net.ssl.HostnameVerifier;
45import javax.net.ssl.SSLException;
46import javax.net.ssl.SSLSession;
47import javax.net.ssl.SSLSocket;
48
49/**
50 * A HostnameVerifier that works the same way as Curl and Firefox.
51 * <p/>
52 * The hostname must match either the first CN, or any of the subject-alts.
53 * A wildcard can occur in the CN, and in any of the subject-alts.
54 * <p/>
55 * The only difference between BROWSER_COMPATIBLE and STRICT is that a wildcard
56 * (such as "*.foo.com") with BROWSER_COMPATIBLE matches all subdomains,
57 * including "a.b.foo.com".
58 *
59 * @author Julius Davies
60 */
61class DefaultHostnameVerifier implements HostnameVerifier {
62
63 /**
64 * This contains a list of 2nd-level domains that aren't allowed to
65 * have wildcards when combined with country-codes.
66 * For example: [*.co.uk].
67 * <p/>
68 * The [*.co.uk] problem is an interesting one. Should we just hope
69 * that CA's would never foolishly allow such a certificate to happen?
70 * Looks like we're the only implementation guarding against this.
71 * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
72 */
73 private final static String[] BAD_COUNTRY_2LDS =
74 { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
75 "lg", "ne", "net", "or", "org" };
76
77 static {
78 // Just in case developer forgot to manually sort the array. :-)
79 Arrays.sort(BAD_COUNTRY_2LDS);
80 }
81
82 public DefaultHostnameVerifier() {
83 super();
84 }
85
86 public final void verify(String host, SSLSocket ssl)
87 throws IOException {
88 if(host == null) {
89 throw new NullPointerException("host to verify is null");
90 }
91
The Android Open Source Projectadc854b2009-03-03 19:28:47 -080092 SSLSession session = ssl.getSession();
The Android Open Source Projectadc854b2009-03-03 19:28:47 -080093 Certificate[] certs = session.getPeerCertificates();
94 X509Certificate x509 = (X509Certificate) certs[0];
95 verify(host, x509);
96 }
97
98 public final boolean verify(String host, SSLSession session) {
99 try {
100 Certificate[] certs = session.getPeerCertificates();
101 X509Certificate x509 = (X509Certificate) certs[0];
102 verify(host, x509);
103 return true;
104 }
105 catch(SSLException e) {
106 return false;
107 }
108 }
109
110 public final void verify(String host, X509Certificate cert)
111 throws SSLException {
112 String[] cns = getCNs(cert);
113 String[] subjectAlts = getDNSSubjectAlts(cert);
114 verify(host, cns, subjectAlts);
115 }
116
117 public final void verify(final String host, final String[] cns,
118 final String[] subjectAlts,
119 final boolean strictWithSubDomains)
120 throws SSLException {
121
122 // Build the list of names we're going to check. Our DEFAULT and
123 // STRICT implementations of the HostnameVerifier only use the
124 // first CN provided. All other CNs are ignored.
125 // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
126 LinkedList<String> names = new LinkedList<String>();
127 if(cns != null && cns.length > 0 && cns[0] != null) {
128 names.add(cns[0]);
129 }
130 if(subjectAlts != null) {
131 for (String subjectAlt : subjectAlts) {
132 if (subjectAlt != null) {
133 names.add(subjectAlt);
134 }
135 }
136 }
137
138 if(names.isEmpty()) {
139 String msg = "Certificate for <" + host +
140 "> doesn't contain CN or DNS subjectAlt";
141 throw new SSLException(msg);
142 }
143
144 // StringBuffer for building the error message.
145 StringBuffer buf = new StringBuffer();
146
147 // We're can be case-insensitive when comparing the host we used to
148 // establish the socket to the hostname in the certificate.
149 String hostName = host.trim().toLowerCase(Locale.ENGLISH);
150 boolean match = false;
151 for(Iterator<String> it = names.iterator(); it.hasNext();) {
152 // Don't trim the CN, though!
153 String cn = it.next();
154 cn = cn.toLowerCase(Locale.ENGLISH);
155 // Store CN in StringBuffer in case we need to report an error.
156 buf.append(" <");
157 buf.append(cn);
158 buf.append('>');
159 if(it.hasNext()) {
160 buf.append(" OR");
161 }
162
163 // The CN better have at least two dots if it wants wildcard
164 // action. It also can't be [*.co.uk] or [*.co.jp] or
165 // [*.org.uk], etc...
166 boolean doWildcard = cn.startsWith("*.") &&
167 cn.lastIndexOf('.') >= 0 &&
168 acceptableCountryWildcard(cn) &&
169 !Inet6Util.isValidIPV4Address(host);
170
171 if(doWildcard) {
172 match = hostName.endsWith(cn.substring(1));
173 if(match && strictWithSubDomains) {
174 // If we're in strict mode, then [*.foo.com] is not
175 // allowed to match [a.b.foo.com]
176 match = countDots(hostName) == countDots(cn);
177 }
178 } else {
179 match = hostName.equals(cn);
180 }
181 if(match) {
182 break;
183 }
184 }
185 if(!match) {
186 throw new SSLException("hostname in certificate didn't match: <" +
187 host + "> !=" + buf);
188 }
189 }
190
191 public static boolean acceptableCountryWildcard(String cn) {
192 int cnLen = cn.length();
193 if(cnLen >= 7 && cnLen <= 9) {
194 // Look for the '.' in the 3rd-last position:
195 if(cn.charAt(cnLen - 3) == '.') {
196 // Trim off the [*.] and the [.XX].
197 String s = cn.substring(2, cnLen - 3);
198 // And test against the sorted array of bad 2lds:
199 int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s);
200 return x < 0;
201 }
202 }
203 return true;
204 }
205
206 public static String[] getCNs(X509Certificate cert) {
207 LinkedList<String> cnList = new LinkedList<String>();
208 /*
209 Sebastian Hauer's original StrictSSLProtocolSocketFactory used
210 getName() and had the following comment:
211
212 Parses a X.500 distinguished name for the value of the
213 "Common Name" field. This is done a bit sloppy right
214 now and should probably be done a bit more according to
215 <code>RFC 2253</code>.
216
217 I've noticed that toString() seems to do a better job than
218 getName() on these X500Principal objects, so I'm hoping that
219 addresses Sebastian's concern.
220
221 For example, getName() gives me this:
222 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
223
224 whereas toString() gives me this:
225 EMAILADDRESS=juliusdavies@cucbc.com
226
227 Looks like toString() even works with non-ascii domain names!
228 I tested it with "&#x82b1;&#x5b50;.co.jp" and it worked fine.
229 */
230 String subjectPrincipal = cert.getSubjectX500Principal().toString();
231 StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
232 while(st.hasMoreTokens()) {
233 String tok = st.nextToken();
234 int x = tok.indexOf("CN=");
235 if(x >= 0) {
236 cnList.add(tok.substring(x + 3));
237 }
238 }
239 if(!cnList.isEmpty()) {
240 String[] cns = new String[cnList.size()];
241 cnList.toArray(cns);
242 return cns;
243 } else {
244 return null;
245 }
246 }
247
248
249 /**
250 * Extracts the array of SubjectAlt DNS names from an X509Certificate.
251 * Returns null if there aren't any.
252 * <p/>
253 * Note: Java doesn't appear able to extract international characters
254 * from the SubjectAlts. It can only extract international characters
255 * from the CN field.
256 * <p/>
257 * (Or maybe the version of OpenSSL I'm using to test isn't storing the
258 * international characters correctly in the SubjectAlts?).
259 *
260 * @param cert X509Certificate
261 * @return Array of SubjectALT DNS names stored in the certificate.
262 */
263 public static String[] getDNSSubjectAlts(X509Certificate cert) {
264 LinkedList<String> subjectAltList = new LinkedList<String>();
265 Collection<List<?>> c = null;
266 try {
267 c = cert.getSubjectAlternativeNames();
268 }
269 catch(CertificateParsingException cpe) {
270 Logger.getLogger(DefaultHostnameVerifier.class.getName())
271 .log(Level.FINE, "Error parsing certificate.", cpe);
272 }
273 if(c != null) {
274 for (List<?> aC : c) {
275 List<?> list = aC;
276 int type = ((Integer) list.get(0)).intValue();
277 // If type is 2, then we've got a dNSName
278 if (type == 2) {
279 String s = (String) list.get(1);
280 subjectAltList.add(s);
281 }
282 }
283 }
284 if(!subjectAltList.isEmpty()) {
285 String[] subjectAlts = new String[subjectAltList.size()];
286 subjectAltList.toArray(subjectAlts);
287 return subjectAlts;
288 } else {
289 return null;
290 }
291 }
292
293 /**
294 * Counts the number of dots "." in a string.
295 * @param s string to count dots from
296 * @return number of dots
297 */
298 public static int countDots(final String s) {
299 int count = 0;
300 for(int i = 0; i < s.length(); i++) {
301 if(s.charAt(i) == '.') {
302 count++;
303 }
304 }
305 return count;
306 }
307
308 /**
309 * Checks to see if the supplied hostname matches any of the supplied CNs
310 * or "DNS" Subject-Alts. Most implementations only look at the first CN,
311 * and ignore any additional CNs. Most implementations do look at all of
312 * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards
313 * according to RFC 2818.
314 *
315 * @param cns CN fields, in order, as extracted from the X.509
316 * certificate.
317 * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted
318 * from the X.509 certificate.
319 * @param host The hostname to verify.
320 * @throws SSLException If verification failed.
321 */
322 public final void verify(
323 final String host,
324 final String[] cns,
325 final String[] subjectAlts) throws SSLException {
326 verify(host, cns, subjectAlts, false);
327 }
328
329}