Conexión HTTPS con certificado de cliente en una aplicación para Android

Estoy tratando de reemplazar la conexión HTTP actualmente en funcionamiento con una conexión HTTPS en una aplicación de Android que estoy escribiendo. La seguridad adicional de una conexión HTTPS es necesaria y por lo tanto no puedo ignorar este paso.

Tengo lo siguiente:

  1. Un servidor configurado para establecer una conexión HTTPS y requiere un certificado de cliente
    • Este servidor tiene un certificado emitido por una CA de gran escala estándar. En resumen, si accedo a esta conexión a través del navegador en Android, funciona bien porque el truststore de dispositivos reconoce la CA. (Así que no es auto-firmado)
  2. Un certificado de cliente que es esencialmente auto-firmado. (Emitido por una CA interna)
  3. Una aplicación de Android que carga este certificado de cliente e intenta conectarse al servidor mencionado anteriormente, pero tiene los siguientes problemas / propiedades:
    • El cliente puede conectarse al servidor cuando el servidor está configurado para no requerir un certificado de cliente. Básicamente, si uso SSLSocketFactory.getSocketFactory() la conexión funciona bien, pero el certificado de cliente es una parte obligatoria de las especificaciones de estas aplicaciones, por lo que:
    • El cliente produce una javax.net.ssl.SSLPeerUnverifiedException: No peer certificate cuando intento conectarme con mi SSLSocketFactory personalizado, pero no estoy completamente seguro de por qué. Esta excepción parece un poco ambiguo después de buscar en Internet para diversas soluciones a esto.

Aquí está el código relavent para el cliente:

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

He comprobado que, sí, en efecto, el keyStore carga un certificado y está contento con eso.

Tengo dos teorías en cuanto a lo que estoy perdiendo de la lectura sobre HTTPS / conexiones SSL, pero como esta es realmente mi primera incursión, estoy un poco desconcertado en cuanto a lo que realmente necesita para resolver este problema.

La primera posibilidad, por lo que puedo decir, es que necesito configurar este SSLSocketFactory con el truststore de los dispositivos que incluye todas las Autoridades de Certificado intermedias y de punto final estándar. Es decir, el valor predeterminado del dispositivo SSLSocketFactory.getSocketFactory() carga un conjunto de CA en el almacén de confianza de la fábrica que se utiliza para confiar en el servidor cuando envía su certificado y eso es lo que está fallando en mi código porque no tengo correctamente El almacén de confianza cargado. Si esto es cierto, ¿cómo me iría mejor al cargar estos datos?

La segunda posibilidad se debe al hecho de que el certificado de cliente es auto-firmado (o emitido por una autoridad de certificación interna – corregirme si estoy equivocado, pero estos realmente son lo mismo, para todos los intentos aquí) . De hecho, este truststore que me falta, y, básicamente, tengo que proporcionar una forma para el servidor para validar el certificado con la CA interna, y también validar que esta CA interna es de hecho "trustable" . Si esto es cierto, ¿exactamente qué clase de cosa estoy buscando? He visto alguna referencia a esto que me hace creer que este puede ser mi problema, como aquí , pero realmente no estoy seguro. Si este es mi problema, ¿qué le pediría a la persona que mantiene la CA interna y, a continuación, ¿cómo añadiría esto a mi código para que funcione mi conexión HTTPS?

La tercera solución, y espero que sea menos posible, es que estoy totalmente equivocado acerca de algún punto aquí y he perdido un paso crucial o estoy descuidando por completo una porción de HTTPS / SSL que yo no conozco actualmente. Si este es el caso, ¿podría por favor darme un poco de dirección para que pueda ir y aprender qué es lo que necesito aprender?

¡Gracias por leer!

Creo que este es el problema.

La primera posibilidad, por lo que puedo decir, es que necesito configurar este SSLSocketFactory con el truststore de los dispositivos que incluye todas las Autoridades de Certificado Intermedio y de punto final estándar

Si esto es cierto, ¿cómo me iría mejor al cargar estos datos?

Pruebe algo como esto (necesitará que su fábrica de socket utilice este gestor de confianza predeterminado):

 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: Por favor, mire la respuesta de Pooks antes de usar el código aquí. Parece que hay una mejor manera de hacer esto ahora.

Hay una manera más sencilla de implementar la solución de @jglouie. Básicamente, si utiliza SSLContext y lo inicializa con null para el parámetro trust manager, debería obtener un contexto SSL utilizando el gestor de confianza predeterminado. Tenga en cuenta que esto no está documentado en la documentación de Android, pero la documentación de Java para SSLContext.init dice

Cualquiera de los dos primeros parámetros puede ser nulo, en cuyo caso se buscará a los proveedores de seguridad instalados la implementación de mayor prioridad de la fábrica apropiada.

Esto es lo que el código se vería así:

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

Había intentado un par de días que finalmente tengo la respuesta, así que me gustaría publicar aquí mis pasos y todo mi código para ayudar a alguien más.

1) para obtener el certificado del sitio que desea conectar

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

2) para crear su clave que necesita BouncyCastle biblioteca se puede descargar aquí

 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) para comprobar si la clave fue creada

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

Y deberías ver algo como esto:

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

Su almacén de claves contiene entrada 1

0, 07-dic-2011, trustedCertEntry,

Huella digital de certificado (MD5):

55: FD: E5: E3: 8A: 4C: D6: B8: 69: EB: 6A: 49: 05: 5F: 18: 48

4) entonces usted necesita copiar el archivo "mykst" en el directorio "res / raw" (crearlo si no existe) en su proyecto android.

5) añadir los permisos en el manifiesto android

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

6) aquí el código!

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

Actividad principal

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

Espero que podría ser útil para alguien más! ¡disfrútala!

Parece que también necesita establecer el nombre de host para su SSLSocketFactory.

Trate de añadir la línea

 socketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); 

Antes de crear una nueva conexión con su SSLFactory .

Aparte de las diferencias en las estructuras, tenemos código similar. En mi implementación que acaba de crear mi propia extensión de la DefaultHttpClient que se parece a la mayoría de su código anterior. Si esto no lo arregla puedo publicar el código de trabajo para eso y usted puede dar ese acercamiento un intento.

Editar: aquí está mi versión de trabajo

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

Y se puede llamar como se muestra:

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

Estoy publicando una respuesta actualizada ya que la gente sigue haciendo referencia y votando sobre esta pregunta. He tenido que cambiar el código de socket de fábrica unas cuantas veces ya que algunas cosas han cambiado desde 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); 

Espero que esto ayude a cualquiera que siga viniendo aquí en el futuro.

  • OKhttp: SSLProtocolException: Protocolo de enlace SSL finalizado
  • javax.net.ssl.SSLPeerUnverifiedException: Nombre de host no verificado:
  • Android: conexión HTTPS (SSL) mediante HttpsURLConnection
  • Cómo crear un formato BKS (BouncyCastle) Java Keystore que contiene una cadena de certificados de cliente
  • Conexión de Https con conexión cerrada por peer en Android 6.0
  • ¿Qué tan seguros son los certificados SSL de clientes en una aplicación para móviles?
  • Certificado de seguridad https no error de confianza en el móvil a través del servidor openshift
  • Ejemplo de servicio Apache Thrift que utiliza HTTPS, en Python
  • Ignorar el certificado SSL Comprobar en Android React Native
  • Cadena de confianza de Android SSL con certificados intermedios (Apache HttpClient 4.X)
  • Confiando en todos los certificados con okHttp
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.