Working with Self Signed Certificates (Certificate Pinning) in Android Applications with Xamarin.Forms

Working with Self Signed Certificates (Certificate Pinning) in Android Applications with Xamarin.Forms

Next up in the sequence of posts talking about app security is looking at working with self-signed certificates in an Android application. Previous posts in this sequence are:

Accessing ASP.NET Core API hosted on Kestrel over Https from iOS Simulator, Android Emulator and UWP Applications.
Publishing ASP.NET Core 3 Web API to Azure App Service with Http/2
Xamarin and the HttpClient For iOS, Android and Windows
Working with Self Signed Certificates (Certificate Pinning) in Windows (UWP) Application with Xamarin.Forms
Working with Self Signed Certificates (Certificate Pinning) in iOS Application with Xamarin.Forms

Similar to the post on working with self signed certificates on iOS, in this post we’re going to briefly talk about non-secure services, followed by looking at how to trust self signed certificates by adding them to the Android bundle and then lastly look at intercepting the certificate validation process when making service calls.

One resource that is particularly useful is the network security documentation provided for Android developers which lists the various elements of the network security configuration file that will be used and referenced in this post.

Non-Secure (i.e. Http) Services

By default, Android, like iOS doesn’t allow applications to connect to non-secure services. This means that connecting to http://192.168.1.107 or http://192.168.1.107.xip.io will not work out of the box and you’ll see an error similar to the following, if you attempt to connect to a non-secure service:

Java.IO.IOException: Cleartext HTTP traffic to 192.168.1.107 not permitted occurred

It’s important to note that this behaviour has changed between Android 8.1 and Android 9 – prior to Android 9 there was no default restrictions on calling non-secure, or plain text, services.

Accessing Http/Plain text services can be enabled again by adjusting the network security configuration for the application. To do this we need to add an xml file to the Resources/xml folder which we’ve called network_security_config.xml with the following contents:

<?xml version=”1.0″ encoding=”utf-8″?>
< network-security-config>
     <base-config cleartextTrafficPermitted=”true” />
< /network-security-config>

Note: this adjusts the network security for the application both in debug and in production. If you want to access plain text services during debugging only, you should change the base-config open and close tags to debug-overrides (everything else remaining the same). Whether the application is running in debugging mode is controlled by the Application (Debuggable = true/false) assembly attribute, which in this case we have on the MvvmCross setup file in the Android project.

#if DEBUG
[assembly: Application(Debuggable = true)]
#else
[assembly: Application(Debuggable = false)]
#endif

We also need to add a reference to the network_security_config.xml file into the application manifest file (AndroidManifest.xml) by adding the networkSecurityConfig attribute.

<?xml version=”1.0″ encoding=”utf-8″?>
< manifest http://schemas.android.com/apk/res/android%22″>http://schemas.android.com/apk/res/android”
           android_versionCode=”1″
           android_versionName=”1.0″
           package=”com.refitmvvmcross”
           android_installLocation=”auto”>
     <uses-sdk android_minSdkVersion=”19″
               android_targetSdkVersion=”28″ />
     <application
         android_allowBackup=”true”
         android_theme=”@style/AppTheme”
         android_label=”@string/app_name”
         android_icon=”@mipmap/ic_launcher”
         android_roundIcon=”@mipmap/ic_launcher_round”
         android:networkSecurityConfig=”@xml/network_security_config”
         android_resizeableActivity=”true”>
         <meta-data
             android_name=”android.max_aspect”
             android_value=”2.1″ />
     </application>
< /manifest>

Note that the network_security_config.xml filename can be changed so long as the name matches the networkSecurityConfig attribute value in the AndroidManifest.xml file. Now if you run the application it will connect to the Http/Plain text endpoint.

image

In addition to enabling/disabling Http/Plain text services across the entire application, it can also be controlled on a per-endpoint basis. See the documentation on the Network Security Configuration for more information.

