Подтвердить что ты не робот

Соединение HTTPS с сертификатом клиента в приложении для Android

Я пытаюсь заменить действующее HTTP-соединение с HTTPS-соединением в приложении для Android, которое я пишу. Необходима дополнительная безопасность соединения HTTPS, поэтому я не могу игнорировать этот шаг.

У меня есть следующее:

  • Сервер, настроенный для установления HTTPS-соединения и требующий сертификата клиента
    • Этот сервер имеет сертификат, выдаваемый стандартным крупномасштабным ЦС. Короче говоря, если я получаю доступ к этому соединению через браузер в Android, он отлично работает, потому что доверительный магазин устройств распознает ЦС. (Так что это не самозапись)
  • Клиентский сертификат, который по сути является самоподписанным. (Выдается внутренним ЦС)
  • Приложение Android, которое загружает этот сертификат клиента и пытается подключиться к вышеупомянутому серверу, но имеет следующие проблемы/свойства:
    • Клиент может подключиться к серверу, когда сервер настроен на не, требующий сертификата клиента. В принципе, если я использую SSLSocketFactory.getSocketFactory(), соединение работает нормально, но клиентский сертификат является обязательной частью этих спецификаций приложений, поэтому:
    • Клиент создает исключение javax.net.ssl.SSLPeerUnverifiedException: No peer certificate, когда я пытаюсь подключиться к своему пользовательскому SSLSocketFactory, но я не совсем уверен, почему. Это исключение кажется немного неоднозначным после поиска в Интернете различных решений.

Вот код для клиента:

SSLSocketFactory socketFactory = null;

public void onCreate(Bundle savedInstanceState) {
    loadCertificateData();
}

private void loadCertificateData() {
    try {
        File[] pfxFiles = Environment.getExternalStorageDirectory().listFiles(new FileFilter() {
            public boolean accept(File file) {
                if (file.getName().toLowerCase().endsWith("pfx")) {
                    return true;
                }
                return false;
            }
        });

        InputStream certificateStream = null;
        if (pfxFiles.length==1) {
            certificateStream = new FileInputStream(pfxFiles[0]);
        }

        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        char[] password = "somePassword".toCharArray();
        keyStore.load(certificateStream, password);

        System.out.println("I have loaded [" + keyStore.size() + "] certificates");

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, password);

        socketFactory = new SSLSocketFactory(keyStore);
    } catch (Exceptions e) {
        // Actually a bunch of catch blocks here, but shortened!
    }
}

