• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java $
3  * $Revision: 653041 $
4  * $Date: 2008-05-03 03:39:28 -0700 (Sat, 03 May 2008) $
5  *
6  * ====================================================================
7  * Licensed to the Apache Software Foundation (ASF) under one
8  * or more contributor license agreements.  See the NOTICE file
9  * distributed with this work for additional information
10  * regarding copyright ownership.  The ASF licenses this file
11  * to you under the Apache License, Version 2.0 (the
12  * "License"); you may not use this file except in compliance
13  * with the License.  You may obtain a copy of the License at
14  *
15  *   http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing,
18  * software distributed under the License is distributed on an
19  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20  * KIND, either express or implied.  See the License for the
21  * specific language governing permissions and limitations
22  * under the License.
23  * ====================================================================
24  *
25  * This software consists of voluntary contributions made by many
26  * individuals on behalf of the Apache Software Foundation.  For more
27  * information on the Apache Software Foundation, please see
28  * <http://www.apache.org/>.
29  *
30  */
31 
32 package org.apache.http.conn.ssl;
33 
34 import org.apache.http.conn.util.InetAddressUtils;
35 
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.security.cert.Certificate;
39 import java.security.cert.CertificateParsingException;
40 import java.security.cert.X509Certificate;
41 import java.util.Arrays;
42 import java.util.Collection;
43 import java.util.Iterator;
44 import java.util.LinkedList;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.StringTokenizer;
48 import java.util.logging.Logger;
49 import java.util.logging.Level;
50 
51 import javax.net.ssl.SSLException;
52 import javax.net.ssl.SSLSession;
53 import javax.net.ssl.SSLSocket;
54 
55 /**
56  * Abstract base class for all standard {@link X509HostnameVerifier}
57  * implementations.
58  *
59  * @author Julius Davies
60  */
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 
AbstractVerifier()82     public AbstractVerifier() {
83         super();
84     }
85 
verify(String host, SSLSocket ssl)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 
92         SSLSession session = ssl.getSession();
93         Certificate[] certs = session.getPeerCertificates();
94         X509Certificate x509 = (X509Certificate) certs[0];
95         verify(host, x509);
96     }
97 
verify(String host, SSLSession session)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 
verify(String host, X509Certificate cert)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 
verify(final String host, final String[] cns, final String[] subjectAlts, final boolean strictWithSubDomains)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 + "> doesn't contain CN or DNS subjectAlt";
140             throw new SSLException(msg);
141         }
142 
143         // StringBuffer for building the error message.
144         StringBuffer buf = new StringBuffer();
145 
146         // We're can be case-insensitive when comparing the host we used to
147         // establish the socket to the hostname in the certificate.
148         String hostName = host.trim().toLowerCase(Locale.ENGLISH);
149         boolean match = false;
150         for(Iterator<String> it = names.iterator(); it.hasNext();) {
151             // Don't trim the CN, though!
152             String cn = it.next();
153             cn = cn.toLowerCase(Locale.ENGLISH);
154             // Store CN in StringBuffer in case we need to report an error.
155             buf.append(" <");
156             buf.append(cn);
157             buf.append('>');
158             if(it.hasNext()) {
159                 buf.append(" OR");
160             }
161 
162             // The CN better have at least two dots if it wants wildcard
163             // action.  It also can't be [*.co.uk] or [*.co.jp] or
164             // [*.org.uk], etc...
165             boolean doWildcard = cn.startsWith("*.") &&
166                                  cn.indexOf('.', 2) != -1 &&
167                                  acceptableCountryWildcard(cn) &&
168                                  !InetAddressUtils.isIPv4Address(host);
169 
170             if(doWildcard) {
171                 match = hostName.endsWith(cn.substring(1));
172                 if(match && strictWithSubDomains) {
173                     // If we're in strict mode, then [*.foo.com] is not
174                     // allowed to match [a.b.foo.com]
175                     match = countDots(hostName) == countDots(cn);
176                 }
177             } else {
178                 match = hostName.equals(cn);
179             }
180             if(match) {
181                 break;
182             }
183         }
184         if(!match) {
185             throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf);
186         }
187     }
188 
acceptableCountryWildcard(String cn)189     public static boolean acceptableCountryWildcard(String cn) {
190         int cnLen = cn.length();
191         if(cnLen >= 7 && cnLen <= 9) {
192             // Look for the '.' in the 3rd-last position:
193             if(cn.charAt(cnLen - 3) == '.') {
194                 // Trim off the [*.] and the [.XX].
195                 String s = cn.substring(2, cnLen - 3);
196                 // And test against the sorted array of bad 2lds:
197                 int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s);
198                 return x < 0;
199             }
200         }
201         return true;
202     }
203 
getCNs(X509Certificate cert)204     public static String[] getCNs(X509Certificate cert) {
205         LinkedList<String> cnList = new LinkedList<String>();
206         /*
207           Sebastian Hauer's original StrictSSLProtocolSocketFactory used
208           getName() and had the following comment:
209 
210                 Parses a X.500 distinguished name for the value of the
211                 "Common Name" field.  This is done a bit sloppy right
212                  now and should probably be done a bit more according to
213                 <code>RFC 2253</code>.
214 
215            I've noticed that toString() seems to do a better job than
216            getName() on these X500Principal objects, so I'm hoping that
217            addresses Sebastian's concern.
218 
219            For example, getName() gives me this:
220            1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
221 
222            whereas toString() gives me this:
223            EMAILADDRESS=juliusdavies@cucbc.com
224 
225            Looks like toString() even works with non-ascii domain names!
226            I tested it with "&#x82b1;&#x5b50;.co.jp" and it worked fine.
227         */
228         String subjectPrincipal = cert.getSubjectX500Principal().toString();
229         StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
230         while(st.hasMoreTokens()) {
231             String tok = st.nextToken();
232             int x = tok.indexOf("CN=");
233             if(x >= 0) {
234                 cnList.add(tok.substring(x + 3));
235             }
236         }
237         if(!cnList.isEmpty()) {
238             String[] cns = new String[cnList.size()];
239             cnList.toArray(cns);
240             return cns;
241         } else {
242             return null;
243         }
244     }
245 
246 
247     /**
248      * Extracts the array of SubjectAlt DNS names from an X509Certificate.
249      * Returns null if there aren't any.
250      * <p/>
251      * Note:  Java doesn't appear able to extract international characters
252      * from the SubjectAlts.  It can only extract international characters
253      * from the CN field.
254      * <p/>
255      * (Or maybe the version of OpenSSL I'm using to test isn't storing the
256      * international characters correctly in the SubjectAlts?).
257      *
258      * @param cert X509Certificate
259      * @return Array of SubjectALT DNS names stored in the certificate.
260      */
getDNSSubjectAlts(X509Certificate cert)261     public static String[] getDNSSubjectAlts(X509Certificate cert) {
262         LinkedList<String> subjectAltList = new LinkedList<String>();
263         Collection<List<?>> c = null;
264         try {
265             c = cert.getSubjectAlternativeNames();
266         }
267         catch(CertificateParsingException cpe) {
268             Logger.getLogger(AbstractVerifier.class.getName())
269                     .log(Level.FINE, "Error parsing certificate.", cpe);
270         }
271         if(c != null) {
272             for (List<?> aC : c) {
273                 List<?> list = aC;
274                 int type = ((Integer) list.get(0)).intValue();
275                 // If type is 2, then we've got a dNSName
276                 if (type == 2) {
277                     String s = (String) list.get(1);
278                     subjectAltList.add(s);
279                 }
280             }
281         }
282         if(!subjectAltList.isEmpty()) {
283             String[] subjectAlts = new String[subjectAltList.size()];
284             subjectAltList.toArray(subjectAlts);
285             return subjectAlts;
286         } else {
287             return null;
288         }
289     }
290 
291     /**
292      * Counts the number of dots "." in a string.
293      * @param s  string to count dots from
294      * @return  number of dots
295      */
countDots(final String s)296     public static int countDots(final String s) {
297         int count = 0;
298         for(int i = 0; i < s.length(); i++) {
299             if(s.charAt(i) == '.') {
300                 count++;
301             }
302         }
303         return count;
304     }
305 
306 }
307