• 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.logging.Logger;
48 import java.util.logging.Level;
49 
50 import javax.net.ssl.DistinguishedNameParser;
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         DistinguishedNameParser dnParser =
206                 new DistinguishedNameParser(cert.getSubjectX500Principal());
207         List<String> cnList = dnParser.getAllMostSpecificFirst("cn");
208 
209         if(!cnList.isEmpty()) {
210             String[] cns = new String[cnList.size()];
211             cnList.toArray(cns);
212             return cns;
213         } else {
214             return null;
215         }
216     }
217 
218 
219     /**
220      * Extracts the array of SubjectAlt DNS names from an X509Certificate.
221      * Returns null if there aren't any.
222      * <p/>
223      * Note:  Java doesn't appear able to extract international characters
224      * from the SubjectAlts.  It can only extract international characters
225      * from the CN field.
226      * <p/>
227      * (Or maybe the version of OpenSSL I'm using to test isn't storing the
228      * international characters correctly in the SubjectAlts?).
229      *
230      * @param cert X509Certificate
231      * @return Array of SubjectALT DNS names stored in the certificate.
232      */
getDNSSubjectAlts(X509Certificate cert)233     public static String[] getDNSSubjectAlts(X509Certificate cert) {
234         LinkedList<String> subjectAltList = new LinkedList<String>();
235         Collection<List<?>> c = null;
236         try {
237             c = cert.getSubjectAlternativeNames();
238         }
239         catch(CertificateParsingException cpe) {
240             Logger.getLogger(AbstractVerifier.class.getName())
241                     .log(Level.FINE, "Error parsing certificate.", cpe);
242         }
243         if(c != null) {
244             for (List<?> aC : c) {
245                 List<?> list = aC;
246                 int type = ((Integer) list.get(0)).intValue();
247                 // If type is 2, then we've got a dNSName
248                 if (type == 2) {
249                     String s = (String) list.get(1);
250                     subjectAltList.add(s);
251                 }
252             }
253         }
254         if(!subjectAltList.isEmpty()) {
255             String[] subjectAlts = new String[subjectAltList.size()];
256             subjectAltList.toArray(subjectAlts);
257             return subjectAlts;
258         } else {
259             return null;
260         }
261     }
262 
263     /**
264      * Counts the number of dots "." in a string.
265      * @param s  string to count dots from
266      * @return  number of dots
267      */
countDots(final String s)268     public static int countDots(final String s) {
269         int count = 0;
270         for(int i = 0; i < s.length(); i++) {
271             if(s.charAt(i) == '.') {
272                 count++;
273             }
274         }
275         return count;
276     }
277 
278 }
279