Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
197: Certificate Pinning in URLSession
I remember a lead developer I worked with on a fintech app a few years back. We were confident in our security because every single endpoint was HTTPS. During a third-party penetration test, the auditor smiled, installed a custom root certificate on the test device, and suddenly, all our "encrypted" API traffic was scrolling across their screen in plain text via a proxy. We had forgotten that by default, iOS trusts any certificate signed by a Root Certificate Authority (CA) present in the system's trust store. If a user is tricked into installing a malicious profile, or if a CA is compromised, your HTTPS tunnel is essentially a screen door.
This is where certificate pinning comes in. Instead of telling the app, "Trust anyone the iPhone trusts," we tell it, "Only trust this specific certificate—and no one else." It effectively narrows the window of trust from hundreds of global authorities down to the exact one you control.
Intercepting the Authentication Challenge
To implement pinning in URLSession, you have to move away from the simple shared session. You need a session with a delegate because the decision to trust or reject a server happens during the TLS handshake, specifically within the urlSession(_:didReceive:completionHandler:) delegate method. When the server presents its certificate, iOS triggers this "challenge."
The goal is to extract the server's certificate from the challenge and compare its data against a local copy you've bundled inside your app's main bundle. If the bytes don't match exactly, you kill the connection immediately. I usually recommend using .der encoded certificates for this, as they are binary and easy to compare.
class SecurityDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust,
let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Extract the data from the server's certificate
let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
// Load your local "pinned" certificate from the app bundle
if let localCertificatePath = Bundle.main.path(forResource: "api_cert", ofType: "der"),
let localCertificateData = try? Data(contentsOf: URL(fileURLWithPath: localCertificatePath)) {
if serverCertificateData == localCertificateData {
// It's a match! We trust this specific certificate.
completionHandler(.useCredential, URLCredential(trust: serverTrust))
return
}
}
// Something doesn't match; this could be a MitM attack
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
The Danger of the "Hard Brick"
Now, here is the part where most developers mess up: certificates expire. If you pin a leaf certificate and that certificate expires on the server, your app will stop working for every single user the moment the server rotates to a new one. You can't "push" a new certificate to users instantly; they have to update the app via the App Store. I've seen apps essentially "brick" themselves for three days while waiting for an emergency update to clear Apple's review process.
To avoid this, you have two real options. First, you can pin the Intermediate CA instead of the leaf certificate. This is slightly less secure but much more flexible, as you can rotate leaf certificates without changing the intermediate one. Second, and more commonly in high-security environments, you can perform Public Key Pinning. Instead of comparing the whole certificate, you extract the public key from the certificate and pin that. Since the public key typically stays the same even when a certificate is renewed, you eliminate the expiration headache.
If you go the public key route, you'll need to use SecCertificateCopyKey to get the key and then compare the hashes. It's a bit more boilerplate, but it saves you from the 3:00 AM panic call when a certificate expires on a Sunday.
📋 Practical Task
Exercise: Hardening the Payment Gateway with Certificate Pinning
You are tasked with securing a sensitive payment processing module. The API server uses a specific certificate that you have been provided as payment_gateway.der.
Implement a URLSessionDelegate class named PaymentSessionDelegate that performs the following:
- Overrides the
urlSession(_:didReceive:completionHandler:)method. - Extracts the server's certificate data from the
serverTrustobject. - Loads the
payment_gateway.derfile from the app bundle. - Compares the two data blobs. If they match, call the completion handler with
.useCredential; otherwise, call it with.cancelAuthenticationChallenge. - Ensure the session is initialized using this delegate rather than the
URLSession.sharedinstance.
There are no comments for now.