SSL Handshake Failures – SSL握手失败

最后修改: 2018年 11月 6日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Secured Socket Layer (SSL) is a cryptographic protocol which provides security in communication over the network. In this tutorial, we’ll discuss various scenarios that can result in an SSL handshake failure and how to it.

安全套接字层(SSL)是一种加密协议,为网络通信提供安全保障。在本教程中,我们将讨论可能导致SSL握手失败的各种情况以及如何解决。

Note that our Introduction to SSL using JSSE covers the basics of SSL in more detail.

请注意,我们的使用JSSE的SSL入门更详细地介绍了SSL的基础知识。

2. Terminology

2.术语

It’s important to note that, due to security vulnerabilities, SSL as a standard is superseded by Transport Layer Security (TLS). Most programming languages, including Java, have libraries to support both SSL and TLS.

值得注意的是,由于安全漏洞,SSL作为一种标准被传输层安全(TLS)所取代。大多数编程语言,包括Java,都有支持SSL和TLS的库。

Since the inception of SSL, many products and languages like OpenSSL and Java had references to SSL which they kept even after TLS took over. For this reason, in the remainder of this tutorial, we will use the term SSL to refer generally to cryptographic protocols.

自从SSL诞生以来,许多产品和语言,如OpenSSL和Java,都有对SSL的引用,甚至在TLS接管后也保留了这些引用。出于这个原因,在本教程的其余部分,我们将使用SSL这个术语来泛指加密协议。

3. Setup

3.设置

For the purpose of this tutorial, we’ll create a simple server and client applications using the Java Socket API to simulate a network connection.

在本教程中,我们将使用Java Socket API创建一个简单的服务器和客户端应用程序来模拟网络连接。

3.1. Creating a Client and a Server

3.1.创建一个客户和一个服务器

In Java, we can use sockets to establish a communication channel between a server and client over the network. Sockets are a part of the Java Secure Socket Extension (JSSE) in Java.

在Java中,我们可以使用s套接字来在服务器和客户端之间通过网络建立一个通信通道。套接字是Java中Java安全套接字扩展(JSSE)的一部分。

Let’s begin by defining a simple server:

让我们从定义一个简单的服务器开始。

int port = 8443;
ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
try (ServerSocket listener = factory.createServerSocket(port)) {
    SSLServerSocket sslListener = (SSLServerSocket) listener;
    sslListener.setNeedClientAuth(true);
    sslListener.setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    sslListener.setEnabledProtocols(
      new String[] { "TLSv1.2" });
    while (true) {
        try (Socket socket = sslListener.accept()) {
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            out.println("Hello World!");
        }
    }
}

The server defined above returns the message “Hello World!” to a connected client.

上面定义的服务器将消息 “Hello World!”返回给连接的客户端。

Next, let’s define a basic client, which we’ll connect to our SimpleServer:

接下来,让我们定义一个基本的客户端,我们将把它连接到我们的SimpleServer:

String host = "localhost";
int port = 8443;
SocketFactory factory = SSLSocketFactory.getDefault();
try (Socket connection = factory.createSocket(host, port)) {
    ((SSLSocket) connection).setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    ((SSLSocket) connection).setEnabledProtocols(
      new String[] { "TLSv1.2" });
    
    SSLParameters sslParams = new SSLParameters();
    sslParams.setEndpointIdentificationAlgorithm("HTTPS");
    ((SSLSocket) connection).setSSLParameters(sslParams);
    
    BufferedReader input = new BufferedReader(
      new InputStreamReader(connection.getInputStream()));
    return input.readLine();
}

Our client prints the message returned by the server.

我们的客户端打印服务器返回的信息。

3.2. Creating Certificates in Java

3.2.在Java中创建证书

SSL provides secrecy, integrity, and authenticity in network communications. Certificates play an important role as far as establishing authenticity.

SSL在网络通信中提供保密性、完整性和真实性。就建立真实性而言,证书发挥着重要作用。

Typically, these certificates are purchased and signed by a Certificate Authority, but for this tutorial, we’ll use self-signed certificates.

通常情况下,这些证书是由证书颁发机构购买和签署的,但在本教程中,我们将使用自签证书。

To achieve this, we can use keytool, which ships with the JDK:

