How do you implement certificate pinning, and how do you handle pin rotation without bricking your app?
Pinning stops a technically-valid-but-untrusted certificate from fooling your app — but get rotation wrong and you brick every install that hasn't updated.
LEARN BY DOINGPut your intuition to work3 steps
How do you implement certificate pinning, and how do you handle pin rotation without bricking your app?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why TLS validation alone isn't enough
Standard TLS already stops passive eavesdropping and most active tampering, because the OS validates the server's certificate chain against a trust store of a few hundred root CAs. The gap pinning closes is a different one: it protects you from a certificate that is technically valid but shouldn't be trusted for your API — a corporate MITM proxy with its own root CA pushed to managed devices, a compromised or coerced CA issuing a fraudulent cert for your domain, or a user who installed a "debugging" proxy CA (Charles, mitmproxy) that now silently intercepts every app on the device, not just the one they meant to debug. Pinning says: "I don't just want a trusted certificate for this host, I want this specific key."
There are two flavors, and the distinction is the whole ballgame for rotation. Certificate pinning pins the exact certificate (or its full hash) — it breaks the instant that certificate is renewed, even with the same key. Public key pinning pins the SHA-256 hash of the Subject Public Key Info (SPKI) instead — as long as the renewed certificate reuses the same key pair, the pin still matches. Every serious implementation (OkHttp's CertificatePinner, TrustKit, Apple's own guidance) pins SPKI hashes for exactly this reason: certificates expire on a schedule you don't fully control, especially with 90-day Let's Encrypt-style lifetimes becoming common, but key pairs can be reused across renewals.
#Implementing it on both platforms
On Android, OkHttp's CertificatePinner does the SPKI comparison for you — you just supply the hashes:
val certificatePinner = CertificatePinner.Builder().add("api.myapp.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // current key.add("api.myapp.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup key, generated but not yet deployed.build()val client = OkHttpClient.Builder().certificatePinner(certificatePinner).build()
You can pull the SPKI hash for any live endpoint straight from the command line:
openssl s_client -connect api.myapp.com:443 -servername api.myapp.com < /dev/null 2>/dev/null \| openssl x509 -pubkey -noout \| openssl pkey -pubin -outform der \| openssl dgst -sha256 -binary \| openssl enc -base64
iOS has no first-party pinning API as turnkey as OkHttp's, so most teams either use TrustKit or implement the URLSessionDelegate challenge callback directly:
final class PinningDelegate: NSObject, URLSessionDelegate {private let pinnedHashes: Set<String> = ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // current key"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" // backup key]func urlSession(_ session: URLSession,didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {guard let trust = challenge.protectionSpace.serverTrust,let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],let leaf = chain.first,let publicKey = SecCertificateCopyKey(leaf),let keyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {completionHandler(.cancelAuthenticationChallenge, nil)return}let hash = Data(SHA256.hash(data: keyData)).base64EncodedString()if pinnedHashes.contains(hash) {completionHandler(.useCredential, URLCredential(trust: trust))} else {completionHandler(.cancelAuthenticationChallenge, nil)}}}
#Rotating pins without a fire drill
The fix isn't "don't pin," it's treating rotation as a first-class part of the design from day one:
- Pin the public key, not the leaf certificate, so a routine renewal that reuses the key pair is a non-event.
- Ship a backup pin before you need it. Generate a second key pair, pin its SPKI hash alongside the active one, and keep the private key offline until the day you actually rotate. The app that's already in users' hands then already trusts the next key before the server ever presents it.
- Give yourself a remote kill switch. A remotely-fetched config (over a connection that itself doesn't hard-fail on pin mismatch, or fetched from a separate, unpinned endpoint used only for this) that can flip pinning to report-only lets you recover from a bad rotation without an emergency app release.
- Set pin-set expiry. Ship pins with a "stop enforcing after this date" fallback so an app version nobody ever updates degrades to standard TLS trust instead of permanently failing — a deliberate trade of some security margin for availability on your long tail of unupdated installs.
- Treat rotation as a cross-team contract. The backend/infra team rotating a TLS certificate needs to know mobile pins depend on it, with enough lead time to ship the backup pin, get it through app review, and let adoption climb before the old key is retired.