带客户端身份验证的 SSL 套接字连接
SSL socket connection with client authentication
我有一个应用程序服务器 运行 一些实用命令,是用 C 编写的。
我必须使用 Java SSL 套接字通过 Java 客户端程序连接到服务器
客户端身份验证。
服务器端的密钥是使用以下方法创建的:
openssl req -new -text -out ser.req
openssl rsa -in privkey.pem -out ser.key
openssl req -x509 -in ser.req -text -key ser.key -out ser.crt
我已获得服务器密钥和证书。我已经结合了密钥和证书
转换为 PKCS12 格式文件:
openssl pkcs12 -inkey ser.key -in ser.crt -export -out ser.pkcs12
然后使用 keytool 将生成的 PKCS12 文件加载到 JSSE 密钥库中:
keytool -importkeystore -srckeystore ser.pkcs12 -srcstoretype PKCS12 -destkeystore ser.keystore
但是当我尝试连接时,出现以下错误:
javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alert.createSSLException(Alert.java:131)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:324)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:267)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:262)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:654)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:473)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:369)
at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:377)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
at sun.security.ssl.TransportContext.dispatch(TransportContext.java:182)
at sun.security.ssl.SSLTransport.decode(SSLTransport.java:149)
at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1143)
at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1054)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:394)
at SSLSocketClient.main(SSLSocketClient.java:67)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:456)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:323)
at sun.security.validator.Validator.validate(Validator.java:271)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:315)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:223)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:129)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:638)
... 11 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:451)
... 17 more
服务器端日志:
SSL open_server: could not accept SSL connection: sslv3 alert certificate unknown
运行 命令:
java -Djavax.net.ssl.keyStore=/path/to/ser.keystore -Djavax.net.ssl.keyStorePassword=passwd SSLSocketClient <server-ip> <port>
有谁知道这个问题的原因吗?
更新了客户端源代码:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
import java.security.cert.CertificateFactory;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
import java.security.SecureRandom;
import javax.net.SocketFactory;
public class SSLSocketClient {
public static void main(String [] args) throws Exception {
String serverName = args[0];
int port = Integer.parseInt(args[1]);
try {
SSLSocketFactory sf =
(SSLSocketFactory)SSLSocketFactory.getDefault();
Socket client = new Socket(serverName, port);
System.out.println("Connected to " + client.getRemoteSocketAddress());
OutputStream outToServer = client.getOutputStream();
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(outToServer));
writeData(out);
out.flush();
InputStream inFromServer = client.getInputStream();
DataInputStream in = new DataInputStream(inFromServer);
readData(in);
outToServer = client.getOutputStream();
out = new DataOutputStream(new BufferedOutputStream(outToServer));
writeData2(out);
out.flush();
Socket newClient = sf.createSocket(client, serverName, port, false);
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void writeData(DataOutputStream out) throws IOException {
char CMD_CHAR_U = 'U';
byte b = (byte) (0x00ff & CMD_CHAR_U);
out.writeByte(b); // <U>
}
private static void writeData2(DataOutputStream out) throws IOException {
char CMD_CHAR_S = 'S';
byte b = (byte) (0x00ff & CMD_CHAR_S);
out.writeByte(b); // <S>
}
private static void readData(DataInputStream in) throws IOException {
char sChar = (char) in.readByte();
System.out.println("<S>\t\t" + sChar);
}
}
现在创建信任库,如 link 所示:
https://jdbc.postgresql.org/documentation/head/ssl-client.html
创建步骤:
openssl x509 -in server.crt -out server.crt.der -outform der
keytool -keystore mystore -alias clientstore -import -file server.crt.der
java -Djavax.net.ssl.trustStore=mystore -Djavax.net.ssl.trustStorePassword=mypassword com.mycompany.MyApp
注意 - 服务器端使用的是 TLSv1 协议
但是还是没能过关。我究竟做错了什么?
我要的是服务端对客户端的crt进行认证。
与服务器的登录协议;我们使用的 SSL 仅用于身份验证
不保护传输:
-------------------------------------------------------------
client server
-------------------------------------------------------------
sock = connect() sock = accept()
<U><LOGIN_SSL=501>
--------------------------------->
'S'|'E'
<---------------------------------
'S'
--------------------------------->
SSL_connect(sock) SSL_accept(sock)
<R><LOGIN_SSL>
<---------------------------------
我认为您的设置有几个问题。
要正确配置与 JSSE 的 SSL 连接,您需要做几件事,具体取决于您是需要验证服务器、客户端还是执行相互验证。
让我们假设以后更完整的相互身份验证用例。
objective 是配置一个SSLSocketFactory
,你可以用它来联系你的服务器。
要配置 SSLSocketFactory
,您需要 SSLContext
。
此元素又需要至少两个元素用于相互身份验证用例,KeyManagerFactory
,客户端 SSL 身份验证所需,即服务器信任客户端,TrustManagerFactory
, 需要将客户端配置为信任服务器。
KeyManagerFactory
和 TrustManagerFactory
都需要正确配置的密钥库和必要的加密 material。
因此,第一步将包括生成此密码 material。
您已经使用服务器证书创建了密钥库:
keytool -keystore serverpublic.keystore -alias clientstore -import -file server.crt.der -storepass yourserverpublickeystorepassword
请注意,与服务器情况类似,您还需要为您的客户端创建一个 public 和私钥对,当然,这与服务器端不同。
您随 OpenSSL 提供的相关代码和 keytool
看起来很合适。请为客户端重复此过程:
openssl req -new -text -out client.csr
openssl rsa -in clientpriv.pem -out client.key
openssl req -x509 -in client.csr -text -key client.key -out client.crt
// You can use PKCS12 also with Java but it is also ok on this way
openssl pkcs12 -inkey client.key -in client.crt -export -out client.pkcs12
// Do not bother yourself and, in this use case, use always the same password for the key and keystore
keytool -importkeystore -srckeystore client.pkcs12 -srcstoretype PKCS12 -destkeystore client.keystore -storepass "yourclientkeystorepassword"
在正确的密钥库就位后,尝试如下操作与您的服务器交互:
// First, let's configure the SSL for client authentication
KeyStore clientKeyStore = KeyStore.getInstance("JKS");
clientKeyStore.load(
new FileInputStream("/path/to/client.keystore"),
"yourclientkeystorepassword".toCharArray()
);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); // SunX509
kmf.init(clientKeyStore, "yourclientkeystorepassword".toCharArray());
KeyManager[] keyManagers = kmf.getKeyManagers();
// Now, let's configure the client to trust the server
KeyStore serverKeyStore = KeyStore.getInstance("JKS");
serverKeyStore.load(
new FileInputStream("/path/to/serverpublic.keystore"),
"yourserverpublickeystorepassword".toCharArray()
);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // SunX509
tmf.init(serverKeyStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null); // You can provide SecureRandom also if you wish
// Create the SSL socket factory and establish the connection
SSLSocketFactory sf = sslContext.getSocketFactory();
SSLSocket socket = (SSLSocket)sf.createSocket(serverName, port);
// Interact with your server. Place your code here
// Please, consider the following link for alternatives approaches on how to
// interchange information with the server:
// https://web.mit.edu/java_v1.5.0_22/distrib/share/docs/guide/security/jsse/samples/sockets/client/SSLSocketClient.java
// It also suggest the use of startHandshake explicitly if your are using PrintWriter for the reason explained in the example an in the docs:
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/net/ssl/SSLSocket.html
//...
// Close the socket
socket.close();
所描述的方法可以扩展为使用更高级别的抽象组件,如 HttpsURLConnection
和 HTTP 客户端,而不是套接字 - 除了以不同方式处理 SSL 的 Apache HttpClient - 如 OkHttp
其中,在引擎盖下,使用 SSLSocketFactory
和相关的东西。
请同时考虑查看来自 IBM DeveloperWorks 的 great article,除了解释前面提到的许多要点之外,如果需要,将为您的客户端和服务器生成密钥库提供很好的指导。
另请注意,根据您的服务器代码,您可能需要将其配置为信任提供的客户端证书。
根据您的评论,您正在使用类似于 Postgresql 8.1 提供的服务器端代码。请参阅 relevant documentation 在该数据库中配置 SSL,如果您使用一些类似的服务器端代码,它可能会有所帮助。
可能最好的方法是生成从您的服务器信任的根证书派生的客户端证书,而不是使用自签名证书。
我认为它也与您的服务器端 SSL 证书和关联的私钥有关:首先,创建一个根自签名证书,您的 CA 证书,配置您的服务器端 C 代码以信任它,然后派生来自该 CA 的客户端和服务器端 SSL 加密material:它可能会简化您的设置并使一切正常工作。
我有一个应用程序服务器 运行 一些实用命令,是用 C 编写的。 我必须使用 Java SSL 套接字通过 Java 客户端程序连接到服务器 客户端身份验证。 服务器端的密钥是使用以下方法创建的:
openssl req -new -text -out ser.req
openssl rsa -in privkey.pem -out ser.key
openssl req -x509 -in ser.req -text -key ser.key -out ser.crt
我已获得服务器密钥和证书。我已经结合了密钥和证书 转换为 PKCS12 格式文件:
openssl pkcs12 -inkey ser.key -in ser.crt -export -out ser.pkcs12
然后使用 keytool 将生成的 PKCS12 文件加载到 JSSE 密钥库中:
keytool -importkeystore -srckeystore ser.pkcs12 -srcstoretype PKCS12 -destkeystore ser.keystore
但是当我尝试连接时,出现以下错误:
javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alert.createSSLException(Alert.java:131)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:324)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:267)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:262)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:654)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:473)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:369)
at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:377)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
at sun.security.ssl.TransportContext.dispatch(TransportContext.java:182)
at sun.security.ssl.SSLTransport.decode(SSLTransport.java:149)
at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1143)
at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1054)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:394)
at SSLSocketClient.main(SSLSocketClient.java:67)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:456)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:323)
at sun.security.validator.Validator.validate(Validator.java:271)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:315)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:223)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:129)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:638)
... 11 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:451)
... 17 more
服务器端日志:
SSL open_server: could not accept SSL connection: sslv3 alert certificate unknown
运行 命令:
java -Djavax.net.ssl.keyStore=/path/to/ser.keystore -Djavax.net.ssl.keyStorePassword=passwd SSLSocketClient <server-ip> <port>
有谁知道这个问题的原因吗?
更新了客户端源代码:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
import java.security.cert.CertificateFactory;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
import java.security.SecureRandom;
import javax.net.SocketFactory;
public class SSLSocketClient {
public static void main(String [] args) throws Exception {
String serverName = args[0];
int port = Integer.parseInt(args[1]);
try {
SSLSocketFactory sf =
(SSLSocketFactory)SSLSocketFactory.getDefault();
Socket client = new Socket(serverName, port);
System.out.println("Connected to " + client.getRemoteSocketAddress());
OutputStream outToServer = client.getOutputStream();
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(outToServer));
writeData(out);
out.flush();
InputStream inFromServer = client.getInputStream();
DataInputStream in = new DataInputStream(inFromServer);
readData(in);
outToServer = client.getOutputStream();
out = new DataOutputStream(new BufferedOutputStream(outToServer));
writeData2(out);
out.flush();
Socket newClient = sf.createSocket(client, serverName, port, false);
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void writeData(DataOutputStream out) throws IOException {
char CMD_CHAR_U = 'U';
byte b = (byte) (0x00ff & CMD_CHAR_U);
out.writeByte(b); // <U>
}
private static void writeData2(DataOutputStream out) throws IOException {
char CMD_CHAR_S = 'S';
byte b = (byte) (0x00ff & CMD_CHAR_S);
out.writeByte(b); // <S>
}
private static void readData(DataInputStream in) throws IOException {
char sChar = (char) in.readByte();
System.out.println("<S>\t\t" + sChar);
}
}
现在创建信任库,如 link 所示: https://jdbc.postgresql.org/documentation/head/ssl-client.html
创建步骤:
openssl x509 -in server.crt -out server.crt.der -outform der
keytool -keystore mystore -alias clientstore -import -file server.crt.der
java -Djavax.net.ssl.trustStore=mystore -Djavax.net.ssl.trustStorePassword=mypassword com.mycompany.MyApp
注意 - 服务器端使用的是 TLSv1 协议
但是还是没能过关。我究竟做错了什么? 我要的是服务端对客户端的crt进行认证。
与服务器的登录协议;我们使用的 SSL 仅用于身份验证 不保护传输:
-------------------------------------------------------------
client server
-------------------------------------------------------------
sock = connect() sock = accept()
<U><LOGIN_SSL=501>
--------------------------------->
'S'|'E'
<---------------------------------
'S'
--------------------------------->
SSL_connect(sock) SSL_accept(sock)
<R><LOGIN_SSL>
<---------------------------------
我认为您的设置有几个问题。
要正确配置与 JSSE 的 SSL 连接,您需要做几件事,具体取决于您是需要验证服务器、客户端还是执行相互验证。
让我们假设以后更完整的相互身份验证用例。
objective 是配置一个SSLSocketFactory
,你可以用它来联系你的服务器。
要配置 SSLSocketFactory
,您需要 SSLContext
。
此元素又需要至少两个元素用于相互身份验证用例,KeyManagerFactory
,客户端 SSL 身份验证所需,即服务器信任客户端,TrustManagerFactory
, 需要将客户端配置为信任服务器。
KeyManagerFactory
和 TrustManagerFactory
都需要正确配置的密钥库和必要的加密 material。
因此,第一步将包括生成此密码 material。
您已经使用服务器证书创建了密钥库:
keytool -keystore serverpublic.keystore -alias clientstore -import -file server.crt.der -storepass yourserverpublickeystorepassword
请注意,与服务器情况类似,您还需要为您的客户端创建一个 public 和私钥对,当然,这与服务器端不同。
您随 OpenSSL 提供的相关代码和 keytool
看起来很合适。请为客户端重复此过程:
openssl req -new -text -out client.csr
openssl rsa -in clientpriv.pem -out client.key
openssl req -x509 -in client.csr -text -key client.key -out client.crt
// You can use PKCS12 also with Java but it is also ok on this way
openssl pkcs12 -inkey client.key -in client.crt -export -out client.pkcs12
// Do not bother yourself and, in this use case, use always the same password for the key and keystore
keytool -importkeystore -srckeystore client.pkcs12 -srcstoretype PKCS12 -destkeystore client.keystore -storepass "yourclientkeystorepassword"
在正确的密钥库就位后,尝试如下操作与您的服务器交互:
// First, let's configure the SSL for client authentication
KeyStore clientKeyStore = KeyStore.getInstance("JKS");
clientKeyStore.load(
new FileInputStream("/path/to/client.keystore"),
"yourclientkeystorepassword".toCharArray()
);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); // SunX509
kmf.init(clientKeyStore, "yourclientkeystorepassword".toCharArray());
KeyManager[] keyManagers = kmf.getKeyManagers();
// Now, let's configure the client to trust the server
KeyStore serverKeyStore = KeyStore.getInstance("JKS");
serverKeyStore.load(
new FileInputStream("/path/to/serverpublic.keystore"),
"yourserverpublickeystorepassword".toCharArray()
);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // SunX509
tmf.init(serverKeyStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null); // You can provide SecureRandom also if you wish
// Create the SSL socket factory and establish the connection
SSLSocketFactory sf = sslContext.getSocketFactory();
SSLSocket socket = (SSLSocket)sf.createSocket(serverName, port);
// Interact with your server. Place your code here
// Please, consider the following link for alternatives approaches on how to
// interchange information with the server:
// https://web.mit.edu/java_v1.5.0_22/distrib/share/docs/guide/security/jsse/samples/sockets/client/SSLSocketClient.java
// It also suggest the use of startHandshake explicitly if your are using PrintWriter for the reason explained in the example an in the docs:
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/net/ssl/SSLSocket.html
//...
// Close the socket
socket.close();
所描述的方法可以扩展为使用更高级别的抽象组件,如 HttpsURLConnection
和 HTTP 客户端,而不是套接字 - 除了以不同方式处理 SSL 的 Apache HttpClient - 如 OkHttp
其中,在引擎盖下,使用 SSLSocketFactory
和相关的东西。
请同时考虑查看来自 IBM DeveloperWorks 的 great article,除了解释前面提到的许多要点之外,如果需要,将为您的客户端和服务器生成密钥库提供很好的指导。
另请注意,根据您的服务器代码,您可能需要将其配置为信任提供的客户端证书。
根据您的评论,您正在使用类似于 Postgresql 8.1 提供的服务器端代码。请参阅 relevant documentation 在该数据库中配置 SSL,如果您使用一些类似的服务器端代码,它可能会有所帮助。
可能最好的方法是生成从您的服务器信任的根证书派生的客户端证书,而不是使用自签名证书。
我认为它也与您的服务器端 SSL 证书和关联的私钥有关:首先,创建一个根自签名证书,您的 CA 证书,配置您的服务器端 C 代码以信任它,然后派生来自该 CA 的客户端和服务器端 SSL 加密material:它可能会简化您的设置并使一切正常工作。