SSL-Handshake-Fehler

Java Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

Secured Socket Layer (SSL) ist ein kryptografisches Protokoll, das Sicherheit bei der Kommunikation über das Netzwerk bietet. In diesem Tutorial werden verschiedene Szenarien erläutert, die zu einem SSL-Handshake-Fehler führen können.

Beachten Sie, dass unsere Einführung in SSL mit JSSE die Grundlagen von SSL ausführlicher behandelt.

2. Terminologie

Es ist wichtig zu beachten, dass SSL als Standard aufgrund von Sicherheitslücken durch TLS (Transport Layer Security) ersetzt wird. Die meisten Programmiersprachen, einschließlich Java, verfügen über Bibliotheken, die sowohl SSL als auch TLS unterstützen.

Seit der Einführung von SSL hatten viele Produkte und Sprachen wie OpenSSL und Java Verweise auf SSL, die sie auch nach der Übernahme von TLS beibehalten haben. Aus diesem Grund verwenden wir im weiteren Verlauf dieses Lernprogramms den Begriff SSL, um allgemein auf kryptografische Protokolle Bezug zu nehmen.

3. Setup

In diesem Lernprogramm erstellen wir einfache Server- und Clientanwendungen mithilfe der Java Socket-API, um eine Netzwerkverbindung zu simulieren.

3.1. Erstellen eines Clients und eines Servers

In Java können wir verwenden s ockets zu etablieren , einen Kommunikationskanal zwischen einem Server und Client über das Netzwerk . Sockets sind Teil der Java Secure Socket Extension (JSSE) in Java.

Beginnen wir mit der Definition eines einfachen Servers:

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!"); } } }

Der oben definierte Server gibt die Meldung "Hallo Welt!" Zurück. zu einem verbundenen Client.

Als nächstes definieren wir einen Basis-Client, den wir mit unserem SimpleServer verbinden:

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(); }

Unser Client druckt die vom Server zurückgegebene Nachricht.

3.2. Erstellen von Zertifikaten in Java

SSL bietet Geheimhaltung, Integrität und Authentizität bei der Netzwerkkommunikation. Zertifikate spielen eine wichtige Rolle bei der Feststellung der Authentizität.

Normalerweise werden diese Zertifikate von einer Zertifizierungsstelle gekauft und signiert. In diesem Lernprogramm werden jedoch selbstsignierte Zertifikate verwendet.

Um dies zu erreichen, können wir Keytool verwenden, das mit dem JDK geliefert wird :

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

Der obige Befehl startet eine interaktive Shell, um Informationen für das Zertifikat wie Common Name (CN) und Distinguished Name (DN) zu sammeln. Wenn wir alle relevanten Details angeben , wird die Datei serverkeystore.jks generiert , die den privaten Schlüssel des Servers und sein öffentliches Zertifikat enthält.

Beachten Sie, dass serverkeystore.jks im Java Key Store (JKS) -Format gespeichert ist, das für Java proprietär ist. In diesen Tagen, keytool wird uns daran erinnern , dass wir sollten PKCS prüfen , mit # 12, die es auch unterstützt.

Wir können Keytool weiter verwenden , um das öffentliche Zertifikat aus der generierten Keystore-Datei zu extrahieren:

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

Der obige Befehl exportiert das öffentliche Zertifikat aus dem Keystore als Datei server.cer . Verwenden wir das exportierte Zertifikat für den Client, indem wir es seinem Truststore hinzufügen:

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

Wir haben jetzt einen Keystore für den Server und einen entsprechenden Truststore für den Client generiert. Wir werden die Verwendung dieser generierten Dateien untersuchen, wenn wir mögliche Handshake-Fehler diskutieren.

Weitere Informationen zur Verwendung des Java-Keystores finden Sie in unserem vorherigen Tutorial.

4. SSL-Handshake

SSL-Handshakes sind ein Mechanismus, mit dem Client und Server das Vertrauen und die Logistik herstellen, die zum Sichern ihrer Verbindung über das Netzwerk erforderlich sind .

Dies ist eine sehr orchestrierte Prozedur, und das Verständnis der Details kann helfen, zu verstehen, warum sie häufig fehlschlägt, worauf wir im nächsten Abschnitt eingehen wollen.

Typische Schritte bei einem SSL-Handshake sind:

  1. Der Client bietet eine Liste möglicher SSL-Versionen und zu verwendender Cipher Suites
  2. Der Server stimmt einer bestimmten SSL-Version und Verschlüsselungssuite zu und antwortet mit seinem Zertifikat
  3. Der Client extrahiert den öffentlichen Schlüssel aus dem Zertifikat und antwortet mit einem verschlüsselten „Pre-Master-Schlüssel“.
  4. Der Server entschlüsselt den „Pre-Master-Schlüssel“ mit seinem privaten Schlüssel
  5. Client und Server berechnen ein "gemeinsames Geheimnis" unter Verwendung des ausgetauschten "Pre-Master-Schlüssels".
  6. Client- und Server-Austauschnachrichten bestätigen die erfolgreiche Ver- und Entschlüsselung mithilfe des „gemeinsamen Geheimnisses“.

