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.