Rotate a pinned certificate by shipping the replacement public key as a backup pin first, waiting until the installed base carries it, then switching the server and rehearsing recovery before the real change. Pin public keys rather than whole certificates, so a renewal that keeps the key needs no app release. Check every network stack in the app: Android’s Network Security Configuration and Apple’s NSPinnedDomains cover only the platform networking stacks. Android and iOS are configured and tested separately, and older installed versions, expiry and network failures belong in the test plan.
Identify the trust model
Start with the reason for pinning. Apple advises pinning only when the risk justifies it; pinning adds a restriction to normal TLS trust and never relaxes it. Record which service needs the extra restriction and who owns its certificate lifecycle.
Inventory each hostname, network stack and app release still in use. Both platforms pin hashes of a certificate’s public key (its SubjectPublicKeyInfo), so a new certificate on the same key is a non-event, while a new key is a change to plan.
| Record | Why it matters |
|---|---|
| Pin type and chain position | A leaf key and a CA key define different accepted identities |
| Hostnames and subdomain scope | The policy must match the connections the app actually makes |
| Current and backup keys | The replacement must be accepted before it serves traffic |
| Network stacks per platform | Each stack enforces only the pins it reads |
| Installed app versions | A new app configuration does not update existing installations |
| Server and client owners | Both sides must agree the switch and recovery conditions |
Keep the backup private key in the organisation’s key-management process. A public-key hash is configuration data; it is never a reason to give the private key to app developers. Use non-production certificates and authorised endpoints for the rehearsal.
Find every network stack
Platform pin configuration only protects the code that reads it.
- Android. Network Security Configuration is enforced by the platform’s default trust manager.
HttpsURLConnectionfollows it, and so do libraries that keep the default trust manager. OkHttp’sCertificatePinneris a separate pin set you maintain in code. Flutter’sdart:ioclient, native code using its own TLS library and any customTrustManagerneed their own pins. - iOS. App Transport Security, including
NSPinnedDomains, applies toURLSessionand the APIs built on it. Apple states that ATS does not apply to lower-level networking interfaces such as the Network framework or CFNetwork. Flutter’sdart:ioclient and native TLS libraries also sit outside it.
For each stack, record where its pins live and how a new key reaches it. A rotation that updates the XML or Info.plist but forgets an in-code pin set breaks that path on switch day.
Otukan pins public keys, supports backup keys and an expiry policy, and declares a recovery path with the first pinned release. It confirms that pinning covers the networking path used by Flutter and React Native apps on each platform. For product scope, see mobile app certificate pinning.
Prepare, distribute and switch
Configure Android
- Locate the Network Security Configuration XML and the
android:networkSecurityConfigattribute in the manifest. Confirm the code path under test uses it. - Review the relevant
domain-configand itspin-set. Android accepts a chain only when it contains at least one pinned key, in addition to normal certificate and hostname checks. - List the current and backup key hashes as
pin digest="SHA-256"entries. Google recommends always including a backup key. - Inspect any
expirationattribute on thepin-set. After that date Android stops enforcing the pins; expiry trades protection for availability and is not a backup key. - Test the release configuration, not a debug build.
debug-overridescan add trust anchors in debuggable builds only, so a debug connection proves nothing about the shipped policy. - Update every in-code pin set, such as
CertificatePinner, in the same release.
Configure iOS
- Inspect
NSAppTransportSecurity→NSPinnedDomainsinInfo.plistfor each relevant domain, includingNSIncludesSubdomainswhere used. - Identify whether the policy uses
NSPinnedCAIdentitiesorNSPinnedLeafIdentities. Each entry is a dictionary holding anSPKI-SHA256-BASE64value, and an array can hold several keys. - Add the replacement key alongside the current one for the planned overlap. Review the choice of CA or leaf pinning with the service owner.
- Test the exact domains and subdomains the app uses. Do not transpose Android’s domain configuration into Apple’s structure.
- Update any pins enforced by code outside
URLSessionin the same release.
Run the operational change
Ship and test the overlapping client policy before touching the server. Set the release gate on evidence about installed versions, not on the assumption that everyone updates. Test the server’s replacement chain with the current release and with older retained builds.
Switch the test server, observe the agreed user journeys and then rehearse the recovery decision. Roll back to the previous server key only if it remains uncompromised and its certificates remain valid. A compromised key is never a recovery option merely because old clients recognise it.
Do not fetch a replacement pin policy over the same connection a pin failure blocks. Apple names an app update as the eventual recovery path and recommends designing the user experience for a failed connection. Treat any separate recovery channel as a security design with its own authentication and review.
Otukan works with your team to rehearse a key rotation before release.
Prove success and rejection
Record the app build, OS version, device, network stack, server chain, network conditions, expected result and reviewer for each run.
| Test condition | Expected result |
|---|---|
| Valid current key | Sign-in and sensitive flows succeed |
| Valid replacement key in the backup set | The same flows succeed without bypassing trust checks |
| Otherwise valid but unpinned key | The protected connection is rejected on every stack |
| Expired certificate or wrong hostname | Ordinary TLS validation still rejects the connection |
| Stale app without the replacement key | Failure and the documented recovery experience are exercised |
| Offline device or interrupted network | Connectivity loss is handled without labelling every outage an attack |
| Clean install and upgrade | Both paths keep the intended pin policy |
Run the rejection cases on a legitimate supported device. Check what the user sees, whether sensitive operations stay restricted and whether diagnostics expose secrets. Compare the recovered state with the original trust policy, not just the return of network traffic.
- Current and backup keys have been tested independently on each network stack.
- Older installed releases have an explicit handling decision.
- Sign-in, update and sensitive-flow regression tests are recorded.
- Recovery keeps certificate and hostname validation in place.
- The service owner and mobile reviewer approve the change record.
Broader threat context is covered on man-in-the-middle prevention. Other technical guides for Android and iOS teams are in the Otukan knowledge base.
Further reading
- Network security configuration — Android Developers
- Identity Pinning: How to configure server certificates for your app — Apple Developer
- NSPinnedDomains — Apple Developer Documentation
- Preventing insecure network connections — Apple Developer Documentation
- Certificate and public key pinning — OWASP