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.LinkedList;
39  import java.util.List;
40  
41  import javax.net.ssl.SSLException;
42  import javax.net.ssl.SSLPeerUnverifiedException;
43  import javax.net.ssl.SSLSession;
44  import javax.security.auth.x500.X500Principal;
45  
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      /**
81       * Constructs new instance with a PublicSuffixMatcher.
82       *
83       * @param publicSuffixMatcher a PublicSuffixMatcher.
84       */
85      public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
86          this.publicSuffixMatcher = publicSuffixMatcher;
87      }
88  
89      /**
90       * Constructs new instance without a PublicSuffixMatcher.
91       */
92      public DefaultHostnameVerifier() {
93          this(null);
94      }
95  
96      @Override
97      public boolean verify(final String host, final SSLSession session) {
98          try {
99              final Certificate[] certs = session.getPeerCertificates();
100             final X509Certificate x509 = (X509Certificate) certs[0];
101             verify(host, x509);
102             return true;
103         } catch (final SSLException ex) {
104             if (LOG.isDebugEnabled()) {
105                 LOG.debug(ex.getMessage(), ex);
106             }
107             return false;
108         }
109     }
110 
111     @Override
112     public void verify(final String host, final X509Certificate cert) throws SSLException {
113         final HostNameType hostType = determineHostFormat(host);
114         switch (hostType) {
115         case IPv4:
116             matchIPAddress(host, getSubjectAltNames(cert, SubjectName.IP));
117             break;
118         case IPv6:
119             matchIPv6Address(host, getSubjectAltNames(cert, SubjectName.IP));
120             break;
121         default:
122             final List<SubjectName> subjectAlts = getSubjectAltNames(cert, SubjectName.DNS);
123             if (subjectAlts.isEmpty()) {
124                 // CN matching has been deprecated by rfc2818 and can be used
125                 // as fallback only when no subjectAlts of type SubjectName.DNS are available
126                 matchCN(host, cert, this.publicSuffixMatcher);
127             } else {
128                 matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
129             }
130         }
131     }
132 
133     static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLPeerUnverifiedException {
134         for (final SubjectName subjectAlt : subjectAlts) {
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 SSLPeerUnverifiedException {
146         final String normalisedHost = normaliseAddress(host);
147         for (final SubjectName subjectAlt : subjectAlts) {
148             if (subjectAlt.getType() == SubjectName.IP) {
149                 final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue());
150                 if (normalisedHost.equals(normalizedSubjectAlt)) {
151                     return;
152                 }
153             }
154         }
155         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
156                 "of the subject alternative names: " + subjectAlts);
157     }
158 
159     static void matchDNSName(final String host, final List<SubjectName> subjectAlts,
160                              final PublicSuffixMatcher publicSuffixMatcher) throws SSLPeerUnverifiedException {
161         final String normalizedHost = DnsUtils.normalizeUnicode(host);
162         for (final SubjectName subjectAlt : subjectAlts) {
163             if (subjectAlt.getType() == SubjectName.DNS) {
164                 final String normalizedSubjectAlt = DnsUtils.normalizeUnicode(subjectAlt.getValue());
165                 if (matchIdentity(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher, true)) {
166                     return;
167                 }
168             }
169         }
170         throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
171                 "of the subject alternative names: " + subjectAlts);
172     }
173 
174     static void matchCN(final String host, final X509Certificate cert,
175                         final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
176         final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
177         final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
178         if (cn == null) {
179             throw new SSLPeerUnverifiedException("Certificate subject for <" + host + "> doesn't contain " +
180                     "a common name and does not have alternative names");
181         }
182         final String normalizedHost = DnsUtils.normalizeUnicode(host);
183         final String normalizedCn = DnsUtils.normalizeUnicode(cn);
184         if (!matchIdentity(normalizedHost, normalizedCn, publicSuffixMatcher, true)) {
185             throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " +
186                     "common name of the certificate subject: " + cn);
187         }
188     }
189 
190     static List<CharSequence> parseFQDN(final CharSequence s) {
191         if (s == null) {
192             return null;
193         }
194         final LinkedList<CharSequence> elements = new LinkedList<>();
195         int pos = 0;
196         for (int i = 0; i < s.length(); i++) {
197             final char ch = s.charAt(i);
198             if (ch == '.') {
199                 elements.addFirst(s.subSequence(pos, i));
200                 pos = i + 1;
201             }
202         }
203         elements.addFirst(s.subSequence(pos, s.length()));
204         return elements;
205     }
206 
207     static boolean matchDomainRoot(final String host, final String domainRoot) {
208         if (domainRoot == null) {
209             return false;
210         }
211         final List<CharSequence> hostElements = parseFQDN(host);
212         final List<CharSequence> rootElements = parseFQDN(domainRoot);
213         if (hostElements.size() >= rootElements.size()) {
214             for (int i = 0; i < rootElements.size(); i++) {
215                 final CharSequence s1 = rootElements.get(i);
216                 final CharSequence s2 = hostElements.get(i);
217                 if (!s1.equals(s2)) {
218                     return false;
219                 }
220             }
221             return true;
222         }
223         return false;
224     }
225 
226     static boolean matchIdentity(final String host, final String identity,
227                                          final PublicSuffixMatcher publicSuffixMatcher,
228                                          final boolean strict) {
229         if (publicSuffixMatcher != null && host.contains(".")) {
230             if (!publicSuffixMatcher.verifyInternal(identity)) {
231                 if (LOG.isDebugEnabled()) {
232                     LOG.debug("Public Suffix List verification failed for identity '{}'", identity);
233                 }
234                 return false;
235             }
236         }
237 
238         // RFC 2818, 3.1. Server Identity
239         // "...Names may contain the wildcard
240         // character * which is considered to match any single domain name
241         // component or component fragment..."
242         // Based on this statement presuming only singular wildcard is legal
243         final int asteriskIdx = identity.indexOf('*');
244         if (asteriskIdx != -1) {
245             final String prefix = identity.substring(0, asteriskIdx);
246             final String suffix = identity.substring(asteriskIdx + 1);
247 
248             if (!prefix.isEmpty() && !host.startsWith(prefix)) {
249                 return false;
250             }
251             if (!suffix.isEmpty() && !host.endsWith(suffix)) {
252                 return false;
253             }
254             // Additional sanity checks on content selected by wildcard can be done here
255             if (strict) {
256                 final String remainder = host.substring(
257                         prefix.length(),
258                         host.length() - suffix.length()
259                 );
260                 return !remainder.contains(".");
261             }
262             return true;
263         }
264 
265         // Direct Unicode comparison
266         return host.equalsIgnoreCase(identity);
267     }
268 
269     static String extractCN(final String subjectPrincipal) throws SSLException {
270         if (subjectPrincipal == null) {
271             return null;
272         }
273         final List<NameValuePair> attributes = DistinguishedNameParser.INSTANCE.parse(subjectPrincipal);
274         for (final NameValuePair attribute: attributes) {
275             if (TextUtils.isBlank(attribute.getName()) || attribute.getValue() == null) {
276                 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
277             }
278             if (attribute.getName().equalsIgnoreCase("cn")) {
279                 return attribute.getValue();
280             }
281         }
282         return null;
283     }
284 
285     static HostNameType determineHostFormat(final String host) {
286         if (InetAddressUtils.isIPv4(host)) {
287             return HostNameType.IPv4;
288         }
289         String s = host;
290         if (s.startsWith("[") && s.endsWith("]")) {
291             s = host.substring(1, host.length() - 1);
292         }
293         if (InetAddressUtils.isIPv6(s)) {
294             return HostNameType.IPv6;
295         }
296         return HostNameType.DNS;
297     }
298 
299     static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
300         return getSubjectAltNames(cert, -1);
301     }
302 
303     static List<SubjectName> getSubjectAltNames(final X509Certificate cert, final int subjectName) {
304         try {
305             final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
306             if (entries == null) {
307                 return Collections.emptyList();
308             }
309             final List<SubjectName> result = new ArrayList<>();
310             for (final List<?> entry : entries) {
311                 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
312                 if (type != null) {
313                     if (type == subjectName || -1 == subjectName) {
314                         final Object o = entry.get(1);
315                         if (o instanceof String) {
316                             result.add(new SubjectName((String) o, type));
317                         } else if (o instanceof byte[]) {
318                             final byte[] bytes = (byte[]) o;
319                             if (type == SubjectName.IP) {
320                                 if (bytes.length == 4) {
321                                     result.add(new SubjectName(byteArrayToIp(bytes), type)); // IPv4
322                                 } else if (bytes.length == 16) {
323                                     result.add(new SubjectName(byteArrayToIPv6(bytes), type)); // IPv6
324                                 }
325                             }
326                         }
327                     }
328                 }
329             }
330             return result;
331         } catch (final CertificateParsingException ignore) {
332             return Collections.emptyList();
333         }
334     }
335 
336     /*
337      * Normalize IPv6 or DNS name.
338      */
339     static String normaliseAddress(final String hostname) {
340         if (hostname == null) {
341             return hostname;
342         }
343         try {
344             final InetAddress inetAddress = InetAddress.getByName(hostname);
345             return inetAddress.getHostAddress();
346         } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
347             return hostname;
348         }
349     }
350 
351     private static String byteArrayToIp(final byte[] bytes) {
352         if (bytes.length != 4) {
353             throw new IllegalArgumentException("Invalid byte array length for IPv4 address");
354         }
355         return (bytes[0] & 0xFF) + "." +
356                 (bytes[1] & 0xFF) + "." +
357                 (bytes[2] & 0xFF) + "." +
358                 (bytes[3] & 0xFF);
359     }
360 
361     private static String byteArrayToIPv6(final byte[] bytes) {
362         if (bytes.length != 16) {
363             throw new IllegalArgumentException("Invalid byte array length for IPv6 address");
364         }
365         final StringBuilder sb = new StringBuilder();
366         for (int i = 0; i < bytes.length; i += 2) {
367             sb.append(String.format("%02x%02x", bytes[i], bytes[i + 1]));
368             if (i < bytes.length - 2) {
369                 sb.append(":");
370             }
371         }
372         return sb.toString();
373     }
374 
375 }