DEV Community

Cover image for SSL Pinning in React Native for iOS and Android
Ajmal Hasan
Ajmal Hasan

Posted on • Edited on

SSL Pinning in React Native for iOS and Android

Securing data transmission in mobile applications is critical. One way to secure this communication is through SSL Pinning. SSL Pinning ensures that your app only communicates with your server using trusted certificates, enhancing the security against man-in-the-middle (MITM) attacks.

This guide will walk you through setting up SSL pinning in both iOS and Android in your React Native app.


Gathering Information for SSL Pinning

Step 1: Test Your SSL Certificate

Start by going to the SSL Labs SSL Test, enter your domain or api base url(like your-domain.com), and initiate a scan. The results will include details about your SSL certificate.

SSL Labs SSL Test Screenshot

Step 2: Requirements for iOS SSL Pinning

Note: iOS requires at least two public key hashes (primary and backup) to enable SSL pinning.

Two Hashes Requirement

Step 3: Copy the SHA256 Pin and Domain:

Locate and copy the Pin SHA256 hash.

Pin SHA256 Screenshot

Note:

  1. We can get SHA256 Pin from Additional Certificates (if supplied) also.
  2. Check Valid until on the page. After this the certificate will expire so you again have to add new ones and make new app builds.
  3. Copy Domain for eg: The domain for https://dev.to/ is dev.to.


SSL Pinning with react-native-ssl-public-key-pinning

You can use the react-native-ssl-public-key-pinning library to implement SSL Pinning in your React Native application to enhance security by ensuring that your app communicates only with trusted servers.

Steps to Implement SSL Pinning

Step 1: Install the Library

First, install the library in your project:

yarn add react-native-ssl-public-key-pinning
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Utility File for SSL Pinning (sslPinning.ts)

In this file, we will configure the SSL pinning for multiple hostnames and load public keys from environment variables.

import Config from 'react-native-config';
import { initializeSslPinning, isSslPinningAvailable } from 'react-native-ssl-public-key-pinning';

/**
 * SSL Pinning Configuration Interface
 */
interface SslPinningConfig {
  [hostname: string]: {
    includeSubdomains: boolean;
    publicKeyHashes: string[];
    expirationDate?: string;
  };
}

/**
 * Initialize SSL Pinning with environment variables for multiple hostnames
 * @returns Promise<boolean> - true if successful, false otherwise
 */
