Самозаверяющее принятие SSL на Android

Как принять самозаверяющий сертификат в Java на Android?

Пример кода будет идеальным.

Я искал повсюду в Интернете, и хотя некоторые люди утверждают, что нашли решение, оно либо не работает, либо нет примера кода для его резервного копирования.

8 ответов

Решение

У меня есть этот функционал в exchangeIt, который подключается к бирже Microsoft через WebDav. Вот некоторый код для создания HttpClient, который будет подключаться к самозаверяющим сертификатам через SSL:

SchemeRegistry schemeRegistry = new SchemeRegistry();
// http scheme
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
// https scheme
schemeRegistry.register(new Scheme("https", new EasySSLSocketFactory(), 443));

HttpParams params = new BasicHttpParams();
params.setParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 30);
params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, new ConnPerRouteBean(30));
params.setParameter(HttpProtocolParams.USE_EXPECT_CONTINUE, false);
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);

EasySSLSocketFactory здесь, и EasyX509TrustManager здесь.

Код для exchangeIt является открытым исходным кодом и размещен здесь на googlecode, если у вас есть какие-либо проблемы. Я больше не работаю над этим, но код должен работать.

Обратите внимание, что начиная с Android 2.2 процесс немного изменился, поэтому отметьте это, чтобы приведенный выше код работал.

Как правильно прокомментировал EJP: "Читатели должны заметить, что этот метод радикально небезопасен. SSL не защищен, если не аутентифицирован хотя бы один узел. См. RFC 2246".

Сказав это, вот другой способ, без каких-либо дополнительных классов:

import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509TrustManager;

private void trustEveryone() {
    try {
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier(){
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }});
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, new X509TrustManager[]{new X509TrustManager(){
            public void checkClientTrusted(X509Certificate[] chain,
                    String authType) throws CertificateException {}
            public void checkServerTrusted(X509Certificate[] chain,
                    String authType) throws CertificateException {}
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }}}, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(
                context.getSocketFactory());
    } catch (Exception e) { // should never happen
        e.printStackTrace();
    }
}

Я столкнулся с этой проблемой вчера, когда переносил RESTful API нашей компании на HTTPS, но использовал самозаверяющие SSL-сертификаты.

Я искал везде, но все "правильные" помеченные ответы, которые я нашел, состояли в отключении проверки сертификата, явно переопределяя весь смысл SSL.

Я наконец пришел к решению:

  1. Создать локальный хранилище ключей

    Чтобы приложение могло проверять свои подписанные сертификаты, необходимо предоставить хранилище ключей с сертификатами таким образом, чтобы Android мог доверять вашей конечной точке.

Формат таких пользовательских хранилищ ключей - "BKS" от BouncyCastle, поэтому вам нужна версия 1.46 BouncyCastleProvider, которую вы можете скачать здесь.

Вам также нужен ваш самоподписанный сертификат, я предполагаю, что он назван self_cert.pem,

Теперь команда для создания вашего хранилища ключей:

<!-- language: lang-sh -->

    $ keytool -import -v -trustcacerts -alias 0 \
    -file *PATH_TO_SELF_CERT.PEM* \
    -keystore *PATH_TO_KEYSTORE* \
    -storetype BKS \
    -provider org.bouncycastle.jce.provider.BouncyCastleProvider \
    -providerpath *PATH_TO_bcprov-jdk15on-146.jar* \
    -storepass *STOREPASS*

PATH_TO_KEYSTORE указывает на файл, в котором будет создано ваше хранилище ключей. ЭТО НЕ ДОЛЖНО СУЩЕСТВОВАТЬ.

PATH_TO_bcprov-jdk15on-146.jar.JAR путь к загруженному.jar-библиотеке

STOREPASS Ваш недавно созданный пароль хранилища ключей.

  1. Включите KeyStore в ваше приложение

Скопируйте ваш недавно созданный склад ключей из PATH_TO_KEYSTORE в res/raw/certs.bks (certs.bks - это просто имя файла; вы можете использовать любое имя, какое пожелаете)

Создать ключ в res/values/strings.xml с

<!-- language: lang-xml -->

    <resources>
    ...
        <string name="store_pass">*STOREPASS*</string>
    ...
    </resources>
  1. Создать этот класс, который наследуетDefaultHttpClient

    import android.content.Context;
    import android.util.Log;
    import org.apache.http.conn.scheme.PlainSocketFactory;
    import org.apache.http.conn.scheme.Scheme;
    import org.apache.http.conn.scheme.SchemeRegistry;
    import org.apache.http.conn.ssl.SSLSocketFactory;
    import org.apache.http.impl.client.DefaultHttpClient;
    import org.apache.http.params.HttpParams;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.*;
    
    public class MyHttpClient extends DefaultHttpClient {
    
        private static Context appContext = null;
        private static HttpParams params = null;
        private static SchemeRegistry schmReg = null;
        private static Scheme httpsScheme = null;
        private static Scheme httpScheme = null;
        private static String TAG = "MyHttpClient";
    
        public MyHttpClient(Context myContext) {
    
            appContext = myContext;
    
            if (httpScheme == null || httpsScheme == null) {
                httpScheme = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);
                httpsScheme = new Scheme("https", mySSLSocketFactory(), 443);
            }
    
            getConnectionManager().getSchemeRegistry().register(httpScheme);
            getConnectionManager().getSchemeRegistry().register(httpsScheme);
    
        }
    
        private SSLSocketFactory mySSLSocketFactory() {
            SSLSocketFactory ret = null;
            try {
                final KeyStore ks = KeyStore.getInstance("BKS");
    
                final InputStream inputStream = appContext.getResources().openRawResource(R.raw.certs);
    
                ks.load(inputStream, appContext.getString(R.string.store_pass).toCharArray());
                inputStream.close();
    
                ret = new SSLSocketFactory(ks);
            } catch (UnrecoverableKeyException ex) {
                Log.d(TAG, ex.getMessage());
            } catch (KeyStoreException ex) {
                Log.d(TAG, ex.getMessage());
            } catch (KeyManagementException ex) {
                Log.d(TAG, ex.getMessage());
            } catch (NoSuchAlgorithmException ex) {
                Log.d(TAG, ex.getMessage());
            } catch (IOException ex) {
                Log.d(TAG, ex.getMessage());
            } catch (Exception ex) {
                Log.d(TAG, ex.getMessage());
            } finally {
                return ret;
            }
        }
    }
    

