001/*
002 * $HeadURL: file:///opt/dev/not-yet-commons-ssl-SVN-repo/tags/commons-ssl-0.3.17/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}