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
| Scenario | SSL Pinning? |
|---|---|
| Standard SaaS app | Usually not needed |
| Banking / financial app | Required |
| Healthcare / medical data | Required |
| App handling government data | Required |
| Enterprise with compliance requirements | Likely 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:
- App connects to server
- OS verifies the server's certificate against any trusted CA
- Connection established
With pinning:
- App connects to server
- App verifies the server's certificate against a specific certificate or public key
- 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 productionReact Native SSL Pinning Library
For production apps, use react-native-ssl-pinning:
npm install react-native-ssl-pinning
npx expo prebuildimport { 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
- 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- Place the certificate file:
- iOS: Add
my-server-cert.certo 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:
- Pin multiple certificates — Include the current and next certificate
- Use public key pinning — Keys can stay the same across certificate renewals
- Emergency bypass — Consider a remote config flag to disable pinning if needed
- 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
| Tool | Purpose |
|---|---|
| Charles Proxy | Verify pinning blocks intercepted traffic |
| mitmproxy | Test man-in-the-middle detection |
openssl s_client | Inspect server certificates |
To verify pinning works:
- Enable Charles Proxy or mitmproxy
- Make an API call from your app
- The call should fail with a certificate error — this means pinning is working