We're Hiring!

Strengthen TLS in React Native Through Certificate Pinning

Pinning concept; colourful t-shirts pegged on washing line

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.

This article and code were written for Approov 1. Though the concepts are equally valid for Approov 2, some code examples may need updating for Approov 2. Please visit the Approov 2 docs for current code examples.

Chrome https reporting

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, with iOS to follow, along with additional package automation and security.

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

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 thedemo-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 ./

You should be ready to build and run the app. Ensure an Android emulator is running or an Android device is connected and launch the app:

$ react-native run-android
...
installing APK 'app-debug.apk' on 'Pixel_2_API_25' for app:debug
Installed on 1 device.

BUILD SUCCESSFUL

Total time: 15.768 secs
Running adb -s emulator-5554 reverse tcp:8081 tcp:8081
Starting the app on emulator-5554 (adb -s emulator-5554 shell am start -n com.example/com.example.MainActivity)
...
Starting: Intent { cmp=com.example/.MainActivity }

From the opening screen, push the TEST HELLO button. A successful connection will show a smiley face:

React Native app screenshot

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 init
File './pinset.json' initialized.

Next, determine several public key hashes from the chain of certificates used for demo-server.approovr.io

SSL Labs provides an SSL server test at https://www.ssllabs.com/ssltest/index.html:

ssl-labs-hashSpecify demo-server.approovr.io as the host and allow the SSL test to complete. In the results, each certificate will have a subject field will show the pinning hash you can use.

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'.
Writing java file './android/app/src/main/java/com/criticalblue/reactnative/
  GeneratedCertificatePinner.java'.

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 android source
./android/app/src/main/java/com/criticalblue/reactnative/GeneratedCertificatePinner.java

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.

React Native app screenshot 2

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'.
Writing java file './android/app/src/main/java/com/criticalblue/reactnative/
  GeneratedCertificatePinner.java'.

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.

Approov shapes app failed request screenshot

Wrapping Up For Now

You have successfully demonstrated a pinning utility for React Native on Android which uses the built-in fetch() API without requiring any manual edits of native Android code.

Future enhancements include:

  • Adding equivalent iOS support.
  • 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.

 

Skip Hovsmith

- Senior Consultant at Approov
Developer and Evangelist - Software Performance and API Security - Linux and Android Client and Microservice Platforms