How can I use TLS 1.2 in Java 6 with an Apache HttpClient?

13,895

Solution 1

I have come up with a solution that does not require upgrading to Java 8. As a disclaimer, I know very little about https security, and I'm sure there are some issues that a more knowledgeable person may notice. This method requires using the built-in HttpsURLConnection, which is very primitive if you're used to using things like Unirest, or even apache HttpClient.

First you need to add BouncyCastle to you pom.

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.54</version>
</dependency>

Then you will create an extension of SSLSocketFactory. Warning! This socket factory will accept all certificates, so use at your own risk.

import java.io.*;
import java.net.UnknownHostException;
import java.security.*;
import java.security.cert.*;
import java.util.*;

import javax.net.ssl.*;
import javax.security.cert.X509Certificate;

import org.bouncycastle.crypto.tls.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class TLSSocketConnectionFactory extends SSLSocketFactory {

    static {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    @Override
    public Socket createSocket(Socket socket, final String host, int port,
            boolean arg3) throws IOException {
        if (socket == null) {
            socket = new Socket();
        }
        if (!socket.isConnected()) {
            socket.connect(new InetSocketAddress(host, port));
        }

        final TlsClientProtocol tlsClientProtocol = new     TlsClientProtocol(socket.getInputStream(), socket.getOutputStream(), new     SecureRandom());

        return _createSSLSocket(host, tlsClientProtocol);
    }

    @Override public String[] getDefaultCipherSuites() { return null; }
    @Override public String[] getSupportedCipherSuites() { return null; }
    @Override public Socket createSocket(String host, int port) throws IOException, UnknownHostException { throw new UnsupportedOperationException(); }
    @Override public Socket createSocket(InetAddress host, int port) throws IOException { throw new UnsupportedOperationException(); }
    @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return null; }
    @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { throw new UnsupportedOperationException(); }

    private SSLSocket _createSSLSocket(final String host, final TlsClientProtocol tlsClientProtocol) {
        return new SSLSocket() {
            private java.security.cert.Certificate[] peertCerts;

            @Override public InputStream getInputStream() throws IOException { return tlsClientProtocol.getInputStream(); }
            @Override public OutputStream getOutputStream() throws IOException { return tlsClientProtocol.getOutputStream(); }
            @Override public synchronized void close() throws IOException { tlsClientProtocol.close(); }
            @Override public void addHandshakeCompletedListener( HandshakeCompletedListener arg0) { }
            @Override public boolean getEnableSessionCreation() { return false; }
            @Override public String[] getEnabledCipherSuites() { return null; }
            @Override public String[] getEnabledProtocols() { return null; }
            @Override public boolean getNeedClientAuth() { return false; }

            @Override
            public SSLSession getSession() {
                return new SSLSession() {

                    @Override
                    public int getApplicationBufferSize() {
                        return 0;
                    }

                    @Override public String getCipherSuite() { throw new UnsupportedOperationException(); }
                    @Override public long getCreationTime() { throw new UnsupportedOperationException(); }
                    @Override public byte[] getId() { throw new UnsupportedOperationException(); }
                    @Override public long getLastAccessedTime() { throw new UnsupportedOperationException(); }
                    @Override public java.security.cert.Certificate[] getLocalCertificates() { throw new UnsupportedOperationException(); }
                    @Override public Principal getLocalPrincipal() { throw new UnsupportedOperationException(); }
                    @Override public int getPacketBufferSize() { throw new UnsupportedOperationException(); }
                    @Override public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { return null; }
                    @Override public java.security.cert.Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { return peertCerts; }
                    @Override public String getPeerHost() { throw new UnsupportedOperationException(); }
                    @Override public int getPeerPort() { return 0; }
                    @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { return null; }
                    @Override public String getProtocol() { throw new UnsupportedOperationException(); }
                    @Override public SSLSessionContext getSessionContext() { throw new UnsupportedOperationException(); }
                    @Override public Object getValue(String arg0) { throw new UnsupportedOperationException(); }
                    @Override public String[] getValueNames() { throw new UnsupportedOperationException(); }
                    @Override public void invalidate() { throw new UnsupportedOperationException(); }
                    @Override public boolean isValid() { throw new UnsupportedOperationException(); }
                    @Override public void putValue(String arg0, Object arg1) { throw new UnsupportedOperationException(); }
                    @Override public void removeValue(String arg0) { throw new UnsupportedOperationException(); }
            };
        }

        @Override public String[] getSupportedProtocols() { return null; }
        @Override public boolean getUseClientMode() { return false; }
        @Override public boolean getWantClientAuth() { return false; }
        @Override public void removeHandshakeCompletedListener(HandshakeCompletedListener arg0) { }
        @Override public void setEnableSessionCreation(boolean arg0) { }
        @Override public void setEnabledCipherSuites(String[] arg0) { }
        @Override public void setEnabledProtocols(String[] arg0) { }
        @Override public void setNeedClientAuth(boolean arg0) { }
        @Override public void setUseClientMode(boolean arg0) { }
        @Override public void setWantClientAuth(boolean arg0) { }
        @Override public String[] getSupportedCipherSuites() { return null; }

            @Override
            public void startHandshake() throws IOException {
                tlsClientProtocol.connect(new DefaultTlsClient() {

                    @SuppressWarnings("unchecked")
                    @Override
                    public Hashtable<Integer, byte[]> getClientExtensions() throws IOException {
                        Hashtable<Integer, byte[]> clientExtensions = super.getClientExtensions();
                        if (clientExtensions == null) {
                            clientExtensions = new Hashtable<Integer, byte[]>();
                        }

                        //Add host_name
                        byte[] host_name = host.getBytes();

                        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        final DataOutputStream dos = new DataOutputStream(baos);
                        dos.writeShort(host_name.length + 3);
                        dos.writeByte(0);
                        dos.writeShort(host_name.length);
                        dos.write(host_name);
                        dos.close();
                        clientExtensions.put(ExtensionType.server_name, baos.toByteArray());
                        return clientExtensions;
                    }

                    @Override
                    public TlsAuthentication getAuthentication() throws IOException {
                        return new TlsAuthentication() {

                            @Override
                            public void notifyServerCertificate(Certificate serverCertificate) throws IOException {
                                try {
                                    KeyStore ks = _loadKeyStore();

                                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
                                    List<java.security.cert.Certificate> certs = new LinkedList<java.security.cert.Certificate>();
                                    boolean trustedCertificate = false;
                                    for ( org.bouncycastle.asn1.x509.Certificate c : serverCertificate.getCertificateList()) {
                                        java.security.cert.Certificate cert = cf.generateCertificate(new ByteArrayInputStream(c.getEncoded()));
                                        certs.add(cert);

                                        String alias = ks.getCertificateAlias(cert);
                                        if(alias != null) {
                                            if (cert instanceof java.security.cert.X509Certificate) {
                                                try {
                                                    ( (java.security.cert.X509Certificate) cert).checkValidity();
                                                    trustedCertificate = true;
                                                } catch(CertificateExpiredException cee) {
                                                   // Accept all the certs!
                                                }
                                            }
                                        } else {
                                            // Accept all the certs!
                                        }

                                    }
                                    if (!trustedCertificate) {
                                        // Accept all the certs!
                                    }
                                    peertCerts = certs.toArray(new java.security.cert.Certificate[0]);
                                } catch (Exception ex) {
                                    ex.printStackTrace();
                                    throw new IOException(ex);
                                }
                            }

                            @Override
                            public TlsCredentials getClientCredentials(CertificateRequest certificateRequest) throws IOException {
                                return null;
                            }

                            private KeyStore _loadKeyStore() throws Exception {
                                FileInputStream trustStoreFis = null;
                                try {
                                    KeyStore localKeyStore = null;

                                    String trustStoreType = System.getProperty("javax.net.ssl.trustStoreType")!=null?System.getProperty("javax.net.ssl.trustStoreType"):KeyStore.getDefaultType();
                                    String trustStoreProvider = System.getProperty("javax.net.ssl.trustStoreProvider")!=null?System.getProperty("javax.net.ssl.trustStoreProvider"):"";

                                    if (trustStoreType.length() != 0) {
                                        if (trustStoreProvider.length() == 0) {
                                            localKeyStore = KeyStore.getInstance(trustStoreType);
                                        } else {
                                            localKeyStore = KeyStore.getInstance(trustStoreType, trustStoreProvider);
                                        }

                                        char[] keyStorePass = null;
                                        String str5 = System.getProperty("javax.net.ssl.trustStorePassword")!=null?System.getProperty("javax.net.ssl.trustStorePassword"):"";

                                        if (str5.length() != 0) {
                                            keyStorePass = str5.toCharArray();
                                        }

                                        localKeyStore.load(trustStoreFis, keyStorePass);

                                        if (keyStorePass != null) {
                                            for (int i = 0; i < keyStorePass.length; i++) {
                                                keyStorePass[i] = 0;
                                            }
                                        }
                                    }
                                    return localKeyStore;
                                } finally {
                                    if (trustStoreFis != null) {
                                        trustStoreFis.close();
                                    }
                                }
                            }
                        };
                    }

                });
            } // startHandshake
        };
    }
}

