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
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
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()
}
}
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
}
}
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
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
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
Or use Charles Proxy:
- Install Charles Proxy
- Configure your device to use it
- Try making requests from your app
- If SSL pinning works, requests should fail when going through Charles
Top comments (1)
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.