Switching the endpoint to a Https endpoint with a self signed certificate (eg https://192.168.1.107:5001) we see the following error being thrown

Javax.Net.Ssl.SSLHandshakeException: <Timeout exceeded getting exception details> occurred

Which of course is completely meaningless, except that we do know it’s related to establishing the SSL connection. If we look to the Output window we can find more information about the exception. Unfortunately when an Android app crashes there is a spew of mostly irrelevant output that’s dumped into the window (I’m sure it’s useful in a lot of cases but in this case it is irrelevant information that makes it hard to find the important information). The best way to find more information is to search for the error that was thrown in the debug session (i.e. Javax.Net.Ssl.SSLHandshakeException and then look at the next 10-15 lines of information. In this case we see

05-04 08:31:21.756 I/MonoDroid( 5948): UNHANDLED EXCEPTION:
05-04 08:31:21.768 I/MonoDroid( 5948): Javax.Net.Ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. —> Java.Security.Cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. —> Java.Security.Cert.CertPathValidatorException: Trust anchor for certification path not found.

This points to the fact that the application hasn’t been able to verify the certificate path for the certificate returned by the service – no surprises there because it’s a self signed certificate and we’ve done nothing to tell Android or the application about the certificate.

Trusting Self Signed Certificates

In this section we’re going to look at using the public key from the self signed certificate in order to allow the application to connect to the secure endpoint, without having to write code to intercept the certificate validation. We’ll cover two different ways but they amount to the same thing – having the public key of the certificate authority available for the application so that it can verify the certificate path of the certificate returned by the service.

Installing to User Certificate Store

The first option is to install the public key of the certificate authority onto the Android device. Android maintains two different certificate stores: system and user. We’re going to be adding the certificate to the user store (adding to the System store requires root access and can be done using ADB as shown in various posts such as this one). The public key that we need to use is the public key of the certificate authority. As discussed in previous posts, we’ve used mkcert to generate our self signed certificate, so the public key of the certificate authority used by mkcert is available at C:Users[username]AppDataLocalmkcertrootCA.pem

The first challenge is to get the certificate onto the device, which I typically find easiest via a download link. Here I’ve added the public key to dropbox and have opened the link in Chrome on the Android device:

image image

After downloading the pem file, clicking on the file in the Downloads list does nothing. You actually need to go to Settings / Security & location / Encryption & credentials / Install from SD card

image
Note that the settings items may be named slightly differently. For example Install from SD card might be Install from storage. The quickest way to get there is to search for certificate and go to the item that says something like Install from SD card or storage.

image

When you click on the Install from SD card/storage settings item you’ll be presented with what amounts to a file picker, typically showing Recent files. From the burger menu you can select Downloads which will reveal the pem file you’ve just downloaded. However, this item will be disabled, presumably because of some security related to downloaded items. My initial thought was that I’d downloaded the wrong certificate format but in actual fact if you go back to the burger menu and browse the contents of the device (see third image above) and click on Download, you’ll see the same rootCA.pem file but this time it’s not dimmed out and you can click on it.

image

When you click on the rootCA.pem, if you have a PIN setup on the device you’ll most likely be asked to enter your pin. After entering your PIN you’ll be asked to enter a Name for the certificate and what the certificate will be used for. The Name isn’t particularly useful since it doesn’t show up in either the Trusted credentials screen (second image, showing the added certificate), nor the Security certificate popup that appears if you click on the certificate for more information. Since we want the certificate to be used by the app we leave the default use which should be VPN and apps. At this point if you open the service endpoint in the browser you should see that the certificate is trusted.

image

Now that we’ve installed the certificate, there’s just one change we need to make to the Android application itself to make use of the certificate. By default Android applications will only use certificates in the system store. However, you can adjust the application to make use of the user certificate store. To do this we need to add a trust-anchors and certificates elements to the network_security_config.xml with the following contents:

<?xml version=”1.0″ encoding=”utf-8″?>
<network-security-config>
     <debug-overrides>
         <trust-anchors>
            <certificates src=”user” />
         </trust-anchors>

     </debug-overrides>
</network-security-config>

In this case we’ve setup the certificates element to only be used when running application in debug mode (ie by using the debug-overrides element). Running the application now will return data from the Https endpoint.

image

The issue with this approach is that the public key for the certificate authority has to be installed on the device in order for the application to work. Since the public key appears in the user store, it means that at any point the user could remove it. Whilst it is unlikely that the user will go in and specifically delete the individual item, it is possible that the user decides to clear our all cached credentials – this has the unfortunate side effect of clearing out the user store, thus preventing the application from running. This said, if you’re only connecting to endpoints for the purpose of development or debugging, and that in production you use certificates from a well known certificate authority, this option may be sufficient and minimises the changes required to the application.

Adding to Application Package

The second option is to add the public key of the certificate authority to the application package itself. We’ll use the DER format for the public key which we generated from the mkcert public key file in the previous post. The kestrel.der file is added to the Resources / raw folder with Build Action set to AndroidResource (if the Custom Tool isn’t set, you can updated it also).

image 

Now that the public key has been added to the package, it’s important that there is a linkage between the network security configuration file and the public key. This is done by adding the trust-anchors and certificates elements to the network_security_config.xml file as follows:

<?xml version=”1.0″ encoding=”utf-8″?>
<network-security-config>
     <base-config>
         <trust-anchors>
             <certificates src=”@raw/kestrel” />
         </trust-anchors>
    </base-config>
</network-security-config>

(again if you only want to use the certificates in debugging, exchange the base-config with debug-overrides)

That’s all you need to do to be able to access a service that uses a self-signed certificate.

Validating Server Certificates (i.e. Certificate Pinning)

The other alternative to working with self signed certificates is to override the certificate validation that goes on as part of each service request. On Android this can be done by overriding the ConfigureCustomSSLSocketFactory and GetSSLHostnameVerifier methods on the AndroidClientHandler. For example, here’s a CustomerAndroidClientHandler that inherits from the AndroidClientHandler and overrides these methods:

public class CustomAndroidClientHandler : AndroidClientHandler
{
     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
         request.Version = new System.Version(2, 0);
         return await base.SendAsync(request, cancellationToken);
     }


    protected override SSLSocketFactory ConfigureCustomSSLSocketFactory(HttpsURLConnection connection)
     {
         return SSLCertificateSocketFactory.GetInsecure(0, null);
     }


    protected override IHostnameVerifier GetSSLHostnameVerifier(HttpsURLConnection connection)
     {
         return new BypassHostnameVerifier();
     }
}


internal class BypassHostnameVerifier : Java.Lang.Object, IHostnameVerifier
{
     public bool Verify(string hostname, ISSLSession session)
     {
         return true;
    }
}

We’ve also included the class BypassHostnameVerifier, which is a basic implementation of the IHostnameVerifier that needs to be returned from the GetSSLHostnameVerifier method. In the ConfigureCustomSSLSocketFactory method we’re returning a factory generated by the GetInsecure method, which according to the method documentation “Returns a new instance of a socket factory with all SSL security checks disabled” and then goes on to provide a warning “Warning: Sockets created using this factory are vulnerable to man-in-the-middle attacks!” I would like at this point to reiterate this warning – by providing your own handling of the SSL checks, you are effectively taking ownership and responsibility of making sure your application isn’t being hacked. As such I would recommend only overriding these methods only when you need to connect to a service that uses a self signed certificate and that you put more validation in the Verify method to ensure only the self signed certificate that you trust is being returned in the service call (ie check for man-in-the-middle attacks).

Note that if you’re just interested in certificate pinning (ie ensuring that your application is connecting to a server that is returning a known certificate) you can simply override the GetSSLHostnameVerifier method. This will leave the default SSL verification/security in place but give you the opportunity to validate that the certificate being returned is what you expect.

Http2 Handling on Android

The bad news is that Http2 combined with self signed certificates doesn’t play nicely with the out of the box AndroidClientHandler, which is the default and preferred handler on Android. When you attempt to connect to a service that is Http2 only and it uses a self signed certificate (ie you’ve had to either install or add the certificate to the application package as above), you’ll probably see an error similar to:

Javax.Net.Ssl.SSLHandshakeException: Connection closed by peer occurred

Unfortunately there’s no simple solution without importing another library. Luckily, the ModerHttpClient library has the code necessary to do this and has been updated slightly in the modernhttpclient-updated nuget package. Please do not include the nuget package in your application as neither the original or the updated package are being actively maintained. Instead, copy the source code that you need (in this case the NativeClientHandler) and take ownership of maintaining it within your application logic.

To round this out, here’s an example that overrides the NativeMessageHandler to set the Http version to 2.

public class CustomNativeClientHandler : NativeMessageHandler
{
     protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
         request.Version = new System.Version(2, 0);
         return await base.SendAsync(request, cancellationToken);
     }
}

And when an instance of the CustomNativeClientHandler is created, the EnableUntrustedCertificates property is set to true – this effectively disables any of the SSL checking, so again opens up the risk of man-in-the-middle attacks

Mvx.IoCProvider.LazyConstructAndRegisterSingleton<HttpMessageHandler, IServiceOptions>(options =>
{
     return new CustomNativeClientHandler
     {
         AutomaticDecompression = options.Compression,
         EnableUntrustedCertificates = true
     };
});

And when we run this, we can see that the Http protocol used is Http/2.

image

In this post we’ve touched on using both self signed certificates, as well as certificate pinning. This is not an easy topic but important for application developers to be across. If you come across issues with your application security, feel free to reach out on twitter or via Built to Roam’s contact page.

———-

Nick Randolph @thenickrandolph

Built to Roam on building cross-platform applications

———-

Leave a Reply

Your email address will not be published. Required fields are marked *