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