Certificate Pinning in iOS
The OWASP security organization includes it in their “General Best Practices” and “iOS Specific Best Practices”. This shows how important this topic is.
This article discusses certificate pinning and related topics.
You will get familiar with things like:
- TLS/SSL and its weaknesses
- Certificate pinning
- What is recommended and how to implement it on iOS
Theory
What is TLS? (Transport Layer Security)
TLS is an extended version of the SSL protocol. It is a cryptographic protocol used to encrypt network traffic. Your browser uses an implementation of that protocol - HTTPS. It is usually displayed next to the website address as a padlock icon.
How TLS works?
Formal definition
TLS uses asymmetric cryptography to provide secure data transportation. Asymmetric cryptography uses two keys, a public key and a private key. The public key is used to encrypt data and the private key decrypts previously encrypted data. When you make a connection with a server you exchange public keys with it. You receive the public key to encrypt data before sending it. The server receives your public key so you can decrypt the data received from it with your private key. The keys are uniquely generated for each connection and are based on a shared secret negotiated at the beginning of the session, also known as a TLS handshake.
Example by analogy
TLS works like a mailbox. Everyone has access to the mailbox and can put a letter inside it, but you need a key to open the mailbox and read letters. Only the person who created the mailbox has the key. TLS is considered undecryptable in reasonable time. This means that the best locksmith would have to work on it for a long time to open it without having a key.
You can also check out this video visualising asymmetric encryption.
Some Dates
- SSL protocol is deprecated since 2015.
- TLS 1.0/1.1 is deprecated since march 2020.
- There are no plans to deprecate TLS 1.2/1.3 yet.
What is a certificate?
A certificate is a data file containing a public key and other information like expiration date, organization name, and others. It allows us to encrypt a connection using the public key.
So what is the problem with using only the TLS protocol for networking?
Imagine you are using a public Wi-Fi network on the train. That network might be created by someone who wants to read the data sent by you. Since that person is providing your internet connection, they can create their own certificate and tell you to encrypt your data with it. Because this person created the certificate, they can read all the data inside it and then send it further to the right receiver. You may not see that you’re using the wrong certificate. This technique is called a man-in-the-middle attack. This illustration shows how it works.
How to prevent using invalid certificates?
This is where certificate pinning and public key pinning come in. These approaches consist of defining a list of valid certificates/public keys. Every time the app sends data, it checks if that certificate is on the list of trusted certificates/public keys. This list of certificates/public keys may be hard-coded or defined in a file. Both solutions prevent man-in-the-middle attacks and should pass a penetration test. If you want to be even more secure, you may also encrypt the certificate to make it harder to get.
More formal definition:
“Server/Host is associated with a certificate or public key. You configure the app to reject all certificates other than the predefined certificates or public keys. Whenever the app connects to a server, it compares the server certificate with the pinned certificate(s) or public key(s). If and only if they match, the app trusts the server and establishes the connection.”
Certificate pinning vs public key pinning
Public key pinning is a simplified implementation of certificate pinning. Unlike certificate pinning, It only validates the public key instead of the whole certificate.
The public key doesn't have an expiration date. So if we have implemented public key pinning, we can regenerate your certificate with the same public key. This allows us to regenerate the certificate without uploading a new version of the app. But, since it doesn’t change, it may violate key rotation policies.
Then there is certificate pinning. It validates the whole certificate, so this implementation is more secure, since even a small change in the certificate will cause an issue. It has an expiration date, so every time you create a new certificate you have to upload a new version of the app.
Usually, the heavier your security implementation, the less flexible and harder to maintain it gets. You should always look for a solution that fits your app.
- If you really care about security and you can do it at the expense of flexibility, you should choose hardcoded certificate pinning. This solution should be used for example in banking applications - it is the safest of those presented here.
- If your certificates change very often and security isn't crucial in your project, you should probably use public key pinning. It will give you more flexibility and you will not block users with older versions of your application.
- If you want to support older versions of the app, but still implement certificate pinning, you can create a procedure to download the new certificate after expiration. It is not as safe as a hardcoded one, but it gives you more flexibility. After the certificate expires, you will download the new one. While you are downloading the new certificate, there is a chance that someone is listening to your communication.
- If your mobile application does not need additional protection, e.g. it uses a public API to display the weather, you do not have to implement certificate pinning.
Is that all? Am I safe now?
Well, no. There are still techniques to fool certificate pinning, but using it makes it extremely hard to decrypt network traffic. You can read about some bypasses here: Certificate pinning bypass. Also there are some advanced techniques to prevent such bypasses: Prevent bypassing of certificate pinning.
What is recommended for iOS?
AlamoFire is a good choice when you are already using that or you are looking for a network library. If networking in your app uses NSURLSession and you only need certificate pinning, TrustKit is a good option.
You shouldn’t implement pinning by yourself, as implementation mistakes are extremely likely and usually lead to severe vulnerabilities.
OWASP’s stance on certificate pinning
OWASP (Open Web Application Security Project) is an organization that provides unbiased and practical, cost-effective information about computer and Internet applications. This organization is well known. A lot of big companies use “OWASP top 10 application security vulnerabilities” in their pentesting plans.
You can find that certificate pinning is listed among the “General Best Practices” and “iOS Specific Best Practices” on the OWASP website.
“You should pin anytime you want to be relatively certain of the remote host's identity or when operating in a hostile environment. Since one or both are almost always true, you should probably pin all the time.” - OWASP pinning cheat sheet
Using certificate pinning in practice
How to get an example certificate.der?
Run this command in your terminal, you should get Google’s certificate with the .der extension:
openssl s_client -connect google.com:443 </dev/null | openssl x509 -outform DER -out google.com.der
Alamofire certificate pinning
The Alamofire library has a built-in class to handle certificate pinning: ServerTrustManager.
Let’s see the initialize method of this class:
init(allHostsMustBeEvaluated: Bool , evaluators: [String: ServerTrustEvaluating])
Evaluators - a dictionary with the website address as a key and the list of defined certificates wrapped in the ServerTrustEvaluating class as a value of it.
allHostsMustBeEvaluated: Bool -
If true: Alamofire will only allow communication with hosts defined in the evaluators and matching the defined certificates.
If false: Alamofire will check certificates only for hosts defined in the evaluators dictionary. Communication with other hosts will not use certificate pinning
Let’s create a ServerTrustManager for your project!
Add your certificate to your project and add an extension class to read that certificate.
struct Certificates {
static let certificate: SecCertificate = Certificates.certificate(filename: "certificateFileName")
private static func certificate(filename: String) -> SecCertificate {
let filePath = Bundle.main.path(forResource: filename, ofType: "fileExtension")!
let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))
let certificate = SecCertificateCreateWithData(nil, data as CFData)!
return certificate
}
}
Now let’s set up the networking class, which will pin that certificate.
import Alamofire
class AlamofireNetworking {
private let certificates = [
"www.yourwebsite.com":
PinnedCertificatesTrustEvaluator(certificates: [Certificates.certificate],
acceptSelfSignedCertificates: false,
performDefaultValidation: true,
validateHost: true)
]
private let session: Session
init(allHostsMustBeEvaluated: Bool) {
let serverTrustPolicy = ServerTrustManager(
allHostsMustBeEvaluated: allHostsMustBeEvaluated,
evaluators: certificates
)
session = Session(serverTrustManager: serverTrustPolicy)
}
func request(_ convertible: URLRequestConvertible) -> DataRequest {
return session.request(convertible)
}
}
The implementation on iOS app is really easy and readable here. I you use the request method from that class, certificate pinning will work. You can see the usage of this class in the unit tests of the project. You can also read the documentation for more information.
TrustKit public key pinning
First, install TrustKit in your project. You can do this using cocoaPods or carthage. Use the instructions to do this.
In order to implement certificate pinning with TrustKit, we have to define the configuration for this tool. To do so, we need to extract publicKeyHashes from our domain. You can easily get them using script provided by TrustKit. Just follow the instructions in their documentation.
Here is a complete class with an URLSession which uses certificate pinning.
final class TrustKitCertificatePinning: NSObject, URLSessionDelegate {
/// URLSession with configured certificate pinning
lazy var session: URLSession = {
URLSession(configuration: URLSessionConfiguration.ephemeral,
delegate: self,
delegateQueue: OperationQueue.main)
}()
private let trustKitConfig = [
kTSKPinnedDomains: [
"www.yourwebsite.com": [
kTSKDisableDefaultReportUri: true, /// Disable reporting errors to default domain.
kTSKEnforcePinning: true,
kTSKIncludeSubdomains: true,
kTSKExpirationDate: "2020-10-09",
kTSKPublicKeyHashes: [
"GesrhCSBz+OCxCt624nic/qqLXAPGUGGf5vwB5jBheU=",
"+7YVLndnzqU0VtEREXo00bJlgdmQ9T9qy2IWVVWNpcE=",
],
]
]
] as [String : Any]
override init() {
TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
super.init()
}
// MARK: TrustKit Pinning Reference
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if TrustKit.sharedInstance().pinningValidator.handle(challenge, completionHandler: completionHandler) == false {
// TrustKit did not handle this challenge: perhaps it was not for server trust
// or the domain was not pinned. Fall back to the default behavior
completionHandler(.performDefaultHandling, nil)
}
}
}
As you can see, there is not much to talk about here, but let’s dig into that code.
This part of code defines configuration for TrustKit. You can define the domains that you want to check.
private let trustKitConfig = [
kTSKPinnedDomains: [
"www.yourwebsite.com": [
kTSKDisableDefaultReportUri: true, /// Disable reporting errors to default domain.
kTSKEnforcePinning: true,
kTSKIncludeSubdomains: true,
kTSKExpirationDate: "2020-10-09",
kTSKPublicKeyHashes: [
"GesrhCSBz+OCxCt624nic/qqLXAPGUGGf5vwB5jBheU=",
"+7YVLndnzqU0VtEREXo00bJlgdmQ9T9qy2IWVVWNpcE=",
],
]
]
] as [String : Any]
You can read more about configuration in the documentation.
To get TrustKit working, you have to initialize a TrustKit shared instance with this configuration.
TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
Then you can use that sharedInstance in the urlSession delegate methods to validate your certificates. This is the default implementation from TrustKit’s documentation.
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if TrustKit.sharedInstance().pinningValidator.handle(challenge, completionHandler: completionHandler) == false {
// TrustKit did not handle this challenge: perhaps it was not for server trust
// or the domain was not pinned. Fall back to the default behavior
completionHandler(.performDefaultHandling, nil)
}
}
The last thing you need to do is assign the created Delegate to your session.
final class TrustKitCertificatePinning: NSObject, URLSessionDelegate {
/// URLSession with configured certificate pinning
lazy var session: URLSession = {
URLSession(configuration: URLSessionConfiguration.ephemeral,
delegate: self,
delegateQueue: OperationQueue.main)
}()
Use your URLSession with certificate pinning :)
Complete CertificatePinning project with unit tests:
https://github.com/karolpiateknet/CertificatePinning
Summary
Thank you for your time. I hope you enjoyed reading my article and learned a lot of useful information.