Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support enabling TLSv1.2 on Android 4.1-4.4. #2372

Closed
mlc opened this issue Feb 25, 2016 · 64 comments
Closed

support enabling TLSv1.2 on Android 4.1-4.4. #2372

mlc opened this issue Feb 25, 2016 · 64 comments
Labels
android Relates to usage specifically on Android

Comments

@mlc
Copy link
Contributor

mlc commented Feb 25, 2016

Our lawyers and security consultants claim that for PCI compliance*, we must disable TLS 1.0 and 1.1 on our servers. For some confusing reason, Android has supported TLS 1.2 since API 16 (android 4.1) but enabled it by default only since API 20 (android "4.4W").

With okhttp 2.6, we were able to force use of TLS 1.2 with:

OkHttpClient cli = new OkHttpClient();
SSLContext sc = SSLContext.getInstance("TLSv1.2");
sc.init(null, null, null);
cli.setSslSocketFactory(new Tls12SocketFactory(sc.getSocketFactory()));
ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
        .tlsVersions(TlsVersion.TLS_1_2)
        .build();
cli.setConnectionSpecs(ImmutableList.of(cs));

where Tls12SocketFactory is this.

However, okhttp 3.1 uses some kind of reflection on internal implementation details of the SSLSocketFactory, so the above implementation no longer works. And, indeed, it's a bit silly to make callers write so much code anyway. Specifying TLS_1_2 in the ConnectionSpec should be enough to get TLSv1.2 whenever it is supported.

As far as I can tell, the only reason why the custom socket factory is needed in the first place is that ConnectionSpec.supportedSpec() calls SSLSocket.getEnabledProtocols() to learn the list of protocols supported by the system, so on Android 4.x where TLS 1.2 is supported but not enabled by default, OkHttp thinks 1.2 is not supported at all.

Sorry for this long bug report: I think the fix is as simple as changing getEnabledProtocols() above to getSupportedProtocols() but wanted to submit this bug for discussion before making a PR with such a change, in case there is some affirmative reason why it's the other way now.

* Originally I understood the PCI compliance deadline to be June 2016; however, it seems like it has since been changed to be June 2018. Regardless, OkHttp should support this change for users that want it.

@swankjesse
Copy link
Member

As a quick fix, try renaming the SSLSocketFactory field in Tls12SocketFactory to delegate. It’s a gross hack, and it’s sad, and it’s how we cope with the absence of the APIs we need doing fancy TLS in Java.

@swankjesse
Copy link
Member

No action for us to take here.

@gotev
Copy link

gotev commented Sep 5, 2016

Had the same issue on Android < 5.0 (16 <= API < 20). Thanks to your posts, I was able to make this work, so for anyone who gets here, this is the out-of-the-box solution. At the time of this writing, I'm using OkHttp 3.4.1.

Edit:
I've done some tests and the same issue also happens on some Samsung devices with API 21. Solved by applying the solution also for API 21

Add Tls12SocketFactory.java with the following content:

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

/**
 * Enables TLS v1.2 when creating SSLSockets.
 * <p/>
 * For some reason, android supports TLS v1.2 from API 16, but enables it by
 * default only from API 20.
 * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
 * @see SSLSocketFactory
 */
public class Tls12SocketFactory extends SSLSocketFactory {
    private static final String[] TLS_V12_ONLY = {"TLSv1.2"};

    final SSLSocketFactory delegate;

    public Tls12SocketFactory(SSLSocketFactory base) {
        this.delegate = base;
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return delegate.getDefaultCipherSuites();
    }

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

    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        return patch(delegate.createSocket(s, host, port, autoClose));
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        return patch(delegate.createSocket(host, port));
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        return patch(delegate.createSocket(host, port, localHost, localPort));
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        return patch(delegate.createSocket(host, port));
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return patch(delegate.createSocket(address, port, localAddress, localPort));
    }

    private Socket patch(Socket s) {
        if (s instanceof SSLSocket) {
            ((SSLSocket) s).setEnabledProtocols(TLS_V12_ONLY);
        }
        return s;
    }
}

Then, add this method somewhere in your code:

public static OkHttpClient.Builder enableTls12OnPreLollipop(OkHttpClient.Builder client) {
    if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) {
        try {
            SSLContext sc = SSLContext.getInstance("TLSv1.2");
            
            sc.init(null, null, null);
            // a more robust version is to pass a custom X509TrustManager 
            // as the second parameter and make checkServerTrusted to accept your server. 
            // Credits: https://github.com/square/okhttp/issues/2372#issuecomment-1774955225
            
            client.sslSocketFactory(new Tls12SocketFactory(sc.getSocketFactory()));

            ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
                    .tlsVersions(TlsVersion.TLS_1_2)
                    .build();

            List<ConnectionSpec> specs = new ArrayList<>();
            specs.add(cs);
            specs.add(ConnectionSpec.COMPATIBLE_TLS);
            specs.add(ConnectionSpec.CLEARTEXT);

            client.connectionSpecs(specs);
        } catch (Exception exc) {
            Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc);
        }
    }

    return client;
}

