Как прикрепить сертификат к Square OKHTTP?

Я думаю, мне нужно создать новую фабрику сокетов SSL? Кроме того, я не хочу использовать глобальный контекст SSL (https://github.com/square/okhttp/issues/184) по понятным причинам.

Спасибо!

ИЗМЕНИТЬ:

Начиная с okhttp 2.1.0 вы можете очень легко прикреплять сертификаты.

исходный код здесь для начала


person Michael Barany    schedule 03.06.2014    source источник
comment
Проверьте здесь самое простое решение: stackoverflow.com/a/45855405/3448003   -  person Bajrang Hudda    schedule 24.08.2017


Ответы (5)


ОБНОВЛЕНИЕ ДЛЯ ОХТТП 3.0

OKHTTP 3.0 имеет встроенную поддержку для закрепления сертификатов. Начните с вставки следующего кода:

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

Это не удастся, потому что AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA не является допустимым хэшем вашего сертификата. Вызванное исключение будет иметь правильные хэши вашего сертификата:

 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)

Убедитесь, что вы добавили их в свой объект CertificatePinner, и вы успешно закрепили свой сертификат:

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

ВСЕ ПРОШЛОЕ ЗДЕСЬ ДЛЯ СТАРЫХ (2.x) ВЕРСИЙ OKHTTP

Прочитав эту запись в блоге Мне удалось изменить концепцию для использования с OkHttp. Вам следует использовать как минимум версию 2.0, если вы хотите избежать использования глобального контекста SSL.

Это изменение применяется только к текущему экземпляру OkHttp и изменяет этот экземпляр таким образом, что он только принимает сертификаты из указанного сертификата. Если вы хотите, чтобы принимались другие сертификаты (например, сертификат из Twitter), вам просто нужно создать новый экземпляр OkHttp без модификаций, описанных ниже.

1. Создание хранилища доверия

Чтобы закрепить сертификат, вам сначала нужно создать хранилище доверенных сертификатов, содержащее этот сертификат. Для создания доверенного хранилища воспользуемся вот этим удобным скриптом от nelenkov, немного модифицированным для наших целей:

#!/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..."

Для запуска этого скрипта вам нужно 3 вещи:

  1. Убедитесь, что keytool (входит в Android SDK) находится в вашей переменной $PATH.
  2. Убедитесь, что у вас есть последний загружаемый файл jar BouncyCastle в том же каталоге, что и скрипт. (Скачать здесь)
  3. Сертификат, который вы хотите закрепить.

Теперь запустите скрипт

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

Введите «да», чтобы доверять сертификату, и по завершении mytruststore.bks будет сгенерирован в вашем текущем каталоге.

2. Примените TrustStore к своему проекту Android

Создайте каталог raw в папке res. Скопируйте mytruststore.bks сюда.

Вот очень простой класс, который прикрепляет ваш сертификат к 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);

        }
    }
}

Как видите, мы создаем новый экземпляр OkHttpClient и вызываем setSslSocketFactory, передавая SSLSocketFactory с нашим пользовательским хранилищем доверенных сертификатов. Убедитесь, что вы установили TRUST_STORE_PASSWORD на пароль, который вы передали в сценарий оболочки. Теперь ваш экземпляр OkHttp должен принимать только указанный вами сертификат.

