Two failures show up in mobile assessments with equal frequency. The first is an app with no pinning at all, where anyone who can install a certificate on the device reads every request in plain text. The second is worse in a different way: pinning done so rigidly that the day the certificate is renewed, every installed copy of the app stops working until users update.
The second failure is why many teams remove pinning after their first outage, and then have neither. Here is how to keep it.
Pin the key, not the certificate
A certificate expires and gets replaced, typically every 90 days with automated issuance. A public key does not have to change when the certificate does — if you reuse the key pair on renewal, a key pin survives renewal untouched.
- Pin the SHA-256 hash of the Subject Public Key Info, not the certificate fingerprint.
- Pin at the intermediate CA level if you rotate leaf keys often, accepting slightly weaker guarantees for far fewer outages.
- Never pin a root you do not control the issuance policy of — it protects less than people assume.
Always ship a backup pin
The rule that prevents almost every pinning outage: at any moment the app should accept two pins — the key in use, and the key you will move to next. Generate the next key pair now, pin it now, and switch to it later.
- Generate the current and next key pairs. Store the next one offline.
- Ship both pins in the app.
- When rotating, deploy a certificate for the next key. Installed apps keep working, because that pin already shipped.
- In the following release, add a third pin for the rotation after that, and drop the retired one.
Give the pin set an expiry
Pins should carry a hard expiry date, after which the app falls back to normal certificate validation. It feels wrong to weaken a control on a timer, and it is the difference between an incident and a catastrophe: an app in the store that nobody updates should degrade to standard TLS, not to unusable.
Set the expiry comfortably beyond your typical adoption curve — six to twelve months for most consumer apps — and treat it as a deadline for shipping the next pin set, not a fallback you rely on.
Implementation notes per platform
- Android: use the network security configuration for the base case, and OkHttp’s CertificatePinner when you need per-host logic. Expiry is a first-class field in the platform config — use it.
- iOS: App Transport Security handles the baseline; implement pinning in the URLSession delegate and evaluate the trust chain properly rather than short-circuiting on the leaf.
- Flutter and React Native: pinning belongs in the HTTP client, and needs testing on both platforms separately. Plugins that "handle pinning" frequently only cover one.
- Every platform: fail closed on a pin mismatch, and never leave a debug flag that disables validation. A badCertificateCallback returning true reaches production more often than anyone would like.
// A pin set with a backup and a hard expiry, in the platform config.
// <pin-set expiration="2027-01-01">
// <pin digest="SHA-256">current-key-hash</pin>
// <pin digest="SHA-256">next-key-hash</pin> <!-- backup, not yet in use -->
// </pin-set>
What pinning does not do
Expect pinning to be bypassed on a rooted or jailbroken device with Frida in about ten minutes. That is not a failure of the control. Pinning defends against interception on a hostile network and against a user tricked into installing a proxy certificate — attacks against many users at once. It does not defend against an attacker with full control of one device, and no amount of hardening changes that.
The mistake is not weak pinning. It is treating pinning as the reason an endpoint does not need its own authorisation checks. If bypassing pinning gives an attacker access to data belonging to other users, the finding is in the API, and pinning was only hiding it.
Testing your implementation
- Intercept with a proxy on a stock device: traffic should fail, not fall back.
- Intercept on a rooted device with a hooking framework: expect success, and confirm the API still enforces authorisation on every request you can now see.
- Simulate rotation in staging by deploying the backup key’s certificate. The current app build must keep working with no update.
- Check the expiry date is in the future in every release build — a shipped pin set that expired last month is an outage waiting for its trigger.