Eșecuri de strângere de mână SSL

Java Top

Tocmai am anunțat noul curs Learn Spring , axat pe fundamentele Spring 5 și Spring Boot 2:

>> VERIFICAȚI CURSUL

1. Prezentare generală

Secured Socket Layer (SSL) este un protocol criptografic care oferă securitate în comunicarea prin rețea. În acest tutorial, vom discuta despre diverse scenarii care pot duce la o eșec de strângere de mână SSL și cum se face.

Rețineți că Introducerea noastră în SSL utilizând JSSE acoperă elementele de bază ale SSL în detaliu.

2. Terminologie

Este important să rețineți că, din cauza vulnerabilităților de securitate, SSL ca standard este înlocuit de Transport Layer Security (TLS). Majoritatea limbajelor de programare, inclusiv Java, au biblioteci care acceptă atât SSL, cât și TLS.

De la înființarea SSL, multe produse și limbaje precum OpenSSL și Java au avut referințe la SSL pe care le-au păstrat chiar și după ce TLS a preluat. Din acest motiv, în restul acestui tutorial, vom folosi termenul SSL pentru a ne referi în general la protocoale criptografice.

3. Configurare

În scopul acestui tutorial, vom crea un server simplu și aplicații client folosind Java Socket API pentru a simula o conexiune de rețea.

3.1. Crearea unui client și a unui server

În Java, putem folosi s ockets pentru a stabili un canal de comunicare între un server și client prin rețea . Soclurile fac parte din Java Secure Socket Extension (JSSE) în Java.

Să începem prin definirea unui server simplu:

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

Serverul definit mai sus returnează mesajul „Hello World!” către un client conectat.

În continuare, să definim un client de bază, pe care îl vom conecta la 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(); }

Clientul nostru tipărește mesajul returnat de server.

3.2. Crearea certificatelor în Java

SSL oferă secret, integritate și autenticitate în comunicațiile de rețea. Certificatele joacă un rol important în ceea ce privește stabilirea autenticității.

De obicei, aceste certificate sunt achiziționate și semnate de o autoritate de certificare, dar pentru acest tutorial vom folosi certificate auto-semnate.

Pentru a realiza acest lucru, putem folosi keytool, care este livrat împreună cu JDK:

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

Comanda de mai sus pornește un shell interactiv pentru a aduna informații pentru certificat precum Common Name (CN) și Distinguished Name (DN). Când furnizăm toate detaliile relevante, acesta generează fișierul serverkeystore.jks , care conține cheia privată a serverului și certificatul său public.

Rețineți că serverkeystore.jks este stocat în formatul Java Key Store (JKS), care este proprietatea Java. În aceste zile, keytool ne va reaminti că ar trebui să luăm în considerare utilizarea PKCS # 12, pe care o acceptă și ea.

Putem utiliza în continuare keytool pentru a extrage certificatul public din fișierul generat de keystore:

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

Comanda de mai sus exportă certificatul public din keystore ca fișier server.cer . Să folosim certificatul exportat pentru client adăugându-l la magazinul său de încredere:

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

Acum am generat un depozit de chei pentru server și un depozit de încredere corespunzător pentru client. Vom trece peste utilizarea acestor fișiere generate atunci când vom discuta despre posibilele eșecuri ale strângerii de mână.

Și mai multe detalii despre utilizarea depozitului de chei Java pot fi găsite în tutorialul nostru anterior.

4. Strângere de mână SSL

Strângerile de mână SSL sunt un mecanism prin care un client și un server stabilesc încrederea și logistica necesare pentru a-și asigura conexiunea prin rețea .

Aceasta este o procedură foarte orchestrată și înțelegerea detaliilor acestui lucru vă poate ajuta să înțelegeți de ce eșuează adesea, pe care intenționăm să o acoperim în secțiunea următoare.

Pașii tipici ai unei strângeri de mână SSL sunt:

  1. Clientul oferă o listă a versiunilor SSL posibile și a suitelor de cifrare de utilizat
  2. Serverul este de acord cu o anumită versiune SSL și o suită de cifrare, răspunzând cu certificatul său
  3. Clientul extrage cheia publică din certificat și răspunde cu o „cheie pre-master” criptată
  4. Serverul decriptează „cheia pre-master” folosind cheia sa privată
  5. Clientul și serverul calculează un „secret partajat” folosind „cheia pre-master” schimbată
  6. Client și server schimbă mesaje care confirmă criptarea și decriptarea cu succes folosind „secretul partajat”

Deși majoritatea pașilor sunt aceiași pentru orice strângere de mână SSL, există o diferență subtilă între SSL unidirecțional și bidirecțional. Să analizăm rapid aceste diferențe.

4.1. Strângerea de mână în SSL unidirecțional

Dacă ne referim la pașii menționați mai sus, la pasul doi se menționează schimbul de certificate. SSL unidirecțional necesită ca un client să poată avea încredere în server prin certificatul său public. Acest lucru lasă serverul să aibă încredere în toți clienții care solicită o conexiune. Nu există nicio modalitate pentru ca un server să solicite și să valideze certificatul public de la clienți care poate prezenta un risc de securitate.

4.2. Strângerea de mână în SSL bidirecțional

Cu SSL unidirecțional, serverul trebuie să aibă încredere în toți clienții. Dar, SSL bidirecțional adaugă posibilitatea ca serverul să poată stabili și clienți de încredere. În timpul unei strângeri de mână în două direcții, atât clientul , cât și serverul trebuie să prezinte și să accepte reciproc certificatele publice înainte de a putea stabili o conexiune de succes.

5. Scenarii de eșec al strângerii de mână

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.

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

5.1. Missing Server Certificate

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:

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.

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

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

5.2. Untrusted Server Certificate

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

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

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.

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

În acest tutorial, am aflat despre configurarea SSL utilizând soclurile Java. Apoi am discutat strângerile de mână SSL cu SSL unidirecțional și bidirecțional. În cele din urmă, am parcurs o listă cu posibilele motive pentru care strângerile de mână SSL pot eșua și am discutat soluțiile.

Ca întotdeauna, codul pentru exemple este disponibil pe GitHub.

Java de jos

Tocmai am anunțat noul curs Learn Spring , axat pe fundamentele Spring 5 și Spring Boot 2:

>> VERIFICAȚI CURSUL