Strengthen TLS in React Native Through Certificate Pinning - iOS Edition

Friday 30 November 2018 By Skip Hovsmith

Topics: iOS, TLS, ReactNative

Enhance React Native’s networking API protection on Android and iOS without touching your Javascript code or manually editing the native code projects.

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.

chrome-https

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.

Certificate pinning is not as popular as it should be because of the perceived implementation and maintenance difficulties. With React Native, it is even more challenging, because the networking interface required to implement pinning is not exposed at the javascript layer.

Currently available packages supporting certificate pinning in React Native require replacement of the built in networking package or manual changes to native code. This npm package, react-native-cert-pinner, pins network fetches without requiring any changes to the javascript code. Underlying native code used to pin the connections is fully generated from a developer-specified JSON configuration file. This is a work in progress, currently available on Android and iOS, with additional package automation and security to follow.

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.

Man-in-the-Middle Attack

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
Hello World!

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
Installing react-native...

Next install the react-native-cert-pinner package:

$ cd example
$ npm install -S react-native-cert-pinner
+ react-native-cert-pinner@0.3.0
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
Scanning folders for symlinks in /Users/skiph/Projects/rn-pinning/rncp-test/example/node_modules (13ms)
rnpm-install info Linking react-native-cert-pinner ios dependency
rnpm-install info Platform 'ios' module react-native-cert-pinner has been successfully linked
rnpm-install info Linking react-native-cert-pinner android dependency
rnpm-install info Platform 'android' module react-native-cert-pinner has been successfully linked

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
$ cp ./node_modules/react-native-cert-pinner/example/index.js ./
$ cp -r ./node_modules/react-native-cert-pinner/example/src ./

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
...
** BUILD SUCCEEDED **

The following commands produced analyzer issues:
     Analyze .../RCTModuleMethod.mm normal x86_64
     Analyze .../RCTImageCache.m normal x86_64
     Analyze .../RCTNetInfo.m normal x86_64
(3 commands with analyzer issues)

Installing build/Build/Products/Debug-iphonesimulator/example.app
Launching org.reactjs.native.example.exampleorg.reactjs.native.example.example: 9252

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:

Public Key Hashes

Edit pinset.json to pin a few of these key hashes:

{
  "domains": {
    "*.approovr.io": {
      "pins": [
        "sha256/oq+Uj+2TYMg13txh1pXW0/VLAkonU3TnoPr5hfxPZVc=",
        "sha256/8Rw90Ej3Ttt8RRkrg+WYDS9n7IS03bk5bjP/UXPtaY8="
      ]
    }
  }
}

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
Reading config file './pinset.json'.
Updating plist file './ios/example/info.plist'.

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
./pinset.json

# default generated source files
./ios/example/info.plist

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:

{
  "domains": {
    "*.approovr.io": {
      "pins": [
        "sha256/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      ]
    }
  }
}

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.