androidsslamazon-web-servicespoodle-attacksslsocketfactory

Disable SSL as a protocol in HttpsURLConnection


Due to the POODLE vulnerability, my server, hosted in Amazon AWS does no longer support SSLv3.

As a result, the first HTTPS connection my Android app does against the server results in an error when the connection was being established.

Error reading server response: javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x77d8ab68: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0x7339ad74:0x00000000)
       [....]
Caused by: javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x77d8ab68: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0x7339ad74:0x00000000)
       at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:448)
       at com.android.okhttp.Connection.upgradeToTls(Connection.java:146)
       at com.android.okhttp.Connection.connect(Connection.java:107)
       at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:294)
       at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:255)
       at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)

The error happens only in the first request. Subsequent requests work for some time.

To fix this I'm trying to remove SSL from the list of protocols accepted by the Android client, and ensure I'm going only with TLS. To do this, I set a custom SSLSocketFactory which removes SSL from the list of enabled protocols and supported cypher suites.

/**
 * SSLSocketFactory that wraps one existing SSLSocketFactory and delegetes into it adding
 * a new cipher suite
 */
public class TLSOnlySocketFactory extends SSLSocketFactory {

    private final SSLSocketFactory delegate;

    public TLSOnlySocketFactory(SSLSocketFactory delegate) {
        this.delegate = delegate;
    }

    @Override
    public String[] getDefaultCipherSuites() {

        return getPreferredDefaultCipherSuites(this.delegate);
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return getPreferredSupportedCipherSuites(this.delegate);
    }



    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        final Socket socket = this.delegate.createSocket(s, host, port, autoClose);

        ((SSLSocket)socket).setEnabledCipherSuites(getPreferredDefaultCipherSuites(delegate));
        ((SSLSocket)socket).setEnabledProtocols(getEnabledProtocols((SSLSocket)socket));

        return socket;
    }



   [.....]

        ((SSLSocket)socket).setEnabledCipherSuites(getPreferredDefaultCipherSuites(delegate));
        ((SSLSocket) socket).setEnabledProtocols(getEnabledProtocols((SSLSocket)socket));

        return socket;
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        final Socket socket = this.delegate.createSocket(host, port);

        ((SSLSocket)socket).setEnabledCipherSuites(getPreferredDefaultCipherSuites(delegate));
        ((SSLSocket) socket).setEnabledProtocols(getEnabledProtocols((SSLSocket)socket));

        return socket;
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        final Socket socket = this.delegate.createSocket(address, port, localAddress, localPort);

        ((SSLSocket)socket).setEnabledCipherSuites(getPreferredDefaultCipherSuites(delegate));
        ((SSLSocket) socket).setEnabledProtocols(getEnabledProtocols((SSLSocket)socket));

        return socket;
    }

    private String[] getPreferredDefaultCipherSuites(SSLSocketFactory sslSocketFactory) {
        return getCipherSuites(sslSocketFactory.getDefaultCipherSuites());
    }

    private String[] getPreferredSupportedCipherSuites(SSLSocketFactory sslSocketFactory) {
        return getCipherSuites(sslSocketFactory.getSupportedCipherSuites());
    }

    private String[] getCipherSuites(String[] cipherSuites) {
        final ArrayList<String> suitesList = new ArrayList<String>(Arrays.asList(cipherSuites));
        final Iterator<String> iterator = suitesList.iterator();
        while (iterator.hasNext()) {
            final String cipherSuite = iterator.next();
            if (cipherSuite.contains("SSL")) {
                iterator.remove();
            }
        }
        return suitesList.toArray(new String[suitesList.size()]);
    }

    private String[] getEnabledProtocols(SSLSocket socket) {
        final ArrayList<String> protocolList = new ArrayList<String>(Arrays.asList(socket.getSupportedProtocols()));
        final Iterator<String> iterator = protocolList.iterator();
        while (iterator.hasNext()) {
            final String protocl = iterator.next();
            if (protocl.contains("SSL")) {
                iterator.remove();
            }
        }
        return protocolList.toArray(new String[protocolList.size()]);
     }

}

