Следующая ошибка может возникать по разным причинам:
javax.net.ssl.SSLHandshakeException: no cipher suites in common
Точки, которые нужно проверить при отладке:
- Хранилище ключей и хранилище доверенных сертификатов правильно загружены и используются для созданиясоединения сокетов
- Сертификат совместим с включенными комплектами шифров
- На клиенте и сервере должен быть включен хотя бы один общий комплект шифров, и этот комплект шифров должен бытьтакже совместим с сертификатом
Например, в следующем примере я использую Java 8 с набором шифров по умолчанию.Сгенерированный мной сертификат использует ECDSA и SHA384, и, следовательно, когда между сервером и клиентом установлено соединение TLS, я вижу, что согласованный набор шифров равен TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
, включив отладку (System.setProperty("javax.net.debug", "ssl");
).
Ниже приведен рабочий пример:
В качестве первого шага необходимо создать пару ключей и сертификат.Для тестирования давайте создадим самозаверяющий сертификат и используем один и тот же сертификат как для сервера, так и для клиента:
keytool -genkeypair -alias server -keyalg EC \
-sigalg SHA384withECDSA -keysize 256 -keystore servercert.p12 \
-storetype pkcs12 -v -storepass abc123 -validity 10000 -ext san=ip:127.0.0.1
Давайте теперь создадим сервер:
package com.sapbasu.javastudy;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Objects;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManagerFactory;
/*
* keytool -genkeypair -alias server -keyalg EC \
* -sigalg SHA384withECDSA -keysize 256 -keystore servercert.p12 \
* -storetype pkcs12 -v -storepass abc123 -validity 10000 -ext san=ip:127.0.0.1
*/
public class TLSServer {
public void serve(int port, String tlsVersion, String trustStoreName,
char[] trustStorePassword, String keyStoreName, char[] keyStorePassword)
throws Exception {
Objects.requireNonNull(tlsVersion, "TLS version is mandatory");
if (port <= 0) {
throw new IllegalArgumentException(
"Port number cannot be less than or equal to 0");
}
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream tstore = TLSServer.class
.getResourceAsStream("/" + trustStoreName);
trustStore.load(tstore, trustStorePassword);
tstore.close();
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream kstore = TLSServer.class
.getResourceAsStream("/" + keyStoreName);
keyStore.load(kstore, keyStorePassword);
KeyManagerFactory kmf = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, keyStorePassword);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
SecureRandom.getInstanceStrong());
SSLServerSocketFactory factory = ctx.getServerSocketFactory();
try (ServerSocket listener = factory.createServerSocket(port)) {
SSLServerSocket sslListener = (SSLServerSocket) listener;
sslListener.setNeedClientAuth(true);
sslListener.setEnabledProtocols(new String[] {tlsVersion});
// NIO to be implemented
while (true) {
try (Socket socket = sslListener.accept()) {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("Hello World!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
Сейчассоздайте клиента:
package com.sapbasu.javastudy;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Objects;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManagerFactory;
public class TLSClient {
public String request(InetAddress serverHost, int serverPort,
String tlsVersion, String trustStoreName, char[] trustStorePassword,
String keyStoreName, char[] keyStorePassword) throws Exception {
Objects.requireNonNull(tlsVersion, "TLS version is mandatory");
Objects.requireNonNull(serverHost, "Server host cannot be null");
if (serverPort <= 0) {
throw new IllegalArgumentException(
"Server port cannot be lesss than or equal to 0");
}
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream tstore = TLSClient.class
.getResourceAsStream("/" + trustStoreName);
trustStore.load(tstore, trustStorePassword);
tstore.close();
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream kstore = TLSClient.class
.getResourceAsStream("/" + keyStoreName);
keyStore.load(kstore, keyStorePassword);
KeyManagerFactory kmf = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, keyStorePassword);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
SecureRandom.getInstanceStrong());
SocketFactory factory = ctx.getSocketFactory();
try (Socket connection = factory.createSocket(serverHost, serverPort)) {
((SSLSocket) connection).setEnabledProtocols(new String[] {tlsVersion});
SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
((SSLSocket) connection).setSSLParameters(sslParams);
BufferedReader input = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
return input.readLine();
}
}
}
Наконец, вот тест JUnit для проверки соединения:
package com.sapbasu.javastudy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.net.InetAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Test;
public class TLSServerClientTest {
private static final int SERVER_PORT = 8444;
private static final String TLS_VERSION = "TLSv1.2";
private static final int SERVER_COUNT = 1;
private static final String SERVER_HOST_NAME = "127.0.0.1";
private static final String TRUST_STORE_NAME = "servercert.p12";
private static final char[] TRUST_STORE_PWD = new char[] {'a', 'b', 'c', '1',
'2', '3'};
private static final String KEY_STORE_NAME = "servercert.p12";
private static final char[] KEY_STORE_PWD = new char[] {'a', 'b', 'c', '1',
'2', '3'};
@Test
public void whenClientSendsServerRequest_givenServerIsUp_returnsHelloWorld()
throws Exception {
TLSServer server = new TLSServer();
TLSClient client = new TLSClient();
System.setProperty("javax.net.debug", "ssl");
ExecutorService serverExecutor = Executors.newFixedThreadPool(SERVER_COUNT);
serverExecutor.submit(() -> {
try {
server.serve(SERVER_PORT, TLS_VERSION, TRUST_STORE_NAME,
TRUST_STORE_PWD, KEY_STORE_NAME, KEY_STORE_PWD);
} catch (Exception e) {
e.printStackTrace();
}
});
try {
String returnedValue = client.request(
InetAddress.getByName(SERVER_HOST_NAME), SERVER_PORT, TLS_VERSION,
TRUST_STORE_NAME, TRUST_STORE_PWD, KEY_STORE_NAME, KEY_STORE_PWD);
assertEquals("Hello World!", returnedValue);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
Примечание: сертификат (в данном примере servercert.p12) долженбыть в пути.В этом примере я сохранил его в папке test / resources структуры папок Maven, чтобы тест JUnit мог получить его в classpath.
Cipher Suite Background
При использовании TLS / SSL используемые криптографические алгоритмы определяются комплектами шифров.Сервер поддерживает набор наборов шифров (вы можете включать или отключать определенные наборы в соответствии с вашими потребностями и желаемым уровнем безопасности).Клиент также поддерживает наборы шифров.Во время настройки соединения используемый набор шифров согласовывается между клиентом и сервером.Предпочтение клиента будет учтено, учитывая, что сервер поддерживает этот конкретный набор шифров.
Вы можете найти список наборов шифров, поддерживаемых поставщиками Sun, до Java 8 здесь .
Типичное имя набора шифров выглядит следующим образом:
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
Здесь *
ECDHE стоитдля эллиптической кривой Diffie Hellman Ephemeral.Это алгоритм обмена ключами.Эллиптический вариант (первый E) используется для исполнения, тогда как эфемерный вариант (последний E) предназначен для прямой секретности.Прямая секретность означает, что если злоумышленник продолжает записывать все сообщения по TLS и в более поздний момент времени каким-то образом получает закрытый ключ, он / она не может расшифровать записанные в прошлом сообщения.
ECDSA - это алгоритм цифровой подписи, используемый для подписи ключа и используемый для аутентификации (проверки целостности) общего секрета.ECDSA слабее и медленнее, чем другие алгоритмы аутентификации, такие как HMAC.Тем не менее, он используется для аутентификации с общим ключом, потому что ему не нужно, чтобы верификатор знал секретный ключ, используемый для создания тега аутентификации.Сервер может очень хорошо использовать свой закрытый ключ для проверки целостности сообщения.
AES_128_GCM - После того, как общий секретный ключ открыт для обеих сторон (обычно это браузер и веб-сервер)), алгоритм шифрования симметричного блочного шифра используется для шифрования обмена сообщениями между сторонами.В данном конкретном случае используется блочный шифр AES со 128-битным ключом и режимом аутентификации GCM.
SHA256 - алгоритм хеширования