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  @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