And when you create your OkHttp instance, use it for example like this:

private OkHttpClient getNewHttpClient() {
    OkHttpClient.Builder client = new OkHttpClient.Builder()
            .followRedirects(true)
            .followSslRedirects(true)
            .retryOnConnectionFailure(true)
            .cache(null)
            .connectTimeout(5, TimeUnit.SECONDS)
            .writeTimeout(5, TimeUnit.SECONDS)
            .readTimeout(5, TimeUnit.SECONDS);

    return enableTls12OnPreLollipop(client).build();
}

Also, be sure to use well known CAs for your server side certificates when targeting older Androids.

Credits to @StuStirling and @techiebrij:

To check your server side certificates:
https://developer.android.com/reference/javax/net/ssl/SSLEngine.html

Or:
https://gist.github.com/gotev/f1a8a221e2d1d09bcb93e823b8e5a05a

For anyone else that may be struggling with this, the thing that fixed mine was to install the latest security fixes that are bundled with Google Play Services.

ProviderInstaller.installIfNeeded(context);

After doing this, the solution for enabling TLS1.2 worked.

@meyn
Copy link

meyn commented Nov 17, 2016

@gotev thanks for sharing, any particular reason you've added the COMPATIBLE_TLS and CLEARTEXT in your connection specs? aren't these necessary only if you want to allow fallbacks to older TLS versions / cleartext http?

@gotev
Copy link

gotev commented Nov 17, 2016

It's like that to be as generic as possibile. You could always remove them from the client if you only use TLS 1.2

@yoavgrosswild
Copy link

Seems like client.sslSocketFactory(sslSocketFactory) is deprecated and the documentation recommends using client.sslSocketFactory(sslSocketFactory, X509TrustManager trustManager). How do i get an X509TrustManager object? just create new ?

@swankjesse
Copy link
Member

https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.Builder.html#sslSocketFactory-javax.net.ssl.SSLSocketFactory-javax.net.ssl.X509TrustManager-

@cantek41
Copy link

cantek41 commented Apr 2, 2017

SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
TrustManager[] trustManagers = new TrustManager[] { new TrustManagerManipulator() };
sslContext.init(null, trustManagers, new SecureRandom());
SSLSocketFactory noSSLv3Factory = new TLSSocketFactory(sslContext.getSocketFactory());
urlConnection.setSSLSocketFactory(noSSLv3Factory);

https://github.com/IKANOW/Infinit.e/blob/master/core/infinit.e.data_model/src/com/ikanow/infinit/e/data_model/utils/TrustManagerManipulator.java

@aquakul
Copy link

aquakul commented Apr 10, 2017

Slightly unrelated. But once I make the changes, how do I actually confirm that app and server are indeed using TLS 1.2 (on my server TLS 1.0, 1.1 and 1.2 all are enabled). Is there a hint in the HTTP packet ?

@swankjesse
Copy link
Member

Check the Handshake object on the Response.

@lorenzowoodridge
Copy link

Has anyone actually come up with a working solution for KitKat?

mikehardy added a commit to mikehardy/Anki-Android that referenced this issue Dec 12, 2019
This is an intermediate step, and an experiment on the way to supporting TLS1.2 on Android API<=21

I can't override the trust manager in OkApacheClient though, and it's deprecated anyway, so we need to move
to the full OkHttp Request/Response implementation, and alter the OkHttpClient builder invocation like so:

square/okhttp#2372 (comment)
mikehardy added a commit to mikehardy/Anki-Android that referenced this issue Dec 12, 2019
This is an intermediate step, and an experiment on the way to supporting TLS1.2 on Android API<=21

I can't override the trust manager in OkApacheClient though, and it's deprecated anyway, so we need to move
to the full OkHttp Request/Response implementation, and alter the OkHttpClient builder invocation like so:

square/okhttp#2372 (comment)
@3c71
Copy link

3c71 commented Mar 12, 2020

Thanks to gotev's solution (on Sep 5th 2016), allowed me to make https work on Android 4.x up-to 5.0.

That said, it didn't work out of the box because this solution doesn't support servers having self-signed certificates. I had to change the init into this:

sslContext.init(null, trustAllCerts, new java.security.SecureRandom());

Also, the connectionSpecs() call turned out to be useless.

@gotev
Copy link

gotev commented Mar 12, 2020

@3c71 instead of self signed, use https://letsencrypt.org/

