Securing your mobile application with certificate pinning is considered as best practice in mobile development to secure the communication between your app and its backend servers. It's a checkpoint in every security assessment and usually advised to implement if not done so. Certificate pinning was originally introduced to detect compromised Certificate Authorities (CAs), but it has become a proven method to defend against man-in-the-middle (MiTM) attacks, secure HTTPS traffic and verify that users using trusted certificates.
In this article I will show you how to embed certificate pinning into your mobile app.
Introduction
The most common form of transferring data between app and server backend is over SSL/TLS. Switching to "https" will enable that encryption, but it is useless if the communicating parties cannot validate the identity of their peers. The problem of identity and trust has been attempted to be solved with the chain-of-trust mechanism: in order for a certificate to be trusted, it has to be traceable back to the trust root, which is also named as Certificate Authority (CA). If an attacker could generate a fraudulent certificate that pass this validation, he could decrypt, read and alter the traffic, e.g. banking details or sensitive private information.
The most famous example for this scenario is the DigiNotar - a former Dutch CA - hack in 2011, were attackers created fake wildcard certificates for multiple well-known domains, like google.com, yahoo.com and others. This breach was detected later, because Google had pinned their certificates into the Chrome browser.
Another example was reported in 2017, where a major CA had issued a number of certificates without proper validation.
Certificate pinning eliminates the dependency on the chain-of-trust and defines a legitimate encryption key. It ensures that the mobile app ignores its store of trusted certificates on the devices and only relies on itself.
How to implement certificate pinning?
First, you need to decide, which certificate do you want to pin. There are three types of certificates for an app to pin:
- The CA (root) certificate
- An intermediate CA certificate
- The server (leaf) certificate
Pining the server certificate is the best option and the recommended choice. It ensures that you have 100 percent guarantee that it is your certificate to be used for communication between app and server. On the flip side, it requires carefully planned rotation when a certificate is going to expire.
To pin the intermediate or even the CA's root cert, will avoid that hassle. However, to pin these certificates would allow an attacker to hijack the traffic if he could create a certificate from the same CA. In combination with a lack of host name validation, this weakens the connection security significantly.
Android
There are three ways to implement certificate pinning on Android:
- TrustManager
- OkHttp and CertificatePinner
- Network Security Configuration (NSC)
TrustManager
To use the TrustManager is a low-level and complex approach that requires multiple steps.
- Add you certificate file to
/res/raw
- Load that certificate into your KeyStore
InputStream inputStream = getResources().openRawResource(R.raw.my_cert)
KeyStore keyStore = KeyStore.getInstance(“AndroidKeyStore”);
keyStore.load(inpuStream, null);
- Create a TrustManagerFactory that trust that KeyStore
String trustManagerAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerAlgorithm);
trustManagerFactory.init(keyStore)
- Create an SSLContext with that TrustMananger
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
- Assign the SocketFactory to the URLConnection
URL url = new URL("https://yourdomain.tld/");
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory()
TrustManager is also used for self-signed certificates, where the corresponding
CA is not trusted by the device.
OkHttp and CertificatePinner
OkHttp is a popular HTTP client for Java and Android. It comes with a class that makes certificate pinning really simple: CertificatePinner. What is actually pinned is not the certificate itself, but the SHA256 hash of the public key of the certificate. This is easier to manage because of its size and it allows to add the fingerprints for backup or renewed certificates without exposing those certificates too early.
String hostname = "yourhost.tld";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
The hostname supports wildcard pattern, e.g. *.yourhost.tld. Please note that the asterisk is only allowed as the left-most label and must be the only character there.
CertificatePinner cannot be used with self-signed certificates if that certificate is not accepted by the TrustManager.
Where do you get the SHA256 hash from?
The SHA256 hash is the fingerprint of the public key of the certificate to pin. There are multiple ways to extract it:
- Use a deliberately false fingerprint and extract the correct one from the output of the exception. The code above will fail with
javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha256/IAEQVIalDTaJXtBfrTYjle+QqgRABR2FGQLebGybwtw=: CN=*.ice.ibmcloud.com,O=International Business Machines Corporation,L=Armonk,ST=New York,C=US
sha256/5kJvNEMw0KjrCAu7eXY5HZdvyCS13BbA0VJG1RSP91w=: CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US
sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=: CN=DigiCert Global Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US
Pinned certificates for verify.ice.ibmcloud.com:
sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Copy and paste the public key hash for the server certificate from the exception into the certificate pinner:
String hostname = "*.ice.ibmcloud.com ";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/IAEQVIalDTaJXtBfrTYjle+QqgRABR2FGQLebGybwtw=")
.build();x
- Extract it from the downloaded certificate: download the certificate in the crt format and execute the following command to extract it:
openssl x509 -in <your_cert_file>.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
- Use SSL Labs to run aserver test. Part of the report are the SHA256 hashes of the certificates.
Network Security Configuration
Network Security Configuration is supported since Android 7.0. It allows you to declare network security settings in XML files. The following code demonstrates how to use it for certificate pinning:
- Declare the usage in the application tag in your manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config" ... > ...
</application>
</manifest>
- Create that Network Security Network configuration file
res/xml/network_security_config.xml
- Add security settings to the configuration file
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">ice.ibmcloud.com</domain>
<pin-set>
<pin digest="SHA-256">IAEQVIalDTaJXtBfrTYjle+QqgRABR2FGQLebGybwtw=</pin>
<pin digest="SHA-256">r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E =</pin>
</pin-set>
</domain-config>
</network-security-config>
iOS
In iOS (Swift), you can store the certificate and compare it with the one from the server in the URLSessionDelegate method:
func urlSession(session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust,
SecTrustEvaluateWithError(serverTrust, nil) == errSecSuccess,
let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else
{
reject (with: completionHandler)
return
}
let serverCertData = SecCertificateCopyData(serverCert) as Data
guard
let localCertPath = Bundle.main.path(forResource: "verify.ice.ibmcloud.com", ofType: "cer"),
let localCertData = NSData(contentsOfFile: localCertPath) as Data?, localCertData == serverCertData else
{
reject (with: completionHandler)
return
}
accept (with: serverTrust, completionHandler)
}
func reject (with completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.cancelAuthenticationChallenge, nil)
}
func accept (with serverTrust: SecTrust, _ completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
func reject (with completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.cancelAuthenticationChallenge, nil)
}
func accept (with serverTrust: SecTrust, _ completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
As an alternative to this classic approach of certificate pinning, iOS also support the Certificate Transparency standard.
Certificate Transparency
Certificate Transparency (CT) is an open source framework to monitor and audit certificates. It was initiated by Google and extends the existing certificate system by public logs, where all submitted certificates of trusted CA’s are recorded. The objective is to provide a publicly available system of logs, where any domain owner can verify whether a certificate was issued by a trusted CA or issued maliciously.
It basically works as follows: when a valid certificate is submitted, CT responds with a signed certificate timestamp (SCT) for that certificate. During a TLS handshake, the web server sends the certificate and the SCT to the client (browser). The existence of an SCT proofs to the client, that the certificate was added to the log. The client contacts CT to verify the certificate.
IBM Verify SDK
In the IBM Verify SDK for Android, we support the CertificatePinner approach as described above. Here is what you need to configure in your app in order to pin the certificate of IBM Cloud Identity:
NetworkHandler.sharedInstance().setCertificatePinner(new CertificatePinner.Builder()
.add("*.ice.ibmcloud.com", "sha256/IAEQVIalDTaJXtBfrTYjle+QqgRABR2FGQLebGybwtw=")
.build());
With iOS, we take advantage of the Certificate Transparency support by Swift. Therefore, the IBM Verify SDK for iOS does not have dedicated methods to pin a certificate. We recommend enabling Certificate Transparency for the required hosts in your application project settings:
Summary
With mobile applications increasingly transfer sensitive data back and forth to the servers, using certificate pinning can help you to strengthen the security of that communication and guard it against MiTM exploits. The gained security outweighs by far the required implementation effort and is highly recommended to protect your data and business reputation.
References
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning