3

I'm trying to send https requests to my server using mutual TLS. The server I got working successfully with TLS. But I can't figure out how to do this on the client-side (Android app). I use spring on the java server. Requests from android app are made using HttpsUrlConnection().

I managed to be able to call HttpsUrlConnection() this how my code looks:

public void test() {
        try {
            URL url = new URL(this.apiUrl);
            HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
            urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
            InputStream in = urlConnection.getInputStream();
            System.out.print(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

My server is configured to use TLSv1.2 protocol. Running test() throws this error:

W/System.err: javax.net.ssl.SSLHandshakeException: Handshake failed
        at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:288)
        at com.android.okhttp.internal.io.RealConnection.connectTls(RealConnection.java:196)
        at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:153)
        at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116)
        at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186)
        at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
        at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
        at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
        at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
W/System.err:     at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:248)
        at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getInputStream(DelegatingHttpsURLConnection.java:211)
W/System.err:     at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:30)
        at nl.management.finance.client.RaboClient.test(RaboClient.java:64)
        at nl.management.finance.MainActivity$RESTTask.doInBackground(MainActivity.java:31)
        at nl.management.finance.MainActivity$RESTTask.doInBackground(MainActivity.java:25)
        at android.os.AsyncTask$3.call(AsyncTask.java:378)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:289)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)
    Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x703daa2ff448: Failure in SSL library, usually a protocol error
    error:10000412:SSL routines:OPENSSL_internal:SSLV3_ALERT_BAD_CERTIFICATE (external/boringssl/src/ssl/tls_record.cc:587 0x703daa2b1148:0x00000001)
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
        at com.android.org.conscrypt.NativeSsl.doHandshake(NativeSsl.java:387)
        at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:226)
        ... 22 more

Why do I see SSLV3 in the stacktrace? Is it not using TLSv1.2? Wireshark shows this https://ibb.co/27mpG4r

This code (from @Hakan54) makes the SSLContext:

public class SSLTrustManagerHelper {

    private InputStream keyStore;
    private String keyStorePassword;
    private InputStream trustStore;
    private String trustStorePassword;

    public SSLTrustManagerHelper(InputStream keyStore,
                                 String keyStorePassword,
                                 InputStream trustStore,
                                 String trustStorePassword) throws ClientException {
        if (keyStore == null || keyStorePassword.trim().isEmpty() || trustStore == null || trustStorePassword.trim().isEmpty()) {
            throw new ClientException("TrustStore or KeyStore details are empty, which are required to be present when SSL is enabled");
        }

        this.keyStore = keyStore;
        this.keyStorePassword = keyStorePassword;
        this.trustStore = trustStore;
        this.trustStorePassword = trustStorePassword;
    }

    public SSLContext clientSSLContext() throws ClientException {
        try {
            TrustManagerFactory trustManagerFactory = getTrustManagerFactory(trustStore, trustStorePassword);
            KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keyStore, keyStorePassword);
            this.keyStore.close();
            this.trustStore.close();

            return getSSLContext(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers());
        } catch (UnrecoverableKeyException | NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | KeyManagementException e) {
            e.printStackTrace();
            throw new ClientException(e);
        }
    }

    private static SSLContext getSSLContext(KeyManager[] keyManagers, TrustManager[] trustManagers) throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(keyManagers, trustManagers, null);
        return sslContext;
    }

    private static KeyManagerFactory getKeyManagerFactory(InputStream keystore, String keystorePassword) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException, ClientException {
        KeyStore keyStore = loadKeyStore(keystore, keystorePassword);
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
        return keyManagerFactory;
    }

    private static TrustManagerFactory getTrustManagerFactory(InputStream truststore, String truststorePassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, ClientException {
        KeyStore trustStore = loadKeyStore(truststore, truststorePassword);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);
        return trustManagerFactory;
    }

    private static KeyStore loadKeyStore(InputStream keystoreStream, String keystorePassword) throws ClientException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
        if (keystoreStream == null) {
            throw new ClientException("keystore was null.");
        }

        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
        keystore.load(keystoreStream, keystorePassword.toCharArray());
        return keystore;
    }

}
keesjanbertus
  • 372
  • 1
  • 5
  • 16

1 Answers1

3

What you are looking for is mutual authentication based on certificates. Both the server and the client needs to trust each other to communicate. And if the server just trust that specific client only it shouldn't be possible for any other client to do a request.

The above example looks okay, but it will be easier to configure with the example below:

import static java.util.Objects.isNull;
import static org.apache.commons.lang3.StringUtils.isBlank;

import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

public class SSLTrustManagerHelper {

    private String keyStore;
    private String keyStorePassword;
    private String trustStore;
    private String trustStorePassword;

    public SSLTrustManagerHelper(String keyStore,
                                 String keyStorePassword,
                                 String trustStore,
                                 String trustStorePassword) {
        if (isBlank(keyStore) || isBlank(keyStorePassword) || isBlank(trustStore) || isBlank(trustStorePassword)) {
            throw new ClientException("TrustStore or KeyStore details are empty, which are required to be present when SSL is enabled");
        }

        this.keyStore = keyStore;
        this.keyStorePassword = keyStorePassword;
        this.trustStore = trustStore;
        this.trustStorePassword = trustStorePassword;
    }

