【问题标题】:SSL socket connection with client authentication带有客户端身份验证的 SSL 套接字连接
【发布时间】:2021-04-22 20:07:18
【问题描述】:

我有一个运行一些实用程序命令的应用程序服务器,这些命令是用 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);
    }
}

现在创建信任库,如链接所示: 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>
                 <---------------------------------

【问题讨论】:

  • 它说第一个错误是unable to find valid certification path to requested target。也许这个链接可以帮助? stackoverflow.com/questions/9210514/…
  • 我建议使用密钥库的完整路径。
  • 你能提供你如何配置你的客户端,所以实际的代码sn -p
  • @Hakan54 用代码更新了帖子。
  • @Abkarino 我已经检查过该链接;尝试提供完整路径。

标签: java ssl openssl keytool


【解决方案1】:

我认为您的设置存在一些问题。

要正确配置与 JSSE 的 SSL 连接,您需要做几件事,具体取决于您需要对服务器、客户端进行身份验证,还是执行相互身份验证。

让我们假设以后更完整的相互身份验证用例。

目标是配置一个SSLSocketFactory,您可以使用它来联系您的服务器。

要配置SSLSocketFactory,您需要SSLContext

这个元素又需要至少两个元素用于相互身份验证用例,一个KeyManagerFactory,用于客户端 SSL 身份验证,即服务器信任客户端,和TrustManagerFactory,用于配置客户端信任服务器。

KeyManagerFactoryTrustManagerFactory 都需要正确配置的密钥库和必要的加密材料。

因此,第一步将包括生成此加密材料。

您已经使用服务器证书创建了一个密钥库:

keytool -keystore serverpublic.keystore -alias clientstore -import -file server.crt.der -storepass yourserverpublickeystorepassword

请注意,以与服务器案例类似的方式,您还需要为您的客户端创建一对公钥和私钥,当然,与服务器不同。

您使用 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,除了解释前面提到的许多要点之外,还将为您在必要时为您的客户端和服务器生成密钥库提供很好的指导。

另外请注意,根据您的服务器代码,您可能需要将其配置为信任提供的客户端证书。

根据您的 cmets,您使用的服务器端代码类似于 Postgresql 8.1 提供的代码。请参阅relevant documentation 在该数据库中配置 SSL,如果您使用一些类似的服务器端代码,它可能会有所帮助。

可能最好的方法是生成从您的服务器信任的根证书派生的客户端证书,而不是使用自签名证书。

我认为它也将与您的服务器端 SSL 证书相关联的私钥:首先,创建一个根自签名证书,您的 CA 证书,配置您的服务器端 C 代码以信任它,然后派生两个客户端和来自该 CA 的服务器端 SSL 加密材料:它可能会简化您的设置并使一切正常工作。

【讨论】:

  • 用例是我有一个 Web 应用程序,它与一个服务器进程(用 C 编码)对话,其中身份验证是基于用户名/密码的,现在服务器端引入了基于密钥/证书的身份验证,所以我也必须这样做。如果我没记错的话,我想我必须进行客户端身份验证,对。你写的代码就是我必须做的改变。
  • 抱歉@devd 回复晚了。是的,我知道要求是能够对您的客户进行身份验证。请按照我提供的示例进行操作。我将更新答案以使其清楚。第一步,您需要为您的客户端加密材料创建一个密钥库,并使用公钥和私钥。根据您的问题,您现在似乎没有。请问,你能创建它吗?
  • 我已经完成了。现在,我在服务器端 SSL open_server: could not accept SSL connection: certificate verify failed 收到此错误。对于客户端代码和日志,请访问和客户端代码:gist.github.com/dipdeb/61f71e79b1e884e65863962a86266888gist.github.com/dipdeb/f58efa8d810fc3cfd709fcb42758abf4
  • 嗨@devd。似乎由于某种原因,服务器不接受提供的客户端证书。您是否正确配置了客户端密钥库?您是否可能还需要在服务器端代码中配置一些信任关系,以便它信任客户端证书?另外,您能否尝试使用 SSLSocket 将一些信息写入服务器,而不是手动启动握手?
  • 你说的可能是正确的,因为我也怀疑服务器端。我们的服务器端代码遵循 Postgresql 8.1 的 ssl 连接过程,我尝试过 jdbc 以 ssl 模式连接到 Postgres8.1,类似的方式,效果很好。
猜你喜欢
  • 2014-08-24
  • 2012-12-15
  • 1970-01-01
  • 2012-06-17
  • 2018-10-06
  • 2021-12-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多