SSL Pinning in React Native Apps
App security is crucial, and SSL pinning in React Native can significantly enhance it. SSL pinning involves embedding a public key, hash, or certificate directly into your code, ensuring that only requests signed with trusted certificates are accepted. This post explains what SSL pinning is, its benefits, and provides a step-by-step guide on implementing SSL pinning in a React Native app for both iOS and Android.
Regardless of the vertical you’re operating in, app security should be among your top concerns. There are many steps you can take to bulletproof your React Native application, but in this post, we’ll focus on one in particular: SSL pinning.
We’re going to explain:
- what SSL pinning is,
- how it can improve your application security,
- how to implement SSL pinning in a React Native app,
- and, most importantly, how to protect your SSL pinning implementation.
So without further ado, let's jump right in!
What is SSL pinning?
When your application consumes data from the API via HTTPS protocol, the client will only trust the server if it can provide a trusted certificate. The server certificate is trusted when it's signed by another certificate that is also trusted, and so on until you reach the root certificate signed by the trusted certificate authority. This concept is known as the chain of trust, and certificates of these trusted authorities are stored in your OS trust store.
If an attacker tries to eavesdrop on this traffic, also known as a Man in the Middle attack (MitM), they don't have a private key of that root certificate. Therefore, they can’t sign data with that certificate, and your system prevents the connection.
Unfortunately, this can be bypassed in two ways:
- an attacker may plant a certificate in your OS trust store or
- in rare cases, the trusted certificate authority can be compromised.
The attack can be made in many ways, and it doesn’t necessarily require physical access to the victim's device.
Pinning is a mechanism that lets you embed a public key, hash, or certificate itself directly into your codebase, so only requests signed with one of the trusted certificates will be accepted.
With that being said, I believe that SSL pinning is a great choice for applications dealing with sensitive user information, like mobile banking or healthcare, where you need to take care of user privacy.
How to implement SSL pinning in React Native?
There are several ways to implement SSL pinning in your React Native project; you can use an npm package like react-native-ssl-pinning, or you go with native implementation for iOS and Android respectively.
<rte-code>react-native-ssl-pinning<rte-code> is a great library that makes SSL pinning super easy to implement in React Native projects, but it has a small caveat—it only lets you use <rte-code>fetch<rte-code>. And if you work on an app that is already live, chances are that you are using some other client like <rte-code>axios<rte-code> to make your requests. That’s why we will focus on native implementations (they’re surprisingly easy, I promise)
Before we jump into native code, we need to get the certificates from the domain we will be pinning, using the command below to show the list of certificates on the domain.
When you have your certificates saved, use this command to convert your certificates to base64 format and save the output for later.
SSL pinning on iOS
To implement SSL pinning on iOS, we will use a native library called Trustkit. We can install it through CocoaPods.
Open your <rte-code>Podfile<rte-code> and add the following line:
Next, go to your <rte-code>/ios<rte-code> directory and run <rte-code>pod install<rte-code> so the library is correctly linked to your project.
Once we have <rte-code>Trustkit<rte-code> installed, the SSL Pinning setup is next. For that, open the <rte-code>AppDelegate.mm<rte-code> file using your favorite editor—we recommend Xcode for dealing with iOS platform files—and call TrustKit’s <rte-code>initSharedInstanceWithConfiguration<rte-code> method with <rte-code>trustKitConfig<rte-code> parameter like this:
Here, we are using key hashes that we generated in the first step with the help of OpenSSL.
And that's it 🎉 Now we can check if our SSL pinning setup works as expected. Run your app on an iOS device or simulator and make a request to a domain that you pinned; everything should work just like before.
Now for the fun part – let's test the failing scenario. Change the public key hash in <rte-code>AppDelegate.m<rte-code> to an invalid one, like:
Rebuild the app and run it again. You probably expected some kind of error message and request failure, but SSL pinning seems to work just fine here...
Why is the connection still allowed even if we provided an invalid key? The perpetrator of this confusion is iOS <rte-code>NSURLSession<rte-code> which maintains its own TLS session cache. For that reason, even after changing the key to an invalid one, we still can communicate with the server just fine.
One way to force a cache reset is to simply delete the app from the simulator and build it again. Once this is done, the connection should fail:
Here, I'm using Proxyman to capture my http traffic, and as you can see, the API call is rejected during handshake because our certificates don't match. This is due to a proxy that we have between our app and the server.
SSL pinning on Android
Under the hood, React Native Android uses OkHttp, a library for network calls with SSL Pinning support out of the box. It makes SSL pinning configuration on Android even simpler than on iOS.
You need to create a new Java file inside <rte-code>android/app/src/main/java/com/<your_domain><rte-code>. I've called it <rte-code>SSLPinningFactory.java<rte-code>, but this is entirely up to you.
The next step will be <rte-code>OkHttpClient<rte-code> setup with our newly created <rte-code>SSLPinningFactory<rte-code> class. We can do so in the app’s <rte-code>MainApplication.java<rte-code>. Import <rte-code>com.facebook.react.modules.network.OkHttpClientProvider<rte-code> and call its <rte-code>setOkHttpClientFactory<rte-code> method with a new instance of our <rte-code>SSLPinningFactory<rte-code> as below:
And that's it! Now you have a working SSL Pinning setup in both Android and iOS 🥳
How to prevent bypassing of SSL pinning?
Now that we have our SSL pinning implementation in place, let's focus on securing it further. There are two main approaches to bypassing SSL pinning:
- Replacing pin certificate data – as long as attackers can find our public key hashes, they can substitute them for their own man-in-the-middle server certificate hashes.
- Disabling the checking routine (method hooking) – on iOS for example, with the help of tools like Frida, the attacker can hook into Trustkit's <rte-code>evaluateTrust<rte-code> method to always return evaluation success value and, as a result, altogether bypass the check.
To prevent that from happening and make your React Native SSL pinning implementation more secure, you should consider the following techniques:
- String encryption – this will make finding and replacing the pin string a bit harder, and with checking string integrity from within the code, you will force the attacker to find a way to encrypt the new pin or find and disable the integrity check.
- Name obfuscation – with the help of obfuscation software, you can make it more difficult for an attacker to locate and intercept your checking routine. If you are interested in this technique, I encourage you to take a closer look at these articles about name obfuscation in iOS and Android for more detailed explanations and tools you can use.
- Hook detection – since method hooking that we briefly discussed before alters the function in memory, we can verify the integrity of key functions in memory before we verify the SSL pin. Knowing the function implementation, we can compare the first few bytes of the function in memory with the expected data. If the first instructions of the function do not match, the function has been hooked, and further execution should be terminated. There is an excellent article from ASEE about method hooking in mobile apps if you are interested in exploring this topic further.
Things to keep in mind when implementing SSL pinning
As with everything, SSL pinning has its strengths and weaknesses; well, not exactly weaknesses, but things you should keep in mind. SSL certificates have an expiration date, and when a certificate expires, it becomes invalid and needs to be renewed. It's a good idea to plan certificate updates ahead and include a backup certificate with a longer expiration date than your main cert.
An application with an expired certificate will fail to connect to the server, which can lead to bad UX. In contrast to web applications where the user connects with the server through a web browser, a mobile application needs to be installed on the user's device. To prevent a situation where your SSL certificate was renewed, but the user didn't update their application, it's worth considering packages like react-native-version-check and forcing users to download the latest version of the app from the store.
Final thoughts on SSL pinning in React Native
There is no one magic trick for making your React Native app 100% secure. Still, SSL pinning is one of the things that will make the API you’re using less susceptible to reverse engineering, add security against malicious certificates, and enhance user privacy.
Feel like your project could benefit from security enhancement? Contact us, and let’s discuss how you can use our React Native development skills to your advantage.