Home | History | Annotate | Download | only in ssl
      1 /*
      2  *  Licensed to the Apache Software Foundation (ASF) under one or more
      3  *  contributor license agreements.  See the NOTICE file distributed with
      4  *  this work for additional information regarding copyright ownership.
      5  *  The ASF licenses this file to You under the Apache License, Version 2.0
      6  *  (the "License"); you may not use this file except in compliance with
      7  *  the License.  You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *  Unless required by applicable law or agreed to in writing, software
     12  *  distributed under the License is distributed on an "AS IS" BASIS,
     13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *  See the License for the specific language governing permissions and
     15  *  limitations under the License.
     16  */
     17 
     18 package javax.net.ssl;
     19 
     20 import java.net.InetAddress;
     21 import java.security.cert.Certificate;
     22 import java.security.cert.CertificateParsingException;
     23 import java.security.cert.X509Certificate;
     24 import java.util.ArrayList;
     25 import java.util.Arrays;
     26 import java.util.Collection;
     27 import java.util.Collections;
     28 import java.util.List;
     29 import java.util.Locale;
     30 
     31 /**
     32  * A HostnameVerifier that works the same way as Curl and Firefox.
     33  *
     34  * <p>The hostname must match either the first CN, or any of the subject-alts.
     35  * A wildcard can occur in the CN, and in any of the subject-alts.
     36  *
     37  * @author Julius Davies
     38  */
     39 class DefaultHostnameVerifier implements HostnameVerifier {
     40 
     41     /**
     42      * This contains a list of 2nd-level domains that aren't allowed to
     43      * have wildcards when combined with country-codes.
     44      * For example: [*.co.uk].
     45      *
     46      * <p>The [*.co.uk] problem is an interesting one.  Should we just hope
     47      * that CA's would never foolishly allow such a certificate to happen?
     48      * Looks like we're the only implementation guarding against this.
     49      * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
     50      */
     51     private static final String[] BAD_COUNTRY_2LDS =
     52           { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
     53             "lg", "ne", "net", "or", "org" };
     54 
     55     static {
     56         // Just in case developer forgot to manually sort the array.  :-)
     57         Arrays.sort(BAD_COUNTRY_2LDS);
     58     }
     59 
     60     public final boolean verify(String host, SSLSession session) {
     61         Certificate[] certs;
     62         try {
     63             certs = session.getPeerCertificates();
     64         } catch (SSLException e) {
     65             return false;
     66         }
     67 
     68         X509Certificate x509 = (X509Certificate) certs[0];
     69 
     70         // We can be case-insensitive when comparing the host we used to
     71         // establish the socket to the hostname in the certificate.
     72         String hostName = host.trim().toLowerCase(Locale.ENGLISH);
     73 
     74         // Verify the first CN provided. Other CNs are ignored. Firefox, wget,
     75         // curl, and Sun Java work this way.
     76         String firstCn = getFirstCn(x509);
     77         if (matches(hostName, firstCn)) {
     78             return true;
     79         }
     80 
     81         for (String cn : getDNSSubjectAlts(x509)) {
     82             if (matches(hostName, cn)) {
     83                 return true;
     84             }
     85         }
     86 
     87         return false;
     88     }
     89 
     90     /**
     91      * Returns true if {@code hostname} matches {@code cn}.
     92      *
     93      * @param hostName a trimmed, lowercase hostname to verify
     94      * @param cn a certificate CN or DNS subject alt. Either a literal name or
     95      *     a wildcard of the form "*.google.com".
     96      */
     97     private boolean matches(String hostName, String cn) {
     98         if (cn == null) {
     99             return false;
    100         }
    101 
    102         // Don't trim the CN, though!
    103         cn = cn.toLowerCase(Locale.ENGLISH);
    104 
    105         if (cn.startsWith("*.")) {
    106             // When a wildcard matches, also check that the wildcard is legit
    107             //   - Wildcards must contain at least two dots: "*.google.com"
    108             //   - Wildcards must be for private domains. No "*.co.uk" etc.
    109             //   - Wildcards must not match IP addresses: "*.8.8"
    110             int matchLength = cn.length() - 1;
    111             return hostName.regionMatches(hostName.length() - matchLength, cn, 1, matchLength)
    112                     && cn.indexOf('.', 2) != -1
    113                     && acceptableCountryWildcard(cn)
    114                     && !InetAddress.isNumeric(hostName);
    115         } else {
    116             return hostName.equals(cn);
    117         }
    118     }
    119 
    120     private boolean acceptableCountryWildcard(String cn) {
    121         int cnLen = cn.length();
    122         if (cnLen >= 7 && cnLen <= 9) {
    123             // Look for the '.' in the 3rd-last position:
    124             if (cn.charAt(cnLen - 3) == '.') {
    125                 // Trim off the [*.] and the [.XX].
    126                 String s = cn.substring(2, cnLen - 3);
    127                 // And test against the sorted array of bad 2lds:
    128                 int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s);
    129                 return x < 0;
    130             }
    131         }
    132         return true;
    133     }
    134 
    135     private String getFirstCn(X509Certificate cert) {
    136         /*
    137          * Sebastian Hauer's original StrictSSLProtocolSocketFactory used
    138          * getName() and had the following comment:
    139          *
    140          *      Parses a X.500 distinguished name for the value of the
    141          *     "Common Name" field.  This is done a bit sloppy right
    142          *     now and should probably be done a bit more according to
    143          *     <code>RFC 2253</code>.
    144          *
    145          * I've noticed that toString() seems to do a better job than
    146          * getName() on these X500Principal objects, so I'm hoping that
    147          * addresses Sebastian's concern.
    148          *
    149          * For example, getName() gives me this:
    150          * 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
    151          *
    152          * whereas toString() gives me this:
    153          * EMAILADDRESS=juliusdavies (at) cucbc.com
    154          *
    155          * Looks like toString() even works with non-ascii domain names!
    156          * I tested it with "&#x82b1;&#x5b50;.co.jp" and it worked fine.
    157          */
    158         String subjectPrincipal = cert.getSubjectX500Principal().toString();
    159         for (String token : subjectPrincipal.split(",")) {
    160             int x = token.indexOf("CN=");
    161             if (x >= 0) {
    162                 return token.substring(x + 3);
    163             }
    164         }
    165         return null;
    166     }
    167 
    168     /**
    169      * Returns all SubjectAlt DNS names from an X509Certificate.
    170      *
    171      * <p>Note: Java doesn't appear able to extract international characters
    172      * from the SubjectAlts.  It can only extract international characters
    173      * from the CN field.
    174      *
    175      * <p>(Or maybe the version of OpenSSL I'm using to test isn't storing the
    176      * international characters correctly in the SubjectAlts?).
    177      */
    178     private List<String> getDNSSubjectAlts(X509Certificate cert) {
    179         Collection<List<?>> subjectAlternativeNames;
    180         try {
    181             subjectAlternativeNames = cert.getSubjectAlternativeNames();
    182         } catch (CertificateParsingException cpe) {
    183             System.logI("Error parsing certificate", cpe);
    184             return Collections.emptyList();
    185         }
    186 
    187         if (subjectAlternativeNames == null) {
    188             return Collections.emptyList();
    189         }
    190 
    191         List<String> subjectAltList = new ArrayList<String>();
    192         for (List<?> pair : subjectAlternativeNames) {
    193             int type = (Integer) pair.get(0);
    194             // If type is 2, then we've got a dNSName
    195             if (type == 2) {
    196                 subjectAltList.add((String) pair.get(1));
    197             }
    198         }
    199         return subjectAltList;
    200     }
    201 }
    202