00:06 Rob is in Berlin to give a talk at UIKonf,
so we're using this opportunity to have a look at what certificate pinning is
and how it works.
00:52 We've prepared a simple app that loads a webpage with URLSession
.
We'd usually be concerned with checking the certificate when doing API calls,
but for the sake of simplicity, we're loading just our website:
class ViewController: UIViewController {
@IBOutlet weak var webView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://www.objc.io")!
let session = URLSession.shared
let task = session.dataTask(with: url) { data, response, error in
if let error = error {
print(error)
return
}
print("Loaded!")
if let data = data {
self.webView.load(data, mimeType: response?.mimeType ?? "", textEncodingName: response?.textEncodingName ?? "", baseURL: url)
}
}
task.resume()
}
}
01:12 This works fine, so we're ready to start making our app more
secure.
About Certificates
01:26 For HTTPS, there's a long list of certificates trusted by the
operating system. These certificates are used to issue individual certificates,
like the one on our server. The problem is that any of the 150+ certificates on
the system's list can be compromised.
02:11 We can narrow down the list of trusted certificates by pinning,
meaning we'll only trust our one certificate (or an intermediate certificate we
control).
02:39 We do so by adding a URLSessionDelegate
, which will be asked for
information during the server handshake process. This delegate could be a
separate object, but for now we'll just use our view controller:
class ViewController: UIViewController, URLSessionDelegate {
}
03:27 To assign a delegate, we can no longer use the shared session. We
create a new session with the view controller as the delegate:
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
03:53 In an extension, we'll implement the relevant method that
evaluates a challenge
:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
}
04:25 This method is called for all kinds of security checks, but since
we're only interested in the certificate challenge, we check for a so-called
trust in the challenge. This trust can contain certificates, and we check that
it contains at least one. The trust object is a C data structure, so we have to
use a specific method to count the certificates:
if let trust = challenge.protectionSpace.serverTrust,
SecTrustGetCertificateCount(trust) > 0 {
}
07:12 If we have a trust, we want to pull the certificate out. There may
be multiple certificates, but they're in order, with the certificate closest to
us being first, so we only check the first certificate, using another C
function:
if let certificate = SecTrustGetCertificateAtIndex(trust, 0) {
}
07:53 If we get a certificate, we can check if it's one of ours by
comparing its content. We get the data as CFData
, which is bridged to Swift's
Data
, so we can simply cast it for easier use:
let data = SecCertificateCopyData(certificate) as Data
09:08 We compare the data against a list of our known certificates.
Because certificates expire, there may be some overlapping ones on the server.
The app should check if any of the trusted certificates are found. That's why we
use an array, which we'll populate later:
class ViewController: UIViewController, URLSessionDelegate {
let certificates: [Data] = []
}
10:56 Back in the delegate method, we check if the found certificate
data is contained in our certificates array. Finally, we have to call the
completion handler to either "use a credential" if we find a trusted
certificate, or "cancel" otherwise. In order to advance successfully, we can
simply create a credential with the trust we found earlier. Here's the complete
delegate method:
extension ViewController {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if let trust = challenge.protectionSpace.serverTrust, SecTrustGetCertificateCount(trust) > 0 {
if let certificate = SecTrustGetCertificateAtIndex(trust, 0) {
let data = SecCertificateCopyData(certificate) as Data
if certificates.contains(data) {
completionHandler(.useCredential, URLCredential(trust: trust))
return
}
}
}
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
13:14 We're still checking against an empty certificates array, but it's
good to check that the challenge fails if we want it to.
Storing a Certificate
13:52 Now we need our certificate's data, so we use openssl
in
Terminal to retrieve it from our server. More specifically, we use the
s_client
command, which can connect to any server over SSL by specifying the
server address and port 443. The < /dev/null
part redirects back in;
otherwise, we'd be talking to the server:
openssl s_client -connect www.objc.io:443 < /dev/null
15:11 In the case of our server address, we run into a common error:
CONNECTED(00000003)
16632:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:/BuildRoot/Library/Caches/com.apple.xbs/Sources/OpenSSL098/OpenSSL098-64.50.6/src/ssl/s23_clnt.c:593:
15:12 This occurs because our website runs on a shared hosting machine,
which may be handling lots of web addresses. We have to specify the server name,
for which we can use the same address:
openssl s_client -connect www.objc.io:443 -servername www.objc.io < /dev/null
16:11 This works, and we receive a lot of data, including a list of
three certificates. We confirm that we see the part of the first certificate
listing that says CN=
, which stands for common name:
Certificate chain
0 s:/OU=GT09961371/OU=See www.rapidssl.com/resources/cps (c)15/OU=Domain Control Validated - RapidSSL(R)/CN=www.objc.io
i:/C=US/O=GeoTrust Inc./CN=RapidSSL SHA256 CA - G3
1 s:/C=US/O=GeoTrust Inc./CN=RapidSSL SHA256 CA - G3
i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
2 s:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
i:/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
16:33 Below that, we can see the actual certificate data we need. To
copy the certificate into a file, we use openssl
again. We repeat the previous
command and pass its output to openssl x509
(the general format name of
certificates), we specify the binary output format (DER
), and we output it to
a new file, named objcio.cer
:
openssl s_client -connect www.objc.io:443 -servername www.objc.io < /dev/null | openssl x509 -outform DER > objcio.cer
17:27 The saved file's size should be greater than zero, around 1 kB.
17:44 We add the certificate file to our project using menu option File
> Add Files, which automatically adds the file to the project's target.
18:32 Finally, we load the certificate file's contents into the view
controller's array of trusted certificates. By initializing the array with a
closure, the property can stay immutable. We force unwrap optionals, because if
reading the file fails, we want the app to crash:
let certificates: [Data] = {
let url = Bundle.main.url(forResource: "objcio", withExtension: "cer")!
let data = try! Data(contentsOf: url)
return [data]
}()
Discussion
21:01 Our application now makes sure the server it connects to is
really our server. This protects users from man-in-the-middle attacks, where a
fake certificate gets pushed onto their device, making the system trust a
potentially malignant server. It also protects us in case any of Apple's trusted
certificates get compromised.
22:04 By default, we need to have a commercial certificate in order to
pass ATS, the transport security system. We could allow arbitrary URLs and use a
self-signed certificate, but in doing so we'd have to undergo extra review by
Apple before our app is approved.
22:56 We've seen the simplest way to implement certificate pinning, in
that we're checking that the one certificate of our request equals the one on
our server. A more advanced option is to also check intermediate certificates.
23:21 It can be tough to work through the documentation of the
necessary C functions in order to figure out what you're supposed to do with
them. Rob wrote some open source
code that offers more
flexibility to the developer and not only compares but also evaluates
certificates. This is a more foolproof starting point than figuring everything
out from scratch.