person Martin Konecny    schedule 03.06.2014
comment
Пожалуйста, не создавайте OkHttpClient для каждого запроса. Создайте один экземпляр и повторно используйте его для каждого запроса. - person Jake Wharton; 03.06.2014
comment
Это пример кода, конечно, вы не будете делать это для каждого запроса - обратите внимание, что URL-адрес также жестко запрограммирован. - person Martin Konecny; 03.06.2014
comment
Благодарю. какая польза от пароля? кто-то может просто скопировать мой APK и увидеть пароль. Есть ли способ создать магазин, не требуя пароля? - person Michael Barany; 03.06.2014
comment
Насколько я понимаю, сертификат - это не то, что вы должны хранить в тайне. Ваш сервер отправляет его клиенту для каждого рукопожатия SSL. В этом случае требуется пароль, поскольку keytool обычно предназначен для хранения закрытых ключей, а не сертификатов. К сожалению, нет возможности отключить пароли, поэтому используйте все, что вам нравится. - person Martin Konecny; 03.06.2014
comment
@JakeWharton Когда у нас есть один экземпляр OkHttpClient и мы пытаемся делать параллельные/одновременные запросы из разных фоновых потоков (также с помощью enqueue ), запросы по-прежнему синхронизируются, а ответы также принимаются один за другим. Можно ли как-то преодолеть это поведение, используя только один экземпляр okhttpclient? - person Gunhan; 01.12.2015
comment
Замечательный ответ! За исключением того, что сейчас он устарел с okhttp3: OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(getPinnedCertSslSocketFactory(context)) .build(); - person xorgate; 17.06.2016
comment
На самом деле это не закрепляет сертификат. Это добавит ваш сертификат в качестве корневого сертификата. Это означает, что каждый сертификат, подписанный добавленным вами, также действителен. - person Agent_L; 18.11.2016
comment
@agent_l это неправильно. Он примет только один созданный вами сертификат. Все остальные сертификаты будут недействительны. - person Martin Konecny; 18.11.2016
comment
@MartinKonecny ​​Я проверил ваш способ с BKS (созданным с помощью portecle), и результат тот же: корневой сертификат в BKS, производный сертификат на веб-сайте, и соединение установлено, хотя этого не должно быть. - person Agent_L; 18.11.2016
comment
java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: якорь доверия для пути сертификации не найден. - person Aderbal Nunes; 09.12.2016
comment
Как получить хэши для OKHTTP 3 через командную строку или другие инструменты? - person Sebastian Roth; 27.01.2017
comment
@SebastianRoth openssl x509 -inform der -in ‹сертификат› -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha1 -binary | openssl enc -base64 выводит sha1 в base64 вашего открытого ключа, если это то, что вы искали... - person Greg; 08.03.2017
comment
Люди с файлом pem могут использовать openssl x509 -inform pem -in ‹certificate› -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha1 -binary | openssl enc -base64 спасибо @estoke - person silentsudo; 22.03.2017
comment
@sajadabbasi это один из недостатков закрепления сертификатов в целом. - person Martin Konecny; 30.11.2018
comment
@MartinKonecny, нет ли способа решить эту проблему? - person sajad abbasi; 30.11.2018
comment
как сгенерировать хэш-ключ ..? - person Atif AbbAsi; 22.05.2021

Это проще, чем я думал, с OkHttp.

Следуй этим шагам:

<сильный>1. Получите открытые ключи sha1. Документация OkHttp дает нам ясный способ сделать это вместе с примером кода. На случай, если он исчезнет, ​​вот он вставлен ниже:

Например, чтобы закрепить https://publicobject.com, начните с неработающей конфигурации:

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

Как и ожидалось, это не удается с исключением привязки сертификата:

javax.net.ssl.SSLPeerUnverifiedException: ошибка закрепления сертификата!
Цепочка сертификатов одноранговых узлов: 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

Закрепленные сертификаты для publicobject.com:

sha1/BOGUSPIN
на com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
на com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
на com.squareup.okhttp.Connection .connect(Connection.java)
на com.squareup.okhttp.Connection.connectAndSetOwner(Connection.java)

Затем вставьте хэши открытого ключа из исключения в конфигурацию пиннера сертификатов:

Примечание: если вы делаете это на Android, вы получите отдельное исключение, если вы делаете это в потоке пользовательского интерфейса, поэтому убедитесь, что вы делаете это в фоновом потоке.

<сильный>2. Настройте клиент 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());

Вот и все!