Next, you need to use your new socket factory. If you are using Spring to handle your Web Services calls, you can extend the existing HttpUrlConnectionMessageSender in order to send your calls for you. Just make sure to update your message sender bean in your spring config to use this class.

import java.io.IOException;
import java.net.*;

import javax.net.ssl.HttpsURLConnection;

import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.http.*;


public class HttpsUrlConnectionMessageSender extends HttpUrlConnectionMessageSender {

    @Override
    public WebServiceConnection createConnection(URI uri) throws IOException {

        URL url = uri.toURL();
        URLConnection connection = url.openConnection();
        if (!(connection instanceof HttpsURLConnection)) {
            throw new HttpTransportException("URI [" + uri + "] is not an HTTPS URL");
        } else {
            HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection;
            httpsURLConnection.setSSLSocketFactory(new TLSSocketConnectionFactory());
            prepareConnection(httpsURLConnection);
            return new HttpsUrlConnection(httpsURLConnection);
        }
    }

    private static class HttpsUrlConnection extends HttpUrlConnection {
        private HttpsUrlConnection(HttpsURLConnection connection) {
            super(connection);
        }
    }
}

Alternatively, if you are not using Spring, or using Rest, you can manually create an HttpsURLConnection as is done in the class above, and send your messages using the outputstream.

