AFNetworking SSL Pinning With Self-Signed Certificates

Rob Napier’s recent talk on iOS security inspired me to implement certificate pinning for an iOS app I am working on1. If you haven’t heard of it before, there is a good introduction to the technique over at the Open Web Application Security Project. The basic idea is that, if you control both the server and client that want to talk over https, there is no need to use third-party Certificate Authorities (of which there are many you explicitly trust without using pinning), because you already have knowledge of the certificate the server will use, so you can just take that server certificate and put it in your client. Then when the client wants to connect to the server it can just match the bundled certificate with the one from the server.

It’s a fairly new idea that gained traction back in 2011 when Chrome implemented pinning for which allowed it to expose a man-in-the-middle attack caused by a fraudulent SSL certificate issued by the CA DigiNotar.

AFNetworking 2.2.1 was recently released with updates to it’s SSL pinning with self-signed certificate abilities that makes this relatively easy to implement. The tricky part is all the different ways you can generate certificates, configuring the server, the different formats, and generally putting it all together. Below is how I got it to work for my app, hopefully it will be helpful to someone else looking to do the same thing.

The self-signed certificate question

The first problem I ran into is whether or not I should use a self-signed certificate, instead of a certificate issued by a third-party authority such as Verisign2. I chose to go the route of a self-signed certificate, because I wanted to pin my app to my own self-signed Certificate Authority (CA)3. This means that my app will connect to any server that uses certificates issued by my self-signed CA. This is safer then pinning to a specific certificate because I didn’t want to worry about updating clients for expiring certificates. This is Rob Napier’s advice from his talk mentioned above.

Note: If your server is also going to be used as a public facing website (and not just an API server like the one I am building), and want to serve content over HTTPS, you are going to need to go the third-party CA route, since if you serve a self-signed cert to a browser, the user will end up just seeing a distressing message about a bum certificate.

Create the CA and certs

Apple has a great technical note for creating self-signed CA’s and certs. It’s ostensibly about doing this for testing purposes, but it should work just fine for production apps using pinning. I followed the directions in the technical note exactly and ended up with all the certs I needed for implementing pinning.

Configuring the Server

After I created the appropriate certs, I needed to setup my server for SSL. I’m using nginx and it’s fairly straightforward to setup. I uploaded the exported self-signed certificate (the “p12” file) to the /etc/nginx/ssl directory on my server using scp:

$ scp ServerCertificate.p12

Note: Make sure to upload the p12 file to the server before extracting the private key and certificate so your private key stays safe during transit.

Next, I extracted the server certificate’s private key and cert (entering the password I set during export):

2 $ openssl pkcs12 -in ServerCertificate.p12 -nokeys -out server.crt $ openssl pkcs12 -in ServerCertificate.p12 -nocerts -nodes -out server.key

Now I had two files on my server, /etc/nginx/ssl/server.crt and /etc/nginx/ssl/server.key. I updated my nginx config (usually found at /etc/nginx/nginx.conf) to flip on ssl for my server:

 server {
    listen 443;
    root /path/to/my/root;
    ssl on;
    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

Note: You don’t need to have the CA root certificate on the server, or concatenate the CA root cert with the server.crt file or anything like that. I spent too much time working under that assumption and took a couple of wrong turns before realizing it wasn’t necessary.

After restarting nginx I made sure it was working by downloading the server’s certificate using openssl, like so:

$ openssl s_client -connect

This printed a bunch of information about the SSL certificate for The output had “Verify return code: 21 (unable to verify the first certificate)”. This just means that openssl couldn’t verify the root certificate (since it doesn’t know anything about my self-signed CA cert). When I passed in my CA certificate the SSL handshake resulted in “Verify return code: 0 (ok)”:

$ openssl s_client -connect -CAfile ./MyCACertificate.pem

Note: openssl expects the CA cert to be in PEM format. Convert it to PEM using openssl x509 -inform DER -in MyCACertificate.cer -out MyCACertificate.pem

Pinning the app with AFNetworking 2.2.1

AFNetworking 2.2.1 has been released that makes it easy to use self-signed certs for pinning (2.2.0 had some problems). All you need to do is to set the security policy of your AFURLSessionManager or AFHTTPRequestOperationManager subclass to use AFSSLPinningModeCertificate, like so:

// This usually would be a subclass of AFHTTPSessionManager
AFHTTPSessionManager *client = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:configuration];
client.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
client.securityPolicy.allowInvalidCertificates = YES;

One thing to note is I needed to set allowInvalidCertificates to YES because I am using self-signed certificates (I’m not a big fan of the name of that property. It really just means “allow self-signed certificates”).

Next, I added my CA certificate to my project (so AFNetworking can access it through the main bundle) with the extension cer, since AFNetworking specifically looks for certificates with that extension. The certificate needs to be in DER format to work, not the PEM format. If you created the CA certificate using Keychain Access, then it should already be in DER format (you can tell by inspecting the contents of the file. If it’s in binary then you’ve got a DER formatted certificate). If you only have it in PEM format you can convert it to DER like so:

$ openssl x509 -in MyCA.pem -outform der -out MyCA.cer

At this point I finally had SSL pinning with self-signed certificates working.

Thoughts, Questions, Concerns, and Confusion

  • If I wasn’t using AFNetworking then I would just use RNPinnedCertValidator, leaving everything else the same.

  • This is making me paranoid about my server’s private keys and my own computer’s security. I know just enough to be dangerous. This whole week-long investigation makes me appreciate the hell out of people who do sysops.

  • How does everyone else do pinning? Also, what’s the difference between certificate and public key pinning? Is one better than the other? Why are there two different ways to do this?

  • The iOS Security APIs are WAY behind of OS X’s. I’m guessing most of the discrepency is because on iOS you are supposed to fail if cert trust can’t be established, where on OS X you are given a chance to recover from failure by showing the user a dialog that allows them to set explicit trust. Although the SecTrustGetTrustResult function on OS X doesn’t do at all what the doc suggests. It returns the same exact set of constants that SecTrustEvaluate returns, even though the docs suggest it should return actual reasons why SecTrustEvaluate failed. It’s not at all a replacement for the deprecated SecTrustGetResult. As of 10.9 there is no non-deprecated way to recover from kSecTrustResultRecoverableTrustFailure. Time to fail some radars.

  • Protecting against MITM needs to be much easier than this. It’s too easy to leak data between an app and a server. Setting up SSL should be easier than this.

  • It’s also possible that I’m just a dummy and it IS easy for everyone else, which is something I suspect to be true.

  • Almost every single tutorial I read on generating self-signed certs recommend that you put your server’s hostname in the Common Name field, even though the HTTPS RFC clearly states that you should use the subjectAltName extension for hostname checking (only old ssl clients use the Common Name for hostname checking).

Let me know if I got anything wrong by messaging me on twitter. I am afterall a cryptography tourist.

  1. That story coming soon.

  2. You don’t need to use a self-signed certificate to get the benefits of pinning. Pinning works just as well with pinning to a cert issued by a third party, although you might run into problems when the certificate expires.

  3. Just like you can create your own certificates, you can create your own certificate authorities, and then use that certificate authority to issue server certificates.