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.io.IOException;
31  import java.io.InputStream;
32  import java.net.InetSocketAddress;
33  import java.net.Proxy;
34  import java.net.Socket;
35  import java.net.SocketAddress;
36  import java.security.AccessController;
37  import java.security.PrivilegedActionException;
38  import java.security.PrivilegedExceptionAction;
39  import java.security.cert.Certificate;
40  import java.security.cert.X509Certificate;
41  import java.util.ArrayList;
42  import java.util.Arrays;
43  import java.util.Collection;
44  import java.util.Collections;
45  import java.util.List;
46  import java.util.Objects;
47  import java.util.regex.Pattern;
48  
49  import javax.net.ssl.HostnameVerifier;
50  import javax.net.ssl.SSLContext;
51  import javax.net.ssl.SSLException;
52  import javax.net.ssl.SSLHandshakeException;
53  import javax.net.ssl.SSLPeerUnverifiedException;
54  import javax.net.ssl.SSLSession;
55  import javax.net.ssl.SSLSocket;
56  import javax.security.auth.x500.X500Principal;
57  
58  import org.apache.hc.client5.http.config.TlsConfig;
59  import org.apache.hc.core5.annotation.Contract;
60  import org.apache.hc.core5.annotation.ThreadingBehavior;
61  import org.apache.hc.core5.http.HttpHost;
62  import org.apache.hc.core5.http.protocol.HttpContext;
63  import org.apache.hc.core5.http.ssl.TLS;
64  import org.apache.hc.core5.http.ssl.TlsCiphers;
65  import org.apache.hc.core5.io.Closer;
66  import org.apache.hc.core5.ssl.SSLContexts;
67  import org.apache.hc.core5.ssl.SSLInitializationException;
68  import org.apache.hc.core5.util.Args;
69  import org.apache.hc.core5.util.Asserts;
70  import org.apache.hc.core5.util.TimeValue;
71  import org.apache.hc.core5.util.Timeout;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * Layered socket factory for TLS/SSL connections.
77   * <p>
78   * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
79   * trusted certificates and to authenticate to the HTTPS server using a private key.
80   *
81   * @deprecated Use {@link DefaultClientTlsStrategy}.
82   */
83  @Deprecated
84  @Contract(threading = ThreadingBehavior.STATELESS)
85  public class SSLConnectionSocketFactory implements org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory {
86  
87      private static final String WEAK_KEY_EXCHANGES
88              = "^(TLS|SSL)_(NULL|ECDH_anon|DH_anon|DH_anon_EXPORT|DHE_RSA_EXPORT|DHE_DSS_EXPORT|"
89              + "DSS_EXPORT|DH_DSS_EXPORT|DH_RSA_EXPORT|RSA_EXPORT|KRB5_EXPORT)_(.*)";
90      private static final String WEAK_CIPHERS
91              = "^(TLS|SSL)_(.*)_WITH_(NULL|DES_CBC|DES40_CBC|DES_CBC_40|3DES_EDE_CBC|RC4_128|RC4_40|RC2_CBC_40)_(.*)";
92      private static final List<Pattern> WEAK_CIPHER_SUITE_PATTERNS = Collections.unmodifiableList(Arrays.asList(
93              Pattern.compile(WEAK_KEY_EXCHANGES, Pattern.CASE_INSENSITIVE),
94              Pattern.compile(WEAK_CIPHERS, Pattern.CASE_INSENSITIVE)));
95  
96      private static final Logger LOG = LoggerFactory.getLogger(SSLConnectionSocketFactory.class);
97  
98      /**
99       * Obtains default SSL socket factory with an SSL context based on the standard JSSE
100      * trust material ({@code cacerts} file in the security properties directory).
101      * System properties are not taken into consideration.
102      *
103      * @return default SSL socket factory
104      */
105     public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
106         return new SSLConnectionSocketFactory(SSLContexts.createDefault(), HttpsSupport.getDefaultHostnameVerifier());
107     }
108 
109     /**
110      * Obtains default SSL socket factory with an SSL context based on system properties
111      * as described in
112      * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html">
113      * Java&#x2122; Secure Socket Extension (JSSE) Reference Guide</a>.
114      *
115      * @return default system SSL socket factory
116      */
117     public static SSLConnectionSocketFactory getSystemSocketFactory() throws SSLInitializationException {
118         return new SSLConnectionSocketFactory(
119                 (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
120                 HttpsSupport.getSystemProtocols(),
121                 HttpsSupport.getSystemCipherSuits(),
122                 HttpsSupport.getDefaultHostnameVerifier());
123     }
124 
125     static boolean isWeakCipherSuite(final String cipherSuite) {
126         for (final Pattern pattern : WEAK_CIPHER_SUITE_PATTERNS) {
127             if (pattern.matcher(cipherSuite).matches()) {
128                 return true;
129             }
130         }
131         return false;
132     }
133 
134     private final javax.net.ssl.SSLSocketFactory socketFactory;
135     private final HostnameVerifier hostnameVerifier;
136     private final String[] supportedProtocols;
137     private final String[] supportedCipherSuites;
138 
139     public SSLConnectionSocketFactory(final SSLContext sslContext) {
140         this(sslContext, HttpsSupport.getDefaultHostnameVerifier());
141     }
142 
143     /**
144      * @since 4.4
145      */
146     public SSLConnectionSocketFactory(
147             final SSLContext sslContext, final HostnameVerifier hostnameVerifier) {
148         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
149                 null, null, hostnameVerifier);
150     }
151 
152     /**
153      * @since 4.4
154      */
155     public SSLConnectionSocketFactory(
156             final SSLContext sslContext,
157             final String[] supportedProtocols,
158             final String[] supportedCipherSuites,
159             final HostnameVerifier hostnameVerifier) {
160         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
161                 supportedProtocols, supportedCipherSuites, hostnameVerifier);
162     }
163 
164     /**
165      * @since 4.4
166      */
167     public SSLConnectionSocketFactory(
168             final javax.net.ssl.SSLSocketFactory socketFactory,
169             final HostnameVerifier hostnameVerifier) {
170         this(socketFactory, null, null, hostnameVerifier);
171     }
172 
173     /**
174      * @since 4.4
175      */
176     public SSLConnectionSocketFactory(
177             final javax.net.ssl.SSLSocketFactory socketFactory,
178             final String[] supportedProtocols,
179             final String[] supportedCipherSuites,
180             final HostnameVerifier hostnameVerifier) {
181         this.socketFactory = Args.notNull(socketFactory, "SSL socket factory");
182         this.supportedProtocols = supportedProtocols;
183         this.supportedCipherSuites = supportedCipherSuites;
184         this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : HttpsSupport.getDefaultHostnameVerifier();
185     }
186 
187     /**
188      * @deprecated Use {@link #prepareSocket(SSLSocket, HttpContext)}
189      */
190     @Deprecated
191     protected void prepareSocket(final SSLSocket socket) throws IOException {
192     }
193 
194     /**
195      * Performs any custom initialization for a newly created SSLSocket
196      * (before the SSL handshake happens).
197      *
198      * The default implementation is a no-op, but could be overridden to, e.g.,
199      * call {@link javax.net.ssl.SSLSocket#setEnabledCipherSuites(String[])}.
200      * @throws IOException may be thrown if overridden
201      */
202     @SuppressWarnings("deprecation")
203     protected void prepareSocket(final SSLSocket socket, final HttpContext context) throws IOException {
204         prepareSocket(socket);
205     }
206 
207     @Override
208     public Socket createSocket(final HttpContext context) throws IOException {
209         return new Socket();
210     }
211 
212     @Override
213     public Socket createSocket(final Proxy proxy, final HttpContext context) throws IOException {
214         return proxy != null ? new Socket(proxy) : createSocket(context);
215     }
216 
217     @Override
218     public Socket connectSocket(
219             final TimeValue connectTimeout,
220             final Socket socket,
221             final HttpHost host,
222             final InetSocketAddress remoteAddress,
223             final InetSocketAddress localAddress,
224             final HttpContext context) throws IOException {
225         final Timeout timeout = connectTimeout != null ? Timeout.of(connectTimeout.getDuration(), connectTimeout.getTimeUnit()) : null;
226         return connectSocket(socket, host, remoteAddress, localAddress, timeout, timeout, context);
227     }
228 
229     @Override
230     public Socket connectSocket(
231             final Socket socket,
232             final HttpHost host,
233             final InetSocketAddress remoteAddress,
234             final InetSocketAddress localAddress,
235             final Timeout connectTimeout,
236             final Object attachment,
237             final HttpContext context) throws IOException {
238         Args.notNull(host, "HTTP host");
239         Args.notNull(remoteAddress, "Remote address");
240         final Socket sock = socket != null ? socket : createSocket(context);
241         if (localAddress != null) {
242             sock.bind(localAddress);
243         }
244         try {
245             connectSocket(sock, remoteAddress, connectTimeout, context);
246         } catch (final IOException ex) {
247             Closer.closeQuietly(sock);
248             throw ex;
249         }
250         // Setup SSL layering if necessary
251         if (sock instanceof SSLSocket) {
252             final SSLSocket sslsock = (SSLSocket) sock;
253             executeHandshake(sslsock, host.getHostName(), attachment, context);
254             return sock;
255         }
256         return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), attachment, context);
257     }
258 
259     /**
260      * Connects the socket to the target host with the given resolved remote address using
261      * {@link Socket#connect(SocketAddress, int)}. This method may be overridden to customize
262      * how precisely {@link Socket#connect(SocketAddress, int)} is handled without impacting
263      * other connection establishment code within {@link #executeHandshake(SSLSocket, String, Object, HttpContext)},
264      * for example.
265      *
266      * @param sock the socket to connect.
267      * @param remoteAddress the resolved remote address to connect to.
268      * @param connectTimeout connect timeout.
269      * @param context the actual HTTP context.
270      * @throws IOException if an I/O error occurs
271      */
272     protected void connectSocket(
273             final Socket sock,
274             final InetSocketAddress remoteAddress,
275             final Timeout connectTimeout,
276             final HttpContext context) throws IOException {
277         Args.notNull(sock, "Socket");
278         Args.notNull(remoteAddress, "Remote address");
279         if (LOG.isDebugEnabled()) {
280             LOG.debug("Connecting socket to {} with timeout {}", remoteAddress, connectTimeout);
281         }
282         // Run this under a doPrivileged to support lib users that run under a SecurityManager this allows granting connect permissions
283         // only to this library
284         try {
285             AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
286                 sock.connect(remoteAddress, Timeout.defaultsToDisabled(connectTimeout).toMillisecondsIntBound());
287                 return null;
288             });
289         } catch (final PrivilegedActionException e) {
290             Asserts.check(e.getCause() instanceof IOException,
291                     "method contract violation only checked exceptions are wrapped: " + e.getCause());
292             // only checked exceptions are wrapped - error and RTExceptions are rethrown by doPrivileged
293             throw (IOException) e.getCause();
294         }
295     }
296 
297     @Override
298     public Socket createLayeredSocket(
299             final Socket socket,
300             final String target,
301             final int port,
302             final HttpContext context) throws IOException {
303         return createLayeredSocket(socket, target, port, null, context);
304     }
305 
306     @Override
307     public Socket createLayeredSocket(
308             final Socket socket,
309             final String target,
310             final int port,
311             final Object attachment,
312             final HttpContext context) throws IOException {
313         final SSLSocket sslsock = (SSLSocket) this.socketFactory.createSocket(
314                 socket,
315                 target,
316                 port,
317                 true);
318         executeHandshake(sslsock, target, attachment, context);
319         return sslsock;
320     }
321 
322     private void executeHandshake(
323             final SSLSocket sslsock,
324             final String target,
325             final Object attachment,
326             final HttpContext context) throws IOException {
327         final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT;
328         if (supportedProtocols != null) {
329             sslsock.setEnabledProtocols(supportedProtocols);
330         } else {
331             sslsock.setEnabledProtocols((TLS.excludeWeak(sslsock.getEnabledProtocols())));
332         }
333         if (supportedCipherSuites != null) {
334             sslsock.setEnabledCipherSuites(supportedCipherSuites);
335         } else {
336             sslsock.setEnabledCipherSuites(TlsCiphers.excludeWeak(sslsock.getEnabledCipherSuites()));
337         }
338         final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout();
339         if (handshakeTimeout != null) {
340             sslsock.setSoTimeout(handshakeTimeout.toMillisecondsIntBound());
341         }
342 
343         prepareSocket(sslsock, context);
344 
345         if (LOG.isDebugEnabled()) {
346             LOG.debug("Enabled protocols: {}", (Object) sslsock.getEnabledProtocols());
347             LOG.debug("Enabled cipher suites: {}", (Object) sslsock.getEnabledCipherSuites());
348             LOG.debug("Starting handshake ({})", handshakeTimeout);
349         }
350         sslsock.startHandshake();
351         verifyHostname(sslsock, target);
352     }
353 
354     private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException {
355         try {
356             SSLSession session = sslsock.getSession();
357             if (session == null) {
358                 // In our experience this only happens under IBM 1.4.x when
359                 // spurious (unrelated) certificates show up in the server'
360                 // chain.  Hopefully this will unearth the real problem:
361                 final InputStream in = sslsock.getInputStream();
362                 in.available();
363                 // If ssl.getInputStream().available() didn't cause an
364                 // exception, maybe at least now the session is available?
365                 session = sslsock.getSession();
366                 if (session == null) {
367                     // If it's still null, probably a startHandshake() will
368                     // unearth the real problem.
369                     sslsock.startHandshake();
370                     session = sslsock.getSession();
371                 }
372             }
373             if (session == null) {
374                 throw new SSLHandshakeException("SSL session not available");
375             }
376             verifySession(hostname, session);
377         } catch (final IOException iox) {
378             // close the socket before re-throwing the exception
379             Closer.closeQuietly(sslsock);
380             throw iox;
381         }
382     }
383 
384     protected void verifySession(
385             final String hostname,
386             final SSLSession sslSession) throws SSLException {
387         verifySession(hostname, sslSession, hostnameVerifier);
388     }
389 
390     void verifySession(
391             final String hostname,
392             final SSLSession sslsession,
393             final HostnameVerifier hostnameVerifier) throws SSLException {
394 
395         if (LOG.isDebugEnabled()) {
396             LOG.debug("Secure session established");
397             LOG.debug(" negotiated protocol: {}", sslsession.getProtocol());
398             LOG.debug(" negotiated cipher suite: {}", sslsession.getCipherSuite());
399 
400             try {
401 
402                 final Certificate[] certs = sslsession.getPeerCertificates();
403                 final Certificate cert = certs[0];
404                 if (cert instanceof X509Certificate) {
405                     final X509Certificate x509 = (X509Certificate) cert;
406                     final X500Principal peer = x509.getSubjectX500Principal();
407 
408                     LOG.debug(" peer principal: {}", peer);
409                     final Collection<List<?>> altNames1 = x509.getSubjectAlternativeNames();
410                     if (altNames1 != null) {
411                         final List<String> altNames = new ArrayList<>();
412                         for (final List<?> aC : altNames1) {
413                             if (!aC.isEmpty()) {
414                                 altNames.add(Objects.toString(aC.get(1), null));
415                             }
416                         }
417                         LOG.debug(" peer alternative names: {}", altNames);
418                     }
419 
420                     final X500Principal issuer = x509.getIssuerX500Principal();
421                     LOG.debug(" issuer principal: {}", issuer);
422                     final Collection<List<?>> altNames2 = x509.getIssuerAlternativeNames();
423                     if (altNames2 != null) {
424                         final List<String> altNames = new ArrayList<>();
425                         for (final List<?> aC : altNames2) {
426                             if (!aC.isEmpty()) {
427                                 altNames.add(Objects.toString(aC.get(1), null));
428                             }
429                         }
430                         LOG.debug(" issuer alternative names: {}", altNames);
431                     }
432                 }
433             } catch (final Exception ignore) {
434             }
435         }
436 
437         if (hostnameVerifier != null) {
438             final Certificate[] certs = sslsession.getPeerCertificates();
439             if (certs.length < 1) {
440                 throw new SSLPeerUnverifiedException("Peer certificate chain is empty");
441             }
442             final Certificate peerCertificate = certs[0];
443             final X509Certificate x509Certificate;
444             if (peerCertificate instanceof X509Certificate) {
445                 x509Certificate = (X509Certificate) peerCertificate;
446             } else {
447                 throw new SSLPeerUnverifiedException("Unexpected certificate type: " + peerCertificate.getType());
448             }
449             if (hostnameVerifier instanceof HttpClientHostnameVerifier) {
450                 ((HttpClientHostnameVerifier) hostnameVerifier).verify(hostname, x509Certificate);
451             } else if (!hostnameVerifier.verify(hostname, sslsession)) {
452                 final List<SubjectName> subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509Certificate);
453                 throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " +
454                         "of the subject alternative names: " + subjectAlts);
455             }
456         }
457     }
458 
459 }