The Modern Guide to Mobile App Certificate Pinning: Security Without the Self-Inflicted Outage

Certificate pinning is one of the most powerful security controls you can implement in a mobile application. It’s a direct defense against sophisticated Man-in-the-Middle (MITM) attacks, where an atta...

Tim Henrich
November 03, 2025
7 min read
540 views

The Modern Guide to Mobile App Certificate Pinning: Security Without the Self-Inflicted Outage

Certificate pinning is one of the most powerful security controls you can implement in a mobile application. It’s a direct defense against sophisticated Man-in-the-Middle (MITM) attacks, where an attacker intercepts traffic by presenting a fraudulent-yet-trusted TLS certificate. By hardcoding the expected certificate or public key within your app, you tell it, "Only trust this specific identity, regardless of what the device's trusted Certificate Authorities say."

For years, however, developers have approached pinning with trepidation. A minor operational mistake—letting a pinned certificate expire, for instance—could lock every user out of your application, creating a self-inflicted denial-of-service attack that can only be fixed with a new app store release.

The good news is that the conversation has evolved. The question in 2024 is no longer if you should pin, but how you can do so in a resilient, manageable way. Modern pinning is not about creating a brittle, static link to a single certificate. It's about building a flexible and robust chain of trust that you control, without creating an operational nightmare.

This guide will walk you through the modern best practices for implementing certificate pinning, ensuring you can secure your app's communications without risking its availability.

Why Standard TLS Validation Isn't Always Enough

Before diving into the implementation, it's crucial to understand why pinning is necessary. Standard TLS validation works by checking a certificate's signature against a chain of trust that ends in a root Certificate Authority (CA) trusted by the operating system. Your phone trusts hundreds of these CAs from around the world.

This system works well, but it has two potential weaknesses that pinning directly addresses:

  1. Compromised Certificate Authority: If any one of the hundreds of trusted CAs is compromised or coerced, an attacker can issue a valid certificate for your domain (e.g., api.yourapp.com). To your app and your users, this certificate looks completely legitimate, allowing the attacker to intercept, read, and modify all API traffic.
  2. Malicious Local CAs: Corporate proxies, firewalls, and even user-installed malware can add new root CAs to a device's trust store. This is often done for legitimate reasons (like corporate content filtering), but it creates an attack vector where a malicious actor can present their own certificate to your app, which the device will happily trust.

Certificate pinning short-circuits this entire process. It enforces a stricter policy: the app will only communicate with a server if its public key or certificate matches a pre-defined, "pinned" value baked into the app itself. The OWASP Mobile Application Security Verification Standard (MASVS) classifies pinning as a Level 2 control, recommended for any application that handles sensitive data.

The Cardinal Rules of Resilient Pinning

The horror stories of bricked apps all stem from a single, outdated practice: pinning the leaf certificate directly. With the rise of automated certificate management and short-lived certificates from services like Let's Encrypt, certificates are often renewed every 60-90 days. If you pin a certificate that expires in two months, your app will stop working in two months unless you force every single user to update.

To avoid this, follow these two cardinal rules.

Rule #1: Pin Public Keys, Not Certificates

A certificate is essentially a signed wrapper around a public key. The certificate itself changes upon every renewal (it has a new serial number, new dates, etc.), but the underlying public key pair can remain the same. By pinning the public key, you decouple your app's security from the certificate's lifecycle.

The standard format for this is the hash of the SubjectPublicKeyInfo (SPKI). You can generate this hash directly from your domain using OpenSSL.

How to Generate an SPKI Hash:

Run the following command in your terminal, replacing api.yourapp.com with your server's hostname:

openssl s_client -connect api.yourapp.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl base64

This command connects to your server, extracts the public key from its certificate, converts it to the correct format (DER), hashes it with SHA-256, and finally Base64-encodes it. The output is the string you will use as your pin.

Rule #2: Always Include a Backup Pin

What happens if your primary key is compromised and you need to revoke it? Or what if your company policy requires a full key rotation every year? If you've only pinned your current public key, you're back in the same trap: you must force an app update.

The solution is to always include at least one backup pin in your app. This pin corresponds to a second key pair that you have generated and stored securely offline. It is not yet active on your servers.

