Home/Blog/Cybersecurity/Certificate Pinning: Implementation Guide for Mobile and Web Apps
Cybersecurity

Certificate Pinning: Implementation Guide for Mobile and Web Apps

Learn how to implement certificate pinning in mobile and web applications to prevent MITM attacks. Covers iOS, Android, and modern alternatives to deprecated HPKP.

By Inventive HQ Team
Certificate Pinning: Implementation Guide for Mobile and Web Apps

Certificate pinning adds a critical layer of protection against man-in-the-middle attacks by ensuring your application only trusts specific certificates, not just any certificate from a trusted CA. This guide covers implementation for mobile and web applications.

Understanding Certificate Pinning

┌─────────────────────────────────────────────────────────────────────────┐
│                    STANDARD TLS VALIDATION                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌────────┐    certificate    ┌────────┐    validate    ┌────────────┐ │
│  │ Server │ ─────────────────►│  App   │ ──────────────►│ Trust Store│ │
│  └────────┘                   └────────┘                │ (100+ CAs) │ │
│                                                          └────────────┘ │
│  Problem: Any of 100+ CAs can issue valid certs for your domain        │
│  Risk: Compromised CA = MITM attack with valid certificate             │
│                                                                          │
├─────────────────────────────────────────────────────────────────────────┤
│                    WITH CERTIFICATE PINNING                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌────────┐    certificate    ┌────────┐    validate    ┌────────────┐ │
│  │ Server │ ─────────────────►│  App   │ ──────────────►│ Pinned Keys│ │
│  └────────┘                   └────────┘    + match     │ (1-3 keys) │ │
│                                    │                     └────────────┘ │
│                                    │                                     │
│                               ┌────▼────┐                               │
│                               │ Reject  │ ← If cert doesn't match pins │
│                               └─────────┘                               │
│                                                                          │
│  Solution: Only accept specific certificates/keys you control           │
│  Benefit: Rogue CA certificates are rejected                            │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

What to Pin

TargetSecurityFlexibilityRecommendation
Leaf CertificateHighestLowestRequires app update on every cert renewal
Intermediate CAHighMediumRecommended - survives leaf rotation
Root CALowHighestToo broad - trusts all certs from CA
Public Key (SPKI)HighHighBest - survives cert renewal if key reused

Extracting Pin Hashes

Get Public Key Hash (SPKI)

# From a live server
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null | \
  openssl x509 -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  base64

# Output: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

# From a certificate file
openssl x509 -in certificate.crt -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  base64

Get Intermediate CA Hash

# View the certificate chain
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null

# Extract intermediate (certificate 1, not 0)
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null | \
  awk '/-----BEGIN CERTIFICATE-----/{i++}i==2' | \
  openssl x509 -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  base64

iOS Implementation

Native URLSession

import Foundation
import Security
import CryptoKit

class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {

    // SHA-256 hashes of pinned public keys (SPKI)
    private let pinnedPublicKeyHashes: Set<String> = [
        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",  // Primary
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="   // Backup
    ]

    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
        }

        // Validate certificate chain
        let policies = [SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString)]
        SecTrustSetPolicies(serverTrust, policies as CFArray)

        var error: CFError?
        guard SecTrustEvaluateWithError(serverTrust, &error) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // Check if any certificate in chain matches our pins
        let certificateCount = SecTrustGetCertificateCount(serverTrust)

        for i in 0..<certificateCount {
            guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, i),
                  let publicKey = SecCertificateCopyKey(certificate),
                  let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
                continue
            }

            // Hash the public key
            let hash = SHA256.hash(data: publicKeyData)
            let hashBase64 = Data(hash).base64EncodedString()

            if pinnedPublicKeyHashes.contains(hashBase64) {
                completionHandler(.useCredential, URLCredential(trust: serverTrust))
                return
            }
        }

        // No pin matched - reject connection
        print("Certificate pinning failed for \(challenge.protectionSpace.host)")
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

// Usage
let delegate = PinnedURLSessionDelegate()
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)

