如何在使用React Native时实现SSL证书固定

Amr*_*raz 37 ssl android objective-c cordova react-native

我需要在我的反应本机应用程序中实现SSL证书固定.

我对SSL/TLS知之甚少,更不用说固定了.我也不是一个本地移动开发人员,虽然我了解Java并且在这个项目上学习了Objective-C足以绕过它.

我开始搜索如何执行此任务.

React Native没有实现这个吗?

不,我的初步搜索引导我查看自2016年8月2日以来未收到任何活动的提案.

从中我了解到react-native使用的OkHttp确实支持Pinning,但是我无法将其从Javascript中删除,这不是真正的要求,而是一个加号.

在Javascript中实现它.

虽然反应似乎使用了nodejs运行时,但它更像是一个浏览器而不是节点,这意味着它不支持所有本机模块,特别是https模块,我已经在本文后面实现了证书固定.因此无法将其带入本机反应.

我尝试使用rn-nodeify,但模块不起作用.自从我目前正在使用RN 0.33到RN 0.35以来,这是真的.

使用phonegap插件实现

我想过使用phongape-plugin然而因为我依赖于需要反应0.32+的库我不能使用react-native-cordova-plugin

只是本地做

虽然我不是本机应用程序开发人员,但我总是可以解决它,只是时间问题.

Android有证书固定

我了解到android支持SSL Pinning但是不成功,因为看起来这种方法在Android 7之前不起作用.以及仅适用于android.

底线

我已经用尽了几个方向,并将继续寻求更多本机实现,也许可以弄清楚如何配置OkHttp和RNNetworking然后可能会回到本地反应.

但是,IOS和android已经有任何实现或指南吗?

Amr*_*raz 47

在从Javascript中耗尽当前频谱的可用选项之后,我决定简单地实现本地证书固定,现在看来我已经完成了所有这些.

如果您不想阅读解决方案的过程,请跳至标题为Android SolutionIOS Solution的标题.

Android的

按照Kudo的建议,我考虑使用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();
Run Code Online (Sandbox Code Playgroud)

我首先学习如何使用react native创建一个toast模块创建一个本机android桥.然后我用一种发送简单请求的方法扩展它

@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();
    }
}
Run Code Online (Sandbox Code Playgroud)

成功发送请求后,我转向发送固定请求.

我在我的文件中使用了这些包

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;
Run Code Online (Sandbox Code Playgroud)

Kudo的方法并不清楚我将获得公钥或如何生成公钥.幸运的是,okhttp3文档除了提供如何使用CertificatePinner的明确演示之外,还说明要获取公钥,我需要做的就是发送一个带有错误引脚的请求,并且错误信息中会出现正确的引脚.

花了一点时间才意识到OkHttpClent.Builder()可以被链接,我可以在构建之前包含CertificatePinner,不像Kudo提案中的误导性示例(可能和旧版本),我提出了这种方法.

@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());
    }
}
Run Code Online (Sandbox Code Playgroud)

然后更换我在错误中得到的公共钥匙链产生了页面的正文,表明我已经成功请求,我更改了密钥的一个字母,以确保它正常工作,我知道我正在进行中.

我终于在ToastModule.java文件中使用了这个方法

@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());
    }
}
Run Code Online (Sandbox Code Playgroud)

Android解决方案扩展了React Native的OkHttpClient

已经弄清楚如何发送固定的http请求是好的,现在我可以使用我创建的方法,但理想情况下我认为最好扩展现有的客户端,以便立即获得实现的好处.

这个解决方案是有效的RN0.35,我不知道它将来会如何公平.

在研究扩展OkHttpClient for RN的方法时,我遇到了这篇文章,解释了如何通过替换SSLSocketFactory来添加TLS 1.2支持.

阅读它我学会了反应使用OkHttpClientProvider来创建XMLHttpRequest对象使用的OkHttpClient实例,因此如果我们替换该实例,我们会将pinning应用于所有应用程序.

我添加了一个名为OkHttpCertPin.javamy android/app/src/main/java/com/dreidevfolder的文件

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;
   }
}
Run Code Online (Sandbox Code Playgroud)

这个包有一个方法扩展,它接受现有的OkHttpClient并重建它添加certificatePinner并返回新构建的实例.

然后我按照这个答案的建议修改了我的MainActivity.java文件,添加了以下方法

.
.
.
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);
  }
.
.
.
Run Code Online (Sandbox Code Playgroud)

执行此解决方案有利于完全重新实现OkHttpClientProvider createClient方法,因为检查提供程序我意识到主版本已实现了TLS 1.2支持,但还不是我可以使用的选项,因此重建被发现是扩展客户的最佳方式.我想知道这种方法在升级时会如何公平,但现在效果很好.

更新似乎从0.43开始这个技巧不再有效.由于时间原因,我现在将我的项目冻结在0.42,直到为什么重建停止工作的原因很清楚.

解决方案IOS

对于IOS,我原本以为我需要遵循类似的方法,再次以Kudo的提议开始作为我的领导.

检查RCTNetwork模块我了解到使用了NSURLConnection,因此我没有尝试使用AFNetworking创建一个全新的模块,如我所提到的那样,我发现了TrustKit

按照其入门指南我简单地添加

pod 'TrustKit'
Run Code Online (Sandbox Code Playgroud)

到我的podfile并运行 pod install

GettingStartedGuide解释了我如何从我的pList.file配置这个pod,但更喜欢使用代码而不是配置文件我将以下行添加到我的AppDelegate.m文件中

.
.
.
#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];
.
.
.
Run Code Online (Sandbox Code Playgroud)

我从我的android实现中得到了公钥哈希,它刚刚起作用(我在我的pod中收到的TrustKit的版本是1.3.2)

我很高兴IOS原来是一口气

作为旁注,TrustKit警告说,如果NSURLSession和Connection已经被淘汰,那么Auto-swizzle将不起作用.这说它到目前为止似乎运作良好.

结论

这个答案为Android和IOS提供了解决方案,因为我能够在本机代码中实现它.

一种可能的改进可以是实现公共平台模块,其中可以在javascript中管理设置公钥和配置android和IOS的网络提供者.

Kudo的提议提到简单地将公钥添加到js包可能会暴露漏洞,在某种程度上可以替换捆绑文件.

我不知道攻击向量是如何起作用的,但是肯定是按照建议签署bundle.js的额外步骤可以保护js包.

另一种方法可能是简单地将js包编码为64位字符串,并将其直接包含在本机代码中,如本期对话中所述.这种方法的好处是可以将js捆绑软件连接到应用程序中,使得攻击者无法访问它,所以我认为.

如果你读到这里,我希望我能帮助你修复你的虫子并希望你享受阳光灿烂的日子.

  • 现在它的RN> 50.0,我想我们仍然需要一个解决方案:) (6认同)
  • 如果您担心黑客会重写JS捆绑软件,还应该担心应用程序本身会被重写,对吗?在什么情况下可以编辑JS捆绑包,但*不能*编辑包含SSL密钥的包含应用程序? (2认同)
  • 对于RN> 0.5,更好的是使用setOkHttpClientFactory并实现OkHttpClientFactory在MainApplication onCreate()方法中设置它OkHttpClientProvider.setOkHttpClientFactory(new OKHTTPIgnoreSSLFactory()); (2认同)