001    /*
002     * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.11/src/java/org/apache/commons/ssl/HostnameVerifier.java $
003     * $Revision: 121 $
004     * $Date: 2007-11-13 21:26:57 -0800 (Tue, 13 Nov 2007) $
005     *
006     * ====================================================================
007     * Licensed to the Apache Software Foundation (ASF) under one
008     * or more contributor license agreements.  See the NOTICE file
009     * distributed with this work for additional information
010     * regarding copyright ownership.  The ASF licenses this file
011     * to you under the Apache License, Version 2.0 (the
012     * "License"); you may not use this file except in compliance
013     * with the License.  You may obtain a copy of the License at
014     *
015     *   http://www.apache.org/licenses/LICENSE-2.0
016     *
017     * Unless required by applicable law or agreed to in writing,
018     * software distributed under the License is distributed on an
019     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
020     * KIND, either express or implied.  See the License for the
021     * specific language governing permissions and limitations
022     * under the License.
023     * ====================================================================
024     *
025     * This software consists of voluntary contributions made by many
026     * individuals on behalf of the Apache Software Foundation.  For more
027     * information on the Apache Software Foundation, please see
028     * <http://www.apache.org/>.
029     *
030     */
031    
032    package org.apache.commons.ssl;
033    
034    import javax.net.ssl.SSLException;
035    import javax.net.ssl.SSLPeerUnverifiedException;
036    import javax.net.ssl.SSLSession;
037    import javax.net.ssl.SSLSocket;
038    import java.io.IOException;
039    import java.io.InputStream;
040    import java.security.cert.Certificate;
041    import java.security.cert.X509Certificate;
042    import java.util.Arrays;
043    import java.util.Iterator;
044    import java.util.TreeSet;
045    
046    /**
047     * Interface for checking if a hostname matches the names stored inside the
048     * server's X.509 certificate.  Correctly implements
049     * javax.net.ssl.HostnameVerifier, but that interface is not recommended.
050     * Instead we added several check() methods that take SSLSocket,
051     * or X509Certificate, or ultimately (they all end up calling this one),
052     * String.  (It's easier to supply JUnit with Strings instead of mock
053     * SSLSession objects!)
054     * </p><p>Our check() methods throw exceptions if the name is
055     * invalid, whereas javax.net.ssl.HostnameVerifier just returns true/false.
056     * <p/>
057     * We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and
058     * HostnameVerifier.ALLOW_ALL implementations.  We also provide the more
059     * specialized HostnameVerifier.DEFAULT_AND_LOCALHOST, as well as
060     * HostnameVerifier.STRICT_IE6.  But feel free to define your own
061     * implementations!
062     * <p/>
063     * Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the
064     * HttpClient "contrib" repository.
065     *
066     * @author Julius Davies
067     * @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a>
068     * @since 8-Dec-2006
069     */
070    public interface HostnameVerifier extends javax.net.ssl.HostnameVerifier {
071    
072        boolean verify(String host, SSLSession session);
073    
074        void check(String host, SSLSocket ssl) throws IOException;
075    
076        void check(String host, X509Certificate cert) throws SSLException;
077    
078        void check(String host, String[] cns, String[] subjectAlts)
079            throws SSLException;
080    
081        void check(String[] hosts, SSLSocket ssl) throws IOException;
082    
083        void check(String[] hosts, X509Certificate cert) throws SSLException;
084    
085    
086        /**
087         * Checks to see if the supplied hostname matches any of the supplied CNs
088         * or "DNS" Subject-Alts.  Most implementations only look at the first CN,
089         * and ignore any additional CNs.  Most implementations do look at all of
090         * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards
091         * according to RFC 2818.
092         *
093         * @param cns         CN fields, in order, as extracted from the X.509
094         *                    certificate.
095         * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted
096         *                    from the X.509 certificate.
097         * @param hosts       The array of hostnames to verify.
098         * @throws SSLException If verification failed.
099         */
100        void check(String[] hosts, String[] cns, String[] subjectAlts)
101            throws SSLException;
102    
103    
104        /**
105         * The DEFAULT HostnameVerifier works the same way as Curl and Firefox.
106         * <p/>
107         * The hostname must match either the first CN, or any of the subject-alts.
108         * A wildcard can occur in the CN, and in any of the subject-alts.
109         * <p/>
110         * The only difference between DEFAULT and STRICT is that a wildcard (such
111         * as "*.foo.com") with DEFAULT matches all subdomains, including
112         * "a.b.foo.com".
113         */
114        public final static HostnameVerifier DEFAULT =
115            new AbstractVerifier() {
116                public final void check(final String[] hosts, final String[] cns,
117                                        final String[] subjectAlts)
118                    throws SSLException {
119                    check(hosts, cns, subjectAlts, false, false);
120                }
121    
122                public final String toString() { return "DEFAULT"; }
123            };
124    
125    
126        /**
127         * The DEFAULT_AND_LOCALHOST HostnameVerifier works like the DEFAULT
128         * one with one additional relaxation:  a host of "localhost",
129         * "localhost.localdomain", "127.0.0.1", "::1" will always pass, no matter
130         * what is in the server's certificate.
131         */
132        public final static HostnameVerifier DEFAULT_AND_LOCALHOST =
133            new AbstractVerifier() {
134                public final void check(final String[] hosts, final String[] cns,
135                                        final String[] subjectAlts)
136                    throws SSLException {
137                    if (isLocalhost(hosts[0])) {
138                        return;
139                    }
140                    check(hosts, cns, subjectAlts, false, false);
141                }
142    
143                public final String toString() { return "DEFAULT_AND_LOCALHOST"; }
144            };
145    
146        /**
147         * The STRICT HostnameVerifier works the same way as java.net.URL in Sun
148         * Java 1.4, Sun Java 5, Sun Java 6.  It's also pretty close to IE6.
149         * This implementation appears to be compliant with RFC 2818 for dealing
150         * with wildcards.
151         * <p/>
152         * The hostname must match either the first CN, or any of the subject-alts.
153         * A wildcard can occur in the CN, and in any of the subject-alts.  The
154         * one divergence from IE6 is how we only check the first CN.  IE6 allows
155         * a match against any of the CNs present.  We decided to follow in
156         * Sun Java 1.4's footsteps and only check the first CN.
157         * <p/>
158         * A wildcard such as "*.foo.com" matches only subdomains in the same
159         * level, for example "a.foo.com".  It does not match deeper subdomains
160         * such as "a.b.foo.com".
161         */
162        public final static HostnameVerifier STRICT =
163            new AbstractVerifier() {
164                public final void check(final String[] host, final String[] cns,
165                                        final String[] subjectAlts)
166                    throws SSLException {
167                    check(host, cns, subjectAlts, false, true);
168                }
169    
170                public final String toString() { return "STRICT"; }
171            };
172    
173        /**
174         * The STRICT_IE6 HostnameVerifier works just like the STRICT one with one
175         * minor variation:  the hostname can match against any of the CN's in the
176         * server's certificate, not just the first one.  This behaviour is
177         * identical to IE6's behaviour.
178         */
179        public final static HostnameVerifier STRICT_IE6 =
180            new AbstractVerifier() {
181                public final void check(final String[] host, final String[] cns,
182                                        final String[] subjectAlts)
183                    throws SSLException {
184                    check(host, cns, subjectAlts, true, true);
185                }
186    
187                public final String toString() { return "STRICT_IE6"; }
188            };
189    
190        /**
191         * The ALLOW_ALL HostnameVerifier essentially turns hostname verification
192         * off.  This implementation is a no-op, and never throws the SSLException.
193         */
194        public final static HostnameVerifier ALLOW_ALL =
195            new AbstractVerifier() {
196                public final void check(final String[] host, final String[] cns,
197                                        final String[] subjectAlts) {
198                    // Allow everything - so never blowup.
199                }
200    
201                public final String toString() { return "ALLOW_ALL"; }
202            };
203    
204        abstract class AbstractVerifier implements HostnameVerifier {
205    
206            /**
207             * This contains a list of 2nd-level domains that aren't allowed to
208             * have wildcards when combined with country-codes.
209             * For example: [*.co.uk].
210             * <p/>
211             * The [*.co.uk] problem is an interesting one.  Should we just hope
212             * that CA's would never foolishly allow such a certificate to happen?
213             * Looks like we're the only implementation guarding against this.
214             * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
215             */
216            private final static String[] BAD_COUNTRY_2LDS =
217                {"ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
218                    "lg", "ne", "net", "or", "org"};
219    
220            private final static String[] LOCALHOSTS = {"::1", "127.0.0.1",
221                "localhost",
222                "localhost.localdomain"};
223    
224    
225            static {
226                // Just in case developer forgot to manually sort the array.  :-)
227                Arrays.sort(BAD_COUNTRY_2LDS);
228                Arrays.sort(LOCALHOSTS);
229            }
230    
231            protected AbstractVerifier() {}
232    
233            /**
234             * The javax.net.ssl.HostnameVerifier contract.
235             *
236             * @param host    'hostname' we used to create our socket
237             * @param session SSLSession with the remote server
238             * @return true if the host matched the one in the certificate.
239             */
240            public boolean verify(String host, SSLSession session) {
241                try {
242                    Certificate[] certs = session.getPeerCertificates();
243                    X509Certificate x509 = (X509Certificate) certs[0];
244                    check(new String[]{host}, x509);
245                    return true;
246                }
247                catch (SSLException e) {
248                    return false;
249                }
250            }
251    
252            public void check(String host, SSLSocket ssl) throws IOException {
253                check(new String[]{host}, ssl);
254            }
255    
256            public void check(String host, X509Certificate cert)
257                throws SSLException {
258                check(new String[]{host}, cert);
259            }
260    
261            public void check(String host, String[] cns, String[] subjectAlts)
262                throws SSLException {
263                check(new String[]{host}, cns, subjectAlts);
264            }
265    
266            public void check(String host[], SSLSocket ssl)
267                throws IOException {
268                if (host == null) {
269                    throw new NullPointerException("host to verify is null");
270                }
271    
272                SSLSession session = ssl.getSession();
273                if (session == null) {
274                    // In our experience this only happens under IBM 1.4.x when
275                    // spurious (unrelated) certificates show up in the server'
276                    // chain.  Hopefully this will unearth the real problem:
277                    InputStream in = ssl.getInputStream();
278                    in.available();
279                    /*
280                      If you're looking at the 2 lines of code above because
281                      you're running into a problem, you probably have two
282                      options:
283    
284                        #1.  Clean up the certificate chain that your server
285                             is presenting (e.g. edit "/etc/apache2/server.crt"
286                             or wherever it is your server's certificate chain
287                             is defined).
288    
289                                                   OR
290    
291                        #2.   Upgrade to an IBM 1.5.x or greater JVM, or switch
292                              to a non-IBM JVM.
293                    */
294    
295                    // If ssl.getInputStream().available() didn't cause an
296                    // exception, maybe at least now the session is available?
297                    session = ssl.getSession();
298                    if (session == null) {
299                        // If it's still null, probably a startHandshake() will
300                        // unearth the real problem.
301                        ssl.startHandshake();
302    
303                        // Okay, if we still haven't managed to cause an exception,
304                        // might as well go for the NPE.  Or maybe we're okay now?
305                        session = ssl.getSession();
306                    }
307                }
308                Certificate[] certs;
309                try {
310                    certs = session.getPeerCertificates();
311                } catch (SSLPeerUnverifiedException spue) {
312                    InputStream in = ssl.getInputStream();
313                    in.available();
314                    // Didn't trigger anything interesting?  Okay, just throw
315                    // original.
316                    throw spue;
317                }
318                X509Certificate x509 = (X509Certificate) certs[0];
319                check(host, x509);
320            }
321    
322            public void check(String[] host, X509Certificate cert)
323                throws SSLException {
324                String[] cns = Certificates.getCNs(cert);
325                String[] subjectAlts = Certificates.getDNSSubjectAlts(cert);
326                check(host, cns, subjectAlts);
327            }
328    
329            public void check(final String[] hosts, final String[] cns,
330                              final String[] subjectAlts, final boolean ie6,
331                              final boolean strictWithSubDomains)
332                throws SSLException {
333                // Build up lists of allowed hosts For logging/debugging purposes.
334                StringBuffer buf = new StringBuffer(32);
335                buf.append('<');
336                for (int i = 0; i < hosts.length; i++) {
337                    String h = hosts[i];
338                    h = h != null ? h.trim().toLowerCase() : "";
339                    hosts[i] = h;
340                    if (i > 0) {
341                        buf.append('/');
342                    }
343                    buf.append(h);
344                }
345                buf.append('>');
346                String hostnames = buf.toString();
347                // Build the list of names we're going to check.  Our DEFAULT and
348                // STRICT implementations of the HostnameVerifier only use the
349                // first CN provided.  All other CNs are ignored.
350                // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
351                TreeSet names = new TreeSet();
352                if (cns != null && cns.length > 0 && cns[0] != null) {
353                    names.add(cns[0]);
354                    if (ie6) {
355                        for (int i = 1; i < cns.length; i++) {
356                            names.add(cns[i]);
357                        }
358                    }
359                }
360                if (subjectAlts != null) {
361                    for (int i = 0; i < subjectAlts.length; i++) {
362                        if (subjectAlts[i] != null) {
363                            names.add(subjectAlts[i]);
364                        }
365                    }
366                }
367                if (names.isEmpty()) {
368                    String msg = "Certificate for " + hosts[0] + " doesn't contain CN or DNS subjectAlt";
369                    throw new SSLException(msg);
370                }
371    
372                // StringBuffer for building the error message.
373                buf = new StringBuffer();
374    
375                boolean match = false;
376                out:
377                for (Iterator it = names.iterator(); it.hasNext();) {
378                    // Don't trim the CN, though!
379                    String cn = (String) it.next();
380                    cn = cn.toLowerCase();
381                    // Store CN in StringBuffer in case we need to report an error.
382                    buf.append(" <");
383                    buf.append(cn);
384                    buf.append('>');
385                    if (it.hasNext()) {
386                        buf.append(" OR");
387                    }
388    
389                    // The CN better have at least two dots if it wants wildcard
390                    // action.  It also can't be [*.co.uk] or [*.co.jp] or
391                    // [*.org.uk], etc...
392                    boolean doWildcard = cn.startsWith("*.") &&
393                                         cn.lastIndexOf('.') >= 0 &&
394                                         !isIP4Address(cn) &&
395                                         acceptableCountryWildcard(cn);
396    
397                    for (int i = 0; i < hosts.length; i++) {
398                        final String hostName = hosts[i].trim().toLowerCase();
399                        if (doWildcard) {
400                            match = hostName.endsWith(cn.substring(1));
401                            if (match && strictWithSubDomains) {
402                                // If we're in strict mode, then [*.foo.com] is not
403                                // allowed to match [a.b.foo.com]
404                                match = countDots(hostName) == countDots(cn);
405                            }
406                        } else {
407                            match = hostName.equals(cn);
408                        }
409                        if (match) {
410                            break out;
411                        }
412                    }
413                }
414                if (!match) {
415                    throw new SSLException("hostname in certificate didn't match: " + hostnames + " !=" + buf);
416                }
417            }
418    
419            public static boolean isIP4Address(final String cn) {
420                boolean isIP4 = true;
421                String tld = cn;
422                int x = cn.lastIndexOf('.');
423                // We only bother analyzing the characters after the final dot
424                // in the name.
425                if (x >= 0 && x + 1 < cn.length()) {
426                    tld = cn.substring(x + 1);
427                }
428                for (int i = 0; i < tld.length(); i++) {
429                    if (!Character.isDigit(tld.charAt(0))) {
430                        isIP4 = false;
431                        break;
432                    }
433                }
434                return isIP4;
435            }
436    
437            public static boolean acceptableCountryWildcard(final String cn) {
438                int cnLen = cn.length();
439                if (cnLen >= 7 && cnLen <= 9) {
440                    // Look for the '.' in the 3rd-last position:
441                    if (cn.charAt(cnLen - 3) == '.') {
442                        // Trim off the [*.] and the [.XX].
443                        String s = cn.substring(2, cnLen - 3);
444                        // And test against the sorted array of bad 2lds:
445                        int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s);
446                        return x < 0;
447                    }
448                }
449                return true;
450            }
451    
452            public static boolean isLocalhost(String host) {
453                host = host != null ? host.trim().toLowerCase() : "";
454                if (host.startsWith("::1")) {
455                    int x = host.lastIndexOf('%');
456                    if (x >= 0) {
457                        host = host.substring(0, x);
458                    }
459                }
460                int x = Arrays.binarySearch(LOCALHOSTS, host);
461                return x >= 0;
462            }
463    
464            /**
465             * Counts the number of dots "." in a string.
466             *
467             * @param s string to count dots from
468             * @return number of dots
469             */
470            public static int countDots(final String s) {
471                int count = 0;
472                for (int i = 0; i < s.length(); i++) {
473                    if (s.charAt(i) == '.') {
474                        count++;
475                    }
476                }
477                return count;
478            }
479        }
480    
481    }