Java – SSL/TLS protocols and cipher suites with the AndroidHttpClient

androidhttpsjavaopensslssl

EDIT: Apologies if my original post was poorly worded. It led to some confusion, represented by comments to the original post. So let me try again:

I started with a question. I wanted to solve a problem on Android, but didn't know how. I spent a lot of time looking around the net for solutions, but found not one single discussion of the matter anywhere. Nevertheless, a number of discussions, including StackOverflow threads, led me to a technique that looked promising. I solved the problem. But the solution was a little involved. So I decided to post the question here, thinking a) there must be a better solution, and hopefully someone will know and post the answere here; or b) maybe this is a good solution, and since I found no discussion of the matter anywhere else on the net, maybe my solution to the problem would be useful to others trying to do the same thing. Either way, the result would be a new contribution to StackOverflow: a question that is not answered elsewhere, with, eventually, the correct answer one way or the other. In fact, StackOverflow even invited me to answer my own question by way of sharing my knowledge when I originally posted it. That was, in fact, part of my motivation. Even the facts of this matter are not collected anywhere that I have found.

So:

Q. When using the AndroidHttpClient to make REST requests via HTTPS, how can I specify which SSL protocols and ciphers to use?

This is important. The point is well taken that there is much that can be done on the server, but there are limits. The same server has to serve browsers, including old ones, as well as other clients. That means the server has to support a broad array of protocols and ciphers. Even within Android, if you have to support a lot of different versions, you're going to have to support a number of different protocols and ciphers.

More importantly, by default, OpenSSL honors the client's cipher preference, not the server's, during the SSL handshake. See this post, for example, which says that you can override that behavior in the client by setting SSL_OP_CIPHER_SERVER_PREFERENCE. It's not entirely clear if this option can even be set on an SSLSocket in Java. Even if it can, you can set the cipher list yourself or tell your client to honor the server's list. Otherwise, you're getting the Android default, whatever it may be for the version you're running on (not the version you link against).

If you take the defaults, the preference list sent by the client to the server by a Jellybean 4.2+ client can be seen here, starting around line 504. The default list of protocols starts around line 620. Though Jellybean 4.2+ includes support for OpenSSL 1.0.1, particularly TLSv1.1 and TLSv1.2, these protocols are not enabled by default. If you do not do something like what I've done, you cannot take advantage of TLSv1.2 support, despite the fact that support for TLSv1.2 is advertised on recent versions of Android. And the details vary quite a bit as you go back through previous Android versions. At least, you may wish to take a close look at the defaults on all supported versions and see what your client is actually doing. You might be surprised.

There is lots more you can say about support for various protocols and ciphers. The point is, there can at times be a need to change these settings in a client.

A. Use a custom SSLSocketFactory

This worked well for me, and in the end, it was not very much code. But it was a little thorny for a couple of reasons:

  • There are two different SSLSocketFactory classes. The client needs an
    org.apache.http.conn.ssl.SSLSocketFactory, but OpenSSL returns a
    javax.net.ssl.SSLSocketFactory. This is definitely confusing. I used
    delegation to make the one call the other, without much problem.
  • Watch out for the difference between the OpenSSLContextImpl and the
    SSLContextImpl. One just wraps the other, but they are not
    interchangeable. When I used the
    SSLContextImpl.engineGetSocketFactory method—I forget what exactly
    happened, but something quietly failed. Be sure to use an
    OpenSSLContextImpl to get your socket factory, not an SSLContextImpl.
    You might also be able to use
    javax.net.ssl.SSLSocketFactory.getDefault(), but I'm not sure.
  • You cannot easily subclass AndroidHttpClient, because its constructor is
    private. This is unfortunate, since it provides some other nice
    goodies, like making sure you shut it down properly instead of
    leaking resources. The DefaultHttpClient works just fine. I borrowed
    the newInstance method from AndroidHttpClient (around line 105).

The key points:

public class SecureSocketFactory extends SSLSocketFactory {
    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        // order should not matter here; the server should select the highest
        // one from this list that it supports
        s.setEnabledProtocols(new String[] { "TLSv1.2", "TLSv1" });

        // order matters here; specify in preference order
        s.setEnabledCipherSuites(new String[] { "ECDHE-RSA-RC4-SHA", "RC4-SHA" });

Then:

// when creating client
HttpParams params;
SchemeRegistry schemeRegistry = new SchemeRegistry();

// use custom socket factory for https
SSLSocketFactory sf = new SecureSocketFactory();
schemeRegistry.register(new Scheme("https", sf, 443));

// use the default for http
schemeRegistry.register(new Scheme("http",
            PlainSocketFactory.getSocketFactory(), 80));

ClientConnectionManager manager =
            new ThreadSafeClientConnManager(params, schemeRegistry);

HttpClient client = new DefaultHttpClient(manager, params);

Below Android 3.0 (Honeycomb/SDK 11), the supported cipher choices become more limited, and there's less motivation to override the defaults. On FROYO/SDK 8, my SecureSocketFactory blows up for some reason, and the jury is out on 10. But it seems to work for 11 upward just fine.

The full solution is in a public github repo.

Another solution might be to use an HttpsUrlConnection, which makes it easy to use a custom socket factory, but I imagine you'll probably lose even more of the convenience of the high-level HTTP client. I don't have any experience with HttpsUrlConnection.

Best Answer

When using the AndroidHttpClient to make REST requests via HTTPS, how can I specify which SSL protocols and ciphers to use?

I don't believe you can do it with AndroidHttpClient. Everything I've done to harden the channel (like cipher lists, certificate pinning, and public key pinning) required a custom class somewhere, whether it was SSLSocketFactory or X509TrustManager. That's Java and that's Android. See How to override the cipherlist sent to the server by Android when using HttpsURLConnection?.