    public SSLContext clientSSLContext() {
        try {
            TrustManagerFactory trustManagerFactory = getTrustManagerFactory(trustStore, trustStorePassword);
            KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keyStore, keyStorePassword);

            return getSSLContext(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers());
        } catch (UnrecoverableKeyException | NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | KeyManagementException e) {
            throw new ClientException(e);
        }
    }

    private static SSLContext getSSLContext(KeyManager[] keyManagers, TrustManager[] trustManagers) throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(keyManagers, trustManagers, null);
        return sslContext;
    }

    private static KeyManagerFactory getKeyManagerFactory(String keystorePath, String keystorePassword) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException {
        KeyStore keyStore = loadKeyStore(keystorePath, keystorePassword);
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
        return keyManagerFactory;
    }

    private static TrustManagerFactory getTrustManagerFactory(String truststorePath, String truststorePassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
        KeyStore trustStore = loadKeyStore(truststorePath, truststorePassword);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);
        return trustManagerFactory;
    }

    private static KeyStore loadKeyStore(String keystorePath, String keystorePassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
        try(InputStream keystoreInputStream = SSLTrustManagerHelper.class.getClassLoader().getResourceAsStream(keystorePath)) {
            if (isNull(keystoreInputStream)) {
                throw new ClientException(String.format("Could not find the keystore file with the given location %s", keystorePath));
            }

            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            keystore.load(keystoreInputStream, keystorePassword.toCharArray());
            return keystore;
        }
    }

}

Here you need to provide the location of the keystore and truststore, and also the passwords. The public class will provide you the ssl context which you can load into your http client.

Make sure you have a client keystore with private and public key, and a truststore where you have the public key of the server. And make sure that the server has the public key of the client in its truststore. You also need to provide your server an additional properties in the application.yml file which enforces the server to validate the client. The property is: client-auth: need

See here a full example of setting up mutual authentication for server and client including example project spring-boot-mutual-tls-sll

Update 2022

I have made the above snippet and other utilities available in a library to make it easier and less verbose to setup ssl configuration. Next to that it also contains some validations. See here for the library GitHub - SSLContext Kickstart

The example which I provided at the first place can be replaced with:

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

class App {
    public static void main(String[] args) {
        SSLFactory sslFactory = SSLFactory.builder()
                .withIdentityMaterial("/path/to/resource/identity.jks", "password".toCharArray())
                .withTrustMaterial("/path/to/resource/truststore.jks", "password".toCharArray())
                .build();

        SSLContext sslContext = sslFactory.getSslContext();
        SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();
    }
}
Hakan54
  • 3,121
  • 1
  • 23
  • 37
  • Thank you for your answer. This does not work with android though, for one you can't get a path to a resource file. So I changed it where I pass input stream. I now get a new error. See my updated post. – keesjanbertus Oct 19 '19 at 14:10
  • 1
    Are your keystore and truststore two separate files? Or are you passing the same inputstream what you created in this way: `InputStream keystore = getResources().openRawResource(getResources().getIdentifier("keystore_v1", "raw", this.getPackageName()));` as a truststore parameter to the constructor of the SSLTrustManagerHelper and reusing it for the keystore parameter? If you use the same inputstream twice you will get an EOFException – Hakan54 Oct 20 '19 at 06:56
  • I did pass the same file... :/. How can I split this `BKS` file into a keystore and a truststore? – keesjanbertus Oct 20 '19 at 08:55
  • 1
    You can do two things: 1: Create two inputstreams from the same file and pass both to the constructor or 2: Create a new keystore name it truststore, which contains the public key of the server. The already existing keystore should then only contain the private and public keys of the client. If you are not familiar with the command line tool of keytool I would advice to use [KeyStore Explorer](https://keystore-explorer.org/) or use this [cheatsheet](https://gist.github.com/Hakky54/7a2f0fcbcf5fdf4674d48f1a0b31c862) what i have written to create the keystore and other stuff from the command line – Hakan54 Oct 20 '19 at 10:47
  • doing as you commented got rid of the error. A new error gets thrown now though. Which I put in my post. Please refer to this post for an explanation https://stackoverflow.com/questions/32312932/using-apache-httpclient-with-android-sdk-23-nosuchmethod?rq=1 How can I replace `HttpClient` with the ones suggested in that post? – keesjanbertus Oct 20 '19 at 14:17
  • Would just like to add that my previous comment no longer applies. I've gotten a bit further. But I'm now having issues with the handshake. I yet again updated my post. Thanks for trying to help btw :-). – keesjanbertus Oct 20 '19 at 18:22
  • 1
    Could you provide the handshake log, because the current exception is not providing all the details. You will get it in your console when you provide the following vm argument: `-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake` – Hakan54 Oct 20 '19 at 21:42
  • Unfortunately that's not possible for android apps. VM arguments can't be set because android uses a different VM or something along those lines... – keesjanbertus Oct 21 '19 at 16:51
  • I managed to solve the issue by debugging with a demo java application. Couldn't have done without your help. Cheers! – keesjanbertus Oct 21 '19 at 20:19
  • Great to hear! Succes verder! :) – Hakan54 Oct 21 '19 at 21:23
  • @Hakan54 let me see if I understood correctly. In the keystore, there should be added both public key and private key for the client, am I right? If so, how would the server or the OkHttpclient (I dont know who what entity does what during the handshake process) know which one is which and when to pass the correct file? – Andrei Manolache Mar 06 '21 at 00:12
  • 1
    The server just asks the client to identify itself. If this option is present during the SSL handshake the client will try to send the client identity. The client identity can be setup by creating the keymanager from a keystore which contains at least one keypair. A keypair is an object which has a public and private key. The client will mostly send the first entry within the keystore to the client. It will cent the public key. The server will receive this and check if it's trusting it. If this public key aka certificate is present in the truststore it will pass – Hakan54 Mar 06 '21 at 08:02