为了实现这一目标,我们可以使用keytool,它随JDK一起提供:

$ keytool -genkey -keypass password \
                  -storepass password \
                  -keystore serverkeystore.jks

The above command starts an interactive shell to gather information for the certificate like Common Name (CN) and Distinguished Name (DN). When we provide all relevant details, it generates the file serverkeystore.jks, which contains the private key of the server and its public certificate.

上述命令启动了一个交互式外壳,以收集证书的信息,如公共名称(CN)和区分名称(DN)。当我们提供所有相关细节时,它会生成文件serverkeystore.jks,其中包含服务器的私钥和公共证书。

Note that serverkeystore.jks is stored in the Java Key Store (JKS) format, which is proprietary to Java. These days, keytool will remind us that we ought to consider using PKCS#12, which it also supports.

请注意,serverkeystore.jks是以Java密钥存储(JKS)格式存储的,这是Java专有的。这些天,keytool会提醒我们,我们应该考虑使用PKCS#12,它也支持这个。

We can further use keytool to extract the public certificate from the generated keystore file:

我们可以进一步使用keytool从生成的keystore文件中提取公共证书。

$ keytool -export -storepass password \
                  -file server.cer \
                  -keystore serverkeystore.jks

The above command exports the public certificate from keystore as a file server.cer. Let’s use the exported certificate for the client by adding it to its truststore:

上述命令将公共证书从钥匙库导出为一个文件server.cer。让我们把导出的证书添加到客户端的信任库中,为客户端使用该证书。

$ keytool -import -v -trustcacerts \
                     -file server.cer \
                     -keypass password \
                     -storepass password \
                     -keystore clienttruststore.jks

We have now generated a keystore for the server and corresponding truststore for the client. We will go over the use of these generated files when we discuss possible handshake failures.

现在我们已经为服务器生成了一个密钥库,并为客户端生成了相应的信任库。当我们讨论可能的握手失败时,我们将讨论这些生成文件的使用。

And more details around the usage of Java’s keystore can be found in our previous tutorial.

围绕Java密钥库使用的更多细节可以在我们的上一篇教程中找到。

4. SSL Handshake

4.SSL握手会

SSL handshakes are a mechanism by which a client and server establish the trust and logistics required to secure their connection over the network.

SSL握手是一种机制,客户和服务器通过这种机制建立信任和物流,以确保他们在网络上的连接

This is a very orchestrated procedure and understanding the details of this can help understand why it often fails, which we intend to cover in the next section.

这是一个非常精心策划的程序,了解其中的细节可以帮助理解为什么它经常失败,我们打算在下一节介绍。

Typical steps in an SSL handshake are:

SSL握手的典型步骤是。

  1. Client provides a list of possible SSL version and cipher suites to use
  2. Server agrees on a particular SSL version and cipher suite, responding back with its certificate
  3. Client extracts the public key from the certificate responds back with an encrypted “pre-master key”
  4. Server decrypts the “pre-master key” using its private key
  5. Client and server compute a “shared secret” using the exchanged “pre-master key”
  6. Client and server exchange messages confirming the successful encryption and decryption using the “shared secret”

While most of the steps are the same for any SSL handshake, there is a subtle difference between one-way and two-way SSL. Let’s quickly review these differences.

虽然任何SSL握手的大部分步骤都是一样的,但单向和双向SSL之间存在着微妙的区别。让我们快速回顾一下这些区别。

4.1. The Handshake in One-way SSL

4.1.单向SSL中的握手

If we refer to the steps mentioned above, step two mentions the certificate exchange. One-way SSL requires that a client can trust the server through its public certificate. This leaves the server to trust all clients that request a connection. There is no way for a server to request and validate the public certificate from clients which can pose a security risk.

如果我们参考上面提到的步骤,第二步提到了证书交换。单向SSL要求客户端可以通过其公共证书信任服务器。这让服务器信任所有请求连接的客户。服务器没有办法从客户那里请求和验证公共证书,这可能造成安全风险。

4.2. The Handshake in Two-way SSL

4.2.双向SSL中的握手

With one-way SSL, the server must trust all clients. But, two-way SSL adds the ability for the server to be able to establish trusted clients as well. During a two-way handshake, both the client and server must present and accept each other’s public certificates before a successful connection can be established.

通过单向SSL,服务器必须信任所有客户。但是,双向SSL增加了服务器也能够建立受信任的客户端的能力。在双向握手期间,客户端和服务器都必须出示并接受对方的公共证书,才能建立成功的连接。

5. Handshake Failure Scenarios

5.握手失败的情况

Having done that quick review, we can look at failure scenarios with greater clarity.

在做了这一快速回顾之后,我们可以更清晰地看待失败的情况。

An SSL handshake, in one-way or two-way communication, can fail for multiple reasons. We will go through each of these reasons, simulate the failure and understand how can we avoid such scenarios.

在单向或双向通信中,SSL握手可能因多种原因而失败。我们将逐一分析这些原因,模拟失败,并了解如何避免这种情况的发生。

In each of these scenarios, we will use the SimpleClient and SimpleServer we created earlier.

在每个场景中,我们将使用我们先前创建的SimpleClientSimpleServer

5.1. Missing Server Certificate

5.1.缺少服务器证书

Let’s try to run the SimpleServer and connect it through the SimpleClient. While we expect to see the message “Hello World!”, we are presented with an exception:

让我们试着运行SimpleServer,并通过SimpleClient连接它。虽然我们期望看到 “Hello World!”的消息,但我们却看到了一个异常。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  Received fatal alert: handshake_failure

Now, this indicates something went wrong. The SSLHandshakeException above, in an abstract manner, is stating that the client when connecting to the server did not receive any certificate.

现在,这表明出了问题。上面的SSLHandshakeException,以一种抽象的方式,说明客户端在连接到服务器时没有收到任何证书。

To address this issue, we will use the keystore we generated earlier by passing them as system properties to the server:

为了解决这个问题,我们将通过把它们作为系统属性传递给服务器来使用我们先前生成的密钥库。

-Djavax.net.ssl.keyStore=serverkeystore.jks -Djavax.net.ssl.keyStorePassword=password

It’s important to note that the system property for the keystore file path should either be an absolute path or the keystore file should be placed in the same directory from where the Java command is invoked to start the server. Java system property for keystore does not support relative paths.

需要注意的是,系统属性中的keystore文件路径应该是一个绝对路径,或者keystore文件应该放在调用Java命令启动服务器的同一目录中。Java系统中的keystore属性不支持相对路径

Does this help us get the output we are expecting? Let’s find out in the next sub-section.

这是否有助于我们获得我们所期望的输出?让我们在下一小节中找出答案。

5.2. Untrusted Server Certificate

5.2.不受信任的服务器证书

As we run the SimpleServer and the SimpleClient again with the changes in the previous sub-section, what do we get as output:

当我们运行SimpleServerSimpleClient的时候,再加上上一小节的改动,我们得到的输出是什么。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  sun.security.validator.ValidatorException: 
  PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
  unable to find valid certification path to requested target

Well, it did not work exactly as we expected, but looks like it has failed for a different reason.

好吧,它没有完全像我们预期的那样工作,但看起来它是由于不同的原因而失败。

This particular failure is caused by the fact that our server is using a self-signed certificate which is not signed by a Certificate Authority (CA).

这个特殊的故障是由于我们的服务器使用的是一个自签名证书,没有经过证书颁发机构(CA)签名。

Really, any time the certificate is signed by something other than what is in the default truststore, we’ll see this error. The default truststore in JDK typically ships with information about common CAs in use.

实际上,任何时候,只要证书是由默认信任库以外的东西签署的,我们就会看到这个错误。JDK 中的默认信任库通常带有关于正在使用的常见 CA 的信息。

To solve this issue here, we will have to force SimpleClient to trust the certificate presented by SimpleServer. Let’s use the truststore we generated earlier by passing them as system properties to the client:

为了解决这个问题,我们必须强迫SimpleClient信任SimpleServer提交的证书。让我们使用我们先前生成的信任库,将它们作为系统属性传递给客户端。

-Djavax.net.ssl.trustStore=clienttruststore.jks -Djavax.net.ssl.trustStorePassword=password

Please note that this is not an ideal solution. In an ideal scenario, we should not use a self-signed certificate but a certificate which has been certified by a Certificate Authority (CA) which clients can trust by default.

