A Year of React Native: SSL Pinning
We’ve been using React Native for over a year now and we’re loving how quickly we can create feature-rich and performant apps for iOS and Android. So far we’ve put 4 React Native apps into production. With each project we tried things out and learned lots along the way. We want to share some of that in this series of posts about React Native.
Edit 6th February 2019: Updated to fix the Android examples to work with React Native 0.54 and above. Thanks to Javier Muñoz for his article React Native SSL Pinning is back! — Android version which outlines this new approach for Android and also walks through how TrustKit can now also be used for Android.
Let's say you've built an app and a backend service. They communicate with each other over HTTPS, so requests and responses are encrypted using a public/private key pair. When your app initiates this communication, your backend sends its certificate (which contains the public key). Your app then checks that the certificate has been signed by a Certificate Authority (CA) that is trusted by the user's device.
There are a couple of ways your users and your backend service are at risk here:
1. Man-in-the-Middle attack - compromised CA. MitM attacks are easy with HTTP. Find an insecure wifi network (your local coffee shop) and redirect HTTP traffic to your own service that collects passwords and bank details. To do this with HTTPS though you'd need a valid certificate from a Certificate Authority that your user's device trusts. Though not a frequent occurrence, Certificate Authorities can be, and have been, hacked. With a compromised Certificate Authority, an attacker could generate a valid certificate for your backend's domain, and use that to intercept the HTTPS connection.
2. Man-in-the-Middle attack - compromised device. If one of your users' devices became compromised, an attacker could install a certificate onto the device. The device would then trust connections that are made using that certificate, so there wouldn't be any need to obtain a certificate signed by a Certificate Authority. They could then use that certificate to intercept the HTTPS connection.
3. Implementation discovery. Users also have the power to install certificates onto their devices and proxy traffic using tools like Charles Proxy. By doing this, they are able to discover implementation details about your backend service by decrypting the traffic. They'd then have a better chance of finding vulnerabilities to exploit in your backend.
SSL pinning narrows these avenues of attack by letting you define the exact certificate or public key that your app will accept when communicating with your backend.
Certificate or public key?
You can pin either, but pinning your public key has a clear advantage: they don't expire, whereas certificates do. When a certificate expires, you can obtain a new one using the same public/private key pair. If you've pinned the certificate, you'll need to replace it with your new certificate and force all of your users to update. If you've pinned the public key, you won't need to do anything.
Root, leaf or intermediate?
If you use a service like AWS Certificate Manager or Let's Encrypt your certificate and public key could change at any time. In those cases it makes more sense to pin an intermediate or root certificate's public key. You can read more about that and about backup methods on Scott Helme's blog.
React Native
We've implemented public key pinning in a few React Native projects recently. Here's how we did it.
First you'll need to obtain the certificate for your domain. You can do that like this:
$ openssl s_client \ -servername your.service.com \ -connect your.service.com:443 \ -prexit \ -showcerts
This will output the whole certificate chain with the leaf certificate at the top and the root certificate at the bottom. Copy the certificate to a new file and save as mycertificate.pem. The file contents should include the 'BEGIN' and 'END' delimiters like this:
-----BEGIN CERTIFICATE----- MIIE... -----END CERTIFICATE-----
iOS and Android need quite different approaches.
iOS
With iOS, you can use TrustKit. The setup is pretty simple, and you have the option of defining your domains and their public keys in code or in config.
TrustKit also comes with a handy tool for extracting public keys from certificates and converting them to Base64 encoded SHA256 hashes.
Clone the TrustKit repository and run this from it:
$ python get_pin_from_certificate.py path/to/the-root-certificate.pem
The output will include the public key hash like this:
kTSKPublicKeyHashes: @[@"PUBLIC_KEY_HASH_IS_HERE"] kTSKPublicKeyAlgorithms : @[kTSKAlgorithmRsa4096]
Android
Configuring pinning in Android is a bit more complicated. There is a version of TrustKit for Android and support for React Native is being discussed, but for now you'll need to do something a bit more manual. We did it with the help of this Stack Overflow answer.
Edit 6th February 2019: TrustKit can now be used with React Native for Android. See Javier Muñoz's article React Native SSL Pinning is back! — Android version
Create a new java file in android/app/src/main/java/com/example/app called OkHttpCertPin.java
It should look like this:
Then in MainActivity.java, add these imports:
and these methods:
If you have any questions or comments, you can find me @samueljmurray or @madebymany
Illustrations by Sam Russell Walker