Certificate Pinning Implementation for Mobile Apps
Apps are intercepted via Charles Proxy or Frida — reading requests, modifying responses, studying the API. This happens because TLS connections validate certificates against system CAs, and the user (or pentester) simply added their own CA as trusted. Over the last 5 years, we implemented Certificate Pinning for 30+ mobile projects — from fintech startups to enterprise solutions. This completely closes the MITM attack vector on non-rooted devices. We configure pinning on iOS and Android, including backup pins and CI checks. Timelines range from 2 to 5 days depending on the number of hosts. Want to evaluate your project? Simply contact us.
Why public key pinning is better than certificate pinning
Certificate pinning — the client stores the full certificate (or its SHA-256 hash) and compares it with what the server sends. The certificate changes on every renewal — every 1-2 years. If you forget to update the pin before rotation, the app stops working for all users simultaneously.
Public key pinning (HPKP-style) — pins the hash of SubjectPublicKeyInfo. When the certificate is renewed, the private key often stays the same, so the pin remains valid. This is the right choice for production. Additionally, maintain a backup pin — hash of a backup key (could be from your CA). Let's compare the approaches:
| Characteristic | Certificate Pinning | Public Key Pinning |
|---|---|---|
| Client update frequency | Every 1-2 years | Every few years |
| Risk of blocking users | High | Low |
| Implementation complexity | Low | Medium |
| Flexibility when changing CA | Requires update | Often not required |
How we implement pinning: step-by-step process
| Stage | Description | Duration |
|---|---|---|
| Infrastructure analysis | Identify all hosts, obtain certificates and public keys | 1-2 hours |
| Hash generation | Compute SHA-256 SPKI for each key, including backup pin | 30 minutes |
| Pinning implementation | Embed validation in the network layer (TrustKit, OkHttp, Flutter) | 1-2 days |
| CI setup | Step to verify that release build includes pinning | 1-2 hours |
| Testing | Verify blocking via proxy, test backup pin | 1 day |
| Documentation | Rotation procedure and emergency update contacts | 1 hour |
iOS: NSURLSession + TrustKit
func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust else { completionHandler(.cancelAuthenticationChallenge, nil) return } guard let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0), let serverKey = SecCertificateCopyKey(serverCert) else { completionHandler(.cancelAuthenticationChallenge, nil) return } let serverKeyData = SecKeyCopyExternalRepresentation(serverKey, nil)! as Data let serverKeyHash = sha256(data: serverKeyData) let pinnedHashes = ["base64encodedSHA256ofSubjectPublicKeyInfo=="] if pinnedHashes.contains(serverKeyHash) { completionHandler(.useCredential, URLCredential(trust: serverTrust)) } else { completionHandler(.cancelAuthenticationChallenge, nil) } } For projects with multiple hosts, use TrustKit — configured via Info.plist, supports backup pins and violation reporting.
Android: OkHttp CertificatePinner
val certificatePinner = CertificatePinner.Builder() .add("api.example.com", "sha256/AAAAAAAAA...primaryKeyHash==") .add("api.example.com", "sha256/BBBBBBBBB...backupKeyHash==") .build() val client = OkHttpClient.Builder() .certificatePinner(certificatePinner) .build() OkHttp automatically computes SHA-256 of SubjectPublicKeyInfo. On mismatch — SSLPeerUnverifiedException. Get hashes via openssl:
openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \ | openssl x509 -pubkey -noout \ | openssl pkey -pubin -outform der \ | openssl dgst -sha256 -binary \ | openssl enc -base64 For Flutter, use the http_certificate_pinning package or a native wrapper via platform channels.
What are the risks of incorrect implementation?
The main operational pain — key rotation. 30+ days before certificate renewal, release an update with a new backup pin, let users update, then rotate the key. If this is not done, the app loses connectivity to the server for all clients. In debug builds, pinning is usually disabled via a BuildConfig.DEBUG flag or a separate flavor. Ensure that the release build in CI is built with pinning — otherwise you lose protection.
Common implementation mistakes
- Using only one pin — if that key is compromised, emergency update is needed.
- Storing pins in plain text in the binary — an attacker can extract them via static analysis. Use obfuscation or encryption.
- No fallback to certificate — if all pins don't match, the app should block the connection, not ignore the error.
- Key rotation without prior client update — the most common cause of mass outages.
What's included in our work
- Pinning configuration for all app hosts (iOS + Android, up to 5 hosts included).
- Backup pin — add hash of a backup key for seamless rotation.
- CI integration — configure flags to separate debug/release builds.
- Documentation — description of the pin update procedure and emergency release contacts.
- Support — for 30 days after implementation, we answer questions and help with the first rotation.
Timelines
Pinning setup for one host with backup pins — 2-3 days. For projects with 3+ hosts — up to 5 days. Pricing is calculated individually, depending on complexity and number of platforms.
Get a consultation: contact us, and we'll estimate timelines for your project. Write to us — we'll evaluate your project in one day.







