Перейти к основному содержимому

Аутентификация сертификата клиента Java HTTPS

· 5 мин. чтения

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 .