blob: c947d7d66d406950c731d0d5a692c6ae87d1c075 [file] [log] [blame]
Narayan Kamathfaf49722013-06-06 10:17:55 +01001/*
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;
Narayan Kamathfaf49722013-06-06 10:17:55 +010032
33/**
34 * A HostnameVerifier consistent with <a
35 * href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
36 */
37public final class OkHostnameVerifier implements HostnameVerifier {
jwilson74623582013-06-23 21:47:59 -040038 public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();
39
Narayan Kamathfaf49722013-06-06 10:17:55 +010040 /**
41 * Quick and dirty pattern to differentiate IP addresses from hostnames. This
42 * is an approximation of Android's private InetAddress#isNumeric API.
43 *
44 * <p>This matches IPv6 addresses as a hex string containing at least one
45 * colon, and possibly including dots after the first colon. It matches IPv4
46 * addresses as strings containing only decimal digits and dots. This pattern
47 * matches strings like "a:.23" and "54" that are neither IP addresses nor
48 * hostnames; they will be verified as IP addresses (which is a more strict
49 * verification).
50 */
51 private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
52 "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
53
54 private static final int ALT_DNS_NAME = 2;
55 private static final int ALT_IPA_NAME = 7;
56
jwilson74623582013-06-23 21:47:59 -040057 private OkHostnameVerifier() {
58 }
59
Alex Klyubin57592012014-12-02 12:51:25 -080060 @Override
Narayan Kamathfaf49722013-06-06 10:17:55 +010061 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) {
Neil Fullere78f1172015-01-20 09:39:41 +000084 List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
85 for (int i = 0, size = altNames.size(); i < size; i++) {
86 if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
Narayan Kamathfaf49722013-06-06 10:17:55 +010087 return true;
88 }
89 }
90 return false;
91 }
92
93 /**
94 * Returns true if {@code certificate} matches {@code hostName}.
95 */
96 private boolean verifyHostName(String hostName, X509Certificate certificate) {
97 hostName = hostName.toLowerCase(Locale.US);
98 boolean hasDns = false;
Neil Fullere78f1172015-01-20 09:39:41 +000099 List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
100 for (int i = 0, size = altNames.size(); i < size; i++) {
Narayan Kamathfaf49722013-06-06 10:17:55 +0100101 hasDns = true;
Neil Fullere78f1172015-01-20 09:39:41 +0000102 if (verifyHostName(hostName, altNames.get(i))) {
Narayan Kamathfaf49722013-06-06 10:17:55 +0100103 return true;
104 }
105 }
106
Tobias Thierercdbc32f2018-01-11 14:56:44 +0000107 // BEGIN Android-removed: Ignore common name in hostname verification. http://b/70278814
108 /*
Narayan Kamathfaf49722013-06-06 10:17:55 +0100109 if (!hasDns) {
110 X500Principal principal = certificate.getSubjectX500Principal();
111 // RFC 2818 advises using the most specific name for matching.
112 String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
113 if (cn != null) {
114 return verifyHostName(hostName, cn);
115 }
116 }
Tobias Thierercdbc32f2018-01-11 14:56:44 +0000117 */
118 // END Android-removed: Ignore common name in hostname verification. http://b/70278814
Narayan Kamathfaf49722013-06-06 10:17:55 +0100119
120 return false;
121 }
122
Neil Fullere78f1172015-01-20 09:39:41 +0000123 public static List<String> allSubjectAltNames(X509Certificate certificate) {
124 List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
125 List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
126 List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size());
127 result.addAll(altIpaNames);
128 result.addAll(altDnsNames);
129 return result;
130 }
131
132 private static List<String> getSubjectAltNames(X509Certificate certificate, int type) {
133 List<String> result = new ArrayList<>();
Narayan Kamathfaf49722013-06-06 10:17:55 +0100134 try {
135 Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
136 if (subjectAltNames == null) {
137 return Collections.emptyList();
138 }
139 for (Object subjectAltName : subjectAltNames) {
140 List<?> entry = (List<?>) subjectAltName;
141 if (entry == null || entry.size() < 2) {
142 continue;
143 }
144 Integer altNameType = (Integer) entry.get(0);
145 if (altNameType == null) {
146 continue;
147 }
148 if (altNameType == type) {
149 String altName = (String) entry.get(1);
150 if (altName != null) {
151 result.add(altName);
152 }
153 }
154 }
155 return result;
156 } catch (CertificateParsingException e) {
157 return Collections.emptyList();
158 }
159 }
160
161 /**
Alex Klyubin57592012014-12-02 12:51:25 -0800162 * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern}.
Narayan Kamathfaf49722013-06-06 10:17:55 +0100163 *
Alex Klyubin57592012014-12-02 12:51:25 -0800164 * @param hostName lower-case host name.
165 * @param pattern domain name pattern from certificate. May be a wildcard pattern such as
166 * {@code *.android.com}.
Narayan Kamathfaf49722013-06-06 10:17:55 +0100167 */
Alex Klyubin57592012014-12-02 12:51:25 -0800168 private boolean verifyHostName(String hostName, String pattern) {
169 // Basic sanity checks
Narayan Kamathfaf49722013-06-06 10:17:55 +0100170 // Check length == 0 instead of .isEmpty() to support Java 5.
Alex Klyubin57592012014-12-02 12:51:25 -0800171 if ((hostName == null) || (hostName.length() == 0) || (hostName.startsWith("."))
172 || (hostName.endsWith(".."))) {
173 // Invalid domain name
174 return false;
175 }
176 if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith("."))
177 || (pattern.endsWith(".."))) {
178 // Invalid pattern/domain name
Narayan Kamathfaf49722013-06-06 10:17:55 +0100179 return false;
180 }
181
Alex Klyubin57592012014-12-02 12:51:25 -0800182 // Normalize hostName and pattern by turning them into absolute domain names if they are not
183 // yet absolute. This is needed because server certificates do not normally contain absolute
184 // names or patterns, but they should be treated as absolute. At the same time, any hostName
185 // presented to this method should also be treated as absolute for the purposes of matching
186 // to the server certificate.
187 // www.android.com matches www.android.com
188 // www.android.com matches www.android.com.
189 // www.android.com. matches www.android.com.
190 // www.android.com. matches www.android.com
191 if (!hostName.endsWith(".")) {
192 hostName += '.';
193 }
194 if (!pattern.endsWith(".")) {
195 pattern += '.';
196 }
197 // hostName and pattern are now absolute domain names.
Narayan Kamathfaf49722013-06-06 10:17:55 +0100198
Alex Klyubin57592012014-12-02 12:51:25 -0800199 pattern = pattern.toLowerCase(Locale.US);
200 // hostName and pattern are now in lower case -- domain names are case-insensitive.
201
202 if (!pattern.contains("*")) {
203 // Not a wildcard pattern -- hostName and pattern must match exactly.
204 return hostName.equals(pattern);
205 }
206 // Wildcard pattern
207
208 // WILDCARD PATTERN RULES:
209 // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
210 // only character in that label (i.e., must match the whole left-most label).
211 // For example, *.example.com is permitted, while *a.example.com, a*.example.com,
212 // a*b.example.com, a.*.example.com are not permitted.
213 // 2. Asterisk (*) cannot match across domain name labels.
214 // For example, *.example.com matches test.example.com but does not match
215 // sub.test.example.com.
216 // 3. Wildcard patterns for single-label domain names are not permitted.
217
218 if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) {
219 // Asterisk (*) is only permitted in the left-most domain name label and must be the only
220 // character in that label
221 return false;
Narayan Kamathfaf49722013-06-06 10:17:55 +0100222 }
223
Alex Klyubin57592012014-12-02 12:51:25 -0800224 // Optimization: check whether hostName is too short to match the pattern. hostName must be at
225 // least as long as the pattern because asterisk must match the whole left-most label and
226 // hostName starts with a non-empty label. Thus, asterisk has to match one or more characters.
227 if (hostName.length() < pattern.length()) {
228 // hostName too short to match the pattern.
229 return false;
Narayan Kamathfaf49722013-06-06 10:17:55 +0100230 }
231
Alex Klyubin57592012014-12-02 12:51:25 -0800232 if ("*.".equals(pattern)) {
233 // Wildcard pattern for single-label domain name -- not permitted.
234 return false;
Narayan Kamathfaf49722013-06-06 10:17:55 +0100235 }
236
Alex Klyubin57592012014-12-02 12:51:25 -0800237 // hostName must end with the region of pattern following the asterisk.
238 String suffix = pattern.substring(1);
239 if (!hostName.endsWith(suffix)) {
240 // hostName does not end with the suffix
241 return false;
Narayan Kamathfaf49722013-06-06 10:17:55 +0100242 }
243
Alex Klyubin57592012014-12-02 12:51:25 -0800244 // Check that asterisk did not match across domain name labels.
245 int suffixStartIndexInHostName = hostName.length() - suffix.length();
246 if ((suffixStartIndexInHostName > 0)
247 && (hostName.lastIndexOf('.', suffixStartIndexInHostName - 1) != -1)) {
248 // Asterisk is matching across domain name labels -- not permitted.
249 return false;
Narayan Kamathfaf49722013-06-06 10:17:55 +0100250 }
251
Alex Klyubin57592012014-12-02 12:51:25 -0800252 // hostName matches pattern
Narayan Kamathfaf49722013-06-06 10:17:55 +0100253 return true;
254 }
255}