• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
OkHostnameVerifier()58   private OkHostnameVerifier() {
59   }
60 
61   @Override
verify(String host, SSLSession session)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 
verify(String host, X509Certificate certificate)71   public boolean verify(String host, X509Certificate certificate) {
72     return verifyAsIpAddress(host)
73         ? verifyIpAddress(host, certificate)
74         : verifyHostName(host, certificate);
75   }
76 
verifyAsIpAddress(String host)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    */
verifyIpAddress(String ipAddress, X509Certificate certificate)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    */
verifyHostName(String hostName, X509Certificate certificate)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 
allSubjectAltNames(X509Certificate certificate)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 
getSubjectAltNames(X509Certificate certificate, int type)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    */
verifyHostName(String hostName, String pattern)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