简体   繁体   English

Android SSLSockets使用自签名证书

[英]Android SSLSockets using self-signed certificates

This is a problem I have been really struggling to solve recently, largely because I felt that there was almost too much information on the Internet regarding this issue that did not help . 这是我最近一直在努力解决的一个问题,很大程度上是因为我觉得互联网上关于这个问题的信息几乎没有太多帮助 So since I have just found a solution that works for me, I decided that I would post the problem and solution here, in the hopes that I can make the Internet a slightly better place for those who come after me! 因为我刚刚找到了一个适合我的解决方案,我决定在这里发布问题和解决方案,希望能让互联网成为那些追随我的人的好地方! (Hopefully, this is not going to contribute to the "unhelpful" content!) (希望这不会导致“无益”的内容!)

I have an Android application that I've been developing. 我有一个我一直在开发的Android应用程序。 Until recently, I've just been using ServerSockets and Sockets to communicate between my app and my server. 直到最近,我一直在使用ServerSockets和Sockets在我的应用程序和我的服务器之间进行通信。 However, the communications do need to be secure, so I've been trying to convert these to SSLServerSockets and SSLSockets, which turns out to be a hell of a lot harder than I was expecting. 但是,通信确实需要是安全的,所以我一直在尝试将它们转换为SSLServerSockets和SSLSockets,这比我预期的要困难得多。

Seeing as it's just a prototype, there is no (security) harm in just using self-signed certificates, which is what I'm doing. 看到它只是一个原型,只使用自签名证书就没有(安全)伤害,这就是我正在做的事情。 As you've probably guessed, this is where the problems come in. This is what I had done, and the problems I encountered. 正如你可能已经猜到的那样,这就是问题所在。这就是我所做的,以及我遇到的问题。

I generated the file " mykeystore.jks " with the following command: 我用以下命令生成了文件“ mykeystore.jks ”:

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

This is the server code: 这是服务器代码:

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

This is an extract from the Android application (client) : 这是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...";

When I ran the code, it didn't work, and this is the output I was getting. 当我运行代码时,它不起作用,这是我得到的输出。

Client output: 客户输出:

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

Server output: 服务器输出:

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

As the certificate is self-signed, the app does not trust it. 由于证书是自签名的,因此应用程序不信任它。 I had a look around Google, and the general consesus is that I need to create an SSLContext (in the client) which is based on a custom TrustManager that accepts this self-signed certificate. 我浏览了一下Google,一般认为我需要创建一个SSLContext(在客户端中),它基于接受这个自签名证书的自定义TrustManager。 Simple enough, I thought. 很简单,我想。 Over the next week, I tried more methods of solving this issue than I can possibly remember, to no avail. 在接下来的一周里,我尝试了更多解决这个问题的方法,而不是我记得的,但无济于事。 I now refer you back to my original statement: there is far too much incomplete information out there, which made figuring out the solution a lot harder than it should have been. 我现在请你回到我原来的陈述:那里有太多不完整的信息,这使得解决方案比应该更加困难。

The only working solution I found, was the make a TrustManager that accepts ALL certificates. 我找到的唯一可行解决方案是创建一个接受所有证书的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; }
}

which can be used like this 可以像这样使用

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

And gives the nice and happy output with no exceptions! 并且毫无例外地给出了美好而快乐的输出!

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

However, this is not a good idea , because the app will still be unsecure, thanks to potential man-in-the-middle attacks. 然而, 这不是一个好主意 ,因为由于潜在的中间人攻击,应用程序仍然是不安全的。

So I was stuck. 所以我被卡住了。 How could I get the app to trust my own self-signed certificate, but not just any and every certificate out there? 我怎么能让应用程序相信我自己的自签名证书,而不仅仅是那里的任何证书?

I found a way which apparently should create an SSLSocket based on a SSLContext which is based on a TrustManager which trusts mykeystore. 我发现了一种显然应该基于SSLContext创建SSLSocket的方法,该SSLContext基于信任mykeystore的TrustManager。 The trick is, we need to load the keystore in to a custom trust manager, such that the SSLSocket is based on an SSLContext which trusts my own self-signed certificate. 诀窍是,我们需要将密钥库加载到自定义信任管理器中,这样SSLSocket基于SSLContext,它信任我自己的自签名证书。 This is done by loading the keystore into the trust manager. 这是通过将密钥库加载到信任管理器中完成的。

The code I found to do this was as follows: 我发现这样做的代码如下:

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

Which promptly failed. 哪个及时失败了。

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

Apparently, Android doesn't support JKS. 显然,Android不支持JKS。 It has to be in the BKS format. 它必须是BKS格式。

So I found a way to convert from JKS to BKS by running the following command: 所以我找到了一种通过运行以下命令从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

Now, I have a file called mykeystore.bks which is exactly the same as mykeystore.jks, except in BKS format (which is the only format Android accepts). 现在,我有一个名为mykeystore.bks的文件,它与mykeystore.jks完全相同,除了BKS格式(这是Android接受的唯一格式)。

Using "mykeystore.bks" with my Android app, and "mykeystore.jks" with my server, it works! 在我的Android应用程序中使用“mykeystore.bks”,在我的服务器上使用“mykeystore.jks”,它可以正常工作!

Client output: 客户输出:

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

Server output: 服务器输出:

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

We're done! 我们完成了! The SSLServerSocket / SSLSocket connection between my Android application and my server is now working with my self-signed certificate. 我的Android应用程序和我的服务器之间的SSLServerSocket / SSLSocket连接正在使用我的自签名证书。

Here is the final code from within my Android application: 以下是我的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...";

(Note that the server code has not changed, and the full server code is in the original question.) (请注意,服务器代码未更改,完整的服务器代码位于原始问题中。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM