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
18package com.squareup.okhttp.internal.tls;
19
20import java.security.cert.Certificate;
21import java.security.cert.CertificateParsingException;
22import java.security.cert.X509Certificate;
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.Collections;
26import java.util.List;
27import java.util.Locale;
28import java.util.regex.Pattern;
29import javax.net.ssl.HostnameVerifier;
30import javax.net.ssl.SSLException;
31import javax.net.ssl.SSLSession;
32import 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 */
38public 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