iOS SSL Pinning With Public Key
Hello 🖐️, today I will explain how to perform SSL Pinning for iOS devices without using any third-party libraries.
guard isCoffeeReady else { return }
First of all, let’s get a general idea. I won’t explain what SSL Pinning is here because there is plenty of information available on the internet about this topic. However, let’s summarize it in one sentence.
SSL Pinning is a security measure that allows an application to match security certificates with a specific expected certificate when communicating with a server.
Now, let’s run an SSL analysis for powerbi.com and continue with our article. As shown in the image, we have three Public Keys that we can use on Apple platforms.
The first one is the certificate signed by powerbi.com for its own domain.
The second one is signed by Microsoft Azure Certificate Authority (CA).
The third one is signed by DigiCert.
You can find which Certificate Authorities Apple trusts at this address. DigiCert’s certificate is on this list, so we are taking the third key created with RSA 4096 bits (e 65537) / SHA384withRSA:
Wl8MFY+9zijGG8QgEHCAK5fhA+ydPZxaLQOFdiEPz3U=
Of course, you can use the others according to your needs, but for now, I will continue with this one. I will also show how to perform pinning for all three.
What is RSA 4096 bits (e 65537) / SHA384withRSA ?
RSA 2048 Bits: This section specifies the length of the RSA key used. A 2048-bit key represents the length of the key. A longer key can provide stronger encryption but requires more computational power.
(e 65537): This section specifies the public exponent of the RSA key. RSA keys typically have a small public exponent, and 65537 (or 0x10001) is a typical value for this public exponent.
SHA256withRSA: This section specifies the digital signature algorithm. “SHA256withRSA” represents an algorithm used for digitally signing data. It indicates that the data is signed with an RSA key after taking the hash of the data and that the hash is calculated using the SHA-256 algorithm.
That’s enough information; let’s move on to coding 👨🏻💻.
First of all, we need three basic libraries for this task: CommonCrypto, CryptoKit, and Foundation. Let’s start by initializing our class and writing our first Crypto method, which calculates the SHA256 hash of the given data.
struct PinningManager {
private var pinnedKeyHashes: [String]!
init(pinnedKeyHashes: [String]) {
self.pinnedKeyHashes = pinnedKeyHashes
}
private func SHA256(_ data: Data) -> Data {
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
_ = data.withUnsafeBytes { buffer in
CC_SHA256(buffer.baseAddress, CC_LONG(data.count), &digest)
}
return Data(bytes: digest, count: digest.count)
}
}
The next piece of code is the method that will be called within the SessionDelegate and trigger other methods in a chain. In order:
func validate(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
do {
let trust = try validateAndGetTrust(with: challenge)
completionHandler(.performDefaultHandling, URLCredential(trust: trust))
} catch {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
private func validateAndGetTrust(with challenge: URLAuthenticationChallenge) throws -> SecTrust {
guard let trust = challenge.protectionSpace.serverTrust else {
throw PinningError.noCertificatesFromServer
}
var trustCertificateChain: [SecCertificate] = []
if #available(iOS 12.0, *) {
for index in 0..<3 {
//0 > RSA 2048 bits (e 65537) / SHA256withRSA
//1 > 2048 bits (e 65537) / SHA384withRSA
//2 > RSA 4096 bits (e 65537) / SHA384withRSA
if let cert = SecTrustGetCertificateAtIndex(trust, index) { // RSA 2048 bits (e 65537) / SHA256withRSA
trustCertificateChain.append(cert)
}
}
}
if #available(iOS 15.0, *) {
trustCertificateChain = SecTrustCopyCertificateChain(trust) as! [SecCertificate]
}
for serverCertificate in trustCertificateChain {
let publicKey = try getPublicKey(for: serverCertificate)
let header = try getSecKeyBlockSize(publicKey)
let publicKeyHash = try getKeyHash(of: publicKey, header: header)
if pinnedKeyHashes.contains(publicKeyHash) {
return trust
}
}
throw PinningError.receivedWrongCertificate
}
private func getPublicKey(for certificate: SecCertificate) throws -> SecKey {
let policy = SecPolicyCreateBasicX509()
var trust: SecTrust?
let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust)
if let trust, trustCreationStatus == errSecSuccess {
var publicKey: SecKey?
if #available(iOS 15, *) {
publicKey = SecTrustCopyKey(trust)
}
if #available(iOS 12, *) {
publicKey = SecCertificateCopyKey(certificate)
}
if publicKey == nil {
throw PinningError.failedToGetPublicKey
}
return publicKey!
} else {
throw PinningError.failedToGetPublicKey
}
}
private func getSecKeyBlockSize(_ key: SecKey) throws -> ASN1Header {
let size = SecKeyGetBlockSize(key)
if size == 256 {
return .rsa2048
}
if size == 512 {
return .rsa4096
}
throw PinningError.failedToGetPublicKeySize
}
private func getKeyHash(of publicKey: SecKey, header: ASN1Header) throws -> String {
guard let publicKeyCFData = SecKeyCopyExternalRepresentation(publicKey, nil) else {
throw PinningError.failedToGetDataFromPublicKey
}
let publicKeyData = (publicKeyCFData as NSData) as Data
var publicKeyWithHeaderData: Data
publicKeyWithHeaderData = Data(header.bytes)
publicKeyWithHeaderData.append(publicKeyData)
let publicKeyHashData = sha256(publicKeyWithHeaderData)
return publicKeyHashData.base64EncodedString()
}
Let’s add our enums:
private enum PinningError: Error {
case noCertificatesFromServer
case failedToGetPublicKey
case failedToGetDataFromPublicKey
case receivedWrongCertificate
case failedToGetPublicKeySize
var localizedDescription: String {
switch self {
case .noCertificatesFromServer: return "Sunucudan Sertifika Alınamadı"
case .failedToGetPublicKey: return "Public Key (PK) Alınamadı"
case .failedToGetDataFromPublicKey: return "Public Key (PK)'den Veri Çıkarılamadı"
case .receivedWrongCertificate: return "Yanlış sertifika"
case .failedToGetPublicKeySize: return "Public Key (PK) Size Alınamadı"
}
}
}
private enum ASN1Header {
case rsa2048
case rsa4096
var bytes: [UInt8] {
switch self {
case .rsa2048:
return [0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00]
case .rsa4096:
return [0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00]
}
}
}
The key point here is to use the correct header bytes in the getKeyHash method. When I first started writing this class, finding the header byte for RSA4096 was quite challenging. In fact, one of the main reasons I wanted to write this article was for this purpose.
Let’s call our code:
You can obtain the Public Keys from a trusted server:
Alamofire.SessionManager(configuration: yourConfiguration, delegate: NetworkManagerSessionDelegate())
This way, after providing our class to Alamofire, the process is complete ✅
This approach has three significant advantages:
Your application can handle requirements like connecting to multiple servers, such as in B2B applications. For example, to handle such cases with tools like TrustKit, you would need to restart the application.
When the SSL certificate expires or needs to be changed, there is no need to update your application.
There is no library dependency.
Full Code (Copy-Paste-Run)
struct PinningManager {
/// Common errors of SSL Pinning
private enum PinningError: Error {
case noCertificatesFromServer
case failedToGetPublicKey
case failedToGetDataFromPublicKey
case receivedWrongCertificate
case failedToGetPublicKeySize
var localizedDescription: String {
switch self {
case .noCertificatesFromServer: return "Can not retrieve certificate"
case .failedToGetPublicKey: return "Public Key (PK) could not fetch"
case .failedToGetDataFromPublicKey: return "Can not extract data from Public Key"
case .receivedWrongCertificate: return "Wrong Certificate"
case .failedToGetPublicKeySize: return "Can not retrieve key size"
}
}
}
/// Abstract Syntax Notation One, ASN.1
private enum ASN1Header {
case rsa2048
case rsa4096
var bytes: [UInt8] {
switch self {
case .rsa2048:
return [0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00]
case .rsa4096:
return [0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00]
}
}
}
/// Pinlenecek Public Key Hashleri
private var pinnedKeyHashes: [String]!
init(pinnedKeyHashes: [String]) {
self.pinnedKeyHashes = pinnedKeyHashes
}
/// Yeni public key set etmek için
/// - Parameter pk: String...
mutating func setNewPK(_ pk: String...) {
pinnedKeyHashes = pk
}
/// Verilen datanın SHA256 Digest (özet)'ini döner, verilen Pinler ile aynısı bu metotta elde edilmeye çalışılır
/// - Parameter data: ASN1Header ve PublicKey'in datası eklenerek
/// - Returns: PublicKey Hash
private func sha256(_ data: Data) -> Data {
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
_ = data.withUnsafeBytes { buffer in
CC_SHA256(buffer.baseAddress, CC_LONG(data.count), &digest)
}
return Data(bytes: digest, count: digest.count)
}
/// PublicKey Hashe Göre ASN.1 Header verilir
/// - Parameter key: Public Key (PK)
/// - Returns: ASN.1 Header
private func getSecKeyBlockSize(_ key: SecKey) throws -> ASN1Header {
let size = SecKeyGetBlockSize(key)
if size == 256 {
return .rsa2048
}
if size == 512 {
return .rsa4096
}
throw PinningError.failedToGetPublicKeySize
}
/// İlk metot, pinningin başarılı olup olmadığına karar verecek yer
/// - Parameters:
/// - challenge: URLAuthenticationChallenge
/// - completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?)
func validate(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
do {
let trust = try validateAndGetTrust(with: challenge)
completionHandler(.performDefaultHandling, URLCredential(trust: trust))
} catch {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
/// URL'den Trust seritifkaları alınır
/// - Parameter challenge: URLAuthenticationChallenge
/// - Returns: SecTrust
private func validateAndGetTrust(with challenge: URLAuthenticationChallenge) throws -> SecTrust {
guard let trust = challenge.protectionSpace.serverTrust else {
throw PinningError.noCertificatesFromServer
}
var trustCertificateChain: [SecCertificate] = []
if #available(iOS 12.0, *) {
for index in 0..<3 {
//0 > RSA 2048 bits (e 65537) / SHA256withRSA
//1 > 2048 bits (e 65537) / SHA384withRSA
//2 > RSA 4096 bits (e 65537) / SHA384withRSA
if let cert = SecTrustGetCertificateAtIndex(trust, index) { // RSA 2048 bits (e 65537) / SHA256withRSA
trustCertificateChain.append(cert)
}
}
}
if #available(iOS 15.0, *) {
trustCertificateChain = SecTrustCopyCertificateChain(trust) as! [SecCertificate]
}
for serverCertificate in trustCertificateChain {
let publicKey = try getPublicKey(for: serverCertificate)
let header = try getSecKeyBlockSize(publicKey)
let publicKeyHash = try getKeyHash(of: publicKey, header: header)
if pinnedKeyHashes.contains(publicKeyHash) {
return trust
}
}
throw PinningError.receivedWrongCertificate
}
/// Alınan sertifikanın içinden Public Key (PK) oluşturulur ve dönülür
/// - Parameter certificate: SecCertificate
/// - Returns: SecKey
private func getPublicKey(for certificate: SecCertificate) throws -> SecKey {
let policy = SecPolicyCreateBasicX509()
var trust: SecTrust?
let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust)
if let trust, trustCreationStatus == errSecSuccess {
var publicKey: SecKey?
if #available(iOS 15, *) {
publicKey = SecTrustCopyKey(trust)
}
if #available(iOS 12, *) {
publicKey = SecCertificateCopyKey(certificate)
}
if publicKey == nil {
throw PinningError.failedToGetPublicKey
}
return publicKey!
} else {
throw PinningError.failedToGetPublicKey
}
}
/// Public Key (PK) Hashi OIuşturulur
/// - Parameters:
/// - publicKey: SecKey
/// - header: ASN1Header
/// - Returns: String
private func getKeyHash(of publicKey: SecKey, header: ASN1Header) throws -> String {
guard let publicKeyCFData = SecKeyCopyExternalRepresentation(publicKey, nil) else {
throw PinningError.failedToGetDataFromPublicKey
}
let publicKeyData = (publicKeyCFData as NSData) as Data
var publicKeyWithHeaderData: Data
publicKeyWithHeaderData = Data(header.bytes)
publicKeyWithHeaderData.append(publicKeyData)
let publicKeyHashData = sha256(publicKeyWithHeaderData)
return publicKeyHashData.base64EncodedString()
}
}
Referanslar
https://www.bugsee.com/blog/ssl-certificate-pinning-in-mobile-applications/
https://medium.com/lunasolutions/mastering-ssl-pinning-in-swift-no-third-party-libraries-required-42a377db80ff
https://support.apple.com/en-us/HT213464