Android Security: SSL Pinning

Matthew Dolan
Jan 13, 2017 · 13 min read
Certificate chain for appmattus.com

Implementing SSL pinning

Which certificate should you pin against in the chain?

Certificate or public key pinning?

Handling compromise

Handling private key leaks

Where do I store the certificate/public key?

Code examples

Retrieving your public keys

#!/bin/bashcerts=`openssl s_client -servername $1 -host $1 -port 443 -showcerts </dev/null 2>/dev/null | sed -n '/Certificate chain/,/Server certificate/p'`rest=$certs
while [[ "$rest" =~ '-----BEGIN CERTIFICATE-----' ]]
do
cert="${rest%%-----END CERTIFICATE-----*}-----END CERTIFICATE-----"
rest=${rest#*-----END CERTIFICATE-----}
echo `echo "$cert" | grep 's:' | sed 's/.*s:\(.*\)/\1/'` echo "$cert" | openssl x509 -pubkey -noout |
openssl rsa -pubin -outform der 2>/dev/null |
openssl dgst -sha256 -binary | openssl enc -base64
done
user$ ./certs.sh www.appmattus.com
/CN=appmattus.com
4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=
/C=US/O=Let’s Encrypt/CN=Let’s Encrypt Authority X3
YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=

Pinning on Android N

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">appmattus.com</domain>
<pin-set>
<pin digest="SHA-256">4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=</pin>
<pin digest="SHA-256">YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</pin>
</pin-set>
</domain-config>
</network-security-config>

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

Pinning with Retrofit

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

Pinning with Picasso

Picasso picasso = new Picasso.Builder(getApplicationContext())
.downloader(new OkHttpDownloader(okHttpClient))
.build();
Picasso.setSingletonInstance(picasso);

Pinning with HttpUrlConnection

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

Pinning with Volley

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

Pinning with Apache HttpClient

SSLSocketFactory socketFactory = (SSLSocketFactory) client
.getConnectionManager()
.getSchemeRegistry()
.getScheme("https")
.getSocketFactory();

X509HostnameVerifier delegate = socketFactory.getHostnameVerifier();

X509HostnameVerifier pinningHostnameVerifier =
new PinningHostnameVerifier(delegate);

socketFactory.setHostnameVerifier(pinningHostnameVerifier);

Pinning in WebViews

Testing

mitmproxy

mitmproxy --add-upstream-certs-to-client-chain --insecure

Alternative tools

Conclusions

Matthew Dolan

Written by

Matt Dolan has been eating doughnuts and developing with Android since the dark days of v1.6.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade