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 google.com 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:
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):
Now I had two files on my server,
/etc/nginx/ssl/server.key. I updated my nginx config (usually found at
/etc/nginx/nginx.conf) to flip on ssl for my server:
1 2 3 4 5 6 7 8
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:
This printed a bunch of information about the SSL certificate for myserver.com. 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)”:
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
AFHTTPRequestOperationManager subclass to use
AFSSLPinningModeCertificate, like so:
1 2 3 4
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:
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
SecTrustGetTrustResultfunction on OS X doesn’t do at all what the doc suggests. It returns the same exact set of constants that
SecTrustEvaluatereturns, even though the docs suggest it should return actual reasons why
SecTrustEvaluatefailed. 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).
That story coming soon.↩
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.↩
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.↩