SSL SSLSockets con certificados autofirmados

Este es un problema que he estado luchando para resolver recientemente, en gran medida porque sentía que había casi demasiada información en Internet con respecto a este tema que no ayudó . Así que desde que acabo de encontrar una solución que funcione para mí, decidí que publicaría el problema y la solución aquí, con la esperanza de que puedo hacer de Internet un lugar un poco mejor para aquellos que vienen después de mí! (Esperemos que esto no va a contribuir al contenido "poco servicial"!)

Tengo una aplicación de Android que he estado desarrollando. Hasta hace poco, acabo de usar ServerSockets y Sockets para comunicarme entre mi aplicación y mi servidor. Sin embargo, las comunicaciones tienen que ser seguras, por lo que he estado tratando de convertir a SSLServerSockets y SSLSockets, que resulta ser un infierno de mucho más difícil de lo que esperaba.

Al ver que sólo es un prototipo, no hay daño (de seguridad) en el uso de certificados autofirmados, que es lo que estoy haciendo. Como probablemente has adivinado, aquí es donde entran los problemas. Esto es lo que había hecho, y los problemas que encontré.

He generado el archivo " mykeystore.jks " con el siguiente comando:

keytool -genkey -alias serverAlias -keyalg RSA -keypass MY_PASSWORD -storepass MY_PASSWORD -keystore mykeystore.jks 

Este es el código del servidor:

 import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetAddress; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; /** * Server */ public class simplesslserver { // Global variables private static SSLServerSocketFactory ssf; private static SSLServerSocket ss; private static final int port = 8081; private static String address; /** * Main: Starts the server and waits for clients to connect. * Each client is given its own thread. * @param args */ public static void main(String[] args) { try { // System properties System.setProperty("javax.net.ssl.keyStore","mykeystore.jks"); System.setProperty("javax.net.ssl.keyStorePassword","MY_PASSWORD"); // Start server ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); ss = (SSLServerSocket) ssf.createServerSocket(port); address = InetAddress.getLocalHost().toString(); System.out.println("Server started at "+address+" on port "+port+"\n"); // Wait for messages while (true) { SSLSocket connected = (SSLSocket) ss.accept(); new clientThread(connected).start(); } } catch (Exception e) { e.printStackTrace(); } } /** * Client thread. */ private static class clientThread extends Thread { // Variables private SSLSocket cs; private InputStreamReader isr; private OutputStreamWriter osw; private BufferedReader br; private BufferedWriter bw; /** * Constructor: Initialises client socket. * @param clientSocket The socket connected to the client. */ public clientThread(SSLSocket clientSocket) { cs = clientSocket; } /** * Starts the thread. */ public void run() { try { // Initialise streams isr = new InputStreamReader(cs.getInputStream()); br = new BufferedReader(isr); osw = new OutputStreamWriter(cs.getOutputStream()); bw = new BufferedWriter(osw); // Get request from client String tmp = br.readLine(); System.out.println("received: "+tmp); // Send response to client String resp = "You said '"+tmp+"'!"; bw.write(resp); bw.newLine(); bw.flush(); System.out.println("response: "+resp); } catch (Exception e) { e.printStackTrace(); } } } } 

Este es un extracto de la aplicación de Android (cliente) :

 String message = "Hello World"; try{ // Create SSLSocketFactory SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault(); // Create socket using SSLSocketFactory SSLSocket client = (SSLSocket) factory.createSocket("SERVER_IP_ADDRESS", 8081); // Print system information System.out.println("Connected to server " + client.getInetAddress() + ": " + client.getPort()); // Writer and Reader BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream())); BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream())); // Send request to server System.out.println("Sending request: "+message); writer.write(message); writer.newLine(); writer.flush(); // Receive response from server String response = reader.readLine(); System.out.println("Received from the Server: "+response); // Close connection client.close(); return response; } catch(Exception e) { e.printStackTrace(); } return "Something went wrong..."; 

Cuando corrí el código, no funcionó, y esta es la salida que estaba recibiendo.

Salida del cliente:

 Connected to server /SERVER_IP_ADDRESS: 8081 javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. ... stack trace ... 

Salida del servidor:

 received: null java.net.SocketException: Connection closed by remote host 

Como el certificado se auto-firma, la aplicación no confía en él. Tuve una mirada alrededor de Google, y el consesus general es que necesito crear un SSLContext (en el cliente) que se basa en un TrustManager personalizado que acepta este certificado auto-firmado. Simplemente, pensé. Durante la semana siguiente, he intentado más métodos de resolver este problema de lo que puedo recordar, sin éxito. Ahora le remito a mi declaración original: hay mucha información incompleta por ahí, lo que hizo que averiguar la solución mucho más difícil de lo que debería haber sido.

La única solución de trabajo que encontré, era hacer un TrustManager que acepta TODOS los certificados.

 private static class AcceptAllTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} @Override public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} @Override public X509Certificate[] getAcceptedIssuers() { return null; } } 

