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 javax.net.ssl;
19
20import java.net.InetAddress;
21import java.security.cert.Certificate;
22import java.security.cert.CertificateParsingException;
23import java.security.cert.X509Certificate;
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.List;
28import java.util.Locale;
29import javax.security.auth.x500.X500Principal;
30
31/**
32 * A HostnameVerifier consistent with <a
33 * href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
34 *
35 * @hide accessible via HttpsURLConnection.getDefaultHostnameVerifier()
36 */
37public final class DefaultHostnameVerifier implements HostnameVerifier {
38    private static final int ALT_DNS_NAME = 2;
39    private static final int ALT_IPA_NAME = 7;
40
41    public final boolean verify(String host, SSLSession session) {
42        try {
43            Certificate[] certificates = session.getPeerCertificates();
44            return verify(host, (X509Certificate) certificates[0]);
45        } catch (SSLException e) {
46            return false;
47        }
48    }
49
50    public boolean verify(String host, X509Certificate certificate) {
51        return InetAddress.isNumeric(host)
52                ? verifyIpAddress(host, certificate)
53                : verifyHostName(host, certificate);
54    }
55
56    /**
57     * Returns true if {@code certificate} matches {@code ipAddress}.
58     */
59    private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
60        for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) {
61            if (ipAddress.equalsIgnoreCase(altName)) {
62                return true;
63            }
64        }
65        return false;
66    }
67
68    /**
69     * Returns true if {@code certificate} matches {@code hostName}.
70     */
71    private boolean verifyHostName(String hostName, X509Certificate certificate) {
72        hostName = hostName.toLowerCase(Locale.US);
73        boolean hasDns = false;
74        for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) {
75            hasDns = true;
76            if (verifyHostName(hostName, altName)) {
77                return true;
78            }
79        }
80
81        if (!hasDns) {
82            X500Principal principal = certificate.getSubjectX500Principal();
83            // RFC 2818 advises using the most specific name for matching.
84            String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
85            if (cn != null) {
86                return verifyHostName(hostName, cn);
87            }
88        }
89
90        return false;
91    }
92
93    private List<String> getSubjectAltNames(X509Certificate certificate, int type) {
94        List<String> result = new ArrayList<String>();
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 true if {@code hostName} matches the name or pattern {@code cn}.
124     *
125     * @param hostName lowercase host name.
126     * @param cn certificate host name. May include wildcards like
127     *     {@code *.android.com}.
128     */
129    public boolean verifyHostName(String hostName, String cn) {
130        if (hostName == null || hostName.isEmpty() || cn == null || cn.isEmpty()) {
131            return false;
132        }
133
134        cn = cn.toLowerCase(Locale.US);
135
136        if (!cn.contains("*")) {
137            return hostName.equals(cn);
138        }
139
140        if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
141            return true; // "*.foo.com" matches "foo.com"
142        }
143
144        int asterisk = cn.indexOf('*');
145        int dot = cn.indexOf('.');
146        if (asterisk > dot) {
147            return false; // malformed; wildcard must be in the first part of the cn
148        }
149
150        if (!hostName.regionMatches(0, cn, 0, asterisk)) {
151            return false; // prefix before '*' doesn't match
152        }
153
154        int suffixLength = cn.length() - (asterisk + 1);
155        int suffixStart = hostName.length() - suffixLength;
156        if (hostName.indexOf('.', asterisk) < suffixStart) {
157            // TODO: remove workaround for *.clients.google.com http://b/5426333
158            if (!hostName.endsWith(".clients.google.com")) {
159                return false; // wildcard '*' can't match a '.'
160            }
161        }
162
163        if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) {
164            return false; // suffix after '*' doesn't match
165        }
166
167        return true;
168    }
169}
170