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