1 /*
2 * ====================================================================
3 * Licensed to the Apache Software Foundation (ASF) under one
4 * or more contributor license agreements. See the NOTICE file
5 * distributed with this work for additional information
6 * regarding copyright ownership. The ASF licenses this file
7 * to you under the Apache License, Version 2.0 (the
8 * "License"); you may not use this file except in compliance
9 * with the License. You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing,
14 * software distributed under the License is distributed on an
15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 * KIND, either express or implied. See the License for the
17 * specific language governing permissions and limitations
18 * under the License.
19 * ====================================================================
20 *
21 * This software consists of voluntary contributions made by many
22 * individuals on behalf of the Apache Software Foundation. For more
23 * information on the Apache Software Foundation, please see
24 * <http://www.apache.org/>.
25 *
26 */
27
28 package org.apache.http.conn.ssl;
29
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.net.InetAddress;
33 import java.net.UnknownHostException;
34 import java.security.cert.Certificate;
35 import java.security.cert.CertificateParsingException;
36 import java.security.cert.X509Certificate;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.Iterator;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.StringTokenizer;
44
45 import javax.net.ssl.SSLException;
46 import javax.net.ssl.SSLSession;
47 import javax.net.ssl.SSLSocket;
48
49 import org.apache.commons.logging.Log;
50 import org.apache.commons.logging.LogFactory;
51 import org.apache.http.annotation.Immutable;
52 import org.apache.http.conn.util.InetAddressUtils;
53
54 /**
55 * Abstract base class for all standard {@link X509HostnameVerifier}
56 * implementations.
57 *
58 * @since 4.0
59 */
60 @Immutable
61 public abstract class AbstractVerifier implements X509HostnameVerifier {
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 private final Log log = LogFactory.getLog(getClass());
83
84 public AbstractVerifier() {
85 super();
86 }
87
88 public final void verify(final String host, final SSLSocket ssl)
89 throws IOException {
90 if(host == null) {
91 throw new NullPointerException("host to verify is null");
92 }
93
94 SSLSession session = ssl.getSession();
95 if(session == null) {
96 // In our experience this only happens under IBM 1.4.x when
97 // spurious (unrelated) certificates show up in the server'
98 // chain. Hopefully this will unearth the real problem:
99 final InputStream in = ssl.getInputStream();
100 in.available();
101 /*
102 If you're looking at the 2 lines of code above because
103 you're running into a problem, you probably have two
104 options:
105
106 #1. Clean up the certificate chain that your server
107 is presenting (e.g. edit "/etc/apache2/server.crt"
108 or wherever it is your server's certificate chain
109 is defined).
110
111 OR
112
113 #2. Upgrade to an IBM 1.5.x or greater JVM, or switch
114 to a non-IBM JVM.
115 */
116
117 // If ssl.getInputStream().available() didn't cause an
118 // exception, maybe at least now the session is available?
119 session = ssl.getSession();
120 if(session == null) {
121 // If it's still null, probably a startHandshake() will
122 // unearth the real problem.
123 ssl.startHandshake();
124
125 // Okay, if we still haven't managed to cause an exception,
126 // might as well go for the NPE. Or maybe we're okay now?
127 session = ssl.getSession();
128 }
129 }
130
131 final Certificate[] certs = session.getPeerCertificates();
132 final X509Certificate x509 = (X509Certificate) certs[0];
133 verify(host, x509);
134 }
135
136 public final boolean verify(final String host, final SSLSession session) {
137 try {
138 final Certificate[] certs = session.getPeerCertificates();
139 final X509Certificate x509 = (X509Certificate) certs[0];
140 verify(host, x509);
141 return true;
142 }
143 catch(final SSLException e) {
144 return false;
145 }
146 }
147
148 public final void verify(final String host, final X509Certificate cert)
149 throws SSLException {
150 final String[] cns = getCNs(cert);
151 final String[] subjectAlts = getSubjectAlts(cert, host);
152 verify(host, cns, subjectAlts);
153 }
154
155 public final void verify(final String host, final String[] cns,
156 final String[] subjectAlts,
157 final boolean strictWithSubDomains)
158 throws SSLException {
159
160 // Build the list of names we're going to check. Our DEFAULT and
161 // STRICT implementations of the HostnameVerifier only use the
162 // first CN provided. All other CNs are ignored.
163 // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
164 final LinkedList<String> names = new LinkedList<String>();
165 if(cns != null && cns.length > 0 && cns[0] != null) {
166 names.add(cns[0]);
167 }
168 if(subjectAlts != null) {
169 for (final String subjectAlt : subjectAlts) {
170 if (subjectAlt != null) {
171 names.add(subjectAlt);
172 }
173 }
174 }
175
176 if(names.isEmpty()) {
177 final String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt";
178 throw new SSLException(msg);
179 }
180
181 // StringBuilder for building the error message.
182 final StringBuilder buf = new StringBuilder();
183
184 // We're can be case-insensitive when comparing the host we used to
185 // establish the socket to the hostname in the certificate.
186 final String hostName = normaliseIPv6Address(host.trim().toLowerCase(Locale.US));
187 boolean match = false;
188 for(final Iterator<String> it = names.iterator(); it.hasNext();) {
189 // Don't trim the CN, though!
190 String cn = it.next();
191 cn = cn.toLowerCase(Locale.US);
192 // Store CN in StringBuilder in case we need to report an error.
193 buf.append(" <");
194 buf.append(cn);
195 buf.append('>');
196 if(it.hasNext()) {
197 buf.append(" OR");
198 }
199
200 // The CN better have at least two dots if it wants wildcard
201 // action. It also can't be [*.co.uk] or [*.co.jp] or
202 // [*.org.uk], etc...
203 final String parts[] = cn.split("\\.");
204 final boolean doWildcard = parts.length >= 3 &&
205 parts[0].endsWith("*") &&
206 acceptableCountryWildcard(cn) &&
207 !isIPAddress(host);
208
209 if(doWildcard) {
210 final String firstpart = parts[0];
211 if (firstpart.length() > 1) { // e.g. server*
212 final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server
213 final String suffix = cn.substring(firstpart.length()); // skip wildcard part from cn
214 final String hostSuffix = hostName.substring(prefix.length()); // skip wildcard part from host
215 match = hostName.startsWith(prefix) && hostSuffix.endsWith(suffix);
216 } else {
217 match = hostName.endsWith(cn.substring(1));
218 }
219 if(match && strictWithSubDomains) {
220 // If we're in strict mode, then [*.foo.com] is not
221 // allowed to match [a.b.foo.com]
222 match = countDots(hostName) == countDots(cn);
223 }
224 } else {
225 match = hostName.equals(normaliseIPv6Address(cn));
226 }
227 if(match) {
228 break;
229 }
230 }
231 if(!match) {
232 throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf);
233 }
234 }
235
236 public static boolean acceptableCountryWildcard(final String cn) {
237 final String parts[] = cn.split("\\.");
238 if (parts.length != 3 || parts[2].length() != 2) {
239 return true; // it's not an attempt to wildcard a 2TLD within a country code
240 }
241 return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
242 }
243
244 public static String[] getCNs(final X509Certificate cert) {
245 final LinkedList<String> cnList = new LinkedList<String>();
246 /*
247 Sebastian Hauer's original StrictSSLProtocolSocketFactory used
248 getName() and had the following comment:
249
250 Parses a X.500 distinguished name for the value of the
251 "Common Name" field. This is done a bit sloppy right
252 now and should probably be done a bit more according to
253 <code>RFC 2253</code>.
254
255 I've noticed that toString() seems to do a better job than
256 getName() on these X500Principal objects, so I'm hoping that
257 addresses Sebastian's concern.
258
259 For example, getName() gives me this:
260 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
261
262 whereas toString() gives me this:
263 EMAILADDRESS=juliusdavies@cucbc.com
264
265 Looks like toString() even works with non-ascii domain names!
266 I tested it with "花子.co.jp" and it worked fine.
267 */
268
269 final String subjectPrincipal = cert.getSubjectX500Principal().toString();
270 final StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
271 while(st.hasMoreTokens()) {
272 final String tok = st.nextToken().trim();
273 if (tok.length() > 3) {
274 if (tok.substring(0, 3).equalsIgnoreCase("CN=")) {
275 cnList.add(tok.substring(3));
276 }
277 }
278 }
279 if(!cnList.isEmpty()) {
280 final String[] cns = new String[cnList.size()];
281 cnList.toArray(cns);
282 return cns;
283 } else {
284 return null;
285 }
286 }
287
288 /**
289 * Extracts the array of SubjectAlt DNS or IP names from an X509Certificate.
290 * Returns null if there aren't any.
291 *
292 * @param cert X509Certificate
293 * @param hostname
294 * @return Array of SubjectALT DNS or IP names stored in the certificate.
295 */
296 private static String[] getSubjectAlts(
297 final X509Certificate cert, final String hostname) {
298 int subjectType;
299 if (isIPAddress(hostname)) {
300 subjectType = 7;
301 } else {
302 subjectType = 2;
303 }
304
305 final LinkedList<String> subjectAltList = new LinkedList<String>();
306 Collection<List<?>> c = null;
307 try {
308 c = cert.getSubjectAlternativeNames();
309 }
310 catch(final CertificateParsingException cpe) {
311 }
312 if(c != null) {
313 for (final List<?> aC : c) {
314 final List<?> list = aC;
315 final int type = ((Integer) list.get(0)).intValue();
316 if (type == subjectType) {
317 final String s = (String) list.get(1);
318 subjectAltList.add(s);
319 }
320 }
321 }
322 if(!subjectAltList.isEmpty()) {
323 final String[] subjectAlts = new String[subjectAltList.size()];
324 subjectAltList.toArray(subjectAlts);
325 return subjectAlts;
326 } else {
327 return null;
328 }
329 }
330
331 /**
332 * Extracts the array of SubjectAlt DNS names from an X509Certificate.
333 * Returns null if there aren't any.
334 * <p/>
335 * Note: Java doesn't appear able to extract international characters
336 * from the SubjectAlts. It can only extract international characters
337 * from the CN field.
338 * <p/>
339 * (Or maybe the version of OpenSSL I'm using to test isn't storing the
340 * international characters correctly in the SubjectAlts?).
341 *
342 * @param cert X509Certificate
343 * @return Array of SubjectALT DNS names stored in the certificate.
344 */
345 public static String[] getDNSSubjectAlts(final X509Certificate cert) {
346 return getSubjectAlts(cert, null);
347 }
348
349 /**
350 * Counts the number of dots "." in a string.
351 * @param s string to count dots from
352 * @return number of dots
353 */
354 public static int countDots(final String s) {
355 int count = 0;
356 for(int i = 0; i < s.length(); i++) {
357 if(s.charAt(i) == '.') {
358 count++;
359 }
360 }
361 return count;
362 }
363
364 private static boolean isIPAddress(final String hostname) {
365 return hostname != null &&
366 (InetAddressUtils.isIPv4Address(hostname) ||
367 InetAddressUtils.isIPv6Address(hostname));
368 }
369
370 /*
371 * Check if hostname is IPv6, and if so, convert to standard format.
372 */
373 private String normaliseIPv6Address(final String hostname) {
374 if (hostname == null || !InetAddressUtils.isIPv6Address(hostname)) {
375 return hostname;
376 }
377 try {
378 InetAddress inetAddress = InetAddress.getByName(hostname);
379 return inetAddress.getHostAddress();
380 } catch (UnknownHostException uhe) { // Should not happen, because we check for IPv6 address above
381 log.error("Unexpected error converting "+hostname, uhe);
382 return hostname;
383 }
384 }
385 }