let task = session.dataTask(with: URL(string: "https://api.example.com/data")!) { data, response, error in
    if let error = error {
        print("Request failed: \(error)")
        return
    }
    // Handle response
}
task.resume()
// In AppDelegate or early initialization
import TrustKit

func configureTrustKit() {
    let trustKitConfig: [String: Any] = [
        kTSKSwizzleNetworkDelegates: true,
        kTSKPinnedDomains: [
            "api.example.com": [
                kTSKEnforcePinning: true,
                kTSKPublicKeyHashes: [
                    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
                    "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
                ],
                kTSKIncludeSubdomains: true,
                kTSKReportUris: ["https://report.example.com/pinning"]
            ]
        ]
    ]

    TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
}

Android Implementation

Network Security Config (Android 7+)

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2025-12-31">
            <!-- Primary pin -->
            <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <!-- Backup pin (REQUIRED) -->
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
        <trust-anchors>
            <certificates src="system"/>
        </trust-anchors>
    </domain-config>
</network-security-config>
<!-- AndroidManifest.xml -->
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

OkHttp CertificatePinner

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

val certificatePinner = CertificatePinner.Builder()
    .add(
        "api.example.com",
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
    )
    .add(
        "*.example.com",  // Wildcard support
        "sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC="
    )
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

// Usage with Retrofit
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

React Native (with Fetch)

// Using react-native-ssl-pinning
import { fetch } from 'react-native-ssl-pinning';

const response = await fetch('https://api.example.com/data', {
  method: 'GET',
  timeoutInterval: 10000,
  sslPinning: {
    certs: ['cert1', 'cert2']  // Certificate files in assets
  }
});

// Or with public key hashes
const response = await fetch('https://api.example.com/data', {
  method: 'GET',
  pkPinning: true,
  sslPinning: {
    certs: [
      'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
      'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB='
    ]
  }
});

Web Applications

HPKP Deprecation and Alternatives

HPKP was deprecated due to risks. Modern alternatives:

┌─────────────────────────────────────────────────────────────────┐
│                  HPKP (Deprecated 2018)                         │
├─────────────────────────────────────────────────────────────────┤
│  Public-Key-Pins: pin-sha256="..."; max-age=5184000            │
│                                                                 │
│  Risks:                                                         │
│  • "HPKP Suicide" - lose keys = site becomes inaccessible      │
│  • "RansomPKP" - attacker pins their keys via XSS              │
│  • Complex rotation requiring careful planning                  │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│              MODERN ALTERNATIVES                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Certificate Transparency (CT) Monitoring                    │
│     - Monitor crt.sh or CT logs for unauthorized certs         │
│     - Alert on certificates you didn't request                 │
│                                                                 │
│  2. Expect-CT Header (Report-Only)                              │
│     Expect-CT: max-age=86400, report-uri="https://..."         │
│     - Requires CT compliance, reports violations               │
│                                                                 │
│  3. CAA DNS Records                                             │
│     example.com. CAA 0 issue "letsencrypt.org"                 │
│     - Restricts which CAs can issue for your domain            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

CAA Records (DNS-Based Pinning)

; Only Let's Encrypt can issue certificates
example.com.    CAA 0 issue "letsencrypt.org"
example.com.    CAA 0 issuewild "letsencrypt.org"

; Report violations
example.com.    CAA 0 iodef "mailto:[email protected]"

Certificate Transparency Monitoring

# Query CT logs for your domain
curl "https://crt.sh/?q=%.example.com&output=json" | jq '.[].id'

# Set up monitoring with certspotter
pip install certspotter
certspotter --domain example.com --webhook https://alerts.example.com/ct

Pin Rotation Strategy

