【问题标题】:How can I implement SSL Certificate Pinning while using React Native如何在使用 React Native 时实现 SSL 证书固定
【发布时间】:2017-03-07 12:34:05
【问题描述】:

我需要在我的 react native 应用程序中实现 SSL 证书固定。

我对 SSL/TLS 知之甚少,更不用说固定了。 我也不是本地移动开发人员,虽然我知道 Java 并且在这个项目中学习了 Objective-C,足以应付。

我开始寻找如何执行这个任务。

React Native 不是已经实现了吗?

不,我最初的搜索将我引导至 this proposal,它自 2016 年 8 月 2 日以来没有收到任何活动。

从中我了解到 react-native 使用了支持 Pinning 的 OkHttp,但我无法从 Javascript 中提取它,这不是真正的要求,而是一个加分项。

用 Javascript 实现。

虽然 react 似乎使用了 nodejs 运行时,但它更像是一个浏览器而不是 node,这意味着它不支持所有本机模块,特别是 https 模块,我在this article 之后为其实现了证书固定。因此无法将其带入 react native。

我尝试使用 rn-nodeify 但模块不起作用。从我目前的 RN 0.33 到 RN 0.35 开始,情况就是如此。

使用phonegap插件实现

我曾想过使用 phongape-plugin,但是由于我依赖于需要 react 0.32+ 的库,所以我不能使用 react-native-cordova-plugin

只做原生

虽然我不是本地应用程序开发人员,但我总是可以尝试一下,只是时间问题。

Android 有证书固定

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

底线

我已经用尽了几个方向,并将继续追求更多的原生实现,也许会弄清楚如何配置 OkHttp 和 RNNetworking,然后可能会桥接回 react-native。

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

