Skip to content
Advertisement

Android app client Mutual TLS with java server

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;
    }

}

Advertisement

Answer

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();
    }
}
9 People found this is helpful
Advertisement