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 package okhttp3.internal.tls;
18
19 import java.security.cert.Certificate;
20 import java.security.cert.CertificateParsingException;
21 import java.security.cert.X509Certificate;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Locale;
27 import javax.net.ssl.HostnameVerifier;
28 import javax.net.ssl.SSLException;
29 import javax.net.ssl.SSLSession;
30
31 import static okhttp3.internal.Util.verifyAsIpAddress;
32
33 /**
34 * A HostnameVerifier consistent with <a href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
35 */
36 public final class OkHostnameVerifier implements HostnameVerifier {
37 public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();
38
39 private static final int ALT_DNS_NAME = 2;
40 private static final int ALT_IPA_NAME = 7;
41
42 private OkHostnameVerifier() {
43 }
44
45 @Override
46 public boolean verify(String host, SSLSession session) {
47 try {
48 Certificate[] certificates = session.getPeerCertificates();
49 return verify(host, (X509Certificate) certificates[0]);
50 } catch (SSLException e) {
51 return false;
52 }
53 }
54
55 public boolean verify(String host, X509Certificate certificate) {
56 return verifyAsIpAddress(host)
57 ? verifyIpAddress(host, certificate)
58 : verifyHostname(host, certificate);
59 }
60
61 /** Returns true if {@code certificate} matches {@code ipAddress}. */
62 private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
63 List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
64 for (int i = 0, size = altNames.size(); i < size; i++) {
65 if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
66 return true;
67 }
68 }
69 return false;
70 }
71
72 /** Returns true if {@code certificate} matches {@code hostname}. */
73 private boolean verifyHostname(String hostname, X509Certificate certificate) {
74 hostname = hostname.toLowerCase(Locale.US);
75 List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
76 for (String altName : altNames) {
77 if (verifyHostname(hostname, altName)) {
78 return true;
79 }
80 }
81 return false;
82 }
83
84 public static List<String> allSubjectAltNames(X509Certificate certificate) {
85 List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
86 List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
87 List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size());
88 result.addAll(altIpaNames);
89 result.addAll(altDnsNames);
90 return result;
91 }
92
93 private static List<String> getSubjectAltNames(X509Certificate certificate, int type) {
94 List<String> result = new ArrayList<>();
95 try {
96 Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
97 if (subjectAltNames == null) {
98 return Collections.emptyList();
99 }
100 for (Object subjectAltName : subjectAltNames) {
101 List<?> entry = (List<?>) subjectAltName;
102 if (entry == null || entry.size() < 2) {
103 continue;
104 }
105 Integer altNameType = (Integer) entry.get(0);
106 if (altNameType == null) {
107 continue;
108 }
109 if (altNameType == type) {
110 String altName = (String) entry.get(1);
111 if (altName != null) {
112 result.add(altName);
113 }
114 }
115 }
116 return result;
117 } catch (CertificateParsingException e) {
118 return Collections.emptyList();
119 }
120 }
121
122 /**
123 * Returns {@code true} iff {@code hostname} matches the domain name {@code pattern}.
124 *
125 * @param hostname lower-case host name.
126 * @param pattern domain name pattern from certificate. May be a wildcard pattern such as {@code
127 * *.android.com}.
128 */
129 public boolean verifyHostname(String hostname, String pattern) {
130 // Basic sanity checks
131 // Check length == 0 instead of .isEmpty() to support Java 5.
132 if ((hostname == null) || (hostname.length() == 0) || (hostname.startsWith("."))
133 || (hostname.endsWith(".."))) {
134 // Invalid domain name
135 return false;
136 }
137 if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith("."))
138 || (pattern.endsWith(".."))) {
139 // Invalid pattern/domain name
140 return false;
141 }
142
143 // Normalize hostname and pattern by turning them into absolute domain names if they are not
144 // yet absolute. This is needed because server certificates do not normally contain absolute
145 // names or patterns, but they should be treated as absolute. At the same time, any hostname
146 // presented to this method should also be treated as absolute for the purposes of matching
147 // to the server certificate.
148 // www.android.com matches www.android.com
149 // www.android.com matches www.android.com.
150 // www.android.com. matches www.android.com.
151 // www.android.com. matches www.android.com
152 if (!hostname.endsWith(".")) {
153 hostname += '.';
154 }
155 if (!pattern.endsWith(".")) {
156 pattern += '.';
157 }
158 // hostname and pattern are now absolute domain names.
159
160 pattern = pattern.toLowerCase(Locale.US);
161 // hostname and pattern are now in lower case -- domain names are case-insensitive.
162
163 if (!pattern.contains("*")) {
164 // Not a wildcard pattern -- hostname and pattern must match exactly.
165 return hostname.equals(pattern);
166 }
167 // Wildcard pattern
168
169 // WILDCARD PATTERN RULES:
170 // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
171 // only character in that label (i.e., must match the whole left-most label).
172 // For example, *.example.com is permitted, while *a.example.com, a*.example.com,
173 // a*b.example.com, a.*.example.com are not permitted.
174 // 2. Asterisk (*) cannot match across domain name labels.
175 // For example, *.example.com matches test.example.com but does not match
176 // sub.test.example.com.
177 // 3. Wildcard patterns for single-label domain names are not permitted.
178
179 if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) {
180 // Asterisk (*) is only permitted in the left-most domain name label and must be the only
181 // character in that label
182 return false;
183 }
184
185 // Optimization: check whether hostname is too short to match the pattern. hostName must be at
186 // least as long as the pattern because asterisk must match the whole left-most label and
187 // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters.
188 if (hostname.length() < pattern.length()) {
189 // hostname too short to match the pattern.
190 return false;
191 }
192
193 if ("*.".equals(pattern)) {
194 // Wildcard pattern for single-label domain name -- not permitted.
195 return false;
196 }
197
198 // hostname must end with the region of pattern following the asterisk.
199 String suffix = pattern.substring(1);
200 if (!hostname.endsWith(suffix)) {
201 // hostname does not end with the suffix
202 return false;
203 }
204
205 // Check that asterisk did not match across domain name labels.
206 int suffixStartIndexInHostname = hostname.length() - suffix.length();
207 if ((suffixStartIndexInHostname > 0)
208 && (hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1)) {
209 // Asterisk is matching across domain name labels -- not permitted.
210 return false;
211 }
212
213 // hostname matches pattern
214 return true;
215 }
216 }
217