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.io.IOException;
31  import java.io.InputStream;
32  import java.net.InetSocketAddress;
33  import java.net.Socket;
34  import java.security.cert.Certificate;
35  import java.security.cert.X509Certificate;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.List;
41  import java.util.regex.Pattern;
42  
43  import javax.net.SocketFactory;
44  import javax.net.ssl.HostnameVerifier;
45  import javax.net.ssl.SSLContext;
46  import javax.net.ssl.SSLHandshakeException;
47  import javax.net.ssl.SSLPeerUnverifiedException;
48  import javax.net.ssl.SSLSession;
49  import javax.net.ssl.SSLSocket;
50  import javax.security.auth.x500.X500Principal;
51  
52  import org.apache.commons.logging.Log;
53  import org.apache.commons.logging.LogFactory;
54  import org.apache.http.HttpHost;
55  import org.apache.http.annotation.Contract;
56  import org.apache.http.annotation.ThreadingBehavior;
57  import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
58  import org.apache.http.conn.util.PublicSuffixMatcherLoader;
59  import org.apache.http.protocol.HttpContext;
60  import org.apache.http.ssl.SSLContexts;
61  import org.apache.http.util.Args;
62  import org.apache.http.util.TextUtils;
63  
64  /**
65   * Layered socket factory for TLS/SSL connections.
66   * <p>
67   * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
68   * trusted certificates and to authenticate to the HTTPS server using a private key.
69   * <p>
70   * SSLSocketFactory will enable server authentication when supplied with
71   * a {@link java.security.KeyStore trust-store} file containing one or several trusted certificates. The client
72   * secure socket will reject the connection during the SSL session handshake if the target HTTPS
73   * server attempts to authenticate itself with a non-trusted certificate.
74   * <p>
75   * Use JDK keytool utility to import a trusted certificate and generate a trust-store file:
76   *    <pre>
77   *     keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
78   *    </pre>
79   * <p>
80   * In special cases the standard trust verification process can be bypassed by using a custom
81   * {@link org.apache.http.conn.ssl.TrustStrategy}. This interface is primarily intended for allowing self-signed
82   * certificates to be accepted as trusted without having to add them to the trust-store file.
83   * <p>
84   * SSLSocketFactory will enable client authentication when supplied with
85   * a {@link java.security.KeyStore key-store} file containing a private key/public certificate
86   * pair. The client secure socket will use the private key to authenticate
87   * itself to the target HTTPS server during the SSL session handshake if
88   * requested to do so by the server.
89   * The target HTTPS server will in its turn verify the certificate presented
90   * by the client in order to establish client's authenticity.
91   * <p>
92   * Use the following sequence of actions to generate a key-store file
93   * </p>
94   *   <ul>
95   *     <li>
96   *      <p>
97   *      Use JDK keytool utility to generate a new key
98   *      </p>
99   *      <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
100  *      <p>
101  *      For simplicity use the same password for the key as that of the key-store
102  *      </p>
103  *     </li>
104  *     <li>
105  *      <p>
106  *      Issue a certificate signing request (CSR)
107  *      </p>
108  *      <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
109  *     </li>
110  *     <li>
111  *      <p>
112  *      Send the certificate request to the trusted Certificate Authority for signature.
113  *      One may choose to act as her own CA and sign the certificate request using a PKI
114  *      tool, such as OpenSSL.
115  *      </p>
116  *     </li>
117  *     <li>
118  *      <p>
119  *       Import the trusted CA root certificate
120  *      </p>
121  *       <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
122  *     </li>
123  *     <li>
124  *       <p>
125  *       Import the PKCS#7 file containing the complete certificate chain
126  *       </p>
127  *       <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
128  *     </li>
129  *     <li>
130  *       <p>
131  *       Verify the content of the resultant keystore file
132  *       </p>
133  *       <pre>keytool -list -v -keystore my.keystore</pre>
134  *     </li>
135  *   </ul>
136  *
137  * @since 4.3
138  */
139 @Contract(threading = ThreadingBehavior.SAFE)
140 @SuppressWarnings("deprecation")
141 public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactory {
142 
143     public static final String TLS   = "TLS";
144     public static final String SSL   = "SSL";
145     public static final String SSLV2 = "SSLv2";
146 
147     /**
148      * @deprecated Use {@link AllowAllHostnameVerifier#INSTANCE}.
149      */
150     @Deprecated
151     public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
152         = AllowAllHostnameVerifier.INSTANCE;
153 
154     /**
155      * @deprecated Use {@link BrowserCompatHostnameVerifier#INSTANCE}.
156      */
157     @Deprecated
158     public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
159         = BrowserCompatHostnameVerifier.INSTANCE;
160 
161     /**
162      * @deprecated Use {@link StrictHostnameVerifier#INSTANCE}.
163      */
164     @Deprecated
165     public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
166         = StrictHostnameVerifier.INSTANCE;
167 
168     private static final String WEAK_KEY_EXCHANGES
169             = "^(TLS|SSL)_(NULL|ECDH_anon|DH_anon|DH_anon_EXPORT|DHE_RSA_EXPORT|DHE_DSS_EXPORT|"
170             + "DSS_EXPORT|DH_DSS_EXPORT|DH_RSA_EXPORT|RSA_EXPORT|KRB5_EXPORT)_(.*)";
171     private static final String WEAK_CIPHERS
172             = "^(TLS|SSL)_(.*)_WITH_(NULL|DES_CBC|DES40_CBC|DES_CBC_40|3DES_EDE_CBC|RC4_128|RC4_40|RC2_CBC_40)_(.*)";
173     private static final List<Pattern> WEAK_CIPHER_SUITE_PATTERNS = Collections.unmodifiableList(Arrays.asList(
174             Pattern.compile(WEAK_KEY_EXCHANGES, Pattern.CASE_INSENSITIVE),
175             Pattern.compile(WEAK_CIPHERS, Pattern.CASE_INSENSITIVE)));
176 
177     private final Log log = LogFactory.getLog(getClass());
178 
179     /**
180      * @since 4.4
181      */
182     public static HostnameVerifier getDefaultHostnameVerifier() {
183         return new DefaultHostnameVerifier(PublicSuffixMatcherLoader.getDefault());
184     }
185 
186     /**
187      * Obtains default SSL socket factory with an SSL context based on the standard JSSE
188      * trust material ({@code cacerts} file in the security properties directory).
189      * System properties are not taken into consideration.
190      *
191      * @return default SSL socket factory
192      */
193     public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
194         return new SSLConnectionSocketFactory(SSLContexts.createDefault(), getDefaultHostnameVerifier());
195     }
196 
197     static boolean isWeakCipherSuite(final String cipherSuite) {
198         for (final Pattern pattern : WEAK_CIPHER_SUITE_PATTERNS) {
199             if (pattern.matcher(cipherSuite).matches()) {
200                 return true;
201             }
202         }
203         return false;
204     }
205 
206     private static String[] split(final String s) {
207         if (TextUtils.isBlank(s)) {
208             return null;
209         }
210         return s.split(" *, *");
211     }
212 
213     /**
214      * Obtains default SSL socket factory with an SSL context based on system properties
215      * as described in
216      * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html">
217      * Java&#x2122; Secure Socket Extension (JSSE) Reference Guide</a>.
218      *
219      * @return default system SSL socket factory
220      */
221     public static SSLConnectionSocketFactory getSystemSocketFactory() throws SSLInitializationException {
222         return new SSLConnectionSocketFactory(
223             (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
224             split(System.getProperty("https.protocols")),
225             split(System.getProperty("https.cipherSuites")),
226             getDefaultHostnameVerifier());
227     }
228 
229     private final javax.net.ssl.SSLSocketFactory socketfactory;
230     private final HostnameVerifier hostnameVerifier;
231     private final String[] supportedProtocols;
232     private final String[] supportedCipherSuites;
233 
234     public SSLConnectionSocketFactory(final SSLContext sslContext) {
235         this(sslContext, getDefaultHostnameVerifier());
236     }
237 
238     /**
239      * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLContext,
240      *   javax.net.ssl.HostnameVerifier)}
241      */
242     @Deprecated
243     public SSLConnectionSocketFactory(
244             final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) {
245         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
246                 null, null, hostnameVerifier);
247     }
248 
249     /**
250      * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLContext,
251      *   String[], String[], javax.net.ssl.HostnameVerifier)}
252      */
253     @Deprecated
254     public SSLConnectionSocketFactory(
255             final SSLContext sslContext,
256             final String[] supportedProtocols,
257             final String[] supportedCipherSuites,
258             final X509HostnameVerifier hostnameVerifier) {
259         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
260                 supportedProtocols, supportedCipherSuites, hostnameVerifier);
261     }
262 
263     /**
264      * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLSocketFactory,
265      *   javax.net.ssl.HostnameVerifier)}
266      */
267     @Deprecated
268     public SSLConnectionSocketFactory(
269             final javax.net.ssl.SSLSocketFactory socketfactory,
270             final X509HostnameVerifier hostnameVerifier) {
271         this(socketfactory, null, null, hostnameVerifier);
272     }
273 
274     /**
275      * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLSocketFactory,
276      *   String[], String[], javax.net.ssl.HostnameVerifier)}
277      */
278     @Deprecated
279     public SSLConnectionSocketFactory(
280             final javax.net.ssl.SSLSocketFactory socketfactory,
281             final String[] supportedProtocols,
282             final String[] supportedCipherSuites,
283             final X509HostnameVerifier hostnameVerifier) {
284         this(socketfactory, supportedProtocols, supportedCipherSuites, (HostnameVerifier) hostnameVerifier);
285     }
286 
287     /**
288      * @since 4.4
289      */
290     public SSLConnectionSocketFactory(
291             final SSLContext sslContext, final HostnameVerifier hostnameVerifier) {
292         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
293                 null, null, hostnameVerifier);
294     }
295 
296     /**
297      * @since 4.4
298      */
299     public SSLConnectionSocketFactory(
300             final SSLContext sslContext,
301             final String[] supportedProtocols,
302             final String[] supportedCipherSuites,
303             final HostnameVerifier hostnameVerifier) {
304         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
305                 supportedProtocols, supportedCipherSuites, hostnameVerifier);
306     }
307 
308     /**
309      * @since 4.4
310      */
311     public SSLConnectionSocketFactory(
312             final javax.net.ssl.SSLSocketFactory socketfactory,
313             final HostnameVerifier hostnameVerifier) {
314         this(socketfactory, null, null, hostnameVerifier);
315     }
316 
317     /**
318      * @since 4.4
319      */
320     public SSLConnectionSocketFactory(
321             final javax.net.ssl.SSLSocketFactory socketfactory,
322             final String[] supportedProtocols,
323             final String[] supportedCipherSuites,
324             final HostnameVerifier hostnameVerifier) {
325         this.socketfactory = Args.notNull(socketfactory, "SSL socket factory");
326         this.supportedProtocols = supportedProtocols;
327         this.supportedCipherSuites = supportedCipherSuites;
328         this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : getDefaultHostnameVerifier();
329     }
330 
331     /**
332      * Performs any custom initialization for a newly created SSLSocket
333      * (before the SSL handshake happens).
334      *
335      * The default implementation is a no-op, but could be overridden to, e.g.,
336      * call {@link javax.net.ssl.SSLSocket#setEnabledCipherSuites(String[])}.
337      * @throws IOException may be thrown if overridden
338      */
339     protected void prepareSocket(final SSLSocket socket) throws IOException {
340     }
341 
342     @Override
343     public Socket createSocket(final HttpContext context) throws IOException {
344         return SocketFactory.getDefault().createSocket();
345     }
346 
347     @Override
348     public Socket connectSocket(
349             final int connectTimeout,
350             final Socket socket,
351             final HttpHost host,
352             final InetSocketAddress remoteAddress,
353             final InetSocketAddress localAddress,
354             final HttpContext context) throws IOException {
355         Args.notNull(host, "HTTP host");
356         Args.notNull(remoteAddress, "Remote address");
357         final Socket sock = socket != null ? socket : createSocket(context);
358         if (localAddress != null) {
359             sock.bind(localAddress);
360         }
361         try {
362             if (connectTimeout > 0 && sock.getSoTimeout() == 0) {
363                 sock.setSoTimeout(connectTimeout);
364             }
365             if (this.log.isDebugEnabled()) {
366                 this.log.debug("Connecting socket to " + remoteAddress + " with timeout " + connectTimeout);
367             }
368             sock.connect(remoteAddress, connectTimeout);
369         } catch (final IOException ex) {
370             try {
371                 sock.close();
372             } catch (final IOException ignore) {
373             }
374             throw ex;
375         }
376         // Setup SSL layering if necessary
377         if (sock instanceof SSLSocket) {
378             final SSLSocket sslsock = (SSLSocket) sock;
379             this.log.debug("Starting handshake");
380             sslsock.startHandshake();
381             verifyHostname(sslsock, host.getHostName());
382             return sock;
383         }
384         return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context);
385     }
386 
387     @Override
388     public Socket createLayeredSocket(
389             final Socket socket,
390             final String target,
391             final int port,
392             final HttpContext context) throws IOException {
393         final SSLSocket sslsock = (SSLSocket) this.socketfactory.createSocket(
394                 socket,
395                 target,
396                 port,
397                 true);
398         if (supportedProtocols != null) {
399             sslsock.setEnabledProtocols(supportedProtocols);
400         } else {
401             // If supported protocols are not explicitly set, remove all SSL protocol versions
402             final String[] allProtocols = sslsock.getEnabledProtocols();
403             final List<String> enabledProtocols = new ArrayList<String>(allProtocols.length);
404             for (final String protocol: allProtocols) {
405                 if (!protocol.startsWith("SSL")) {
406                     enabledProtocols.add(protocol);
407                 }
408             }
409             if (!enabledProtocols.isEmpty()) {
410                 sslsock.setEnabledProtocols(enabledProtocols.toArray(new String[enabledProtocols.size()]));
411             }
412         }
413         if (supportedCipherSuites != null) {
414             sslsock.setEnabledCipherSuites(supportedCipherSuites);
415         } else {
416             // If cipher suites are not explicitly set, remove all insecure ones
417             final String[] allCipherSuites = sslsock.getEnabledCipherSuites();
418             final List<String> enabledCipherSuites = new ArrayList<String>(allCipherSuites.length);
419             for (final String cipherSuite : allCipherSuites) {
420                 if (!isWeakCipherSuite(cipherSuite)) {
421                     enabledCipherSuites.add(cipherSuite);
422                 }
423             }
424             if (!enabledCipherSuites.isEmpty()) {
425                 sslsock.setEnabledCipherSuites(enabledCipherSuites.toArray(new String[enabledCipherSuites.size()]));
426             }
427         }
428 
429         if (this.log.isDebugEnabled()) {
430             this.log.debug("Enabled protocols: " + Arrays.asList(sslsock.getEnabledProtocols()));
431             this.log.debug("Enabled cipher suites:" + Arrays.asList(sslsock.getEnabledCipherSuites()));
432         }
433 
434         prepareSocket(sslsock);
435         this.log.debug("Starting handshake");
436         sslsock.startHandshake();
437         verifyHostname(sslsock, target);
438         return sslsock;
439     }
440 
441     private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException {
442         try {
443             SSLSession session = sslsock.getSession();
444             if (session == null) {
445                 // In our experience this only happens under IBM 1.4.x when
446                 // spurious (unrelated) certificates show up in the server'
447                 // chain.  Hopefully this will unearth the real problem:
448                 final InputStream in = sslsock.getInputStream();
449                 in.available();
450                 // If ssl.getInputStream().available() didn't cause an
451                 // exception, maybe at least now the session is available?
452                 session = sslsock.getSession();
453                 if (session == null) {
454                     // If it's still null, probably a startHandshake() will
455                     // unearth the real problem.
456                     sslsock.startHandshake();
457                     session = sslsock.getSession();
458                 }
459             }
460             if (session == null) {
461                 throw new SSLHandshakeException("SSL session not available");
462             }
463 
464             if (this.log.isDebugEnabled()) {
465                 this.log.debug("Secure session established");
466                 this.log.debug(" negotiated protocol: " + session.getProtocol());
467                 this.log.debug(" negotiated cipher suite: " + session.getCipherSuite());
468 
469                 try {
470 
471                     final Certificate[] certs = session.getPeerCertificates();
472                     final X509Certificate x509 = (X509Certificate) certs[0];
473                     final X500Principal peer = x509.getSubjectX500Principal();
474 
475                     this.log.debug(" peer principal: " + peer.toString());
476                     final Collection<List<?>> altNames1 = x509.getSubjectAlternativeNames();
477                     if (altNames1 != null) {
478                         final List<String> altNames = new ArrayList<String>();
479                         for (final List<?> aC : altNames1) {
480                             if (!aC.isEmpty()) {
481                                 altNames.add((String) aC.get(1));
482                             }
483                         }
484                         this.log.debug(" peer alternative names: " + altNames);
485                     }
486 
487                     final X500Principal issuer = x509.getIssuerX500Principal();
488                     this.log.debug(" issuer principal: " + issuer.toString());
489                     final Collection<List<?>> altNames2 = x509.getIssuerAlternativeNames();
490                     if (altNames2 != null) {
491                         final List<String> altNames = new ArrayList<String>();
492                         for (final List<?> aC : altNames2) {
493                             if (!aC.isEmpty()) {
494                                 altNames.add((String) aC.get(1));
495                             }
496                         }
497                         this.log.debug(" issuer alternative names: " + altNames);
498                     }
499                 } catch (final Exception ignore) {
500                 }
501             }
502 
503             if (!this.hostnameVerifier.verify(hostname, session)) {
504                 final Certificate[] certs = session.getPeerCertificates();
505                 final X509Certificate x509 = (X509Certificate) certs[0];
506                 final List<SubjectName> subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509);
507                 throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " +
508                         "of the subject alternative names: " + subjectAlts);
509             }
510             // verifyHostName() didn't blowup - good!
511         } catch (final IOException iox) {
512             // close the socket before re-throwing the exception
513             try { sslsock.close(); } catch (final Exception x) { /*ignore*/ }
514             throw iox;
515         }
516     }
517 
518 }