DEV Community

ArshTechPro
ArshTechPro

Posted on

Mastering SSL Pinning in iOS: From Basics to Production

Why SSL Pinning is Required

Imagine you're having a private conversation in a coffee shop. You trust the person you're talking to, but what if someone nearby is pretending to be them? That's essentially what can happen with your app's network communication.

The Problem with Standard SSL/TLS

When your iOS app communicates with a server, it uses SSL/TLS encryption. By default, iOS validates the server's certificate against trusted Certificate Authorities (CAs) stored on the device. This works well, but it has vulnerabilities:

1. Man-in-the-Middle (MITM) Attacks

  • Attackers can install malicious certificates on a device (especially on jailbroken devices or compromised networks)
  • They can intercept and read your app's "secure" communication
  • Your sensitive data (passwords, personal info, financial data) becomes exposed

2. Compromised Certificate Authorities

  • If a CA is hacked or issues fraudulent certificates, attackers can impersonate your server
  • Your app would trust these fake certificates because they come from a "trusted" CA

3. Corporate/Public WiFi Networks

  • Some networks use SSL inspection tools that decrypt traffic
  • This breaks the security chain your app relies on

The Solution: SSL Pinning

SSL Pinning is like giving your app a photo of the person it should trust. Instead of trusting any certificate signed by a CA, your app only trusts specific certificates or public keys that you've hardcoded into it.

Think of it as:

  • Normal SSL: "I'll talk to anyone with a valid driver's license"
  • SSL Pinning: "I'll only talk to John Smith with driver's license #123456"

How SSL Pinning Works in iOS

There are two main approaches to SSL pinning:

1. Certificate Pinning

Pin the entire SSL certificate of your server in your app.

Pros: Very secure
Cons: Must update your app whenever the certificate expires (typically yearly)

2. Public Key Pinning (Recommended)

Pin only the public key from your server's certificate.

Pros: More flexible - you can rotate certificates without updating the app as long as you keep the same key pair
Cons: Slightly more complex to implement


Implementation in Swift

Let me show you how to implement SSL pinning step by step.

Step 1: Extract Your Server's Certificate

First, get your server's certificate. Open Terminal and run:

openssl s_client -connect yourserver.com:443 -showcerts < /dev/null | openssl x509 -outform DER > certificate.cer
Enter fullscreen mode Exit fullscreen mode

Add this .cer file to your Xcode project.

Step 2: Basic Certificate Pinning Implementation

Here's a complete, production-ready implementation:

import Foundation
import Security

class SSLPinningManager: NSObject {

    // MARK: - Certificate Pinning

    /// Validates the server's certificate against pinned certificates
    static func validateCertificatePinning(
        challenge: URLAuthenticationChallenge,
        certificateNames: [String]
    ) -> Bool {

        // Get the server trust from the challenge
        guard let serverTrust = challenge.protectionSpace.serverTrust else {
            print("Server trust is unavailable")
            return false
        }

        // Get the server's certificate
        guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            print("Could not get server certificate")
            return false
        }

        // Convert server certificate to data
        let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data

        // Check against each pinned certificate
        for certificateName in certificateNames {
            if let pinnedCertificateData = loadCertificate(named: certificateName) {
                if serverCertificateData == pinnedCertificateData {
                    print("Certificate pinning successful")
                    return true
                }
            }
        }

        print("Certificate pinning failed")
        return false
    }

    /// Loads a certificate from the app bundle
    private static func loadCertificate(named name: String) -> Data? {
        guard let certificatePath = Bundle.main.path(forResource: name, ofType: "cer"),
              let certificateData = try? Data(contentsOf: URL(fileURLWithPath: certificatePath)) else {
            print("Could not load certificate: \(name)")
            return nil
        }
        return certificateData
    }

    // MARK: - Public Key Pinning (Recommended)

    /// Validates the server's public key against pinned keys
    static func validatePublicKeyPinning(
        challenge: URLAuthenticationChallenge,
        publicKeyHashes: [String]
    ) -> Bool {

        guard let serverTrust = challenge.protectionSpace.serverTrust else {
            print("Server trust is unavailable")
            return false
        }

        // Get the server's public key
        guard let serverPublicKey = SecTrustCopyKey(serverTrust) else {
            print("Could not extract server public key")
            return false
        }

        // Get the public key data and create hash
        guard let serverPublicKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, nil) as Data? else {
            print("Could not get public key data")
            return false
        }

        let serverPublicKeyHash = sha256(data: serverPublicKeyData)

        // Check against pinned public key hashes
        if publicKeyHashes.contains(serverPublicKeyHash) {
            print("Public key pinning successful")
            return true
        }

        print("Public key pinning failed")
        return false
    }

    /// Creates SHA256 hash of data
    private static func sha256(data: Data) -> String {
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        data.withUnsafeBytes {
            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
        }
        return hash.map { String(format: "%02x", $0) }.joined()
    }
}

