1 /*
2  * ====================================================================
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  * ====================================================================
20  *
21  * This software consists of voluntary contributions made by many
22  * individuals on behalf of the Apache Software Foundation.  For more
23  * information on the Apache Software Foundation, please see
24  * <http://www.apache.org/>.
25  *
26  */

27
28 package org.apache.http.conn.ssl;
29
30 import java.net.InetAddress;
31 import java.net.UnknownHostException;
32 import java.security.cert.Certificate;
33 import java.security.cert.CertificateParsingException;
34 import java.security.cert.X509Certificate;
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.List;
39 import java.util.NoSuchElementException;
40
41 import javax.naming.InvalidNameException;
42 import javax.naming.NamingException;
43 import javax.naming.directory.Attribute;
44 import javax.naming.directory.Attributes;
45 import javax.naming.ldap.LdapName;
46 import javax.naming.ldap.Rdn;
47 import javax.net.ssl.HostnameVerifier;
48 import javax.net.ssl.SSLException;
49 import javax.net.ssl.SSLPeerUnverifiedException;
50 import javax.net.ssl.SSLSession;
51 import javax.security.auth.x500.X500Principal;
52
53 import org.apache.commons.logging.Log;
54 import org.apache.commons.logging.LogFactory;
55 import org.apache.http.annotation.Contract;
56 import org.apache.http.annotation.ThreadingBehavior;
57 import org.apache.http.conn.util.DnsUtils;
58 import org.apache.http.conn.util.DomainType;
59 import org.apache.http.conn.util.InetAddressUtils;
60 import org.apache.http.conn.util.PublicSuffixMatcher;
61
62 /**
63  * Default {@link javax.net.ssl.HostnameVerifier} implementation.
64  *
65  * @since 4.4
66  */