private void someMethodInvokedToEstablishAHttpsConnection() {
    try {
        HttpParams standardParams = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(standardParams, 5000);
        HttpConnectionParams.setSoTimeout(standardParams, 30000);

        SchemeRegistry schRegistry = new SchemeRegistry();
        schRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        schRegistry.register(new Scheme("https", socketFactory, 443));
        ClientConnectionManager connectionManager = new ThreadSafeClientConnManager(standardParams, schRegistry);

        HttpClient client = new DefaultHttpClient(connectionManager, standardParams);
        HttpPost request = new HttpPost();
        request.setURI(new URI("https://TheUrlOfTheServerIWantToConnectTo));
        request.setEntity("Some set of data used by the server serialized into string format");
        HttpResponse response = client.execute(request);
        resultData = EntityUtils.toString(response.getEntity());
    } catch (Exception e) {
        // Catch some exceptions (Actually multiple catch blocks, shortened)
    }
}

Я подтвердил, что да, действительно, keyStore загружает сертификат и все это доволен.

У меня есть две теории относительно того, что мне не хватает в чтении о соединениях HTTPS/SSL, но поскольку это действительно мой первый набег, я немного озадачен тем, что мне действительно нужно для решения этой проблемы.

Первая возможность, насколько я могу судить, заключается в том, что мне нужно настроить этот SSLSocketFactory с помощью доверительного магазина устройств, который включает в себя все стандартные средние сертификаты промежуточных и конечных точек. То есть, по умолчанию устройство SSLSocketFactory.getSocketFactory() загружает некоторый набор ЦС в доверительный магазин factory, который используется, чтобы доверять серверу, когда он отправляет свой сертификат, и это то, что не работает в моем коде, потому что я неправильно загружен хранилище доверия. Если это так, как мне лучше всего загружать эти данные?

Вторая возможность связана с тем, что сертификат клиента самоподписан (или выпущен внутренним центром сертификации - исправьте меня, если я ошибаюсь, но они действительно равны одному и тому же, для всех целей и цели здесь). На самом деле это доверенное хранилище, которое мне не хватает, и в основном мне нужно предоставить сервер для проверки сертификата с внутренним ЦС, а также проверить, что этот внутренний ЦС фактически "заслуживает доверия". Если это правда, именно то, что я ищу? Я видел некоторые ссылки на это, что заставляет меня поверить, что это может быть моей проблемой, как в здесь, но я действительно не уверен. Если это действительно моя проблема, что бы я попросил у человека, который поддерживает внутренний ЦС, и как я могу добавить это в свой код, чтобы мое HTTPS-соединение работало?

Третье и, надеюсь, менее возможное решение состоит в том, что я совершенно ошибаюсь в некоторых вопросах здесь и пропустил важный шаг или полностью игнорирую часть HTTPS/SSL, которую я просто не знаю в настоящее время, Если это так, не могли бы вы предоставить мне немного направления, чтобы я мог пойти и узнать, что мне нужно узнать?

Спасибо за чтение!

4b9b3361

Ответ 1

Я думаю, что это действительно проблема.

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

Если это так, как мне лучше всего загружать эти данные?

Попробуйте что-то вроде этого (вам понадобится ваш сокет factory, чтобы использовать этот менеджер доверия по умолчанию):

X509TrustManager manager = null;
FileInputStream fs = null;

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

try
{
    fs = new FileInputStream(System.getProperty("javax.net.ssl.trustStore")); 
    keyStore.load(fs, null);
}
finally
{
    if (fs != null) { fs.close(); }
}

trustManagerFactory.init(keyStore);
TrustManager[] managers = trustManagerFactory.getTrustManagers();

for (TrustManager tm : managers)
{
    if (tm instanceof X509TrustManager) 
    {
        manager = (X509TrustManager) tm;
        break;
    }
}

РЕДАКТИРОВАТЬ: Пожалуйста, посмотрите на ответ Pooks, прежде чем использовать код здесь. Похоже, теперь есть лучший способ сделать это.

Ответ 2

Там есть более простой способ реализовать решение @jglouie. В принципе, если вы используете SSLContext и инициализируете его с помощью null для параметра доверительного управления, вы должны получить контекст SSL, используя диспетчер доверия по умолчанию. Обратите внимание, что это не документировано в документации для Android, но Java-документация для SSLContext.init говорит

Любой из первых двух параметров может быть нулевым, и в этом случае обнаруженные поставщики безопасности будут искать реализацию с наивысшим приоритетом соответствующего factory.

Вот как выглядит код:

// This can be any protocol supported by your target devices.
// For example "TLSv1.2" is supported by the latest versions of Android
final String SSL_PROTOCOL = "TLS";

try {               
   sslContext = SSLContext.getInstance(SSL_PROTOCOL);

   // Initialize the context with your key manager and the default trust manager 
   // and randomness source
   sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
} catch (NoSuchAlgorithmException e) {
   Log.e(TAG, "Specified SSL protocol not supported! Protocol=" + SSL_PROTOCOL);
   e.printStackTrace();
} catch (KeyManagementException e) {
   Log.e(TAG, "Error setting up the SSL context!");
   e.printStackTrace();
}

// Get the socket factory
socketFactory = sslContext.getSocketFactory();

Ответ 3

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

1), чтобы получить сертификат сайта, который вы хотите подключить

echo | openssl s_client -connect ${MY_SERVER}:443 2>&1 |  sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > mycert.pem

2), чтобы создать свой ключ, вам нужна библиотека BouncyCastle, вы можете скачать здесь

keytool -import -v -trustcacerts -alias 0 -file mycert.pem -keystore "store_directory/mykst" -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "directory_of_bouncycastle/bcprov-jdk16-145.jar" -storepass mypassword

3), чтобы проверить, был ли создан ключ

keytool -list -keystore "carpeta_almacen/mykst" -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "directory_of_bouncycastle/bcprov-jdk16-145.jar" -storetype BKS -storepass mypassword

и вы должны увидеть что-то вроде этого:

Tipo de almacén de claves: BKS Proveedor de almacén de claves: BC

Сульманный андалус конфелента 1

0, 07-dic-2011, trustedCertEntry,

Huella digital de certado (MD5):

55: FD: Е5: E3: 8A: 4C: D6: В8: 69: ЕВ: 6A: 49: 05: 5F: 18: 48

4), тогда вам нужно скопировать файл "mykst" в каталог "res/raw" (создать его, если он не существует) в вашем проекте Android.

5) добавьте разрешения в манифест андроида

  <uses-permission android:name="android.permission.INTERNET"/>

6) здесь код!

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:padding="10dp" >

    <Button
        android:id="@+id/button"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Cargar contenido" />

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="#4888ef">
        <ProgressBar
            android:id="@+id/loading"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:indeterminate="true"
            android:layout_centerInParent="true"
            android:visibility="gone"/>
        <ScrollView
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:fillViewport="true"
            android:padding="10dp">
            <TextView
                android:id="@+id/output"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:textColor="#FFFFFF"/>
        </ScrollView>
    </RelativeLayout>
