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

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

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

  1. Сервер, настроенный для установления HTTPS-соединения, и требует сертификата клиента
    • Этот сервер имеет сертификат, выдаваемый стандартным крупномасштабным ЦС. Короче говоря, если я получаю доступ к этому соединению через браузер в Android, он отлично работает, потому что устройства truststore распознают CA. (Так что это не самозапись)
  2. Сертификат клиента, который по сути является самоподписанным. (Выдается внутренним ЦС)
  3. Приложение Android, которое загружает этот клиентский сертификат и пытается подключиться к вышеупомянутому серверу, но имеет следующие проблемы / свойства:
    • Клиент может подключиться к серверу, когда сервер настроен на отсутствие сертификата клиента. В принципе, если я использую SSLSocketFactory.getSocketFactory() соединение работает нормально, но клиентский сертификат является обязательной частью этих спецификаций приложений, поэтому:
    • Клиент создает javax.net.ssl.SSLPeerUnverifiedException: No peer certificate исключения 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() загружает несколько наборов сертификатов в заводские хранилища, которые используются, чтобы доверять серверу при отправке его сертификата, и это то, что не работает в моем коде, потому что я неправильно Загружен хранилище доверия. Если это так, как мне лучше всего загружать эти данные?

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

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

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

Solutions Collecting From Web of "Соединение HTTPS с сертификатом клиента в приложении для Android"

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

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

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

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

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

EDIT: Пожалуйста, посмотрите на ответ Pooks, прежде чем использовать код здесь. Похоже, теперь есть лучший способ сделать это.

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

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

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

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

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

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

Основная деятельность

 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's 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 ""; } } } 

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

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

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

 socketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); 

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

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

Edit: вот моя рабочая версия

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

Я отправляю обновленный ответ, так как люди все еще ссылаются и голосуют по этому вопросу. Мне пришлось несколько раз изменить заводский код сокета, поскольку некоторые вещи изменились с тех пор, как 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); 

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