Beware that if the service you are communication with uses cookies, you will need to set a system-wide cookie manager. However, the Java 6 cookie manager is bug-ridden. You may need to copy the source-code from the Java 8 CookieManager in order for your cookies to work.

if (CookieHandler.getDefault() == null) {
    CookieHandler.setDefault(new CookieManager());
}

Solution 2

The main problem you will face with Bouncy Castle is that its TLS implementation does not implement the JSSE API. Practically, this means you won't get any support for SSLSocket, SSLSocketFactory (or HttpsURLConnection, but that doesn't matter when you're using Apache HTTP Client).

In addition, TLS isn't the best documented feature of Bouncy Castle, which is going to make your work more difficult. There are examples in the answers to this other question.

You might somehow be able to write you custom Socket implementation that wraps the necessary calls to Bouncy Castle's TLS API. You could then return such sockets from your custom org.apache.http.conn.ssl.SSLConnectionSocketFactory implementation (you could probably use dummy values in the constructors if your subclass does something completely different for its method that return a Socket anyway).

This might be feasible, but it will probably require a considerable amount of effort. An upgrade to Java 8 (which supports TLS 1.2 with the traditional JSSE classes) would generally be easier (depending on your constraints).

Share:
13,895
zalpha314
Author by

zalpha314

Updated on June 04, 2022

Comments

  • zalpha314
    zalpha314 almost 2 years

    I'm working on a Java 6 project using Apache HttpClient 3.5 and Spring Web Services 2.1.0.

    My Spring WebServicesTemplate is sending WebServices requests using an apache HttpClient. However, the service that I am contacting has phased out TLS 1.0 in favor of TLS 1.2. Java 6 does not support TLS 1.2. I was led to bouncycastle, which should give me TLS 1.2 support.

    How can I integrate bouncycastle with Spring or my HttpClient, so that I can send requests that support TLS 1.2? I have found a solution which provides an extended SSLSocketFactory. However, HttpClient only accepts SSLConnectionSocketFactory.

    Upgrading to Java 8 is not feasible in the short term, but I can make it a long-term priority if someone can confirm that it would fix my issue.