</LinearLayout>

MyHttpClient

package com.example.https;


import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Enumeration;

import org.apache.http.conn.ClientConnectionManager;
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.impl.conn.SingleClientConnManager;

import android.content.Context;
import android.os.Build;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;

public class MyHttpClient extends DefaultHttpClient {

    final Context context;

    public MyHttpClient(Context context) {
        this.context = context;
    }

    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        // Register for port 443 our SSLSocketFactory with our keystore
        // to the ConnectionManager
        registry.register(new Scheme("https", newSslSocketFactory(), 443));
        return new SingleClientConnManager(getParams(), registry);
    }

    private SSLSocketFactory newSslSocketFactory() {
        try {
            // Trust manager / truststore
            KeyStore trustStore=KeyStore.getInstance(KeyStore.getDefaultType());

            // If we're on an OS version prior to Ice Cream Sandwich (4.0) then use the standard way to get the system
            //   trustStore -- System.getProperty() else we need to use the special name to get the trustStore KeyStore
            //   instance as they changed their trustStore implementation.
            if (Build.VERSION.RELEASE.compareTo("4.0") < 0) {
                TrustManagerFactory trustManagerFactory=TrustManagerFactory
                        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
                FileInputStream trustStoreStream=new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
                trustStore.load(trustStoreStream, null);
                trustManagerFactory.init(trustStore);
                trustStoreStream.close();
            } else {
                trustStore=KeyStore.getInstance("AndroidCAStore");
            }

            InputStream certificateStream = context.getResources().openRawResource(R.raw.mykst);
            KeyStore keyStore=KeyStore.getInstance("BKS");
            try {
                keyStore.load(certificateStream, "mypassword".toCharArray());
                Enumeration<String> aliases=keyStore.aliases();
                while (aliases.hasMoreElements()) {
                    String alias=aliases.nextElement();
                    if (keyStore.getCertificate(alias).getType().equals("X.509")) {
                        X509Certificate cert=(X509Certificate)keyStore.getCertificate(alias);
                        if (new Date().after(cert.getNotAfter())) {
                            // This certificate has expired
                            return null;
                        }
                    }
                }
            } catch (IOException ioe) {
                // This occurs when there is an incorrect password for the certificate
                return null;
            } finally {
                certificateStream.close();
            }

            KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, "mypassword".toCharArray());

            return new SSLSocketFactory(keyStore, "mypassword", trustStore);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}

MainActivity

package com.example.https;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;

import javax.net.ssl.SSLSocketFactory;

public class MainActivity extends Activity {

    private View loading;
    private TextView output;
    private Button button;

    SSLSocketFactory socketFactory = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loading = findViewById(R.id.loading);
        output = (TextView) findViewById(R.id.output);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new CargaAsyncTask().execute(new Void[0]);
            }
        });
    }

    class CargaAsyncTask extends AsyncTask<Void, Void, String> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            loading.setVisibility(View.VISIBLE);
            button.setEnabled(false);
        }

        @Override
        protected String doInBackground(Void... params) {
            // Instantiate the custom HttpClient
            DefaultHttpClient client = new MyHttpClient(getApplicationContext());
            HttpGet get = new HttpGet("https://www.google.com");
            // Execute the GET call and obtain the response
            HttpResponse getResponse;
            String resultado = null;
            try {
                getResponse = client.execute(get);
                HttpEntity responseEntity = getResponse.getEntity();
                InputStream is = responseEntity.getContent();
                resultado = convertStreamToString(is);
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return resultado;
        }

        @Override
        protected void onPostExecute(String result) {
            super.onPostExecute(result);
            loading.setVisibility(View.GONE);
            button.setEnabled(true);
            if (result == null) {
                output.setText("Error");
            } else {
                output.setText(result);
            }
        }

    }

    public static String convertStreamToString(InputStream is) throws IOException {
        /*
         * To convert the InputStream to String we use the
         * Reader.read(char[] buffer) method. We iterate until the
         * Reader return -1 which means there no more data to
         * read. We use the StringWriter class to produce the string.
         */
        if (is != null) {
            Writer writer = new StringWriter();

            char[] buffer = new char[1024];
            try {
                Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                int n;
                while ((n = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, n);
                }
            } finally {
                is.close();
            }
            return writer.toString();
        } else {
            return "";
        }
    }
}

Я надеюсь, что это может быть полезно для кого-то другого!! наслаждайтесь!

Ответ 4

Похоже, вам нужно также указать имя хоста для вашего SSLSocketFactory.

Попробуйте добавить строку

socketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

перед созданием нового соединения с вашим SSLFactory.