export const initializeSslPinningLib = async (): Promise<boolean> => {
  try {
    // Check if SSL Pinning is supported on the device
    if (!isSslPinningAvailable()) {
      console.warn('SSL pinning is not available on this device');
      return false;
    }

    // Get SSL pinning configuration values from environment variables
    const hostname1 = Config.SSL_PINNING_HOSTNAME_1;
    const primaryKey1 = Config.SSL_PINNING_KEY_PRIMARY_1;
    const backupKey1 = Config.SSL_PINNING_KEY_BACKUP_1;

    const hostname2 = Config.SSL_PINNING_HOSTNAME_2;
    const primaryKey2 = Config.SSL_PINNING_KEY_PRIMARY_2;
    const backupKey2 = Config.SSL_PINNING_KEY_BACKUP_2;

    // Validate the required environment variables for at least one hostname
    if (!hostname1 || !primaryKey1 || !backupKey1) {
      console.error('SSL pinning environment variables are not properly configured');
      console.error(
        'Required: SSL_PINNING_HOSTNAME_1, SSL_PINNING_KEY_PRIMARY_1, SSL_PINNING_KEY_BACKUP_1'
      );
      return false;
    }

    // Build the configuration object for SSL pinning
    const sslPinningConfig: SslPinningConfig = {
      [hostname1]: {
        includeSubdomains: true,
        publicKeyHashes: [primaryKey1, backupKey1],
      },
    };

    // Add the second hostname if configured
    if (hostname2 && primaryKey2 && backupKey2) {
      sslPinningConfig[hostname2] = {
        includeSubdomains: true,
        publicKeyHashes: [primaryKey2, backupKey2],
      };
    }

    // Initialize SSL Pinning with the configuration
    await initializeSslPinning(sslPinningConfig);

    const hostnames = Object.keys(sslPinningConfig);
    console.log(
      `SSL pinning initialized for hostnames: ${hostnames.join(', ')}`
    );
    alert(
      `SSL pinning initialized for hostnames: ${hostnames.join(', ')}`
    );
    return true;
  } catch (error) {
    console.error('Failed to initialize SSL pinning:', error);
    return false;
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Using the Utility in Your App

You can now use the initializeSslPinningLib function to initialize SSL pinning when the app starts or on a specific event.

import { useEffect } from 'react';
import { initializeSslPinningLib } from './sslPinning'; // Adjust the import according to your file structure

useEffect(() => {
  const initializeSecurityTest = async () => {
    const success = await initializeSslPinningLib();
    if (success) {
      console.log('SSL pinning successfully initialized');
    } else {
      console.log('Failed to initialize SSL pinning');
    }
  };

  initializeSecurityTest();
}, []);
Enter fullscreen mode Exit fullscreen mode

Step 4: Testing SSL Pinning

When testing the implementation:

  • Invalid Public Keys: API calls should not proceed if the SSL certificate doesn't match the pinned keys.
  • Valid Public Keys: The API should work as expected with valid SHA256 public key hashes.

If the app tries to connect to a server with an incorrect certificate, the connection should be terminated, and you should prevent any API calls from occurring, thereby ensuring no communication with untrusted servers.

Final Notes:

  • SSL Pinning is essential for security, but you must ensure that your public keys are properly configured, and your environment variables are securely managed.
  • This library works for both iOS and Android as long as you configure the environment variables and public keys correctly.
  • Security Best Practices: Do not hardcode public keys directly in the app; always fetch them securely, and use react-native-config or similar secure configuration management.



OR YOU CAN IMPLEMENT WITHOUT ANY LIBRARY USING NATIVE CODE AS PER BELOW:

Implementing SSL Pinning on iOS

Step 1: Update Your Podfile

Add TrustKit to your Podfile for SSL pinning support.

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' # Add TrustKit for SSL Pinning
  ...
Enter fullscreen mode Exit fullscreen mode

Run pod install to install the new dependency.

Step 2: Configure AppDelegate.mm

  1. Import TrustKit and Configure TrustKit in the application:didFinishLaunchingWithOptions: method:
...
   #import <TrustKit/TrustKit.h>
...

   - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
   {
...
     NSDictionary *trustKitConfig = @{
       kTSKSwizzleNetworkDelegates: @YES,
       kTSKPinnedDomains: @{
           @"yourdomain.com" : @{
               kTSKIncludeSubdomains: @YES,
               kTSKEnforcePinning: @YES,
               kTSKDisableDefaultReportUri: @YES,
               kTSKPublicKeyHashes : @[
                 @"<Primary SHA256 key>", // Replace with your actual SHA256 key
                 @"<Backup SHA256 key>",  // Replace with a backup key
               ],
           },
       }};
     [TrustKit initSharedInstanceWithConfiguration:trustKitConfig];
...

Enter fullscreen mode Exit fullscreen mode

Update "yourdomain.com" with your domain and replace <Primary SHA256 key> and <Backup SHA256 key> with your actual SHA256 keys.


Implementing SSL Pinning on Android

a) JAVA

Step 1: Create SSLPinningFactory.java

Inside android/app/src/main/java/com/yourappname/, create a new file named SSLPinningFactory.java and add the following code:

package com.yourappname; // Update with your package name

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 = "yourdomain.com"; // Update with your domain

   public OkHttpClient createNewNetworkModuleClient() {

      CertificatePinner certificatePinner = new CertificatePinner.Builder()
        .add(hostname, "sha256/<your SHA256 key>") // Replace with your SHA256 key
        .build();

      OkHttpClient.Builder clientBuilder = OkHttpClientProvider.createClientBuilder();
      return clientBuilder.certificatePinner(certificatePinner).build();
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Replace "your SHA256 key" with your actual SHA256 hash.

Step 2: Register the SSL Pinning Factory in MainApplication.java

In MainApplication.java, configure SSL Pinning by adding the following line in the onCreate method:

+ 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()); // Register the SSLPinningFactory
  }
}
Enter fullscreen mode Exit fullscreen mode

---------------------OR----------------------

b) KOTLIN

SSLPinningFactory.kt

package com.-----

import com.facebook.react.modules.network.OkHttpClientFactory
import com.facebook.react.modules.network.OkHttpClientProvider
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

class SSLPinningFactory : OkHttpClientFactory {
    companion object {
        private const val hostname = "-----"
        private val sha256Keys = listOf(
            "sha256/------",
            "sha256/------")
    }
    override fun createNewNetworkModuleClient(): OkHttpClient {
        val certificatePinnerBuilder = CertificatePinner.Builder()
        for (key in sha256Keys) {
            certificatePinnerBuilder.add(hostname, key)
        }
        val certificatePinner = certificatePinnerBuilder.build()
        val clientBuilder = OkHttpClientProvider.createClientBuilder()
        return clientBuilder.certificatePinner(certificatePinner).build()
    }
}
Enter fullscreen mode Exit fullscreen mode

MainApplication.kt

...
+ import com.facebook.react.modules.network.OkHttpClientProvider // Import OkHttpClientProvider

