SSL Pinning in React Native Apps

Authors
Kamil Bieganowski
Software Engineer
@
Callstack

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.

react-native-ssl-pinning 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 fetch And if you work on an app that is already live, chances are that you are using some other client like axios 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.

openssl s_client -showcerts -servername your-domain.com -connect your-domain.com:443
view raw fetch_cert.sh hosted with ❤ by GitHub

When you have your certificates saved, use this command to convert your certificates to base64 format and save the output for later.

openssl x509 -in <path-to-your-cert>.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
view raw base64.sh hosted with ❤ by GitHub

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 Podfile and add the following line:

require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, '12.4'
install! 'cocoapods', :deterministic_uuids => false
target 'sslPinning' do
config = use_native_modules!
+ pod 'TrustKit'
# Flags change depending on the env values.
flags = get_default_flags()
...
view raw Podfile.diff hosted with ❤ by GitHub

Next, go to your /iosdirectory and run pod installso the library is correctly linked to your project.

Once we have Trustkit installed, the SSL Pinning setup is next. For that, open the AppDelegate.mm file using your favorite editor—we recommend Xcode for dealing with iOS platform files—and call TrustKit’s initSharedInstanceWithConfiguration method with trustKitConfigparameter like this:

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
+#import <TrustKit/TrustKit.h>
#import <React/RCTAppSetupUtils.h>
...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
+ NSDictionary *trustKitConfig =
+ @{
+ kTSKSwizzleNetworkDelegates: @YES,
+ kTSKPinnedDomains: @{
+ @"sslpinning.com" : @{
+ kTSKIncludeSubdomains: @YES,
+ kTSKEnforcePinning: @YES,
+ kTSKDisableDefaultReportUri: @YES,
+ kTSKPublicKeyHashes : @[
+ @"<Primary SHA256 key>",
+ @"<Backup SHA256 key>",
+ ],
+ },
+ }};
+ [TrustKit initSharedInstanceWithConfiguration:trustKitConfig];
RCTAppSetupPrepareApp(application);

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 AppDelegate.m to an invalid one, like:

"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

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 NSURLSession 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:

SSL pinning with Proxyman

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 android/app/src/main/java/com/<your_domain>. I've called it SSLPinningFactory.java, but this is entirely up to you.

package com.sslpinning; // your domain here
import com.facebook.react.modules.network.OkHttpClientFactory;
import com.facebook.react.modules.network.OkHttpClientProvider;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
public class SSLPinningFactory implements OkHttpClientFactory {
private static String hostname = "sslpinning.com";
public OkHttpClient createNewNetworkModuleClient() {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/<your SHA256 key>")
.build();
OkHttpClient.Builder clientBuilder = OkHttpClientProvider.createClientBuilder();
return clientBuilder.certificatePinner(certificatePinner).build();
}
}

The next step will be OkHttpClient setup with our newly created SSLPinningFactory class. We can do so in the app’s MainApplication.java. Import com.facebook.react.modules.network.OkHttpClientProvider and call its setOkHttpClientFactory method with a new instance of our SSLPinningFactory as below:

...
+import com.facebook.react.modules.network.OkHttpClientProvider;
...
public class MainApplication extends Application implements ReactApplication {
...
@Override
public void onCreate() {
super.onCreate();
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
+ OkHttpClientProvider.setOkHttpClientFactory(new SSLPinningFactory());
}
...
}

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 evaluateTrust 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.

Insights

Learn more about Security

cover

Dec 17Podcast

A (Secure) Christmas Carol: The Story of Npmezer Scrooge

A holiday story about npm audits, unpinned dependencies, and the real cost of ignoring JavaScript security warnings.

cover

Aug 20Article

Secure Communication Between Isolated React Native Instances

react-native-sandbox now supports secure, direct communication between sandboxed React Native instances, modeled after the web’s postMessage API with origin-based access control.

cover

May 13Article

React Native Legal: Zero-Config OSS License Screens for Your App

Add license acknowledgments to your React Native app with ease using React Native Legal-a zero-config tool for OSS compliance across iOS, Android, tvOS, and AndroidTV platforms.

cover

Jul 3Podcast

Authorization: Security Deep-Dive With Okta (Part 2)

Let's talk access control, delegated authorization, and more. Learn modern security frameworks and get tips for creating reliable apps.

cover

Jul 2Podcast

Authentication: Security Deep-Dive With Okta (Part 1)

Join us as we discuss authentication's past, present, and future. From MFA to passkeys, learn how to enhance security and efficiency in your applications.

cover

Mar 8Article

Learn Once, Hack Everywhere

Demystify reverse engineering of mobile apps, dive into tools, analyze .apk files, and secure React Native code with our article.

cover

Oct 28Article

Stripe Identity: ID Authentication Made Simple

Learn about preventing fraud with Stripe Identity React Native SDK.

cover

Nov 21Article

Secure Your React Native app

Learn how to make your React Native app as secure fot your application and customers.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.