Article by Pushkar Deshmukh
Certificate Pinning in iOS (Part 1): A Practical Guide Using URLSession
Learn how to implement certificate pinning in iOS using URLSession with a simple, real-world approach. This guide also explains SSL certificates (root, intermediate, and leaf) in an easy-to-understand way and how they impact pinning decisions.
Pushkar Deshmukh
Senior iOS Engineer
🚀 Introduction
When your iOS app talks to a backend server, it relies on HTTPS (SSL/TLS) to secure communication.
But here’s the catch:
👉 Even HTTPS can be compromised via Man-in-the-Middle (MITM) attacks if a malicious certificate is trusted by the system.
That’s where Certificate Pinning comes in.
Certificate pinning ensures your app only trusts specific certificates — not just any certificate trusted by the system.
In this blog, we’ll:
Understand how SSL certificates work (in simple terms)
Learn about Root, Intermediate, and Leaf certificates
Implement Certificate Pinning using URLSession
🔐 What is Certificate Pinning?
Normally, iOS trusts a server if:
Its certificate is valid
It is signed by a trusted Certificate Authority (CA)
👉 With certificate pinning, we add an extra rule:
“Only trust this specific certificate (or set of certificates), even if others are valid.”
🌳 Understanding the Certificate Chain (Very Important)
Before jumping into code, you need to understand how certificates actually work.
Think of it like a trust chain:
Root Certificate (Top Authority)
↓
Intermediate Certificate
↓
Leaf Certificate (Your Server)
🥇 1. Root Certificate
Issued by a trusted Certificate Authority (CA)
Pre-installed in iOS (Apple maintains this list)
Example: DigiCert, GlobalSign
👉 Key point:
You don’t control this
It rarely changes
🥈 2. Intermediate Certificate
Issued by the Root CA
Used to sign server (leaf) certificates
👉 Why it exists:
Security (root key stays offline)
Flexibility for certificate management
🥉 3. Leaf Certificate (Server Certificate)
This is your server’s certificate (e.g.,
api.yourapp.com)Issued by an intermediate certificate
👉 Key point:
This is what your app connects to
This changes frequently (renewals, updates)
⚖️ How Each Certificate Affects Pinning
Certificate Type | Stability | Control | Pinning Suitability |
|---|---|---|---|
Root | Very High | No | ❌ Not recommended |
Intermediate | Medium | No | ⚠️ Risky |
Leaf | Low | Yes | ✅ Most common |
🧠 Practical Insight
Leaf pinning → More secure, but breaks when cert renews
Intermediate pinning → Less maintenance, but slightly weaker
Root pinning → Too broad, defeats purpose
👉 In most real-world apps:
✅ Leaf certificate pinning is preferred
🛠️ Part 1: Certificate Pinning using URLSession
We’ll implement pinning using:
URLSessionURLSessionDelegate
📥 Step 1: Get the Certificate
Open your API URL in browser (
https://api.yourserver.com)Download the certificate
Convert it to
.cerformatAdd it to your Xcode project
👉 Make sure:
Target membership is enabled
🧩 Step 2: Create a Pinned Session Delegate
import Foundation
final class PinnedSessionDelegate: NSObject, URLSessionDelegate {
private let pinnedCertificates: [Data]
override init() {
// Load local certificate
guard let certPath = Bundle.main.path(forResource: "server", ofType: "cer"),
let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)) else {
fatalError("Certificate not found")
}
self.pinnedCertificates = [certData]
}
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
}
// Get server certificate
let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
guard let cert = serverCertificate else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let serverCertData = SecCertificateCopyData(cert) as Data
// Compare with pinned certificate
if pinnedCertificates.contains(serverCertData) {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
🌐 Step 3: Use the Custom Session
let session = URLSession(
configuration: .default,
delegate: PinnedSessionDelegate(),
delegateQueue: nil
)
let url = URL(string: "https://api.yourserver.com/data")!
let task = session.dataTask(with: url) { data, response, error in
if let error = error {
print("Error: \(error)")
return
}
print("Success")
}
task.resume()
⚠️ Common Mistakes (Important)
❌ 1. Pinning wrong certificate
Always verify you’re using correct
.cer
❌ 2. Not handling certificate renewal
Leaf certificates expire → app breaks
❌ 3. Using default session accidentally
Pinning only works with your custom delegate
🧪 How to Test Pinning
Use tools like:
Charles Proxy
Proxyman
👉 If pinning works:
Requests should fail when intercepted
📊 When Should You Use Certificate Pinning?
Use it when:
You handle sensitive data (banking, payments)
You want extra security beyond HTTPS
Avoid it when:
Backend certificates change frequently
You don’t control backend infra
🔮 What’s Next (Part 2)
In Part 2, we’ll cover:
👉 Public Key Pinning
More flexible than certificate pinning
Survives certificate renewals
Used by large-scale apps
🧠 Final Thoughts
Certificate pinning is powerful—but comes with tradeoffs.
Security vs Maintainability
Start simple:
Implement leaf certificate pinning
Monitor certificate expiry
Move to public key pinning for scalability




Comments
Loading comments…