请注意,这不是一个理想的解决方案。在一个理想的情况下,我们不应该使用自签名的证书,而应该使用由客户可以默认信任的证书颁发机构(CA)认证的证书。

Let’s go to the next sub-section to find out if we get our expected output now.

让我们进入下一个小节,看看我们现在是否得到了预期的输出。

5.3. Missing Client Certificate

5.3.缺少客户证书

Let’s try one more time running the SimpleServer and the SimpleClient, having applied the changes from previous sub-sections:

让我们再试一次运行SimpleServer和SimpleClient,在应用了前面几个小节的变化后。

Exception in thread "main" java.net.SocketException: 
  Software caused connection abort: recv failed

Again, not something we expected. The SocketException here tells us that the server could not trust the client. This is because we have set up a two-way SSL. In our SimpleServer we have:

同样,这不是我们所期望的。这里的SocketException告诉我们,服务器无法信任客户端。这是因为我们已经设置了一个双向的SSL。在我们的SimpleServer中,我们有。

((SSLServerSocket) listener).setNeedClientAuth(true);

The above code indicates an SSLServerSocket is required for client authentication through their public certificate.

上述代码表明,客户通过其公共证书进行认证时需要一个SSLServerSocket

We can create a keystore for the client and a corresponding truststore for the server in a way similar to the one that we used when creating the previous keystore and truststore.

我们可以为客户端创建一个密钥库,为服务器创建一个相应的信任库,其方式与我们创建前一个密钥库和信任库时所用的方式类似。

We will restart the server and pass it the following system properties:

我们将重新启动服务器,并将以下系统属性传递给它。

-Djavax.net.ssl.keyStore=serverkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=clienttruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

Then, we will restart the client by passing these system properties:

然后,我们将通过这些系统属性来重新启动客户端。

-Djavax.net.ssl.keyStore=serverkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=clienttruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

Finally, we have the output we desired:

最后,我们得到了我们想要的输出。

Hello World!

5.4. Incorrect Certificates

5.4.不正确的证书

Apart from the above errors, a handshake can fail due to a variety of reasons related to how we have created the certificates. One common error is related to an incorrect CN. Let’s explore the details of the server keystore we created previously:

除了上述错误外,握手可能由于各种原因而失败,这些原因与我们创建证书的方式有关。一个常见的错误是与不正确的CN有关。让我们探讨一下我们之前创建的服务器密钥库的细节。

keytool -v -list -keystore serverkeystore.jks

When we run the above command, we can see the details of the keystore, specifically the owner:

当我们运行上述命令时,我们可以看到钥匙库的细节,特别是所有者。

...
Owner: CN=localhost, OU=technology, O=baeldung, L=city, ST=state, C=xx
...

The CN of the owner of this certificate is set to localhost. The CN of the owner must exactly match the host of the server. If there is any mismatch it will result in an SSLHandshakeException.

该证书所有者的CN被设置为localhost。所有者的CN必须与服务器的主机完全匹配。如果有任何不匹配,将导致SSLHandshakeException

Let’s try to regenerate the server certificate with CN as anything other than localhost. When we use the regenerated certificate now to run the SimpleServer and SimpleClient it promptly fails:

让我们尝试重新生成服务器证书,并将 CN 设为 localhost 以外的其他名称。当我们现在使用重新生成的证书来运行SimpleServerSimpleClient时,就会立即失败。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
    java.security.cert.CertificateException: 
    No name matching localhost found

The exception trace above clearly indicates that the client was expecting a certificate bearing the name as localhost which it did not find.

上面的异常跟踪清楚地表明,客户正在期待一个名称为localhost的证书,但它没有找到。

Please note that JSSE does not mandate hostname verification by default. We have enabled hostname verification in the SimpleClient through explicit use of HTTPS:

请注意,JSSE默认情况下不强制要求进行主机名验证。我们通过明确使用HTTPS在SimpleClient中启用了主机名验证。

SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
((SSLSocket) connection).setSSLParameters(sslParams);

Hostname verification is a common cause of failure and in general and should always be enforced for better security. For details on hostname verification and its importance in security with TLS, please refer to this article.

主机名验证是一个常见的失败原因,一般来说,为了更好的安全,应该始终强制执行。关于主机名验证的细节及其在TLS安全方面的重要性,请参考这篇文章

