#java #android #ssl #self-signed
#java #Android #ssl #самозаверяющий
Вопрос:
Это проблема, которую я действительно пытался решить в последнее время, в основном потому, что я чувствовал, что в Интернете было слишком много информации об этой проблеме, которая не помогла. Итак, поскольку я только что нашел решение, которое работает для меня, я решил опубликовать проблему и решение здесь, в надежде, что я смогу сделать Интернет немного лучшим местом для тех, кто придет после меня! (Надеюсь, это не приведет к «бесполезному» контенту!)
У меня есть приложение для Android, которое я разрабатывал. До недавнего времени я просто использовал ServerSockets и сокеты для связи между моим приложением и моим сервером. Тем не менее, коммуникации должны быть безопасными, поэтому я пытался преобразовать их в 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'!
Однако это не очень хорошая идея, потому что приложение все равно будет небезопасным из-за потенциальных атак «человек посередине».
Итак, я застрял. Как я мог бы заставить приложение доверять моему собственному самозаверяющему сертификату, а не только любому имеющемуся сертификату?
Ответ №1:
Я нашел способ, который, по-видимому, должен создать SSLSocket на основе SSLContext, который основан на TrustManager, который доверяет mykeystore. Хитрость в том, что нам нужно загрузить хранилище ключей в пользовательский менеджер доверия, чтобы 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).
Используя «mykeystore.bks» с моим приложением для Android и «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'!
Мы закончили! Соединение SSLServerSocket / SSLSocket между моим приложением Android и моим сервером теперь работает с моим самозаверяющим сертификатом.
Вот окончательный код из моего приложения для 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...";
(Обратите внимание, что код сервера не изменился, а полный код сервера приведен в исходном вопросе.)