Securing API Communication Against Man in the Middle Attacks
Plain HTTPS trusts the entire operating system trust store, and that is exactly the gap rogue CAs, corporate proxies and tools like mitmproxy exploit. SSL pinning locks a certificate or public key directly into the app and turns intercepted traffic into a dead end for attackers, once certificate pinning and public key pinning are implemented cleanly for iOS and Android, tested properly and backed by a rollover strategy.
Table of Contents
- 1. Why HTTPS alone is not enough for mobile apps
- 2. Certificate pinning vs public key pinning
- 3. Implementing SSL pinning on iOS
- 4. Implementing SSL pinning on Android
- 5. Code walkthrough: react-native-ssl-pinning
- 6. Certificate rotation and pin rollover strategy
- 7. Testing and verifying SSL pinning
- 8. Interplay with other security layers
- 9. Pinning strategies compared
- 10. Summary
- 11. FAQ
1. Why HTTPS alone is not enough for mobile apps
HTTPS encrypts the transport, but the trust decision rests entirely on the operating system trust store. Any one of the several hundred preinstalled root CAs on iOS and Android can technically issue a valid certificate for any domain. If one of those CAs is compromised, misconfigured, or coerced into issuing a fraudulent certificate, any app that relies solely on system TLS validation will accept the forged connection without hesitation. This is exactly where SSL pinning comes in: it adds an extra check on top of standard certificate validation, an expectation the developer bakes into the app at build time.
On managed corporate devices (MDM) this risk is very concrete. IT departments often install an additional root certificate so a corporate proxy can decrypt, log and filter the entire TLS traffic stream. From the company's point of view this is a legitimate, intentional man in the middle. For an app handling sensitive data, for example in banking or health, it means credentials, session tokens and API responses land on the proxy in plain text, unless the app enforces SSL pinning instead of trusting that extended trust store.
Tools like Charles Proxy or mitmproxy make the problem tangible: once a root certificate is installed on the device and traffic is routed through the proxy, any unprotected HTTPS connection can be fully read and modified. For React Native apps, debug builds, third party SDKs and the Metro bundler add further attack surface on top of that. Skipping React Native SSL pinning implicitly assumes that nobody on the user's network path has installed an additional certificate, an assumption that often does not hold on public Wi-Fi or managed devices.
2. Certificate pinning vs public key pinning
With certificate pinning, the app stores the full hash of the server certificate, usually a SHA-256 digest over the DER-encoded certificate file. When the certificate expires or is renewed, that hash inevitably changes, even if the underlying key stays the same. Every certificate renewal therefore forces an app update with a new pin, which in practice causes exactly the outage that security teams were trying to avoid: users on an older app version suddenly lose every API connection the moment the server certificate rotates.
Public key pinning solves this by pinning not the certificate itself but the SPKI hash, the SHA-256 hash over the Subject Public Key Info, the public key in DER format. As long as a certificate renewal reuses the same key pair, which is standard practice for most CAs, the SPKI hash stays stable across several certificate generations. That is exactly why public key pinning is the recommended default for SSL pinning in mobile apps in 2026: it ties trust to the key, not to a document with a limited validity window.
The trade off is still real. If a certificate renewal also swaps the key, for instance because of a new provider, a new CA, or a security policy that forces it, public key pinning breaks just like certificate pinning. The difference is frequency: SPKI hashes almost always survive typical yearly certificate renewals, while plain certificate pinning forces a new app release on every single renewal. Backup pins for a second, not yet active key reduce the remaining risk even further.
3. Implementing SSL pinning on iOS
On iOS, SSL pinning typically hooks into the server trust evaluation of NSURLSession, either through a custom URLSessionDelegate with manual SPKI checks inside urlSession(_:didReceive:completionHandler:), or declaratively through TrustKit, a library that wraps exactly this logic. TrustKit swizzles the app's networking delegates and compares the SPKI hash of the presented certificate against a configured list of pins per domain on every TLS handshake, including optional backup pins for the rollover case.
The interaction with App Transport Security (ATS) matters here. ATS enforces modern TLS versions and blocks unencrypted connections by default, but it does not replace SSL pinning. Anyone defining an ATS exception for a single domain in Info.plist under NSAppTransportSecurity, for example for a legacy endpoint, should scope that exception as narrowly as possible and still keep TrustKit active for the production API hosts. ATS and pinning complement each other: ATS secures the transport parameters, SSL pinning secures the identity of the server against any CA that the trust store accepts but that is, in fact, wrong.
4. Implementing SSL pinning on Android
On Android, React Native's networking stack runs on OkHttp by default, which ships CertificatePinner.Builder() as a native, well documented API for SSL pinning. A single call like CertificatePinner.Builder().add(hostname, "sha256/BASE64HASH") is enough to register one or more SPKI pins for a host. Alternatively, and preferred by many teams because it needs no code changes in the OkHttp client, pinning can be configured declaratively via network_security_config.xml, using a <pin-set> element with multiple <pin digest="SHA-256"> entries per domain.
For React Native projects, the community library react-native-ssl-pinning is the most pragmatic route because it exposes a fetch-like API and internally drives both the OkHttp CertificatePinner on Android and NSURLSession validation on iOS. That way, SSL pinning does not need to be maintained separately for both platforms in native code, but can be configured centrally from the JavaScript layer, while the actual cryptographic check still runs natively and stays fast.
5. Code walkthrough: react-native-ssl-pinning
The first step is extracting the SPKI hash from the production server before writing any code at all. A chain of openssl s_client, openssl x509, openssl pkey and openssl dgst pulls the base64-encoded SHA-256 hash of the public key directly out of the live TLS connection, with no access to the private key or the server configuration required.
# Extract the SPKI (Subject Public Key Info) pin for SSL pinning
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| base64
On the iOS side, TrustKit registers the extracted pin at app launch, typically inside the AppDelegate, together with a second, not yet active backup pin for the later rotation.
// AppDelegate.swift - TrustKit configuration for SSL pinning on iOS
import TrustKit
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let trustKitConfig: [String: Any] = [
kTSKSwizzleNetworkDelegates: true,
kTSKPinnedDomains: [
"api.example.com": [
kTSKPublicKeyHashes: [
"AbCdEf1234567890PrimaryPinBase64==",
"ZyXwVu0987654321BackupPinBase64=="
],
kTSKEnforcePinning: true,
kTSKIncludeSubdomains: true
]
]
]
TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
return true
}
On the Android side, the same pin (and the backup pin) gets registered on the OkHttp client, either in the native module React Native uses for networking, or directly through network_security_config.xml. The snippet below shows the programmatic variant using CertificatePinner.Builder().
// NetworkModule.kt - OkHttp CertificatePinner for Android SSL pinning
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AbCdEf1234567890PrimaryPinBase64==")
.add("api.example.com", "sha256/ZyXwVu0987654321BackupPinBase64==")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
On the JavaScript side, react-native-ssl-pinning wraps both native implementations behind a fetch-compatible API. The sslPinning option references bundled certificate files inside the app package rather than raw hash strings, which means the matching .cer files need to be copied into the iOS and Android projects up front.
// api/client.js - fetch wrapper using react-native-ssl-pinning
import { fetch } from 'react-native-ssl-pinning';
export async function fetchOrders(token) {
const response = await fetch('https://api.example.com/v1/orders', {
method: 'GET',
timeoutInterval: 15000,
headers: {
Authorization: `Bearer ${token}`,
},
// sslPinning references bundled certificate files, not raw hash strings
sslPinning: {
certs: ['api-example-com-primary', 'api-example-com-backup'],
},
});
if (response.status !== 200) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
6. Certificate rotation and pin rollover strategy
A single, hardcoded pin is a genuine outage risk. If the server certificate or the underlying key expires without a matching backup pin already present in the shipped app version, every user on that version instantly loses all API connectivity, with no server side bug involved at all. That is why any serious SSL pinning setup needs at least a second, currently inactive pin, shipped months ahead of the planned rotation.
The established rollover flow looks like this: the new key or certificate is prepared on the server but not yet activated. The corresponding SPKI hash gets added as a backup pin in a new app release and rolled out, with enough lead time for the largest possible share of active installs to receive the update before the old pin expires. Only once monitoring shows that the vast majority of clients know the backup pin does the server switch over to the new certificate, and the old pin can be removed in a following release.
OTA update mechanisms like EAS Update or CodePush act as an important safety net in this rollover chain, since they let teams roll out JavaScript side pin configuration without a full app store review when a pin expiry emergency looms. They do not, however, replace a native rebuild once the pins are hardcoded into native TrustKit or OkHttp code, which is why teams that depend on fast pin updates should deliberately push the pin list as far as possible into configurable code that can be updated over the air.
7. Testing and verifying SSL pinning
Whether SSL pinning actually works can only be verified reliably with an active MITM proxy. Using mitmproxy, Charles Proxy or Proxyman, a custom root certificate is installed on the test device and the entire app traffic is routed through the proxy. A correctly implemented SSL pinning configuration detects that the certificate presented by the proxy does not match the stored pin and aborts the connection immediately, usually with an explicit connection error rather than a successful but tampered response.
The most common false negative shows up when testing is done exclusively on debug builds. Many development setups and some pinning libraries disable pin validation by default in debug configurations, so that local debugging through a proxy is not blocked. Testing SSL pinning only on a debug build may show no effect at all, wrongly suggesting that something is broken while the release build actually protects correctly, or the reverse, mistaking a broken configuration for a working one simply because the debug build bypasses it anyway.
The reliable test procedure is therefore always a release build, or at least a release-like build profile, installed on a physical device with an active MITM proxy and its root certificate installed. Only once that test case reliably results in a connection failure, while the same app works normally without an active proxy, can SSL pinning be considered properly verified.
8. Interplay with other security layers
SSL pinning is a network layer control and does not replace authentication or authorization at the API level. Short-lived JWTs with expiry windows of a few minutes limit the damage if a token still gets captured, while refresh tokens can be invalidated server side. These two mechanisms complement each other: SSL pinning prevents an attacker from decrypting the traffic in the first place, short-lived tokens limit the blast radius if some other weakness, for example a compromised device, exposes tokens anyway.
Request signing, where every API call is additionally signed with a secret, device-specific key, addresses a third risk: tampering with requests through an in-app vulnerability, independent of the transport channel. It is important to keep these layers clearly separated and never treat one as a substitute for another. SSL pinning solves only the problem of a trustworthy transport connection, while authentication and request signing ensure that even over an intact transport connection, only authorized, unmodified requests get accepted.
9. Pinning strategies compared
Choosing between no pinning, certificate pinning, public key pinning and a combination of public key pinning with backup pins has direct consequences for protection strength, renewal resilience and operational risk. The table below lines up the four approaches along these dimensions.
| Approach | MITM protection | Renewal resilience | Operational risk |
|---|---|---|---|
| No pinning | None against rogue CAs / proxies | Unrestricted, no pin to maintain | Very low |
| Certificate pinning | High, exact certificate match | Low, breaks on every renewal | High without a rollover plan |
| Public key pinning (SPKI) | High, key based | High with a stable key pair | Moderate, rollover recommended |
| SPKI + backup pins | High, with rotation lead time | Very high, planned transition | Low with a clean process |
In practice, combining public key pinning with at least one backup pin is the approach with the best ratio of protection strength to operational safety. Plain certificate pinning without a rollover plan merely shifts the risk from a security problem to a self-inflicted outage problem, which is often overlooked in the evaluation.
Mironsoft
React Native security, API hardening and mobile app audits
Is your React Native app's API traffic actually protected?
We implement SSL pinning for iOS and Android, build a workable pin rollover strategy and verify the protection against mitmproxy and Charles Proxy before your app ships to production.
Pinning implementation
TrustKit for iOS, OkHttp CertificatePinner for Android, wired cleanly through react-native-ssl-pinning
Rollover strategy
Backup pins, expiry monitoring and an OTA update process for critical pin rotations
Security testing
Verification with mitmproxy and Charles Proxy on real release builds, including a report
10. Summary
SSL pinning closes the gap that plain HTTPS leaves open on mobile: blind trust in the entire operating system trust store, rogue CAs and corporate proxies included. Public key pinning based on the SPKI hash is the more robust choice over classic certificate pinning, since it survives certificate renewals as long as the key pair stays the same. On iOS the implementation typically runs through TrustKit, on Android through OkHttp's CertificatePinner or network_security_config.xml, and react-native-ssl-pinning unifies both platforms behind one fetch API.
Without a well thought out rollover strategy backed by backup pins, SSL pinning itself becomes an outage risk the moment a certificate or key expires. Testing must always happen on release builds with an active MITM proxy such as mitmproxy or Charles Proxy, since debug builds frequently bypass pin validation. And SSL pinning remains a network layer control that meaningfully complements short-lived tokens, request signing and API authorization, but never replaces them.
React Native SSL pinning, the key takeaways
Public key over certificate
Pin the SPKI hash instead of the full certificate, it survives renewals with a stable key pair far more reliably.
Platform implementation
TrustKit on iOS, OkHttp CertificatePinner on Android, react-native-ssl-pinning as the cross-platform bridge.
Rollover is mandatory
Ship backup pins months before certificate expiry, otherwise SSL pinning itself becomes the outage risk.
Testing on release builds
Verify with mitmproxy or Charles Proxy always on release configuration, debug builds often bypass pinning.