SSLServerSocket 和证书设置
SSLServerSocket and certificate setup
我使用 ServerSocket
和 Socket
对象编写了 client/server Java 程序。然后我修改了代码以使用 SSLServerSocket
和 'SSLSocket` 但是我抛出了不同的异常,包括:
javax.net.ssl.SSLHandshakeException: no cipher suites in common
我希望尽可能多地以编程方式进行。我也可以使用自签名证书。
我遵循的一个教程建议使用 keytool
java 应用程序创建一个证书,然后将该文件移动到您的 java 项目中。我已经使用终端命令 keytool -genkey -alias zastore -keyalg RSA -keystore za.store
完成了该操作。我将密码指定为 password
.
然后我调用函数 System.setProperty
希望 SSLSockets 工作,但它仍然没有。
这是我的服务器代码
public class Server implements Runnable
{
private SSLServerSocket serverSocket;
private int portNumber;
private Thread acceptThread;
private LinkedList<Connection> connections;
private ConnectionListener connectionListener;
public Server(int port, ConnectionListener connectionListener)
{
this.connectionListener = connectionListener;
portNumber = port;
connections = new LinkedList<Connection>();
try
{
System.setProperty("javax.net.ssl.trustStore", "za.store");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SSLServerSocketFactory sslssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
serverSocket = (SSLServerSocket) sslssf.createServerSocket(portNumber,15);
}
catch (IOException e)
{
e.printStackTrace();
}
}
public void startListening()
{
acceptThread = new Thread(this);
acceptThread.start();
}
public void stopListening()
{
for(Connection c:connections)
{
c.stopListeningAndDisconnect();
}
try
{
serverSocket.close();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void run()
{
try
{
while(true)
{
SSLSocket s = (SSLSocket) serverSocket.accept();
Connection c = new Connection(s,connectionListener);
connections.add(c);
System.out.println("New Connection Established From"+s.getInetAddress().toString());
}
}
catch(java.net.SocketException e)
{
System.out.println("Listening thread terminated with exception.");
}
catch(IOException e)
{
e.printStackTrace();
}
}
public void removeConnection(Connection c)
{
connections.remove(c);
}
public void printConnections()
{
System.out.println("Number of connections "+connections.toString());
for(int i=0; i<connections.size(); i++)
{
System.out.println(connections.toString());
}
}
}
然后是按下按钮时连接的客户端代码片段:
@Override
public void actionPerformed(ActionEvent e)
{
if(e.getSource() == connect)
{
try
{
System.setProperty("javax.net.ssl.trustStore", "za.store");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SSLSocketFactory sslsf = (SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket s = (SSLSocket)sslsf.createSocket(ipBox.getText(), Integer.parseInt(portBox.getText()));
Connection c = new Connection(s,parent);
parent.connectionSuccessful(c);
}
catch (NumberFormatException e1)
{
JOptionPane.showMessageDialog(this, "Error! Port number must be a number", "Error", JOptionPane.ERROR_MESSAGE);
}
catch (UnknownHostException e1)
{
JOptionPane.showMessageDialog(this, "Error! Unable to find that host", "Error", JOptionPane.ERROR_MESSAGE);
}
catch (IOException e1)
{
e1.printStackTrace();
}
}
}
一篇 Whosebug 文章建议我的服务器 "doesn't have a certificate." 我不知道那是什么意思,也不知道如何获取服务器并将其放置在正确的位置。
由于各种原因可能会出现以下错误:
javax.net.ssl.SSLHandshakeException: no cipher suites in common
调试时检查的要点:
- 密钥库和信任库已正确加载并用于创建套接字连接
- 证书与启用的密码套件兼容
- 客户端和服务端应该至少启用一个common密码套件,并且该密码套件也应该与证书兼容
例如在下面的示例中,我使用 Java 8 和默认的密码套件集。我生成的证书使用 ECDSA 和 SHA384,因此当服务器和客户端之间建立 TLS 连接时,我可以通过启用调试 (System.setProperty("javax.net.debug", "ssl");
) 看到协商的密码套件是 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
.
以下是一个工作示例:
第一步,需要创建密钥对和证书。出于测试目的,让我们创建一个自签名证书,并为服务器和客户端使用相同的证书:
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)应该在类路径中。在这个例子中,我将它保存在 Maven 文件夹结构的 test/resources 文件夹中,以便 JUnit 测试可以在类路径中获取它。
密码套件背景
使用TLS/SSL时,要使用的加密算法由密码套件决定。服务器支持一组密码套件(您可以根据需要和所需的安全级别启用或禁用某些套件)。客户端还支持一组密码套件。在连接设置期间,要使用的密码套件在客户端和服务器之间协商。鉴于服务器支持该特定密码套件,客户端首选项将得到尊重。
您会找到 Sun 提供商支持的密码套件列表,最多 Java 8 here。
典型的密码套件名称如下所示:
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
这里,
ECDHE 代表椭圆曲线 Diffie Hellman Ephemeral。这是一个密钥交换算法。 Elliptic 变体(第一个 E)用于性能,而 Ephemeral 变体(最后一个 E)用于前向保密。前向保密意味着如果攻击者通过 TLS 记录所有通信并在稍后某个时间点以某种方式获取私钥,he/she 无法解密过去记录的通信。
ECDSA是一种用于对密钥进行签名的数字签名算法,用于验证(验证其完整性)共享秘密。 ECDSA 比 HMAC 等其他身份验证算法更弱且更慢。然而它被用于共享密钥认证,因为它不需要验证者知道用于创建认证标签的密钥。服务器可以很好地使用它的私钥来验证消息的完整性。
AES_128_GCM - 一旦双方(通常是浏览器和网络服务器)共享了一个公共密钥,就会使用对称分组密码算法加密各方之间的消息交换。在这种特殊情况下,使用具有 128 位密钥和 GCM 身份验证模式的分组密码 AES。
SHA256 - PRF
的哈希算法
您设置的系统属性有误。
服务器需要 javax.net.keyStore
(不是 trustStore
)和 javax.net.keyStorePassword
—— 有时 javax.net.keyStoreType
但可能不是你的情况。对于使用自签名证书的服务器,客户端需要 truststore 来包含该证书,并且根本不需要任何密钥库。虽然没有记录,但如果它们在同一系统上,您可以使用服务器密钥库(包含 privateKeyEntry)作为客户端信任库,JSSE 自动将 privateKey 的叶证书视为 trustedCert。在任何其他情况下,您需要提取服务器的证书(不是密钥),copy/send 到客户端,并使用 keytool -importcert
将证书(不是密钥)放入密钥库 文件 用作信任库;这可以是自定义文件(使用 sysprops 标识)或位于 JRE/lib/security/cacerts 的默认信任库(当前 Windows 除外,如果 JRE 在 \Program Files[ (x86)]
下,这是安装程序默认设置,因为 Windows 现在会惹恼那些试图在那里更改文件的人)。
并且这些 sysprops(使用时)必须在加载 JSSE 类 之前设置——当您调用 SSL[Server]SocketFactory
时设置它们通常已经太晚了。通常它们是在使用 -Dname=value
选项调用 Java 的命令行上设置的;保证尽早完成。如果您不能这样做,请将 setProperty
调用放在 main
方法的开头,或 GUI 的等效方法。
此清单为'no common/shared cipher'因为没有密钥+证书,服务器无法支持进行服务器身份验证的密码套件,默认情况下Java/JSSE仅启用进行服务器身份验证的密码套件,因为其他密码套件在 many/most 情况下不安全。
我肯定我以前回答过第二部分,但找不到一个骗子。第一部分出现在许多答案中,但通常没有在问题中清楚地呈现。
我使用 ServerSocket
和 Socket
对象编写了 client/server Java 程序。然后我修改了代码以使用 SSLServerSocket
和 'SSLSocket` 但是我抛出了不同的异常,包括:
javax.net.ssl.SSLHandshakeException: no cipher suites in common
我希望尽可能多地以编程方式进行。我也可以使用自签名证书。
我遵循的一个教程建议使用 keytool
java 应用程序创建一个证书,然后将该文件移动到您的 java 项目中。我已经使用终端命令 keytool -genkey -alias zastore -keyalg RSA -keystore za.store
完成了该操作。我将密码指定为 password
.
然后我调用函数 System.setProperty
希望 SSLSockets 工作,但它仍然没有。
这是我的服务器代码
public class Server implements Runnable
{
private SSLServerSocket serverSocket;
private int portNumber;
private Thread acceptThread;
private LinkedList<Connection> connections;
private ConnectionListener connectionListener;
public Server(int port, ConnectionListener connectionListener)
{
this.connectionListener = connectionListener;
portNumber = port;
connections = new LinkedList<Connection>();
try
{
System.setProperty("javax.net.ssl.trustStore", "za.store");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SSLServerSocketFactory sslssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
serverSocket = (SSLServerSocket) sslssf.createServerSocket(portNumber,15);
}
catch (IOException e)
{
e.printStackTrace();
}
}
public void startListening()
{
acceptThread = new Thread(this);
acceptThread.start();
}
public void stopListening()
{
for(Connection c:connections)
{
c.stopListeningAndDisconnect();
}
try
{
serverSocket.close();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void run()
{
try
{
while(true)
{
SSLSocket s = (SSLSocket) serverSocket.accept();
Connection c = new Connection(s,connectionListener);
connections.add(c);
System.out.println("New Connection Established From"+s.getInetAddress().toString());
}
}
catch(java.net.SocketException e)
{
System.out.println("Listening thread terminated with exception.");
}
catch(IOException e)
{
e.printStackTrace();
}
}
public void removeConnection(Connection c)
{
connections.remove(c);
}
public void printConnections()
{
System.out.println("Number of connections "+connections.toString());
for(int i=0; i<connections.size(); i++)
{
System.out.println(connections.toString());
}
}
}
然后是按下按钮时连接的客户端代码片段:
@Override
public void actionPerformed(ActionEvent e)
{
if(e.getSource() == connect)
{
try
{
System.setProperty("javax.net.ssl.trustStore", "za.store");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SSLSocketFactory sslsf = (SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket s = (SSLSocket)sslsf.createSocket(ipBox.getText(), Integer.parseInt(portBox.getText()));
Connection c = new Connection(s,parent);
parent.connectionSuccessful(c);
}
catch (NumberFormatException e1)
{
JOptionPane.showMessageDialog(this, "Error! Port number must be a number", "Error", JOptionPane.ERROR_MESSAGE);
}
catch (UnknownHostException e1)
{
JOptionPane.showMessageDialog(this, "Error! Unable to find that host", "Error", JOptionPane.ERROR_MESSAGE);
}
catch (IOException e1)
{
e1.printStackTrace();
}
}
}
一篇 Whosebug 文章建议我的服务器 "doesn't have a certificate." 我不知道那是什么意思,也不知道如何获取服务器并将其放置在正确的位置。
由于各种原因可能会出现以下错误:
javax.net.ssl.SSLHandshakeException: no cipher suites in common
调试时检查的要点:
- 密钥库和信任库已正确加载并用于创建套接字连接
- 证书与启用的密码套件兼容
- 客户端和服务端应该至少启用一个common密码套件,并且该密码套件也应该与证书兼容
例如在下面的示例中,我使用 Java 8 和默认的密码套件集。我生成的证书使用 ECDSA 和 SHA384,因此当服务器和客户端之间建立 TLS 连接时,我可以通过启用调试 (System.setProperty("javax.net.debug", "ssl");
) 看到协商的密码套件是 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
.
以下是一个工作示例:
第一步,需要创建密钥对和证书。出于测试目的,让我们创建一个自签名证书,并为服务器和客户端使用相同的证书:
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)应该在类路径中。在这个例子中,我将它保存在 Maven 文件夹结构的 test/resources 文件夹中,以便 JUnit 测试可以在类路径中获取它。
密码套件背景
使用TLS/SSL时,要使用的加密算法由密码套件决定。服务器支持一组密码套件(您可以根据需要和所需的安全级别启用或禁用某些套件)。客户端还支持一组密码套件。在连接设置期间,要使用的密码套件在客户端和服务器之间协商。鉴于服务器支持该特定密码套件,客户端首选项将得到尊重。
您会找到 Sun 提供商支持的密码套件列表,最多 Java 8 here。
典型的密码套件名称如下所示:
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
这里,
ECDHE 代表椭圆曲线 Diffie Hellman Ephemeral。这是一个密钥交换算法。 Elliptic 变体(第一个 E)用于性能,而 Ephemeral 变体(最后一个 E)用于前向保密。前向保密意味着如果攻击者通过 TLS 记录所有通信并在稍后某个时间点以某种方式获取私钥,he/she 无法解密过去记录的通信。
ECDSA是一种用于对密钥进行签名的数字签名算法,用于验证(验证其完整性)共享秘密。 ECDSA 比 HMAC 等其他身份验证算法更弱且更慢。然而它被用于共享密钥认证,因为它不需要验证者知道用于创建认证标签的密钥。服务器可以很好地使用它的私钥来验证消息的完整性。
AES_128_GCM - 一旦双方(通常是浏览器和网络服务器)共享了一个公共密钥,就会使用对称分组密码算法加密各方之间的消息交换。在这种特殊情况下,使用具有 128 位密钥和 GCM 身份验证模式的分组密码 AES。
SHA256 - PRF
的哈希算法您设置的系统属性有误。
服务器需要 javax.net.keyStore
(不是 trustStore
)和 javax.net.keyStorePassword
—— 有时 javax.net.keyStoreType
但可能不是你的情况。对于使用自签名证书的服务器,客户端需要 truststore 来包含该证书,并且根本不需要任何密钥库。虽然没有记录,但如果它们在同一系统上,您可以使用服务器密钥库(包含 privateKeyEntry)作为客户端信任库,JSSE 自动将 privateKey 的叶证书视为 trustedCert。在任何其他情况下,您需要提取服务器的证书(不是密钥),copy/send 到客户端,并使用 keytool -importcert
将证书(不是密钥)放入密钥库 文件 用作信任库;这可以是自定义文件(使用 sysprops 标识)或位于 JRE/lib/security/cacerts 的默认信任库(当前 Windows 除外,如果 JRE 在 \Program Files[ (x86)]
下,这是安装程序默认设置,因为 Windows 现在会惹恼那些试图在那里更改文件的人)。
并且这些 sysprops(使用时)必须在加载 JSSE 类 之前设置——当您调用 SSL[Server]SocketFactory
时设置它们通常已经太晚了。通常它们是在使用 -Dname=value
选项调用 Java 的命令行上设置的;保证尽早完成。如果您不能这样做,请将 setProperty
调用放在 main
方法的开头,或 GUI 的等效方法。
此清单为'no common/shared cipher'因为没有密钥+证书,服务器无法支持进行服务器身份验证的密码套件,默认情况下Java/JSSE仅启用进行服务器身份验证的密码套件,因为其他密码套件在 many/most 情况下不安全。
我肯定我以前回答过第二部分,但找不到一个骗子。第一部分出现在许多答案中,但通常没有在问题中清楚地呈现。