person spierce7    schedule 18.08.2015
comment
+1 за решение, однако модернизация не использует OkHttpClient, который я устанавливаю с помощью CertificatePinner, есть идеи? - person dhaval; 31.03.2016
comment
@dhaval Вы можете использовать построитель модернизации и предоставить ему OkHttpClient. - person spierce7; 27.07.2016
comment
прохладно! Если срок действия сертификата моего сайта истекает через месяц, и я хочу, чтобы и старый, и новый сертификат работали после его запуска, могу ли я добавить оба булавки сертификата в свое приложение? - person Bootstrapper; 17.11.2016
comment
@Bootstrapper Я ни в коем случае не эксперт в этом. Но я только что прошел через этот процесс. Я работал со своей ИТ-командой, и они заменили сертификаты в тестовой среде, и я смог просто подключиться к ней. В ходе тестирования я обнаружил, что если хотя бы один из сертификатов совпадает, запрос все равно будет успешным. Тем не менее, вы должны провести собственное тестирование, так как я проверил это очень кратко, и в итоге мы приняли меру, которая не полагалась на этот подход. - person spierce7; 17.11.2016
comment
Ну, технически это закрепление хэша, а не самого сертификата. Зачем подвергать себя столкновению, даже если оно чисто гипотетическое? - person Agent_L; 18.11.2016
comment
Мне это кажется смешным, но если вас это не устраивает, не прикрепляйте хэш. - person spierce7; 18.11.2016
comment
я получаю это исключение вместо того, что с sha256, есть идеи, как это исправить? javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. - person Fonix; 11.01.2017
comment
хорошо, похоже, что это может быть на стороне сервера, при использовании моего сервера UAT он выдает эту ошибку, но против моего сервера prod все в порядке, поэтому должно быть так, как сертификаты настроены на сервере, хотя не знаю, как это работает, может быть, кто-то может прояснить - person Fonix; 11.01.2017
comment
@Fonix У вас есть какое-нибудь решение? - person Palak Darji; 23.01.2021

Если у вас нет доступа к домену (например, ограниченный доступ) и вы не можете проверить поддельный хэш, но у вас есть файл сертификата, вы можете использовать openssl для его получения:

openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
person Grzegorz Pawełczuk    schedule 12.07.2017
comment
Если ваш сертификат уже der (например, если он взят из Chrome), команда openssl x509 -inform der -in certificate.cer -fingerprint -sha256 -noout | openssl dgst -sha256 -binary | openssl enc -base64 - person Tom; 10.01.2018
comment
Будьте осторожны и обязательно используйте -pubkey (-fingerprint создаст sha256 для всего сертификата, что неверно) - person DummyData; 27.01.2021

Чтобы расширить образец, исходный код @Michael-barany поделился, я провел некоторое тестирование, и похоже, что это вводящий в заблуждение пример кода. В примере кода исключение отметило 4 хэша sha1 из исключения цепочки сертификатов:

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

затем впоследствии добавил все 4 хэша открытого ключа sha1 в 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();

Однако, учитывая тесты, которые я выполнил и просмотрел код, будет интерпретирован только первый действительный, поэтому вам лучше всего включить только ОДИН из возвращаемых хэшей. Вы можете использовать наиболее конкретный хеш "DmxUShsZuNiqPQsX2Oi9uv2sCnw" для точного сертификата сайта... или вы можете использовать самый широкий хэш "T5x9IXmcrQ7YuQxXnxoCmeeQ84c" для корневого центра сертификации в зависимости от желаемого состояния безопасности.

person Dave Cobb    schedule 11.05.2015
comment
Вам нужно было бы опубликовать это на github Square, но я думаю, что намерение автора состояло в том, чтобы иметь запасные хэши на случай, если сертификат необходимо будет отозвать и заменить, это не вызовет хаоса в существующем клиентском коде. Опять же, это только мои мысли. - person Michael Barany; 12.05.2015
comment
@MichaelBarany, FWIW AFNetworking для iOS поддерживает обе проверки что каждый закрепленный сертификат находится в цепочке и проверяет, что по крайней мере один закрепленный сертификат находится в цепочке. Я не эксперт в том, что более идеально, но приятно иметь такую ​​гибкость. - person deRonbrown; 14.05.2015

Я нашел пример, упомянутый в разделе Неизвестный центр сертификации этой ссылки developer.android.com/training/articles/security-ssl очень полезно.

SSLSocketFactory, возвращенный в context.getSocketFactory(), можно затем использовать для установки OkHttpClient в методе setSslSocketFactory().

Примечание. В разделе «Неизвестный центр сертификации» также упоминается ссылка для загрузки файла сертификата для использования и проверки этого кода.

Вот пример метода, который я написал для получения 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;
}

Позже я просто устанавливаю это на OkHttpClient, как это

httpClient.setSslSocketFactory(sslSocketFactory);

а затем сделать вызов https

httpClient.newCall(requestBuilder.build()).enqueue(callback);
person Vinayak    schedule 13.10.2015