┌─────────────────────────────────────────────────────────────────┐
│                    PIN ROTATION TIMELINE                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Month 1          Month 2          Month 3          Month 4     │
│  ────────────────────────────────────────────────────────────── │
│                                                                  │
│  [Pin A: Active]   [Pin A: Active]  [Pin A: Removed]            │
│  [Pin B: Backup]   [Pin B: Active]  [Pin B: Active]             │
│                    [Pin C: Added]   [Pin C: Backup]             │
│                                                                  │
│  Steps:                                                          │
│  1. Always have 2+ pins (active + backup)                       │
│  2. Add new backup pin BEFORE removing old pin                  │
│  3. Wait for app update propagation (2-4 weeks)                 │
│  4. Rotate active pin to use new certificate                    │
│  5. Remove old pin only after full propagation                  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Automated Pin Rotation Check

#!/usr/bin/env python3
"""Check if pinned certificates are expiring soon."""

import ssl
import socket
from datetime import datetime, timedelta
import hashlib
import base64

def get_cert_pins(hostname, port=443):
    """Extract public key pins from server certificate chain."""
    context = ssl.create_default_context()
    pins = []

    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert_chain = ssock.getpeercert(binary_form=True)
            # Note: This gets leaf cert only; for full chain, use different approach

            cert = ssl.DER_cert_to_PEM_cert(cert_chain)
            # Extract and hash public key
            # (Simplified - use cryptography library for production)

    return pins

def check_pin_expiration(pins, warning_days=30):
    """Check if any pinned certificates are expiring soon."""
    alerts = []
    today = datetime.now()

    for pin in pins:
        days_until_expiry = (pin['expiration'] - today).days
        if days_until_expiry < warning_days:
            alerts.append({
                'pin': pin['hash'],
                'expires': pin['expiration'],
                'days_remaining': days_until_expiry
            })

    return alerts

# Check and alert
alerts = check_pin_expiration([
    {'hash': 'AAAA...', 'expiration': datetime(2025, 6, 1)},
    {'hash': 'BBBB...', 'expiration': datetime(2025, 12, 1)}
])

for alert in alerts:
    print(f"WARNING: Pin {alert['pin'][:20]}... expires in {alert['days_remaining']} days")

Emergency Bypass Mechanism

Include a kill switch for emergencies:

// iOS - Remote config based bypass
class PinningConfiguration {
    static let shared = PinningConfiguration()

    var isPinningEnabled: Bool {
        // Check remote config
        return RemoteConfig.shared.getBool("pinning_enabled", default: true)
    }

    var pinnedHashes: [String] {
        // Allow remote pin updates for emergencies
        if let remotePins = RemoteConfig.shared.getStringArray("pinned_keys") {
            return remotePins
        }
        return defaultPins
    }

    private let defaultPins = [
        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
    ]
}
// Android - Remote config based bypass
object PinningConfig {
    private val remoteConfig = FirebaseRemoteConfig.getInstance()

    val isPinningEnabled: Boolean
        get() = remoteConfig.getBoolean("pinning_enabled")

    val pins: List<String>
        get() {
            val remotePins = remoteConfig.getString("pinned_keys")
            return if (remotePins.isNotEmpty()) {
                remotePins.split(",")
            } else {
                defaultPins
            }
        }

    private val defaultPins = listOf(
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
    )
}

Testing Certificate Pinning

Verify Pinning Works

# Use mitmproxy or Charles Proxy to intercept traffic
# If pinning is working, the app should refuse to connect

# Android: Install proxy CA as user cert (won't be trusted for pinned domains)
# iOS: Trust proxy CA in Settings > General > About > Certificate Trust Settings

Debugging Pin Failures