【问题讨论】:

    标签: android objective-c cordova ssl react-native


    【解决方案1】:

    在用尽 Javascript 的当前可用选项范围后,我决定简单地在本机上实现证书固定,现在一切看起来都很简单,因为我已经完成了。

    如果您不想通读获得解决方案的过程,请跳至标题为 Android 解决方案IOS 解决方案 的标题。

    安卓

    Kudo's recommendation 之后,我想到了使用 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();
    

    我首先学习了如何创建一个原生的android bridge with react native创建一个 toast 模块。然后我用一种发送简单请求的方法对其进行了扩展

    @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();
        }
    }
    

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

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

    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 的方法并不清楚我将在哪里获得公钥或如何生成它们。幸运的是okhttp3 docs 除了提供如何使用 CertificatePinner 的清晰演示之外,还指出要获取公钥,我需要做的就是发送一个带有不正确 pin 的请求,并且正确的 pin 将出现在错误消息中。

    在意识到 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());
        }
    }
    

    然后替换我在错误中得到的公共钥匙串会返回页面的正文,表明我已经提出了成功的请求,我更改了密钥的一个字母以确保它可以正常工作并且我知道我在正轨上。

    我的 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());
        }
    }
    

    Android 解决方案扩展 React Native 的 OkHttpClient

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

    此解决方案自RN0.35 起有效,我不知道它将来会如何公平。

    在研究为 RN 扩展 OkHttpClient 的方法时,我遇到了 this article,它解释了如何通过替换 SSLSocketFactory 添加 TLS 1.2 支持。

    通过阅读,我了解到 react 使用 OkHttpClientProvider 来创建 XMLHttpRequest 对象使用的 OkHttpClient 实例,因此如果我们替换该实例,我们将对所有应用程序应用固定。

    我在android/app/src/main/java/com/dreidev 文件夹中添加了一个名为OkHttpCertPin.java 的文件

    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;
       }
    }
    

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

    然后我通过添加以下方法修改了 this answer's advice 之后的 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);
      }
    .
    .
    .
    

    执行此解决方案是为了完全重新实现 OkHttpClientProvider createClient 方法,在检查提供程序时我意识到 the master version 已经实现了 TLS 1.2 支持但还不是可供我使用的可用选项,因此找到了重建成为扩展客户的最佳方式。我想知道当我升级时这种方法如何公平,但现在它运作良好。

    更新似乎从 0.43 开始这个技巧不再起作用了。由于时间限制,我现在将我的项目冻结在 0.42,直到重建停止工作的原因明确为止。

    解决方案IOS

    对于 IOS,我认为我需要遵循类似的方法,再次以 Kudo 的提议作为我的主导。

    检查 RCTNetwork 模块后,我了解到使用了 NSURLConnection,因此我发现 TrustKit

    按照我简单添加的入门指南

    pod 'TrustKit'
    

    到我的 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];
    .
    .
    .
    

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

    我很高兴IOS变成了一口气

    作为旁注,TrustKit 警告说,如果 NSURLSession 和 Connection 已经被 swizzled,它的 Auto-swizzle 将不起作用。也就是说,到目前为止它似乎运作良好。

    结论

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

    一个可能的改进可能是实现一个通用平台模块,其中设置公钥和配置 android 和 IOS 的网络提供程序都可以在 javascript 中进行管理。

    Kudo's proposal 提到简单地将公钥添加到 js 包中可能会暴露一个漏洞,其中可以以某种方式替换包文件。

    我不知道该攻击向量如何发挥作用,但可以肯定的是,按照提议对 bundle.js 进行签名的额外步骤可能会保护 js 包。

    另一种方法可能是将 js 包简单地编码为 64 位字符串,并将其直接作为 mentioned in this issue's conversation 包含在本机代码中。这种方法的好处是混淆以及将 js 包硬连线到应用程序中,使攻击者无法访问它,或者我认为是这样。

    如果您读到这里,我希望我对您修复错误的追求有所启发,并祝您享受阳光明媚的一天。

    【讨论】:

    • 只是想补充一下:首先,现在还有android的信任工具包,它有一些好处。其次,由于某种原因,我在 ios 上的 React Native 上的调配对我不起作用,仍然无法弄清楚为什么。而且看起来没有像在 Android 中那样将它集成到 Networking 模块的简单方法。
    • 不确定此方法在 RN 0.44.0 中是否有效。客户端已被替换,但 Javascript 仍可以在未设置有效 pin 的情况下向固定域发出请求。
    • 如果你担心 JS 包被黑客重写,你也应该担心应用程序本身被重写,对吗?在什么情况下可以编辑 JS 包但能够编辑嵌入了 SSL 密钥的包含应用程序?
    • 现在是 RN > 50.0,我认为我们仍然需要解决这个问题 :)
    • 对于 RN >0.5 更好的是使用 setOkHttpClientFactory 并实现 OkHttpClientFactory 在 MainApplication 的 onCreate() 方法中设置 OkHttpClientProvider.setOkHttpClientFactory(new OKHTTPIgnoreSSLFactory());
    【解决方案2】:

    你可以使用这个库https://github.com/nlt2390/react-native-pinning-ssl

    它使用 SHA1 密钥而不是证书来验证 SSL 连接。

    【讨论】:

    • 对我来说,它不起作用。我有一个错误:“错误:失败 isSSLvalid$ base64.js: 46: 18 ...”
    • @user3884677 你试过链接“react-native link react-native-pinning-ssl”吗?如果你使用 Expo,你需要在链接之前运行“npm runeject”。
    • 是的,我尝试链接它,但我不使用 expo。您可以在此链接中查看更多详细信息:stackoverflow.com/questions/52160396/…
    • 我尝试创建一个新项目并添加了这个插件,完全没有问题。你的 RN 是什么版本的?
    • @user3884677 我可以在 android 4.4.4 中重现此错误。请在我调试时尝试使用 android > 5.0.0。谢谢
    猜你喜欢
    • 1970-01-01
    • 2018-01-02
    • 1970-01-01
    • 2021-06-11
    • 1970-01-01
    • 2021-12-04
    • 2020-08-02
    • 2014-04-01
    • 2019-09-17
    相关资源
    最近更新 更多