Android SSLSockets使用自签名证书

BSn*_*apZ 5 java ssl android self-signed

这是我最近一直在努力解决的一个问题,很大程度上是因为我觉得互联网上关于这个问题的信息几乎没有太大帮助.因为我刚刚找到了一个适合我的解决方案,我决定在这里发布问题和解决方案,希望能让互联网成为那些追随我的人的好地方!(希望这不会导致"无益"的内容!)

我有一个我一直在开发的Android应用程序.直到最近,我一直在使用ServerSockets和Sockets在我的应用程序和我的服务器之间进行通信.但是,通信确实需要是安全的,所以我一直在尝试将它们转换为SSLServerSockets和SSLSockets,这比我预期的要困难得多.

看到它只是一个原型,只使用自签名证书就没有(安全)伤害,这就是我正在做的事情.正如你可能已经猜到的那样,这就是问题所在.这就是我所做的,以及我遇到的问题.

我用以下命令生成了文件" mykeystore.jks ":

keytool -genkey -alias serverAlias -keyalg RSA -keypass MY_PASSWORD -storepass MY_PASSWORD -keystore mykeystore.jks
Run Code Online (Sandbox Code Playgroud)

这是服务器代码:

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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是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...";
Run Code Online (Sandbox Code Playgroud)

当我运行代码时,它不起作用,这是我得到的输出.

客户输出:

Connected to server /SERVER_IP_ADDRESS: 8081
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
... stack trace ...
Run Code Online (Sandbox Code Playgroud)

服务器输出:

received: null
java.net.SocketException: Connection closed by remote host
Run Code Online (Sandbox Code Playgroud)

由于证书是自签名的,因此应用程序不信任它.我浏览了一下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; }
}
Run Code Online (Sandbox Code Playgroud)

可以像这样使用

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);
Run Code Online (Sandbox Code Playgroud)

并且毫无例外地给出了美好而快乐的输出!

Connected to server /SERVER_IP_ADDRESS: 8081
Sending request: Hello World
Received from the Server: You said 'Hello World'!
Run Code Online (Sandbox Code Playgroud)

然而,这不是一个好主意,因为由于潜在的中间人攻击,应用程序仍然是不安全的.

所以我被卡住了.我怎么能让应用程序相信我自己的自签名证书,而不仅仅是那里的任何证书?

BSn*_*apZ 4

我找到了一种显然应该基于 SSLContext 创建 SSLSocket 的方法,该 SSLContext 基于信任 mykeystore 的 TrustManager。技巧是,我们需要将密钥库加载到自定义信任管理器中,以便 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();
Run Code Online (Sandbox Code Playgroud)

这很快就失败了。

java.security.KeyStoreException: java.security.NoSuchAlgorithmException: KeyStore JKS implementation not found
Run Code Online (Sandbox Code Playgroud)

显然,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
Run Code Online (Sandbox Code Playgroud)

现在,我有一个名为 mykeystore.bks 的文件,它与 mykeystore.jks 完全相同,只是 BKS 格式(这是 Android 接受的唯一格式)。

在我的 Android 应用程序中使用“mykeystore.bks”,在我的服务器中使用“mykeystore.jks”,它可以工作!

客户端输出:

Connected to server /SERVER_IP_ADDRESS: 8081
Sending request: Hello World
Received from the Server: You said 'Hello World'!
Run Code Online (Sandbox Code Playgroud)

服务器输出:

received: Hello World
response: You said 'Hello World'!
Run Code Online (Sandbox Code Playgroud)

我们完成了!我的 Android 应用程序和服务器之间的 SSLServerSocket / SSLSocket 连接现在正在使用我的自签名证书。

这是我的 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...";
Run Code Online (Sandbox Code Playgroud)

(请注意,服务器代码没有更改,完整的服务器代码位于原始问题中。)