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