Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android SSLSockets using self-signed certificates

This is a problem I have been really struggling to solve recently, largely because I felt that there was almost too much information on the Internet regarding this issue that did not help. So since I have just found a solution that works for me, I decided that I would post the problem and solution here, in the hopes that I can make the Internet a slightly better place for those who come after me! (Hopefully, this is not going to contribute to the "unhelpful" content!)

I have an Android application that I've been developing. Until recently, I've just been using ServerSockets and Sockets to communicate between my app and my server. However, the communications do need to be secure, so I've been trying to convert these to SSLServerSockets and SSLSockets, which turns out to be a hell of a lot harder than I was expecting.

Seeing as it's just a prototype, there is no (security) harm in just using self-signed certificates, which is what I'm doing. As you've probably guessed, this is where the problems come in. This is what I had done, and the problems I encountered.

I generated the file "mykeystore.jks" with the following command:

keytool -genkey -alias serverAlias -keyalg RSA -keypass MY_PASSWORD -storepass MY_PASSWORD -keystore mykeystore.jks

This is the server code:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;

/**
 * Server
 */
public class simplesslserver {
    // Global variables
    private static SSLServerSocketFactory ssf;
    private static SSLServerSocket ss;
    private static final int port = 8081;
    private static String address;

    /**
    * Main: Starts the server and waits for clients to connect.
    * Each client is given its own thread.
    * @param args
    */
    public static void main(String[] args) {
        try {
            // System properties
            System.setProperty("javax.net.ssl.keyStore","mykeystore.jks");
            System.setProperty("javax.net.ssl.keyStorePassword","MY_PASSWORD");

            // Start server
            ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
            ss = (SSLServerSocket) ssf.createServerSocket(port);
            address = InetAddress.getLocalHost().toString();
            System.out.println("Server started at "+address+" on port "+port+"\n");

            // Wait for messages
            while (true) {
                SSLSocket connected = (SSLSocket) ss.accept();
                new clientThread(connected).start();
            }                
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
    * Client thread.
    */
    private static class clientThread extends Thread {
        // Variables
        private SSLSocket cs;
        private InputStreamReader isr;
        private OutputStreamWriter osw;
        private BufferedReader br;
        private BufferedWriter bw;

        /**
        * Constructor: Initialises client socket.
        * @param clientSocket The socket connected to the client.
        */
        public clientThread(SSLSocket clientSocket) {
            cs = clientSocket;
        }

        /**
        * Starts the thread.
        */
        public void run() {
            try {
                // Initialise streams
                isr = new InputStreamReader(cs.getInputStream());
                br = new BufferedReader(isr);
                osw = new OutputStreamWriter(cs.getOutputStream());
                bw = new BufferedWriter(osw);

                // Get request from client
                String tmp = br.readLine();
                System.out.println("received: "+tmp);

                // Send response to client
                String resp = "You said '"+tmp+"'!";
                bw.write(resp);           
                bw.newLine();
                bw.flush();
                System.out.println("response: "+resp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

This is an extract from the Android application (client):

String message = "Hello World";
try{
    // Create SSLSocketFactory
    SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();

    // Create socket using SSLSocketFactory
    SSLSocket client = (SSLSocket) factory.createSocket("SERVER_IP_ADDRESS", 8081);

    // Print system information
    System.out.println("Connected to server " + client.getInetAddress() + ": " + client.getPort());

    // Writer and Reader
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
    BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));

    // Send request to server
    System.out.println("Sending request: "+message);
    writer.write(message);
    writer.newLine();
    writer.flush();

    // Receive response from server
    String response = reader.readLine();
    System.out.println("Received from the Server: "+response);

    // Close connection
    client.close();

    return response;
} catch(Exception e) {
    e.printStackTrace();
}
return "Something went wrong...";

When I ran the code, it didn't work, and this is the output I was getting.

Client output:

Connected to server /SERVER_IP_ADDRESS: 8081
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
... stack trace ...

Server output:

received: null
java.net.SocketException: Connection closed by remote host

As the certificate is self-signed, the app does not trust it. I had a look around Google, and the general consesus is that I need to create an SSLContext (in the client) which is based on a custom TrustManager that accepts this self-signed certificate. Simple enough, I thought. Over the next week, I tried more methods of solving this issue than I can possibly remember, to no avail. I now refer you back to my original statement: there is far too much incomplete information out there, which made figuring out the solution a lot harder than it should have been.

The only working solution I found, was the make a TrustManager that accepts ALL certificates.

private static class AcceptAllTrustManager implements X509TrustManager {
    @Override
    public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}

    @Override
    public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}

    @Override
    public X509Certificate[] getAcceptedIssuers() { return null; }
}

which can be used like this

SSLContext sslctx = SSLContext.getInstance("TLS");
sslctx.init(new KeyManager[0], new TrustManager[] {new AcceptAllTrustManager()}, new SecureRandom());
SSLSocketFactory factory = sslctx.getSocketFactory();
SSLSocket client = (SSLSocket) factory.createSocket("IP_ADDRESS", 8081);

And gives the nice and happy output with no exceptions!

Connected to server /SERVER_IP_ADDRESS: 8081
Sending request: Hello World
Received from the Server: You said 'Hello World'!

However, this is not a good idea, because the app will still be unsecure, thanks to potential man-in-the-middle attacks.

So I was stuck. How could I get the app to trust my own self-signed certificate, but not just any and every certificate out there?

like image 847
BSnapZ Avatar asked Jul 01 '14 23:07

BSnapZ


1 Answers

I found a way which apparently should create an SSLSocket based on a SSLContext which is based on a TrustManager which trusts mykeystore. The trick is, we need to load the keystore in to a custom trust manager, such that the SSLSocket is based on an SSLContext which trusts my own self-signed certificate. This is done by loading the keystore into the trust manager.

The code I found to do this was as follows:

KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(getResources().openRawResource(R.raw.mykeystore), "MY_PASSWORD".toCharArray());

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

SSLContext sslctx = SSLContext.getInstance("TLS");
sslctx.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

SSLSocketFactory factory = sslctx.getSocketFactory();

Which promptly failed.

java.security.KeyStoreException: java.security.NoSuchAlgorithmException: KeyStore JKS implementation not found

Apparently, Android doesn't support JKS. It has to be in the BKS format.

So I found a way to convert from JKS to BKS by running the following command:

keytool -importkeystore -srckeystore mykeystore.jks -destkeystore mykeystore.bks -srcstoretype JKS -deststoretype BKS -srcstorepass MY_PASSWORD -deststorepass MY_PASSWORD -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov-jdk15on-150.jar

Now, I have a file called mykeystore.bks which is exactly the same as mykeystore.jks, except in BKS format (which is the only format Android accepts).

Using "mykeystore.bks" with my Android app, and "mykeystore.jks" with my server, it works!

Client output:

Connected to server /SERVER_IP_ADDRESS: 8081
Sending request: Hello World
Received from the Server: You said 'Hello World'!

Server output:

received: Hello World
response: You said 'Hello World'!

We're done! The SSLServerSocket / SSLSocket connection between my Android application and my server is now working with my self-signed certificate.

Here is the final code from within my Android application:

String message = "Hello World";
try{
    // Load the server keystore
    KeyStore keyStore = KeyStore.getInstance("BKS");
    keyStore.load(ctx.getResources().openRawResource(R.raw.mykeystore), "MY_PASSWORD".toCharArray());

    // Create a custom trust manager that accepts the server self-signed certificate
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(keyStore);

    // Create the SSLContext for the SSLSocket to use
    SSLContext sslctx = SSLContext.getInstance("TLS");
    sslctx.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

    // Create SSLSocketFactory
    SSLSocketFactory factory = sslctx.getSocketFactory();

    // Create socket using SSLSocketFactory
    SSLSocket client = (SSLSocket) factory.createSocket("SERVER_IP_ADDRESS", 8081);

    // Print system information
    System.out.println("Connected to server " + client.getInetAddress() + ": " + client.getPort());

    // Writer and Reader
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
    BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));

    // Send request to server
    System.out.println("Sending request: "+message);
    writer.write(message);
    writer.newLine();
    writer.flush();

    // Receive response from server
    String response = reader.readLine();
    System.out.println("Received from the Server: "+response);

    // Close connection
    client.close();

    return response;
} catch(Exception e) {
    e.printStackTrace();
}
return "Something went wrong...";

(Note that the server code has not changed, and the full server code is in the original question.)

like image 64
BSnapZ Avatar answered Sep 27 '22 19:09

BSnapZ