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

Need Expert Cybersecurity Guidance?

Our team of security experts is ready to help protect your business from evolving threats.