Кроме различий в структурах, мы имеем аналогичный код. В моей реализации я просто создал собственное расширение DefaultHttpClient, которое похоже на большую часть вашего кода выше. Если это не исправить, я могу опубликовать рабочий код для этого, и вы можете попробовать этот подход.

Изменить: здесь моя рабочая версия

    public class ActivateHttpClient extends DefaultHttpClient { 
    final Context context;


    /**
     * Public constructor taking two arguments for ActivateHttpClient.
     * @param context - Context referencing the calling Activity, for creation of
     * the socket factory.
     * @param params - HttpParams passed to this, specifically to set timeouts on the
     * connection.
     */
    public ActivateHttpClient(Context context, HttpParams params) {
        this.setParams(params);
    }


    /* (non-Javadoc)
     * @see org.apache.http.impl.client.DefaultHttpClient#createClientConnectionManager()
     * Create references for both http and https schemes, allowing us to attach our custom
     * SSLSocketFactory to either
     */
    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory
                .getSocketFactory(), 80));
        registry.register(new Scheme("https", newSslSocketFactory(), 443));
        return new SingleClientConnManager(getParams(), registry);
    }

    /**
     * Creation of new SSLSocketFactory, which imports a certificate from
     * a server which self-signs its own certificate.
     * @return
     */
    protected SSLSocketFactory newSslSocketFactory() {
        try {

            //Keystore must be in BKS (Bouncy Castle Keystore)
            KeyStore trusted = KeyStore.getInstance("BKS");

            //Reference to the Keystore
            InputStream in = context.getResources().openRawResource(
                    R.raw.cert);

            //Password to the keystore
            try {
                trusted.load(in, PASSWORD_HERE.toCharArray());
            } finally {
                in.close();
            }

            // Pass the keystore to the SSLSocketFactory. The factory is
            // responsible
            // for the verification of the server certificate.
            SSLSocketFactory sf = new SSLSocketFactory(trusted);

            // Hostname verification from certificate
            // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506
            sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
            return sf;

            // return new SSLSocketFactory(trusted);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AssertionError(e);
        }
    }

}

и можно вызвать, как показано:

HttpParams params = new BasicHttpParams();

    // Set the timeout in milliseconds until a connection is established.
    int timeoutConnection = 500;
    HttpConnectionParams.setConnectionTimeout( params , timeoutConnection );

    // Set the default socket timeout (SO_TIMEOUT)
    // in milliseconds which is the timeout for waiting for data.
    int timeoutSocket = 1000;
    HttpConnectionParams.setSoTimeout( params , timeoutSocket );
            //ADD more connection options here!

    String url =
            "https:// URL STRING HERE";
    HttpGet get = new HttpGet( url );

    ActivateHttpClient client =
            new ActivateHttpClient( this.context, params );



    // Try to execute the HttpGet, throwing errors
    // if no response is received, or if there is
    // an error in the execution.
    HTTPResponse response = client.execute( get );

Ответ 5

Я отправляю обновленный ответ, так как люди все еще ссылаются и голосуют по этому вопросу. Мне пришлось несколько раз менять код сокета factory, поскольку некоторые вещи изменились с тех пор, как Android 4.0

// Trust manager / truststore
KeyStore trustStore=KeyStore.getInstance(KeyStore.getDefaultType());

// If we're on an OS version prior to Ice Cream Sandwich (4.0) then use the standard way to get the system
//   trustStore -- System.getProperty() else we need to use the special name to get the trustStore KeyStore
//   instance as they changed their trustStore implementation.
if (Build.VERSION.RELEASE.compareTo("4.0") < 0) {
    TrustManagerFactory trustManagerFactory=TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    FileInputStream trustStoreStream=new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
    trustStore.load(trustStoreStream, null);
    trustManagerFactory.init(trustStore);
    trustStoreStream.close();
} else {
    trustStore=KeyStore.getInstance("AndroidCAStore");
}

InputStream certificateStream=new FileInputStream(userCertFile);
KeyStore keyStore=KeyStore.getInstance("PKCS12");
try {
    keyStore.load(certificateStream, certPass.toCharArray());
    Enumeration<String> aliases=keyStore.aliases();
    while (aliases.hasMoreElements()) {
        String alias=aliases.nextElement();
        if (keyStore.getCertificate(alias).getType().equals("X.509")) {
            X509Certificate cert=(X509Certificate)keyStore.getCertificate(alias);
            if (new Date().after(cert.getNotAfter())) {
                // This certificate has expired
                return;
            }
        }
    }
} catch (IOException ioe) {
    // This occurs when there is an incorrect password for the certificate
    return;
} finally {
    certificateStream.close();
}

KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, certPass.toCharArray());

socketFactory=new SSLSocketFactory(keyStore, certPass, trustStore);

Надеюсь, это поможет любому, кто еще придет сюда в будущем.