Während die meisten Schritte für jeden SSL-Handshake gleich sind, gibt es einen subtilen Unterschied zwischen Einweg- und Zweiweg-SSL. Lassen Sie uns diese Unterschiede schnell überprüfen.

4.1. Der Handshake in One-Way-SSL

Wenn wir uns auf die oben genannten Schritte beziehen, wird in Schritt 2 der Zertifikatsaustausch erwähnt. Einweg-SSL erfordert, dass ein Client dem Server über sein öffentliches Zertifikat vertrauen kann. Dadurch kann der Server allen Clients vertrauen , die eine Verbindung anfordern. Es gibt keine Möglichkeit für einen Server, das öffentliche Zertifikat von Clients anzufordern und zu validieren, was ein Sicherheitsrisiko darstellen kann.

4.2. Der Handshake in Zwei-Wege-SSL

Bei Einweg-SSL muss der Server allen Clients vertrauen. Durch bidirektionales SSL kann der Server jedoch auch vertrauenswürdige Clients einrichten. Während eines bidirektionalen Handshakes müssen sowohl der Client als auch der Server die öffentlichen Zertifikate des jeweils anderen vorlegen und akzeptieren, bevor eine erfolgreiche Verbindung hergestellt werden kann.

5. Handshake-Fehlerszenarien

Nach dieser kurzen Überprüfung können wir Fehlerszenarien klarer betrachten.

Ein SSL-Handshake bei Einweg- oder Zweiwegkommunikation kann aus mehreren Gründen fehlschlagen. Wir werden jeden dieser Gründe durchgehen, den Fehler simulieren und verstehen, wie wir solche Szenarien vermeiden können.

In jedem dieser Szenarien verwenden wir den zuvor erstellten SimpleClient und SimpleServer .

5.1. Fehlendes Serverzertifikat

Versuchen wir, den SimpleServer auszuführen und über den SimpleClient zu verbinden . Während wir die Meldung "Hallo Welt!" Erwarten, wird uns eine Ausnahme angezeigt:

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

Dies weist darauf hin, dass ein Fehler aufgetreten ist. Die obige SSLHandshakeException besagt abstrakt, dass der Client beim Herstellen einer Verbindung zum Server kein Zertifikat erhalten hat.

Um dieses Problem zu beheben, verwenden wir den zuvor generierten Schlüsselspeicher, indem wir ihn als Systemeigenschaften an den Server übergeben:

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

Es ist wichtig zu beachten, dass die Systemeigenschaft für den Pfad der Keystore-Datei entweder ein absoluter Pfad sein sollte oder dass die Keystore-Datei in demselben Verzeichnis abgelegt werden sollte, von dem aus der Java-Befehl aufgerufen wird, um den Server zu starten. Die Java-Systemeigenschaft für den Keystore unterstützt keine relativen Pfade.

Hilft uns dies, die erwartete Leistung zu erzielen? Lassen Sie es uns im nächsten Unterabschnitt herausfinden.

5.2. Nicht vertrauenswürdiges Serverzertifikat

Was erhalten wir als Ausgabe, wenn wir den SimpleServer und den SimpleClient mit den Änderungen im vorherigen Unterabschnitt erneut ausführen :

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

Nun, es hat nicht genau so funktioniert, wie wir es erwartet hatten, aber es sieht so aus, als wäre es aus einem anderen Grund gescheitert.

Dieser besondere Fehler wird durch die Tatsache verursacht, dass unser Server ein selbstsigniertes Zertifikat verwendet, das nicht von einer Zertifizierungsstelle (CA) signiert ist.

Wirklich, jedes Mal, wenn das Zertifikat von etwas anderem als dem im Standard-Truststore signierten signiert wird, wird dieser Fehler angezeigt. Der Standard-Truststore in JDK enthält normalerweise Informationen zu häufig verwendeten allgemeinen Zertifizierungsstellen.

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:

-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.

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

5.3. Missing Client Certificate

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

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:

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

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

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=servertruststore.jks \ -Djavax.net.ssl.trustStorePassword=password

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

-Djavax.net.ssl.keyStore=clientkeystore.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

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:

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.

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:

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.

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

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.

5.5. Incompatible SSL Version

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

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.

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.

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

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

When we run our client again, we will get an 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

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.

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

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

When we restart our client we will get an 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

In diesem Tutorial haben wir gelernt, wie Sie SSL mithilfe von Java-Sockets einrichten. Dann diskutierten wir SSL-Handshakes mit Einweg- und Zweiweg-SSL. Schließlich haben wir eine Liste möglicher Gründe für das Fehlschlagen von SSL-Handshakes durchgesehen und die Lösungen erörtert.

Wie immer ist der Code für die Beispiele auf GitHub verfügbar.

Java unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs