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.Locale;
40  import java.util.NoSuchElementException;
41  
42  import javax.naming.InvalidNameException;
43  import javax.naming.NamingException;
44  import javax.naming.directory.Attribute;
45  import javax.naming.directory.Attributes;
46  import javax.naming.ldap.LdapName;
47  import javax.naming.ldap.Rdn;
48  import javax.net.ssl.HostnameVerifier;
49  import javax.net.ssl.SSLException;
50  import javax.net.ssl.SSLPeerUnverifiedException;
51  import javax.net.ssl.SSLSession;
52  import javax.security.auth.x500.X500Principal;
53  
54  import org.apache.commons.logging.Log;
55  import org.apache.commons.logging.LogFactory;
56  import org.apache.http.annotation.Contract;
57  import org.apache.http.annotation.ThreadingBehavior;
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 = host.toLowerCase(Locale.ROOT);
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 = subjectAlt.getValue().toLowerCase(Locale.ROOT);
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 = host.toLowerCase(Locale.ROOT);
184         final String normalizedCn = cn.toLowerCase(Locale.ROOT);
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 boolean strict) {
202         if (publicSuffixMatcher != null && host.contains(".")) {
203             if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, DomainType.ICANN))) {
204                 return false;
205             }
206         }
207 
208         // RFC 2818, 3.1. Server Identity
209         // "...Names may contain the wildcard
210         // character * which is considered to match any single domain name
211         // component or component fragment..."
212         // Based on this statement presuming only singular wildcard is legal
213         final int asteriskIdx = identity.indexOf('*');
214         if (asteriskIdx != -1) {
215             final String prefix = identity.substring(0, asteriskIdx);
216             final String suffix = identity.substring(asteriskIdx + 1);
217             if (!prefix.isEmpty() && !host.startsWith(prefix)) {
218                 return false;
219             }
220             if (!suffix.isEmpty() && !host.endsWith(suffix)) {
221                 return false;
222             }
223             // Additional sanity checks on content selected by wildcard can be done here
224             if (strict) {
225                 final String remainder = host.substring(
226                         prefix.length(), host.length() - suffix.length());
227                 if (remainder.contains(".")) {
228                     return false;
229                 }
230             }
231             return true;
232         }
233         return host.equalsIgnoreCase(identity);
234     }
235 
236     static boolean matchIdentity(final String host, final String identity,
237                                  final PublicSuffixMatcher publicSuffixMatcher) {
238         return matchIdentity(host, identity, publicSuffixMatcher, false);
239     }
240 
241     static boolean matchIdentity(final String host, final String identity) {
242         return matchIdentity(host, identity, null, false);
243     }
244 
245     static boolean matchIdentityStrict(final String host, final String identity,
246                                        final PublicSuffixMatcher publicSuffixMatcher) {
247         return matchIdentity(host, identity, publicSuffixMatcher, true);
248     }
249 
250     static boolean matchIdentityStrict(final String host, final String identity) {
251         return matchIdentity(host, identity, null, true);
252     }
253 
254     static String extractCN(final String subjectPrincipal) throws SSLException {
255         if (subjectPrincipal == null) {
256             return null;
257         }
258         try {
259             final LdapName subjectDN = new LdapName(subjectPrincipal);
260             final List<Rdn> rdns = subjectDN.getRdns();
261             for (int i = rdns.size() - 1; i >= 0; i--) {
262                 final Rdn rds = rdns.get(i);
263                 final Attributes attributes = rds.toAttributes();
264                 final Attribute cn = attributes.get("cn");
265                 if (cn != null) {
266                     try {
267                         final Object value = cn.get();
268                         if (value != null) {
269                             return value.toString();
270                         }
271                     } catch (final NoSuchElementException ignore) {
272                         // ignore exception
273                     } catch (final NamingException ignore) {
274                         // ignore exception
275                     }
276                 }
277             }
278             return null;
279         } catch (final InvalidNameException e) {
280             throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
281         }
282     }
283 
284     static HostNameType determineHostFormat(final String host) {
285         if (InetAddressUtils.isIPv4Address(host)) {
286             return HostNameType.IPv4;
287         }
288         String s = host;
289         if (s.startsWith("[") && s.endsWith("]")) {
290             s = host.substring(1, host.length() - 1);
291         }
292         if (InetAddressUtils.isIPv6Address(s)) {
293             return HostNameType.IPv6;
294         }
295         return HostNameType.DNS;
296     }
297 
298     static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
299         try {
300             final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
301             if (entries == null) {
302                 return Collections.emptyList();
303             }
304             final List<SubjectName> result = new ArrayList<SubjectName>();
305             for (final List<?> entry : entries) {
306                 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
307                 if (type != null) {
308                     if (type == SubjectName.DNS || type == SubjectName.IP) {
309                         final Object o = entry.get(1);
310                         if (o instanceof String) {
311                             result.add(new SubjectName((String) o, type));
312                         } else if (o instanceof byte[]) {
313                             // TODO ASN.1 DER encoded form
314                         }
315                     }
316                 }
317             }
318             return result;
319         } catch (final CertificateParsingException ignore) {
320             return Collections.emptyList();
321         }
322     }
323 
324     /*
325      * Normalize IPv6 or DNS name.
326      */
327     static String normaliseAddress(final String hostname) {
328         if (hostname == null) {
329             return hostname;
330         }
331         try {
332             final InetAddress inetAddress = InetAddress.getByName(hostname);
333             return inetAddress.getHostAddress();
334         } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
335             return hostname;
336         }
337     }
338 }