// Import for SHA256
import CommonCrypto
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement URLSessionDelegate

Now integrate this with your network layer:

class NetworkManager: NSObject, URLSessionDelegate {

    static let shared = NetworkManager()
    private var session: URLSession!

    // Your pinned certificates (without .cer extension)
    private let pinnedCertificates = ["certificate"]

    // OR use public key hashes (get these from your server team)
    private let pinnedPublicKeyHashes = [
        "abcd1234efgh5678ijkl90mnopqrstuv...", // Your server's public key hash
        "wxyz0987abcd6543efgh21mnopqrstuv..."  // Backup key hash
    ]

    override init() {
        super.init()
        let configuration = URLSessionConfiguration.default
        session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
    }

    // MARK: - URLSessionDelegate

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

        // Only handle server trust authentication
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
            completionHandler(.performDefaultHandling, nil)
            return
        }

        // Validate using certificate pinning
        let isValid = SSLPinningManager.validateCertificatePinning(
            challenge: challenge,
            certificateNames: pinnedCertificates
        )

        // OR validate using public key pinning (recommended)
        // let isValid = SSLPinningManager.validatePublicKeyPinning(
        //     challenge: challenge,
        //     publicKeyHashes: pinnedPublicKeyHashes
        // )

        if isValid, let serverTrust = challenge.protectionSpace.serverTrust {
            let credential = URLCredential(trust: serverTrust)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }

    // MARK: - Network Request Example

    func fetchData(from urlString: String, completion: @escaping (Result<Data, Error>) -> Void) {
        guard let url = URL(string: urlString) else {
            completion(.failure(NSError(domain: "Invalid URL", code: -1)))
            return
        }

        let task = session.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let data = data else {
                completion(.failure(NSError(domain: "No data", code: -1)))
                return
            }

            completion(.success(data))
        }
        task.resume()
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Usage Example

// In your ViewController or anywhere you make network calls
NetworkManager.shared.fetchData(from: "https://yourserver.com/api/data") { result in
    switch result {
    case .success(let data):
        print("Data received securely: \(data)")
        // Process your data

    case .failure(let error):
        print("Request failed: \(error.localizedDescription)")
        // This could mean SSL pinning validation failed
    }
}
Enter fullscreen mode Exit fullscreen mode

How to Get Public Key Hashes

To get your server's public key hash for pinning:

# Get the certificate
openssl s_client -connect yourserver.com:443 < /dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
Enter fullscreen mode Exit fullscreen mode

This will output a hash like: abcd1234efgh5678...


Best Practices

1. Pin Multiple Keys
Always pin your current certificate AND a backup key. If your primary certificate expires or is compromised, you can switch to the backup without forcing users to update the app.

2. Use Public Key Pinning
It's more flexible than certificate pinning and doesn't require app updates when certificates are rotated.

3. Monitor Expiration
Set up alerts for when your certificates are about to expire.

4. Test Thoroughly
Test your implementation with tools like Charles Proxy or Burp Suite to ensure MITM attacks are blocked.

5. Don't Pin in Debug Builds
Disable pinning in debug mode so developers can use network debugging tools:

#if DEBUG
    // Skip SSL pinning in debug
    completionHandler(.performDefaultHandling, nil)
    return
#endif
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls to Avoid

Don't pin intermediate certificates - They change more frequently

Don't use only one pinned certificate - Have backups

Don't forget to handle certificate expiration - Plan ahead

Don't test only on simulators - Test on real devices with real network conditions


Testing Your Implementation

Use this curl command to test if your server is configured correctly:

curl -v https://yourserver.com
Enter fullscreen mode Exit fullscreen mode

Or use Charles Proxy:

  1. Install Charles Proxy
  2. Configure your device to use it
  3. Try making requests from your app
  4. If SSL pinning works, requests should fail when going through Charles

Top comments (1)

Collapse
 
arshtechpro profile image
ArshTechPro

Normal SSL: "I'll talk to anyone with a valid driver's license"
SSL Pinning: "I'll only talk to John Smith with driver's license #123456"

Some comments may only be visible to logged-in visitors. Sign in to view all comments.