    001/*
    002 * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.15/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
    032package org.apache.commons.ssl;
    033
    034import javax.net.ssl.SSLException;
    035import javax.net.ssl.SSLPeerUnverifiedException;
    036import javax.net.ssl.SSLSession;
    037import javax.net.ssl.SSLSocket;
    038import java.io.IOException;
    039import java.io.InputStream;
    040import java.security.cert.Certificate;
    041import java.security.cert.X509Certificate;
    042import java.util.Arrays;
    043import java.util.Iterator;
    044import 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 */
    070public 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}