class MainApplication : Application(), ReactApplication {

...

  override fun onCreate() {
    super.onCreate()
    SoLoader.init(this, false)

    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      load() // Load native entry point for New Architecture
    }
    ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)

    // Register SSLPinningFactory for OkHttpClientProvider
    + OkHttpClientProvider.setOkHttpClientFactory(SSLPinningFactory())
  }
}

Enter fullscreen mode Exit fullscreen mode

For Multiple Domain with .env do like this:

.env

# Domain 1
SSL_PINNING_HOSTNAME_1="-----.com"
SSL_PINNING_KEY_PRIMARY_1="---="
SSL_PINNING_KEY_BACKUP_1="---="

# Domain 2
SSL_PINNING_HOSTNAME_2="paperless---.com"
SSL_PINNING_KEY_PRIMARY_2="asas---="
SSL_PINNING_KEY_BACKUP_2="sadsd---="
Enter fullscreen mode Exit fullscreen mode

SSLPinningFactory.kt

package com.yourappname

import com.facebook.react.modules.network.OkHttpClientFactory
import com.facebook.react.modules.network.OkHttpClientProvider
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

class SSLPinningFactory : OkHttpClientFactory {
    companion object {
        private val domains = mapOf(
            BuildConfig.SSL_PINNING_HOSTNAME_1 to listOf(
                "sha256/${BuildConfig.SSL_PINNING_KEY_PRIMARY_1}",
                "sha256/${BuildConfig.SSL_PINNING_KEY_BACKUP_1}"
            ),
            BuildConfig.SSL_PINNING_HOSTNAME_2 to listOf(
                "sha256/${BuildConfig.SSL_PINNING_KEY_PRIMARY_2}",
                "sha256/${BuildConfig.SSL_PINNING_KEY_BACKUP_2}"
            )
        )
    }

    override fun createNewNetworkModuleClient(): OkHttpClient {
        val certificatePinnerBuilder = CertificatePinner.Builder()

        // Iterate through domains and keys to configure SSL pinning
        for ((hostname, keys) in domains) {
            for (key in keys) {
                certificatePinnerBuilder.add(hostname, key)
            }
        }

        val certificatePinner = certificatePinnerBuilder.build()
        val clientBuilder = OkHttpClientProvider.createClientBuilder()
        return clientBuilder.certificatePinner(certificatePinner).build()
    }
}

Enter fullscreen mode Exit fullscreen mode

AppDelegate.mm

#import <TrustKit/TrustKit.h>
#import "RNCConfig.h" // Import react-native-config


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
...

NSString *sslPinningHostname1 = [RNCConfig envFor:@"SSL_PINNING_HOSTNAME_1"];
NSString *sslPinningKeyPrimary1 = [RNCConfig envFor:@"SSL_PINNING_KEY_PRIMARY_1"];
NSString *sslPinningKeyBackup1 = [RNCConfig envFor:@"SSL_PINNING_KEY_BACKUP_1"];

NSString *sslPinningHostname2 = [RNCConfig envFor:@"SSL_PINNING_HOSTNAME_2"];
NSString *sslPinningKeyPrimary2 = [RNCConfig envFor:@"SSL_PINNING_KEY_PRIMARY_2"];
NSString *sslPinningKeyBackup2 = [RNCConfig envFor:@"SSL_PINNING_KEY_BACKUP_2"];

NSDictionary *trustKitConfig = @{
    kTSKSwizzleNetworkDelegates: @YES,
    kTSKPinnedDomains: @{
        sslPinningHostname1: @{  // First domain
            kTSKIncludeSubdomains: @YES,
            kTSKEnforcePinning: @YES,
            kTSKDisableDefaultReportUri: @YES,
            kTSKPublicKeyHashes: @[
                sslPinningKeyPrimary1,
                sslPinningKeyBackup1,
            ],
        },
        sslPinningHostname2: @{  // Second domain
            kTSKIncludeSubdomains: @YES,
            kTSKEnforcePinning: @YES,
            kTSKDisableDefaultReportUri: @YES,
            kTSKPublicKeyHashes: @[
                sslPinningKeyPrimary2,
                sslPinningKeyBackup2,
            ],
        },
    }
};

// Initialize TrustKit with the configuration
[TrustKit initSharedInstanceWithConfiguration:trustKitConfig];

...
...

Enter fullscreen mode Exit fullscreen mode

Conclusion

That’s it! You’ve successfully implemented SSL pinning for both iOS and Android in your React Native app. This setup ensures that only secure, trusted connections to your server are allowed, protecting your app from potential MITM attacks. 🥳

For more information, refer to the detailed guide from Callstack's SSL Pinning Blog.

Top comments (0)