这是我最近一直在努力解决的一个问题,主要是因为我觉得 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/