SSL is a tricky beast, which isn't overly well understood by most developers. The common mis-conception (or partial understanding) is that it's about encryption: hiding the content as it passes from my device to the server and back.
Just want code to do certificate pinning? The GitHub repo is here. It's now (2017-01-09) in Swift 3.0, but the 2.0 code is tagged.
But thats only half the story. One of the main features of SSL [1] is trust.
For an SSL connection to be setup, an encryption key needs to be sent from the server to the client. This is where the well known public/private key pair comes in - the client creates a key, encrypts it with the servers public key (part of the certificate data which is sent from the server), and the server can decrypt it and use it. After that, every thing is encrypted with a symmetric protocol like AES, DES etc.
But to do this, both parties must trust each other, either directly, or via a proxy relationship. This is where certificates and certificate chains come in.
If you open an HTTPS site in your browser, or use a tool like openssl s_client
, you can see the chain
This states that we are using the leaf (bottom) certificate for our communications, but we are signed by the node above us (Let's Encrypt), which is in turn signed by the node above that. Eventually, one of the certificates (usually the top one) is pre-installed into the browser, so the browser knows to trust anything which is indirectly signed by one of these certificates.
So, given the chain above, my certificate is presented as the one to trust, but is validated by being signed by the Let's Encrypt intermediate, which is signed by the DST Root CA X3 Root, which is already in Chrome. The chain is similar for other websites such as banks:
In this case, this banks certificate is a "Class 3 EV" certificate. This doesn't provide any extra protection during communication, but it's very difficult to get these - you have to go to some fairly great lengths before they are issued, as the CA needs to verify who you are to a very high degree.
As of March 2017, Google have announced that they will no longer trust Symantec certs, so pinning to the EV, in this case, will cause issues, 'cos it's likely to be changed in 12 months. Idea is still good, just pick a more trustworthy CA and plan for even the intermediate to be revoked.
Now, what happens when the browser can't find a pre-trusted root certificate? You get an error message like this:
This doesn't mean that your communication is unencrypted - just that no chain of trust could be established, so the browser can't tell who it's talking to. It might be the server (most likely), but it could be the NSA for all it knows. Hence the warning.
Side note: You can do all of this with self-signed certificates, tho you'd need to include your CA Root certificate in your app, and make the network stack trust it. Or just pin to some part of the chain and ignore any errors around not trusting the chain. However, with the advent of Let's Encrypt, there is very little reason to self-sign anymore. They are just as secure, however, if setup correctly.
One of the other, not well used features of SSL is client certificates. All the verification above is about the server proving to the client who it is. Client Certificates do it the other way: the client provides a signed certificate, with a similar chain of trust, which the server can verify. This can be used in place of a username/password login, or just as an extra level of checking. Management of the certificates can be problematic across browsers, so it's not used a lot. More info here, tho to be honest, there isn't much out there on how to do it.
Man in the Middle attacks are still possible, even with certificates and chains of trust: you could be connecting via a proxy (cafe, hotel, place of work) which intercepts all your HTTP(S) traffic, acting as the server so your browser thinks it's talking to the server, but the proxy is talking to the server on your behalf - while able to see all the content as it goes past (and possibly change it).
The mitigation for this potential attack is called certificate pinning. At its core, it's similar to client certificates - the client has some piece of information which it can provide or use to validate the connection beyond what the server has provided. But in this case, the client has some part of the expected server certificate which it can check against.
In most cases, this information is the public key used to generate the certificate, which is part of the certificate. This key is used to encrypt and send data back to the server, which has the private key and can decrypt it. As this is a fairly large piece of data (2048 bits usually), it's normally stored as a sha1
or sha256
hash, for quick string comparison.
To do a certificate pinning check, you need to do the following steps when the client connects to the server:
- The client library (Foundation, OkHttp, HttpClient etc) usually provides the server url as well as the provided certificate(s).
- The client then processes the certificate and extracts the public key block (2048 bit block of binary). On Android, this is a fairly trivial process as it's built into the cut down Bouncy Castle shipped with Android, but on iOS this is a fairly involved process involving either the Keychain or OpenSSL. .NET is also fairly easy. More on iOS later.
- Based on the extracted public key (hash), it's compared to a list of known hashes and the connection is either accepted or rejected.
One of the other considerations is WHAT to pin to. In the screen shot above, you have a choice of three certificates: the server's certificate, the intermediate, or the root, but there may be more levels. Each has it's own up and down sides:
- Server certificates are usually regenerated every 12-24 months, so if you pin to one of those, and your app is meant to stick around for a while without being upgraded, there is a good chance that it'll stop working when the certificate is changed. This level provides the highest level of trust, tho, as it's your certificate, but also the highest level of risk: when you regenerate your certificate (at least every 12-24 months) the pinning may break.
- The intermediate certificate directly above your server certificate. This is normally the one for the company your paid for your certificate. These usually last a lot, lot longer (20+ years), and are usually the best place to pin: you picked your provider based on their security (didn't you?) so they should have processes in place so they don't issue a valid certificate for your domain to someone else. With that assumption, and the massively lowered risk of pinning to a long lived certificate, this is usually the best place to pin to.
- Something further up the chain. You can pin to these, but they don't really give any more trust than the intermediate certificate, while expanding the range of lower-level certificates - you might go from accepting something which is an EV 3 certificate (expensive, hard to get, lots of checking), to anything signed by Verisign. Still trusty, but they use this root to (indirectly) sign their low-trust, cheap ones too.
Note that if your CA is compromised, and has to regenerate the intermediate, this will also invalidate your server certificate, so there is the same risk there as the case where your own certificate is compromised.
So, the general rule is:
- If you have a user base which upgrades quickly, or you can force the client to upgrade quickly (web apps), pinning to the leaf is an ok option. If you have full control over the whole chain (self-signed), the leaf node is not a bad option.
- For pretty much everything else, pin to the provider of your server certificate. This has the best risk to trust ratio.
The OWASP page on pinning has some more info on this.
So, in short, the parts of SSL are:
- Encryption - stopping the message being read by a 3rd party in transit.
- Trust - making sure you are talking to the server you think you are (certificates, certificate pinning)
- Authentication - proving to the server that you are who you say you are (client certificates)
So, how do we do this on iOS? If you are using a library like AlamoFire or AFNetworking, it's usually baked into the library. OkHttp does it on Android, and HttpClient should support it out of the box on .NET/Xamarin using ServicePointManager
.
But if you're using the built in iOS stuff - NSURLConnection
, NSURLSession
- you'll need to do it somewhat manually. The steps are something like:
- Build a list of hashes which you will accept. This could be one or more, tho usually it's just one.
- Intercept
NSURLConnection
orNSURLSession
to get passed the challenge. There are methods onNSURLConnectionDelegate
andNSURLSessionDelegate
which do this. - Extract the public keys from the provided server certificates, and generate a hash
- Compare the hash to the pre-loaded list, and accept or reject based on that list.
This sounds easy, except for the 3rd item - iOS doesn't provide any easy way to get the public key out of the certificate, you have to load the certificate into the keychain, then query the keychain for the public key. It's awkward.
However, I've written a Swift library (class) which does exactly this. It's based on AlamoFire and AFNetworking, but extracts out just the certificate pinning bits, and it's quite easy to use:
print("being challanged! for \(challenge.protectionSpace.host)")
guard let trust = challenge.protectionSpace.serverTrust else {
print("invalid trust!")
completionHandler(.CancelAuthenticationChallenge, nil)
return
}
let credential = NSURLCredential(trust: trust)
let pinner = setupCertificatePinner()
if (!pinner.validateCertificateTrustChain(trust)) {
print("failed: invalid certificate chain!")
challenge.sender?.cancelAuthenticationChallenge(challenge)
}
if (pinner.validateTrustPublicKeys(trust)) {
completionHandler(.UseCredential, credential)
} else {
print("couldn't validate trust for \(challenge.protectionSpace.host)")
completionHandler(.CancelAuthenticationChallenge, nil)
}
Full documentation is on the GitHub site. Happy pinning!
I'm going to use SSL to represent SSL, TLS and generally everything with an S on the end of the HTTP prefix. While they differ in a lot of ways, in this context they are all the same. ↩︎