@3c71
Copy link

3c71 commented Mar 12, 2020

@gotev, thanks, however I don't use okhttp to connect to my own site, which I have a valid certificate for. I'm obviously referring to all those sites with self-signed certificates, like all those routers or NAS out there which can't actually be accessed with HTTPs unless certificate is either trusted automatically or manually confirmed. In both cases, it's mandatory to change the init() call.

@mirh
Copy link

mirh commented Apr 6, 2020

No action for us to take here.

Ehrm.. I don't want to be that guy, but why did you decide to drop older android versions altogether? (rather than enabling this)
Is it just because of your policy of "only defaults"?
Or were those 200 lines of code becoming that much of a burden (despite even the possibility of the conscrypt embedding)?

And I mean, it's not even like the market for this wouldn't be there given the number of referenced issues above.

@swankjesse
Copy link
Member

Square doesn't ship software on Android 4.4 and I don’t want to do the work to test & support a platform I don’t use. In addition to 200 lines of code, there’s also code to integrate Conscrypt. Conscrypt is native code and there’s cost to making that work reliably everywhere.

I expect that if anyone really cared enough, they’d maintain an unofficial backport! @mirh is that you?

@c4augustus
Copy link

We have slammed into this problem as well because we are providing a free healthcare app to a poor part of the world where many suffering users can only afford a Samsung J1 Ace that is stuck on an non-upgradeable Android 4.4. So lack of support for older Androids turns out to be a rest-of-the-world-problem rather than a first-world-problem.

@swankjesse
Copy link
Member

@c4augustus some options to consider:

  • Get TLSv1.2 via the Google Play Services TLS provider. This requires your users have Google Play devices. You may also need to put UI in your app to prompt users to update Play Services if necessary. And then you just configure it before making an HTTPS call.

  • Get TLSv1.2 via Conscrypt. This requires you ship the TLS native code in your application, and configure it before you make an HTTPS call.

  • Change your HTTPS server to accept TLSv1.1. There are potential security consequences to this. But if your server supports both TLSv1.1 and TLSv1.2 it’s likely to not harm security for the TLSv1.2-capable devices.

@kairusds
Copy link

@c4augustus some options to consider:

  • Get TLSv1.2 via the Google Play Services TLS provider. This requires your users have Google Play devices. You may also need to put UI in your app to prompt users to update Play Services if necessary. And then you just configure it before making an HTTPS call.
  • Get TLSv1.2 via Conscrypt. This requires you ship the TLS native code in your application, and configure it before you make an HTTPS call.
  • Change your HTTPS server to accept TLSv1.1. There are potential security consequences to this. But if your server supports both TLSv1.1 and TLSv1.2 it’s likely to not harm security for the TLSv1.2-capable devices.

Quick question: how do I configure Conscrypt?

@c4augustus
Copy link

Here is how I fixed this for our app. Sorry that these are screenshots, but our repo is private.
Screenshot 2021-09-13 at 11 09 51
Screenshot 2021-09-13 at 11 10 25
Screenshot 2021-09-13 at 11 10 53
Screenshot 2021-09-13 at 11 11 09
Screenshot 2021-09-13 at 11 11 45
Screenshot 2021-09-13 at 11 12 11
Screenshot 2021-09-13 at 11 12 42
Screenshot 2021-09-13 at 11 12 55

@Edijae
Copy link

Edijae commented Jun 21, 2022

Here is how I fixed this for our app. Sorry that these are screenshots, but our repo is private.
Screenshot 2021-09-13 at 11 09 51
Screenshot 2021-09-13 at 11 10 25
Screenshot 2021-09-13 at 11 10 53
Screenshot 2021-09-13 at 11 11 09
Screenshot 2021-09-13 at 11 11 45
Screenshot 2021-09-13 at 11 12 11
Screenshot 2021-09-13 at 11 12 42
Screenshot 2021-09-13 at 11 12 55

@c4augustus the test for this was failing with org.mockito.exceptions.verification.NoInteractionsWanted: No interactions wanted here But found this interaction on mock 'sSLContext'

verifyNoMoreInteractions(
                SSLContext::class.java,
                Platform::class.java,
                sslSocketMock,
                sslContextMock,
                sslSocketFactoryMock,
                platformMock,
                inetAddressMock,
                localInetAddressMock
        )

@CyxouD
Copy link

CyxouD commented Oct 23, 2023

@gotev for me to work (on a specific device without Google Play Services), I had to call sc.init(null, trustManager, null) instead of sc.init(null, null, null), where trustManager is your custom trust manager which extends X509TrustManager. The key point is to write it secure and make checkServerTrusted to accept your server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
android Relates to usage specifically on Android
Projects
None yet
Development

No branches or pull requests