web-dev-qa-db-fra.com

Comment épingler un certificat avec Square OKHTTP?

Je pense que je dois créer une nouvelle SSL Socket Factory? De plus, je ne veux pas utiliser le contexte SSL global ( https://github.com/square/okhttp/issues/184 ) pour des raisons évidentes.

merci!

MODIFIER:

Depuis okhttp 2.1.0, vous pouvez épingler des certificats très facilement.

Voir le code source ici pour commencer

45
Michael Barany

MISE À JOUR POUR OKHTTP 3.0

OKHTTP 3.0 a prise en charge intégrée pour épingler les certificats. Commencez par coller le code suivant:

 String hostname = "yourdomain.com";
 CertificatePinner certificatePinner = new CertificatePinner.Builder()
     .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
     .build();
 OkHttpClient client = OkHttpClient.Builder()
     .certificatePinner(certificatePinner)
     .build();

 Request request = new Request.Builder()
     .url("https://" + hostname)
     .build();
 client.newCall(request).execute();

Cela échouera car AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA n'est pas un hachage valide de votre certificat. L'exception levée aura les hachages corrects de votre certificat:

 javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
   Peer certificate chain:
     sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL
     sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA
     sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
     sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root
   Pinned certificates for publicobject.com:
     sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
   at okhttp3.CertificatePinner.check(CertificatePinner.Java)
   at okhttp3.Connection.upgradeToTls(Connection.Java)
   at okhttp3.Connection.connect(Connection.Java)
   at okhttp3.Connection.connectAndSetOwner(Connection.Java)

Assurez-vous de les ajouter à votre objet CertificatePinner et vous avez réussi à épingler votre certificat:

 CertificatePinner certificatePinner = new CertificatePinner.Builder()
   .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
   .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
   .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
   .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
   .build();

TOUT PASSÉ ICI IS POUR LES VERSIONS ANCIENNES (2.x) D'OKHTTP

Après avoir lu ce billet de blog j'ai pu modifier le concept pour l'utiliser avec OkHttp. Vous devez utiliser au moins la version 2.0 si vous souhaitez éviter d'utiliser un contexte SSL global.

Cette modification s'applique uniquement à l'instance actuelle d'OkHttp et modifie cette instance afin qu'elle uniquement accepte les certificats du certificat spécifié. Si vous souhaitez que d'autres certificats (comme celui de Twitter) soient acceptés, il vous suffit de créer une nouvelle instance OkHttp sans les modifications décrites ci-dessous.

1. Création d'un TrustStore

Pour épingler un certificat, vous devez d'abord créer un fichier de clés certifiées contenant ce certificat. Pour créer le magasin de confiance, nous utiliserons ce script pratique de nelenkov légèrement modifié pour nos besoins:

#!/bin/bash

if [ "$#" -ne 3 ]; then
  echo "Usage: importcert.sh <CA cert PEM file> <bouncy castle jar> <keystore pass>"
  exit 1
fi

CACERT=$1
BCJAR=$2
SECRET=$3

TRUSTSTORE=mytruststore.bks
ALIAS=`openssl x509 -inform PEM -subject_hash -noout -in $CACERT`

if [ -f $TRUSTSTORE ]; then
    rm $TRUSTSTORE || exit 1
fi

echo "Adding certificate to $TRUSTSTORE..."
keytool -import -v -trustcacerts -alias $ALIAS \
      -file $CACERT \
      -keystore $TRUSTSTORE -storetype BKS \
      -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
      -providerpath $BCJAR \
      -storepass $SECRET

echo "" 
echo "Added '$CACERT' with alias '$ALIAS' to $TRUSTSTORE..."

Pour exécuter ce script, vous avez besoin de 3 choses:

  1. Assurez-vous que keytool (inclus dans Android SDK) est sur votre $ PATH.
  2. Assurez-vous que le dernier fichier jar BouncyCastle est téléchargé dans le même répertoire que le script. (Télécharger ici )
  3. Le certificat que vous souhaitez épingler.

Maintenant, exécutez le script

./gentruststore.sh your_cert.pem bcprov-jdk15on-150.jar your_secret_pass

Tapez "oui" pour faire confiance au certificat, et une fois terminé mytruststore.bks sera généré dans votre répertoire actuel.

2. Appliquez votre TrustStore à votre projet Android

Créez un répertoire raw sous votre dossier res. Copie mytruststore.bks ici.

Voici maintenant une classe très simple qui épingle votre cert à OkHttp

import Android.content.Context;
import Android.util.Log;

import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import Java.io.InputStream;
import Java.io.Reader;
import Java.security.KeyStore;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;


/**
 * Created by martin on 02/06/14.
 */
public class Pinning {

    Context context;
    public static String TRUST_STORE_PASSWORD = "your_secret";
    private static final String ENDPOINT = "https://api.yourdomain.com/";

    public Pinning(Context c) {
        this.context = c;
    }

    private SSLSocketFactory getPinnedCertSslSocketFactory(Context context) {
        try {
            KeyStore trusted = KeyStore.getInstance("BKS");
            InputStream in = context.getResources().openRawResource(R.raw.mytruststore);
            trusted.load(in, TRUST_STORE_PASSWORD.toCharArray());
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(trusted);
            sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            Log.e("MyApp", e.getMessage(), e);
        }
        return null;
    }

    public void makeRequest() {
        try {
            OkHttpClient client = new OkHttpClient();
            client.setSslSocketFactory(getPinnedCertSslSocketFactory(context));

            Request request = new Request.Builder()
                    .url(ENDPOINT)
                    .build();

            Response response = client.newCall(request).execute();

            Log.d("MyApp", response.body().string());

        } catch (Exception e) {
            Log.e("MyApp", e.getMessage(), e);

        }
    }
}

Comme vous pouvez le voir, nous instancions une nouvelle instance de OkHttpClient et appelons setSslSocketFactory, en passant un SSLSocketFactory avec notre truststore personnalisé. Assurez-vous de définir TRUST_STORE_PASSWORD au mot de passe que vous avez passé dans le script Shell. Votre instance OkHttp ne doit désormais accepter que le certificat que vous avez spécifié.

70
Martin Konecny

C'est plus facile que je ne le pensais avec OkHttp.

Suivez ces étapes:

1. Obtenez les clés publiques sha1. La documentation OkHttp nous donne un moyen clair de le faire avec un exemple de code. Dans le cas où il disparaît, il est collé ci-dessous:

Par exemple, pour épingler https://publicobject.com , commencez avec une configuration cassée:

String hostname = "publicobject.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
    .add(hostname, "sha1/BOGUSPIN")
    .build();
OkHttpClient client = new OkHttpClient();
client.setCertificatePinner(certificatePinner);

Request request = new Request.Builder()
    .url("https://" + hostname)
    .build();
client.newCall(request).execute();   

Comme prévu, cela échoue avec une exception d'épinglage de certificat:

javax.net.ssl.SSLPeerUnverifiedException: échec de l'épinglage du certificat!
Chaîne de certificats d'homologue: sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw =: CN = publicobject.com, OU = PositiveSSL sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw =: CN = COMODO RSA Validation de domaine serveur sécurisé CA sha1WVOOWV = Autorité de certification sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c =: CN = racine de l'autorité de certification externe AddTrust

Certificats épinglés pour publicobject.com:

sha1/BOGUSPIN
sur com.squareup.okhttp.CertificatePinner.check (CertificatePinner.Java)
sur com.squareup.okhttp.Connection.upgradeToTls (Connection.Java)
sur com.squareup.okhttp.Connection.connect (Connection.Java)
sur com.squareup.okhttp.Connection.connectAndSetOwner (Connection.Java)

Suivi en collant les hachages de clé publique de l'exception dans la configuration du pinner de certificat:

Note latérale: Si vous faites cela sur Android vous obtiendrez une exception distincte si vous le faites sur un thread d'interface utilisateur, alors assurez-vous de le faire sur un thread d'arrière-plan.

2. Configurez votre client OkHttp:

OkHttpClient client = new OkHttpClient();
client.setCertificatePinner(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());

C'est tout ce qu'on peut en dire!

26
spierce7

Si vous n'avez pas accès au domaine (accès restreint par exemple) et que vous ne pouvez pas tester le hachage bidon, mais vous avez un fichier de certificat, vous pouvez utiliser openssl pour le récupérer:

openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
10
Grzegorz Pawełczuk

Pour développer l'exemple code source @ Michael-barany partagé, j'ai fait quelques tests et il semble que ce soit un exemple de code trompeur. Dans l'exemple de code, l'exception a noté 4 hachages sha1 de l'exception de chaîne de certificats:

javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL
sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA
sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root

puis ajouté les 4 hachages de clé publique sha1 au CertificatePinner Builder.

CertificatePinner 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();

Cependant, étant donné les tests que j'ai effectués et l'examen du code, seul le premier valide serait interprété, vous seriez donc mieux à même de n'inclure QUE l'un des hachages retournés. Vous pouvez utiliser le hachage le plus spécifique "DmxUShsZuNiqPQsX2Oi9uv2sCnw" pour le certificat de site précis ... ou vous pouvez utiliser le hachage le plus large "T5x9IXmcrQ7YuQxXnxoCmeeQ84c" pour la racine CA en fonction de la posture de sécurité souhaitée.

6
Dave Cobb

J'ai trouvé l'exemple mentionné dans la section Autorité de certification inconnue de ce lien developer.Android.com/training/articles/security-ssl = très utile.

Le SSLSocketFactory retourné dans context.getSocketFactory () peut ensuite être utilisé pour définir OkHttpClient dans la méthode setSslSocketFactory ().

Remarque: La section Autorité de certification inconnue mentionne également le lien pour télécharger un fichier de certificat à utiliser et vérifier ce code.

Voici l'exemple de méthode que j'ai écrit pour obtenir SSLSocketFactory

private SSLSocketFactory getSslSocketFactory() {
    try {
        // Load CAs from an InputStream
        // (could be from a resource or ByteArrayInputStream or ...)
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        // From https://www.washington.edu/itconnect/security/ca/load-der.crt
        InputStream caInput = getApplicationContext().getResources().openRawResource(R.raw.loadder);
        Certificate ca = null;
        try {
            ca = cf.generateCertificate(caInput);
            System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
        } catch (CertificateException e) {
            e.printStackTrace();
        } finally {
            caInput.close();
        }

        // Create a KeyStore containing our trusted CAs
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);
        if (ca == null)
            return null;
        keyStore.setCertificateEntry("ca", ca);

        // Create a TrustManager that trusts the CAs in our KeyStore
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);

        // Create an SSLContext that uses our TrustManager
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, tmf.getTrustManagers(), null);

        return context.getSocketFactory();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

Plus tard, je configure simplement ceci sur OkHttpClient comme ceci

httpClient.setSslSocketFactory(sslSocketFactory);

puis faites l'appel https

httpClient.newCall(requestBuilder.build()).enqueue(callback);
0
Vinayak