Теперь просто используйте экземпляр **MyHttpClient** как вы бы с **DefaultHttpClient** чтобы сделать ваши HTTPS-запросы, и он будет правильно использовать и проверять ваши самозаверяющие SSL-сертификаты.

HttpResponse httpResponse;

HttpPost httpQuery = new HttpPost("https://yourserver.com");
... set up your query ...

MyHttpClient myClient = new MyHttpClient(myContext);

try{

    httpResponse = myClient.(peticionHttp);

    // Check for 200 OK code
    if (httpResponse.getStatusLine().getStatusCode() == HttpURLConnection.HTTP_OK) {
        ... do whatever you want with your response ...
    }

}catch (Exception ex){
    Log.d("httpError", ex.getMessage());
}

Если я что-то не пропустил, другие ответы на этой странице ОПАСНЫ и функционально эквивалентны тому, чтобы вообще не использовать SSL. Если вы доверяете самозаверяющим сертификатам, не выполняя дальнейших проверок, чтобы убедиться, что сертификаты именно те, которые вы ожидаете, тогда любой может создать самозаверяющий сертификат и выдать себя за ваш сервер. На данный момент у вас нет реальной безопасности.

Единственный законный способ сделать это (без записи полного стека SSL) - добавить дополнительный доверенный якорь, которому нужно доверять в процессе проверки сертификата. Оба включают жесткое кодирование сертификата доверенного якоря в ваше приложение и добавление его к любым доверенным якорям, которые предоставляет ОС (иначе вы не сможете подключиться к своему сайту, если получите настоящий сертификат).

Я знаю два способа сделать это:

  1. Создайте пользовательское хранилище доверия, как описано на http://www.ibm.com/developerworks/java/library/j-customssl/

  2. Создайте пользовательский экземпляр X509TrustManager и переопределите метод getAcceptedIssuers, чтобы получить массив, содержащий ваш сертификат:

    public X509Certificate[] getAcceptedIssuers()
    {
        X509Certificate[] trustedAnchors =
            super.getAcceptedIssuers();
    
        /* Create a new array with room for an additional trusted certificate. */
        X509Certificate[] myTrustedAnchors = new X509Certificate[trustedAnchors.length + 1];
        System.arraycopy(trustedAnchors, 0, myTrustedAnchors, 0, trustedAnchors.length);  
    
        /* Load your certificate.
    
           Thanks to http://stackru.com/questions/11857417/x509trustmanager-override-without-allowing-all-certs
           for this bit.
         */
        InputStream inStream = new FileInputStream("fileName-of-cert");
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate)cf.generateCertificate(inStream);
        inStream.close();
    
        /* Add your anchor cert as the last item in the array. */
        myTrustedAnchors[trustedAnchors.length] = cert;
    
        return myTrustedAnchors;
    }
    

Обратите внимание, что этот код полностью не протестирован и может даже не скомпилироваться, но, по крайней мере, должен направить вас в правильном направлении.

Ответ Брайана Яргера работает и в Android 2.2, если вы измените большую перегрузку метода createSocket следующим образом. Мне потребовалось некоторое время, чтобы заставить работать самоподписанные SSL.

public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
    return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
}

Есть много альтернатив для этого варианта использования. Если вы не хотите иметь какой-либо пользовательский код в своей кодовой базе, такой как пользовательский TrustManagerЯ бы предложил попробовать GitHub - SSLContext Kickstart и следующий фрагмент кода:

      <dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart</artifactId>
    <version>6.7.0</version>
</dependency>

SSL-конфигурация

      SSLFactory sslFactory = SSLFactory.builder()
    .withUnsafeTrustMaterial()
    .build();

SSLContext sslContext = sslFactory.getSslContext();
SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();

Конфигурация HTTP-клиента

      HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("https", sslSocketFactory, 443));

ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

HttpClient httpClient = new DefaultHttpClient(ccm, params);

HttpsUrlConnection

      HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory); 

@Chris - опубликовать это как ответ, так как я не могу добавлять комментарии (пока). Мне интересно, если ваш подход должен работать при использовании веб-просмотра. Я не могу заставить это сделать на Android 2.3 - вместо этого я просто получаю белый экран.

После еще нескольких поисков я наткнулся на это простое исправление для обработки ошибок SSL в веб-представлении, которое мне показалось очаровательным.

В обработчике я проверяю, нахожусь ли я в специальном режиме разработки, и вызываю handler.proceed(), в противном случае я вызываю handler.cancel(). Это позволяет мне разрабатывать самозаверяющий сертификат на локальном веб-сайте.

На Android, HttpProtocolParams принимает ProtocolVersion скорее, чем HttpVersion,

ProtocolVersion pv = new ProtocolVersion("HTTP", 1, 1);
HttpProtocolParams.setVersion(params, pv);
Другие вопросы по тегам