Skip to main content

Command Palette

Search for a command to run...

SSL Pinning in iOS (Swift): Certificate & Public Key Pinning Explained

Published
3 min read
SSL Pinning in iOS (Swift): Certificate & Public Key Pinning Explained

When building iOS apps that communicate with backend servers, security is critical.
Even though HTTPS encrypts data, apps can still be vulnerable to Man-in-the-Middle (MITM) attacks if a malicious certificate is trusted.

SSL Pinning solves this by ensuring your app trusts only a specific certificate or public key, not just any certificate signed by a trusted CA.

In this blog, we’ll cover:

  • What SSL Pinning is

  • Certificate Pinning vs Public Key Pinning

  • How to implement both in Swift

What is SSL Pinning?

SSL Pinning is a technique where an app validates the server’s identity by comparing:

  • The server certificate, or

  • The server public key

against a trusted copy bundled inside the app.

If the validation fails → the connection is rejected.

Certificate Pinning vs Public Key Pinning

TypeDescriptionProsCons
Certificate PinningMatches entire SSL certificateStrong securityBreaks when cert renews
Public Key PinningMatches only public keySurvives cert renewalSlightly complex

Prerequisites

  • Download your server’s SSL certificate (.cer)

  • Add it to your Xcode project

  • Enable Target → Signing & Capabilities → App Transport Security

Common Setup (URLSession Delegate)

class NetworkManager: NSObject, URLSessionDelegate {
    lazy var session: URLSession = {
        let config = URLSessionConfiguration.default
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()
}

Certificate Pinning (Swift)

Step 1: Add Certificate to App Bundle

Add server.cer to your project and ensure Copy Bundle Resources is enabled.

Step 2: Validate Certificate

Connection succeeds only if certificates match exactly

func urlSession(
    _ session: URLSession,
    didReceive challenge: URLAuthenticationChallenge,
    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
    guard
        let serverTrust = challenge.protectionSpace.serverTrust,
        let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0),
        let localCertPath = Bundle.main.path(forResource: "server", ofType: "cer"),
        let localCertData = try? Data(contentsOf: URL(fileURLWithPath: localCertPath))
    else {
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    let serverCertData = SecCertificateCopyData(certificate) as Data

    if localCertData == serverCertData {
        let credential = URLCredential(trust: serverTrust)
        completionHandler(.useCredential, credential)
    } else {
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

Public Key Pinning (Swift)

Public Key Pinning extracts the public key from the certificate and compares it.

Step 1: Extract Public Key

func publicKey(from certificate: SecCertificate) -> SecKey? {
    let policy = SecPolicyCreateBasicX509()
    var trust: SecTrust?

    SecTrustCreateWithCertificates(certificate, policy, &trust)
    return trust.flatMap { SecTrustCopyKey($0) }
}

Step 2: Compare Public Keys

Works even if certificate renews (same key)

func urlSession(
    _ session: URLSession,
    didReceive challenge: URLAuthenticationChallenge,
    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
    guard
        let serverTrust = challenge.protectionSpace.serverTrust,
        let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0),
        let serverKey = publicKey(from: serverCertificate),
        let localCertPath = Bundle.main.path(forResource: "server", ofType: "cer"),
        let localCertData = try? Data(contentsOf: URL(fileURLWithPath: localCertPath)),
        let localCertificate = SecCertificateCreateWithData(nil, localCertData as CFData),
        let localKey = publicKey(from: localCertificate)
    else {
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    if serverKey == localKey {
        let credential = URLCredential(trust: serverTrust)
        completionHandler(.useCredential, credential)
    } else {
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

Common Mistakes

  • Forgetting to update cert after expiry (certificate pinning)

  • Using pinning in debug builds only

  • Pinning to third-party APIs (can break unexpectedly)

Best Practices

  • Use Public Key Pinning for production apps

  • Keep pinning logic modular

  • Always have a fallback strategy

  • Monitor certificate expiration dates

Summary

SSL Pinning significantly improves app security by protecting against MITM attacks.
If your app handles payments, authentication, or sensitive data, pinning is highly recommended.

Certificate Pinning → Maximum security
Public Key Pinning → Better long-term stability

About Me

Exploring iOS, Swift, and Kotlin through real-world challenges, clean architecture, and production-ready mobile solutions.

More from this blog

Dev on Mobile

12 posts

Practical mobile development with iOS, Swift, and Kotlin, focused on real-world examples, clean architecture, performance, and security.