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