Securing Mobile APIs: A Practical Guide to Modern Certificate Pinning
In the world of mobile security, the Man-in-the-Middle (MITM) attack remains a persistent and dangerous threat. An attacker on a compromised network—be it public Wi-Fi or a corporate environment—can intercept traffic between your mobile app and your backend services. Even with standard TLS, a compromised or malicious Certificate Authority (CA) could issue a fraudulent certificate for your domain, making the encrypted connection worthless.
This is where certificate pinning comes in. It’s a powerful security mechanism that allows a mobile app to reject connections to servers presenting an untrusted certificate, even if the device's operating system considers it valid. By "pinning" the expected public key or certificate to your app, you enforce a strict trust relationship, effectively cutting off MITM attacks that rely on fraudulent certificates.
However, the history of certificate pinning is fraught with tales of caution. Early implementations were brittle, leading to self-inflicted outages that "bricked" apps when certificates expired unexpectedly. The good news? The landscape has evolved. Modern pinning is no longer a fragile, high-risk endeavor. It’s a manageable, declarative, and essential layer in a defense-in-depth security strategy. This guide will show you how to implement it the right way.
The Old Way vs. The New Way: From Brittle to Agile Pinning
The fear surrounding certificate pinning stems from its original implementation: hardcoding certificate details directly into the app's source code.
The Anti-Pattern: Hardcoded Static Pinning
The old method involved taking a hash of a specific leaf certificate and embedding it as a string in the application binary. When the app made a network request, it would compare the server's certificate hash against this hardcoded value.
The Problem: This approach is incredibly fragile. A certificate has a finite lifespan. When it expires and you rotate it, the hash changes. Unless you had the foresight to release a new version of your app with the new hash before the old certificate expired, your app's networking would completely break. Every user who hadn't updated would be locked out, leading to a support nightmare and lost trust. This operational brittleness made static pinning an anti-pattern for most use cases.
The Modern Standard: Declarative and Managed Pinning
Today, both Android and iOS provide robust, declarative mechanisms for implementing pinning that live in configuration files, not compiled code. This separation of configuration from logic is the first major improvement.
The second, and most critical, evolution is the move towards dynamic pinning. While the initial pinning rules are bundled with the app, the app can be designed to periodically fetch an updated set of pins from a secure, separate configuration endpoint. This means you can rotate certificates, add new ones, or respond to a key compromise by simply updating a remote configuration file—no emergency app release required.
This modern approach transforms pinning from a high-stakes gamble into a sustainable security control, especially when paired with proactive certificate lifecycle management. A tool like Expiring.at becomes indispensable here, giving you a centralized dashboard to track all certificate expiration dates. This visibility allows you to schedule pin updates well in advance, turning a potential crisis into a routine maintenance task.
Practical Implementation: A Step-by-Step Guide
Let's walk through how to implement modern, declarative certificate pinning on both Android and iOS.
Step 1: Generate Your Public Key (SPKI) Hashes
First, a crucial best practice: pin the public key, not the full certificate. The Subject Public Key Info (SPKI) is a part of the certificate that contains the public key. By pinning the hash of the SPKI, you can renew your certificate with the same underlying key pair, and the pin will remain valid. This gives you flexibility without compromising security.
You also need a backup pin. Generate a second key pair and Certificate Signing Request (CSR) and store it securely offline. You'll generate a hash for this backup public key as well. If your primary key is ever compromised, you can have a CA issue a new certificate using your backup CSR, and your app will continue to function seamlessly.
You can use OpenSSL to generate the base64-encoded SHA-256 hash of a domain's SPKI.
To get the hash from a live domain:
openssl s_client -connect api.yourdomain.com:443 | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
To get the hash from a certificate file (e.g., yourdomain.pem
):
openssl x509 -in yourdomain.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
Run this command for both your primary and backup certificate keys. You will get two base64 strings as output. Save these.
Example output:
pin-sha256="khKIx2k8xtkFqIe4l2M6x2k8xtkFqIe4l2M6x2k8xtk="
(Primary)
pin-sha256="pL1+Jv6d3B1fBCd/s2v5f2g9hcMvxu3sSwv+x1PhmH8="
(Backup)
Step 2: Implementation on Android (via NetworkSecurityConfiguration.xml
)
Since Android 7.0 (Nougat), the recommended way to implement pinning is with a Network Security Configuration file. This is an XML file that lets you define network security policies declaratively.
-
Create the XML file:
In your project'sres/xml
directory, create a new file namednetwork_security_config.xml
. -
Add your pinning configuration:
Populate the file with your domain and the SPKI hashes you generated.xml <?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config> <domain includeSubdomains="true">api.yourdomain.com</domain> <pin-set> <!-- Primary Pin (SPKI of your server's current certificate) --> <pin digest="SHA-256">khKIx2k8xtkFqIe4l2M6x2k8xtkFqIe4l2M6x2k8xtk=</pin> <!-- Backup Pin (SPKI of your backup key) --> <pin digest="SHA-256">pL1+Jv6d3B1fBCd/s2v5f2g9hcMvxu3sSwv+x1PhmH8=</pin> </pin-set> </domain-config> </network-security-config>
-
Link the configuration in your manifest:
Finally, point to this file from yourAndroidManifest.xml
by adding theandroid:networkSecurityConfig
attribute to the<application>
tag.xml <?xml version="1.0" encoding="utf-8"?> <manifest ... > <application android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" ... > ... </application> </manifest>
That's it. The Android OS will now automatically enforce these pinning rules for all network connections made by your app to api.yourdomain.com
and its subdomains.
Step 3: Implementation on iOS (via Info.plist
)
iOS provides a similar declarative mechanism through the Info.plist
file, under the App Transport Security (ATS) settings.
-
Open your
Info.plist
file:
You can edit this as source code or using the Xcode property list editor. -
Add the
NSPinnedDomains
dictionary:
Inside the top-levelNSAppTransportSecurity
dictionary, add a new dictionary key calledNSPinnedDomains
. -
Configure your domain and pins:
InsideNSPinnedDomains
, create a new dictionary for your domain. Within it, specify your SPKI hashes.Here is the raw XML snippet for your
Info.plist
:xml <key>NSAppTransportSecurity</key> <dict> <key>NSPinnedDomains</key> <dict> <key>api.yourdomain.com</key> <dict> <!-- Include subdomains --> <key>NSIncludesSubdomains</key> <true/> <!-- Array of SPKI hashes --> <key>NSPinnedLeafIdentities</key> <array> <!-- Primary Pin --> <dict> <key>SPKI-SHA256-BASE64</key> <string>khKIx2k8xtkFqIe4l2M6x2k8xtkFqIe4l2M6x2k8xtk=</string> </dict> <!-- Backup Pin --> <dict> <key>SPKI-SHA256-BASE64</key> <string>pL1+Jv6d3B1fBCd/s2v5f2g9hcMvxu3sSwv+x1PhmH8=</string> </dict> </array> </dict> </dict> </dict>
Just like on Android, the operating system now handles the enforcement. If the app tries to connect to api.yourdomain.com
and the server's certificate public key doesn't match one of the pinned hashes, URLSession
will fail the connection.
Best Practices for a Bulletproof Pinning Strategy
Implementing the code is only half the battle. A successful pinning strategy relies on a solid operational process.
Pin the Intermediate or the Leaf? A Strategic Choice
While we've focused on pinning the leaf certificate's public key (the most secure option), you can also pin to an intermediate CA.
- Leaf Pinning: Maximum security. You are trusting only a specific key that you control. This is the recommended approach for most high-security applications.
- Intermediate Pinning: More operational