SSLSockets для Android с использованием самозаверяющих сертификатов

#java #android #ssl #self-signed

#java #Android #ssl #самозаверяющий

Вопрос:

Это проблема, которую я действительно пытался решить в последнее время, в основном потому, что я чувствовал, что в Интернете было слишком много информации об этой проблеме, которая не помогла. Итак, поскольку я только что нашел решение, которое работает для меня, я решил опубликовать проблему и решение здесь, в надежде, что я смогу сделать Интернет немного лучшим местом для тех, кто придет после меня! (Надеюсь, это не приведет к «бесполезному» контенту!)

У меня есть приложение для Android, которое я разрабатывал. До недавнего времени я просто использовал ServerSockets и сокеты для связи между моим приложением и моим сервером. Тем не менее, коммуникации должны быть безопасными, поэтому я пытался преобразовать их в SSLServerSockets и SSLSockets, что оказалось намного сложнее, чем я ожидал.

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

Я сгенерировал файл «mykeystore.jks» с помощью следующей команды:

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

Это код сервера:

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

Это выдержка из приложения для Android (клиента):

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

Когда я запустил код, он не сработал, и вот результат, который я получал.

Вывод клиента:

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

Вывод с сервера:

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

Поскольку сертификат самозаверяющий, приложение ему не доверяет. Я просмотрел Google, и общий вывод заключается в том, что мне нужно создать SSLContext (в клиенте), основанный на пользовательском TrustManager, который принимает этот самозаверяющий сертификат. Я думал, что это достаточно просто. В течение следующей недели я перепробовал больше методов решения этой проблемы, чем могу вспомнить, но безрезультатно. Теперь я возвращаю вас к моему первоначальному заявлению: существует слишком много неполной информации, что сделало поиск решения намного сложнее, чем должно было быть.

Единственным рабочим решением, которое я нашел, было создание TrustManager, который принимает ВСЕ сертификаты.

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

которые можно использовать следующим образом

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

И дает приятный и довольный результат без исключений!

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

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

Итак, я застрял. Как я мог бы заставить приложение доверять моему собственному самозаверяющему сертификату, а не только любому имеющемуся сертификату?

Ответ №1:

Я нашел способ, который, по-видимому, должен создать SSLSocket на основе SSLContext, который основан на TrustManager, который доверяет mykeystore. Хитрость в том, что нам нужно загрузить хранилище ключей в пользовательский менеджер доверия, чтобы SSLSocket был основан на SSLContext, который доверяет моему собственному самозаверяющему сертификату. Это делается путем загрузки хранилища ключей в менеджер доверия.

Код, который я нашел для этого, был следующим:

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

Который быстро завершился неудачей.

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

По-видимому, Android не поддерживает JKS. Это должно быть в формате BKS.

Итак, я нашел способ конвертировать из JKS в BKS, выполнив следующую команду:

 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
  

Теперь у меня есть файл с именем mykeystore.bks, который точно такой же, как mykeystore.jks, за исключением формата BKS (который является единственным форматом, который принимает Android).

Используя «mykeystore.bks» с моим приложением для Android и «mykeystore.jks» с моим сервером, это работает!

Вывод клиента:

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

Вывод с сервера:

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

Мы закончили! Соединение SSLServerSocket / SSLSocket между моим приложением Android и моим сервером теперь работает с моим самозаверяющим сертификатом.

Вот окончательный код из моего приложения для 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...";
  

(Обратите внимание, что код сервера не изменился, а полный код сервера приведен в исходном вопросе.)