View Javadoc
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, null, false);
240     }
241 
242     static boolean matchIdentity(final String host, final String identity) {
243         return matchIdentity(host, identity, null, null, false);
244     }
245 
246     static boolean matchIdentityStrict(final String host, final String identity,
247                                        final PublicSuffixMatcher publicSuffixMatcher) {
248         return matchIdentity(host, identity, publicSuffixMatcher, null, true);
249     }
250 
251     static boolean matchIdentityStrict(final String host, final String identity) {
252         return matchIdentity(host, identity, null, null, true);
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 }