Mobile
SSL pinning: locking down certificates in a mobile app
If an app accepts any certificate the system trusts, its traffic can be intercepted. How to set up pinning on Android and in Flutter, and how to prepare for certificate rotation.
· 7 min read

In the security review of our own app, one of the high-severity Android findings was "insecure certificate validation". The app used HTTPS and the certificate was genuine. The problem was elsewhere: the app accepted any certificate authority (CA) the device trusted. In this article we look at why that is not enough, how SSL pinning works, and how to implement it without breaking the app when certificates rotate.
The problem: HTTPS is there, but trust is too broad
Normally, when an app connects to a server, the system validates the certificate chain: was the certificate issued by one of the trusted CAs on the device? That list contains dozens of CAs. On top of that, the user or anyone else with access to the device can add their own CA.
That is exactly what the reviewer tests: they install their proxy tool's CA on the device and try to decrypt the app's traffic. If the app notices nothing and keeps working, logins, tokens and financial data are visible in plain text. This falls under the NETWORK group of MASVS and category M5 (Insecure Communication) of the OWASP Mobile Top 10 2024.
What SSL pinning is
Pinning tells the app: "trust only this specific key or certificate". The app stores a hash of the expected public key. If none of the certificates in the server's chain match that hash during the connection, the connection is dropped — even if the system trusts the certificate.
Usually it is not the certificate itself that is hashed but its public key (SPKI). A certificate can be renewed while the key stays the same, which makes the pin more stable.
What to pin: leaf, intermediate or root CA
This is the most important decision, because the wrong choice can take the app offline overnight.
- Leaf (the server certificate) — the strictest option. But server certificates are renewed regularly. Let's Encrypt certificates, for example, rotate every 90 days. If the new certificate comes with a new key, every older version of the app can no longer connect.
- Intermediate CA — a middle option, but CAs also rotate their intermediates.
- Root CA — the most stable option. Root certificates stay unchanged for years.
In our app we pinned to specific, selected root CAs. This cuts off third-party and proxy CAs completely, yet does not get in the way of renewing the server certificate as usual.
Rule: always keep at least two pins — a primary and a backup. The backup should ideally belong to a different CA, so the app keeps working if something goes wrong with one CA.
Android: network_security_config
Since Android 7.0, pinning can be configured without code, through XML. The file res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.uz</domain>
<pin-set expiration="2027-12-31">
<!-- SHA-256 hash of the primary root CA public key -->
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- backup: a different root CA -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>
And in AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false">
Android matches the pin against any certificate in the chain, so a root CA hash works. The expiration attribute sets how long the pins are valid: after that date pinning is turned off and normal validation is used. This prevents the app from breaking completely on devices that have not updated it for a long time, but remember to move the date forward in time.
You can get the hash of a root CA's public key like this:
openssl x509 -in root_ca.pem -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| openssl enc -base64
Flutter: the Dart side is configured separately
This is a common mistake in Flutter apps. Dart's HTTP client does not use the Android system network stack, so network_security_config has no effect on requests made from Dart. You need to configure both.
In Dart we disabled the system trust list and loaded only the selected root CAs:
final context = SecurityContext(withTrustedRoots: false);
context.setTrustedCertificatesBytes(rootCaPemBytes);
final client = HttpClient(context: context);
This way, whatever CA is installed on the device, the Dart client trusts only the root certificates shipped with the app. If you need several roots, you can combine them in one PEM file.
Preparing for certificate rotation
The main risk of pinning is cutting off your own users. To avoid that:
- Pin to a root CA, not the leaf, unless you strictly control the leaf key yourself.
- Add a backup pin — a different CA or a pre-generated backup key.
- Plan hosting or CA changes in advance. First add the new CA's pin to a new app version, wait for users to update, and only then change the certificate on the server.
- Track the
expirationdate and extend it with every major release. - Cover every domain — API, file server, analytics. If pinning exists only on the main domain, the rest stay open.
Test it
Install a proxy tool's CA (for example Burp Suite or mitmproxy) on a test device and launch the app. A correctly configured app should fail to connect, and no decrypted traffic should appear in the proxy. Then confirm that everything works on a normal network.
Keep in mind that pinning can be disabled with Frida. That is why it works best together with RASP and root detection.
Conclusion
SSL pinning is a few lines of configuration, but done wrong it can stop the app. The right approach: pin to root CAs, keep a backup pin, configure the Android and Dart sides separately, and plan rotation ahead of time.
If you want to know whether your app's traffic can be intercepted, request a security scan. We will check it, show you the gaps and help you prepare for the review.


