Android Security SSL Pinning
Hi Everyone in this Android tutorial, I am sharing how to use Android security SSL Pinning. Core to SSL (Secure Socket Layer) is the X509 certificate, and trust in the chain of certificates that leads from your leaf certificate through an intermediate certificate authority (CA) to a root certificate authority. This chain is determined when the SSL connection is established. The First Few Milliseconds of an HTTPS Connection talks a little about the process of creating a secure connection.
Using SSL in the Android app is easy, however ensuring that the connection is actually secure is a different matter. A man-in-the-middle attack can be carried out using several methods including ARP cache poisoning and DNS spoofing.
Root certificates come pre-installed on Android devices with around 150 included in Android N. You can check what’s on your own device by going to Settings > Security > Trusted Credentials. There is an assumption that none of these root CAs or the 1000’s of intermediate CAs these root certificates trust will miss-issue leaf certificates for domain names they shouldn’t. If you don’t believe me read about the CAs DigiNotar, GlobalSign, and Comodo. In addition to all this, the user’s device could be compromised with a rogue certificate installed on it through social engineering.
SSL pinning also knew as Public Key Pinning is an attempt to solve these issues, ensuring that the certificate chain used is the one your app expects by checking a particular public key or certificate appears in the chain.
So let’s Start on Code Part of Android Security SSL Pinning.
Pinning on Android N
Table of Contents
If your minimum SDK is Android N (API 24) then the implementation couldn’t be simpler as Android has a new API in town the Network Security Configuration. Even better this configuration even works for WebViews with no additional effort on your part.
Through a simple entry in your AndroidManifest.xml file, you specify an XML configuration file that defines the pins you require. Of course, being XML based this isn’t useful if you want to dynamically specify your pins.
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">Codeplayon.com</domain>
<pin-set>
<pin digest="SHA-256">-----------------------------------------</pin>
<pin digest="SHA-256">-----------------------------------------</pin>
</pin-set>
</domain-config>
</network-security-config>
It is possible to configure an expiration date by using <pin-set expiration=“2017–02–28“> but it is worth noting you are then accepting insecure connections once that date has passed for users that don’t/can’t upgrade your app.
The Network Security Configuration also makes it easy if you need to support self-signed certificates or certificate authorities that are not trusted system root certificates.
Pinning with OkHttp
Implementation with OkHttp is pretty straightforward with the CertificatePinner class.
CertificatePinner certPinner = new CertificatePinner.Builder()
.add("appmattus.com",
"sha256/4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=")
.build();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.certificatePinner(certPinner)
.build();
OkHttp has offered certificate pinning since OkHttp 2.1. Unfortunately, early versions suffer from a Vulnerability in OkHttp’s Certificate Pinner so ensure you use at least OkHttp 3.2.0 or OkHttp 2.7.5.
Pinning with Retrofit
With Retrofit being built on top of OkHttp, configuring it for pinning is as simple as setting up an OkHttpClient as shown above and supplying that to your Retrofit.Builder.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://appmattus.com")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
Pinning with Picasso
Picasso, as with Retrofit, is just a matter of configuring the downloader.
If you use OkHttp then you just provide the configured OkHttpClient. Currently, Picasso 2 doesn’t support OkHttp 3 out of the box so you may need to use the Picasso 2 OkHttp3 Downloader.
Picasso picasso = new Picasso.Builder(getApplicationContext())
.downloader(new OkHttpDownloader(okHttpClient))
.build();
Picasso.setSingletonInstance(picasso);
The implementation with the UrlConnectionDownloader is slightly more work but you can implement a similar technique as shown for Volley by overloading the openConnection method of the downloader and overriding the HostnameVerifier.
Pinning with HttpUrlConnection
Firstly if you are still using HttpUrlConnection consider upgrading to OkHttp. The version built into Android, naturally, is a fixed version so you won’t get any security updates or bug fixes.
In the Android training document, Security with HTTPS and SSL, the implementation suggested is based off pinning your certificates through a custom TrustManager and SSLSocketFactory. However as with the other APIs presented here I will show you how to pin against the SPKI instead.
Instead the certificates must be sanitised using X509TrustManagerExtensions introduced in API 17 to return a trusted chain back to the devices trust store that is checked instead. Below is a modified version doing just this along with base 64 encoding for the pins to match the other APIs presented here.
private void validatePinning( X509TrustManagerExtensions trustManagerExt, HttpsURLConnection conn, Set<String> validPins) throws SSLException { String certChainMsg = ""; try { MessageDigest md = MessageDigest.getInstance("SHA-256"); List<X509Certificate> trustedChain = trustedChain(trustManagerExt, conn); for (X509Certificate cert : trustedChain) { byte[] publicKey = cert.getPublicKey().getEncoded(); md.update(publicKey, 0, publicKey.length); String pin = Base64.encodeToString(md.digest(), Base64.NO_WRAP); certChainMsg += " sha256/" + pin + " : " + cert.getSubjectDN().toString() + "\n"; if (validPins.contains(pin)) { return; } } } catch (NoSuchAlgorithmException e) { throw new SSLException(e); } throw new SSLPeerUnverifiedException("Certificate pinning " + "failure\n Peer certificate chain:\n" + certChainMsg); }private List<X509Certificate> trustedChain( X509TrustManagerExtensions trustManagerExt, HttpsURLConnection conn) throws SSLException { Certificate[] serverCerts = conn.getServerCertificates(); X509Certificate[] untrustedCerts = Arrays.copyOf(serverCerts, serverCerts.length, X509Certificate[].class); String host = conn.getURL().getHost(); try { return trustManagerExt.checkServerTrusted(untrustedCerts, "RSA", host); } catch (CertificateException e) { throw new SSLException(e); } }
This would then be called as follows:
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init((KeyStore) null); // Find first X509TrustManager in the TrustManagerFactory X509TrustManager x509TrustManager = null; for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { if (trustManager instanceof X509TrustManager) { x509TrustManager = (X509TrustManager) trustManager; break; } } X509TrustManagerExtensions trustManagerExt = new X509TrustManagerExtensions(x509TrustManager);...URL url = new URL("https://www.appmattus.com/"); HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); urlConnection.connect();Set<String> validPins = Collections.singleton ("4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8="); validatePinning(trustManagerExt, urlConnection, validPins);
The call to urlConnection.connect() performs the SSL handshake however does not transmit any data until you call urlConnection.getInputStream().
Pinning with Volley
The usual suggested route for pinning with Volley is to pin against certificates as shown in Public Key Pinning with Volley Library. This involves creating a custom SSLSocketFactory which adds in an inherent risk of introducing other security vulnerabilities.
Overriding the HostnameVerifier can be achieved with the following:
RequestQueue requestQueue = Volley.newRequestQueue(appContext, new HurlStack() { @Override protected HttpURLConnection createConnection(URL url) throws IOException { HttpURLConnection connection = super.createConnection(url); if (connection instanceof HttpsURLConnection) { HostnameVerifier delegate = urlConnection.getHostnameVerifier(); HostnameVerifier pinningVerifier = new PinningHostnameVerifier(delegate); urlConnection.setHostnameVerifier(pinningVerifier); } return connection; } });...public static class PinningHostnameVerifier implements HostnameVerifier { private final HostnameVerifier delegate; private PinningHostnameVerifier(HostnameVerifier delegate) { this.delegate = delegate; } @Override public boolean verify(String host, SSLSession sslSession) { if (delegate.verify(host, sslSession)) { try { validatePinning(sslSession.getPeerCertificates(), host, validPins); return true; } catch (SSLException e) { throw new RuntimeException(e); } } return false; } }
The above can be used on a plain implementation of HttpUrlConnection also if you don’t want to set up.
Pinning with Apache HttpClient
As with HttpUrlConnection you really shouldn’t be using HttpClient anymore especially with the Apache HTTP Client Removal in Android 6.0. Android was frozen at Apache HttpClient v4.0 since API 1.
The same technique shown for Volley also works for Apache HttpClient with some tweaks to the PinningHostnameVerifier:
SSLSocketFactory socketFactory = (SSLSocketFactory) client
.getConnectionManager()
.getSchemeRegistry()
.getScheme("https")
.getSocketFactory();
X509HostnameVerifier delegate = socketFactory.getHostnameVerifier();
X509HostnameVerifier pinningHostnameVerifier =
new PinningHostnameVerifier(delegate);
socketFactory.setHostnameVerifier(pinningHostnameVerifier);