Home | History | Annotate | Download | only in tls
      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 com.squareup.okhttp.internal.tls;
     19 
     20 import java.security.cert.Certificate;
     21 import java.security.cert.CertificateParsingException;
     22 import java.security.cert.X509Certificate;
     23 import java.util.ArrayList;
     24 import java.util.Collection;
     25 import java.util.Collections;
     26 import java.util.List;
     27 import java.util.Locale;
     28 import java.util.regex.Pattern;
     29 import javax.net.ssl.HostnameVerifier;
     30 import javax.net.ssl.SSLException;
     31 import javax.net.ssl.SSLSession;
     32 import javax.security.auth.x500.X500Principal;
     33 
     34 /**
     35  * A HostnameVerifier consistent with <a
     36  * href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
     37  */
     38 public final class OkHostnameVerifier implements HostnameVerifier {
     39   public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();
     40 
     41   /**
     42    * Quick and dirty pattern to differentiate IP addresses from hostnames. This
     43    * is an approximation of Android's private InetAddress#isNumeric API.
     44    *
     45    * <p>This matches IPv6 addresses as a hex string containing at least one
     46    * colon, and possibly including dots after the first colon. It matches IPv4
     47    * addresses as strings containing only decimal digits and dots. This pattern
     48    * matches strings like "a:.23" and "54" that are neither IP addresses nor
     49    * hostnames; they will be verified as IP addresses (which is a more strict
     50    * verification).
     51    */
     52   private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
     53       "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
     54 
     55   private static final int ALT_DNS_NAME = 2;
     56   private static final int ALT_IPA_NAME = 7;
     57 
     58   private OkHostnameVerifier() {
     59   }
     60 
     61   @Override
     62   public boolean verify(String host, SSLSession session) {
     63     try {
     64       Certificate[] certificates = session.getPeerCertificates();
     65       return verify(host, (X509Certificate) certificates[0]);
     66     } catch (SSLException e) {
     67       return false;
     68     }
     69   }
     70 
     71   public boolean verify(String host, X509Certificate certificate) {
     72     return verifyAsIpAddress(host)
     73         ? verifyIpAddress(host, certificate)
     74         : verifyHostName(host, certificate);
     75   }
     76 
     77   static boolean verifyAsIpAddress(String host) {
     78     return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
     79   }
     80 
     81   /**
     82    * Returns true if {@code certificate} matches {@code ipAddress}.
     83    */
     84   private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
     85     List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
     86     for (int i = 0, size = altNames.size(); i < size; i++) {
     87       if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
     88         return true;
     89       }
     90     }
     91     return false;
     92   }
     93 
     94   /**
     95    * Returns true if {@code certificate} matches {@code hostName}.
     96    */
     97   private boolean verifyHostName(String hostName, X509Certificate certificate) {
     98     hostName = hostName.toLowerCase(Locale.US);
     99     boolean hasDns = false;
    100     List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
    101     for (int i = 0, size = altNames.size(); i < size; i++) {
    102       hasDns = true;
    103       if (verifyHostName(hostName, altNames.get(i))) {
    104         return true;
    105       }
    106     }
    107 
    108     if (!hasDns) {
    109       X500Principal principal = certificate.getSubjectX500Principal();
    110       // RFC 2818 advises using the most specific name for matching.
    111       String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
    112       if (cn != null) {
    113         return verifyHostName(hostName, cn);
    114       }
    115     }
    116 
    117     return false;
    118   }
    119 
    120   public static List<String> allSubjectAltNames(X509Certificate certificate) {
    121     List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
    122     List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
    123     List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size());
    124     result.addAll(altIpaNames);
    125     result.addAll(altDnsNames);
    126     return result;
    127   }
    128 
    129   private static List<String> getSubjectAltNames(X509Certificate certificate, int type) {
    130     List<String> result = new ArrayList<>();
    131     try {
    132       Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
    133       if (subjectAltNames == null) {
    134         return Collections.emptyList();
    135       }
    136       for (Object subjectAltName : subjectAltNames) {
    137         List<?> entry = (List<?>) subjectAltName;
    138         if (entry == null || entry.size() < 2) {
    139           continue;
    140         }
    141         Integer altNameType = (Integer) entry.get(0);
    142         if (altNameType == null) {
    143           continue;
    144         }
    145         if (altNameType == type) {
    146           String altName = (String) entry.get(1);
    147           if (altName != null) {
    148             result.add(altName);
    149           }
    150         }
    151       }
    152       return result;
    153     } catch (CertificateParsingException e) {
    154       return Collections.emptyList();
    155     }
    156   }
    157 
    158   /**
    159    * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern}.
    160    *
    161    * @param hostName lower-case host name.
    162    * @param pattern domain name pattern from certificate. May be a wildcard pattern such as
    163    *        {@code *.android.com}.
    164    */
    165   private boolean verifyHostName(String hostName, String pattern) {
    166     // Basic sanity checks
    167     // Check length == 0 instead of .isEmpty() to support Java 5.
    168     if ((hostName == null) || (hostName.length() == 0) || (hostName.startsWith("."))
    169         || (hostName.endsWith(".."))) {
    170       // Invalid domain name
    171       return false;
    172     }
    173     if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith("."))
    174         || (pattern.endsWith(".."))) {
    175       // Invalid pattern/domain name
    176       return false;
    177     }
    178 
    179     // Normalize hostName and pattern by turning them into absolute domain names if they are not
    180     // yet absolute. This is needed because server certificates do not normally contain absolute
    181     // names or patterns, but they should be treated as absolute. At the same time, any hostName
    182     // presented to this method should also be treated as absolute for the purposes of matching
    183     // to the server certificate.
    184     //   www.android.com  matches www.android.com
    185     //   www.android.com  matches www.android.com.
    186     //   www.android.com. matches www.android.com.
    187     //   www.android.com. matches www.android.com
    188     if (!hostName.endsWith(".")) {
    189       hostName += '.';
    190     }
    191     if (!pattern.endsWith(".")) {
    192       pattern += '.';
    193     }
    194     // hostName and pattern are now absolute domain names.
    195 
    196     pattern = pattern.toLowerCase(Locale.US);
    197     // hostName and pattern are now in lower case -- domain names are case-insensitive.
    198 
    199     if (!pattern.contains("*")) {
    200       // Not a wildcard pattern -- hostName and pattern must match exactly.
    201       return hostName.equals(pattern);
    202     }
    203     // Wildcard pattern
    204 
    205     // WILDCARD PATTERN RULES:
    206     // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
    207     //    only character in that label (i.e., must match the whole left-most label).
    208     //    For example, *.example.com is permitted, while *a.example.com, a*.example.com,
    209     //    a*b.example.com, a.*.example.com are not permitted.
    210     // 2. Asterisk (*) cannot match across domain name labels.
    211     //    For example, *.example.com matches test.example.com but does not match
    212     //    sub.test.example.com.
    213     // 3. Wildcard patterns for single-label domain names are not permitted.
    214 
    215     if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) {
    216       // Asterisk (*) is only permitted in the left-most domain name label and must be the only
    217       // character in that label
    218       return false;
    219     }
    220 
    221     // Optimization: check whether hostName is too short to match the pattern. hostName must be at
    222     // least as long as the pattern because asterisk must match the whole left-most label and
    223     // hostName starts with a non-empty label. Thus, asterisk has to match one or more characters.
    224     if (hostName.length() < pattern.length()) {
    225       // hostName too short to match the pattern.
    226       return false;
    227     }
    228 
    229     if ("*.".equals(pattern)) {
    230       // Wildcard pattern for single-label domain name -- not permitted.
    231       return false;
    232     }
    233 
    234     // hostName must end with the region of pattern following the asterisk.
    235     String suffix = pattern.substring(1);
    236     if (!hostName.endsWith(suffix)) {
    237       // hostName does not end with the suffix
    238       return false;
    239     }
    240 
    241     // Check that asterisk did not match across domain name labels.
    242     int suffixStartIndexInHostName = hostName.length() - suffix.length();
    243     if ((suffixStartIndexInHostName > 0)
    244         && (hostName.lastIndexOf('.', suffixStartIndexInHostName - 1) != -1)) {
    245       // Asterisk is matching across domain name labels -- not permitted.
    246       return false;
    247     }
    248 
    249     // hostName matches pattern
    250     return true;
    251   }
    252 }
    253