How to Implement Ssl Certificate Pinning While Using React Native

How can I implement SSL Certificate Pinning while using React Native

After exhausting the current spectrum of available options from Javascript I decided to simply implement certificate pinning natively it all seems so simple now that I'm done.

Skip to headers titled Android Solution and IOS Solution if you don't want to read through the process of reaching the solution.

Android

Following Kudo's recommendation I thought out to implement pinning using okhttp3.

client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
.add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
.add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
.add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
.build())
.build();

I first started by learning how to create a native android bridge with react nativecreating a toast module. I then extended it with a method for sending a simple request

@ReactMethod
public void showURL(String url, int duration) {
try {
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
Toast.makeText(getReactApplicationContext(), response.body().string(), duration).show();
} catch (IOException e) {
Toast.makeText(getReactApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
}
}

Succeeding in sending a request I then turned to sending a request pinned.

I used these packages in my file

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.CertificatePinner;
import java.io.IOException;

import java.util.Map;
import java.util.HashMap;

Kudo's approach wasn't clear on where I would get the public keys or how to generate them. luckily okhttp3 docs in addition to providing a clear demonstration of how to use the CertificatePinner stated that to get the public keys all I would need to do is send a request with an incorrect pin, and the correct pins will appear in the error message.

After taking a moment to realise that OkHttpClent.Builder() can be chained and I can include the CertificatePinner before the build, unlike the misleading example in Kudo's proposal (probably and older version) I came up with this method.

@ReactMethod
public void getKeyChainForHost(String hostname, Callback errorCallbackContainingCorrectKeys,
Callback successCallback) {
try {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient client = (new OkHttpClient.Builder()).certificatePinner(certificatePinner).build();

Request request = new Request.Builder()
.url("https://" + hostname)
.build();
Response response =client.newCall(request).execute();
successCallback.invoke(response.body().string());
} catch (Exception e) {
errorCallbackContainingCorrectKeys.invoke(e.getMessage());
}
}

Then replacing the public keychains I got in the error yielded back the page's body, indicating I had made a successful request, I change one letter of the key to make sure it was working and I knew I was on track.

I finally had this method in my ToastModule.java file

@ReactMethod
public void getKeyChainForHost(String hostname, Callback errorCallbackContainingCorrectKeys,
Callback successCallback) {
try {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/+Jg+cke8HLJNzDJB4qc1Aus14rNb6o+N3IrsZgZKXNQ=")
.add(hostname, "sha256/aR6DUqN8qK4HQGhBpcDLVnkRAvOHH1behpQUU1Xl7fE=")
.add(hostname, "sha256/HXXQgxueCIU5TTLHob/bPbwcKOKw6DkfsTWYHbxbqTY=")
.build();
OkHttpClient client = (new OkHttpClient.Builder()).certificatePinner(certificatePinner).build();

Request request = new Request.Builder()
.url("https://" + hostname)
.build();
Response response =client.newCall(request).execute();
successCallback.invoke(response.body().string());
} catch (Exception e) {
errorCallbackContainingCorrectKeys.invoke(e.getMessage());
}
}

Android Solution Extending React Native's OkHttpClient

Having figured out how to send pinned http request was good, now I can use the method I created, but ideally I thought it would be best to extend the existing client, so as to immediately gain the benefit of implementing.

This solution is valid as of RN0.35 and I don't know how it will fair in the future.

While looking into ways of extending the OkHttpClient for RN I came across this article explaining how to add TLS 1.2 support through replacing the SSLSocketFactory.

reading it I learned react uses an OkHttpClientProvider for creating the OkHttpClient instance used by the XMLHttpRequest Object and therefore if we replace that instance we would apply pinning to all the app.

I added a file called OkHttpCertPin.java to my android/app/src/main/java/com/dreidev folder

package com.dreidev;

import android.util.Log;

import com.facebook.react.modules.network.OkHttpClientProvider;
import com.facebook.react.modules.network.ReactCookieJarContainer;

import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.CertificatePinner;

public class OkHttpCertPin {
private static String hostname = "*.efghermes.com";
private static final String TAG = "OkHttpCertPin";

public static OkHttpClient extend(OkHttpClient currentClient){
try {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/+Jg+cke8HLJNzDJB4qc1Aus14rNb6o+N3IrsZgZKXNQ=")
.add(hostname, "sha256/aR6DUqN8qK4HQGhBpcDLVnkRAvOHH1behpQUU1Xl7fE=")
.add(hostname, "sha256/HXXQgxueCIU5TTLHob/bPbwcKOKw6DkfsTWYHbxbqTY=")
.build();
Log.d(TAG, "extending client");
return currentClient.newBuilder().certificatePinner(certificatePinner).build();
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
return currentClient;
}
}

This package has a method extend which takes an existing OkHttpClient and rebuilds it adding the certificatePinner and returns the newly built instance.

I then modified my MainActivity.java file following this answer's advice by adding the following methods

.
.
.
import com.facebook.react.ReactActivity;
import android.os.Bundle;

import com.dreidev.OkHttpCertPin;
import com.facebook.react.modules.network.OkHttpClientProvider;
import okhttp3.OkHttpClient;

public class MainActivity extends ReactActivity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
rebuildOkHtttp();
}

private void rebuildOkHtttp() {
OkHttpClient currentClient = OkHttpClientProvider.getOkHttpClient();
OkHttpClient replacementClient = OkHttpCertPin.extend(currentClient);
OkHttpClientProvider.replaceOkHttpClient(replacementClient);
}
.
.
.

This solution was carried out in favor of completely reimplementing the OkHttpClientProvider createClient method, as inspecting the provider I realized that the master version had implemented TLS 1.2 support but was not yet an available option for me to use, and so rebuilding was found to be the best means of extending the client. I'm wondering how this approach will fair as I upgrade but for now it works well.

Update It seems that starting 0.43 this trick no longer works. For timebound reasons I will freeze my project at 0.42 for now, until the reason for why rebuilding stopped working is clear.

Solution IOS

For IOS I had thought I would need to follow a similar method, again starting with Kudo's proposal as my lead.

Inspecting the RCTNetwork module I learned that NSURLConnection was used, so instead of trying to create a completely new module with AFNetworking as suggested in the proposal I discovered TrustKit

following its Getting Started Guide I simply added

pod 'TrustKit'

to my podfile and ran pod install

the GettingStartedGuide explained how I can configure this pod from my pList.file but preferring to use code than configuration files I added the following lines to my AppDelegate.m file

.
.
.
#import <TrustKit/TrustKit.h>
.
.
.
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

// Initialize TrustKit
NSDictionary *trustKitConfig =
@{
// Auto-swizzle NSURLSession delegates to add pinning validation
kTSKSwizzleNetworkDelegates: @YES,

kTSKPinnedDomains: @{

// Pin invalid SPKI hashes to *.yahoo.com to demonstrate pinning failures
@"efghermes.com" : @{
kTSKEnforcePinning:@YES,
kTSKIncludeSubdomains:@YES,
kTSKPublicKeyAlgorithms : @[kTSKAlgorithmRsa2048],

// Wrong SPKI hashes to demonstrate pinning failure
kTSKPublicKeyHashes : @[
@"+Jg+cke8HLJNzDJB4qc1Aus14rNb6o+N3IrsZgZKXNQ=",
@"aR6DUqN8qK4HQGhBpcDLVnkRAvOHH1behpQUU1Xl7fE=",
@"HXXQgxueCIU5TTLHob/bPbwcKOKw6DkfsTWYHbxbqTY="
],

// Send reports for pinning failures
// Email info@datatheorem.com if you need a free dashboard to see your App's reports
kTSKReportUris: @[@"https://overmind.datatheorem.com/trustkit/report"]
},

}
};

[TrustKit initializeWithConfiguration:trustKitConfig];
.
.
.

I got the public key hashes from my android implementation and it just worked (the version of TrustKit I received in my pods is 1.3.2)

I was glad IOS turned out to be a breath

As a side note TrustKit warned that it's Auto-swizzle won't work if the NSURLSession and Connection are already swizzled. that said it seems to be working well so far.

Conclusion

This answer presents the solution for both Android and IOS, given I was able to implement this in native code.

One possible improvement may be to implement a common platform module where setting public keys and configuring the Network providers of both android and IOS can be managed in javascript.

Kudo's proposal mentioned simply adding the public keys to the js bundle may however expose a vulnerability, where somehow the bundle file can be replaced.

I don't know how that attack vector can function, but certainly the extra step of signing the bundle.js as proposed may protect the js bundle.

Another approach may be to simply encode the js bundle into a 64 bit string and include it in the native code directly as mentioned in this issue's conversation. This approach has the benefit of obfuscating as well hardwiring the js bundle into the app, making it inaccessible for attackers or so I think.

If you read this far I hope I enlightened you on your quest for fixing your bug and wish you enjoy a sunny day.

How to secure the source code of react native application?

The Difference Between WHO and WHAT is Accessing the API Server

I want to know how to make my backend endpoints accept only requests that are coming from my application, not from anything else like Postman.

First, you need to understand the difference between WHO and WHAT is accessing the API Server to be in a better position to look for a solution to your problem.

I wrote a series of articles around API and Mobile security, and in the article Why Does Your Mobile App Need An Api Key? you can read in detail the difference between who and what is accessing your API server, but I will extract here the main takes from it:

The what is the thing making the request to the API server. Is it really a genuine instance of your mobile app, or is it a bot, an automated script or an attacker manually poking around your API server with a tool like Postman?

The who is the user of the mobile app that we can authenticate, authorize and identify in several ways, like using OpenID Connect or OAUTH2 flows.

So think about the who as the user your API server will be able to Authenticate and Authorize access to the data, and think about the what as the software making that request in behalf of the user.

When you grasp this idea and it's ingrained in your mindset, then you will look into mobile API security with another perspective and be able to see attack surfaces that you never though they existed before.

Certificate Pinning and MitM Atacks

What I was thinking of, is saving a secret on the client’s side that is to be sent with each request to the backend, so that I can make sure the request is coming from my app. I think SSL pinning is meant for this.

Certificate pinning on the mobile app side serves to guarantee that the app is talking only with your API server and not anything else, like when a MitM attack occurs and the app has its requests intercepted, and potentially modified and/or replayed, or simply saved to later extract the secrets from it.

Pinning doesn't guarantee to your API server that the request is coming indeed from what it expects, a genuine and unmodified version of your mobile app, "unless" you implement mutual pinning, that isn't encouraged to do so, because you will need to ship the private key for the API server certificate in the mobile app. Even if you do so, all an attacker needs to do is to extract the private key and will be able to communicate with your API server like if it was your genuine mobile app.

I don't have an article to implement pinning on a react-native mobile app but you can take a look to the one I wrote for Android to understand better all the process. Read my article Securing HTTPS with Certificate Pinning on Android on how you can implement certificate pinning and by the end you will understand how it can prevent a MitM attack.

In this article you have learned that certificate pinning is the act of associating a domain name with their expected X.509 certificate, and that this is necessary to protect trust based assumptions in the certificate chain. Mistakenly issued or compromised certificates are a threat, and it is also necessary to protect the mobile app against their use in hostile environments like public wifis, or against DNS Hijacking attacks.

You also learned that certificate pinning should be used anytime you deal with Personal Identifiable Information or any other sensitive data, otherwise the communication channel between the mobile app and the API server can be inspected, modified or redirected by an attacker.

Finally you learned how to prevent MitM attacks with the implementation of certificate pinning in an Android app that makes use of a network security config file for modern Android devices, and later by using TrustKit package which supports certificate pinning for both modern and old devices.

Bypassing Certificate Pinning

I think SSL pinning is meant for this.

The good news is that you already learned how good pinning is to prevent MitM attacks, now the bad news is that it can be bypassed, and yes I also wrote an article on how to it on Android (sorry to not be specific on react-native). If you want to learn the mechanics of it then read my article How to Bypass Certificate Pinning with Frida on an Android App:

Today I will show how to use the Frida instrumentation framework to hook into the mobile app at runtime and instrument the code in order to perform a successful MitM attack even when the mobile app has implemented certificate pinning.

Bypassing certificate pinning is not too hard, just a little laborious, and allows an attacker to understand in detail how a mobile app communicates with its API, and then use that same knowledge to automate attacks or build other services around it.

Code Obfuscation and Modifying Code

I know that anyone can access my app source code if they extract the APK file. I want to make sure that no one can alter or steal my source code.
Sorry, but once you release it to the public is up for grabs for everyone, even if heavily obfuscated its still possible to modify it statically or during runtime.

I read that I can make my code unreadable by Obfuscating it ( I still need to figure out how I am going to do that on my EAS build ), is this enough?

No, you can use the best obfuscation tool, but then an attacker well versed in deobuscation techniques will be able to understand your code and modify it statically or at runtime. Several open-source tools exist to ake this easy, and if you read the article to bypass certificate pinning then you already saw an example of doing it at runtime with Frida:

Inject your own scripts into black box processes. Hook any function, spy on crypto APIs or trace private application code, no source code needed. Edit, hit save, and instantly see the results. All without compilation steps or program restarts.

RASP - Runtime Application Self-Protection

And I have to use JailMonkey to detect if the device is rooted.

Using Frida the check can be modified to always return that the device is not rooted. Also JailMonkey may not detect all ways used to hide that a device is rooted, and this a moving target, because hackers and developers are in a constant cat and mouse game.

Sensitive Info Security

I am using Expo secure store to save my sensitive info on the client side.

Even when a secret is securely stored it will need to be used at some point, and the attacker will hook Frida to this point and extract the secret or do it in a MitM attack.

Possible Solutions

Is this approach good enough, is there anything I am missing?

From all I wrote it looks no matter what you are doomed to failure in properly secure your sensitive info and to guarantee that your API server knows that what is making the request is the genuine mobile app it expects, but security its all about of applying as many layers of defences as possible, like done in medieval castles, prisons, etc., because this will increase the level of effort, time and expertise required to succeed in an attack.

You now need to find a solution that allows you to detect MitM attacks, tampered and modified apk binaries, Frida present at runtime and that can deliver a runtime secret to mobile apps that pass a mobile app attestation that guarantees with a very high degree of confidence that such threats are not present. Unfortunately I don't know any open-source project that can deliver all this features, but a commercial solution exists (I work there), and if you want to learn more about you can read the article:

Hands-on Mobile App and API Security - Runtime Secrets Protection

In a previous article we saw how to protect API keys by using Mobile App Attestation and delegating the API requests to a Proxy. This blog post will cover the situation where you can’t delegate the API requests to the Proxy, but where you want to remove the API keys (secrets) from being hard-coded in your mobile app to mitigate against the use of static binary analysis and/or runtime instrumentation techniques to extract those secrets.

We will show how to have your secrets dynamically delivered to genuine and unmodified versions of your mobile app, that are not under attack, by using Mobile App Attestation to secure the just-in-time runtime secret delivery. We will demonstrate how to achieve this with the same Astropiks mobile app from the previous article. The app uses NASA's picture of the day API to retrieve images and descriptions, which requires a registered API key that will be initially hard-coded into the app.

Do You Want To Go The Extra Mile?

In any response to a security question I always like to reference the excellent work from the OWASP foundation.

For APIS

OWASP API Security Top 10

The OWASP API Security Project seeks to provide value to software developers and security assessors by underscoring the potential risks in insecure APIs, and illustrating how these risks may be mitigated. In order to facilitate this goal, the OWASP API Security Project will create and maintain a Top 10 API Security Risks document, as well as a documentation portal for best practices when creating or assessing APIs.

For Mobile Apps

OWASP Mobile Security Project - Top 10 risks

The OWASP Mobile Security Project is a centralized resource intended to give developers and security teams the resources they need to build and maintain secure mobile applications. Through the project, our goal is to classify mobile security risks and provide developmental controls to reduce their impact or likelihood of exploitation.

OWASP - Mobile Security Testing Guide:

The Mobile Security Testing Guide (MSTG) is a comprehensive manual for mobile app security development, testing and reverse engineering.

React-native : self-signed certification implementation

I use react-native-ssl-pinning, it's work fine with Certificate pinning.

And the real problem was coming from a bad install of server certificate.

react-native java.security.cert.CertPathValidatorException: Trust anchor for certification path not found

I managed to make the application connect with my API successfully, but I'm still researching the origin of the problem.

More context:

  • My mobile application is made by React Native + Expo.
  • I compiled a version for iOS and another for Android.
  • The problem just suddenly happened only in the Android
    (In all the android devices without have made any change in the mobile app or the server)
  • iOS can connect with the API without problem
  • The website can communicate with the API without problem

It seems to be a problem of the library axios (specifically axiosinstance).
Its weird because I didn't change anything in the android app, it just suddenly stopped to work.
(in all the android devices at the same time).

And just doing tests I realized that axiosinstance fail by doing the request (with the exception that I already showed you), but in an inexplicable way if the first request pointing to my API that I do in the app is by using axios it works perfectly, and after that axiosinstance is capable to perform any request to my API without fail.

I know it sounds weird, but now my app is working again.

By other Side, just to let you know, I also tested an android app called API Tester v5.6 and it fails connecting to my API giving me the same exception, but it last version API Tester 5.7 (which was released just some days ago) works without problem.

I also tried to connect to my API with ApiClient v2.4.7 and it fails with the same exception.

Again, I don't understand what is the real problem but definitely is not the certificate.

Anyway, now you know, maybe I should post this problem to the axios guys?

Regards!



Related Topics



Leave a reply



Submit