«

»

Sep 25

Android – работа с ssl-сертификатами (как организовать передачу данных через HTTPS)

Что делать, если необходимо получать данные в андроиде через защищенное https-соединение? Почему генерируется ошибка SSLException: Not trusted server certificate ? Как добавить сертификат с сервера в локальное хранилище ключей на андроид-устройстве? Если вас волнуют эти вопросы – вам будет полезна инструкция для установки ssl сертификатов и сниппет кода для их загрузки в ваше андроид-приложение.

1. Как узнать, что за сертификат используется сервером (На Linux/Mac Os – как правило, пакет openssl предустановлен, для  Windows – актуальные ссылки в разделе бинарниики там):

openssl s_client -connect <server-ip>:<server-port>

для самоподписанных сертификатов выведется нечто вроде:

Certificate chain
0 s:/CN=localhost
i:/CN=localhost
— Server certificate
—–BEGIN CERTIFICATE—–

—–END CERTIFICATE—–

если используется сертификат, подписанный сторонней компанией – будут отображены координаты, по которым можно раздобыть публичный сертификат

2. Создаем файл сертификата

А). Для самоподписанного сертификата:

1. создать пустой файл my-certificate-file.pem

2. скопировать в новый файл закодированные данные сертификата:

—–BEGIN CERTIFICATE—–

—–END CERTIFICATE—–

да, открывающий\закрывающий теги должны быть.

Если сертификат самоподписанный можно использовать данный bash-script

echo | openssl s_client -connect <server-ip>:<port> 2>/dev/null | sed -ne ‘/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p’ > my-certificate-file.pem

Б). Если сертификат используемый сервером подписан сторонней организацией, на предыдущем шаге мы увидим ее контакты по которым его можно запросить. Почитать про разные типы сертификатов и попробовать онлайн-конвертер из одного типа в другой можно тут – https://www.sslshopper.com/ssl-converter.html

3. Создаем файл-хранилище с этим единственным ключом:

keytool -import -v -trustcacerts -alias myalias -file <(openssl x509 -in my-certificate-file.pem) -keystore mystore.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath /<full-path>/bcprov-jdk16-146.jar -storepass <initial-password-for-storage>

-alias myalias – псевдоним для работы с сертификатом

-keystore mystore.bks – говорим какой файл создать для хранения этого сертификата

-storetype BKS – android поддерживает такой тип хранилища, для того чтобы сгенерировать, его мы должны добавить в наш java CLASSPATH соответствующий jar-файл, последнюю версию которого можно взять отсюда – http://www.bouncycastle.org/latest_releases.html

для создания хранилища необходимо отдельно указать поставщика данного типа хранилища, а также путь к java-классу его реализующего.

-provider org.bouncycastle.jce.provider.BouncyCastleProvider

-providerpath /<full-path>/bcprov-jdk16-146.jar

(если этого не сделать появляется ошибка: keytool error: java.security.KeyStoreException: BKS not found)

4. Файл-хранилище мы можем добавить себе в проект чтобы использовать на этапе тестирования – например в папку assets. В обычном режиме работы, очевидно, его необходимо считывать из определенной директории для того чтобы работать с актуальной версией сертификата.

5. В android-приложении пригодится следующий сниппет кода:

1 этап – загружаем файл-хранилище с нашим сертификатом:

KeyStore ks = null;
try
{
    ks = KeyStore.getInstance("BKS");
    final InputStream in = this.getResources().getAssets().open("mystore.bks");
    try
    {
        String pass = "input-your-password-here";
        ks.load(in, pass.toCharArray());
    }
    finally
    {
        in.close();
    }
 }
 catch( Exception e )
 {
     Log.i("Your_Function_Name","Exception - "+e.toString());
 }

В реальном приложении, очевидно, файл необходимо будет считывать из предопределенной директории.

2 этап – создаем фабрику управления доверительными соединениями, она будет использоваться для проверки сертифкатов всех https соединений (я еще раз подчеркиваю – ВСЕХ)

TrustManagerFactory Main_TMF= null;
try
{
    Main_TMF= TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    Main_TMF.init(ks);
}
catch(Exception e)
{
    Log.i("Your_Function_Name","Exception - "+e.toString());
}

X509TrustManager Main_Trust_Manager = null;

for( TrustManager tm : Main_TMF.getTrustManagers() )
if (tm instanceof X509TrustManager)
{
    Main_Trust_Manager = (X509TrustManager) tm;
    break;
}

final X509TrustManager Cur_Trust_Manager = Main_Trust_Manager;
SSLContext sslContext = null;
try
{
    sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, new TrustManager[]{Cur_Trust_Manager}, new SecureRandom());
}
catch (NoSuchAlgorithmException e)
{
    Log.i("Your_Function_Name","NoSuchAlgorithmException - "+e.toString());
}
catch (KeyManagementException e)
{
    Log.i("Your_Function_Name","KeyManagementException - "+e.toString());
}
catch (Exception e)
{
    Log.i("Your_Function_Name","Exception - "+e.toString());
}

После загрузки нашего хранилища ключей мы создаем фабрику для управления доверенными соединениями на основе этого хранилища. С помощью этой фабрики получаем экземпляр менеджера соединений специфичного для ssl (X509TrustManager) и инициализируем объект SSLContext, который будет использоваться для создания защищенного соединения.

3 этап – указываем какие настройки будут использоваться при установлении защищенных соединений:

HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier()
{
    @Override
    public boolean verify(String hostname, SSLSession session)
    {
        try
        {
            Cur_Trust_Manager.checkServerTrusted((X509Certificate[]) session.getPeerCertificates(),session.getCipherSuite());
            return true;
        }
        catch( CertificateException e )
        {
            Log.i("HostnameVerifier- checkServerTrusted","CertificateException - "+e.toString());
        }
        catch (Exception e)
        {
            Log.i("HostnameVerifier- checkServerTrusted","Exception - "+e.toString());
        }
        return false;
   }
});

После этого, если вы все сделали правильно, при каждом https соединении будут вызываться соотвествующие проверки, использующие подготовленный файл хранилище.

UPDATE 26.09.2012:

Хорошая статья по теме использования собственных сертификатов безопасности:

http://nelenkov.blogspot.com/2011/12/using-custom-certificate-trust-store-on.html

UPDATE 04.10.2012:

С помощью одной целеустремленной читательницы удалось приоткрыть завесу тайны над ошибкой: “SSL handshake terminated: ssl=0x1fcf30: Failure in SSL library, usually a protocol error” возникающей при попытке использовать двусторонюю аутентификацию – т.е. если не только клиент проверяет подлинность сервера но и сервер, проверяет что за клиент к нему лезет.

//client's KeyStore init
File f = new File(path);
final InputStream in = new FileInputStream(f);
KeyManagerFactory mgrFact = KeyManagerFactory.getInstance("X509");
KeyStore clientStore = KeyStore.getInstance("PKCS12");
clientStore.load(in, PASSWORD.toCharArray());
mgrFact.init(clientStore, PASSWORD.toCharArray());

// server's KeyStore init
KeyStore ks = null;
try
{
    ks = KeyStore.getInstance("BKS");
    final InputStream in = this.getResources().getAssets().open("mystore.bks");
    try
    {
        String pass = "input-your-password-here";
        ks.load(in, pass.toCharArray());
    }
    finally
    {
        in.close();
    }
 }
 catch( Exception e )
 {
     Log.i("Your_Function_Name","Exception - "+e.toString());
 }
/*
load server's truststorage 
*/
TrustManagerFactory Main_TMF= null;
try
{
    Main_TMF= TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    Main_TMF.init(ks);
}
catch(Exception e)
{
    Log.i("Your_Function_Name","Exception - "+e.toString());
}

X509TrustManager Main_Trust_Manager = null;

for( TrustManager tm : Main_TMF.getTrustManagers() )
if (tm instanceof X509TrustManager)
{
    Main_Trust_Manager = (X509TrustManager) tm;
    break;
}
final X509TrustManager Cur_Trust_Manager = Main_Trust_Manager;

// init ssl context for future usage
SSLContext sslContext = null;
try
{
    sslContext = SSLContext.getInstance("TLS");
}
catch (NoSuchAlgorithmException e)
{
    // TODO Auto-generated catch block
    e.printStackTrace();
}

try
{
    sslContext.init( mgrFact.getKeyManagers(), new TrustManager[]{Cur_Trust_Manager}, new java.security.SecureRandom());
}
catch (KeyManagementException e)
{
    // TODO Auto-generated catch block
    e.printStackTrace();
} 

по мотивам:
http://www.javadocexamples.com/java_source/chapter10 – https аутентификация сервером клиента
http://www.devdaily.com/java/jwarehouse/android/core/java/android/net/http/CertificateChainValidator.java.shtml – приятственный класс для валидации сертификатов

10 comments

Skip to comment form

  1. Marilu

    This “free sharing” of ifnromaiton seems too good to be true. Like communism.

    1. Sandra

      Deadly accuarte answer. You’ve hit the bullseye!

  2. Eventmaker

    keytool error: java.lang.ClassNotFoundException: org.bouncycastle.jce.provider.BouncyCastleProvider

    Что не так?

    1. admin

      в списке ключей для утилиты keytool есть такая строчка -providerpath /full-path/bcprov-jdk16-146.jar
      вместо full-path необходимо подставить ваш путь к архиву (скачать его можно оттуда – http://www.bouncycastle.org/latest_releases.html)

      1. Eventmaker

        Спасибо, разобрался, ещё вопрос:
        На вторую строчку третьего этапа ругается Eclipse – The method setDefaultHostnameVerifier(HostnameVerifier) in the type HttpsURLConnection is not applicable for the arguments (new HostnameVerifier(){})

        Если не сложно, подскажите, пожалуйста, как в этом случае настроить HttpClient?

        1. admin

          эта ошибка указывает что передаваемый тип не совсем то что нам надо, а потому:

          1. сделать clean проекта
          2. удостовериться что импорты для HttpsURLConnection и HostnameVerifier такие и только такие:
          import javax.net.ssl.HostnameVerifier;
          import javax.net.ssl.HttpsURLConnection;

          3. строчку с верифером заменить на
          HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier()
          {
          public boolean verify(String hostname, SSLSession session)
          {
          return true;
          }
          });

        2. Halima

          This atrlice keeps it real, no doubt.

    2. Javier

      AFAIC that’s the best aneswr so far!

  3. pglagolev

    Добрый день,
    подскажите пожалуйста возможно ли выгрузить сертификат из хранилища в андройде? Икак это сделать, если возможно?
    Спасибо

    1. admin

      с помощью все той-же команды keytool с ключиком importkeystore.
      вот тут хороший пример:
      http://www.xinotes.org/notes/note/1295/

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>