Your operational workflow then becomes:
1. Normal Operation: Your server uses the primary key. Your app validates connections against the primary pin.
2. Planned Rotation or Emergency: You generate a new certificate using the backup key pair.
3. Deployment: You deploy the new certificate to your servers.
4. Seamless Transition: Your mobile app, which already trusts the backup pin, seamlessly connects to the server with the new certificate. No app update is required.
5. Update Pins: In your next scheduled app release, you can introduce a new backup pin and phase out the old primary pin.

Implementation Guide: Pinning on Android and iOS

Leveraging platform-native tools and mature libraries is the most reliable way to implement pinning.

Android: Network Security Configuration

Since Android 7.0 (API 24), the recommended approach is to use a declarative Network Security Configuration file. This method is simple, robust, and doesn't require writing any custom code.

Step 1: Generate Your Pins

First, generate the SPKI hash for your current (primary) key and your backup key.

# Generate primary pin
openssl s_client -connect api.yourapp.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl base64
# Output: pIN+o7flUPpE0nQ4aD1D1dM9rAod2B+4vJv5mZQd3o0=

# Generate backup pin from a backup key file (e.g., backup_public_key.pem)
openssl pkey -in backup_public_key.pem -pubout -outform der | openssl dgst -sha256 -binary | openssl base64
# Output: yYOgE0gVQAyJgV3a5b2fzzVHYiL1gUnxSLxo2zI3zNo=

Step 2: Create the Configuration File

Create a new XML file at res/xml/network_security_config.xml.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">api.yourapp.com</domain>
        <pin-set expiration="2025-09-01">
            <!-- Current primary key pin -->
            <pin digest="SHA-256">pIN+o7flUPpE0nQ4aD1D1dM9rAod2B+4vJv5mZQd3o0=</pin>
            <!-- Backup key pin -->
            <pin digest="SHA-256">yYOgE0gVQAyJgV3a5b2fzzVHYiL1gUnxSLxo2zI3zNo=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

The expiration attribute is a safety feature. If the date passes, the pinning is disabled. This prevents your app from being permanently bricked if you forget to update the pins, but it should be seen as a last resort. Proactive monitoring is always better.

Step 3: Link the Configuration in Your Manifest

Finally, reference this file in your AndroidManifest.xml:

<application
    ...
    android:networkSecurityConfig="@xml/network_security_config">
    ...
</application>

That's it. The Android OS will now enforce these pins for all network connections made to api.yourapp.com.

iOS: Using TrustKit for Robust Pinning

While iOS offers some basic pinning capabilities in Info.plist, it's less flexible than Android's solution. For a robust implementation with backup pins and violation reporting, the open-source library TrustKit is the industry standard.

Step 1: Install TrustKit

Add TrustKit to your project using Swift Package Manager or CocoaPods.

Step 2: Configure TrustKit in Info.plist

You configure the pinning policy directly in your app's Info.plist file. This is where you'll define your domains, pins, and other settings.

<key>TrustKit</key>
<dict>
    <key>TSKSwizzleNetworkDelegates</key>
    <true/>
    <key>TSKPinnedDomains</key>
    <dict>
        <key>api.yourapp.com</key>
        <dict>
            <key>TSKIncludeSubdomains</key>
            <true/>
            <key>TSKPublicKeyHashes</key>
            <array>
                <!-- Current primary key pin -->
                <string>pIN+o7flUPpE0nQ4aD1D1dM9rAod2B+4vJv5mZQd3o0=</string>
                <!-- Backup key pin -->
                <string>yYOgE0gVQAyJgV3a5b2fzzVHYiL1gUnxSLxo2zI3zNo=</string>
            </array>
            <key>TSKReportUris</key>
            <array>
                <string>https://report.yourapp.com/pinning-violation</string>
            </array>
        </dict>
    </dict>
</dict>

This configuration tells TrustKit to:
* Enforce pinning for api.yourapp.com and its subdomains.
* Trust the two specified SPKI hashes (primary and backup).
* Send a report to the specified URI if a pinning validation fails. This is an invaluable feature for detecting potential attacks in the wild.

Step 3: Initialize TrustKit

In your `AppDelegate.swift

Share This Insight

Related Posts