Short answer: SSL pinning errors usually come from mismatched certificates, expired pins, or misconfigured code. In Flutter, check your http package configuration. In React Native, verify the native module setup and certificate hashes. Re-pin the correct certificate and test against production servers.
Key takeaways
- SSL pinning errors often stem from using the wrong certificate hash.
- Always test pinning against production endpoints, not just staging.
- Flutter uses the `http` package or platform channels for pinning.
- React Native relies on native modules like `react-native-ssl-pinning`.
- Expired or renewed certificates require updating your pinned keys.
- Certificate expiration only matters if you pin the leaf certificate.
What you will find here
- What Causes SSL Pinning Errors?
- How to Fix SSL Pinning Errors in Flutter
- How to Fix SSL Pinning Errors in React Native
- Comparison: Flutter vs React Native SSL Pinning Approaches
- Step-by-Step Checklist to Debug SSL Pinning Errors
- Common Mistakes and How to Avoid Them
- Testing SSL Pinning Effectively
- When to Consider Alternative Approaches
- How to Handle Certificate Rotation Gracefully
- Debugging with Network Logs
SSL pinning errors are frustrating. Your app was working fine, then suddenly network requests fail with a certificate error. This happens more often than you’d think, and it’s rarely a mystery once you know what to look for. Let’s walk through the common causes and fixes for both Flutter and React Native.
What Causes SSL Pinning Errors?
SSL pinning verifies that the server’s certificate matches a known copy embedded in your app. When they don’t match, the connection fails. Common causes include:
- Using the wrong certificate or public key hash.
- Pinning against a staging certificate that differs from production.
- Certificate renewal without updating the pin.
- Intermediate certificate changes.
- Code misconfiguration, like flipping a boolean flag.
How to Fix SSL Pinning Errors in Flutter
Flutter doesn’t have built-in SSL pinning. You implement it via packages or platform channels. Let’s look at the most common approach.
Using the http Package with Custom Client
The http package allows you to provide a custom HttpClient. Here’s a typical setup:
import 'dart:io';
import 'package:http/http.dart' as http;
HttpClient createPinnedClient() {
SecurityContext context = SecurityContext();
context.setTrustedCertificates('assets/cert.pem');
return HttpClient(context: context);
}
void fetchData() async {
var client = http.Client();
// Override the client's IOClient
// Actually you need to use IOClient with your custom HttpClient
}
If the certificate file is missing or the wrong format, requests fail. Common fixes:
- Ensure the certificate is in PEM or PKCS12 format.
- Verify the file path matches the asset declaration in
pubspec.yaml. - Use the full certificate chain, not just the leaf certificate.
- Test against production, not just localhost.
Using Platform Channels for Native Pinning
For more control, use platform-specific implementations. On Android, you can use TrustManager; on iOS, NSURLSession did receive challenge. Errors here often stem from native code typos or certificate hash mismatches. Double-check the base64-encoded SHA-256 hash of the public key.
How to Fix SSL Pinning Errors in React Native
React Native relies on native modules. The most popular is react-native-ssl-pinning. Here’s how to use it:
import { fetch } from 'react-native-ssl-pinning';
fetch('https://example.com', {
method: 'GET',
sslPinning: {
certs: ['cert1']
}
})
.then(response => console.log(response))
.catch(error => console.log(error));
If you see an SSL error, check these:
- The certificate file in your app’s native assets (e.g.,
android/app/src/main/assetsfor Android, bundled into iOS bundle for iOS). - The certificate must be in
.cerformat for iOS and.pemor.derfor Android. - If you renamed the file, update the reference.
Simulator vs Real Device
SSL pinning sometimes works on simulators but fails on real devices. This happens if the simulator trusts your local proxy (like Charles) while the device doesn’t. Always test on a physical device without any proxy.
Comparison: Flutter vs React Native SSL Pinning Approaches
| Feature | Flutter | React Native |
|---|---|---|
| Built-in support | None (must use packages or native code) | None (uses third-party modules) |
| Popular approach | Custom HttpClient or platform channels |
react-native-ssl-pinning module |
| Certificate format | PEM or PKCS12 | .cer (iOS), .pem/.der (Android) |
| Debugging difficulty | Medium (need to read native logs) | Medium (errors in native callbacks) |
| Common error source | Missing asset file or wrong format | Wrong certificate file location |
Step-by-Step Checklist to Debug SSL Pinning Errors
- Verify the certificate: Export the correct certificate from the server using openssl:
openssl s_client -connect example.com:443 -showcerts. Copy the server’s certificate (not the root CA) and save as .pem. - Generate the hash: If pinning by hash, run
openssl x509 -in cert.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64. - Embed correctly: Place the certificate file in the right assets folder. For Flutter, declare in
pubspec.yaml. For React Native, add to native asset directories. - Check the code: Ensure the filename matches exactly, including case. In Flutter, verify the
SecurityContextloads the file. In React Native, confirm thesslPinningoptions array references the correct cert name. - Test against production: Many teams pin against staging certificates by mistake. Deploy a test build that hits the production server.
- Monitor certificate expiry: Set a reminder to update pins before certificates renew. If you pin the leaf certificate, you must update after every renewal. Pinning the public key or intermediate CA reduces renewal frequency.
Common Mistakes and How to Avoid Them
One big mistake is pinning the wrong certificate. You might think you’re pinning the server’s certificate, but you accidentally pinned a root CA. Requests then fail because the leaf certificate doesn’t match. Always double-check the certificate chain.
Another mistake is forgetting to update pins after a certificate renewal. This is especially common when using Let’s Encrypt, which renews every 90 days. Automate your pin update process as part of your CI/CD pipeline. For more on securing your APIs, see this guide on API security.
Also, beware of including the entire certificate chain in a single file incorrectly. Some implementations expect only the server certificate, while others expect the full chain. Read the documentation carefully.
Testing SSL Pinning Effectively
You can test SSL pinning by running a proxy like mitmproxy. If your app fails with a proxy, pinning works. But if it fails without a proxy, you have a bug. Also test on different network conditions (Wi-Fi, cellular) and different certificate paths.
For a deeper dive into securing your mobile app, check out our introductory post on app security.
When to Consider Alternative Approaches
SSL pinning is not the only way to secure network traffic. Certificate transparency, or simply relying on the system’s trust store, are alternatives. Pinning adds extra security but also maintenance overhead. If your app experiences frequent pinning errors, evaluate if the extra security justifies the complexity. For many apps, standard TLS with certificate validation is enough.
How to Handle Certificate Rotation Gracefully
One practical approach to reduce pinning errors is to pin multiple certificates or hashes. For example, pin both the current certificate and a backup one. This way, when the server rotates its certificate, your app still trusts the old pin until you release an update. You can also pin the public key of an intermediate CA rather than the leaf. That intermediate CA changes less often, so you avoid frequent updates. Just be sure to keep the intermediate CA’s public key secure and update it only when the CA itself is replaced.
Debugging with Network Logs
When an SSL pinning error occurs, check the native logs. On Android, run adb logcat | grep -i ssl to see TLS handshake details. On iOS, look in the device console for NSURLSession errors. These logs often reveal whether the pin check failed due to a hash mismatch or a missing certificate. For Flutter, you can also catch the error in Dart and inspect the status code or message. In React Native, the error object from the promise might include a nativeError property with more detail. Combine these logs with your checklist to narrow down the exact cause.
Frequently asked questions
Why does SSL pinning work on staging but fail on production?
The staging server likely uses a different certificate than production. If you pinned the staging certificate, the app will fail when connecting to production. Always pin against the production certificate, and test builds with production endpoints before release.
What happens when a pinned certificate expires?
If you pin the leaf certificate, the app will stop working once it expires. You must release an app update with the new certificate. If you pin the public key or an intermediate CA, you have more flexibility since those change less frequently.
Can SSL pinning cause app rejection on iOS?
No, Apple does not reject apps for using SSL pinning. However, if your pinning breaks connectivity for users, you may get negative reviews. Ensure you have a fallback mechanism or update pins promptly.
How do I debug SSL pinning errors on iOS?
Use the device logs via Xcode (Window > Devices and Simulators). Look for errors like “NSURLErrorDomain error -1200” or “SSL pinning error”. Also, enable verbose logging in the native module if available.
Is SSL pinning necessary for all mobile apps?
No. SSL pinning is recommended for apps handling sensitive data (finance, health, authentication). For less sensitive apps, standard TLS validation is often sufficient. Pinning adds maintenance burden that may not be justified.
1 thought on “How to Fix SSL Pinning Errors in Flutter and React Native”