001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.conn.ssl;
029
030import java.io.IOException;
031import java.io.InputStream;
032import java.net.InetSocketAddress;
033import java.net.Socket;
034import java.security.cert.Certificate;
035import java.security.cert.X509Certificate;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.Collection;
039import java.util.List;
040
041import javax.net.SocketFactory;
042import javax.net.ssl.HostnameVerifier;
043import javax.net.ssl.SSLContext;
044import javax.net.ssl.SSLHandshakeException;
045import javax.net.ssl.SSLPeerUnverifiedException;
046import javax.net.ssl.SSLSession;
047import javax.net.ssl.SSLSocket;
048import javax.security.auth.x500.X500Principal;
049
050import org.apache.commons.logging.Log;
051import org.apache.commons.logging.LogFactory;
052import org.apache.http.HttpHost;
053import org.apache.http.annotation.Contract;
054import org.apache.http.annotation.ThreadingBehavior;
055import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
056import org.apache.http.conn.util.PublicSuffixMatcherLoader;
057import org.apache.http.protocol.HttpContext;
058import org.apache.http.ssl.SSLContexts;
059import org.apache.http.util.Args;
060import org.apache.http.util.TextUtils;
061
062/**
063 * Layered socket factory for TLS/SSL connections.
064 * <p>
065 * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
066 * trusted certificates and to authenticate to the HTTPS server using a private key.
067 * <p>
068 * SSLSocketFactory will enable server authentication when supplied with
069 * a {@link java.security.KeyStore trust-store} file containing one or several trusted certificates. The client
070 * secure socket will reject the connection during the SSL session handshake if the target HTTPS
071 * server attempts to authenticate itself with a non-trusted certificate.
072 * <p>
073 * Use JDK keytool utility to import a trusted certificate and generate a trust-store file:
074 *    <pre>
075 *     keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
076 *    </pre>
077 * <p>
078 * In special cases the standard trust verification process can be bypassed by using a custom
079 * {@link org.apache.http.conn.ssl.TrustStrategy}. This interface is primarily intended for allowing self-signed
080 * certificates to be accepted as trusted without having to add them to the trust-store file.
081 * <p>
082 * SSLSocketFactory will enable client authentication when supplied with
083 * a {@link java.security.KeyStore key-store} file containing a private key/public certificate
084 * pair. The client secure socket will use the private key to authenticate
085 * itself to the target HTTPS server during the SSL session handshake if
086 * requested to do so by the server.
087 * The target HTTPS server will in its turn verify the certificate presented
088 * by the client in order to establish client's authenticity.
089 * <p>
090 * Use the following sequence of actions to generate a key-store file
091 * </p>
092 *   <ul>
093 *     <li>
094 *      <p>
095 *      Use JDK keytool utility to generate a new key
096 *      </p>
097 *      <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
098 *      <p>
099 *      For simplicity use the same password for the key as that of the key-store
100 *      </p>
101 *     </li>
102 *     <li>
103 *      <p>
104 *      Issue a certificate signing request (CSR)
105 *      </p>
106 *      <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
107 *     </li>
108 *     <li>
109 *      <p>
110 *      Send the certificate request to the trusted Certificate Authority for signature.
111 *      One may choose to act as her own CA and sign the certificate request using a PKI
112 *      tool, such as OpenSSL.
113 *      </p>
114 *     </li>
115 *     <li>
116 *      <p>
117 *       Import the trusted CA root certificate
118 *      </p>
119 *       <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
120 *     </li>
121 *     <li>
122 *       <p>
123 *       Import the PKCS#7 file containing the complete certificate chain
124 *       </p>
125 *       <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
126 *     </li>
127 *     <li>
128 *       <p>
129 *       Verify the content of the resultant keystore file
130 *       </p>
131 *       <pre>keytool -list -v -keystore my.keystore</pre>
132 *     </li>
133 *   </ul>
134 *
135 * @since 4.3
136 */
137@Contract(threading = ThreadingBehavior.SAFE)
138@SuppressWarnings("deprecation")
139public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactory {
140
141    public static final String TLS   = "TLS";
142    public static final String SSL   = "SSL";
143    public static final String SSLV2 = "SSLv2";
144
145    @Deprecated
146    public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
147        = AllowAllHostnameVerifier.INSTANCE;
148
149    @Deprecated
150    public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
151        = BrowserCompatHostnameVerifier.INSTANCE;
152
153    @Deprecated
154    public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
155        = StrictHostnameVerifier.INSTANCE;
156
157    private final Log log = LogFactory.getLog(getClass());
158
159    /**
160     * @since 4.4
161     */
162    public static HostnameVerifier getDefaultHostnameVerifier() {
163        return new DefaultHostnameVerifier(PublicSuffixMatcherLoader.getDefault());
164    }
165
166    /**
167     * Obtains default SSL socket factory with an SSL context based on the standard JSSE
168     * trust material ({@code cacerts} file in the security properties directory).
169     * System properties are not taken into consideration.
170     *
171     * @return default SSL socket factory
172     */
173    public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
174        return new SSLConnectionSocketFactory(SSLContexts.createDefault(), getDefaultHostnameVerifier());
175    }
176
177    private static String[] split(final String s) {
178        if (TextUtils.isBlank(s)) {
179            return null;
180        }
181        return s.split(" *, *");
182    }
183
184    /**
185     * Obtains default SSL socket factory with an SSL context based on system properties
186     * as described in
187     * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html">
188     * Java&#x2122; Secure Socket Extension (JSSE) Reference Guide</a>.
189     *
190     * @return default system SSL socket factory
191     */
192    public static SSLConnectionSocketFactory getSystemSocketFactory() throws SSLInitializationException {
193        return new SSLConnectionSocketFactory(
194            (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
195            split(System.getProperty("https.protocols")),
196            split(System.getProperty("https.cipherSuites")),
197            getDefaultHostnameVerifier());
198    }
199
200    private final javax.net.ssl.SSLSocketFactory socketfactory;
201    private final HostnameVerifier hostnameVerifier;
202    private final String[] supportedProtocols;
203    private final String[] supportedCipherSuites;
204
205    public SSLConnectionSocketFactory(final SSLContext sslContext) {
206        this(sslContext, getDefaultHostnameVerifier());
207    }
208
209    /**
210     * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLContext,
211     *   javax.net.ssl.HostnameVerifier)}
212     */
213    @Deprecated
214    public SSLConnectionSocketFactory(
215            final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) {
216        this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
217                null, null, hostnameVerifier);
218    }
219
220    /**
221     * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLContext,
222     *   String[], String[], javax.net.ssl.HostnameVerifier)}
223     */
224    @Deprecated
225    public SSLConnectionSocketFactory(
226            final SSLContext sslContext,
227            final String[] supportedProtocols,
228            final String[] supportedCipherSuites,
229            final X509HostnameVerifier hostnameVerifier) {
230        this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
231                supportedProtocols, supportedCipherSuites, hostnameVerifier);
232    }
233
234    /**
235     * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLSocketFactory,
236     *   javax.net.ssl.HostnameVerifier)}
237     */
238    @Deprecated
239    public SSLConnectionSocketFactory(
240            final javax.net.ssl.SSLSocketFactory socketfactory,
241            final X509HostnameVerifier hostnameVerifier) {
242        this(socketfactory, null, null, hostnameVerifier);
243    }
244
245    /**
246     * @deprecated (4.4) Use {@link #SSLConnectionSocketFactory(javax.net.ssl.SSLSocketFactory,
247     *   String[], String[], javax.net.ssl.HostnameVerifier)}
248     */
249    @Deprecated
250    public SSLConnectionSocketFactory(
251            final javax.net.ssl.SSLSocketFactory socketfactory,
252            final String[] supportedProtocols,
253            final String[] supportedCipherSuites,
254            final X509HostnameVerifier hostnameVerifier) {
255        this(socketfactory, supportedProtocols, supportedCipherSuites, (HostnameVerifier) hostnameVerifier);
256    }
257
258    /**
259     * @since 4.4
260     */
261    public SSLConnectionSocketFactory(
262            final SSLContext sslContext, final HostnameVerifier hostnameVerifier) {
263        this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
264                null, null, hostnameVerifier);
265    }
266
267    /**
268     * @since 4.4
269     */
270    public SSLConnectionSocketFactory(
271            final SSLContext sslContext,
272            final String[] supportedProtocols,
273            final String[] supportedCipherSuites,
274            final HostnameVerifier hostnameVerifier) {
275        this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
276                supportedProtocols, supportedCipherSuites, hostnameVerifier);
277    }
278
279    /**
280     * @since 4.4
281     */
282    public SSLConnectionSocketFactory(
283            final javax.net.ssl.SSLSocketFactory socketfactory,
284            final HostnameVerifier hostnameVerifier) {
285        this(socketfactory, null, null, hostnameVerifier);
286    }
287
288    /**
289     * @since 4.4
290     */
291    public SSLConnectionSocketFactory(
292            final javax.net.ssl.SSLSocketFactory socketfactory,
293            final String[] supportedProtocols,
294            final String[] supportedCipherSuites,
295            final HostnameVerifier hostnameVerifier) {
296        this.socketfactory = Args.notNull(socketfactory, "SSL socket factory");
297        this.supportedProtocols = supportedProtocols;
298        this.supportedCipherSuites = supportedCipherSuites;
299        this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : getDefaultHostnameVerifier();
300    }
301
302    /**
303     * Performs any custom initialization for a newly created SSLSocket
304     * (before the SSL handshake happens).
305     *
306     * The default implementation is a no-op, but could be overridden to, e.g.,
307     * call {@link javax.net.ssl.SSLSocket#setEnabledCipherSuites(String[])}.
308     * @throws IOException may be thrown if overridden
309     */
310    protected void prepareSocket(final SSLSocket socket) throws IOException {
311    }
312
313    @Override
314    public Socket createSocket(final HttpContext context) throws IOException {
315        return SocketFactory.getDefault().createSocket();
316    }
317
318    @Override
319    public Socket connectSocket(
320            final int connectTimeout,
321            final Socket socket,
322            final HttpHost host,
323            final InetSocketAddress remoteAddress,
324            final InetSocketAddress localAddress,
325            final HttpContext context) throws IOException {
326        Args.notNull(host, "HTTP host");
327        Args.notNull(remoteAddress, "Remote address");
328        final Socket sock = socket != null ? socket : createSocket(context);
329        if (localAddress != null) {
330            sock.bind(localAddress);
331        }
332        try {
333            if (connectTimeout > 0 && sock.getSoTimeout() == 0) {
334                sock.setSoTimeout(connectTimeout);
335            }
336            if (this.log.isDebugEnabled()) {
337                this.log.debug("Connecting socket to " + remoteAddress + " with timeout " + connectTimeout);
338            }
339            sock.connect(remoteAddress, connectTimeout);
340        } catch (final IOException ex) {
341            try {
342                sock.close();
343            } catch (final IOException ignore) {
344            }
345            throw ex;
346        }
347        // Setup SSL layering if necessary
348        if (sock instanceof SSLSocket) {
349            final SSLSocket sslsock = (SSLSocket) sock;
350            this.log.debug("Starting handshake");
351            sslsock.startHandshake();
352            verifyHostname(sslsock, host.getHostName());
353            return sock;
354        } else {
355            return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context);
356        }
357    }
358
359    @Override
360    public Socket createLayeredSocket(
361            final Socket socket,
362            final String target,
363            final int port,
364            final HttpContext context) throws IOException {
365        final SSLSocket sslsock = (SSLSocket) this.socketfactory.createSocket(
366                socket,
367                target,
368                port,
369                true);
370        if (supportedProtocols != null) {
371            sslsock.setEnabledProtocols(supportedProtocols);
372        } else {
373            // If supported protocols are not explicitly set, remove all SSL protocol versions
374            final String[] allProtocols = sslsock.getEnabledProtocols();
375            final List<String> enabledProtocols = new ArrayList<String>(allProtocols.length);
376            for (final String protocol: allProtocols) {
377                if (!protocol.startsWith("SSL")) {
378                    enabledProtocols.add(protocol);
379                }
380            }
381            if (!enabledProtocols.isEmpty()) {
382                sslsock.setEnabledProtocols(enabledProtocols.toArray(new String[enabledProtocols.size()]));
383            }
384        }
385        if (supportedCipherSuites != null) {
386            sslsock.setEnabledCipherSuites(supportedCipherSuites);
387        }
388
389        if (this.log.isDebugEnabled()) {
390            this.log.debug("Enabled protocols: " + Arrays.asList(sslsock.getEnabledProtocols()));
391            this.log.debug("Enabled cipher suites:" + Arrays.asList(sslsock.getEnabledCipherSuites()));
392        }
393
394        prepareSocket(sslsock);
395        this.log.debug("Starting handshake");
396        sslsock.startHandshake();
397        verifyHostname(sslsock, target);
398        return sslsock;
399    }
400
401    private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException {
402        try {
403            SSLSession session = sslsock.getSession();
404            if (session == null) {
405                // In our experience this only happens under IBM 1.4.x when
406                // spurious (unrelated) certificates show up in the server'
407                // chain.  Hopefully this will unearth the real problem:
408                final InputStream in = sslsock.getInputStream();
409                in.available();
410                // If ssl.getInputStream().available() didn't cause an
411                // exception, maybe at least now the session is available?
412                session = sslsock.getSession();
413                if (session == null) {
414                    // If it's still null, probably a startHandshake() will
415                    // unearth the real problem.
416                    sslsock.startHandshake();
417                    session = sslsock.getSession();
418                }
419            }
420            if (session == null) {
421                throw new SSLHandshakeException("SSL session not available");
422            }
423
424            if (this.log.isDebugEnabled()) {
425                this.log.debug("Secure session established");
426                this.log.debug(" negotiated protocol: " + session.getProtocol());
427                this.log.debug(" negotiated cipher suite: " + session.getCipherSuite());
428
429                try {
430
431                    final Certificate[] certs = session.getPeerCertificates();
432                    final X509Certificate x509 = (X509Certificate) certs[0];
433                    final X500Principal peer = x509.getSubjectX500Principal();
434
435                    this.log.debug(" peer principal: " + peer.toString());
436                    final Collection<List<?>> altNames1 = x509.getSubjectAlternativeNames();
437                    if (altNames1 != null) {
438                        final List<String> altNames = new ArrayList<String>();
439                        for (final List<?> aC : altNames1) {
440                            if (!aC.isEmpty()) {
441                                altNames.add((String) aC.get(1));
442                            }
443                        }
444                        this.log.debug(" peer alternative names: " + altNames);
445                    }
446
447                    final X500Principal issuer = x509.getIssuerX500Principal();
448                    this.log.debug(" issuer principal: " + issuer.toString());
449                    final Collection<List<?>> altNames2 = x509.getIssuerAlternativeNames();
450                    if (altNames2 != null) {
451                        final List<String> altNames = new ArrayList<String>();
452                        for (final List<?> aC : altNames2) {
453                            if (!aC.isEmpty()) {
454                                altNames.add((String) aC.get(1));
455                            }
456                        }
457                        this.log.debug(" issuer alternative names: " + altNames);
458                    }
459                } catch (final Exception ignore) {
460                }
461            }
462
463            if (!this.hostnameVerifier.verify(hostname, session)) {
464                final Certificate[] certs = session.getPeerCertificates();
465                final X509Certificate x509 = (X509Certificate) certs[0];
466                final List<SubjectName> subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509);
467                throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " +
468                        "of the subject alternative names: " + subjectAlts);
469            }
470            // verifyHostName() didn't blowup - good!
471        } catch (final IOException iox) {
472            // close the socket before re-throwing the exception
473            try { sslsock.close(); } catch (final Exception x) { /*ignore*/ }
474            throw iox;
475        }
476    }
477
478}