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   public boolean verify(String host, SSLSession session) {
     62     try {
     63       Certificate[] certificates = session.getPeerCertificates();
     64       return verify(host, (X509Certificate) certificates[0]);
     65     } catch (SSLException e) {
     66       return false;
     67     }
     68   }
     69 
     70   public boolean verify(String host, X509Certificate certificate) {
     71     return verifyAsIpAddress(host)
     72         ? verifyIpAddress(host, certificate)
     73         : verifyHostName(host, certificate);
     74   }
     75 
     76   static boolean verifyAsIpAddress(String host) {
     77     return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
     78   }
     79 
     80   /**
     81    * Returns true if {@code certificate} matches {@code ipAddress}.
     82    */
     83   private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
     84     for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) {
     85       if (ipAddress.equalsIgnoreCase(altName)) {
     86         return true;
     87       }
     88     }
     89     return false;
     90   }
     91 
     92   /**
     93    * Returns true if {@code certificate} matches {@code hostName}.
     94    */
     95   private boolean verifyHostName(String hostName, X509Certificate certificate) {
     96     hostName = hostName.toLowerCase(Locale.US);
     97     boolean hasDns = false;
     98     for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) {
     99       hasDns = true;
    100       if (verifyHostName(hostName, altName)) {
    101         return true;
    102       }
    103     }
    104 
    105     if (!hasDns) {
    106       X500Principal principal = certificate.getSubjectX500Principal();
    107       // RFC 2818 advises using the most specific name for matching.
    108       String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
    109       if (cn != null) {
    110         return verifyHostName(hostName, cn);
    111       }
    112     }
    113 
    114     return false;
    115   }
    116 
    117   private List<String> getSubjectAltNames(X509Certificate certificate, int type) {
    118     List<String> result = new ArrayList<String>();
    119     try {
    120       Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
    121       if (subjectAltNames == null) {
    122         return Collections.emptyList();
    123       }
    124       for (Object subjectAltName : subjectAltNames) {
    125         List<?> entry = (List<?>) subjectAltName;
    126         if (entry == null || entry.size() < 2) {
    127           continue;
    128         }
    129         Integer altNameType = (Integer) entry.get(0);
    130         if (altNameType == null) {
    131           continue;
    132         }
    133         if (altNameType == type) {
    134           String altName = (String) entry.get(1);
    135           if (altName != null) {
    136             result.add(altName);
    137           }
    138         }
    139       }
    140       return result;
    141     } catch (CertificateParsingException e) {
    142       return Collections.emptyList();
    143     }
    144   }
    145 
    146   /**
    147    * Returns true if {@code hostName} matches the name or pattern {@code cn}.
    148    *
    149    * @param hostName lowercase host name.
    150    * @param cn certificate host name. May include wildcards like
    151    *     {@code *.android.com}.
    152    */
    153   public boolean verifyHostName(String hostName, String cn) {
    154     // Check length == 0 instead of .isEmpty() to support Java 5.
    155     if (hostName == null || hostName.length() == 0 || cn == null || cn.length() == 0) {
    156       return false;
    157     }
    158 
    159     cn = cn.toLowerCase(Locale.US);
    160 
    161     if (!cn.contains("*")) {
    162       return hostName.equals(cn);
    163     }
    164 
    165     if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
    166       return true; // "*.foo.com" matches "foo.com"
    167     }
    168 
    169     int asterisk = cn.indexOf('*');
    170     int dot = cn.indexOf('.');
    171     if (asterisk > dot) {
    172       return false; // malformed; wildcard must be in the first part of the cn
    173     }
    174 
    175     if (!hostName.regionMatches(0, cn, 0, asterisk)) {
    176       return false; // prefix before '*' doesn't match
    177     }
    178 
    179     int suffixLength = cn.length() - (asterisk + 1);
    180     int suffixStart = hostName.length() - suffixLength;
    181     if (hostName.indexOf('.', asterisk) < suffixStart) {
    182       // TODO: remove workaround for *.clients.google.com http://b/5426333
    183       if (!hostName.endsWith(".clients.google.com")) {
    184         return false; // wildcard '*' can't match a '.'
    185       }
    186     }
    187 
    188     if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) {
    189       return false; // suffix after '*' doesn't match
    190     }
    191 
    192     return true;
    193   }
    194 }
    195