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 DigiNotarGlobalSign, 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

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

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

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://appmattus.com")
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build();

Pinning with Picasso

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

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

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

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);

 

You may also like...