44 votes

Comment puis-je mettre en œuvre l'épinglage des certificats SSL en utilisant React Native ?

J'ai besoin d'implémenter l'épinglage des certificats SSL dans mon application react native.

Je ne connais que très peu de choses sur SSL/TLS et encore moins sur le pinning. Je ne suis pas non plus un développeur mobile natif, bien que je connaisse Java et que j'aie appris Objective-C sur ce projet, suffisamment pour me débrouiller.

J'ai commencé à chercher comment exécuter cette tâche.

React Native ne l'implémente-t-il pas déjà ?

Non, ma recherche initiale m'a conduit à cette proposition qui n'a reçu aucune activité depuis le 2 août 2016.

J'ai appris que react-native utilise OkHttp, qui prend en charge l'épinglage, mais que je ne serais pas en mesure de le faire à partir de Javascript, ce qui n'est pas vraiment une exigence mais un plus.

Implémentez-le en Javascript.

Bien que react semble utiliser le runtime nodejs, il s'agit plus d'un navigateur que de node, ce qui signifie qu'il ne prend pas en charge tous les modules natifs, en particulier le module https, pour lequel j'avais implémenté l'épinglage des certificats de la manière suivante cet article . Ainsi ne pouvait-il pas le porter en réaction native.

J'ai essayé d'utiliser rn-nodeify mais les modules n'ont pas fonctionné. Cela a été vrai depuis RN 0.33 jusqu'à RN 0.35 sur lequel je suis actuellement.

Mise en œuvre à l'aide du plugin phonegap

J'ai pensé à utiliser un phongape-plugin Cependant, comme j'ai une dépendance sur des bibliothèques qui nécessitent react 0.32+, je ne peux pas utiliser react-native-cordova-plugin

Il suffit de le faire en mode natif

Bien que je ne sois pas un développeur d'applications natives, je peux toujours m'y essayer, ce n'est qu'une question de temps.

Android dispose de l'épinglage des certificats

J'ai appris qu'Android supporte Épinglage SSL mais sans succès car il semble que cette approche ne fonctionne pas avant Android 7. En plus de ne fonctionner que pour Android.

L'essentiel

J'ai épuisé plusieurs directions et je vais continuer à poursuivre une implémentation plus native, peut-être comprendre comment configurer OkHttp et RNNetworking puis peut-être revenir à react-native.

Mais existe-t-il déjà des implémentations ou des guides pour IOS et Android ?

53voto

Amr Draz Points 1376

Après avoir épuisé le spectre actuel des options disponibles en Javascript, j'ai décidé d'implémenter simplement l'épinglage de certificats de manière native ; tout semble si simple maintenant que j'ai terminé.

Passer aux en-têtes intitulés Solution Android y Solution IOS si vous ne voulez pas lire le processus pour arriver à la solution.

Android

Tras Recommandation de Kudo J'ai pensé à mettre en œuvre l'épinglage en utilisant 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();

J'ai d'abord commencé par apprendre à créer un fichier natif Pont Android avec react native en créant un module de toast. Je l'ai ensuite étendu avec une méthode pour envoyer une simple requête

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

Ayant réussi à envoyer une demande, je me suis ensuite tourné vers l'envoi d'une demande épinglée.

J'ai utilisé ces paquets dans mon dossier

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;

L'approche de Kudo n'était pas claire sur l'endroit où j'obtiendrais les clés publiques ou sur la façon de les générer. Heureusement Documents sur okhttp3 en plus de fournir une démonstration claire de la façon d'utiliser le CertificatePinner a déclaré que pour obtenir les clés publiques, tout ce que je devais faire était d'envoyer une demande avec un pin incorrect, et les pins corrects apparaîtront dans le message d'erreur.

Après avoir pris un moment pour réaliser que OkHttpClent.Builder() peut être enchaîné et que je peux inclure le CertificatePinner avant la construction, contrairement à l'exemple trompeur de la proposition de Kudo (probablement une ancienne version), j'ai trouvé cette méthode.

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

Ensuite, en remplaçant les trousseaux de clés publiques que j'ai obtenus dans l'erreur, j'ai récupéré le corps de la page, indiquant que j'avais effectué une demande avec succès. J'ai changé une lettre de la clé pour m'assurer qu'elle fonctionnait et je savais que j'étais sur la bonne voie.

J'avais finalement cette méthode dans mon fichier 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());
    }
}

Solution Android étendant le OkHttpClient de React Native

Avoir compris comment envoyer une requête http épinglée était une bonne chose, maintenant je peux utiliser la méthode que j'ai créée, mais idéalement j'ai pensé qu'il serait mieux d'étendre le client existant, afin de bénéficier immédiatement des avantages de la mise en œuvre.

Cette solution est valable à partir de RN0.35 et je ne sais pas comment cela se passera à l'avenir.

En cherchant des moyens d'étendre OkHttpClient pour RN, je suis tombé sur cet article expliquant comment ajouter le support TLS 1.2 en remplaçant le SSLSocketFactory.

En le lisant, j'ai appris que react utilise un OkHttpClientProvider pour créer l'instance OkHttpClient utilisée par l'objet XMLHttpRequest et donc que si nous remplaçons cette instance, nous appliquerons le pinning à toutes les applications.

J'ai ajouté un fichier appelé OkHttpCertPin.java à mon android/app/src/main/java/com/dreidev dossier

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

Ce paquet a une méthode extend qui prend un OkHttpClient existant et le reconstruit en ajoutant le certificatePinner et retourne la nouvelle instance construite.

J'ai ensuite modifié mon fichier MainActivity.java de la façon suivante le conseil de cette réponse en ajoutant les méthodes suivantes

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

Cette solution a été réalisée en faveur d'une réimplémentation complète de la méthode OkHttpClientProvider createClient, car en inspectant le fournisseur, je me suis rendu compte que la version maître avait implémenté la prise en charge de TLS 1.2 mais n'était pas encore une option disponible pour moi, et donc la reconstruction s'est avérée être le meilleur moyen d'étendre le client. Je me demande comment cette approche se comportera lors des mises à jour, mais pour l'instant, elle fonctionne bien.

Mise à jour Il semble qu'à partir de la 0.43, cette astuce ne fonctionne plus. Pour des raisons de temps, je vais geler mon projet à la 0.42 pour le moment, jusqu'à ce que la raison pour laquelle la reconstruction a cessé de fonctionner soit claire.

Solution IOS

Pour IOS, j'avais pensé que je devrais suivre une méthode similaire, en commençant à nouveau par la proposition de Kudo.

En inspectant le module RCTNetwork, j'ai appris que NSURLConnection était utilisé. Au lieu d'essayer de créer un module entièrement nouveau avec AFNetworking, comme le suggère la proposition, j'ai découvert que TrustKit

en suivant son guide de démarrage, j'ai simplement ajouté

pod 'TrustKit'

dans mon podfile et j'ai exécuté pod install

Le Guide de démarrage explique comment configurer ce pod à partir de mon fichier pList.mais préférant utiliser du code plutôt que des fichiers de configuration, j'ai ajouté les lignes suivantes à mon fichier 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];
.
.
.

J'ai obtenu les hachages des clés publiques de mon implémentation Android et cela a fonctionné (la version de TrustKit que j'ai reçue dans mes pods est 1.3.2).

J'étais content que l'IOS s'avère être un souffle

Comme une note latérale TrustKit a averti que son Auto-swizzle ne fonctionnera pas si le NSURLSession et la connexion sont déjà swizzled. Cela dit, il semble fonctionner bien jusqu'à présent.

Conclusion

Cette réponse présente la solution pour Android et IOS, étant donné que j'ai pu l'implémenter en code natif.

Une amélioration possible serait de mettre en œuvre un module de plateforme commune où la définition des clés publiques et la configuration des fournisseurs de réseau d'Android et d'IOS pourraient être gérées en javascript.

Proposition de Kudo Le simple fait d'ajouter les clés publiques au bundle js peut toutefois exposer une vulnérabilité, dans la mesure où le fichier du bundle peut être remplacé.

Je ne sais pas comment ce vecteur d'attaque peut fonctionner, mais l'étape supplémentaire consistant à signer le bundle.js comme proposé peut certainement protéger le bundle js.

Une autre approche peut être de simplement encoder le paquet js dans une chaîne de caractères de 64 bits et de l'inclure dans le code natif directement comme mentionnés dans la conversation de ce numéro . Cette approche a l'avantage d'obscurcir et de câbler le paquet js dans l'application, le rendant inaccessible aux attaquants, du moins je le pense.

Si vous avez lu jusqu'ici, j'espère vous avoir éclairé dans votre quête pour réparer votre bug et je vous souhaite une bonne journée ensoleillée.

1 votes

Je voulais juste ajouter : Premièrement, maintenant il y a aussi un kit de confiance pour Android et cela a quelques avantages. Deuxièmement, pour une raison quelconque, le swizzling ne fonctionne pas pour moi avec React Native sur ios, je n'arrive toujours pas à comprendre pourquoi. Et il semble qu'il n'y ait pas de moyen facile de l'intégrer au module de mise en réseau comme vous l'avez fait pour Android.

1 votes

Pas sûr que cette méthode fonctionne dans RN 0.44.0. Le client est remplacé mais Javascript peut encore faire des requêtes vers des domaines épinglés sans que des épingles valides soient définies.

2 votes

Si vous vous inquiétez de la réécriture du bundle JS par un pirate, vous devriez également vous inquiéter de la réécriture de l'application elle-même, non ? Dans quelles conditions pourrait-on modifier le paquet JS mais no être capable d'éditer l'application contenant les clés SSL intégrées ?

0voto

leLabrador Points 11

Vous pouvez utiliser cette librairie https://github.com/nlt2390/react-native-pinning-ssl

Il vérifie la connexion SSL en utilisant des clés SHA1, et non des certificats.

0 votes

Pour moi, ça ne fonctionne pas. J'ai une erreur : "Error : Failed isSSLvalid$ base64.js : 46 : 18 ..."

0 votes

@user3884677 avez-vous essayé de lier "react-native link react-native-pinning-ssl" ? Et si vous utilisez Expo, vous devez exécuter "npm run eject" avant de lier.

0 votes

Oui j'ai essayé de le lier et je n'utilise pas expo. Vous pouvez voir plus de détails dans ce lien : stackoverflow.com/questions/52160396/ .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X