ScaleRocket/Mobile

SSL Pinning

Certificate pinning for API calls — when it's needed and how to configure it in React Native.

SSL Pinning

Certificate pinning (SSL pinning) ensures your app only communicates with your specific server by verifying the server's certificate against a known copy, preventing man-in-the-middle attacks even on compromised networks.

When You Need It

ScenarioSSL Pinning?
Standard SaaS appUsually not needed
Banking / financial appRequired
Healthcare / medical dataRequired
App handling government dataRequired
Enterprise with compliance requirementsLikely needed

For most apps, HTTPS with proper certificate validation is sufficient. SSL pinning adds complexity and maintenance overhead (you must update the app when certificates rotate).

How It Works

Without pinning:

  1. App connects to server
  2. OS verifies the server's certificate against any trusted CA
  3. Connection established

With pinning:

  1. App connects to server
  2. App verifies the server's certificate against a specific certificate or public key
  3. If it doesn't match, connection is rejected

This prevents attacks where a rogue CA issues a fake certificate for your domain.

Implementation with expo-network

For basic pinning in Expo, use a custom fetch wrapper:

import { Platform } from "react-native";

const PINNED_CERTIFICATES = {
  "api.example.com": "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
};

// Note: True certificate pinning requires native modules
// This example shows the concept — use a library for production

React Native SSL Pinning Library

For production apps, use react-native-ssl-pinning:

npm install react-native-ssl-pinning
npx expo prebuild
import { fetch as pinnedFetch } from "react-native-ssl-pinning";

async function secureApiCall(endpoint: string) {
  const response = await pinnedFetch(`https://api.example.com${endpoint}`, {
    method: "GET",
    headers: { "Content-Type": "application/json" },
    sslPinning: {
      certs: ["my-server-cert"], // Certificate file name without extension
    },
  });

  return await response.json();
}

Adding Certificates

  1. Export the certificate from your server:
openssl s_client -connect api.example.com:443 -showcerts < /dev/null \
  | openssl x509 -outform DER -out my-server-cert.cer
  1. Place the certificate file:
  • iOS: Add my-server-cert.cer to the Xcode project
  • Android: Place in android/app/src/main/assets/

Public Key Pinning

Pin the public key instead of the full certificate. This survives certificate renewals as long as the key pair stays the same:

const response = await pinnedFetch("https://api.example.com/data", {
  method: "GET",
  sslPinning: {
    certs: ["my-server-cert"],
  },
  pkPinning: true, // Pin the public key, not the full cert
});

Certificate Rotation Strategy

Certificates expire. Plan for rotation:

  1. Pin multiple certificates — Include the current and next certificate
  2. Use public key pinning — Keys can stay the same across certificate renewals
  3. Emergency bypass — Consider a remote config flag to disable pinning if needed
  4. Monitor expiry — Alert before certificates expire
// Pin both current and backup certificates
const response = await pinnedFetch(url, {
  sslPinning: {
    certs: ["current-cert", "backup-cert"],
  },
});

Important Considerations

  • SSL pinning requires a development build (not Expo Go)
  • You must update the app when certificates change (unless using public key pinning)
  • Pinning can break during development if using proxy tools (Charles, Proxyman)
  • Add a debug mode that disables pinning in __DEV__:
const fetchOptions = __DEV__
  ? { method: "GET" }
  : { method: "GET", sslPinning: { certs: ["my-cert"] } };

Testing

ToolPurpose
Charles ProxyVerify pinning blocks intercepted traffic
mitmproxyTest man-in-the-middle detection
openssl s_clientInspect server certificates

To verify pinning works:

  1. Enable Charles Proxy or mitmproxy
  2. Make an API call from your app
  3. The call should fail with a certificate error — this means pinning is working

On this page