67 @Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
68 public final class DefaultHostnameVerifier implements HostnameVerifier {
69
70     enum HostNameType {
71
72         IPv4(7), IPv6(7), DNS(2);
73
74         final int subjectType;
75
76         HostNameType(final int subjectType) {
77             this.subjectType = subjectType;
78         }
79
80     }
81
82     private final Log log = LogFactory.getLog(getClass());
83
84     private final PublicSuffixMatcher publicSuffixMatcher;
85
86     public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
87         this.publicSuffixMatcher = publicSuffixMatcher;
88     }
89
90     public DefaultHostnameVerifier() {
91         this(null);
92     }
93
94     @Override
95     public boolean verify(final String host, final SSLSession session) {
96         try {
97             final Certificate[] certs = session.getPeerCertificates();
98             final X509Certificate x509 = (X509Certificate) certs[0];
99             verify(host, x509);
100             return true;
101         } catch (final SSLException ex) {
102             if (log.isDebugEnabled()) {
103                 log.debug(ex.getMessage(), ex);
104             }
105             return false;
106         }
107     }
108
109     public void verify(
110             final String host, final X509Certificate cert) throws SSLException {
111         final HostNameType hostType = determineHostFormat(host);
112         final List<SubjectName> subjectAlts = getSubjectAltNames(cert);
113         if (subjectAlts != null && !subjectAlts.isEmpty()) {
114             switch (hostType) {
115                 case IPv4:
116                     matchIPAddress(host, subjectAlts);
117                     break;
118                 case IPv6:
119                     matchIPv6Address(host, subjectAlts);
120                     break;
121                 default:
122                     matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
123             }
124         } else {
125             // CN matching has been deprecated by rfc2818 and can be used
126             // as fallback only when no subjectAlts are available
127             final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
128             final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
129             if (cn == null) {
130                 throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
131                         "a common name and does not have alternative names");
132             }
133             matchCN(host, cn, this.publicSuffixMatcher);
134         }
135     }
136
137     static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLException {
138         for (int i = 0; i < subjectAlts.size(); i++) {
139             final SubjectName subjectAlt = subjectAlts.get(i);
140             if (subjectAlt.getType() == SubjectName.IP) {
141                 if (host.equals(subjectAlt.getValue())) {
142                     return;
143                 }
144             }
145         }
146         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
147                 "of the subject alternative names: " + subjectAlts);
148     }
149
150     static void matchIPv6Address(final String host, final List<SubjectName> subjectAlts) throws SSLException {
151         final String normalisedHost = normaliseAddress(host);
152         for (int i = 0; i < subjectAlts.size(); i++) {
153             final SubjectName subjectAlt = subjectAlts.get(i);
154             if (subjectAlt.getType() == SubjectName.IP) {
155                 final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue());
156                 if (normalisedHost.equals(normalizedSubjectAlt)) {
157                     return;
158                 }
159             }
160         }
161         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
162                 "of the subject alternative names: " + subjectAlts);
163     }
164
165     static void matchDNSName(final String host, final List<SubjectName> subjectAlts,
166                              final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
167         final String normalizedHost = DnsUtils.normalize(host);
168         for (int i = 0; i < subjectAlts.size(); i++) {
169             final SubjectName subjectAlt = subjectAlts.get(i);
170             if (subjectAlt.getType() == SubjectName.DNS) {
171                 final String normalizedSubjectAlt = DnsUtils.normalize(subjectAlt.getValue());
172                 if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
173                     return;
174                 }
175             }
176         }
177         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
178                 "of the subject alternative names: " + subjectAlts);
179     }
180
181     static void matchCN(final String host, final String cn,
182                  final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
183         final String normalizedHost = DnsUtils.normalize(host);
184         final String normalizedCn = DnsUtils.normalize(cn);
185         if (!matchIdentityStrict(normalizedHost, normalizedCn, publicSuffixMatcher)) {
186             throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " +
187                     "common name of the certificate subject: " + cn);
188         }
189     }
190
191     static boolean matchDomainRoot(final String host, final String domainRoot) {
192         if (domainRoot == null) {
193             return false;
194         }
195         return host.endsWith(domainRoot) && (host.length() == domainRoot.length()
196                 || host.charAt(host.length() - domainRoot.length() - 1) == '.');
197     }
198
199     private static boolean matchIdentity(final String host, final String identity,
200                                          final PublicSuffixMatcher publicSuffixMatcher,
201                                          final DomainType domainType,
202                                          final boolean strict) {
203         if (publicSuffixMatcher != null && host.contains(".")) {
204             if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, domainType))) {
205                 return false;
206             }
207         }
208
209         // RFC 2818, 3.1. Server Identity
210         // "...Names may contain the wildcard
211         // character * which is considered to match any single domain name
212         // component or component fragment..."
213         // Based on this statement presuming only singular wildcard is legal
214         final int asteriskIdx = identity.indexOf('*');
215         if (asteriskIdx != -1) {
216             final String prefix = identity.substring(0, asteriskIdx);
217             final String suffix = identity.substring(asteriskIdx + 1);
218             if (!prefix.isEmpty() && !host.startsWith(prefix)) {
219                 return false;
220             }
221             if (!suffix.isEmpty() && !host.endsWith(suffix)) {
222                 return false;
223             }
224             // Additional sanity checks on content selected by wildcard can be done here
225             if (strict) {
226                 final String remainder = host.substring(
227                         prefix.length(), host.length() - suffix.length());
228                 if (remainder.contains(".")) {
229                     return false;
230                 }
231             }
232             return true;
233         }
234         return host.equalsIgnoreCase(identity);
235     }
236
237     static boolean matchIdentity(final String host, final String identity,
238                                  final PublicSuffixMatcher publicSuffixMatcher) {
239         return matchIdentity(host, identity, publicSuffixMatcher, nullfalse);
240     }
241
242     static boolean matchIdentity(final String host, final String identity) {
243         return matchIdentity(host, identity, nullnullfalse);
244     }
245
246     static boolean matchIdentityStrict(final String host, final String identity,
247                                        final PublicSuffixMatcher publicSuffixMatcher) {
248         return matchIdentity(host, identity, publicSuffixMatcher, nulltrue);
249     }
250
251     static boolean matchIdentityStrict(final String host, final String identity) {
252         return matchIdentity(host, identity, nullnulltrue);
253     }
254
255     static boolean matchIdentity(final String host, final String identity,
256                                  final PublicSuffixMatcher publicSuffixMatcher,
257                                  final DomainType domainType) {
258         return matchIdentity(host, identity, publicSuffixMatcher, domainType, false);
259     }
260
261     static boolean matchIdentityStrict(final String host, final String identity,
262                                        final PublicSuffixMatcher publicSuffixMatcher,
263                                        final DomainType domainType) {
264         return matchIdentity(host, identity, publicSuffixMatcher, domainType, true);
265     }
266
267     static String extractCN(final String subjectPrincipal) throws SSLException {
268         if (subjectPrincipal == null) {
269             return null;
270         }
271         try {
272             final LdapName subjectDN = new LdapName(subjectPrincipal);
273             final List<Rdn> rdns = subjectDN.getRdns();
274             for (int i = rdns.size() - 1; i >= 0; i--) {
275                 final Rdn rds = rdns.get(i);
276                 final Attributes attributes = rds.toAttributes();
277                 final Attribute cn = attributes.get("cn");
278                 if (cn != null) {
279                     try {
280                         final Object value = cn.get();
281                         if (value != null) {
282                             return value.toString();
283                         }
284                     } catch (final NoSuchElementException ignore) {
285                         // ignore exception
286                     } catch (final NamingException ignore) {
287                         // ignore exception
288                     }
289                 }
290             }
291             return null;
292         } catch (final InvalidNameException e) {
293             throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
294         }
295     }
296
297     static HostNameType determineHostFormat(final String host) {
298         if (InetAddressUtils.isIPv4Address(host)) {
299             return HostNameType.IPv4;
300         }
301         String s = host;
302         if (s.startsWith("[") && s.endsWith("]")) {
303             s = host.substring(1, host.length() - 1);
304         }
305         if (InetAddressUtils.isIPv6Address(s)) {
306             return HostNameType.IPv6;
307         }
308         return HostNameType.DNS;
309     }
310
311     static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
312         try {
313             final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
314             if (entries == null) {
315                 return Collections.emptyList();
316             }
317             final List<SubjectName> result = new ArrayList<SubjectName>();
318             for (final List<?> entry : entries) {
319                 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
320                 if (type != null) {
321                     if (type == SubjectName.DNS || type == SubjectName.IP) {
322                         final Object o = entry.get(1);
323                         if (o instanceof String) {
324                             result.add(new SubjectName((String) o, type));
325                         } else if (o instanceof byte[]) {
326                             // TODO ASN.1 DER encoded form
327                         }
328                     }
329                 }
330             }
331             return result;
332         } catch (final CertificateParsingException ignore) {
333             return Collections.emptyList();
334         }
335     }
336
337     /*
338      * Normalize IPv6 or DNS name.
339      */

340     static String normaliseAddress(final String hostname) {
341         if (hostname == null) {
342             return hostname;
343         }
344         try {
345             final InetAddress inetAddress = InetAddress.getByName(hostname);
346             return inetAddress.getHostAddress();
347         } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
348             return hostname;
349         }
350     }
351 }
352