Самоподписанный SSL-прием на Android

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

Образец кода был бы идеальным.

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

Ответ 1

У меня есть эта функция в exchangeIt, которая подключается к Microsoft Exchange через 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 процесс немного изменился, поэтому проверьте этот, чтобы сделать код выше.

Ответ 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();
    }
}

Ответ 3

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

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

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

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

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

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

Вам также нужен ваш самозаверяющий сертификат, я предполагаю, что он называется 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 указывает на файл, в котором будет создано ваше хранилище ключей. Он НЕ ДОЛЖЕН EXIST.

PATH_TO_bcprov-jdk15on-146.jar.JAR - путь к загруженной .jar libary.

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

Ответ 4

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

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

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

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

  • Создайте собственный экземпляр 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://stackoverflow.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;
    }
    

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

Ответ 5

Ответ Брайана Яргера работает и в 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);
}

Ответ 6

На Android HttpProtocolParams принимает ProtocolVersion, а не HttpVersion.

ProtocolVersion pv = new ProtocolVersion("HTTP", 1, 1);
HttpProtocolParams.setVersion(params, pv);

Ответ 7

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

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

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