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.security.cert.Certificate;
33  import java.security.cert.X509Certificate;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.List;
37  import java.util.Locale;
38  
39  import javax.net.ssl.SSLException;
40  import javax.net.ssl.SSLSession;
41  import javax.net.ssl.SSLSocket;
42  import javax.security.auth.x500.X500Principal;
43  
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  import org.apache.http.conn.util.InetAddressUtils;
47  import org.apache.http.util.Args;
48  
49  /**
50   * Abstract base class for all standard {@link X509HostnameVerifier}
51   * implementations.
52   *
53   * @since 4.0
54   *
55   * @deprecated (4.4) use an implementation of {@link javax.net.ssl.HostnameVerifier} or
56   *  {@link DefaultHostnameVerifier}.
57   */
58  @Deprecated
59  public abstract class AbstractVerifier implements X509HostnameVerifier {
60  
61      private final Log log = LogFactory.getLog(getClass());
62  
63      final static String[] BAD_COUNTRY_2LDS =
64              { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
65                      "lg", "ne", "net", "or", "org" };
66  
67      static {
68          // Just in case developer forgot to manually sort the array.  :-)
69          Arrays.sort(BAD_COUNTRY_2LDS);
70      }
71  
72      @Override
73      public final void verify(final String host, final SSLSocket ssl)
74              throws IOException {
75          Args.notNull(host, "Host");
76          SSLSession session = ssl.getSession();
77          if(session == null) {
78              // In our experience this only happens under IBM 1.4.x when
79              // spurious (unrelated) certificates show up in the server'
80              // chain.  Hopefully this will unearth the real problem:
81              final InputStream in = ssl.getInputStream();
82              in.available();
83              /*
84                If you're looking at the 2 lines of code above because
85                you're running into a problem, you probably have two
86                options:
87  
88                  #1.  Clean up the certificate chain that your server
89                       is presenting (e.g. edit "/etc/apache2/server.crt"
90                       or wherever it is your server's certificate chain
91                       is defined).
92  
93                                             OR
94  
95                  #2.   Upgrade to an IBM 1.5.x or greater JVM, or switch
96                        to a non-IBM JVM.
97              */
98  
99              // If ssl.getInputStream().available() didn't cause an
100             // exception, maybe at least now the session is available?
101             session = ssl.getSession();
102             if(session == null) {
103                 // If it's still null, probably a startHandshake() will
104                 // unearth the real problem.
105                 ssl.startHandshake();
106 
107                 // Okay, if we still haven't managed to cause an exception,
108                 // might as well go for the NPE.  Or maybe we're okay now?
109                 session = ssl.getSession();
110             }
111         }
112 
113         final Certificate[] certs = session.getPeerCertificates();
114         final X509Certificate x509 = (X509Certificate) certs[0];
115         verify(host, x509);
116     }
117 
118     @Override
119     public final boolean verify(final String host, final SSLSession session) {
120         try {
121             final Certificate[] certs = session.getPeerCertificates();
122             final X509Certificate x509 = (X509Certificate) certs[0];
123             verify(host, x509);
124             return true;
125         } catch(final SSLException ex) {
126             if (log.isDebugEnabled()) {
127                 log.debug(ex.getMessage(), ex);
128             }
129             return false;
130         }
131     }
132 
133     @Override
134     public final void verify(
135             final String host, final X509Certificate cert) throws SSLException {
136         final List<SubjectName> allSubjectAltNames = DefaultHostnameVerifier.getSubjectAltNames(cert);
137         final List<String> subjectAlts = new ArrayList<String>();
138         if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host)) {
139             for (SubjectName subjectName: allSubjectAltNames) {
140                 if (subjectName.getType() == SubjectName.IP) {
141                     subjectAlts.add(subjectName.getValue());
142                 }
143             }
144         } else {
145             for (SubjectName subjectName: allSubjectAltNames) {
146                 if (subjectName.getType() == SubjectName.DNS) {
147                     subjectAlts.add(subjectName.getValue());
148                 }
149             }
150         }
151         final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
152         final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
153         verify(host,
154                 cn != null ? new String[] {cn} : null,
155                 subjectAlts != null && !subjectAlts.isEmpty() ? subjectAlts.toArray(new String[subjectAlts.size()]) : null);
156     }
157 
158     public final void verify(final String host, final String[] cns,
159                              final String[] subjectAlts,
160                              final boolean strictWithSubDomains)
161             throws SSLException {
162 
163         final String cn = cns != null && cns.length > 0 ? cns[0] : null;
164         final List<String> subjectAltList = subjectAlts != null && subjectAlts.length > 0 ? Arrays.asList(subjectAlts) : null;
165 
166         final String normalizedHost = InetAddressUtils.isIPv6Address(host) ?
167                 DefaultHostnameVerifier.normaliseAddress(host.toLowerCase(Locale.ROOT)) : host;
168 
169         if (subjectAltList != null) {
170             for (final String subjectAlt: subjectAltList) {
171                 final String normalizedAltSubject = InetAddressUtils.isIPv6Address(subjectAlt) ?
172                         DefaultHostnameVerifier.normaliseAddress(subjectAlt) : subjectAlt;
173                 if (matchIdentity(normalizedHost, normalizedAltSubject, strictWithSubDomains)) {
174                     return;
175                 }
176             }
177             throw new SSLException("Certificate for <" + host + "> doesn't match any " +
178                     "of the subject alternative names: " + subjectAltList);
179         } else if (cn != null) {
180             final String normalizedCN = InetAddressUtils.isIPv6Address(cn) ?
181                     DefaultHostnameVerifier.normaliseAddress(cn) : cn;
182             if (matchIdentity(normalizedHost, normalizedCN, strictWithSubDomains)) {
183                 return;
184             }
185             throw new SSLException("Certificate for <" + host + "> doesn't match " +
186                     "common name of the certificate subject: " + cn);
187         } else {
188             throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
189                     "a common name and does not have alternative names");
190         }
191     }
192 
193     private static boolean matchIdentity(final String host, final String identity, final boolean strict) {
194         if (host == null) {
195             return false;
196         }
197         final String normalizedHost = host.toLowerCase(Locale.ROOT);
198         final String normalizedIdentity = identity.toLowerCase(Locale.ROOT);
199         // The CN better have at least two dots if it wants wildcard
200         // action.  It also can't be [*.co.uk] or [*.co.jp] or
201         // [*.org.uk], etc...
202         final String parts[] = normalizedIdentity.split("\\.");
203         final boolean doWildcard = parts.length >= 3 && parts[0].endsWith("*") &&
204                 (!strict || validCountryWildcard(parts));
205         if (doWildcard) {
206             boolean match;
207             final String firstpart = parts[0];
208             if (firstpart.length() > 1) { // e.g. server*
209                 final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server
210                 final String suffix = normalizedIdentity.substring(firstpart.length()); // skip wildcard part from cn
211                 final String hostSuffix = normalizedHost.substring(prefix.length()); // skip wildcard part from normalizedHost
212                 match = normalizedHost.startsWith(prefix) && hostSuffix.endsWith(suffix);
213             } else {
214                 match = normalizedHost.endsWith(normalizedIdentity.substring(1));
215             }
216             return match && (!strict || countDots(normalizedHost) == countDots(normalizedIdentity));
217         } else {
218             return normalizedHost.equals(normalizedIdentity);
219         }
220     }
221 
222     private static boolean validCountryWildcard(final String parts[]) {
223         if (parts.length != 3 || parts[2].length() != 2) {
224             return true; // it's not an attempt to wildcard a 2TLD within a country code
225         }
226         return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
227     }
228 
229     public static boolean acceptableCountryWildcard(final String cn) {
230         return validCountryWildcard(cn.split("\\."));
231     }
232 
233     public static String[] getCNs(final X509Certificate cert) {
234         final String subjectPrincipal = cert.getSubjectX500Principal().toString();
235         try {
236             final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal);
237             return cn != null ? new String[] { cn } : null;
238         } catch (final SSLException ex) {
239             return null;
240         }
241     }
242 
243     /**
244      * Extracts the array of SubjectAlt DNS names from an X509Certificate.
245      * Returns null if there aren't any.
246      * <p>
247      * Note:  Java doesn't appear able to extract international characters
248      * from the SubjectAlts.  It can only extract international characters
249      * from the CN field.
250      * </p>
251      * <p>
252      * (Or maybe the version of OpenSSL I'm using to test isn't storing the
253      * international characters correctly in the SubjectAlts?).
254      * </p>
255      *
256      * @param cert X509Certificate
257      * @return Array of SubjectALT DNS names stored in the certificate.
258      */
259     public static String[] getDNSSubjectAlts(final X509Certificate cert) {
260         final List<SubjectName> subjectAltNames = DefaultHostnameVerifier.getSubjectAltNames(cert);
261         if (subjectAltNames == null) {
262             return null;
263         }
264         final List<String> dnsAlts = new ArrayList<String>();
265         for (SubjectName subjectName: subjectAltNames) {
266             if (subjectName.getType() == SubjectName.DNS) {
267                 dnsAlts.add(subjectName.getValue());
268             }
269         }
270         return dnsAlts.isEmpty() ? dnsAlts.toArray(new String[dnsAlts.size()]) : null;
271     }
272 
273     /**
274      * Counts the number of dots "." in a string.
275      * @param s  string to count dots from
276      * @return  number of dots
277      */
278     public static int countDots(final String s) {
279         int count = 0;
280         for(int i = 0; i < s.length(); i++) {
281             if(s.charAt(i) == '.') {
282                 count++;
283             }
284         }
285         return count;
286     }
287 
288 }