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(
105             final String host, final X509Certificate cert) throws SSLException {
106         final HostNameType hostType = determineHostFormat(host);
107         final List<SubjectName> subjectAlts = getSubjectAltNames(cert);
108         if (subjectAlts != null && !subjectAlts.isEmpty()) {
109             switch (hostType) {
110                 case IPv4:
111                     matchIPAddress(host, subjectAlts);
112                     break;
113                 case IPv6:
114                     matchIPv6Address(host, subjectAlts);
115                     break;
116                 default:
117                     matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
118             }
119         } else {
120             // CN matching has been deprecated by rfc2818 and can be used
121             // as fallback only when no subjectAlts are available
122             final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
123             final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
124             if (cn == null) {
125                 throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
126                         "a common name and does not have alternative names");
127             }
128             matchCN(host, cn, this.publicSuffixMatcher);
129         }
130     }
131 
132     static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLException {
133         for (int i = 0; i < subjectAlts.size(); i++) {
134             final SubjectName subjectAlt = subjectAlts.get(i);
135             if (subjectAlt.getType() == SubjectName.IP) {
136                 if (host.equals(subjectAlt.getValue())) {
137                     return;
138                 }
139             }
140         }
141         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
142                 "of the subject alternative names: " + subjectAlts);
143     }
144 
145     static void matchIPv6Address(final String host, final List<SubjectName> subjectAlts) throws SSLException {
146         final String normalisedHost = normaliseAddress(host);
147         for (int i = 0; i < subjectAlts.size(); i++) {
148             final SubjectName subjectAlt = subjectAlts.get(i);
149             if (subjectAlt.getType() == SubjectName.IP) {
150                 final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue());
151                 if (normalisedHost.equals(normalizedSubjectAlt)) {
152                     return;
153                 }
154             }
155         }
156         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
157                 "of the subject alternative names: " + subjectAlts);
158     }
159 
160     static void matchDNSName(final String host, final List<SubjectName> subjectAlts,
161                              final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
162         final String normalizedHost = DnsUtils.normalize(host);
163         for (int i = 0; i < subjectAlts.size(); i++) {
164             final SubjectName subjectAlt = subjectAlts.get(i);
165             if (subjectAlt.getType() == SubjectName.DNS) {
166                 final String normalizedSubjectAlt = DnsUtils.normalize(subjectAlt.getValue());
167                 if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
168                     return;
169                 }
170             }
171         }
172         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
173                 "of the subject alternative names: " + subjectAlts);
174     }
175 
176     static void matchCN(final String host, final String cn,
177                  final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
178         final String normalizedHost = DnsUtils.normalize(host);
179         final String normalizedCn = DnsUtils.normalize(cn);
180         if (!matchIdentityStrict(normalizedHost, normalizedCn, publicSuffixMatcher)) {
181             throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " +
182                     "common name of the certificate subject: " + cn);
183         }
184     }
185 
186     static boolean matchDomainRoot(final String host, final String domainRoot) {
187         if (domainRoot == null) {
188             return false;
189         }
190         return host.endsWith(domainRoot) && (host.length() == domainRoot.length()
191                 || host.charAt(host.length() - domainRoot.length() - 1) == '.');
192     }
193 
194     private static boolean matchIdentity(final String host, final String identity,
195                                          final PublicSuffixMatcher publicSuffixMatcher,
196                                          final DomainType domainType,
197                                          final boolean strict) {
198         if (publicSuffixMatcher != null && host.contains(".")) {
199             if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, domainType))) {
200                 return false;
201             }
202         }
203 
204         // RFC 2818, 3.1. Server Identity
205         // "...Names may contain the wildcard
206         // character * which is considered to match any single domain name
207         // component or component fragment..."
208         // Based on this statement presuming only singular wildcard is legal
209         final int asteriskIdx = identity.indexOf('*');
210         if (asteriskIdx != -1) {
211             final String prefix = identity.substring(0, asteriskIdx);
212             final String suffix = identity.substring(asteriskIdx + 1);
213             if (!prefix.isEmpty() && !host.startsWith(prefix)) {
214                 return false;
215             }
216             if (!suffix.isEmpty() && !host.endsWith(suffix)) {
217                 return false;
218             }
219             // Additional sanity checks on content selected by wildcard can be done here
220             if (strict) {
221                 final String remainder = host.substring(
222                         prefix.length(), host.length() - suffix.length());
223                 if (remainder.contains(".")) {
224                     return false;
225                 }
226             }
227             return true;
228         }
229         return host.equalsIgnoreCase(identity);
230     }
231 
232     static boolean matchIdentity(final String host, final String identity,
233                                  final PublicSuffixMatcher publicSuffixMatcher) {
234         return matchIdentity(host, identity, publicSuffixMatcher, null, false);
235     }
236 
237     static boolean matchIdentity(final String host, final String identity) {
238         return matchIdentity(host, identity, null, null, false);
239     }
240 
241     static boolean matchIdentityStrict(final String host, final String identity,
242                                        final PublicSuffixMatcher publicSuffixMatcher) {
243         return matchIdentity(host, identity, publicSuffixMatcher, null, true);
244     }
245 
246     static boolean matchIdentityStrict(final String host, final String identity) {
247         return matchIdentity(host, identity, null, null, true);
248     }
249 
250     static boolean matchIdentity(final String host, final String identity,
251                                  final PublicSuffixMatcher publicSuffixMatcher,
252                                  final DomainType domainType) {
253         return matchIdentity(host, identity, publicSuffixMatcher, domainType, false);
254     }
255 
256     static boolean matchIdentityStrict(final String host, final String identity,
257                                        final PublicSuffixMatcher publicSuffixMatcher,
258                                        final DomainType domainType) {
259         return matchIdentity(host, identity, publicSuffixMatcher, domainType, true);
260     }
261 
262     static String extractCN(final String subjectPrincipal) throws SSLException {
263         if (subjectPrincipal == null) {
264             return null;
265         }
266         final List<NameValuePair> attributes = DistinguishedNameParser.INSTANCE.parse(subjectPrincipal);
267         for (final NameValuePair attribute: attributes) {
268             if (TextUtils.isBlank(attribute.getName()) || attribute.getValue() == null) {
269                 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
270             }
271             if (attribute.getName().equalsIgnoreCase("cn")) {
272                 return attribute.getValue();
273             }
274         }
275         return null;
276     }
277 
278     static HostNameType determineHostFormat(final String host) {
279         if (InetAddressUtils.isIPv4Address(host)) {
280             return HostNameType.IPv4;
281         }
282         String s = host;
283         if (s.startsWith("[") && s.endsWith("]")) {
284             s = host.substring(1, host.length() - 1);
285         }
286         if (InetAddressUtils.isIPv6Address(s)) {
287             return HostNameType.IPv6;
288         }
289         return HostNameType.DNS;
290     }
291 
292     static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
293         try {
294             final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
295             if (entries == null) {
296                 return Collections.emptyList();
297             }
298             final List<SubjectName> result = new ArrayList<>();
299             for (final List<?> entry : entries) {
300                 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
301                 if (type != null) {
302                     if (type == SubjectName.DNS || type == SubjectName.IP) {
303                         final Object o = entry.get(1);
304                         if (o instanceof String) {
305                             result.add(new SubjectName((String) o, type));
306                         } else if (o instanceof byte[]) {
307                             // TODO ASN.1 DER encoded form
308                         }
309                     }
310                 }
311             }
312             return result;
313         } catch (final CertificateParsingException ignore) {
314             return Collections.emptyList();
315         }
316     }
317 
318     /*
319      * Normalize IPv6 or DNS name.
320      */
321     static String normaliseAddress(final String hostname) {
322         if (hostname == null) {
323             return hostname;
324         }
325         try {
326             final InetAddress inetAddress = InetAddress.getByName(hostname);
327             return inetAddress.getHostAddress();
328         } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
329             return hostname;
330         }
331     }
332 }