As you see, my SSLSocketFactory delegates into another SSLSocketFactory and what it does is simply removing SSL from the list of enabled protocols.

I establish this factory as

final TLSOnlySocketFactory tlsOnlySocketFactory = new TLSOnlySocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
HttpsURLConnection.setDefaultSSLSocketFactory(tlsOnlySocketFactory);

This does NOT fix the issue. From time to time, I still see the error when the connection was established. Oddly enough, this does not fix it, but it clearly minimises the occurrences of the issue.

How could I force the HttpsUrlConnection in my Android client to use only TLS?

Thank you.


Solution

  • I think I have solved this. The fundamental idea is the same than in the code in the question (avoid SSLv3 as the only protocol available), but the code performing it is different:

    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.net.InetAddress;
    import java.net.Socket;
    import java.net.SocketAddress;
    import java.net.SocketException;
    import java.nio.channels.SocketChannel;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    import javax.net.ssl.HandshakeCompletedListener;
    import javax.net.ssl.HttpsURLConnection;
    import javax.net.ssl.SSLParameters;
    import javax.net.ssl.SSLSession;
    import javax.net.ssl.SSLSocket;
    import javax.net.ssl.SSLSocketFactory;
    
        /**
         * {@link javax.net.ssl.SSLSocketFactory} that doesn't allow {@code SSLv3} only connections
         * <p>fixes https://github.com/koush/ion/issues/386</p>
         *
         * <p> see https://code.google.com/p/android/issues/detail?id=78187 </p>
         */
        public class NoSSLv3Factory extends SSLSocketFactory {
            private final SSLSocketFactory delegate;
    
            public NoSSLv3Factory() {
                this.delegate = HttpsURLConnection.getDefaultSSLSocketFactory();
            }
    
            @Override
            public String[] getDefaultCipherSuites() {
                return delegate.getDefaultCipherSuites();
            }
    
            @Override
            public String[] getSupportedCipherSuites() {
                return delegate.getSupportedCipherSuites();
            }
    
            private static Socket makeSocketSafe(Socket socket) {
                if (socket instanceof SSLSocket) {
                    socket = new NoSSLv3SSLSocket((SSLSocket) socket);
                }
                return socket;
            }
    
            @Override
            public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
                return makeSocketSafe(delegate.createSocket(s, host, port, autoClose));
            }
    
            @Override
            public Socket createSocket(String host, int port) throws IOException {
                return makeSocketSafe(delegate.createSocket(host, port));
            }
    
            @Override
            public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
                return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort));
            }
    
            @Override
            public Socket createSocket(InetAddress host, int port) throws IOException {
                return makeSocketSafe(delegate.createSocket(host, port));
            }
    
            @Override
            public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
                return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort));
            }
    
            /**
             * Created by robUx4 on 25/10/2014.
             */
            private static class DelegateSSLSocket extends SSLSocket {
    
                protected final SSLSocket delegate;
    
                DelegateSSLSocket(SSLSocket delegate) {
                    this.delegate = delegate;
                }
    
                @Override
                public String[] getSupportedCipherSuites() {
                    return delegate.getSupportedCipherSuites();
                }
    
                @Override
                public String[] getEnabledCipherSuites() {
                    return delegate.getEnabledCipherSuites();
                }
    
                @Override
                public void setEnabledCipherSuites(String[] suites) {
                    delegate.setEnabledCipherSuites(suites);
                }
    
                @Override
                public String[] getSupportedProtocols() {
                    return delegate.getSupportedProtocols();
                }
    
                @Override
                public String[] getEnabledProtocols() {
                    return delegate.getEnabledProtocols();
                }
    
                @Override
                public void setEnabledProtocols(String[] protocols) {
                    delegate.setEnabledProtocols(protocols);
                }
    
                @Override
                public SSLSession getSession() {
                    return delegate.getSession();
                }
    
                @Override
                public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
                    delegate.addHandshakeCompletedListener(listener);
                }
    
                @Override
                public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
                    delegate.removeHandshakeCompletedListener(listener);
                }
    
                @Override
                public void startHandshake() throws IOException {
                    delegate.startHandshake();
                }
    
                @Override
                public void setUseClientMode(boolean mode) {
                    delegate.setUseClientMode(mode);
                }
    
                @Override
                public boolean getUseClientMode() {
                    return delegate.getUseClientMode();
                }
    
                @Override
                public void setNeedClientAuth(boolean need) {
                    delegate.setNeedClientAuth(need);
                }
    
                @Override
                public void setWantClientAuth(boolean want) {
                    delegate.setWantClientAuth(want);
                }
    
                @Override
                public boolean getNeedClientAuth() {
                    return delegate.getNeedClientAuth();
                }
    
                @Override
                public boolean getWantClientAuth() {
                    return delegate.getWantClientAuth();
                }
    
                @Override
                public void setEnableSessionCreation(boolean flag) {
                    delegate.setEnableSessionCreation(flag);
                }
    
                @Override
                public boolean getEnableSessionCreation() {
                    return delegate.getEnableSessionCreation();
                }
    
                @Override
                public void bind(SocketAddress localAddr) throws IOException {
                    delegate.bind(localAddr);
                }
    
                @Override
                public synchronized void close() throws IOException {
                    delegate.close();
                }
    
                @Override
                public void connect(SocketAddress remoteAddr) throws IOException {
                    delegate.connect(remoteAddr);
                }
    
                @Override
                public void connect(SocketAddress remoteAddr, int timeout) throws IOException {
                    delegate.connect(remoteAddr, timeout);
                }
    
                @Override
                public SocketChannel getChannel() {
                    return delegate.getChannel();
                }
    
                @Override
                public InetAddress getInetAddress() {
                    return delegate.getInetAddress();
                }
    
                @Override
                public InputStream getInputStream() throws IOException {
                    return delegate.getInputStream();
                }
    
                @Override
                public boolean getKeepAlive() throws SocketException {
                    return delegate.getKeepAlive();
                }
    
                @Override
                public InetAddress getLocalAddress() {
                    return delegate.getLocalAddress();
                }
    
                @Override
                public int getLocalPort() {
                    return delegate.getLocalPort();
                }
    
                @Override
                public SocketAddress getLocalSocketAddress() {
                    return delegate.getLocalSocketAddress();
                }
    
                @Override
                public boolean getOOBInline() throws SocketException {
                    return delegate.getOOBInline();
                }
    
                @Override
                public OutputStream getOutputStream() throws IOException {
                    return delegate.getOutputStream();
                }
    
                @Override
                public int getPort() {
                    return delegate.getPort();
                }
    
                @Override
                public synchronized int getReceiveBufferSize() throws SocketException {
                    return delegate.getReceiveBufferSize();
                }
    
                @Override
                public SocketAddress getRemoteSocketAddress() {
                    return delegate.getRemoteSocketAddress();
                }
    
                @Override
                public boolean getReuseAddress() throws SocketException {
                    return delegate.getReuseAddress();
                }
    
                @Override
                public synchronized int getSendBufferSize() throws SocketException {
                    return delegate.getSendBufferSize();
                }
    
                @Override
                public int getSoLinger() throws SocketException {
                    return delegate.getSoLinger();
                }
    
                @Override
                public synchronized int getSoTimeout() throws SocketException {
                    return delegate.getSoTimeout();
                }
    
                @Override
                public boolean getTcpNoDelay() throws SocketException {
                    return delegate.getTcpNoDelay();
                }
    
                @Override
                public int getTrafficClass() throws SocketException {
                    return delegate.getTrafficClass();
                }
    
                @Override
                public boolean isBound() {
                    return delegate.isBound();
                }
    
                @Override
                public boolean isClosed() {
                    return delegate.isClosed();
                }
    
                @Override
                public boolean isConnected() {
                    return delegate.isConnected();
                }
    
                @Override
                public boolean isInputShutdown() {
                    return delegate.isInputShutdown();
                }
    
                @Override
                public boolean isOutputShutdown() {
                    return delegate.isOutputShutdown();
                }
    
                @Override
                public void sendUrgentData(int value) throws IOException {
                    delegate.sendUrgentData(value);
                }
    
                @Override
                public void setKeepAlive(boolean keepAlive) throws SocketException {
                    delegate.setKeepAlive(keepAlive);
                }
    
                @Override
                public void setOOBInline(boolean oobinline) throws SocketException {
                    delegate.setOOBInline(oobinline);
                }
    
                @Override
                public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
                    delegate.setPerformancePreferences(connectionTime, latency, bandwidth);
                }
    
                @Override
                public synchronized void setReceiveBufferSize(int size) throws SocketException {
                    delegate.setReceiveBufferSize(size);
                }
    
                @Override
                public void setReuseAddress(boolean reuse) throws SocketException {
                    delegate.setReuseAddress(reuse);
                }
    
                @Override
                public synchronized void setSendBufferSize(int size) throws SocketException {
                    delegate.setSendBufferSize(size);
                }
    
                @Override
                public void setSoLinger(boolean on, int timeout) throws SocketException {
                    delegate.setSoLinger(on, timeout);
                }
    
                @Override
                public synchronized void setSoTimeout(int timeout) throws SocketException {
                    delegate.setSoTimeout(timeout);
                }
    
                @Override
                public void setSSLParameters(SSLParameters p) {
                    delegate.setSSLParameters(p);
                }
    
                @Override
                public void setTcpNoDelay(boolean on) throws SocketException {
                    delegate.setTcpNoDelay(on);
                }
    
                @Override
                public void setTrafficClass(int value) throws SocketException {
                    delegate.setTrafficClass(value);
                }
    
                @Override
                public void shutdownInput() throws IOException {
                    delegate.shutdownInput();
                }
    
                @Override
                public void shutdownOutput() throws IOException {
                    delegate.shutdownOutput();
                }
    
                @Override
                public String toString() {
                    return delegate.toString();
                }
    
                @Override
                public boolean equals(Object o) {
                    return delegate.equals(o);
                }
            }
    
            /**
             * An {@link javax.net.ssl.SSLSocket} that doesn't allow {@code SSLv3} only connections
             * <p>fixes https://github.com/koush/ion/issues/386</p>
             */
            private static class NoSSLv3SSLSocket extends DelegateSSLSocket {
    
                private NoSSLv3SSLSocket(SSLSocket delegate) {
                    super(delegate);
    
                    String canonicalName = delegate.getClass().getCanonicalName();
                    if (!canonicalName.equals("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl")) {
                        // try replicate the code from HttpConnection.setupSecureSocket()
                        try {
                            Method msetUseSessionTickets = delegate.getClass().getMethod("setUseSessionTickets", boolean.class);
                            if (null != msetUseSessionTickets) {
                                msetUseSessionTickets.invoke(delegate, true);
                            }
                        } catch (NoSuchMethodException ignored) {
                        } catch (InvocationTargetException ignored) {
                        } catch (IllegalAccessException ignored) {
                        }
                    }
                }
    
                @Override
                public void setEnabledProtocols(String[] protocols) {
                    if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) {
                        // no way jose
                        // see issue https://code.google.com/p/android/issues/detail?id=78187
                        List<String> enabledProtocols = new ArrayList<String>(Arrays.asList(delegate.getEnabledProtocols()));
                        if (enabledProtocols.size() > 1) {
                            enabledProtocols.remove("SSLv3");
                        }
                        protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]);
                    }
                    super.setEnabledProtocols(protocols);
                }
            }
    
        }
    

    and somewhere in your code, before creating the Connection:

        static {
        HttpsURLConnection.setDefaultSSLSocketFactory(new NoSSLv3Factory());
    }
    

    This code is taken from https://code.google.com/p/android/issues/detail?id=78187, where you can find a fully explanation on why this is happening in Android 4.X.

    I've had this in production from one week and seems to have done the trick.