7

I am creating a simple android app that will be used in a closed local network. In this local network, a flask server is running which is configured to use a self-signed certificate via nginx proxying. The backend application of the server works fine using the self-signed certificate, I have verified this both using my browser and postman. (Obviously, I had to explicitly ask the browser to trust my certificate).

For days, I have been trying to find some definitive answer online on how to make my android app accept my certificate, but all the things I have tried have led me to a dead end. Sometimes the solutions where deprecated, and other times just too complicated for such a trivial thing.

The http requests are sent using Retrofit; as I understand, I must somehow configure my retrofit instance's http client to accept my certificate.

I have managed to use a client that accepts any certificate, but this is not what I want. Ideally, my certificate would be added to the "set" of certificates that are trusted by default by official CAs, so that the app can possibly send requests to outside resources as well.

So, given that the backend application is running on e.g. 192.168.1.10:443, how would I go about this?

Note: I have read the instructions given here https://developer.android.com/training/articles/security-config.html#TrustingAdditionalCas and have added

android:networkSecurityConfig="@xml/network_security_config"

to my manifest file, but I am getting the following error:

Hostname 192.168.1.10 not verified: certificate sha256/...../..... and continues to list the information of the certificate like common name etc.

Theo Stefou
  • 389
  • 2
  • 16

2 Answers2

8

Shlomi Katriel's answer was something I had already tried, but it led me to the solution indirectly. Please keep in mind that the only reason I was able to solve this was because I have root access to the server and can do with it as I please.

This answer was basically the key to solving the whole thing. I will post all the steps in case someone else needs this.

Step 1 Create your self-signed certificate. In my case, I used the openssl utility. It is very important to include the -addext flag. Here is an example taken directly from the answer I linked above:

openssl req \
-newkey rsa:2048 \
-nodes \
-x509 \
-days 36500 -nodes \
-addext "subjectAltName = IP.1:1.2.3.4" \
-keyout /etc/ssl/private/nginx-selfsigned2.key \
-out /etc/ssl/certs/nginx-selfsigned2.crt

Replace 1.2.3.4 with the local ip of your server. This will include the server's local ip in the subjectAltName property of the certificate. Without this, you will keep getting the error "Hostname ... not verified ..."

Using the certificate in a running nginx instance is a different subject which can be done easily and there are plenty of resources online.

Step 2 Open android studio and create a new android resource directory (if you don't already have it) under res named raw. Inside this directory, copy the contents of the .crt file that the command above produced.

Step 3 Create an android resource directory under res named xml. In there, create a new xml file which in my case I called network_security_config.xml. In there, I inserted the following:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="@raw/certificate"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

Instead of "certificate", use the filename of the certificate file you copied in step 2.

Step 4 Add android:networkSecurityConfig="@xml/network_security_config" to the application element in your AndroidManifest.xml

Theo Stefou
  • 389
  • 2
  • 16
  • 1
    Thanks a lot , I've been trying dozens of tweaks on this topic, your answer saved me. Android"s doc isn't very clear, one has to make sure to use "base-config" and not "domain-config". – Manolo de la Vega Jun 20 '22 at 10:32
  • 1
    My gods: THANK you Theo! I have been stuck on this for DAYS now! This approach works for Chrome, iOS, and Android, although, if you DO wanna use it for all of em, the -days parameter needs to be reduced (apple no longer supports anything over 398 days for certs issued after 8-31-2020; see: support.apple.com/HT211025). Finally, if you don't want the user to be prompted for details, append `-subj "/C=/ST=/L=/O=/OU=>/CN=` replacing each with whatever seed text you like. – NerdyDeeds Jan 31 '23 at 17:07
  • 1
    So, for anyone interested in a universally-cross-platform, pre-seeded, silent version of this WORKING Self-Signed generator: `openssl req -newkey rsa:2048 -nodes -x509 -days 365 -nodes -subj "/C=US/ST=California/L=San Jose/O=Silicon Chips/OU=Cool Ranch/CN=Silicon Chips n Dip Certificate" -addext "subjectAltName = IP.1:$(ipconfig getifaddr en0)" -keyout path/to/key.pem -out path/to/cert.pem` (where `$(ipconfig getifaddr en0)` will obtain your local IP address and authorize it specifically) – NerdyDeeds Jan 31 '23 at 17:46
3

Best solution

pull the strings to make the owner of the server use a certificate, which is signed by a custom root CA, then pin this certificate using networkSecurityConfig. In my opinion it should be a requirement that should prevent a feature from being released.

Alternative

I would create 2 http clients:

  1. One with breached trust manager that accepts all certificates (empty imolementation of chechServerTrusted(...)) - only for that specific local network request that expected to fail the SSL handshake.
  2. Normal one for the rest of the requests.

By doing so, you're exposing this request for man-in-a-middle attack in case an attacker got into the local network - both attacker and real server certificates are not trusted. They can also mimic the certificate metadata so you won't tell the difference. Only you know if your system can afford the risk (and how probable is the risk anyway).

More insights:

  • networkSecurityConfig is meant for well known certificates so you can hard code public keys, so I don't believe it can solve your case.
  • You cannot install certificate authority on device without the actual self signed certificate.

Update

According to Android Developers, you need the following configuration:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="@raw/my_custom_cas"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

They claim res/raw/my_custom_cas accepts all common formats:

Add the trusted CAs, in PEM or DER format, to res/raw/trusted_roots. Note that if using PEM format the file must contain only PEM data and no extra text. You can also provide multiple <certificates> elements instead of one.

Shlomi Katriel
  • 2,103
  • 1
  • 7
  • 20