2014-07-01 18 views
5

Questo è un problema che ho dovuto davvero risolvere di recente, in gran parte perché ritenevo che ci fossero troppe informazioni su Internet relative a questo problema che non ha aiutato. Quindi, da quando ho appena trovato una soluzione che funziona per me, ho deciso di postare il problema e la soluzione qui, nella speranza che io possa rendere Internet un posto leggermente migliore per coloro che verranno dopo di me! (Si spera che questo non contribuisca al contenuto "inutile"!)SSLSocket Android che utilizzano certificati autofirmati

Ho un'applicazione Android che ho sviluppato. Fino a poco tempo fa, stavo usando ServerSockets e Sockets per comunicare tra la mia app e il mio server. Tuttavia, le comunicazioni devono essere sicure, quindi ho cercato di convertirle in SSLServerSockets e SSLSockets, che risulta essere molto più difficile di quanto mi aspettassi.

Visto che si tratta solo di un prototipo, non c'è alcun danno (di sicurezza) nell'utilizzo di certificati autofirmati, che è quello che sto facendo. Come probabilmente avete indovinato, è qui che i problemi arrivano. Questo è quello che ho fatto e i problemi che ho incontrato.

ho generato il file "mykeystore.jks" con il seguente comando:

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

Questo è il codice server:

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

Questo è un estratto del Android applicazione (client):

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

Quando ho eseguito il codice, non ha funzionato, e questo è l'output che stavo ottenendo.

uscita 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 ... 

uscita Server:

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

Come il certificato è autofirmato, l'applicazione non si fida di esso. Ho dato un'occhiata a Google, e il consenso generale è che ho bisogno di creare un SSLContext (nel client) che è basato su un TrustManager personalizzato che accetta questo certificato autofirmato. Abbastanza semplice, pensai. Durante la settimana successiva, ho provato più metodi per risolvere questo problema di quanto io possa ricordare, senza successo. Ora ti rimando alla mia dichiarazione originale: ci sono troppe informazioni incomplete là fuori, che hanno reso la soluzione molto più difficile di quanto avrebbe dovuto essere.

L'unica soluzione funzionante che ho trovato è stata la creazione di un TrustManager che accetta i certificati ALL.

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

che può essere utilizzato in questo modo

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

E dà l'output bello e felice senza eccezioni!

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

Tuttavia, questa non è una buona idea, in quanto l'applicazione sarà ancora non protetta, grazie a potenziali attacchi man-in-the-middle.

Quindi ero bloccato. Come posso ottenere che l'app consideri attendibile il mio certificato autofirmato, ma non tutti i certificati in circolazione?

risposta

3

Ho trovato un modo che apparentemente dovrebbe creare un SSLSocket basato su un SSLContext che è basato su un TrustManager che si fida di mykeystore. Il trucco è che dobbiamo caricare il keystore in un trust manager personalizzato, in modo tale che SSLSocket sia basato su un SSLContext che si fida del mio certificato autofirmato. Questo viene fatto caricando il keystore nel trust manager.

Il codice che ho trovato per fare questo è stato il seguente:

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

che prontamente è riuscita.

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

A quanto pare, Android non supporta JKS. Deve essere nel formato BKS.

Così ho trovato un modo per convertire da JKS al BKS eseguendo il comando seguente:

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 

Ora, ho un file chiamato mykeystore.bks che è esattamente la stessa di mykeystore.jks, tranne che in Formato BKS (che è l'unico formato accettato da Android).

Utilizzando "mykeystore.bks" con la mia app per Android e "mykeystore.jks" con il mio server, funziona! Uscita

Cliente: uscita

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

Server:

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

Abbiamo finito! La connessione SSLServerSocket/SSLSocket tra la mia applicazione Android e il mio server ora funziona con il mio certificato autofirmato.

Ecco il codice finale da dentro il mio applicazione 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..."; 

(Si noti che il codice del server non è cambiato, e il codice del server completo è nella domanda iniziale.)

Problemi correlati