Why TLS alone is not enough

TLS encrypts the connection between your app and server, but it relies on a chain of trust: the device trusts a set of Certificate Authorities (CAs), and any of those CAs can issue a certificate for your domain. If an attacker installs a rogue CA certificate on the device—through social engineering, malware, or a compromised enterprise MDM profile—they can generate valid-looking certificates for your API domain and intercept all traffic.

This is not theoretical. We have demonstrated this attack in every mobile penetration test we have conducted where pinning was absent. The setup takes minutes with tools like mitmproxy or Burp Suite. Once the proxy is in place, the attacker sees credentials, session tokens, transaction payloads, and KYC data in plaintext.

Certificate pinning solves this by hardcoding the expected certificate or public key into the app. Even if the device trusts a rogue CA, the app refuses to connect unless the server presents the exact certificate or key it expects.

What to pin: certificates vs public keys

You can pin the full leaf certificate, the intermediate CA certificate, or the Subject Public Key Info (SPKI) hash. Each has trade-offs:

Leaf certificate pinning

Most specific. Breaks when the certificate renews (typically every 90 days with Let's Encrypt). Requires app update to rotate. High maintenance, high risk of bricking.

SPKI hash pinning

Pins the public key hash. Survives certificate renewals if the same key pair is reused. Recommended approach for most fintech apps. Balance of security and operational stability.

Intermediate CA pinning

Pins the CA that signed your certificate. Less specific but more resilient to rotation. Weaker security—any certificate from that CA would be accepted. Use only as a backup pin.

Our recommendation: pin the SPKI hash of your leaf certificate's public key as the primary pin, and include the SPKI hash of a backup key (generated but not yet deployed) as a secondary pin. This gives you a rotation path without app updates.

Android implementation

Network Security Config (declarative)

Android's built-in approach is the network_security_config.xml file. This is the simplest method and works across all HTTP libraries that use the platform's TLS stack:

Create res/xml/network_security_config.xml with your domain, the SPKI pin digests (Base64-encoded SHA-256 of the public key), and an expiration date. Reference it in your AndroidManifest.xml with android:networkSecurityConfig="@xml/network_security_config". Always include at least two pins—a primary and a backup—to prevent bricking if your primary key is compromised.

OkHttp (programmatic)

If you need more control (dynamic pin updates, custom failure handling), OkHttp's CertificatePinner API lets you pin programmatically. Build a CertificatePinner instance with your domain and SPKI hashes, then attach it to the OkHttp client. This approach works for apps using Retrofit or any OkHttp-based networking.

Critical Mistake

Pinning in debug but not in release

We regularly find Android apps where certificate pinning is configured in the network security config but wrapped in a debug-only override that disables it in production—or the opposite: debug builds override pinning (which is fine) but the logic accidentally carries into release builds. Always verify pinning is active in your release APK by testing with an intercepting proxy before submission.

iOS implementation

App Transport Security (ATS)

iOS enforces TLS 1.2+ by default through ATS, but ATS does not provide certificate pinning out of the box. It prevents downgrade attacks and cleartext traffic, but you still need to implement pinning separately.

URLSession delegate

Implement the URLSessionDelegate method urlSession(_:didReceive:completionHandler:) to inspect the server's certificate chain. Extract the public key from the server certificate, compute its SPKI hash, and compare it against your pinned hashes. If no match, cancel the connection with .cancelAuthenticationChallenge.

TrustKit (library)

For a battle-tested implementation, TrustKit provides declarative pinning via Info.plist configuration. It supports pin reporting (sending failure reports to your server when pinning fails), backup pins, and expiration dates. TrustKit swizzles URLSession at runtime, so pinning is enforced globally without modifying individual network calls.

Pin rotation strategy

The biggest operational risk with certificate pinning is bricking your app when certificates rotate. Here is the strategy we recommend to every digital banking client:

Need to verify your certificate pinning implementation? Our mobile pentest includes Frida-based pin bypass testing to confirm your defences hold under real attack conditions.

Test Your Pinning Implementation

Testing with Frida

During our mobile app pentests, we use Frida to bypass certificate pinning and verify whether the implementation is resilient. Frida scripts can hook into the TLS validation functions at runtime and force them to accept any certificate. If your pinning can be bypassed with a generic Frida script, it is not strong enough.

Defences against Frida-based bypass: implement root/jailbreak detection to refuse to run on compromised devices, use integrity checks to detect Frida's presence (Frida injects a library that can be detected), and consider commercial solutions like Guardsquare or Appdome that provide runtime application self-protection (RASP).

Certificate pinning is not glamorous, but it is the difference between an app that protects users on hostile networks and one that gives attackers a front-row seat to every transaction. Implement it, test it, and maintain it.

Related reading

Blog: Securing Flutter & React Native Apps · iOS vs Android Security · Mobile App Pentest Checklist

Guides: Mobile App Pentest Nigeria · OWASP for Fintech · Pentest Tools & Methodology

Services: Penetration Testing · Authentication Security · API Security

Frequently asked questions

{faqs.map((faq) => (
{faq.q}

{faq.a}

))}