5.5. Incompatible SSL Version

5.5.不兼容的SSL版本

Currently, there are various cryptographic protocols including different versions of SSL and TLS in operation.

目前,有各种加密协议,包括不同版本的SSL和TLS在运行。

As mentioned earlier, SSL, in general, has been superseded by TLS for its cryptographic strength. The cryptographic protocol and version are an additional element that a client and a server must agree on during a handshake.

如前所述,一般来说,SSL的加密强度已经被TLS所取代。加密协议和版本是客户端和服务器在握手过程中必须同意的一个额外元素。

For example, if the server uses a cryptographic protocol of SSL3 and the client uses TLS1.3 they cannot agree on a cryptographic protocol and an SSLHandshakeException will be generated.

例如,如果服务器使用SSL3的加密协议,而客户端使用TLS1.3,他们无法就加密协议达成一致,将产生一个SSLHandshakeException

In our SimpleClient let’s change the protocol to something that is not compatible with the protocol set for the server:

在我们的SimpleClient中,让我们把协议改为与服务器的协议设置不兼容的内容。

((SSLSocket) connection).setEnabledProtocols(new String[] { "TLSv1.1" });

When we run our client again, we will get an SSLHandshakeException:

当我们再次运行我们的客户端时,我们将得到一个SSLHandshakeException

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

The exception trace in such cases is abstract and does not tell us the exact problem. To resolve these types of problems it is necessary to verify that both the client and server are using either the same or compatible cryptographic protocols.

这种情况下的异常跟踪是抽象的,不能告诉我们确切的问题。为了解决这些类型的问题,有必要验证客户端和服务器是否使用相同或兼容的加密协议。

5.6. Incompatible Cipher Suite

5.6.不兼容的密码套件

The client and server must also agree on the cipher suite they will use to encrypt messages.

客户端和服务器还必须就他们将用于加密信息的密码套件达成一致。

During a handshake, the client will present a list of possible ciphers to use and the server will respond with a selected cipher from the list. The server will generate an SSLHandshakeException if it cannot select a suitable cipher.

在握手过程中,客户端将提出一个可能使用的密码列表,而服务器将从列表中选择一个密码作为回应。如果服务器不能选择一个合适的密码,它将产生一个SSLHandshakeException

In our SimpleClient let’s change the cipher suite to something that is not compatible with the cipher suite used by our server:

在我们的SimpleClient中,让我们把密码套件改为与我们服务器使用的密码套件不兼容的东西。

((SSLSocket) connection).setEnabledCipherSuites(
  new String[] { "TLS_RSA_WITH_AES_128_GCM_SHA256" });

When we restart our client we will get an SSLHandshakeException:

当我们重新启动我们的客户端时,我们将得到一个SSLHandshakeException

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  Received fatal alert: handshake_failure

Again, the exception trace is quite abstract and does not tell us the exact problem. The resolution to such an error is to verify the enabled cipher suites used by both the client and server and ensure that there is at least one common suite available.

同样,异常跟踪是相当抽象的,并没有告诉我们确切的问题。解决这种错误的方法是验证客户端和服务器所使用的启用的密码套件,并确保至少有一个可用的通用套件。

Normally, clients and servers are configured to use a wide variety of cipher suites so this error is less likely to happen. If we encounter this error it is typically because the server has been configured to use a very selective cipher. A server may choose to enforce a selective set of ciphers for security reasons.

通常情况下,客户端和服务器被配置为使用各种密码套件,因此这种错误不太可能发生。如果我们遇到这个错误,通常是因为服务器被配置为使用非常有选择性的密码。服务器可能出于安全原因选择执行一套有选择性的密码。

6. Conclusion

6.结论

In this tutorial, we learned about setting up SSL using Java sockets. Then we discussed SSL handshakes with one-way and two-way SSL. Finally, we went through a list of possible reasons that SSL handshakes may fail and discussed the solutions.

在本教程中,我们学习了如何使用Java套接字设置SSL。然后我们讨论了单向和双向SSL的握手。最后,我们列举了SSL握手可能失败的原因并讨论了解决方案。

As always, the code for the examples is available over on GitHub.

像往常一样,这些例子的代码可以在GitHub上找到over