这是我最近一直在努力解决的一个问题,很大程度上是因为我觉得互联网上关于这个问题的信息太多了没有帮助。因为我刚刚找到了一个适合我的解决方案,我决定在这里发布问题和解决方案,希望能让互联网成为那些追随我的人的好地方! (希望这不会导致“无益”的内容!)
我有一个我一直在开发的Android应用程序。直到最近,我一直在使用ServerSockets和Sockets在我的应用程序和我的服务器之间进行通信。但是,通信确实需要是安全的,所以我一直在尝试将它们转换为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'!
然而,这不是一个好主意,因为潜在的中间人攻击会使应用仍然不安全。
所以我被困住了。我怎么能让应用程序相信我自己的自签名证书,而不仅仅是那里的任何证书?
答案 0 :(得分:3)
我找到了一种显然应该基于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();
哪个很快就失败了。
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...";
(请注意,服务器代码未更改,完整服务器代码位于原始问题中。)