1. Обзор
HTTPS — это расширение HTTP, которое обеспечивает безопасную связь между двумя объектами в компьютерной сети. HTTPS использует протокол TLS (Transport Layer Security) для обеспечения безопасных соединений.
TLS может быть реализован с односторонней или двусторонней проверкой сертификата . В одностороннем порядке сервер делится своим общедоступным сертификатом, чтобы клиент мог убедиться, что это доверенный сервер. Альтернативой является двусторонняя проверка. И клиент, и сервер совместно используют свои общедоступные сертификаты для проверки подлинности друг друга .
В этой статье речь пойдет о двусторонней проверке сертификата, когда сервер также будет проверять сертификат клиента .
2. Версии Java и TLS
TLS 1.3 — последняя версия протокола. Эта версия более производительна и безопасна . Он имеет более эффективный протокол рукопожатия и использует современные криптографические алгоритмы.
Java начала поддерживать эту версию протокола в Java 11. Мы будем использовать эту версию для генерации сертификатов и реализации простой пары клиент-сервер, которая использует TLS для аутентификации друг друга.
3. Генерация сертификатов в Java
Поскольку мы используем двустороннюю аутентификацию TLS , нам нужно сгенерировать сертификаты для клиента и сервера.
В производственной среде рекомендуется приобретать сертификаты в центре сертификации. Однако для целей тестирования или демонстрации достаточно использовать самозаверяющие сертификаты . В этой статье мы собираемся использовать Java keytool
для создания самозаверяющих сертификатов.
3.1. Сертификат сервера
Во- первых, мы создаем хранилище ключей сервера :
keytool -genkey -alias serverkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore serverkeystore.p12 -storepass password -ext san=ip:127.0.0.1,dns:localhost
Мы используем параметр keytool -ext
для установки альтернативных имен субъекта (SAN) для определения локального имени хоста/IP-адреса, который идентифицирует сервер. Как правило, с помощью этой опции мы можем указать несколько адресов. Однако клиенты будут вынуждены использовать один из этих адресов для подключения к серверу.
Далее экспортируем сертификат в файл server-certificate.pem
:
keytool -exportcert -keystore serverkeystore.p12 -alias serverkey -storepass password -rfc -file server-certificate.pem
Наконец, мы добавляем сертификат сервера в хранилище доверенных сертификатов клиента :
keytool -import -trustcacerts -file server-certificate.pem -keypass password -storepass password -keystore clienttruststore.jks
3.2. Сертификат клиента
Точно так же мы создаем хранилище ключей клиента и экспортируем его сертификат:
keytool -genkey -alias clientkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore clientkeystore.p12 -storepass password -ext san=ip:1`27.0.0.1,dns:localhost
keytool -exportcert -keystore clientkeystore.p12 -alias clientkey -storepass password -rfc -file client-certificate.pem
keytool -import -trustcacerts -file client-certificate.pem -keypass password -storepass password -keystore servertruststore.jks
В последней команде мы добавили сертификат клиента в хранилище доверенных сертификатов сервера .
4. Реализация серверной Java
При использовании сокетов Java реализация сервера тривиальна. Класс SSLSocketEchoServer
получает SSLServerSocket
для простой поддержки аутентификации TLS. Нам просто нужно указать шифр и протоколы, а все остальное — это просто стандартный эхо-сервер, который отвечает на те же сообщения, что и клиент:
public class SSLSocketEchoServer {
static void startServer(int port) throws IOException {
ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
try (SSLServerSocket listener = (SSLServerSocket) factory.createServerSocket(port)) {
listener.setNeedClientAuth(true);
listener.setEnabledCipherSuites(new String[] { "TLS_AES_128_GCM_SHA256" });
listener.setEnabledProtocols(new String[] { "TLSv1.3" });
System.out.println("listening for messages...");
try (Socket socket = listener.accept()) {
InputStream is = new BufferedInputStream(socket.getInputStream());
byte[] data = new byte[2048];
int len = is.read(data);
String message = new String(data, 0, len);
OutputStream os = new BufferedOutputStream(socket.getOutputStream());
System.out.printf("server received %d bytes: %s%n", len, message);
String response = message + " processed by server";
os.write(response.getBytes(), 0, response.getBytes().length);
os.flush();
}
}
}
}
Сервер прослушивает клиентские соединения. Для вызова listener.setNeedClientAuth(true)
клиент должен поделиться своим сертификатом с сервером . В фоновом режиме реализация SSLServerSocket
аутентифицирует клиента с помощью протокола TLS.
В нашем случае самозаверяющий сертификат клиента находится в доверенном хранилище сервера, так что сокет примет соединение . Сервер продолжает читать сообщение, используя InputStream
. Затем он использует OututStream
для отражения входящего сообщения с добавлением подтверждения.
5. Реализация клиентской Java
Так же, как и с сервером, клиентская реализация представляет собой простой класс SSLScocketClient
:
public class SSLScocketClient {
static void startClient(String host, int port) throws IOException {
SocketFactory factory = SSLSocketFactory.getDefault();
try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
socket.setEnabledCipherSuites(new String[] { "TLS_AES_128_GCM_SHA256" });
socket.setEnabledProtocols(new String[] { "TLSv1.3" });
String message = "Hello World Message";
System.out.println("sending message: " + message);
OutputStream os = new BufferedOutputStream(socket.getOutputStream());
os.write(message.getBytes());
os.flush();
InputStream is = new BufferedInputStream(socket.getInputStream());
byte[] data = new byte[2048];
int len = is.read(data);
System.out.printf("client received %d bytes: %s%n", len, new String(data, 0, len));
}
}
}
Сначала мы создаем SSLSocket
, который устанавливает соединение с сервером. В фоновом режиме сокет установит рукопожатие установления соединения TLS. В рамках этого рукопожатия клиент проверит сертификат сервера и проверит, находится ли он в хранилище доверенных сертификатов
клиента .
После того, как соединение было успешно установлено, клиент отправляет сообщение на сервер, используя выходной поток. Затем он считывает ответ сервера с входным потоком.
6. Запуск приложений
Чтобы запустить сервер, откройте командное окно и выполните:
java -Djavax.net.ssl.keyStore=/path/to/serverkeystore.p12 \
-Djavax.net.ssl.keyStorePassword=password \
-Djavax.net.ssl.trustStore=/path/to/servertruststore.jks \
-Djavax.net.ssl.trustStorePassword=password \
com.foreach.httpsclientauthentication.SSLSocketEchoServer
Указываем системные свойства для javax.net.ssl.
хранилище ключей
и javax.net.ssl.
trustStore
, чтобы указать на файлы serverkeystore.p12
и servertruststore.jks
, которые мы создали ранее с помощью keytool
.
Чтобы запустить клиент, мы открываем другое командное окно и запускаем:
java -Djavax.net.ssl.keyStore=/path/to/clientkeystore.p12 \
-Djavax.net.ssl.keyStorePassword=password \
-Djavax.net.ssl.trustStore=/path/to/clienttruststore.jks \
-Djavax.net.ssl.trustStorePassword=password \
com.foreach.httpsclientauthentication.SSLScocketClient
Точно так же мы устанавливаем javax.net.ssl.keyStore
и javax.net.ssl.
Системные свойства trustStore , чтобы они указывали на файлы
clientkeystore.p12
и clienttruststore.jks
, которые мы создали ранее с помощью keytool
.
7. Заключение
Мы написали простую клиент-серверную реализацию Java, которая использует сертификаты сервера и клиента для выполнения двунаправленной аутентификации TLS .
Мы использовали keytool
для создания самозаверяющих сертификатов.
Исходный код примеров можно найти на GitHub .