View Javadoc

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.ArrayList;
38  import java.util.Arrays;
39  import java.util.Collection;
40  import java.util.Iterator;
41  import java.util.LinkedList;
42  import java.util.List;
43  import java.util.Locale;
44  import java.util.NoSuchElementException;
45  
46  import javax.naming.InvalidNameException;
47  import javax.naming.NamingException;
48  import javax.naming.directory.Attribute;
49  import javax.naming.directory.Attributes;
50  import javax.naming.ldap.LdapName;
51  import javax.naming.ldap.Rdn;
52  import javax.net.ssl.SSLException;
53  import javax.net.ssl.SSLSession;
54  import javax.net.ssl.SSLSocket;
55  
56  import org.apache.commons.logging.Log;
57  import org.apache.commons.logging.LogFactory;
58  import org.apache.http.annotation.Immutable;
59  import org.apache.http.conn.util.InetAddressUtils;
60  
61  /**
62   * Abstract base class for all standard {@link X509HostnameVerifier}
63   * implementations.
64   *
65   * @since 4.0
66   */
67  @Immutable
68  public abstract class AbstractVerifier implements X509HostnameVerifier {
69  
70      /**
71       * This contains a list of 2nd-level domains that aren't allowed to
72       * have wildcards when combined with country-codes.
73       * For example: [*.co.uk].
74       * <p/>
75       * The [*.co.uk] problem is an interesting one.  Should we just hope
76       * that CA's would never foolishly allow such a certificate to happen?
77       * Looks like we're the only implementation guarding against this.
78       * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
79       */
80      private final static String[] BAD_COUNTRY_2LDS =
81            { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
82              "lg", "ne", "net", "or", "org" };
83  
84      static {
85          // Just in case developer forgot to manually sort the array.  :-)
86          Arrays.sort(BAD_COUNTRY_2LDS);
87      }
88  
89      private final Log log = LogFactory.getLog(getClass());
90  
91      public AbstractVerifier() {
92          super();
93      }
94  
95      public final void verify(final String host, final SSLSocket ssl)
96            throws IOException {
97          if(host == null) {
98              throw new NullPointerException("host to verify is null");
99          }
100 
101         SSLSession session = ssl.getSession();
102         if(session == null) {
103             // In our experience this only happens under IBM 1.4.x when
104             // spurious (unrelated) certificates show up in the server'
105             // chain.  Hopefully this will unearth the real problem:
106             final InputStream in = ssl.getInputStream();
107             in.available();
108             /*
109               If you're looking at the 2 lines of code above because
110               you're running into a problem, you probably have two
111               options:
112 
113                 #1.  Clean up the certificate chain that your server
114                      is presenting (e.g. edit "/etc/apache2/server.crt"
115                      or wherever it is your server's certificate chain
116                      is defined).
117 
118                                            OR
119 
120                 #2.   Upgrade to an IBM 1.5.x or greater JVM, or switch
121                       to a non-IBM JVM.
122             */
123 
124             // If ssl.getInputStream().available() didn't cause an
125             // exception, maybe at least now the session is available?
126             session = ssl.getSession();
127             if(session == null) {
128                 // If it's still null, probably a startHandshake() will
129                 // unearth the real problem.
130                 ssl.startHandshake();
131 
132                 // Okay, if we still haven't managed to cause an exception,
133                 // might as well go for the NPE.  Or maybe we're okay now?
134                 session = ssl.getSession();
135             }
136         }
137 
138         final Certificate[] certs = session.getPeerCertificates();
139         final X509Certificate x509 = (X509Certificate) certs[0];
140         verify(host, x509);
141     }
142 
143     public final boolean verify(final String host, final SSLSession session) {
144         try {
145             final Certificate[] certs = session.getPeerCertificates();
146             final X509Certificate x509 = (X509Certificate) certs[0];
147             verify(host, x509);
148             return true;
149         }
150         catch(final SSLException e) {
151             return false;
152         }
153     }
154 
155     public final void verify(final String host, final X509Certificate cert)
156           throws SSLException {
157         final String[] cns = getCNs(cert);
158         final String[] subjectAlts = getSubjectAlts(cert, host);
159         verify(host, cns, subjectAlts);
160     }
161 
162     public final void verify(final String host, final String[] cns,
163                              final String[] subjectAlts,
164                              final boolean strictWithSubDomains)
165           throws SSLException {
166 
167         // Build the list of names we're going to check.  Our DEFAULT and
168         // STRICT implementations of the HostnameVerifier only use the
169         // first CN provided.  All other CNs are ignored.
170         // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
171         final LinkedList<String> names = new LinkedList<String>();
172         if(cns != null && cns.length > 0 && cns[0] != null) {
173             names.add(cns[0]);
174         }
175         if(subjectAlts != null) {
176             for (final String subjectAlt : subjectAlts) {
177                 if (subjectAlt != null) {
178                     names.add(subjectAlt);
179                 }
180             }
181         }
182 
183         if(names.isEmpty()) {
184             final String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt";
185             throw new SSLException(msg);
186         }
187 
188         // StringBuilder for building the error message.
189         final StringBuilder buf = new StringBuilder();
190 
191         // We're can be case-insensitive when comparing the host we used to
192         // establish the socket to the hostname in the certificate.
193         final String hostName = normaliseIPv6Address(host.trim().toLowerCase(Locale.ENGLISH));
194         boolean match = false;
195         for(final Iterator<String> it = names.iterator(); it.hasNext();) {
196             // Don't trim the CN, though!
197             String cn = it.next();
198             cn = cn.toLowerCase(Locale.ENGLISH);
199             // Store CN in StringBuilder in case we need to report an error.
200             buf.append(" <");
201             buf.append(cn);
202             buf.append('>');
203             if(it.hasNext()) {
204                 buf.append(" OR");
205             }
206 
207             // The CN better have at least two dots if it wants wildcard
208             // action.  It also can't be [*.co.uk] or [*.co.jp] or
209             // [*.org.uk], etc...
210             final String parts[] = cn.split("\\.");
211             final boolean doWildcard =
212                     parts.length >= 3 && parts[0].endsWith("*") &&
213                     validCountryWildcard(cn) && !isIPAddress(host);
214 
215             if(doWildcard) {
216                 final String firstpart = parts[0];
217                 if (firstpart.length() > 1) { // e.g. server*
218                     final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server
219                     final String suffix = cn.substring(firstpart.length()); // skip wildcard part from cn
220                     final String hostSuffix = hostName.substring(prefix.length()); // skip wildcard part from host
221                     match = hostName.startsWith(prefix) && hostSuffix.endsWith(suffix);
222                 } else {
223                     match = hostName.endsWith(cn.substring(1));
224                 }
225                 if(match && strictWithSubDomains) {
226                     // If we're in strict mode, then [*.foo.com] is not
227                     // allowed to match [a.b.foo.com]
228                     match = countDots(hostName) == countDots(cn);
229                 }
230             } else {
231                 match = hostName.equals(normaliseIPv6Address(cn));
232             }
233             if(match) {
234                 break;
235             }
236         }
237         if(!match) {
238             throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf);
239         }
240     }
241 
242     /**
243      * @deprecated (4.3.1) should not be a part of public APIs.
244      */
245     @Deprecated
246     public static boolean acceptableCountryWildcard(final String cn) {
247         final String parts[] = cn.split("\\.");
248         if (parts.length != 3 || parts[2].length() != 2) {
249             return true; // it's not an attempt to wildcard a 2TLD within a country code
250         }
251         return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
252     }
253 
254     boolean validCountryWildcard(final String cn) {
255         final String parts[] = cn.split("\\.");
256         if (parts.length != 3 || parts[2].length() != 2) {
257             return true; // it's not an attempt to wildcard a 2TLD within a country code
258         }
259         return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
260     }
261 
262     public static String[] getCNs(final X509Certificate cert) {
263         final String subjectPrincipal = cert.getSubjectX500Principal().toString();
264         try {
265             return extractCNs(subjectPrincipal);
266         } catch (SSLException ex) {
267             return null;
268         }
269     }
270 
271     static String[] extractCNs(final String subjectPrincipal) throws SSLException {
272         if (subjectPrincipal == null) {
273             return null;
274         }
275         final List<String> cns = new ArrayList<String>();
276         try {
277             final LdapName subjectDN = new LdapName(subjectPrincipal);
278             final List<Rdn> rdns = subjectDN.getRdns();
279             for (int i = rdns.size() - 1; i >= 0; i--) {
280                 final Rdn rds = rdns.get(i);
281                 final Attributes attributes = rds.toAttributes();
282                 final Attribute cn = attributes.get("cn");
283                 if (cn != null) {
284                     try {
285                         final Object value = cn.get();
286                         if (value != null) {
287                             cns.add(value.toString());
288                         }
289                     } catch (NoSuchElementException ignore) {
290                     } catch (NamingException ignore) {
291                     }
292                 }
293             }
294         } catch (InvalidNameException e) {
295             throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
296         }
297         return cns.isEmpty() ? null : cns.toArray(new String[ cns.size() ]);
298     }
299 
300     /**
301      * Extracts the array of SubjectAlt DNS or IP names from an X509Certificate.
302      * Returns null if there aren't any.
303      *
304      * @param cert X509Certificate
305      * @param hostname
306      * @return Array of SubjectALT DNS or IP names stored in the certificate.
307      */
308     private static String[] getSubjectAlts(
309             final X509Certificate cert, final String hostname) {
310         final int subjectType;
311         if (isIPAddress(hostname)) {
312             subjectType = 7;
313         } else {
314             subjectType = 2;
315         }
316 
317         final LinkedList<String> subjectAltList = new LinkedList<String>();
318         Collection<List<?>> c = null;
319         try {
320             c = cert.getSubjectAlternativeNames();
321         }
322         catch(final CertificateParsingException cpe) {
323         }
324         if(c != null) {
325             for (final List<?> aC : c) {
326                 final List<?> list = aC;
327                 final int type = ((Integer) list.get(0)).intValue();
328                 if (type == subjectType) {
329                     final String s = (String) list.get(1);
330                     subjectAltList.add(s);
331                 }
332             }
333         }
334         if(!subjectAltList.isEmpty()) {
335             final String[] subjectAlts = new String[subjectAltList.size()];
336             subjectAltList.toArray(subjectAlts);
337             return subjectAlts;
338         } else {
339             return null;
340         }
341     }
342 
343     /**
344      * Extracts the array of SubjectAlt DNS names from an X509Certificate.
345      * Returns null if there aren't any.
346      * <p/>
347      * Note:  Java doesn't appear able to extract international characters
348      * from the SubjectAlts.  It can only extract international characters
349      * from the CN field.
350      * <p/>
351      * (Or maybe the version of OpenSSL I'm using to test isn't storing the
352      * international characters correctly in the SubjectAlts?).
353      *
354      * @param cert X509Certificate
355      * @return Array of SubjectALT DNS names stored in the certificate.
356      */
357     public static String[] getDNSSubjectAlts(final X509Certificate cert) {
358         return getSubjectAlts(cert, null);
359     }
360 
361     /**
362      * Counts the number of dots "." in a string.
363      * @param s  string to count dots from
364      * @return  number of dots
365      */
366     public static int countDots(final String s) {
367         int count = 0;
368         for(int i = 0; i < s.length(); i++) {
369             if(s.charAt(i) == '.') {
370                 count++;
371             }
372         }
373         return count;
374     }
375 
376     private static boolean isIPAddress(final String hostname) {
377         return hostname != null &&
378             (InetAddressUtils.isIPv4Address(hostname) ||
379                     InetAddressUtils.isIPv6Address(hostname));
380     }
381 
382     /*
383      * Check if hostname is IPv6, and if so, convert to standard format.
384      */
385     private String normaliseIPv6Address(final String hostname) {
386         if (hostname == null || !InetAddressUtils.isIPv6Address(hostname)) {
387             return hostname;
388         }
389         try {
390             final InetAddress inetAddress = InetAddress.getByName(hostname);
391             return inetAddress.getHostAddress();
392         } catch (final UnknownHostException uhe) { // Should not happen, because we check for IPv6 address above
393             log.error("Unexpected error converting "+hostname, uhe);
394             return hostname;
395         }
396     }
397 }