Que se puede utilizar de esta manera

 SSLContext sslctx = SSLContext.getInstance("TLS"); sslctx.init(new KeyManager[0], new TrustManager[] {new AcceptAllTrustManager()}, new SecureRandom()); SSLSocketFactory factory = sslctx.getSocketFactory(); SSLSocket client = (SSLSocket) factory.createSocket("IP_ADDRESS", 8081); 

Y da la salida agradable y feliz sin excepciones!

 Connected to server /SERVER_IP_ADDRESS: 8081 Sending request: Hello World Received from the Server: You said 'Hello World'! 

Sin embargo, esto no es una buena idea , porque la aplicación seguirá siendo insegura, gracias a posibles ataques de hombre en el medio.

Así que estaba atascado. ¿Cómo puedo conseguir que la aplicación confíe en mi propio certificado autofirmado, pero no sólo en todos los certificados disponibles?

Encontré una manera que aparentemente debería crear un SSLSocket basado en un SSLContext que se basa en un TrustManager que confía mykeystore. El truco es que tenemos que cargar el almacén de claves en un gestor de confianza personalizado, de modo que SSLSocket se base en un SSLContext que confía en mi propio certificado autofirmado. Esto se hace cargando el keystore en el gestor de confianza.

El código que encontré para hacer esto era como sigue:

 KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(getResources().openRawResource(R.raw.mykeystore), "MY_PASSWORD".toCharArray()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); SSLContext sslctx = SSLContext.getInstance("TLS"); sslctx.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); SSLSocketFactory factory = sslctx.getSocketFactory(); 

Que rápidamente falló.

 java.security.KeyStoreException: java.security.NoSuchAlgorithmException: KeyStore JKS implementation not found 

Aparentemente, Android no es compatible con JKS. Tiene que estar en el formato BKS.

Así que encontré una manera de convertir de JKS a BKS ejecutando el siguiente comando:

 keytool -importkeystore -srckeystore mykeystore.jks -destkeystore mykeystore.bks -srcstoretype JKS -deststoretype BKS -srcstorepass MY_PASSWORD -deststorepass MY_PASSWORD -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov-jdk15on-150.jar 

Ahora, tengo un archivo llamado mykeystore.bks que es exactamente igual que mykeystore.jks, excepto en formato BKS (que es el único formato que acepta Android).

Utilizando "mykeystore.bks" con mi aplicación para Android, y "mykeystore.jks" con mi servidor, ¡funciona!

Salida del cliente:

 Connected to server /SERVER_IP_ADDRESS: 8081 Sending request: Hello World Received from the Server: You said 'Hello World'! 

Salida del servidor:

 received: Hello World response: You said 'Hello World'! 

¡Ya terminamos! La conexión SSLServerSocket / SSLSocket entre mi aplicación de Android y mi servidor ahora está trabajando con mi certificado autofirmado.

Aquí está el código final de mi aplicación de Android:

 String message = "Hello World"; try{ // Load the server keystore KeyStore keyStore = KeyStore.getInstance("BKS"); keyStore.load(ctx.getResources().openRawResource(R.raw.mykeystore), "MY_PASSWORD".toCharArray()); // Create a custom trust manager that accepts the server self-signed certificate TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); // Create the SSLContext for the SSLSocket to use SSLContext sslctx = SSLContext.getInstance("TLS"); sslctx.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); // Create SSLSocketFactory SSLSocketFactory factory = sslctx.getSocketFactory(); // Create socket using SSLSocketFactory SSLSocket client = (SSLSocket) factory.createSocket("SERVER_IP_ADDRESS", 8081); // Print system information System.out.println("Connected to server " + client.getInetAddress() + ": " + client.getPort()); // Writer and Reader BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream())); BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream())); // Send request to server System.out.println("Sending request: "+message); writer.write(message); writer.newLine(); writer.flush(); // Receive response from server String response = reader.readLine(); System.out.println("Received from the Server: "+response); // Close connection client.close(); return response; } catch(Exception e) { e.printStackTrace(); } return "Something went wrong..."; 

(Tenga en cuenta que el código del servidor no ha cambiado y el código completo del servidor está en la pregunta original).

  • Cómo hacer la reanudación de sesión SSL en Android
  • La conexión de twitter falló
  • ¿Android Volley soporta SSL?
  • Necesitan ayuda para entender las cadenas de certificados
  • Tengo un archivo keystore, ¿cómo puedo proporcionar keyManagers a sslContext en la aplicación de Android?
  • El servidor no admite la suite de cifrado predeterminada de Android 5.0
  • ¿Cómo puedo establecer SignalR en Android Studio para ignorar los problemas de SSL para el desarrollo
  • SSLHandshakeException Ancla de confianza para ruta de certificación no encontrada Android HTTPS
  • Android java.security.cert.CertPathValidatorException: Ancla de confianza para la ruta de certificación no encontrada
  • ¿Cómo anular la lista de cifrado enviada al servidor por Android cuando se utiliza HttpsURLConnection?
  • Determinar el certificado de firma de un APK
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.