java - 使用自签名证书的 Android SSLSockets

标签 java android ssl self-signed

这是我最近一直在努力解决的一个问题,主要是因为我觉得 Internet 上关于这个问题的信息太多没有帮助。因此,由于我刚刚找到了适合我的解决方案,所以我决定将问题和解决方案发布在这里,希望我可以让互联网成为追随我的人稍微好一点的地方! (希望这不会造成“无用”的内容!)

我有一个我一直在开发的 Android 应用程序。直到最近,我一直在使用 ServerSockets 和套接字在我的应用程序和服务器之间进行通信。然而,通信确实需要安全,所以我一直在尝试将它们转换为 SSLServerSockets 和 SSLSockets,结果证明这比我预期的要难得多。

鉴于它只是一个原型(prototype),仅使用自签名证书不会(安全)危害,这正是我正在做的。正如您可能已经猜到的,这就是问题所在。这就是我所做的,以及我遇到的问题。

我使用以下命令生成了文件“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

由于证书是自签名的,应用程序不信任它。我环顾了谷歌,一般的结论是我需要创建一个 SSLContext(在客户端中),它基于接受这个自签名证书的自定义 TrustManager。很简单,我想。在接下来的一周里,我尝试了比我记得的更多的方法来解决这个问题,但都无济于事。我现在请您回到我原来的陈述:那里有太多不完整的信息,这使得找出解决方案比本来应该的要困难得多。

我找到的唯一可行的解​​决方案是制作一个接受ALL 证书的 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);

并无一异常(exception)地给出漂亮而快乐的输出!

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

但是,这不是一个好主意,因为由于潜在的中间人攻击,该应用仍然不安全。

所以我被卡住了。我怎样才能让应用程序信任我自己的自签名证书,而不是任何一个证书?

最佳答案

我找到了一种显然应该基于 SSLContext 创建 SSLSocket 的方法,而 SSLContext 基于信任 mykeystore 的 TrustManager。 诀窍是,我们需要将 keystore 加载到自定义信任管理器中,这样 SSLSocket 就基于信任我自己的自签名证书的 SSLContext。这是通过将 keystore 加载到信任管理器中来完成的。

我找到的代码如下:

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 接受的唯一格式)。

在我的 Android 应用程序中使用“mykeystore.bks”,在我的服务器中使用“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'!

我们完成了!我的 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...";

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

关于java - 使用自签名证书的 Android SSLSockets,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24520833/

相关文章:

android - 如何在 RecyclerView 中选择位置?

ssl - 配置 cURL 以在 Windows 上使用默认系统证书存储

ssl - 无法使用 SSL RMI 启动 JMeter 4.0 客户端

Java : how to create a directory inside under usr in linux from a java app?

java - 保持 java JMX 连接打开是一个好主意

android - 如何使用adb shell将文件夹从计算机推送到sdcard

android - 获取 SQLite FTS3 表的行 ID

android - Heroku 不验证证书请求,为什么?

java - GWT:导入后 "No source code is available for type"

java - 如何调用远程java类(方法)