// iOS - Detailed logging
func urlSession(_ session: URLSession,
                didReceive challenge: URLAuthenticationChallenge,
                completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

    guard let serverTrust = challenge.protectionSpace.serverTrust else {
        print("No server trust available")
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    let count = SecTrustGetCertificateCount(serverTrust)
    print("Certificate chain contains \(count) certificates")

    for i in 0..<count {
        if let cert = SecTrustGetCertificateAtIndex(serverTrust, i) {
            let summary = SecCertificateCopySubjectSummary(cert) as String? ?? "Unknown"
            print("Certificate \(i): \(summary)")

            if let publicKey = SecCertificateCopyKey(cert),
               let data = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? {
                let hash = SHA256.hash(data: data)
                print("  Public key hash: \(Data(hash).base64EncodedString())")
            }
        }
    }

    // Continue with validation...
}

Best Practices

  1. Always include backup pins - At least 2 pins to survive rotation
  2. Pin intermediate CA - Better balance of security and flexibility
  3. Use public key pinning - Survives certificate renewal with same key
  4. Implement kill switch - Remote config to disable in emergencies
  5. Monitor expiration - Alert well before pinned certs expire
  6. Test thoroughly - Use proxy tools to verify pinning works
  7. Plan rotation - Document pin rotation procedure
  8. Report failures - Log pinning failures for security monitoring
  9. Consider CT - Certificate Transparency as complementary protection
  10. Use CAA records - Restrict which CAs can issue for your domain

Next Steps

Frequently Asked Questions

Find answers to common questions

Certificate pinning is a security technique where an application only accepts specific certificates or public keys for a server, rather than trusting any certificate signed by a trusted CA. This prevents man-in-the-middle (MITM) attacks even when an attacker has obtained a valid certificate from a compromised or rogue CA. It's especially important for mobile apps handling sensitive data like banking, healthcare, or authentication.

Certificate pinning validates the entire certificate (including expiration date), requiring app updates when certificates rotate. Public key pinning validates only the public key, which typically remains constant across certificate renewals if you reuse the same key pair. Public key pinning is generally preferred as it's more resilient to certificate rotation. You can also pin intermediate CA keys for flexibility.

HPKP was deprecated in Chrome 72 (2018) due to serious risks including "HPKP Suicide" (losing access to your own site if you lose pinned keys), "RansomPKP" attacks (attackers pinning their own keys), and the complexity of managing pin rotation. Modern alternatives include Certificate Transparency monitoring, Expect-CT headers, and application-level pinning for mobile apps where you control the client.

In iOS, use URLSession's delegate method urlSession(_:didReceive:completionHandler:) to validate the server's certificate chain. Extract the public key from SecTrust, compare its SHA-256 hash against your pinned hashes, and call completionHandler with .cancelAuthenticationChallenge if it doesn't match. Libraries like TrustKit and Alamofire simplify this with declarative configuration.

Android supports native pinning via network_security_config.xml (Android 7+). Define pins using SHA-256 hashes of public keys: base64hash==. Include backup pins for rotation. For older Android versions or more control, use OkHttp's CertificatePinner class. Always include at least one backup pin to avoid lockouts during rotation.

Risks include app lockout (users can't connect if pins are wrong or certificates rotate unexpectedly), difficult debugging (pinning hides the real certificate from inspection tools), emergency recovery challenges (can't quickly bypass pinning if something breaks), and update dependency (requiring app store approval to fix pin issues). Always include backup pins and have a bypass mechanism for emergencies.

Use OpenSSL to extract the public key and hash it: openssl s_client -connect domain.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | base64. This outputs the base64-encoded SHA-256 hash of the Subject Public Key Info (SPKI). Pin this value in your app configuration.

Pin the intermediate CA certificate for the best balance of security and flexibility. Leaf pinning is most secure but requires app updates on every certificate renewal. Root CA pinning is too broad (trusts all certificates from that CA). Intermediate pinning lets you rotate leaf certificates without app updates while still limiting trust to certificates from your specific CA chain.

Never silently ignore pinning failures—this defeats the security purpose. Log failures with details (expected vs received pins) for debugging. Show users a clear error explaining the connection isn't secure. Implement a kill switch (remote config flag) to disable pinning in emergencies without requiring app updates. Consider pinning in report-only mode during initial rollout.

Certificate Transparency (CT) is a public log system where CAs must record all issued certificates. Instead of pinning specific certificates, you can monitor CT logs for unauthorized certificates issued for your domain. CT provides similar MITM protection without the operational risks of pinning. Use Expect-CT headers to require CT compliance, and set up CT log monitoring to alert on suspicious certificates.

Don't wait for a breach to act

Get a free security assessment. Our experts will identify your vulnerabilities and create a protection plan tailored to your business.