The first edition of this article implemented TLS certificate pinning for React Native apps on Android. Since then, the react-native-cert-pinner package has been enhanced to support pinning on iOS devices, and this edition of the post walks through the previous example for iOS.
Beginning in July 2018 with the 68 release, Chrome began marking all sites not running HTTPS (TLS over HTTP) as “not secure”. TLS uses site certificates to establish a chain of trust and encrypt communication at the transport layer.
SOURCE: Google Security Blog
This is a significant boost in networking, API and mobile security, but especially on mobile devices, it may not be enough. Unfortunately, it is too easy to spoof mobile devices into trusting certificates signed by unexpected certificate authorities. Certificate Pinning should be used to limit trust to website leaf certificates or only those intermediate or root authorities trusted by the app itself.
Preventing Man in the Middle Attacks
A Year of React Native: SSL Pinning does a nice job of describing risks to a mobile connection, even when using TLS. Compromises to a certificate authority or mobile device can cause an app to improperly trust a spoofed server certificate and allow an attacker to insert itself in the middle of the connection, silently decrypting, observing, possibly modifying, and re-encrypting supposedly secure communications.
Certificate Pinning builds on existing HTTPS (SSL or TLS over HTTP) techniques. With TLS, the mobile device follows the chain of certificates until it reaches a certificate signed by an authority it trusts.
Certificate pinning is used to identify specific certificates or limit the number of certificate authorities trusted to sign for a target website. By pinning a limited list of trusted server certificates within the app, fraudulently signed certificates, even if their certificate authorities are trusted by the device, will be rejected by the app. The app can pin a server’s leaf and intermediate certificates.
It is generally recommended to pin multiple certificate’s public keys so that the app can still trust one key if other keys are compromised.
SSL pinning is a mitigation method designed to reduce the effectiveness of MitM attacks enabled by spoofing a back-end server’s SSL certificate. Pinning on intermediate keys eases certificate rotation and renewals. Checking the hash of a public key is convenient and hides certificate information from any attackers.
React Native Example App
The npm react-native-cert-pinner module contains an example app which we will use to demonstrate certificate pinning. The app checks the HTTPS connection to the demo-server.approovr.io server:
|$ curl https://demo-server.approovr.io
Because TLS is not exposed through React Native networking calls such as fetch(), a native module must be introduced, and the Expo environment cannot be easily used for development.
Start by initializing a React Native project using react-native-cli:
|$ react-native init example
Next install the react-native-cert-pinner package:
|$ cd example
$ npm install -S react-native-cert-pinner
added 4 packages from 2 contributors and audited packages in 6.689s
found 0 vulnerabilities
Use react-native to automatically link the cert pinner native module:
$ react-native link
Delete the default index.js and App.js files and install index.js and src/ files from the example directory in the cert pinner package:
$ rm ./index.js ./App.js
For iOS, copy in the example podfile and install the pod manually:
$ cp ./node_modules/react-native-cert-pinner/example/ios/podfile ./ios/$ cd ios && pod install
You should be ready to build and run the app. Ensure an iOS simulator is running or an iOS device is connected, and launch the app:
$ react-native run-ios
The following commands produced analyzer issues:
From the opening screen, push the Test Hello button at the bottom of the screen. A successful connection will show a smiley face:
Although the connection was made successfully over TLS, certificate pinning was not used.
Pinning a Trusted Certificate
To add certificate pinning, start by initializing a pinset configuration file in the home directory of the example project:
$ npx pinset initFile './pinset.json' initialized.
Next, determine several public key hashes from the chain of certificates used for demo-server.approovr.io. Report URI has a convenient look up service at https://report-uri.com/home/pkp_hash. As this was being written, the available public key hashes are:
Edit pinset.json to pin a few of these key hashes:
In a production app, you would add pins for each server domain your app communicates with. If you connect to many servers, consider using an API proxy gateway to improve API protection and reduce the number of pin sets you need to manage.
Generate the required native project files by running pinset gen:
$ npx pinset gen
If you consider publishing hashes of public key certificates to be a security breach, you may want to remove or ignore the pinset configuration and generated files from your repository. In your root .gitignore file, add:
# default configuration file
Rebuild and launch the modified app. You should again see a successful connection, but this time the connection is pinned by at least one of the public key hashes.
Rejecting an Unrecognized Certificate
To test certificate pinning, change the *.approovr.io public key hashes in pinset.json so they do not match any of the expected values:
Regenerate the native project files by running pinset gen:
|$ npx pinset gen
Reading config file './pinset.json'.
Updating plist file './ios/example/info.plist'.
Rebuild and launch the modified app. This time you should see a connection failure, because the app could not find a public key hash which matched any of its expected pins.
Wrapping Up for Now
You have successfully demonstrated a pinning utility for React Native on iOS which uses the built-in fetch() API without requiring any manual edits of native iOS code. See the first edition of this article to follow the same example in Android.
Future package enhancements include:
- Automatically regenerating native source files whenever the pin set configuration changes.
- Adding source regeneration and git ignores to the mostly automatic react-native linking step.
- Adding certificate public key hash lookup to the pinset utility.
- Strengthening security of pinset information within the app.
By simplifying certificate pinning for React Native apps, the react-native-cert-pinner package should help more developers use these techniques to strengthen the integrity of their mobile API connections.