Using self-signed certificates when developing Android applications
Tags: android, ssl, nginx
Published: 2022-09-05 17:36:23

I recently started learning how to develop mobile applications on Android. I've been doing mostly backend work before I joined RiseUp, but in the past 2 years I've learned to love web development, and mobile development seemed like a natural next step.
 
The first three weeks were awesome! Google created an amazing set of hands-on tutorials you can do to get started. But then I wanted to test my skills with real-world scenarios, and decided to implement our login flow in Android. The flow itself is not that interesting so I won't go into details there. What's important is that we use Nginx in our local (dev) environment to funnel all requests to our backend services. Nginx is responsible not only for routing API calls to the right service, but also for SSL termination. This allows our backend services to avoid meddling with SSL termination on their own.
 
Our local Nginx listens to port 9090, so I thought something like this should work pretty well*:

```kotlin
val loginApiService = Retrofit.Builder()
.baseUrl("https://localhost:9090")
.create(LoginApiService::class.java)                                
        
val token = loginApiService.authenticate(..)
```

_*I simplified the code to make it more concise. _

But when I ran the application, I got the following error:

```
java.net.ConnectException: Failed to connect to localhost/127.0.0.1:9090
```

Say what??? Well… apparently localhost on mobile devices is the device itself. That actually makes sense.

## Problem no.1 - forwarding calls to localhost to my laptop

What I really wanted is that any calls to `localhost` should go to my laptop and not the device. `adb` to the rescue!

Android Debug Bridge (adb) is a CLI tool that helps you communicate with your mobile devices (virtual or physical). You can easily install in using brew:

```shell
brew install android-platform-tools
```

It has a cool command called `reverse` that allows you to forward any calls from any device to your laptop. This is how it's done:

```shell
adb reverse tcp:9090 tcp:9090
```

This basically tells the device that every TCP call on port 9090 should be forwarded to port 9090 on your laptop. 

Problem solved! So I ran my application again and this time I got this:

```
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
```

## Problem no.2 - telling your device to trust a self-signed certificate

Our local Nginx uses a self-signed SSL certificate. It was created by an `openssl` command. This worked great for web development and we didn't have any real issues with it so far.  

With Android (and iOS) devices, you need to tell your device to trust that certificate. There are two steps here:

### Step 1 - Install a CA certificate on the device

First you need to tell your device to trust the root CA of your self signed certificate. Unfortunately, when using `openssl` to generate a certificate, you don't really have a root CA. So first I needed to change that. 

I searched the Internet and found a great tool called [mkcert](https://github.com/FiloSottile/mkcert). `mkcert` is a simple tool for making locally-trusted development certificates (I shamelessly copied from their README. Sue me).

When using `mkcert` you also get a PEM encoded CA Certificate which is exactly what our Android device is expecting. To find that certificate you can run:

```shell
 echo "$(mkcert -CAROOT)/rootCA.pem"
```

Copy the certificate to your device. Now on your device open the _CA Certificate _ settings screen and follow the instructions to install the certificate you copied. 

### Step 2 - Tell your application to trust user added CAs

By default, Android applications will not trust user added CAs. To make it trust your certificate you need to create a new file under `res/xml` called `network_security_config` with the following content:

```xml
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <!-- Trust preinstalled CAs -->
            <certificates src="system" />
            <!-- Additionally trust user added CAs -->
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>
```

Notice that we're trusting user added certificates only in debug mode. Released applications should work against properly signed SSL certificates. 

Now add the configuration file to your AndroidManifest file:

```xml
<application   
...
android:networkSecurityConfig="@xml/network_security_config"
...
/>
```

That's it! 🎉

P.S. This works well for both physical and virtual devices.

To summarize, in order to enable your Android application to connect with an HTTPs server on your laptop with a self-signed certificate, you should:
1. Use `adb` to forward all calls to localhost
2. Generate a root CA certificate and install in on your device
3. Tell your application to trust user added CAs