Неуспехи при SSL ръкостискане

Java Top

Току що обявих новия курс Learn Spring , фокусиран върху основите на Spring 5 и Spring Boot 2:

>> ПРЕГЛЕД НА КУРСА

1. Общ преглед

Secure Socket Layer (SSL) е криптографски протокол, който осигурява сигурност в комуникацията по мрежата. В този урок ще обсъдим различни сценарии, които могат да доведат до неуспех на SSL ръкостискането и как да го направим.

Имайте предвид, че нашето Въведение в SSL с помощта на JSSE обхваща по-подробно основите на SSL.

2. Терминология

Важно е да се отбележи, че поради уязвимости в сигурността, SSL като стандарт е заменен от Transport Layer Security (TLS). Повечето езици за програмиране, включително Java, имат библиотеки, които поддържат SSL и TLS.

От създаването на SSL, много продукти и езици като OpenSSL и Java имат препратки към SSL, които те запазват дори след като TLS пое. Поради тази причина в останалата част от този урок ще използваме термина SSL, за да се позоваваме най-общо на криптографски протоколи.

3. Настройка

За целите на този урок ще създадем прости сървърни и клиентски приложения, използващи Java Socket API, за да симулираме мрежова връзка.

3.1. Създаване на клиент и сървър

В Java можем да използваме s ockets, за да установим комуникационен канал между сървър и клиент по мрежата . Сокетите са част от Java Secure Socket Extension (JSSE) в Java.

Нека започнем с дефиниране на прост сървър:

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

Определеният по-горе сървър връща съобщението „Hello World!“ към свързан клиент.

След това нека дефинираме основен клиент, който ще свържем с нашия 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(); }

Нашият клиент отпечатва съобщението, върнато от сървъра.

3.2. Създаване на сертификати в Java

SSL осигурява секретност, цялост и автентичност в мрежовите комуникации. Сертификатите играят важна роля, що се отнася до установяването на автентичността.

Обикновено тези сертификати се закупуват и подписват от сертифициращ орган, но за този урок ще използваме самоподписани сертификати.

За да постигнем това, можем да използваме keytool, който се доставя с JDK:

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

Горната команда стартира интерактивна обвивка за събиране на информация за сертификата като Общо име (CN) и Отличително име (DN). Когато предоставяме всички подходящи подробности, той генерира файла serverkeystore.jks , който съдържа частния ключ на сървъра и неговия публичен сертификат.

Имайте предвид, че serverkeystore.jks се съхранява във формата на Java Key Store (JKS), който е собственост на Java. Тези дни keytool ще ни напомни, че трябва да обмислим използването на PKCS # 12, който той също поддържа.

По-нататък можем да използваме keytool за извличане на публичния сертификат от генерирания файл на хранилището на ключове:

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

Горната команда експортира публичния сертификат от хранилището на ключове като файлов сървър.cer . Нека използваме експортирания сертификат за клиента, като го добавим към неговия магазин за доверие:

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

Сега сме генерирали хранилище на ключове за сървъра и съответно хранилище за доверие за клиента. Ще разгледаме използването на тези генерирани файлове, когато обсъдим възможни неуспехи при ръкостискането.

И повече подробности около използването на хранилището на ключове на Java можете да намерите в предишния ни урок.

4. SSL ръкостискане

SSL ръкостисканията са механизъм, чрез който клиент и сървър установяват доверието и логистиката, необходими за осигуряване на връзката им по мрежата .

Това е много организирана процедура и разбирането на детайлите на това може да помогне да се разбере защо често се проваля, което възнамеряваме да разгледаме в следващия раздел.

Типичните стъпки в SSL ръкостискането са:

  1. Клиентът предоставя списък с възможните SSL версия и пакети за шифроване, които да използва
  2. Сървърът се съгласява за конкретна SSL версия и набор от шифри, отговаряйки със своя сертификат
  3. Клиентът извлича публичния ключ от сертификата, отговаря в отговор с криптиран „пред-главен ключ“
  4. Сървърът дешифрира „предварителния ключ“, използвайки своя частен ключ
  5. Клиентът и сървърът изчисляват „споделена тайна“, като използват обменния „ключ преди мастер“
  6. Съобщения за обмен на клиенти и сървъри, потвърждаващи успешното криптиране и дешифриране с помощта на „споделената тайна“

Докато повечето стъпки са еднакви за всяко SSL ръкостискане, има фина разлика между еднопосочния и двупосочния SSL. Нека да прегледаме бързо тези разлики.

4.1. Ръкостискането в еднопосочен SSL

Ако се позовем на стъпките, споменати по-горе, стъпка втора споменава обмена на сертификати. Еднопосочният SSL изисква клиентът да може да се довери на сървъра чрез неговия публичен сертификат. Това оставя сървъра да се доверява на всички клиенти, които поискат връзка. Няма начин сървърът да поиска и потвърди публичния сертификат от клиенти, което може да представлява риск за сигурността.

4.2. Ръкостискането в двупосочен SSL

С еднопосочен SSL сървърът трябва да се доверява на всички клиенти. Но двупосочният SSL добавя способността на сървъра да може да установява и доверени клиенти. По време на двупосочно ръкостискане, клиентът и сървърът трябва да представят и приемат публичните сертификати един на друг, преди да може да се установи успешна връзка.

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.

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

В този урок научихме за настройването на SSL с помощта на Java сокети. След това обсъдихме SSL ръкостискания с еднопосочен и двупосочен SSL. Накрая разгледахме списък с възможни причини, поради които SSL ръкостисканията може да се провалят, и обсъдихме решенията.

Както винаги, кодът за примерите е достъпен в GitHub.

Дъно на Java

Току що обявих новия курс Learn Spring , фокусиран върху основите на Spring 5 и Spring Boot 2:

>> ПРЕГЛЕД НА КУРСА